5 Commits
Author SHA1 Message Date
NUEEX 0eab5f7f6c Consolidate licensing docs and remove personal build paths
Move essential provenance, contributor and distribution requirements into
LICENSING.md; remove the review/validation reports and fix references and
scope entries. Preserve the exact historical MIT notice, third-party credits
and unresolved libjbc permissions.

Use a portable resource label and generic profile-path checks instead of a
personal Windows path and developer identifier. Production output remains
byte-identical; path-scanner cases and the 150-file licensing inventory pass.
2026-10-26 17:05:32 +00:00
NUEEX c00f689c49 License nueex's original work under GPL-3.0-or-later
Define explicit original, mixed and excluded file scope, credit nueex, and
preserve historical MIT grants and third-party notices. Keep libjbc's
redistribution permission unresolved and exclude third-party fonts, branding
and imagery from the original-work grant.

Retain the latest upstream build fixes and removal of obsolete tools.
Application/build edits are licensing comments only; the production ELF and
all 35 object files match the baseline byte for byte. No binaries or release
artifacts are included.
2026-10-26 16:47:23 +00:00
NUEEX 164e3fb007 Clean unused development tools and document reproducible source builds
Remove 17 unused preview, test, legacy delivery, resource-extraction and font files whose removal does not affect application builds. Retain production build, inspection, runtime and package validation tools and editable artwork.

Add BUILD.md, correct source availability in README, support external SDKs in CMake, make the Make wrapper default to production, and fix compiler path / LLD discovery and nested output directories.

Validation: fresh GitHub checkout; pinned SDK archive verified and freshly extracted; Windows production PKG build, validation and independent extraction; diagnostic build; CMake portable library build. Production ELF, OELF, eboot.bin and every staged runtime/metadata payload match the pre-cleanup GitHub build byte-for-byte. No application source, headers or libjbc integration changed.
2026-10-24 17:07:32 +01:00
NUEEX a15ef5d9d0 docs: remove source code availability question 2026-10-24 16:35:18 +01:00
NUEEX 4b051d6fae v 1.01 2026-10-24 15:24:31 +00:00
148 changed files with 45517 additions and 52 deletions

No files matched your search

+1
View File
@@ -0,0 +1 @@
* text=auto eol=lf
+21
View File
@@ -0,0 +1,21 @@
.research/
.tools/
build/
build-*/
*.o
*.elf
*.self
*.pkg
__pycache__/
artifacts/
*.oelf
*.log
tests/
docs/
.github/*
!.github/FUNDING.yml
distribution/*
!distribution/LICENSES.txt
DISCOVERABILITY.md
tools/build_host.py
tools/Collect-PS4LaunchLog.ps1
+138
View File
@@ -0,0 +1,138 @@
# Build PS MTP Explorer
The repository includes the source, libjbc integration, fonts, and artwork
for version **1.01**, with title ID `NUEX00001` and PS4 SFO version `01.01`.
External compiler and SDK binaries are installed separately.
## Requirements
- **Python 3.12 or later**, available as `python` (or `python3` on Unix).
- **OpenOrbis PS4 Toolchain v0.5.4**, including its headers, libraries,
packaging tools, and `samples/hello_world` runtime/about modules.
- **Clang and LLD 18.1.8**, the release compiler/linker. Install LLVM
separately; the SDK bootstrap below does not install LLVM.
- For **Windows packaging**, an x64 .NET runtime. The SDK's PkgTool targets
.NET Core 3.0; the build enables major-version roll-forward and has been
verified with .NET 9. Check with `dotnet --list-runtimes`.
Downloads: [Python](https://www.python.org/downloads/),
[OpenOrbis v0.5.4](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/releases/tag/v0.5.4),
[LLVM 18.1.8](https://github.com/llvm/llvm-project/releases/tag/llvmorg-18.1.8),
and [.NET](https://dotnet.microsoft.com/download/dotnet).
This build audit verified Windows. Linux/macOS builds need working versions
of the SDK's platform-specific conversion/packaging tools and their runtime
dependencies; they were not executed in this audit.
No proprietary Sony SDK, Node.js, Pillow, test fixtures, sibling resource
folders, or previous build output is required to compile the application.
## Clone and install the SDK
```sh
git clone https://github.com/nueex/ps-mtp-explorer.git
cd ps-mtp-explorer
python tools/bootstrap_openorbis.py
```
The script downloads the official SDK archive, checks its pinned SHA-256,
and extracts it under `.tools/sdk/OpenOrbis/PS4Toolchain`. Network access
is required for the download; builds can then use the local SDK offline.
Downloaded SDK/compiler binaries and build outputs are excluded from Git.
Alternatively, use an existing SDK by setting `OO_PS4_TOOLCHAIN` or passing
`--sdk "/path/to/PS4Toolchain"`. Keep the SDK's sample runtime modules:
packaging uses its open-source dummy libc/FIOS2 modules and about module.
## Compile and package
From the repository root, with `clang` and `ld.lld` on PATH:
```sh
python tools/build_ps4.py --production --package --cc clang --ld ld.lld --output build-ps4
python tools/inspect_ps4.py build-ps4
python tools/verify_production_release.py build-ps4
```
Explicit paths are also supported, including paths containing spaces:
```sh
python tools/build_ps4.py --production --package --sdk "/path/to/PS4Toolchain" --cc "/path/to/clang" --ld "/path/to/ld.lld" --output build-ps4
python tools/inspect_ps4.py build-ps4 --sdk "/path/to/PS4Toolchain"
python tools/verify_production_release.py build-ps4
```
On Windows use `.exe` executable names; for example PowerShell accepts
`--cc "C:/Program Files/LLVM/bin/clang.exe"` and
`--ld "C:/Program Files/LLVM/bin/ld.lld.exe"`.
Without `--cc`, the build detects `.tools/llvm18/bin/clang.exe`, then a local
`.tools/ziglang/zig[.exe]`, then `clang` on PATH. Pass `--ld` explicitly
when selecting a particular linker. Otherwise the build detects LLD beside
the local LLVM installation or as `ld.lld` on PATH.
The installable file is:
```text
build-ps4/IV0000-NUEX00001_00-NUEEXMTP00000000.pkg
```
The output directory also contains the ELF, OELF, `package/eboot.bin`, an
independently extracted PKG payload, and JSON build/inspection/audit reports.
Packaging checks hashes/signatures, extracts the PKG independently, and
compares its payload and identifiers with the staged inputs. The final audit
checks production metadata and the absence of debug/test payloads and private
paths. Run the inspection command before the production audit.
Omit `--package` to compile/link only. Use `--experimental-usb` instead of
`--production` for the diagnostic build with logging. Production builds
disable diagnostic logging. The scripts do not deploy to a console; install
the resulting PKG manually using GoldHEN.
GNU Make is an optional wrapper (`PYTHON` defaults to `python3`):
```sh
make -f Makefile.ps4 PYTHON=python package
```
Set `OO_PS4_TOOLCHAIN` for an external SDK. Add `EXPERIMENTAL_USB=1` for a
diagnostic build. The Python commands above do not require Make.
## Optional host library build
CMake builds the portable core as a static library, not a desktop app or
PS4 package. Install the SDK first for its stb headers, plus CMake 3.16+,
a C99 compiler, and a supported build tool. With Clang and Ninja on PATH:
```sh
cmake -S . -B build-host -G Ninja -DCMAKE_C_COMPILER=clang
cmake --build build-host
```
For an external SDK, add `-DOO_PS4_TOOLCHAIN="/path/to/PS4Toolchain"` during
configuration, or set the `OO_PS4_TOOLCHAIN` environment variable.
Test harnesses are not required by this target.
## Assets and notices
**Original Author: nueex — Copyright (C) 2026 nueex.** Original project work is
GPL-3.0-or-later under [LICENSING.md](LICENSING.md) and [LICENSE_SCOPE.json](LICENSE_SCOPE.json).
Historical MIT grants and all third-party exclusions remain. Successful builds
do not establish redistribution permission for inherited libjbc code or
excluded artwork/branding; the complete application remains uncleared.
Normal builds regenerate the embedded font from the bundled, hash-checked
`assets/fonts/NotoSans-Light.ttf`. Artwork is already embedded in the
committed `src/ps4/reference_assets.inc`; no asset-generation step is needed.
Editable SVGs and PNGs plus provenance remain in `assets/`.
For optional artwork editing, `tools/prepare_clean_icons.mjs` exports
`prepareCleanIcons({sharp})`. It rasterizes the SVGs and rebuilds the embedded
artwork from the bundled PNGs. This editing workflow requires Node.js and
the `sharp` package; those dependencies are unnecessary for application builds.
Retain [THIRD_PARTY_LICENSES.md](THIRD_PARTY_LICENSES.md),
`distribution/LICENSES.txt`, and the notices under `assets/` and
`third_party/libjbc/` when redistributing applicable materials.
Successful compilation and package checks do not establish compatibility
with every firmware, GoldHEN version, phone, or USB device. Console behavior
still requires testing on the intended hardware.
+30
View File
@@ -0,0 +1,30 @@
# PS MTP Explorer
# Original Author: nueex
# Copyright (C) 2026 nueex
# SPDX-License-Identifier: GPL-3.0-or-later
# GNU GPL version 3 or later; see LICENSE and LICENSING.md.
# WITHOUT ANY WARRANTY; see LICENSE.
# Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
cmake_minimum_required(VERSION 3.16)
project(ps_mtp_explorer VERSION 1.01 LANGUAGES C)
set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_EXTENSIONS OFF)
add_library(psmtp src/pkg.c src/transfer_meter.c src/keyboard.c src/metadata.c src/name.c src/device_sound.c src/usb_label.c src/device_watch.c src/locations.c src/ui_format.c src/copy_plan.c src/copy.c src/sha256.c src/mtp.c src/usb_select.c src/navigator.c src/browser.c src/diagnostic.c src/diag_log.c src/usb_module.c)
target_include_directories(psmtp PUBLIC include)
if(DEFINED ENV{OO_PS4_TOOLCHAIN})
set(_default_sdk "$ENV{OO_PS4_TOOLCHAIN}")
else()
set(_default_sdk "${CMAKE_CURRENT_SOURCE_DIR}/.tools/sdk/OpenOrbis/PS4Toolchain")
endif()
set(OO_PS4_TOOLCHAIN "${_default_sdk}" CACHE PATH "OpenOrbis PS4 Toolchain directory")
find_path(PSMTP_STB_INCLUDE_DIR stb_image.h
PATHS "${OO_PS4_TOOLCHAIN}/include/stb" NO_DEFAULT_PATH)
if(NOT PSMTP_STB_INCLUDE_DIR)
message(FATAL_ERROR "Missing stb headers. Run python tools/bootstrap_openorbis.py or set OO_PS4_TOOLCHAIN to an installed SDK.")
endif()
target_include_directories(psmtp SYSTEM PRIVATE "${PSMTP_STB_INCLUDE_DIR}")
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(psmtp PRIVATE -Wall -Wextra -Wpedantic -Werror)
endif()
-24
View File
@@ -1,24 +0,0 @@
# Repository discovery metadata
**Repository:** `ps-mtp-explorer`
**Display title:** PS MTP Explorer — PS4 File Manager & Direct PKG Installation
**Description:** High-speed USB MTP file manager supporting direct PKG installation for jailbroken PS4 consoles by nueex.
## GitHub topics
`ps4` · `ps4-homebrew` · `mtp` · `file-manager` · `pkg-installer` · `phone-to-ps4` · `goldhen` · `ps4-tools` · `usb-transfer` · `pkg-installation`
## Search phrases
- PS4 file manager
- PS4 MTP transfer
- Android to PS4 USB file transfer
- Install PKG from phone to PS4
- Direct PS4 PKG installation
- PS4 internal storage file explorer
- PS4 USB file manager
- transfer files from PS4 to Android
- GoldHEN file manager
- PS MTP Explorer by nueex
These phrases describe implemented workflows. Topic selection and readable documentation improve discoverability; search position and AI recommendations are not guaranteed.
+66
View File
@@ -0,0 +1,66 @@
# Hardware Safety & Electrical Analysis
## Executive verdict
**PS MTP Explorer is electrically safe under normal operating conditions:** a healthy PS4 and phone, a compliant USB data cable, and adequate ventilation.
The app transfers files using standard USB Media Transfer Protocol (MTP) operations through Sony's USB APIs. The reviewed code contains no commands to raise USB voltage, disable electrical protections, or change cooling controls. The audit found **no credible mechanism by which normal use of the app would electrically damage the Southbridge or motherboard**.
This is a source-review conclusion, not a laboratory certification or a guarantee against faulty hardware. Damaged cables, shorts, liquid, defective accessories, and overheating remain real risks with any USB device. Electrical safety also does not guarantee software stability or protect against file loss.
## Myth vs. fact
### Myth 1: “A 33W, 67W, or 120W phone forces excessive power from the PS4.”
**Fact: A phone's fast-charging rating describes what it can accept from a compatible charger, not what it draws from every USB port.**
In a normal direct connection, the PS4 is the **source** supplying power and the phone is the **sink** receiving it. The phone's charging circuitry must follow the power available from the connection. It cannot make the PS4 generate a fast charger's higher voltage simply by requesting it. Fast charging requires compatible hardware and a supported charging protocol on both ends. [Qualcomm's charging FAQ](https://www.qualcomm.com/products/features/quick-charge/faq) explains this hardware dependency.
Ordinary USB data connections use nominal **5 V** power. Standard configured high-power budgets are **500 mA for USB 2.0** and **900 mA for single-lane SuperSpeed USB**. A USB 2.0 phone does not automatically get the SuperSpeed budget just because the port supports USB 3. These are USB protocol budgets, **not a measured PS4 current limit or an exact hardware cutoff**. Charging extensions can allow different currents; this audit did not establish their availability on every PS4 revision. See the [USB Type-C specification](https://www.usb.org/sites/default/files/USB%20Type-C%20Spec%20R2.0%20-%20August%202019.pdf).
Expect ordinary USB charging rather than the phone's advertised fast-charge rate. Actual charging speed varies; the battery may charge slowly or lose charge if the phone itself uses more power than it receives.
### Myth 2: “The Southbridge burns out because the phone's charging current passes through it.”
**Fact: The USB data path and charging-power path serve different jobs.**
| Path | What it carries |
| :--- | :--- |
| USB data lines → USB controller and associated I/O circuitry | Electrical signals representing file data |
| Console power supply → USB power circuitry → VBUS and ground wires → phone | Power used by the phone |
The Southbridge handles I/O; it is not the series power path that carries the phone's normal charging current. USB host designs use separate power distribution and overcurrent protection, such as power switches or resettable fuses. The [USB motherboard power-design guide](https://www.usb.org/sites/default/files/power_delivery_motherboards.pdf) explains these arrangements.
The exact protection parts and thresholds on every PS4 board revision were not verified. Separate power and data paths also do not make the console immune to faults: a damaged connector or cable can short power onto a data line and damage the USB circuitry.
### Myth 3: “The app overrides USB voltage or disables safety limits.”
**Fact: The reviewed implementation requests file transfers, not electrical changes.**
Its USB code discovers devices, reads descriptors, opens and claims an MTP interface, and exchanges control and bulk transfers. Its recovery reset targets the MTP interface; it is not a voltage change or a physical power cycle.
The review found no voltage-control commands, charging-protocol negotiation, power-role switching, fan-curve changes, or writes to motherboard power-control registers. These conclusions come from reviewing the USB adapter (`src/ps4/usb_ps4.c`), interface selection (`src/usb_select.c`), MTP implementation (`src/mtp.c`), and privileged-access code.
Homebrew can have elevated privileges, so this finding describes **what this code does**. It is not a claim that all privileged software is incapable of affecting hardware.
### Myth 4: “A 50 GB or larger transfer electrically overloads the port.”
**Fact: A larger file mainly means the same kind of USB activity continues for longer.**
USB data uses electrical signals with levels controlled by the USB hardware. Sending more bytes does not increase the signal voltage or request more charging voltage. File size alone does not establish an electrical overload.
Sustained transfers keep the USB circuitry, processor, storage, and phone active, producing normal operating heat, much like other sustained USB storage activity. The audit found no special electrical overload mechanism caused by large MTP transfers. Temperatures were not measured, so this does not promise a particular thermal margin on an aging, dusty, damaged, or poorly ventilated console.
## Practical tips
- Use a compliant, undamaged **USB data cable**. Replace cables with frayed insulation, loose plugs, or bent contacts.
- Keep the PS4's vents clean and unobstructed, with space for airflow.
- Avoid non-standard USB splitters, faulty OTG adapters, and Y-cables that connect a charger and the PS4 to the same power line. Poorly designed powered hubs can also feed power back toward the console.
- Do not keep using a port or cable that becomes unusually hot, smells burnt, or shows visible damage. Follow [Sony's PS4 safety guidance](https://www.playstation.com/content/dam/global_pdc/en/corporate/support/manuals/ps4-docs/2200/CUH-22XXA_PS4_Safety_Guide_Web%24en.pdf).
- Keep the phone connected until a transfer finishes to reduce the chance of incomplete files.
## What the audit establishes
The hardware review assessed the local source and USB power standards. It did not measure console current, voltage, temperature, protection-trip thresholds, or behavior under electrical faults, and it did not verify every PS4 motherboard revision or every released binary.
**Normal MTP use with sound hardware presents no identified app-specific electrical danger. A phone's fast-charging rating and a file's size are not evidence that the app can burn out a PS4.**
+670 -17
View File
@@ -1,21 +1,674 @@
MIT License
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (c) 2026 PS MTP Explorer contributors
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Preamble
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
+22
View File
@@ -0,0 +1,22 @@
MIT License
Copyright (C) 2026 nueex
Copyright (c) 2026 PS MTP Explorer contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+285
View File
@@ -0,0 +1,285 @@
{
"schema_version": 1,
"effective_date": "2026-10-26",
"local_base_revision": "164e3fb007154b2ca817fd944f49205b255326ce",
"original_author": "nueex",
"copyright": "Copyright (C) 2026 nueex",
"license": "GPL-3.0-or-later",
"scope_document": "LICENSING.md",
"ownership_basis": "Express maintainer confirmation on 2026-10-26 of original project code, UI design, icons and project-specific artwork; third-party intellectual property excluded.",
"fully_original_files": [
".gitattributes",
".github/FUNDING.yml",
".gitignore",
"BUILD.md",
"CMakeLists.txt",
"HARDWARE_SAFETY.md",
"LICENSE_SCOPE.json",
"LICENSING.md",
"LICENSING_ASSETS.json",
"Makefile.ps4",
"README.md",
"THIRD_PARTY_LICENSES.md",
"assets/icon.png",
"assets/icons/copy.svg",
"assets/icons/delete.svg",
"assets/icons/document.svg",
"assets/icons/drive.svg",
"assets/icons/folder.svg",
"assets/icons/gamepad.svg",
"assets/icons/home.svg",
"assets/icons/info.svg",
"assets/icons/move.svg",
"assets/icons/phone.svg",
"assets/icons/rename.svg",
"assets/icons/settings.svg",
"assets/icons/usb.svg",
"assets/reference/NOTICE.txt",
"assets/reference/copy.png",
"assets/reference/delete.png",
"assets/reference/document.png",
"assets/reference/drive.png",
"assets/reference/folder.png",
"assets/reference/home.png",
"assets/reference/icon0.png",
"assets/reference/info.png",
"assets/reference/move.png",
"assets/reference/phone.png",
"assets/reference/provenance.json",
"assets/reference/rename.png",
"assets/reference/settings.png",
"assets/reference/usb.png",
"assets/reference/wordmark.png",
"include/psmtp/browser.h",
"include/psmtp/copy.h",
"include/psmtp/copy_paths.h",
"include/psmtp/copy_plan.h",
"include/psmtp/device_sound.h",
"include/psmtp/device_watch.h",
"include/psmtp/diag_log.h",
"include/psmtp/diagnostic.h",
"include/psmtp/keyboard.h",
"include/psmtp/locations.h",
"include/psmtp/log_paths.h",
"include/psmtp/metadata.h",
"include/psmtp/mtp.h",
"include/psmtp/name.h",
"include/psmtp/navigator.h",
"include/psmtp/pkg.h",
"include/psmtp/sha256.h",
"include/psmtp/transfer_meter.h",
"include/psmtp/ui_format.h",
"include/psmtp/usb_label.h",
"include/psmtp/usb_module.h",
"include/psmtp/usb_select.h",
"include/psmtp/version.h",
"src/browser.c",
"src/copy.c",
"src/copy_plan.c",
"src/device_sound.c",
"src/device_watch.c",
"src/diag_log.c",
"src/diagnostic.c",
"src/keyboard.c",
"src/locations.c",
"src/metadata.c",
"src/mtp.c",
"src/name.c",
"src/navigator.c",
"src/pkg.c",
"src/ps4/console_render.c",
"src/ps4/console_render.h",
"src/ps4/copy_file.c",
"src/ps4/copy_file.h",
"src/ps4/device_audio.c",
"src/ps4/device_audio.h",
"src/ps4/dirent_check.h",
"src/ps4/exit.c",
"src/ps4/exit.h",
"src/ps4/file_ops.c",
"src/ps4/file_ops.h",
"src/ps4/file_store.c",
"src/ps4/file_store.h",
"src/ps4/main.c",
"src/ps4/package_install.c",
"src/ps4/package_install.h",
"src/ps4/preview.c",
"src/ps4/preview.h",
"src/ps4/process_access.c",
"src/ps4/process_access.h",
"src/ps4/release_config.h",
"src/ps4/screen.c",
"src/ps4/screen.h",
"src/ps4/stat_abi.h",
"src/ps4/storage_space.c",
"src/ps4/storage_space.h",
"src/ps4/ui_draw.c",
"src/ps4/ui_draw.h",
"src/ps4/ui_font.c",
"src/ps4/ui_font.h",
"src/ps4/usb_ps4.c",
"src/sha256.c",
"src/transfer_meter.c",
"src/ui_format.c",
"src/usb_label.c",
"src/usb_module.c",
"src/usb_select.c",
"tools/bootstrap_openorbis.py",
"tools/build_ps4.py",
"tools/embed_font.py",
"tools/inspect_ps4.py",
"tools/package_identity.py",
"tools/prepare_clean_icons.mjs",
"tools/ps4_runtime.py",
"tools/verify_production_release.py"
],
"mixed_files": [
{
"path": "assets/explorer.png",
"covered": "Only original nueex UI/design, authored geometric graphics, project title/launcher and generated representation of those owned portions; GPL-3.0-or-later.",
"excluded": "PS4/Sony branding, Bloodborne artwork, unresolved demo scenery and other third-party expression. See LICENSING_ASSETS.json."
},
{
"path": "assets/icons/platform.svg",
"covered": "Only original nueex UI/design, authored geometric graphics, project title/launcher and generated representation of those owned portions; GPL-3.0-or-later.",
"excluded": "PS4/Sony branding, Bloodborne artwork, unresolved demo scenery and other third-party expression. See LICENSING_ASSETS.json."
},
{
"path": "assets/reference/platform.png",
"covered": "Only original nueex UI/design, authored geometric graphics, project title/launcher and generated representation of those owned portions; GPL-3.0-or-later.",
"excluded": "PS4/Sony branding, Bloodborne artwork, unresolved demo scenery and other third-party expression. See LICENSING_ASSETS.json."
},
{
"path": "distribution/LICENSES.txt",
"covered": "Original current-scope notice prose by nueex only.",
"excluded": "Retained historical MIT text, third-party credits/terms and GPL/OFL/legal texts; no relicensing of those materials."
},
{
"path": "src/ps4/link.x",
"covered": "Only independently copyrightable original nueex changes, to the extent owned, offered under GPL-3.0-or-later.",
"excluded": "Inherited OpenOrbis linker script remains GPL-3.0-only; existing whole-file marker/notice unchanged."
},
{
"path": "src/ps4/reference_assets.inc",
"covered": "Only original nueex UI/design, authored geometric graphics, project title/launcher and generated representation of those owned portions; GPL-3.0-or-later.",
"excluded": "PS4/Sony branding, Bloodborne artwork, unresolved demo scenery and other third-party expression. See LICENSING_ASSETS.json."
},
{
"path": "third_party/libjbc/defs.h",
"covered": "Only independently copyrightable original additions/changes owned by nueex relative to pinned upstream inputs; GPL-3.0-or-later.",
"excluded": "All inherited libjbc expression; no upstream license or redistribution permission is supplied.",
"upstream_revision": "835fe016ff0ae5dd89b9249f39cc0fe093fd07dd",
"provenance": "third_party/libjbc/provenance.json",
"changes_notice": "third_party/libjbc/NOTICE.txt"
},
{
"path": "third_party/libjbc/jailbreak.c",
"covered": "Only independently copyrightable original additions/changes owned by nueex relative to pinned upstream inputs; GPL-3.0-or-later.",
"excluded": "All inherited libjbc expression; no upstream license or redistribution permission is supplied.",
"upstream_revision": "835fe016ff0ae5dd89b9249f39cc0fe093fd07dd",
"provenance": "third_party/libjbc/provenance.json",
"changes_notice": "third_party/libjbc/NOTICE.txt"
},
{
"path": "third_party/libjbc/kernelrw.c",
"covered": "Only independently copyrightable original additions/changes owned by nueex relative to pinned upstream inputs; GPL-3.0-or-later.",
"excluded": "All inherited libjbc expression; no upstream license or redistribution permission is supplied.",
"upstream_revision": "835fe016ff0ae5dd89b9249f39cc0fe093fd07dd",
"provenance": "third_party/libjbc/provenance.json",
"changes_notice": "third_party/libjbc/NOTICE.txt"
},
{
"path": "third_party/libjbc/kernelrw.h",
"covered": "Only independently copyrightable original additions/changes owned by nueex relative to pinned upstream inputs; GPL-3.0-or-later.",
"excluded": "All inherited libjbc expression; no upstream license or redistribution permission is supplied.",
"upstream_revision": "835fe016ff0ae5dd89b9249f39cc0fe093fd07dd",
"provenance": "third_party/libjbc/provenance.json",
"changes_notice": "third_party/libjbc/NOTICE.txt"
},
{
"path": "third_party/libjbc/libjbc.h",
"covered": "Only independently copyrightable original additions/changes owned by nueex relative to pinned upstream inputs; GPL-3.0-or-later.",
"excluded": "All inherited libjbc expression; no upstream license or redistribution permission is supplied.",
"upstream_revision": "835fe016ff0ae5dd89b9249f39cc0fe093fd07dd",
"provenance": "third_party/libjbc/provenance.json",
"changes_notice": "third_party/libjbc/NOTICE.txt"
}
],
"excluded_files": [
{
"path": "LICENSE",
"terms": "Unmodified GNU GPLv3 license document; FSF copyright, not nueex artwork/code."
},
{
"path": "LICENSES/MIT-legacy.txt",
"terms": "Verbatim pre-migration MIT grant and copyright notices; preserved historical permissions."
},
{
"path": "assets/fonts/NotoSans-Light-OFL.txt",
"terms": "Noto/SIL OFL-1.1 font material or its provenance/license text; no nueex relicensing."
},
{
"path": "assets/fonts/NotoSans-Light.provenance.json",
"terms": "Noto/SIL OFL-1.1 font material or its provenance/license text; no nueex relicensing."
},
{
"path": "assets/fonts/NotoSans-Light.ttf",
"terms": "Noto/SIL OFL-1.1 font material or its provenance/license text; no nueex relicensing."
},
{
"path": "assets/fonts/OFL.txt",
"terms": "Noto/SIL OFL-1.1 font material or its provenance/license text; no nueex relicensing."
},
{
"path": "assets/icons/ps4-wordmark.png",
"terms": "Externally owned or unresolved artwork; no project GPL/MIT grant. See LICENSING_ASSETS.json."
},
{
"path": "assets/reference/thumb_image.png",
"terms": "Externally owned or unresolved artwork; no project GPL/MIT grant. See LICENSING_ASSETS.json."
},
{
"path": "assets/reference/thumb_pkg.png",
"terms": "Externally owned or unresolved artwork; no project GPL/MIT grant. See LICENSING_ASSETS.json."
},
{
"path": "assets/reference/thumb_video.png",
"terms": "Externally owned or unresolved artwork; no project GPL/MIT grant. See LICENSING_ASSETS.json."
},
{
"path": "src/ps4/font_data.inc",
"terms": "Noto/SIL OFL-1.1 font material or its provenance/license text; no nueex relicensing."
},
{
"path": "third_party/libjbc/NOTICE.txt",
"terms": "Upstream libjbc or its preserved attribution/provenance; redistribution unresolved. Original notice prose does not license the upstream code."
},
{
"path": "third_party/libjbc/README.md",
"terms": "Upstream libjbc or its preserved attribution/provenance; redistribution unresolved. Original notice prose does not license the upstream code."
},
{
"path": "third_party/libjbc/jailbreak.h",
"terms": "Upstream libjbc or its preserved attribution/provenance; redistribution unresolved. Original notice prose does not license the upstream code."
},
{
"path": "third_party/libjbc/provenance.json",
"terms": "Upstream libjbc or its preserved attribution/provenance; redistribution unresolved. Original notice prose does not license the upstream code."
}
],
"interpretation": [
"The grant covers only rights held by nueex. For original prose/metadata it does not relicense quoted notices, legal text or referenced third-party works.",
"No whole-file GPL-3.0-or-later label applies to mixed files. Upstream grants and notices remain unchanged.",
"Unlisted/ignored local files, research clones, SDKs and historical artifacts are outside this inventory.",
"Historical MIT grants remain effective for previously MIT-licensed material. This does not license new contributions under MIT.",
"Original project-title/launcher art is covered on the maintainer ownership confirmation; third-party typography, fonts and marks are not relicensed."
],
"complete_application_distribution_cleared": false,
"remaining_issues": [
"libjbc upstream redistribution permissions",
"Third-party marks and Bloodborne/demo scenery clearance",
"Exact corresponding-source/runtime compliance for a future complete release"
],
"initial_local_review_revision": "4b051d6fae77a3fedb6cc85ed2828ed4fb36d167",
"publication_integration": "Based on current main 164e3fb; upstream removal of obsolete tools and unused font retained. No new binaries or release artifacts."
}
+199
View File
@@ -0,0 +1,199 @@
# PS MTP Explorer licensing scope
**Original Author: nueex — Copyright (C) 2026 nueex**
Effective for this source revision: **2026-10-26**, based on nueex's express
confirmation of authorship and ownership of the project's original code,
project-specific UI designs, icons and artwork. No historical release is changed.
## Grant for original work
PS MTP Explorer's original work by nueex, identified in
[LICENSE_SCOPE.json](LICENSE_SCOPE.json), is free software/work: you can
redistribute it and/or modify it under the terms of the GNU General Public
License as published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version (**GPL-3.0-or-later**).
This work is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the [GNU General Public License](LICENSE) for details.
A complete, unmodified copy of GPLv3 is in `LICENSE`.
This is a grant of nueex's rights in nueex's original contributions. It does not
grant ownership of, or additional permissions for, any other person's work.
The exclusions below define the scope of this grant; they do not add restrictions
to the GPL rights granted for the covered original work.
## File inventory
The explicit file lists in [LICENSE_SCOPE.json](LICENSE_SCOPE.json) identify:
- **Fully original files:** application C files, application headers, project
scripts/configuration, original project prose/metadata, independently authored
geometric SVGs and their raster versions, and the project-specific launcher
and title artwork. The source headers use `SPDX-License-Identifier:
GPL-3.0-or-later`. Artwork is identified in the inventory without changing its
pixels or introducing metadata into the image files.
- **Mixed files:** the grant covers only nueex's original contributions.
`platform.svg` and its raster/embedded counterpart include third-party PS4
branding. `assets/explorer.png` combines the original UI with third-party or
unresolved images. `reference_assets.inc` combines owned graphics with those
excluded images. Original independently copyrightable local changes to libjbc
are covered only to the extent nueex owns them; the upstream implementation
is not. Original changes to the OpenOrbis-derived linker script do not alter
that file's existing GPL-3.0-only terms. Do not assign a whole-file GPL-3.0-or-later
identifier to any of these mixed files.
- **Excluded files:** fonts and generated font data, externally owned or
unresolved artwork, upstream library contents, third-party provenance/legal
notices, and license texts. They retain their separate terms. The inventory's
excluded entries apply even if a file is under `src/` or `assets/`.
The project prose in README and licensing documents is covered only as original
prose; quotations, reproduced legal notices, references and third-party material
retain their own rights. The GPL text itself is copyrighted by the Free Software
Foundation, not nueex. Ignored SDKs, local research clones, historical build
artifacts and other unlisted local files are not relicensed by this inventory.
New files need their own appropriate licensing/provenance review.
## Historical MIT permissions
[LICENSES/MIT-legacy.txt](LICENSES/MIT-legacy.txt) preserves the complete
pre-migration MIT license, including both nueex's copyright credit and the
existing `Copyright (c) 2026 PS MTP Explorer contributors` notice. It is a
historical notice, not a blanket MIT grant for new contributions after this
migration or for excluded components.
Previously MIT-licensed versions and material retain their original permissions.
Adding GPL notices does not withdraw earlier MIT grants, including permissions
for unchanged material already received under MIT. Neither the old MIT license
nor this GPL grant supplies missing rights in libjbc or third-party artwork.
Do not rewrite old tags, packages, archives or their notices to suggest otherwise.
## Separate third-party terms and unresolved distribution rights
See [THIRD_PARTY_LICENSES.md](THIRD_PARTY_LICENSES.md) and the per-asset
[LICENSING_ASSETS.json](LICENSING_ASSETS.json) record.
- **libjbc:** upstream redistribution permission remains unresolved. Credits to
sleirsgoevy, LightningMods and bucanero and all existing notices are retained.
The grant for nueex's local additions cannot license their inherited code.
No linking exception or upstream permission grant is asserted here.
- **OpenOrbis:** its SDK/runtime notices and the derived linker script's
`GPL-3.0-only` identifier remain. The original application work can be used
under GPL version 3 with GPLv3-only components; that does not make those
components available under later versions.
- **musl/stb:** upstream MIT/permissive/public-domain alternatives and
component notices remain as supplied; they are not replaced by nueex's GPL.
- **Noto fonts:** SIL OFL 1.1 and both original notices remain, including for
the generated embedded font data.
- **Branding/demo artwork:** Sony/PS4 marks, the Bloodborne cover and demo
scenery without an established ownership/license record remain excluded.
The authorship confirmation for original project design is not permission for
third-party intellectual property. No trademark rights are granted.
**The complete repository/source bundle and compiled PS4 application have not
been cleared for distribution merely by this original-work migration.**
Obtain the necessary libjbc permissions and resolve excluded artwork, then
verify corresponding-source/runtime obligations before making a new release.
This outstanding work does not suspend the GPL grant for nueex's original work.
## libjbc provenance and permission
The integration is based on [bucanero/ps4-libjbc at
835fe016ff0ae5dd89b9249f39cc0fe093fd07dd](https://github.com/bucanero/ps4-libjbc/tree/835fe016ff0ae5dd89b9249f39cc0fe093fd07dd),
forked from [sleirsgoevy/ps4-libjbc](https://github.com/sleirsgoevy/ps4-libjbc).
The pinned source tree has no explicit license file or express source/header
license grant established by the review dated 2026-10-26. Public availability,
attribution, inclusion of source and use by other GPL projects do not by
themselves establish redistribution permission for this dependency.
The relevant upstream contributors identified in its history are:
- **Sergey Lisov / sleirsgoevy:** original implementation, kernel access,
process/prison/vnode handling and reference-count fixes.
- **LightningMods:** Sony credential/capability handling in the merged
[PR #1](https://github.com/sleirsgoevy/ps4-libjbc/pull/1), including fields used
by this application's access profile and system-module loading.
- **Damián Parrino / bucanero:** OpenOrbis static-library/public-header
integration in [commit 730cdae](https://github.com/bucanero/ps4-libjbc/commit/730cdae2900e86fea163b294764106ee916c7844)
and subsequent upstream merges.
These contributions identify prospective permission contacts, not proof of
current ownership or authority to license other contributors' work. Keep the
input hashes in [third_party/libjbc/provenance.json](third_party/libjbc/provenance.json)
and integration-change record in [NOTICE.txt](third_party/libjbc/NOTICE.txt).
Only `jailbreak.c` and `kernelrw.c` are compiled into the application.
The preferred resolution is an upstream GPL-compatible license or retained
written grants covering all relevant rights in that pinned code. MIT or
GPL-3.0-or-later are suitable requests; GPLv3-only material can be used under
version 3 without becoming available under later versions. Grants must permit
the intended modification and source/binary redistribution; informal homebrew
approval or attribution alone is insufficient. Retain the grant, identity,
covered revision/files and complete license terms. A grant or linking exception
from nueex cannot supply rights held by libjbc's upstream authors.
The [ps4sdk privilege routines](https://github.com/ps4dev/ps4sdk/blob/4df9d001b66ae4ec07d9a51b62d1e4c5e270eecc/common/kernel/source/kernel/privilege.c)
and [Payload Guest's removal of libjbc](https://github.com/Al-Azif/ps4-payload-guest/releases/tag/v0.97)
were investigated as alternatives; neither is a verified drop-in for this app's
complete filesystem/installer credential profile. Do not remove the library or
bypass the startup access checks merely to change licensing status. Any
replacement needs independent provenance/license review and must preserve the
GoldHEN runtime probe, original Unix ownership, Sony credentials/capabilities,
prison/vnode lifetime handling, failure checks and all transfer/install functions.
Host checks alone do not establish console kernel ABI compatibility.
[Apollo Save Tool's GPL notice](https://github.com/bucanero/apollo-ps4/blob/main/README.md#license)
provides an original-author/version-3-or-later model; it does not grant rights
to this separate libjbc dependency.
## Artwork and complete-release requirements
The maintainer's ownership confirmation covers original project launcher/title
artwork and UI/design, as inventoried. It does not license external intellectual
property. The remaining fixed-asset issues are:
- `assets/icons/ps4-wordmark.png`, the composite `platform.svg` and its raster
contain PS4 branding; only the authored gamepad/design portions are covered.
- `assets/reference/thumb_pkg.png` contains a Bloodborne cover excerpt;
`thumb_image.png` and `thumb_video.png` contain scenery without an established
ownership/license record. All three are embedded in `reference_assets.inc`,
despite their historical QA-only description. No compiler elimination or
non-distribution is assumed.
- `assets/explorer.png` depicts the mixed UI and demo imagery. The generated
include and screenshot need corresponding updates if excluded assets change;
replacing a PNG alone leaves its old embedded pixels in the executable.
Document adequate permissions for external elements or replace them with cleared
artwork. Preserve third-party copyright and trademark notices. No live user-file
preview, transfer or installation capability needs removal to replace fixed
demo graphics. Current pixels and functionality remain unchanged.
Before distributing a complete source bundle or PS4 release, resolve libjbc and
external-asset rights, retain all applicable notices, and provide corresponding
source matching the exact binary revision. Include modified library code,
required runtime sources, editable inputs and build scripts, plus applicable
GPLv3 installation information when required. The older third-party archive
alone is not complete corresponding source for the application. Verify a fresh
production build, package extraction and supported console behavior if code or
assets change. Do not assume a system-library exception for libjbc, rewrite
historical releases, or treat successful compilation as distribution clearance.
## Packaging and appearance
The root GPL text, this scope document, inventory, historical MIT notice and
third-party notices must travel with source distributions of this revision.
The production builder retains the existing package file list and copies the
combined `distribution/LICENSES.txt` notice into its local output. The repository
contains the full scope documents and historical MIT notice for matching source
distributions. Removed upstream preview/delivery/collector helpers are not
restored by this licensing update. A future distributor must provide the full
applicable source and notices; technical packaging is not legal clearance.
The current combined notice retains the older third-party/legal texts before
which the scoped original-work notice is added. Old release artifacts are untouched.
No UI strings, renderer behavior, artwork pixels or PS4 access implementation
are changed by the migration. The visible `by nueex` credit and OpenOrbis About
module remain; the fuller author/copyright and licensing details are in README
and the distributed legal notices. A future About redesign must preserve
upstream credits and be reviewed separately.
+210
View File
@@ -0,0 +1,210 @@
{
"schema_version": 1,
"reviewed_on": "2026-10-26",
"scope": "Original project artwork covered by the scoped nueex GPL grant, excluded third-party elements, and dependent mixed copies. See LICENSING.md and LICENSE_SCOPE.json.",
"original_author_credit": "Original Author: nueex - Copyright (C) 2026 nueex (original contributions only)",
"status_meaning": "Current status distinguishes original owned portions from third-party IP. Ownership confirmation is not a grant from external rights holders. No pixels were changed.",
"groups": [
{
"id": "launcher",
"status": "original-project-work-GPL-3.0-or-later",
"permission_evidence": "Maintainer ownership confirmation and scoped grant dated 2026-10-26, recorded in LICENSING.md.",
"source_record": "app icon.jpg",
"use": "PS4 package icon and README",
"required_action": "No replacement needed for original project artwork. Preserve editable originals/provenance and any separate font/third-party notices. No rights to external IP are granted.",
"files": [
{
"path": "assets/reference/icon0.png",
"sha256": "64148659f9a5f245940f022acfe57734de96e16b59bc2c66a594355ae12864f1",
"dimensions": [
512,
512
]
},
{
"path": "assets/icon.png",
"sha256": "64148659f9a5f245940f022acfe57734de96e16b59bc2c66a594355ae12864f1",
"dimensions": [
512,
512
]
}
],
"original_work_license": "GPL-3.0-or-later"
},
{
"id": "application-title",
"status": "original-project-work-GPL-3.0-or-later",
"permission_evidence": "Maintainer ownership confirmation and scoped grant dated 2026-10-26, recorded in LICENSING.md.",
"source_record": "1792455114194.jpg; crop recorded in assets/reference/provenance.json",
"use": "Production header; embedded ui_wordmark; README screenshot",
"required_action": "No replacement needed for original project artwork. Preserve editable originals/provenance and any separate font/third-party notices. No rights to external IP are granted.",
"files": [
{
"path": "assets/reference/wordmark.png",
"sha256": "caef162d0d94c80daf663746955a30261192f6cbce87cbc2adedb355ed66fd92",
"dimensions": [
570,
48
]
}
],
"original_work_license": "GPL-3.0-or-later"
},
{
"id": "platform-brand",
"status": "mixed-original-work-and-excluded-branding",
"permission_evidence": "Original authored portion confirmed by nueex; no external PS4 branding grant established.",
"source_record": "Supplied PS4 wordmark; SVG mixes that raster with authored gamepad.svg",
"use": "Production header; embedded ui_platform; README screenshot",
"required_action": "Document applicable branding/artwork rights, or use a plain-text platform label and authored gamepad.",
"files": [
{
"path": "assets/icons/ps4-wordmark.png",
"sha256": "89a8583a3058d9c236f291add96924dfa9b484313d4c3c65d422720f05665729",
"dimensions": [
112,
39
]
},
{
"path": "assets/icons/platform.svg",
"sha256": "f3d1e528885b49a511c2f0bda77595186ac406988efa360a7fba0f9176e08d11"
},
{
"path": "assets/reference/platform.png",
"sha256": "32f960b8b317254d65cd02e63b55a62645ae05f1589baedd3b93e13571a69793",
"dimensions": [
176,
39
]
}
],
"original_work_license": "GPL-3.0-or-later for authored gamepad/design only"
},
{
"id": "package-demo",
"status": "permission-or-replacement-needed",
"permission_evidence": null,
"source_record": "1792455114194.jpg; Bloodborne cover excerpt",
"use": "Embedded ui_thumb_pkg; demo rendering; README screenshot",
"required_action": "Obtain relevant artwork permission or replace with an original generic package illustration.",
"files": [
{
"path": "assets/reference/thumb_pkg.png",
"sha256": "e350d2e16463112ebc7de154e881ad67e039ef97c41a0d36984499fde5126c63",
"dimensions": [
178,
132
]
}
]
},
{
"id": "image-demo",
"status": "permission-or-replacement-needed",
"permission_evidence": null,
"source_record": "1792455114194.jpg; unidentified scenery crop",
"use": "Embedded ui_thumb_image; demo rendering; README screenshot",
"required_action": "Identify creator/license or replace with original/licensed demo scenery.",
"files": [
{
"path": "assets/reference/thumb_image.png",
"sha256": "f6aad5cf7e477d8399092d1cbde9bb73a37619d13bd73ae1a8c50118f0769921",
"dimensions": [
218,
132
]
}
]
},
{
"id": "video-demo",
"status": "permission-or-replacement-needed",
"permission_evidence": null,
"source_record": "1792455114194.jpg; unidentified scenery crop with play glyph",
"use": "Embedded ui_thumb_video; demo rendering; README screenshot",
"required_action": "Identify creator/license or replace with original/licensed demo scenery and authored play glyph.",
"files": [
{
"path": "assets/reference/thumb_video.png",
"sha256": "749f5e6a4b211c4cead7aee8e082ff7749fa6492c85f839ba95d290c83ac1f07",
"dimensions": [
218,
132
]
}
]
},
{
"id": "documentation-screenshot",
"status": "mixed-original-UI-and-excluded-imagery",
"permission_evidence": "Original UI/design confirmed by nueex; depicted third-party marks/demo content remain excluded.",
"source_record": "App renderer with demo content and supplied title/platform marks",
"use": "README; not a packaged UI background",
"required_action": "Re-render after clearing/replacing the source assets, or document permission adequate for this use.",
"files": [
{
"path": "assets/explorer.png",
"sha256": "3993411f16283c421fb40eba70e5bdad518c885a80995e55363c6ab214309bf7",
"dimensions": [
1920,
1080
]
}
],
"original_work_license": "GPL-3.0-or-later for original UI/design only"
}
],
"generated_dependency": {
"path": "src/ps4/reference_assets.inc",
"sha256": "8c598c7f1b351ce7b37bc57751b2f808a396f96c07033d891edbf1781c30b328",
"required_action": "Original owned graphic portions are GPL-3.0-or-later. Third-party branding/demo arrays remain excluded. Regenerate only after changing/clearing inputs; no whole-file GPL header.",
"verified_embedded_assets": [
{
"symbol": "ui_wordmark",
"source_png": "assets/reference/wordmark.png",
"exact_rgba_match": true,
"rgba_sha256": "b90dc0c25f7d9cb6c845f83ed34a3d7a33e84298b92fa2177c51ed50b6ef2c17"
},
{
"symbol": "ui_platform",
"source_png": "assets/reference/platform.png",
"exact_rgba_match": true,
"rgba_sha256": "30f9729c8db24ae0880c953b81a5b21dd405d2d5e04d3971c718c41216a6fe79"
},
{
"symbol": "ui_thumb_pkg",
"source_png": "assets/reference/thumb_pkg.png",
"exact_rgba_match": true,
"rgba_sha256": "7c2e05e2771d30bba90fd827abee969bbd1379ef09969b07edcf567a8fc984f2"
},
{
"symbol": "ui_thumb_image",
"source_png": "assets/reference/thumb_image.png",
"exact_rgba_match": true,
"rgba_sha256": "494ee4573f9a0bc7f12a5e9f33e0a6208bf505828810d251cdd15adc3c10ff9b"
},
{
"symbol": "ui_thumb_video",
"source_png": "assets/reference/thumb_video.png",
"exact_rgba_match": true,
"rgba_sha256": "4205edc16862e8d10958c929bba24ec31a901ab62c280219defe50e2f26a3fee"
}
]
},
"composite_platform_check": {
"embedded_raster_matches": "assets/icons/ps4-wordmark.png",
"exact_bytes_match": true
},
"already_documented_separate_terms": {
"authored_geometric_icons": "Now GPL-3.0-or-later under LICENSE_SCOPE.json; historical MIT grants retained in LICENSES/MIT-legacy.txt. platform.svg remains a mixed exception.",
"noto_fonts": "OFL-1.1; preserve both supplied notices.",
"runtime_user_previews": "User-selected artwork/thumbnail decoding is a feature, separate from distributing fixed demo images."
},
"ownership_confirmation": {
"date": "2026-10-26",
"basis": "nueex expressly confirms creation/ownership of original project code, UI designs, icons and project-specific artwork; excludes external libraries and third-party IP.",
"scope_document": "LICENSING.md"
}
}
+22
View File
@@ -0,0 +1,22 @@
# PS MTP Explorer
# Original Author: nueex
# Copyright (C) 2026 nueex
# SPDX-License-Identifier: GPL-3.0-or-later
# GNU GPL version 3 or later; see LICENSE and LICENSING.md.
# WITHOUT ANY WARRANTY; see LICENSE.
# Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
# Cross-platform Python owns the compiler/link/package commands so flags do not drift.
# Run: make -f Makefile.ps4 [package]; see BUILD.md.
PYTHON ?= python3
ifeq ($(EXPERIMENTAL_USB),1)
MODE := --experimental-usb
else
MODE := --production
endif
SDK_ARG := $(if $(OO_PS4_TOOLCHAIN),--sdk "$(OO_PS4_TOOLCHAIN)",)
all:
$(PYTHON) tools/build_ps4.py $(MODE) $(SDK_ARG)
package:
$(PYTHON) tools/build_ps4.py $(MODE) $(SDK_ARG) --package
.PHONY: all package
+31 -11
View File
@@ -18,6 +18,7 @@
<p align="center">
<a href="https://github.com/nueex/ps-mtp-explorer/releases/latest"><strong>Download PKG</strong></a> ·
<a href="#getting-started">Getting started</a> ·
<a href="BUILD.md">Build from source</a> ·
<a href="#install-pkg-from-phone-to-ps4">Install packages</a> ·
<a href="#controller-guide">Controls</a> ·
<a href="https://github.com/nueex/ps-mtp-explorer/issues">Report an issue</a>
@@ -46,6 +47,10 @@
<sub>Interface preview rendered from the app using sample files.</sub>
</p>
## Hardware safety
PS MTP Explorer uses standard USB data transfers and is electrically safe under normal operating conditions. Read the [Hardware Safety & Electrical Analysis](HARDWARE_SAFETY.md) for clear answers about fast-charging phones, the Southbridge, large transfers, and safe cables.
## Getting started
### What you need
@@ -59,8 +64,8 @@
### Install and open
Download **[PS-MTP-Explorer-1.01.pkg](https://github.com/nueex/ps-mtp-explorer/releases/download/v1.01/PS-MTP-Explorer-1.01.pkg)** and install it using GoldHEN's package installer. Version **1.01** updates the existing `NUEX00001` app; its PS4 `APP_VER` and `VERSION` metadata are **01.01**. The previous [1.00 release](https://github.com/nueex/ps-mtp-explorer/releases/tag/v1.00) remains available.
Download **[PS-MTP-Explorer-1.01.pkg](https://github.com/nueex/ps-mtp-explorer/releases/download/v1.01/PS-MTP-Explorer-1.01.pkg)** and install it using GoldHEN's package installer. Version **1.01** updates the existing `NUEX00001` app; its PS4 `APP_VER` and `VERSION` metadata are **01.01**. The previous [1.00 release](https://github.com/nueex/ps-mtp-explorer/releases/tag/v1.00) remains available.
Then:
1. **Connect your phone via USB.** Unlock it and select File Transfer / MTP.
@@ -110,6 +115,14 @@ For phone installations, leave enough internal space for **both the temporary PK
| **Touchpad** | Refresh devices and the current view. |
| **Left from the file area's left edge** | Enter the device sidebar. |
## Build from source
See [BUILD.md](BUILD.md) for the required tools, SDK setup, production PKG
build commands, package validation, and optional CMake host library build.
Application source, bundled artwork, fonts, and the libjbc integration are
included in this repository; install the external compiler and OpenOrbis SDK
as described in the guide.
## Common questions
<details>
@@ -133,15 +146,9 @@ Phone package installation requires free internal storage for both the package a
<details>
<summary><strong>Where are downloads and source code?</strong></summary>
The compiled application is distributed through GitHub Releases. This repository contains release documentation and assets; the application source is not published here. Required third-party notices and corresponding runtime materials accompany the release separately.
The application source and build scripts are included in this repository; follow [BUILD.md](BUILD.md) to compile your own copy. Previously released binaries retain their accompanying notices. See [LICENSING.md](LICENSING.md) for the current original-work GPL grant and unresolved requirements for distributing the complete application.
</details>
<details>
<summary><strong>Where is the project's source code?</strong></summary>
The source code will be published in the future.
</details>
## Feedback
[Open an issue](https://github.com/nueex/ps-mtp-explorer/issues) with your PS4 firmware/GoldHEN version, phone model and Android version if relevant, the storage source/destination, and steps to reproduce the issue. Include the error shown on screen.
@@ -154,12 +161,25 @@ If PS MTP Explorer makes managing your PS4 easier, you can support development o
## Credits & third-party notices
Created by **nueex**. Built with [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain), musl, stb and Noto Sans.
**Original Author: nueex — Copyright (C) 2026 nueex** (original project work). Built with [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain), musl, stb and Noto Sans.
**libjbc - PS4 Jailbreak C library by sleirsgoevy, with modifications by bucanero. Provided and used with full author attribution for PS4 homebrew execution.**
Additional libjbc contributor: **LightningMods**, for upstream Sony credential/capability handling.
See [THIRD_PARTY_LICENSES.md](THIRD_PARTY_LICENSES.md) and the third-party materials attached to the release for component notices. PS MTP Explorer is an independent homebrew project and is not affiliated with Sony Interactive Entertainment.
## License and scope
**nueex's original work is licensed under GNU GPL v3.0-or-later (`GPL-3.0-or-later`)**, effective 2026-10-26. You may redistribute and/or modify the covered work under GNU GPL version 3 or, at your option, any later version. It is provided **WITHOUT ANY WARRANTY**, including MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the complete [LICENSE](LICENSE), [licensing scope](LICENSING.md), and explicit [file/contribution inventory](LICENSE_SCOPE.json).
The grant covers original application code, project-specific UI/design, authored icons and artwork owned by nueex. Mixed files are covered only for nueex's original contributions. Third-party libraries, OpenOrbis-derived material, fonts, trademarks and externally owned artwork retain their existing terms. No whole-file GPL-3.0-or-later grant is asserted for libjbc or mixed assets.
**The complete application/source bundle is not cleared for redistribution.** libjbc's inherited-code permissions and excluded third-party artwork/branding remain unresolved; see [LICENSING.md](LICENSING.md) and the [asset inventory](LICENSING_ASSETS.json). This status does not limit the GPL grant for nueex's original work. No component, feature or UI appearance was changed by this migration.
Previously MIT-licensed versions and material retain their original permissions. The complete [historical MIT notice](LICENSES/MIT-legacy.txt), including existing copyright notices, is preserved. These GPL notices do not revoke prior MIT grants or grant new rights to third-party content. Historical releases and archives are unchanged.
---
<p align="center"><strong>GoldHEN file manager | PS4 MTP transfer | Android to PS4 USB | Direct PKG installation</strong><br>PS MTP Explorer by nueex</p>
<p align="center"><strong>GoldHEN file manager | PS4 MTP transfer | Android to PS4 USB | Direct PKG installation</strong><br>PS MTP Explorer by nueex</p>
+6
View File
@@ -1,5 +1,7 @@
# Third-party notices
**Original-work migration (2026-10-26):** Original Author: nueex — Copyright (C) 2026 nueex. Only nueex's original work is now offered under GPL-3.0-or-later; see [LICENSING.md](LICENSING.md) and [LICENSE_SCOPE.json](LICENSE_SCOPE.json). Third-party terms remain unchanged. The complete application is not cleared for redistribution: libjbc and excluded third-party artwork/branding still require resolution. See [LICENSING.md](LICENSING.md). Attribution and source availability alone do not establish redistribution permission. The [historical MIT grant](LICENSES/MIT-legacy.txt) remains effective for previously licensed material.
PS MTP Explorer includes third-party components with their own terms. This file does not relicense those components. License texts and corresponding OpenOrbis runtime materials are provided in `PS-MTP-Explorer-1.01-Third-Party.zip` alongside the compiled release. Application source is not included in that archive.
| Component | Attribution and terms |
@@ -9,9 +11,13 @@ PS MTP Explorer includes third-party components with their own terms. This file
| stb_image / stb_truetype | Sean Barrett and contributors; MIT or public-domain options as stated in the bundled upstream headers. |
| Noto Sans Light | Google and Noto Project contributors; SIL Open Font License 1.1. Font notices are retained. |
| libjbc | sleirsgoevy, with modifications by bucanero. The pinned bucanero/ps4-libjbc snapshot is `835fe016ff0ae5dd89b9249f39cc0fe093fd07dd`. That upstream snapshot does not contain a standalone license file; no MIT/GPL license is assigned to it here. |
| OpenOrbis-derived linker script | `src/ps4/link.x` retains its existing `GPL-3.0-only` identifier and upstream derivation notice. It is not covered by the project's MIT grant, and is not changed to GPL-3.0-or-later. |
| Artwork and wordmarks | Project-specific original launcher/title artwork and authored UI graphics are covered by nueex's scoped GPL grant following the ownership confirmation. The PS4/Sony logo, Bloodborne cover, unresolved demo scenery and other third-party elements remain excluded. The platform SVG and screenshot are mixed works. See [assets/reference/NOTICE.txt](assets/reference/NOTICE.txt) and [LICENSING_ASSETS.json](LICENSING_ASSETS.json); no project MIT/GPL grant is asserted for external elements. |
**libjbc - PS4 Jailbreak C library by sleirsgoevy, with modifications by bucanero. Provided and used with full author attribution for PS4 homebrew execution.**
The compiled integration uses the kernel-access and process-credential portions of libjbc. The release's third-party archive retains the pinned upstream snapshot, attribution and integration sources. Its upstream repository is [bucanero/ps4-libjbc](https://github.com/bucanero/ps4-libjbc), forked from [sleirsgoevy/ps4-libjbc](https://github.com/sleirsgoevy/ps4-libjbc).
Additional upstream contributor: **LightningMods**, whose merged [PR #1](https://github.com/sleirsgoevy/ps4-libjbc/pull/1) adds Sony credential/capability handling used by this integration. Permission review must cover the relevant contributions of sleirsgoevy, LightningMods and bucanero; this credit is not a license grant. The [artwork inventory](LICENSING_ASSETS.json) identifies supplied assets and their embedded/documentation copies needing clearance. These complete-distribution issues do not prevent nueex from separately licensing original work nueex owns under GPL-3.0-or-later.
OpenOrbis packaging/conversion tools and LLVM are build-time tools; their executables are not shipped in the application distribution. No proprietary Sony SDK or console firmware module is bundled.
+93
View File
@@ -0,0 +1,93 @@
Copyright 2015-2021 Google LLC. All Rights Reserved.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,5 @@
{
"url": "https://raw.githubusercontent.com/notofonts/noto-fonts/main/hinted/ttf/NotoSans/NotoSans-Light.ttf",
"sha256": "74ffdd438f2ae232371ceb00444dae93e2590d263a4ff4c5e2c8385aa8012fdb",
"license": "OFL-1.1; see OFL.txt"
}
Binary file not shown.
+93
View File
@@ -0,0 +1,93 @@
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/latin-greek-cyrillic)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 39"><g fill="none" stroke="#eee7e8" stroke-width="2.5" stroke-linejoin="round"><path d="M25 5H4v27h5"/><path d="M10 11h15l7 7v18H10V11Z"/><path d="M25 11v7h7M15 23h12M15 28h12M15 33h8"/></g></svg>

After

Width:  |  Height:  |  Size: 253 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 39"><g fill="none" stroke="#eee7e8" stroke-width="2.7" stroke-linecap="round" stroke-linejoin="round"><path d="M5 10h26M12 9V5h12v4M8 14l2 21h16l2-21M15 16v14M21 16v14"/></g></svg>

After

Width:  |  Height:  |  Size: 237 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 81 110"><defs><linearGradient id="paper" x2=".8" y2="1"><stop stop-color="#f6eff2"/><stop offset="1" stop-color="#d5c6d1"/></linearGradient></defs><path d="M9 4h39l28 28v69a5 5 0 0 1-5 5H9a5 5 0 0 1-5-5V9a5 5 0 0 1 5-5Z" fill="url(#paper)" stroke="#cbbbc6"/><path d="M48 4v23a5 5 0 0 0 5 5h23L48 4Z" fill="#fff8fc" stroke="#cbbbc6" stroke-linejoin="round"/><path d="M15 46h48M15 56h48M15 66h48M15 76h48M15 86h34" fill="none" stroke="#a89ba8" stroke-width="2.5" stroke-linecap="round" opacity=".8"/><path d="M9 9v92" stroke="#fff9fc" opacity=".65"/></svg>

After

Width:  |  Height:  |  Size: 608 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><g fill="#eee7e8"><ellipse cx="20" cy="8" rx="16" ry="6"/><path d="M4 12c7 6 25 6 32 0v18c0 4-7 8-16 8S4 34 4 30V12Z"/></g></svg>

After

Width:  |  Height:  |  Size: 190 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 192 156"><defs><linearGradient id="back" x2="0" y2="1"><stop stop-color="#ffc952"/><stop offset="1" stop-color="#e89c24"/></linearGradient><linearGradient id="front" x2="0.6" y2="1"><stop stop-color="#ffca57"/><stop offset="1" stop-color="#eda72e"/></linearGradient></defs><path d="M8 17a8 8 0 0 1 8-8h48c4 0 7 2 9 5l15 18h88a8 8 0 0 1 8 8v102a8 8 0 0 1-8 8H16a8 8 0 0 1-8-8V17Z" fill="url(#back)" stroke="#e8a22c"/><path d="M16 40h160a8 8 0 0 1 8 8v94a8 8 0 0 1-8 8H16a8 8 0 0 1-8-8V48a8 8 0 0 1 8-8Z" fill="url(#front)" stroke="#f5bc4a"/><path d="M17 42h158" stroke="#ffe299" stroke-width="1.5" opacity=".8"/><path d="M24 41v108H16a7 7 0 0 1-7-7V49a8 8 0 0 1 8-8h7Z" fill="#ffd373" opacity=".42"/><path d="M11 22h56" stroke="#ffe197" stroke-width="1.5" opacity=".7"/></svg>

After

Width:  |  Height:  |  Size: 829 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 36"><path fill="#eee7e8" d="M11 5c4 0 5 2 13 2s9-2 13-2c5 0 7 5 8 11l2 12c.5 4-4 6-7 3l-8-7H16l-8 7c-3 3-7.5 1-7-3l2-12C4 10 6 5 11 5Z"/><g fill="#292529"><path d="M10 11h4v4h4v4h-4v4h-4v-4H6v-4h4v-4Z"/><circle cx="36" cy="13" r="1.5"/><circle cx="40" cy="17" r="1.5"/><circle cx="32" cy="17" r="1.5"/><circle cx="36" cy="21" r="1.5"/><rect x="20" y="10" width="8" height="7" rx="1.5"/></g></svg>

After

Width:  |  Height:  |  Size: 453 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 34 35"><path fill="#eee7e8" d="m2 16 15-13 15 13-2 3-3-2v15h-8V22h-5v10H7V17l-3 2-2-3Z"/></svg>

After

Width:  |  Height:  |  Size: 149 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><circle cx="20" cy="20" r="16" fill="none" stroke="#eee7e8" stroke-width="3"/><circle cx="20" cy="11.5" r="2" fill="#eee7e8"/><rect x="18.3" y="17" width="3.4" height="13" rx="1" fill="#eee7e8"/></svg>

After

Width:  |  Height:  |  Size: 262 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 38"><g fill="none" stroke="#eee7e8" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="30" r="4.5"/><circle cx="27" cy="30" r="4.5"/><path d="m10 26 19-22M25 26 6 4M11 27 17.5 19 24 27"/></g><circle cx="17.5" cy="19" r="1.4" fill="#eee7e8"/></svg>

After

Width:  |  Height:  |  Size: 338 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 34 46"><path fill="#eee7e8" fill-rule="evenodd" d="M7 2h20a4 4 0 0 1 4 4v34a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4V6a4 4 0 0 1 4-4Zm0 6v30h20V8H7Zm7-4v1h6V4h-6Zm3 35a1.6 1.6 0 1 0 0 3.2 1.6 1.6 0 0 0 0-3.2Z"/></svg>

After

Width:  |  Height:  |  Size: 260 B

+2
View File
@@ -0,0 +1,2 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 176 39"><image width="112" height="39" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHAAAAAnCAYAAADXa8wEAAAACXBIWXMAAAsSAAALEgHS3X78AAAJSElEQVR4nO1aa4xdVRWe6RStFGwBn9X4FqMkahQRlfoCajR9GCEKNhBNeCnFloKUl60PRMWoEU3EGoIYG3Ti4x8//NMfICYwKJqm0sflzr1z9lr7nP04575n5s6923xnzrk9nTLQmXvn2iF3JSvnnsfdZ+317bX2t/Y+Q0MDGchABjKQgQxkIANZPuKcG0516KUqe/bsWeGci3V0dHTEOXeC7t+/fyXUQZ2bV/FM0saSOMzNgoH2V7qxsdOcc8fp2Oy1WVuTPr2kwVtqgQN71M5wN0C40dGRXtjxvG3PDpJ40PZ9sGDEmkLhvJD5U9r3N5SC4HOa6DIj5ZWK6KpQymsM0Q0R841aiB2G6FbDfJsW4k7LfDtUCRGrYd5lib6upNxcq9XWZTvYK3sbUfSOSMrrDdH3NfMDIfNvDfPDhvl3huj3lugRyzxqmf9ipXzUEv3NSrnfCPEUj4+/NbGnZ05GtPeqrYW8NHaoJvqWm5lxjTB0bmrKuelph3PXarlupGatC6Ushb7/hF8obMi+cxG2DuO/xWLxFVWt9zWiqGqZZ22dT6anXbNadSXfd4ZoJpSyZoi8mtZvSNvs0n+xTek5HT78aqvUpkYYfjcIgg9009+TNgBHPTFxoatWd1uiu4wQu+LoIrpFE90cSrkDiqiDWim3xyrEdj2Pdp5hfnKmWnU1Y1xF61YgxDWL7VRqq2X+jWs2nRaiPVkqOcv8WEi00zJvi5i3GSGutUJs1USXW+aNivnTEdEHDfN5kZRvy+Vya3rkO6Ti2CbMs/BVWalD05VKPHaslLuT504bWq7i+/4ZFaX+3ogijP52FASNkpQfQccXMxcdOHDgZZaoZJhbzVqtXVLqEWZePdRHwUACYMnpsCoWP18KgvFGFCHb1EIp667ZnEEg9A3AZPKN2WPC3rrXfH4V2jZEO5FKDdEk0rMR4qcLnTPS6FNE51tmOMnVrUUUbsN1pNXOe8fGOgw0w0JHEka8aCY6B7ghLhYvqBrzV0Rb1VpPCbHFCnETMo6bnHRWiNuXdQSmzkIKw1xlmJuter0NspEpWU7KmWm0hr5/mSFqA0BD1EKaTMuJJe5LZ67LjY2tscw/rhrTxBxbNeb+YHz89bhniK6fqdVcu9FAf29b/gCOjo5ERB+C0w1R3GHL/LNFRGAMkBViKwZDBsAvZO8vQR+OIylg5FbK52J+VC4/az1vc9bGUMqbm5VKCuA3/28AdlLqSaTTsWzKSot81EJJugGpqGgNhzenq1WkvR3pfLYAe2KANNHVAC8BsKk8b1P2fo990Gmz5HnnWuZHwcqnymUXSXnf0WeeeU3GVzFIIG5gvTGAvn9r3wHs9WqFlfK9JaUO1K1tlpVCFLKemHjjQgvdTgQSXR35fgpgO/T9y7P3ex11+Xx+bVnr3ZY5rlXAeK3vXzTXrvQI5g4AW/U6bLylrwBmU0Xo++/XQnzDEv3QEN1vhfiVFeIBy/zrVDXRXsu81xI9aIgeiotoFNDM+zTRHzBiMe9hNIJwYG4IpbwW7YNQLNC2WQdJeV0pCODIOIViTsze74EPOmndEH22HARPxcARVTXzTand8BXm8Lm+A3FBGdHuN4CdGisI1lsp/1lSqurabYd6C47HiIJRAANHnLdqtfgecj6MniqVHKg0tGpM6mRliHKG+elunJ0Z4dctBYDZqJO53GvLvv8g+og52zCP6qTwn6+G7QDIfEcMYD9TaPpyRF0o5QwMgHMs80+MEFcaIT4TSXlJJOWlkRCXas+7REl5MZbcYoZJ9AlL9HGkFiPEx4zvf1QXixf4ExPvC4Lg3KhQOCvrqEXauCQAOgCXKQ0s0VUVrXNxaWDMESvl1vReMrcPv8iK1p7pctm5fpKYNG1oon2IqFIQtLBe2ON3xDVYN//H0RDd0CsAs/ZYId5khPgT6lT4IPL9vXz0aEpSXnRHpRMEzN9DJuobgJ2lNK1faZmfboRhG+kPaSMxHOxyZDHabeG8VAC6DIHC4MVCgyWK179KSv0nYN6YPgumnM53mS2pEzRh4SOG6N4UQCXErsQXL0990a0f5nVMNLvCUY5XOMIQznko7ezQKSAZFvq18jEAu2KhZWuR+v+REI7pmjH3gHV2Y2fIfE8MIOpdKbcP9c0xhcJ6I8QMnJIA+PNTCMDhZISvwJZWFASdOtB63saTBTCNPM/zzkH/6mE4U1YKpGsKi/f5fH6VUupM3Le53BpkJafUmfgd5vNrC4XCWVEUQc8uC/EqpdS6kPktfqHwduV577JCXIQyA20263UMsr3gAVhE10TvnpiYeGc2CnuyZ5h2XDF/0jK3LfMMWKQR4ju4nqXKvZRknTJdADhOkbZSTZ7rpGFLdBe2vGIAhahGSp2ftPeCduL/aV8M85+RLrGTgb6iMLdEwjIfsURHDdFhy/ysZf6vZT6IoyE6hOuG6IhlxjPPGfxHysAwG2QvtJUMfrTnKkrFg8wQVQ1RZbJcrlohtsCG7JpqV5I2pDzvYswt2DOrhyGAvCPu+BIBuFBxzKvBcEMpD1SMaYFoWSJG1Jx0G8eY4t3NSuUxy/y4YX7SMo8ZZgCUwzKZIcpbooJlLlrmCewf4pic43rBMI9bKfPxkaiohRiPQWWeRBZDMBiimmXOJcAfqoXhv8Hkk8E4bJi/WI2ix0tSXrhoJp0FMJSyZYgayOEh81eyne6FdIhDPr9KM3/VSnknmBo2ky3R3SHzt42U91qi+1I1RL8wzNhdfwIDbLJUckhRmGNCKXf20sYkpa1E5GNgYIcDW1XYEnO+fwZ+CyFOj3c+8vlVOEd6LR08eE4YhmtrSq0zRP+ajKLZOZDoj7juhDg93ZnJ2ouvHZrN5l6sVi1mcWPuIvH69MWoYzTRe3rpnGxbdaI3W6Iy0hgoe+cLAGizeUxxPjUVLxrECwNEFU2kp8rlfCkIfrRY++KUfGzNdkUvs4wW4pd1Y3i6UhGW6AcnvLfXnKLzqR3zau15u12z+bAS4svZ+71+H45Wyi2W+UbDfEXEfAXeaZi/hFGJecIyb0pVed5mLcQGzfxhfLOT1q0u2Q3vlV1dagwObKvm868rFotnz33med6ZllxLUl6cCuxzXnFL+Z3JcpXsd599elecxubqCywOrFgOwL3kPx4eyEAGMpCBDGQgAxnIQAYykIEM9VP+B3Kbo7r/ZPHRAAAAAElFTkSuQmCC"/><svg x="128" y="1" width="46" height="35" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 36"><path fill="#eee7e8" d="M11 5c4 0 5 2 13 2s9-2 13-2c5 0 7 5 8 11l2 12c.5 4-4 6-7 3l-8-7H16l-8 7c-3 3-7.5 1-7-3l2-12C4 10 6 5 11 5Z"/><g fill="#292529"><path d="M10 11h4v4h4v4h-4v4h-4v-4H6v-4h4v-4Z"/><circle cx="36" cy="13" r="1.5"/><circle cx="40" cy="17" r="1.5"/><circle cx="32" cy="17" r="1.5"/><circle cx="36" cy="21" r="1.5"/><rect x="20" y="10" width="8" height="7" rx="1.5"/></g></svg>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 38"><path fill="#eee7e8" d="m6 25 18-19 8 8-18 19-11 3 3-11Zm20-21 2-2a3 3 0 0 1 4 0l3 3a3 3 0 0 1 0 4l-2 3-7-8Z"/><path fill="#151315" d="m5 33 3-1-2-2-1 3Z"/></svg>

After

Width:  |  Height:  |  Size: 223 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><path fill="#eee7e8" fill-rule="evenodd" d="m16 2 8 0 1.3 5.3 3.4 2 5.2-1.5 4 6.9-4 3.8v4l4 3.8-4 6.9-5.2-1.5-3.4 2L24 39h-8l-1.3-5.3-3.4-2-5.2 1.5-4-6.9 4-3.8v-4l-4-3.8 4-6.9 5.2 1.5 3.4-2L16 2Zm4 11a7.5 7.5 0 1 0 0 15 7.5 7.5 0 0 0 0-15Z"/></svg>

After

Width:  |  Height:  |  Size: 309 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 34 44"><g fill="#eee7e8"><path fill-rule="evenodd" d="M9 2h16v14H9V2Zm3 3v4h3V5h-3Zm7 0v4h3V5h-3Z"/><path d="M7 17h20v19a6 6 0 0 1-6 6h-8a6 6 0 0 1-6-6V17Z"/><rect x="12" y="12" width="10" height="8" rx="1"/></g></svg>

After

Width:  |  Height:  |  Size: 272 B

+28
View File
@@ -0,0 +1,28 @@
Current original-work grant (2026-10-26): Original Author: nueex -
Copyright (C) 2026 nueex. The maintainer confirms ownership of original project
UI/design, authored geometric icons, launcher and title artwork; those original
contributions are offered under GPL-3.0-or-later. See ../../LICENSING.md and
../../LICENSE_SCOPE.json for exact file/contribution scope and the preserved
historical MIT grant. Third-party fonts, PS4/Sony branding, Bloodborne artwork
and unresolved demo scenery are excluded. Composite assets and screenshots
are not licensed as wholly original works. Pixels and renderer data are unchanged.
The following earlier provenance/review notes are retained as historical
records. The ownership confirmation resolves the earlier uncertainty about
nueex's original project artwork only; it grants no rights in external elements.
Derived from user-supplied psmtpResource images. Rights to supplied artwork remain with its owners; no application MIT licensing of that artwork is asserted. Only isolated assets and thumbnail excerpts are extracted. Complete screenshots are not used as production UI backgrounds.
As of 0.8.1, folder, document, device, Settings, Home, action and gamepad
pictograms are authored geometric SVGs in assets/icons/, covered by the
application's MIT license. Supplied wordmarks, launcher artwork and QA-only
thumbnail excerpts remain subject to the original notice above. The PS4
wordmark is preserved within the platform asset; its gamepad is replaced.
Licensing follow-up (2026-10-26): "QA-only" describes the intended demo use,
not exclusion from distributed code. The three thumbnail arrays are present in
src/ps4/reference_assets.inc and referenced by the renderer's asset table.
Their rights, the supplied title/launcher and the PS4 wordmark remain unresolved.
assets/explorer.png also depicts these materials. See ../../LICENSING.md
and ../../LICENSING_ASSETS.json for the clearance/replacement map. No new
license grant or public redistribution permission is asserted by this notice.
Binary file not shown.

After

Width:  |  Height:  |  Size: 852 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 945 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 707 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

+233
View File
@@ -0,0 +1,233 @@
{
"source": "psmtpResource",
"references": {
"1792455114194.jpg": {
"size": [
2752,
1536
],
"sha256": "22a1f3eed16f4dd6d6459f46a629e96827963acf60a1ba287c80748f067dabe3"
},
"1792495732103.jpg": {
"size": [
2728,
1536
],
"sha256": "4d9df254bc71141818a2e4adac6a129b5b548b8f927a387775172e1caed8bc9d"
},
"1792496064015.jpg": {
"size": [
2752,
1536
],
"sha256": "7811ffa78c7bdc390157f6e4baef539dc2cd7350db1077bd79b602720d73f175"
},
"app icon.jpg": {
"size": [
2048,
2048
],
"sha256": "e835a43d9348aa124a46010264e848469533920bfee912f5ff0a26681ca00048"
}
},
"assets": {
"wordmark": {
"source": "1792455114194.jpg",
"box_display_2048": [
68,
43,
675,
94
],
"size": [
570,
48
],
"mode": "white",
"sha256": "caef162d0d94c80daf663746955a30261192f6cbce87cbc2adedb355ed66fd92"
},
"platform": {
"source": "assets/icons/platform.svg",
"size": [
176,
39
],
"mode": "clean-vector",
"svg_sha256": "f3d1e528885b49a511c2f0bda77595186ac406988efa360a7fba0f9176e08d11",
"sha256": "32f960b8b317254d65cd02e63b55a62645ae05f1589baedd3b93e13571a69793"
},
"folder": {
"source": "assets/icons/folder.svg",
"size": [
192,
156
],
"mode": "clean-vector",
"svg_sha256": "b37c0bfb6f3e1506316f3174dc295480486256098abf9064643f8e9313019234",
"sha256": "7541a857f2979c70ddba70401407639d80c20056667f002edf136f61aa694c04"
},
"phone": {
"source": "assets/icons/phone.svg",
"size": [
34,
46
],
"mode": "clean-vector",
"svg_sha256": "b12ba96c63a7874edba674deb203880c950aeda792bc0d08efb73840a7125c18",
"sha256": "8f9a8d946e64be08f77e770d562ec932c6fbcbe8124e7d53bee41c76c5a34377"
},
"usb": {
"source": "assets/icons/usb.svg",
"size": [
34,
44
],
"mode": "clean-vector",
"svg_sha256": "d21b783e29def09b02163a1f1c88c582039daccc1bcc5580fc906bd097668d6a",
"sha256": "dae969afc408a95e4bf2cff4241d2d5dade9f43b499d1fe7b5973b8a94e9fc78"
},
"drive": {
"source": "assets/icons/drive.svg",
"size": [
40,
40
],
"mode": "clean-vector",
"svg_sha256": "bb0798b93f3a590b2c75aec682f96743d53eb6a23a9211ccfca8d2368f97dd18",
"sha256": "8f6a63311a4c57ede7f03b9bba03b1c1990cd290483457996b17efdc5d5ef49c"
},
"settings": {
"source": "assets/icons/settings.svg",
"size": [
40,
40
],
"mode": "clean-vector",
"svg_sha256": "0679779afd54a42f0a6db6b31bb78b1553b6b6161409d4d0cb6ef610f62eb486",
"sha256": "7c1dec749eb902fbac81a353d0ceebe0331f27ab6860bb3ae7130cdcbe4e011c"
},
"home": {
"source": "assets/icons/home.svg",
"size": [
34,
35
],
"mode": "clean-vector",
"svg_sha256": "27b4370dadf7243c24c396aec21f85b32d8ad372a82ed6ee2391f75840d367b9",
"sha256": "f4a800f40907e444665ca155a863034f1ae3f8f0e27e330c1af1dbf13e6da8fe"
},
"copy": {
"source": "assets/icons/copy.svg",
"size": [
36,
39
],
"mode": "clean-vector",
"svg_sha256": "df07d599b7e076c168183582eb8d49ea979eb8440c3bab1378e869f1e7e0a961",
"sha256": "9f6155c893ad5528a207698f6a5cf73a2ad085813dc351c3aa75dca6eb622623"
},
"move": {
"source": "assets/icons/move.svg",
"size": [
36,
38
],
"mode": "clean-vector",
"svg_sha256": "d01aaba940c28627bf19d078d04073ef8730a3fdcae435cb700be947cc4da6ca",
"sha256": "1c1cba3b46bc3beda67ba4d60800d07f7c1b925e51af89e34697dc464c2b67ab"
},
"rename": {
"source": "assets/icons/rename.svg",
"size": [
36,
38
],
"mode": "clean-vector",
"svg_sha256": "ab3f92b4a2f2d00dfed4fae0aa1c2fbec9e6af4de96089e77f0de53dd47c20cf",
"sha256": "71a95317b3f9b52039387de4ac091d51f88ab0ec8012a2cbb9203a3782501fa6"
},
"delete": {
"source": "assets/icons/delete.svg",
"size": [
36,
39
],
"mode": "clean-vector",
"svg_sha256": "a721f0e2cd4830567bcfc2ef6acbd495f9c085b3bccf9e6c3085dcba27db3055",
"sha256": "272f4c162993eca08fd61740cc31206e296383012a42c5bc491f17e46155464d"
},
"info": {
"source": "assets/icons/info.svg",
"size": [
39,
40
],
"mode": "clean-vector",
"svg_sha256": "1b30809f9131aaba8c0be00a394d673983256dbd45973fb35934517efbc96bc1",
"sha256": "f3f71f0e2daf284bba3cf1f13fe0c007a7127817fcdfb7f7cf5afd3c356d85a1"
},
"document": {
"source": "assets/icons/document.svg",
"size": [
81,
110
],
"mode": "clean-vector",
"svg_sha256": "b0463604b0faa0a4006c39e674603d7af6b8d9820c394298f74a48218863513c",
"sha256": "cc86a1a51fc078b6ad2b9af99c77617b815a9a5fd500fc847b21dd24581c816c"
},
"thumb_pkg": {
"source": "1792455114194.jpg",
"box_display_2048": [
490,
754,
644,
885
],
"size": [
178,
132
],
"mode": "rgba",
"sha256": "e350d2e16463112ebc7de154e881ad67e039ef97c41a0d36984499fde5126c63"
},
"thumb_image": {
"source": "1792455114194.jpg",
"box_display_2048": [
1000,
754,
1226,
885
],
"size": [
218,
132
],
"mode": "rgba",
"sha256": "f6aad5cf7e477d8399092d1cbde9bb73a37619d13bd73ae1a8c50118f0769921"
},
"thumb_video": {
"source": "1792455114194.jpg",
"box_display_2048": [
1288,
754,
1516,
885
],
"size": [
218,
132
],
"mode": "rgba",
"sha256": "749f5e6a4b211c4cead7aee8e082ff7749fa6492c85f839ba95d290c83ac1f07"
},
"icon0": {
"source": "app icon.jpg",
"size": [
512,
512
],
"sha256": "64148659f9a5f245940f022acfe57734de96e16b59bc2c66a594355ae12864f1"
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 738 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because it is too large. Load diff
+19
View File
@@ -0,0 +1,19 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#ifndef PSMTP_BROWSER_H
#define PSMTP_BROWSER_H
#include "mtp.h"
#include <stdio.h>
int mtp_browser(mtp_client *, FILE *, FILE *, int);
void mtp_print_name(FILE *, const char *);
#endif
+46
View File
@@ -0,0 +1,46 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#ifndef PSMTP_COPY_H
#define PSMTP_COPY_H
#include "psmtp/mtp.h"
#include <stdio.h>
typedef struct {
mtp_object_info info;
uint32_t handle;
uint64_t size;
int extended,size_property,is_pkg,streamed,partial_supported;
} mtp_copy_source;
enum mtp_copy_phase { MTP_COPY_CONFIRM, MTP_COPY_RECEIVE, MTP_COPY_LOCAL, MTP_COPY_SOURCE, MTP_COPY_PUBLISH, MTP_COPY_PREPARE };
typedef int (*mtp_copy_progress)(void *,enum mtp_copy_phase,uint64_t,uint64_t);
#define MTP_COPY_SPACE_STREAMING 1
/* Backend owns its temporary file. Callbacks return mtp_error except the documented space reservation result.
* publish must NEVER replace a pre-existing target. A nonzero return may
* still have published: set *published=1 immediately after atomic publication.
* dispose removes ONLY the owned temporary entry, never the published file. */
typedef struct {
void *user;
int (*begin)(void *,const mtp_copy_source *);
int (*space)(void *,uint64_t *);
int (*write)(void *,const uint8_t *,size_t,size_t *);
int (*sync)(void *);
int (*rewind)(void *);
int (*read)(void *,uint8_t *,size_t,size_t *);
int (*publish)(void *,int *published);
int (*dispose)(void *);
int (*prepare)(void *,mtp_copy_progress,void *);
} mtp_copy_sink;
typedef struct { uint8_t digest[32]; int published,cleanup_failed; } mtp_copy_result;
int mtp_copy_probe(mtp_client *,uint32_t,const mtp_object_info *,mtp_copy_source *,FILE *);
int mtp_copy_run_quick(mtp_client *,const mtp_copy_source *,const mtp_copy_sink *,mtp_copy_progress,void *,mtp_copy_result *,FILE *);
int mtp_copy_run(mtp_client *,const mtp_copy_source *,const mtp_copy_sink *,mtp_copy_progress,void *,mtp_copy_result *,FILE *);
#endif
+15
View File
@@ -0,0 +1,15 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#ifndef PSMTP_COPY_PATHS_H
#define PSMTP_COPY_PATHS_H
#include "psmtp/log_paths.h"
#define PSMTP_COPY_DIRECTORY PSMTP_LOG_DIRECTORY "/copies"
#endif
+23
View File
@@ -0,0 +1,23 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#ifndef PSMTP_COPY_PLAN_H
#define PSMTP_COPY_PLAN_H
#include "copy.h"
#define MTP_PLAN_ITEMS 512u
#define MTP_PLAN_DEPTH 10u
typedef struct { mtp_copy_source source; size_t parent,depth; int folder; } mtp_plan_item;
typedef struct {
mtp_plan_item items[MTP_PLAN_ITEMS];
size_t count,files,folders;
uint64_t bytes;
} mtp_copy_plan;
typedef int (*mtp_plan_progress)(void *,size_t,size_t);
int mtp_copy_plan_build(mtp_copy_plan *,mtp_client *,uint32_t,const mtp_object_info *,mtp_plan_progress,void *,FILE *);
#endif
+19
View File
@@ -0,0 +1,19 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#ifndef PSMTP_DEVICE_SOUND_H
#define PSMTP_DEVICE_SOUND_H
#include <stddef.h>
#include <stdint.h>
#define PSMTP_CHIME_RATE 48000u
#define PSMTP_CHIME_FRAMES 24000u
#define PSMTP_CHIME_BLOCK 256u
void psmtp_chime_render(int connected,size_t frame,int16_t *,size_t frames);
#endif
+25
View File
@@ -0,0 +1,25 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#ifndef PSMTP_DEVICE_WATCH_H
#define PSMTP_DEVICE_WATCH_H
#include "diagnostic.h"
#define PSMTP_WATCH_MAX 128u
#define PSMTP_DEVICE_POLL_MS 100u
typedef struct {uint8_t bus,address;uint16_t vendor,product;uint32_t signature;} psmtp_device_record;
typedef struct {psmtp_device_record record;uint64_t connection,retry_at,name_retry_at;int ready,mtp;unsigned name_attempts;char name[96];} psmtp_watched_device;
typedef struct {psmtp_watched_device entries[PSMTP_WATCH_MAX];size_t count;uint64_t next_connection;} psmtp_device_watch;
/* probe returns 1=MTP, 0=another device, -1=temporarily unavailable.
* Quiet polls classify at most one new/retry device; force classifies all. */
int psmtp_device_watch_update(psmtp_device_watch *,const psmtp_device_record *,size_t,uint64_t,int,int (*)(void *,size_t),void *,psmtp_ps4_candidate *);
#define PSMTP_DEVICE_ADDED 1u
#define PSMTP_DEVICE_REMOVED 2u
unsigned psmtp_device_changes(const psmtp_ps4_candidate *,size_t,unsigned,const psmtp_ps4_candidate *,size_t,unsigned);
#endif
+32
View File
@@ -0,0 +1,32 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#ifndef PSMTP_DIAG_LOG_H
#define PSMTP_DIAG_LOG_H
#include <stddef.h>
#include <stdint.h>
#define DIAG_LOG_LINES 2048u
#define DIAG_LOG_COLUMNS 96u
/* Receives each completed, wrapped ASCII screen line exactly once.
* The callback must not re-enter this log. Scrolling never calls it. */
typedef void (*diag_log_line_sink)(void *, const char *, size_t);
typedef struct {
char lines[DIAG_LOG_LINES][DIAG_LOG_COLUMNS+1];
uint64_t first, current;
size_t column;
diag_log_line_sink sink;
void *sink_user;
} diag_log;
void diag_log_init(diag_log *);
void diag_log_write(diag_log *, const char *, size_t);
void diag_log_set_sink(diag_log *, diag_log_line_sink, void *);
void diag_log_finish_line(diag_log *);
uint64_t diag_log_end(const diag_log *);
const char *diag_log_line(const diag_log *, uint64_t);
#endif
+38
View File
@@ -0,0 +1,38 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#ifndef PSMTP_DIAGNOSTIC_H
#define PSMTP_DIAGNOSTIC_H
#include "mtp.h"
#include <stdio.h>
#define MTP_DIAG_STORAGES 4u
#define MTP_DIAG_OBJECTS 32u
int mtp_diagnose_roots(mtp_client *, FILE *, int names);
typedef struct {
uint16_t vendor_id,product_id;
uint8_t mtp;
uint64_t connection;
char name[96];
} psmtp_ps4_candidate;
typedef struct {
int communicate, names;
void *user;
void (*devices)(void *,const psmtp_ps4_candidate *,size_t);
int (*choose_device)(void *, const psmtp_ps4_candidate *, size_t);
int (*browse)(void *,mtp_client *,FILE *);
} psmtp_ps4_options;
int psmtp_ps4_diagnose(const psmtp_ps4_options *, FILE *);
int psmtp_ps4_poll_devices(psmtp_ps4_candidate *,size_t *,int,FILE *);
void psmtp_ps4_usb_shutdown(FILE *);
#endif
+19
View File
@@ -0,0 +1,19 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#ifndef PSMTP_KEYBOARD_H
#define PSMTP_KEYBOARD_H
#include <stddef.h>
enum {PSMTP_KEY_CHARACTER,PSMTP_KEY_BACKSPACE,PSMTP_KEY_SHIFT,PSMTP_KEY_CLEAR,PSMTP_KEY_SAVE};
#define PSMTP_KEY_COUNT 44u
int psmtp_key_action(size_t);
char psmtp_key_character(size_t,int);
size_t psmtp_key_move(size_t,int,int);
void psmtp_key_rect(size_t,int *,int *,int *,int *);
#endif
+29
View File
@@ -0,0 +1,29 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#ifndef PSMTP_LOCATIONS_H
#define PSMTP_LOCATIONS_H
#include "diagnostic.h"
#define PSMTP_LOCATION_MAX 137u
#define PSMTP_USB_SLOTS 8u
enum { PSMTP_LOCATION_PHONE, PSMTP_LOCATION_INTERNAL, PSMTP_LOCATION_USB };
typedef struct {
int kind; unsigned slot; uint16_t vendor_id,product_id;
char label[96],root[256];
uint64_t connection;
int named;
} psmtp_location;
typedef struct {
psmtp_location entries[PSMTP_LOCATION_MAX]; size_t count,active,selected;
} psmtp_locations;
void psmtp_locations_build(psmtp_locations *,const psmtp_ps4_candidate *,size_t,unsigned);
int psmtp_location_same(const psmtp_location *,const psmtp_location *);
int psmtp_locations_move(psmtp_locations *,int);
int psmtp_location_phone_index(const psmtp_location *,const psmtp_ps4_candidate *,size_t);
#endif
+15
View File
@@ -0,0 +1,15 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#ifndef PSMTP_LOG_PATHS_H
#define PSMTP_LOG_PATHS_H
#define PSMTP_LOG_DIRECTORY "/data/psmtp"
#define PSMTP_SCAN_LOG_PATH PSMTP_LOG_DIRECTORY "/psmtp-scan.log"
#define PSMTP_DIAGNOSTIC_LOG_PATH PSMTP_LOG_DIRECTORY "/psmtp-diag.log"
#endif
+24
View File
@@ -0,0 +1,24 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#ifndef PSMTP_METADATA_H
#define PSMTP_METADATA_H
#include <stddef.h>
#include <stdint.h>
typedef struct {void *user;uint64_t size;int (*read)(void *,uint64_t,uint8_t *,size_t);size_t consumed;} psmtp_meta_reader;
typedef struct {
unsigned width,height;uint64_t duration_ms,items;int items_known,loaded,error;
char title[128],content_id[64],title_id[32],version[32],codec[24];
uint32_t *image;unsigned image_width,image_height;
uint64_t revision;
} psmtp_metadata;
int psmtp_metadata_load(psmtp_meta_reader *,const char *,psmtp_metadata *);
int psmtp_metadata_image(const uint8_t *,size_t,psmtp_metadata *);
void psmtp_metadata_free(psmtp_metadata *);
#endif
+119
View File
@@ -0,0 +1,119 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#ifndef PSMTP_MTP_H
#define PSMTP_MTP_H
#include <stddef.h>
#include <stdint.h>
#define MTP_MAX_CONTAINER (1024u * 1024u)
#define MTP_MAX_ITEMS 4096u
#define MTP_MAX_STORAGES 64u
#define MTP_MAX_OPS 512u
#define MTP_STRING_BYTES 1024u
#define MTP_RX_BYTES 16384u
#define MTP_ROOT UINT32_C(0xffffffff)
#define MTP_COPY_CHUNK 65536u
#define MTP_STREAM_CHUNK (256u*1024u)
#define MTP_UPLOAD_STANDARD_MAX_SIZE UINT64_C(4294967282)
#define MTP_UPLOAD_MAX_SIZE (UINT64_MAX/8)
enum mtp_error {
MTP_OK = 0, MTP_IO = -1, MTP_TIMEOUT = -2, MTP_DISCONNECTED = -3,
MTP_PROTOCOL = -4, MTP_LIMIT = -5, MTP_MEMORY = -6, MTP_RESPONSE = -7,
MTP_STATE = -8, MTP_UNSUPPORTED = -9, MTP_CANCELLED = -10, MTP_VERIFY = -11,
MTP_FILESYSTEM = -12, MTP_NO_SPACE = -13, MTP_SPACE_UNKNOWN = -14, MTP_EXISTS = -15
};
/* Return an mtp_error; set actual even on failure. No transfer replay on error.
* read() must accept packet-aligned MTP_RX_BYTES requests. now_ms is monotonic.
* Adapter owns handles; client never resets/detaches/closes the USB transport. */
typedef struct {
void *user;
int (*read)(void *, uint8_t *, size_t, size_t *, uint32_t);
int (*write)(void *, const uint8_t *, size_t, size_t *, uint32_t);
uint64_t (*now_ms)(void *);
} mtp_transport;
typedef struct {
mtp_transport io;
uint8_t rx[MTP_RX_BYTES];
size_t rx_pos, rx_len;
uint32_t next_tid, last_tid, timeout_ms;
uint16_t last_op, response;
uint32_t response_params[5]; size_t response_param_count;
uint16_t out_max_packet;
int session, poisoned,content_read,session_cleanup_required,session_recovered;
int background_io;int32_t background_native_error;
} mtp_client;
typedef struct {
uint8_t identity[32]; int identity_valid;
uint16_t standard_version, vendor_version, functional_mode;
uint32_t vendor_extension;
uint16_t operations[MTP_MAX_OPS];
size_t operation_count;
char vendor_description[MTP_STRING_BYTES];
char manufacturer[MTP_STRING_BYTES], model[MTP_STRING_BYTES];
char version[MTP_STRING_BYTES];
} mtp_device_info;
typedef struct {
uint16_t type, filesystem, access;
uint64_t capacity, free_bytes;
uint32_t free_objects;
char description[MTP_STRING_BYTES], label[MTP_STRING_BYTES];
} mtp_storage_info;
typedef struct {
uint32_t storage, size32, parent, association_desc, sequence;
uint16_t format, protection, association;
uint32_t image_width,image_height;
char name[MTP_STRING_BYTES];
char modified[MTP_STRING_BYTES];
} mtp_object_info;
void mtp_init(mtp_client *, mtp_transport);
const char *mtp_error_string(int);
int mtp_get_device_info(mtp_client *, mtp_device_info *);
int mtp_supports(const mtp_device_info *, uint16_t);
int mtp_open_session(mtp_client *, uint32_t);
int mtp_close_session(mtp_client *);
int mtp_get_storage_ids(mtp_client *, uint32_t *, size_t, size_t *);
int mtp_get_storage_info(mtp_client *, uint32_t, mtp_storage_info *);
int mtp_get_handles(mtp_client *, uint32_t, uint32_t, uint32_t *, size_t, size_t *);
int mtp_get_object_info(mtp_client *, uint32_t, mtp_object_info *);
int mtp_delete_object(mtp_client *,uint32_t);
int mtp_get_object_size(mtp_client *,uint32_t,uint64_t *);
int mtp_get_partial(mtp_client *,uint32_t,uint64_t,size_t,int extended,uint8_t *,size_t *);
typedef int (*mtp_download_consume)(void *,const uint8_t *,size_t);
typedef int (*mtp_download_progress)(void *,uint64_t,uint64_t);
typedef struct {uint64_t read_calls,read_bytes,io_ms;} mtp_download_stats;
int mtp_get_object_stream(mtp_client *,uint32_t,uint64_t,uint8_t *,size_t,mtp_download_consume,mtp_download_progress,void *,mtp_download_stats *);
int mtp_rename_object(mtp_client *,uint32_t,uint16_t,const char *);
typedef int (*mtp_upload_read)(void *,uint8_t *,size_t);
typedef int (*mtp_upload_progress)(void *,uint64_t,uint64_t);
int mtp_send_object_info(mtp_client *,uint32_t,uint32_t,const char *,uint64_t,int,uint32_t *);
int mtp_send_object(mtp_client *,uint64_t,mtp_upload_read,mtp_upload_progress,void *);
int mtp_get_thumbnail(mtp_client *,uint32_t,uint8_t **,size_t *);
int mtp_parse_device(const uint8_t *, size_t, mtp_device_info *);
int mtp_parse_storage(const uint8_t *, size_t, mtp_storage_info *);
int mtp_parse_object(const uint8_t *, size_t, mtp_object_info *);
int mtp_parse_ids(const uint8_t *, size_t, uint32_t *, size_t, size_t *);
int mtp_decode_string(const uint8_t *, size_t, char *, size_t, size_t *);
#endif
+16
View File
@@ -0,0 +1,16 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#ifndef PSMTP_NAME_H
#define PSMTP_NAME_H
#include <stddef.h>
#include <stdint.h>
int psmtp_name_encode(const char *,uint8_t *,size_t,size_t *);
int psmtp_name_valid(const char *);
#endif
+47
View File
@@ -0,0 +1,47 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#ifndef PSMTP_NAVIGATOR_H
#define PSMTP_NAVIGATOR_H
#include "mtp.h"
#include <stdio.h>
#define MTP_NAV_PAGE 16u
#define MTP_NAV_DEPTH 32u
typedef struct { uint32_t handle; mtp_object_info info; uint64_t bytes; int exact_size; } mtp_nav_entry;
typedef struct { uint32_t parent; size_t selected; char folder[MTP_STRING_BYTES]; } mtp_nav_position;
typedef struct {
mtp_client *client;
FILE *log;
uint32_t storages[MTP_MAX_STORAGES],handles[MTP_MAX_ITEMS],parent;
size_t storage_count,storage_index,count,selected,page_start,page_count,depth;
mtp_storage_info storage;
char folder_name[MTP_STRING_BYTES];
mtp_nav_entry entries[MTP_NAV_PAGE];
mtp_nav_position history[MTP_NAV_DEPTH];
int size64_supported;
int sort_mode;
int rename_supported,thumbnail_supported,partial_supported,partial64_supported;
char device_name[96];uint8_t device_identity[32];int identity_valid;
mtp_nav_entry *catalog;
int (*scan_progress)(void *,size_t,size_t);
void *scan_user;
} mtp_navigator;
int mtp_is_folder(const mtp_object_info *);
int mtp_nav_open_ex(mtp_navigator *,mtp_client *,FILE *,int (*)(void *,size_t,size_t),void *);
int mtp_nav_open(mtp_navigator *,mtp_client *,FILE *);
int mtp_nav_move(mtp_navigator *,int delta);
int mtp_nav_enter(mtp_navigator *);
int mtp_nav_back(mtp_navigator *);
int mtp_nav_storage(mtp_navigator *,size_t index);
int mtp_nav_refresh(mtp_navigator *);
int mtp_nav_sort(mtp_navigator *,int);
int mtp_nav_close(mtp_navigator *);
const mtp_nav_entry *mtp_nav_selected(const mtp_navigator *);
#endif
+20
View File
@@ -0,0 +1,20 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#ifndef PSMTP_PKG_H
#define PSMTP_PKG_H
#include "mtp.h"
typedef struct {void *user;uint64_t size;int (*read)(void *,uint64_t,uint8_t *,size_t);} psmtp_pkg_source;
typedef struct {uint64_t size,install_bytes;uint32_t type,flags;int patch;char content_id[37],title_id[10],package_type[6];uint8_t header_digest[32];} psmtp_pkg_info;
int psmtp_pkg_name(const char *);
int psmtp_pkg_inspect(const psmtp_pkg_source *,psmtp_pkg_info *);
typedef int (*psmtp_pkg_send)(void *,const uint8_t *,size_t);
int psmtp_pkg_http(const psmtp_pkg_source *,const char *,size_t,const char *,psmtp_pkg_send,void *);
#endif
+18
View File
@@ -0,0 +1,18 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#ifndef PSMTP_SHA256_H
#define PSMTP_SHA256_H
#include <stdint.h>
#include <stddef.h>
typedef struct { uint32_t h[8]; uint64_t bytes; uint8_t block[64]; size_t used; } psmtp_sha256;
void psmtp_sha256_init(psmtp_sha256 *);
void psmtp_sha256_update(psmtp_sha256 *,const void *,size_t);
void psmtp_sha256_final(psmtp_sha256 *,uint8_t digest[32]);
#endif
+21
View File
@@ -0,0 +1,21 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#ifndef PSMTP_TRANSFER_METER_H
#define PSMTP_TRANSFER_METER_H
#include <stdint.h>
typedef struct {uint64_t time,bytes;} psmtp_transfer_sample;
typedef struct {
psmtp_transfer_sample samples[24];unsigned count;
uint64_t total,last_done,last_time,last_change,remaining_seconds;
int active,phase,ready,eta_known;double bytes_per_second;
} psmtp_transfer_meter;
void psmtp_transfer_measure(psmtp_transfer_meter *,int,uint64_t,uint64_t,uint64_t);
void psmtp_transfer_text(const psmtp_transfer_meter *,char *,unsigned);
#endif
+21
View File
@@ -0,0 +1,21 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#ifndef PSMTP_UI_FORMAT_H
#define PSMTP_UI_FORMAT_H
#include <stddef.h>
#include <stdint.h>
void psmtp_ui_size(char *,size_t,uint64_t);
const char *psmtp_ui_kind(const char *,int);
int psmtp_ui_name_compare(const char *,const char *);
enum {PSMTP_SORT_NAME,PSMTP_SORT_NAME_DESC,PSMTP_SORT_SIZE,PSMTP_SORT_SIZE_DESC,PSMTP_SORT_NEWEST,PSMTP_SORT_OLDEST,PSMTP_SORT_TYPE,PSMTP_SORT_COUNT};
const char *psmtp_ui_sort_label(int);
int psmtp_ui_compare(int,const char *,int,uint64_t,const char *,const char *,int,uint64_t,const char *);
void psmtp_ui_extension(char out[12],const char *,int);
#endif
+16
View File
@@ -0,0 +1,16 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#ifndef PSMTP_USB_LABEL_H
#define PSMTP_USB_LABEL_H
#include <stdint.h>
#include <stddef.h>
int psmtp_usb_label(const uint8_t *,size_t,char *,size_t);
#endif
+28
View File
@@ -0,0 +1,28 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#ifndef PSMTP_USB_MODULE_H
#define PSMTP_USB_MODULE_H
#include <stdint.h>
#include <stdio.h>
typedef struct {
void *user;
int32_t (*is_loaded)(void *);
int32_t (*load)(void *);
int32_t (*find_handle)(void *, int32_t *);
int32_t (*unload)(void *);
} psmtp_usb_module_ops;
typedef struct { int32_t handle; int owns_reference; } psmtp_usb_module;
int psmtp_usb_module_acquire(psmtp_usb_module *, const psmtp_usb_module_ops *, FILE *);
int psmtp_usb_module_release(psmtp_usb_module *, const psmtp_usb_module_ops *, FILE *);
#endif
+28
View File
@@ -0,0 +1,28 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#ifndef PSMTP_USB_SELECT_H
#define PSMTP_USB_SELECT_H
#include <stddef.h>
#include <stdint.h>
#define USB_MAX_ALTS 32u
#define USB_MAX_ENDPOINTS 8u
#define USB_MAX_CONFIG_BYTES 4096u
typedef struct { uint8_t address, attributes; uint16_t max_packet; } usb_endpoint;
typedef struct {
uint8_t number, alternate, cls, subclass, protocol;
size_t endpoint_count;
usb_endpoint endpoints[USB_MAX_ENDPOINTS];
} usb_alt;
typedef struct { uint8_t interface_number, bulk_in, bulk_out, interrupt_in; uint16_t out_max_packet,in_max_packet; } usb_mtp_interface;
/* 0=unique standard still-image candidate; -1=none; -2=ambiguous; -3=invalid. */
int usb_select_mtp(const usb_alt *, size_t, usb_mtp_interface *);
int usb_parse_config(const uint8_t *, size_t, usb_alt *, size_t *, uint8_t *);
#endif
+13
View File
@@ -0,0 +1,13 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#ifndef PSMTP_VERSION_H
#define PSMTP_VERSION_H
#define PSMTP_VERSION "1.01"
#endif
+109
View File
@@ -0,0 +1,109 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#include "psmtp/browser.h"
#include "psmtp/navigator.h"
#include <inttypes.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
void mtp_print_name(FILE *out,const char *s) {
const unsigned char *p=(const unsigned char *)s;
fputc('"',out);
for(;*p;++p) {
if(p[0]==0xe2 && p[1] && p[2] && p[1]==0x80 && p[2]>=0xaa && p[2]<=0xae) {
fprintf(out,"\\u%04x",0x202a+p[2]-0xaa); p+=2;
} else if(p[0]==0xe2 && p[1] && p[2] && p[1]==0x81 && p[2]>=0xa6 && p[2]<=0xa9) {
fprintf(out,"\\u%04x",0x2066+p[2]-0xa6); p+=2;
} else if(p[0]==0xc2 && p[1]>=0x80 && p[1]<=0x9f) {
fprintf(out,"\\u%04x",0x80+p[1]-0x80); ++p;
} else if(*p<32 || *p==127 || *p=='"' || *p=='\\') fprintf(out,"\\x%02x",*p);
else fputc(*p,out);
}
fputc('"',out);
}
static void diagnostic(FILE *out,mtp_client *c,const char *stage,int e) {
fprintf(out,"stage=%s status=%d (%s) op=0x%04x tid=%" PRIu32 " response=0x%04x poisoned=%d\n",
stage,e,mtp_error_string(e),c->last_op,c->last_tid,c->response,c->poisoned);
}
static int parse_hex(char **s,uint32_t *v) {
char *end; unsigned long long x;
while(**s==' ' || **s=='\t') ++*s;
if(!**s || **s=='-' || **s=='+') return 0;
errno=0; x=strtoull(*s,&end,16);
if(end==*s || errno || x>UINT32_MAX) return 0;
*s=end; *v=(uint32_t)x; return 1;
}
static int end_line(const char *s) { return strspn(s," \t\r\n")==strlen(s); }
int mtp_browser(mtp_client *c,FILE *input,FILE *out,int names) {
mtp_device_info device; uint32_t storages[MTP_MAX_STORAGES],ids[MTP_MAX_ITEMS];
size_t ns=0,n=0,i,j; int e, result=0; char line[128];
const uint16_t required[]={0x1002,0x1003,0x1004,0x1005,0x1007,0x1008};
e=mtp_get_device_info(c,&device); diagnostic(out,c,"device-info",e); if(e) return e;
fprintf(out,"device-info standard=%u vendor-extension=0x%08" PRIx32 " operations=%zu (identity strings suppressed)\n",
device.standard_version,device.vendor_extension,device.operation_count);
for(i=0;i<sizeof(required)/sizeof(required[0]);++i) if(!mtp_supports(&device,required[i])) {
fprintf(out,"missing-operation=0x%04x\n",required[i]); return MTP_UNSUPPORTED;
}
e=mtp_open_session(c,1); diagnostic(out,c,"open-session",e); if(e) return e;
e=mtp_get_storage_ids(c,storages,MTP_MAX_STORAGES,&ns); diagnostic(out,c,"storage-ids",e);
if(e) { result=e; goto cleanup; }
for(i=0;i<ns;++i) {
mtp_storage_info si;
e=mtp_get_storage_info(c,storages[i],&si); diagnostic(out,c,"storage-info",e);
if(e) { result=e; goto cleanup; }
fprintf(out,"storage=%08" PRIx32 " capacity=%" PRIu64 " free=%" PRIu64 " access=%u\n",storages[i],si.capacity,si.free_bytes,si.access);
}
fprintf(out,"Commands: ls <storage-hex> <parent-hex>; root parent=ffffffff; q.\nNames %s. Handles are valid only during this session.\n",names?"enabled":"hidden (launch with --names to display)");
for(;;) {
char *s; uint32_t storage,parent; int known=0;
fprintf(out,"mtp> "); fflush(out);
if(!fgets(line,sizeof(line),input)) break;
if(!strchr(line,'\n') && !feof(input)) {
int ch; while((ch=fgetc(input))!='\n' && ch!=EOF) {} fprintf(out,"input too long\n"); continue;
}
if(line[0]=='q' && end_line(line+1)) break;
if(strncmp(line,"ls ",3)) { fprintf(out,"expected ls or q\n"); continue; }
s=line+3;
if(!parse_hex(&s,&storage) || !parse_hex(&s,&parent) || !end_line(s) || !parent) {
fprintf(out,"invalid storage/parent; parent 0 (all objects) is disabled\n"); continue;
}
for(i=0;i<ns;++i) if(storages[i]==storage) known=1;
if(!known) { fprintf(out,"unknown storage\n"); continue; }
if(parent!=MTP_ROOT) {
mtp_object_info folder;
e=mtp_get_object_info(c,parent,&folder); diagnostic(out,c,"selected-folder",e);
if(e) { result=e; goto cleanup; }
if(folder.storage!=storage || !mtp_is_folder(&folder)) {
fprintf(out,"parent is not a folder in selected storage\n"); continue;
}
}
e=mtp_get_handles(c,storage,parent,ids,MTP_MAX_ITEMS,&n); diagnostic(out,c,"object-handles",e);
if(e) { result=e; goto cleanup; }
fprintf(out,"items=%zu\n",n);
for(j=0;j<n;++j) {
mtp_object_info obj;
e=mtp_get_object_info(c,ids[j],&obj); diagnostic(out,c,"object-info",e);
if(e) { result=e; goto cleanup; }
if(obj.storage!=storage || (parent!=MTP_ROOT && obj.parent!=parent) ||
(parent==MTP_ROOT && obj.parent!=0 && obj.parent!=MTP_ROOT)) {
fprintf(out,"inconsistent storage/parent metadata\n"); result=MTP_PROTOCOL; goto cleanup;
}
fprintf(out,"handle=%08" PRIx32 " kind=%s format=%04x size32=%" PRIu32 "%s",
ids[j],mtp_is_folder(&obj)?"folder":"object",obj.format,obj.size32,
obj.size32==UINT32_MAX?" (size unknown or >=4GiB)":"");
if(names) { fputs(" name=",out); mtp_print_name(out,obj.name); }
fputc('\n',out);
}
}
cleanup:
e=mtp_close_session(c); diagnostic(out,c,"close-session",e);
return result?result:e;
}
+151
View File
@@ -0,0 +1,151 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#include "psmtp/copy.h"
#include "psmtp/sha256.h"
#include <string.h>
#include <stdlib.h>
#include <inttypes.h>
#include <ctype.h>
static int same(const mtp_object_info *a,const mtp_object_info *b) {
return a->storage==b->storage && a->parent==b->parent && a->format==b->format && a->size32==b->size32 &&
!strcmp(a->name,b->name) && !strcmp(a->modified,b->modified);
}
static int pkg(const char *s) {
size_t n=strlen(s); return n>=4 && s[n-4]=='.' && tolower((unsigned char)s[n-3])=='p' &&
tolower((unsigned char)s[n-2])=='k' && tolower((unsigned char)s[n-1])=='g';
}
int mtp_copy_probe(mtp_client *c,uint32_t id,const mtp_object_info *selected,mtp_copy_source *s,FILE *out) {
mtp_device_info *dev; int e; uint64_t size;
memset(s,0,sizeof(*s)); s->handle=id;
e=mtp_get_object_info(c,id,&s->info); if(e) return e;
if(s->info.format==0x3001 || !same(selected,&s->info)) return MTP_VERIFY;
dev=(mtp_device_info *)malloc(sizeof(*dev)); if(!dev) return MTP_MEMORY;
e=mtp_get_device_info(c,dev);
if(!e) {
s->size_property=mtp_supports(dev,0x9803);
s->extended=mtp_supports(dev,0x95c1);
s->streamed=mtp_supports(dev,0x1009);s->partial_supported=s->extended || mtp_supports(dev,0x101b);
if(!s->streamed && !s->partial_supported) e=MTP_UNSUPPORTED;
if(mtp_supports(dev,0x101b) && s->info.size32!=UINT32_MAX) s->extended=0;
}
free(dev); if(e) return e;
size=s->info.size32;
if(s->size_property) {
e=mtp_get_object_size(c,id,&size);
if(e==MTP_RESPONSE && s->info.size32!=UINT32_MAX) { s->size_property=0; size=s->info.size32; e=0; }
if(e) return e;
} else if(s->info.size32==UINT32_MAX) return MTP_UNSUPPORTED;
if(s->info.size32!=UINT32_MAX && size!=s->info.size32) return MTP_VERIFY;
if(size>=UINT32_MAX && !s->extended && !s->streamed) return MTP_UNSUPPORTED;
if(size>UINT64_MAX/8) return MTP_LIMIT;
s->size=size; s->is_pkg=pkg(s->info.name);
if(out) fprintf(out,"stage=copy-probe handle=%08" PRIx32 " bytes=%" PRIu64 " partial64=%d size64=%d stream=%d pkg=%d names-in-log=0\n",
id,size,s->extended,s->size_property,s->streamed,s->is_pkg);
return 0;
}
static int recheck(mtp_client *c,const mtp_copy_source *s) {
mtp_object_info info; uint64_t size; int e=mtp_get_object_info(c,s->handle,&info);
if(e) return e;
if(!same(&info,&s->info)) return MTP_VERIFY;
if(s->size_property) { e=mtp_get_object_size(c,s->handle,&size); if(e) return e; if(size!=s->size) return MTP_VERIFY; }
return 0;
}
static int capacity(const mtp_copy_sink *sink,uint64_t remaining) {
uint64_t available=0; int e=sink->space(sink->user,&available);
if(e==MTP_COPY_SPACE_STREAMING) return 0;
if(e) return e;
return available<remaining?MTP_LIMIT:0;
}
typedef struct {
mtp_client *client;const mtp_copy_source *source;const mtp_copy_sink *sink;
mtp_copy_progress progress;void *user;FILE *out;psmtp_sha256 sha;
uint64_t offset,logged,guarded,phone_ms,write_ms,hash_ms,read_ms,sync_ms,ui_ms,guard_ms,writes,requests;
uint8_t first[4096];size_t first_n;
} copy_context;
static uint64_t copy_now(copy_context *x){return x->client->io.now_ms?x->client->io.now_ms(x->client->io.user):0;}
static void copy_elapsed(copy_context *x,uint64_t before,uint64_t *total){uint64_t after=copy_now(x);if(after>=before)*total+=after-before;}
static int copy_report(copy_context *x,int phase){uint64_t before;int cancel;if(!x->progress)return 0;before=copy_now(x);cancel=x->progress(x->user,(enum mtp_copy_phase)phase,x->offset,x->source->size);copy_elapsed(x,before,&x->ui_ms);return cancel;}
static int stream_progress(void *v,uint64_t done,uint64_t total){copy_context *x=v;(void)done;(void)total;return copy_report(x,MTP_COPY_RECEIVE);}
static int copy_consume(void *v,const uint8_t *data,size_t n){
copy_context *x=v;size_t j,written=0;uint64_t before;int rc;
if(!n || n>x->source->size-x->offset)return MTP_VERIFY;
for(j=0;j<n && x->first_n<sizeof(x->first);++j)x->first[x->first_n++]=data[j];
if(x->source->is_pkg && x->first_n>=4 && memcmp(x->first,"\177CNT",4))return MTP_VERIFY;
while(written<n){size_t actual=0;before=copy_now(x);rc=x->sink->write(x->sink->user,data+written,n-written,&actual);copy_elapsed(x,before,&x->write_ms);++x->writes;
if(rc)return rc;if(!actual || actual>n-written)return MTP_IO;written+=actual;
}
before=copy_now(x);psmtp_sha256_update(&x->sha,data,n);copy_elapsed(x,before,&x->hash_ms);x->offset+=n;
if(x->offset-x->guarded>=UINT64_C(1048576) || x->offset==x->source->size){before=copy_now(x);rc=capacity(x->sink,x->source->size-x->offset);copy_elapsed(x,before,&x->guard_ms);x->guarded=x->offset;if(rc)return rc;}
if(x->offset-x->logged>=UINT64_C(67108864) || x->offset==x->source->size){if(x->out)fprintf(x->out,"stage=copy-progress phase=1 bytes=%" PRIu64 "/%" PRIu64 "\n",x->offset,x->source->size);x->logged=x->offset;}
return 0;
}
static int copy_run(mtp_client *c,const mtp_copy_source *s,const mtp_copy_sink *sink,mtp_copy_progress progress,void *user,mtp_copy_result *result,FILE *out,int quick) {
uint8_t *buf=NULL,hash[32];copy_context x={0};mtp_download_stats stream={0};
uint64_t started_ms=c->io.now_ms?c->io.now_ms(c->io.user):0,receive_ms=0,verify_ms=0,phase_start=0,before;size_t n,k;int e=0,phase=0,started=0,clean,used_stream=s->streamed;
memset(result,0,sizeof(*result));x.client=c;x.source=s;x.sink=sink;x.progress=progress;x.user=user;x.out=out;
if(!sink || !sink->begin || !sink->space || !sink->write || !sink->sync || !sink->rewind || !sink->read || !sink->publish || !sink->dispose)return MTP_STATE;
buf=malloc(MTP_STREAM_CHUNK);if(!buf)return MTP_MEMORY;
e=recheck(c,s);if(e)goto done;started=1;e=sink->begin(sink->user,s);if(e)goto done;
if(sink->prepare){phase=MTP_COPY_PREPARE;e=sink->prepare(sink->user,progress,user);if(e)goto done;e=recheck(c,s);if(e)goto done;}
e=capacity(sink,s->size);if(e)goto done;
if(out)fprintf(out,"stage=copy-path method=%s buffer=%u partial-chunk=%u\n",used_stream?"getobject-stream":"partial",MTP_STREAM_CHUNK,MTP_COPY_CHUNK);
for(phase=MTP_COPY_RECEIVE;phase<=MTP_COPY_LOCAL;++phase){
psmtp_sha256_init(&x.sha);x.offset=x.logged=x.guarded=0;phase_start=copy_now(&x);
if(out)fprintf(out,"stage=copy-pass phase=%d total=%" PRIu64 "\n",phase,s->size);
if(phase==MTP_COPY_LOCAL){before=copy_now(&x);e=sink->sync(sink->user);copy_elapsed(&x,before,&x.sync_ms);if(e)goto done;e=sink->rewind(sink->user);if(e)goto done;}
if(phase==MTP_COPY_RECEIVE && used_stream){
e=mtp_get_object_stream(c,s->handle,s->size,buf,MTP_STREAM_CHUNK,copy_consume,stream_progress,&x,&stream);x.phone_ms+=stream.io_ms;
if(e==MTP_RESPONSE && c->response==0x2005 && !c->poisoned && !x.offset && s->partial_supported && (s->extended || s->size<UINT32_MAX)){
used_stream=0;e=0;if(out)fputs("stage=copy-path method=partial reason=getobject-not-supported\n",out);
}
if(e)goto done;
}
if(phase==MTP_COPY_LOCAL || !used_stream){
for(;;){
if(copy_report(&x,phase)){e=MTP_CANCELLED;goto done;}if(x.offset==(phase==MTP_COPY_LOCAL && quick?x.first_n:s->size))break;
n=(size_t)(s->size-x.offset>(phase==MTP_COPY_LOCAL?MTP_STREAM_CHUNK:MTP_COPY_CHUNK)?(phase==MTP_COPY_LOCAL?MTP_STREAM_CHUNK:MTP_COPY_CHUNK):s->size-x.offset);k=0;
if(phase==MTP_COPY_LOCAL && quick)n=x.first_n-(size_t)x.offset;
before=copy_now(&x);
if(phase==MTP_COPY_LOCAL){e=sink->read(sink->user,buf,n,&k);copy_elapsed(&x,before,&x.read_ms);}
else {e=mtp_get_partial(c,s->handle,x.offset,n,s->extended,buf,&k);copy_elapsed(&x,before,&x.phone_ms);++x.requests;}
if(e)goto done;if(!k || k>n){e=MTP_VERIFY;goto done;}
if(phase==MTP_COPY_RECEIVE){e=copy_consume(&x,buf,k);if(e)goto done;}
else {if(quick && memcmp(buf,x.first+(size_t)x.offset,k)){e=MTP_VERIFY;goto done;}before=copy_now(&x);psmtp_sha256_update(&x.sha,buf,k);copy_elapsed(&x,before,&x.hash_ms);x.offset+=k;
if(x.offset-x.logged>=UINT64_C(67108864) || x.offset==s->size){if(out)fprintf(out,"stage=copy-progress phase=2 bytes=%" PRIu64 "/%" PRIu64 "\n",x.offset,s->size);x.logged=x.offset;}}
}
}
if(phase==MTP_COPY_RECEIVE && s->is_pkg && x.first_n<4){e=MTP_VERIFY;goto done;}
if(phase==MTP_COPY_LOCAL && !quick){k=0;e=sink->read(sink->user,buf,1,&k);if(e)goto done;if(k){e=MTP_VERIFY;goto done;}}
psmtp_sha256_final(&x.sha,hash);
if(phase==MTP_COPY_RECEIVE){memcpy(result->digest,hash,32);copy_elapsed(&x,phase_start,&receive_ms);}
else {copy_elapsed(&x,phase_start,&verify_ms);if(!quick && memcmp(hash,result->digest,32)){e=MTP_VERIFY;goto done;}}
}
e=recheck(c,s);if(e)goto done;if(progress && progress(user,MTP_COPY_PUBLISH,s->size,s->size)){e=MTP_CANCELLED;goto done;}
before=copy_now(&x);e=sink->sync(sink->user);copy_elapsed(&x,before,&x.sync_ms);if(e)goto done;e=sink->publish(sink->user,&result->published);
done:
if(e && phase==MTP_COPY_RECEIVE)copy_elapsed(&x,phase_start,&receive_ms);
if(e && phase==MTP_COPY_LOCAL && !verify_ms)copy_elapsed(&x,phase_start,&verify_ms);
if(started){clean=sink->dispose(sink->user);if(clean){result->cleanup_failed=1;if(!e)e=clean;}}
if(out){
fprintf(out,"stage=copy-end status=%d phase=%d offset=%" PRIu64 " published=%d cleanup-failed=%d op=%04x response=%04x poisoned=%d\n",e,buf?phase:0,x.offset,result->published,result->cleanup_failed,c->last_op,c->response,c->poisoned);
if(c->io.now_ms){uint64_t ended=copy_now(&x);fprintf(out,"stage=copy-timing bytes=%" PRIu64 " elapsed-ms=%" PRIu64 " receive-ms=%" PRIu64 " verify-ms=%" PRIu64 " status=%d\n",s->size,ended>=started_ms?ended-started_ms:0,receive_ms,verify_ms,e);}
fprintf(out,"stage=copy-profile method=%s partial-requests=%" PRIu64 " usb-read-calls=%" PRIu64 " phone-ms=%" PRIu64 " writes=%" PRIu64 " write-ms=%" PRIu64 " hash-ms=%" PRIu64 " reread-ms=%" PRIu64 " sync-ms=%" PRIu64 " ui-ms=%" PRIu64 " guard-ms=%" PRIu64 "\n",used_stream?"getobject-stream":"partial",x.requests,stream.read_calls,x.phone_ms,x.writes,x.write_ms,x.hash_ms,x.read_ms,x.sync_ms,x.ui_ms,x.guard_ms);
if(!e && result->published){fputs("copy-sha256=",out);for(n=0;n<32;++n)fprintf(out,"%02x",result->digest[n]);fprintf(out," local-reread=%s phone-content-reads=1 verification=%s installation=not-started\n",quick?"quick-prefix":"match",quick?"quick-prefix-size":"full-sha256-reread");}
}
free(buf);return e;
}
int mtp_copy_run(mtp_client *c,const mtp_copy_source *s,const mtp_copy_sink *sink,mtp_copy_progress progress,void *user,mtp_copy_result *result,FILE *out){return copy_run(c,s,sink,progress,user,result,out,0);}
int mtp_copy_run_quick(mtp_client *c,const mtp_copy_source *s,const mtp_copy_sink *sink,mtp_copy_progress progress,void *user,mtp_copy_result *result,FILE *out){return copy_run(c,s,sink,progress,user,result,out,1);}
+44
View File
@@ -0,0 +1,44 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#include "psmtp/copy_plan.h"
#include "psmtp/navigator.h"
#include <stdlib.h>
#include <string.h>
static int append(mtp_copy_plan *p,mtp_client *c,uint32_t handle,const mtp_object_info *info,size_t parent,size_t depth,mtp_plan_progress progress,void *user,FILE *out) {
size_t at=p->count,i,n; mtp_plan_item *item; uint32_t *handles; int rc;
if(progress && progress(user,p->files,p->folders)) return MTP_CANCELLED;
if(depth>MTP_PLAN_DEPTH || p->count==MTP_PLAN_ITEMS) return MTP_LIMIT;
if(!handle || handle==MTP_ROOT) return MTP_PROTOCOL;
for(i=0;i<p->count;++i) if(p->items[i].source.handle==handle) return MTP_PROTOCOL;
item=&p->items[p->count++]; item->parent=parent; item->depth=depth; item->folder=mtp_is_folder(info);
item->source.handle=handle; item->source.info=*info;
if(!item->folder) {
rc=mtp_copy_probe(c,handle,info,&item->source,out); if(rc) return rc;
if(UINT64_MAX-p->bytes<item->source.size) return MTP_LIMIT;
p->bytes+=item->source.size; ++p->files; return 0;
}
++p->folders;
handles=malloc(MTP_MAX_ITEMS*sizeof(*handles)); if(!handles) return MTP_MEMORY;
rc=mtp_get_handles(c,info->storage,handle,handles,MTP_MAX_ITEMS,&n);
if(rc) { free(handles); return rc; }
if(n>MTP_PLAN_ITEMS-p->count) { free(handles); return MTP_LIMIT; }
for(i=0;i<n;++i) {
mtp_object_info child;
if(progress && progress(user,p->files,p->folders)) { rc=MTP_CANCELLED; break; }
rc=mtp_get_object_info(c,handles[i],&child); if(rc) break;
if(child.storage!=info->storage || child.parent!=handle) { rc=MTP_PROTOCOL; break; }
rc=append(p,c,handles[i],&child,at,depth+1,progress,user,out); if(rc) break;
}
free(handles); return rc;
}
int mtp_copy_plan_build(mtp_copy_plan *p,mtp_client *c,uint32_t handle,const mtp_object_info *info,mtp_plan_progress progress,void *user,FILE *out) {
memset(p,0,sizeof(*p));
return append(p,c,handle,info,SIZE_MAX,0,progress,user,out);
}
+40
View File
@@ -0,0 +1,40 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#include "psmtp/device_sound.h"
static const int16_t sine[65]={
0,3212,6393,9512,12539,15446,18204,20787,23170,25329,27245,28898,30273,31356,32137,32609,
32767,32609,32137,31356,30273,28898,27245,25329,23170,20787,18204,15446,12539,9512,6393,3212,
0,-3212,-6393,-9512,-12539,-15446,-18204,-20787,-23170,-25329,-27245,-28898,-30273,-31356,-32137,-32609,
-32767,-32609,-32137,-31356,-30273,-28898,-27245,-25329,-23170,-20787,-18204,-15446,-12539,-9512,-6393,-3212,0};
static int32_t tone(unsigned hz,size_t frame) {
uint64_t phase=(uint64_t)frame*hz*64*65536/PSMTP_CHIME_RATE;
unsigned index=(unsigned)(phase>>16)&63u,fraction=(unsigned)phase&65535u;
return sine[index]+(int32_t)((int64_t)(sine[index+1]-sine[index])*fraction/65536);
}
void psmtp_chime_render(int connected,size_t frame,int16_t *out,size_t frames) {
static const unsigned notes[2][3]={{784,659,440},{659,880,1109}};
size_t i;
for(i=0;i<frames;++i) {
size_t position=frame+i;int32_t left=0,right=0;unsigned voice;
for(voice=0;voice<3 && position<PSMTP_CHIME_FRAMES;++voice) {
size_t delay=voice*2400u,local,length=PSMTP_CHIME_FRAMES-delay;int64_t envelope,value;
if(position<delay)continue;local=position-delay;
envelope=(int64_t)(length-1-local)*32768/(int64_t)length;
envelope=envelope*envelope/32768*envelope/32768;
if(local<240)envelope=envelope*(int64_t)local/240;
value=(tone(notes[!!connected][voice],local)*8+tone(notes[!!connected][voice]*2,local)+tone(notes[!!connected][voice]*3,local))/10;
value=value*envelope*(voice==0?3000:2200)/(32768*32767LL);
left+=(int32_t)(value*(voice==2?7:10)/10);right+=(int32_t)(value*(voice==0?7:10)/10);
}
out[i*2]=(int16_t)left;out[i*2+1]=(int16_t)right;
}
}
+53
View File
@@ -0,0 +1,53 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#include "psmtp/device_watch.h"
#include <string.h>
static int same(const psmtp_device_record *a,const psmtp_device_record *b) {
return a->bus==b->bus && a->address==b->address && a->vendor==b->vendor && a->product==b->product && a->signature==b->signature;
}
int psmtp_device_watch_update(psmtp_device_watch *w,const psmtp_device_record *records,size_t count,uint64_t now,int force,int (*probe)(void *,size_t),void *user,psmtp_ps4_candidate *out) {
psmtp_device_watch next={0};size_t i,j;int budget=force?128:1;
if(count>PSMTP_WATCH_MAX || (!records && count) || !out || !probe) return MTP_STATE;
next.next_connection=w->next_connection;next.count=count;
for(i=0;i<count;++i) {
if(!records[i].address) return MTP_PROTOCOL;
for(j=0;j<i;++j) if(records[i].bus==records[j].bus && records[i].address==records[j].address) return MTP_PROTOCOL;
next.entries[i].record=records[i];
for(j=0;j<w->count;++j) if(same(&records[i],&w->entries[j].record)) {next.entries[i]=w->entries[j];break;}
if(!next.entries[i].connection) {
if(next.next_connection==UINT64_MAX) return MTP_LIMIT;
next.entries[i].connection=++next.next_connection;
}
}
for(i=0;i<count;++i) {
psmtp_watched_device *e=&next.entries[i];
if(!e->ready && budget && (force || now>=e->retry_at)) {
int rc=probe(user,i);--budget;
if(rc>=0) {e->ready=1;e->mtp=rc==1;} else e->retry_at=now>UINT64_MAX-500?UINT64_MAX:now+500;
}
memset(&out[i],0,sizeof(out[i]));out[i].vendor_id=e->record.vendor;out[i].product_id=e->record.product;
out[i].mtp=(uint8_t)e->mtp;out[i].connection=e->connection;
memcpy(out[i].name,e->name,sizeof(out[i].name));
}
*w=next;return 0;
}
static int present(const psmtp_ps4_candidate *p,const psmtp_ps4_candidate *list,size_t n) {
size_t i;for(i=0;i<n;++i) if(list[i].mtp && list[i].connection==p->connection && list[i].vendor_id==p->vendor_id && list[i].product_id==p->product_id) return 1;
return 0;
}
unsigned psmtp_device_changes(const psmtp_ps4_candidate *old,size_t old_n,unsigned old_usb,const psmtp_ps4_candidate *next,size_t next_n,unsigned next_usb) {
size_t i;unsigned events=0;
if(next_usb&~old_usb) events|=PSMTP_DEVICE_ADDED;
if(old_usb&~next_usb) events|=PSMTP_DEVICE_REMOVED;
for(i=0;i<old_n;++i) if(old[i].mtp && !present(&old[i],next,next_n)) events|=PSMTP_DEVICE_REMOVED;
for(i=0;i<next_n;++i) if(next[i].mtp && !present(&next[i],old,old_n)) events|=PSMTP_DEVICE_ADDED;
return events;
}
+41
View File
@@ -0,0 +1,41 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#include "psmtp/diag_log.h"
#include <string.h>
void diag_log_init(diag_log *log) { memset(log,0,sizeof(*log)); }
static void next_line(diag_log *log) {
if(log->sink) log->sink(log->sink_user,log->lines[log->current%DIAG_LOG_LINES],log->column);
++log->current; log->column=0;
if(log->current-log->first>=DIAG_LOG_LINES) log->first=log->current-DIAG_LOG_LINES+1;
log->lines[log->current%DIAG_LOG_LINES][0]=0;
}
void diag_log_write(diag_log *log,const char *text,size_t n) {
size_t i;
for(i=0;i<n;++i) {
unsigned char c=(unsigned char)text[i]; char *line;
if(c=='\r') continue;
if(c=='\n') { next_line(log); continue; }
if(log->column==DIAG_LOG_COLUMNS) next_line(log);
line=log->lines[log->current%DIAG_LOG_LINES];
line[log->column++]=(char)(c>=32 && c<127?c:'?');
line[log->column]=0;
}
}
void diag_log_set_sink(diag_log *log,diag_log_line_sink sink,void *user) {
log->sink=sink; log->sink_user=user;
}
void diag_log_finish_line(diag_log *log) { if(log->column) next_line(log); }
uint64_t diag_log_end(const diag_log *log) { return log->current+(log->column?1u:0u); }
const char *diag_log_line(const diag_log *log,uint64_t index) {
if(index<log->first || index>=diag_log_end(log)) return NULL;
return log->lines[index%DIAG_LOG_LINES];
}
+72
View File
@@ -0,0 +1,72 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#include "psmtp/diagnostic.h"
#include "psmtp/browser.h"
#include "psmtp/navigator.h"
#include <inttypes.h>
static void stage(FILE *out,mtp_client *c,const char *name,int rc) {
fprintf(out,"stage=%s status=%d (%s) op=0x%04x tid=%" PRIu32 " response=0x%04x poisoned=%d\n",
name,rc,mtp_error_string(rc),c->last_op,c->last_tid,c->response,c->poisoned);
fflush(out);
}
int mtp_diagnose_roots(mtp_client *c,FILE *out,int names) {
mtp_device_info di; uint32_t storages[MTP_MAX_STORAGES],objects[MTP_MAX_ITEMS];
size_t ns=0,no=0,i,j; int e,result=0;
const uint16_t required[]={0x1002,0x1003,0x1004,0x1005,0x1007,0x1008};
fputs("stage=device-info begin (read-only; identity strings suppressed)\n",out); fflush(out);
e=mtp_get_device_info(c,&di); stage(out,c,"device-info",e); if(e) return e;
for(i=0;i<sizeof(required)/sizeof(required[0]);++i) if(!mtp_supports(&di,required[i])) {
fprintf(out,"stage=capability status=%d missing-op=0x%04x\n",MTP_UNSUPPORTED,required[i]);
return MTP_UNSUPPORTED;
}
fputs("stage=open-session begin\n",out); fflush(out);
e=mtp_open_session(c,1); stage(out,c,"open-session",e); if(e) return e;
fputs("stage=storage-ids begin\n",out); fflush(out);
e=mtp_get_storage_ids(c,storages,MTP_MAX_STORAGES,&ns); stage(out,c,"storage-ids",e);
if(e) { result=e; goto cleanup; }
fprintf(out,"storage-count=%zu\n",ns);
if(!ns) {
fputs("NO STORAGE: unlock phone, select File Transfer and accept its access prompt.\n",out);
result=MTP_UNSUPPORTED; goto cleanup;
}
for(i=0;i<ns && i<MTP_DIAG_STORAGES;++i) {
mtp_storage_info si;
fprintf(out,"stage=storage-info begin storage=%08" PRIx32 "\n",storages[i]); fflush(out);
e=mtp_get_storage_info(c,storages[i],&si); stage(out,c,"storage-info",e);
if(e) { result=e; goto cleanup; }
fprintf(out,"storage=%08" PRIx32 " capacity=%" PRIu64 " free=%" PRIu64 " access=%u\n",
storages[i],si.capacity,si.free_bytes,si.access);
fprintf(out,"stage=root-handles begin storage=%08" PRIx32 " parent=ffffffff\n",storages[i]); fflush(out);
e=mtp_get_handles(c,storages[i],MTP_ROOT,objects,MTP_MAX_ITEMS,&no); stage(out,c,"root-handles",e);
if(e) { result=e; goto cleanup; }
fprintf(out,"root-objects=%zu metadata-sample-limit=%u\n",no,MTP_DIAG_OBJECTS);
for(j=0;j<no && j<MTP_DIAG_OBJECTS;++j) {
mtp_object_info object;
fprintf(out,"stage=root-object-info begin handle=%08" PRIx32 "\n",objects[j]); fflush(out);
e=mtp_get_object_info(c,objects[j],&object); stage(out,c,"root-object-info",e);
if(e) { result=e; goto cleanup; }
if(object.storage!=storages[i] || (object.parent!=0 && object.parent!=MTP_ROOT)) {
fputs("stage=root-consistency status=-4 (storage/parent mismatch)\n",out);
result=MTP_PROTOCOL; goto cleanup;
}
fprintf(out,"handle=%08" PRIx32 " kind=%s format=%04x size32=%" PRIu32,
objects[j],mtp_is_folder(&object)?"folder":"object",object.format,object.size32);
if(names) { fputs(" name=",out); mtp_print_name(out,object.name); }
fputc('\n',out);
}
if(no>MTP_DIAG_OBJECTS) fputs("Metadata sample capped; root handle count above is complete within the 4096-handle limit.\n",out);
}
if(ns>MTP_DIAG_STORAGES) fputs("Storage diagnostics capped at four storages.\n",out);
cleanup:
fputs("stage=close-session begin (skipped on unusable stream)\n",out); fflush(out);
e=mtp_close_session(c); stage(out,c,"close-session",e);
return result?result:e;
}
+27
View File
@@ -0,0 +1,27 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#include "psmtp/keyboard.h"
static const unsigned starts[]={0,12,22,31,41},counts[]={12,10,9,10,3};
static unsigned row(size_t key){unsigned r=0;while(r<4 && key>=starts[r+1])++r;return r;}
int psmtp_key_action(size_t key){return key==11?PSMTP_KEY_BACKSPACE:key==31?PSMTP_KEY_SHIFT:key==41?PSMTP_KEY_CLEAR:key==43?PSMTP_KEY_SAVE:PSMTP_KEY_CHARACTER;}
char psmtp_key_character(size_t key,int shift){const char *digits="1234567890-",*top="qwertyuiop",*home="asdfghjkl",*bottom="zxcvbnm._";char c=0;
if(key<11)c=digits[key];else if(key>=12 && key<22)c=top[key-12];else if(key>=22 && key<31)c=home[key-22];else if(key>=32 && key<41)c=bottom[key-32];else if(key==42)c=' ';
if(shift && c>='a' && c<='z')c=(char)(c-'a'+'A');return c;
}
void psmtp_key_rect(size_t key,int *x,int *y,int *w,int *h){unsigned r=row(key),c=(unsigned)key-starts[r];*y=400+(int)r*71;*h=56;
if(r==4){*x=c==0?408:c==1?610:1320;*w=c==1?692:184;}
else {*w=r==0?84:88;*x=(r==0?408:r==1?480:r==2?524:480)+(int)c*(r==0?92:96);}
}
size_t psmtp_key_move(size_t key,int dx,int dy){unsigned r=row(key),c=(unsigned)key-starts[r];
if(dx){if(dx<0 && c)--c;else if(dx>0 && c+1<counts[r])++c;return starts[r]+c;}
if(dy){int x,y,w,h,bx,by,bw,bh;unsigned next=dy<0?(r?r-1:r):(r<4?r+1:r),i,best=0;int distance=10000;psmtp_key_rect(key,&x,&y,&w,&h);
for(i=0;i<counts[next];++i){int d;psmtp_key_rect(starts[next]+i,&bx,&by,&bw,&bh);d=(bx+bw/2)-(x+w/2);if(d<0)d=-d;if(d<distance){distance=d;best=i;}}return starts[next]+best;
}return key;
}
+51
View File
@@ -0,0 +1,51 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#include "psmtp/locations.h"
#include <string.h>
#include <stdio.h>
int psmtp_location_same(const psmtp_location *a,const psmtp_location *b) {
return a->kind==b->kind && a->vendor_id==b->vendor_id && a->product_id==b->product_id &&
(a->kind==PSMTP_LOCATION_PHONE && (a->connection || b->connection)?a->connection==b->connection:a->slot==b->slot);
}
void psmtp_locations_build(psmtp_locations *l,const psmtp_ps4_candidate *phones,size_t n,unsigned usb) {
psmtp_locations old=*l;psmtp_location active={0},selected={0};int previous=l->count!=0;size_t i,j,number=0;
if(previous) {active=l->entries[l->active];selected=l->entries[l->selected];}
memset(l,0,sizeof(*l));if(n>128) n=128;
for(i=0;i<n;++i) if(phones[i].mtp) {
psmtp_location *e=&l->entries[l->count++];e->kind=PSMTP_LOCATION_PHONE;e->slot=(unsigned)i;
e->vendor_id=phones[i].vendor_id;e->product_id=phones[i].product_id;e->connection=phones[i].connection;
snprintf(e->label,sizeof(e->label),"Phone %zu",++number);
if(phones[i].name[0]) {snprintf(e->label,sizeof(e->label),"%s",phones[i].name);e->named=1;}
}
for(i=0;i<PSMTP_USB_SLOTS;++i) if(usb&(1u<<i)) {
psmtp_location *e=&l->entries[l->count++];e->kind=PSMTP_LOCATION_USB;e->slot=(unsigned)i;
snprintf(e->label,sizeof(e->label),"USB%zu",i);snprintf(e->root,sizeof(e->root),"/mnt/usb%zu",i);
}
{psmtp_location *e=&l->entries[l->count++];e->kind=PSMTP_LOCATION_INTERNAL;strcpy(e->label,"Internal storage");strcpy(e->root,"/");}
if(previous) for(i=0;i<l->count;++i) {
for(j=0;j<old.count;++j) if(psmtp_location_same(&old.entries[j],&l->entries[i])) {if(old.entries[j].named>=l->entries[i].named) {strcpy(l->entries[i].label,old.entries[j].label);l->entries[i].named=old.entries[j].named;}break;}
if(psmtp_location_same(&active,&l->entries[i])) l->active=i;
if(psmtp_location_same(&selected,&l->entries[i])) l->selected=i;
}
}
int psmtp_locations_move(psmtp_locations *l,int delta) {
size_t old=l->selected,step;if(!l->count) return 0;
if(delta<0) {step=(size_t)(-(int64_t)delta);l->selected=step>old?0:old-step;}
else {step=(size_t)delta;l->selected=step>l->count-1-old?l->count-1:old+step;}
return l->selected!=old;
}
int psmtp_location_phone_index(const psmtp_location *e,const psmtp_ps4_candidate *p,size_t n) {
size_t i;if(e->kind!=PSMTP_LOCATION_PHONE) return -1;
if(e->connection) {
for(i=0;i<n;++i) if(p[i].mtp && p[i].connection==e->connection && p[i].vendor_id==e->vendor_id && p[i].product_id==e->product_id) return (int)i;
return -1;
}
return e->slot<n && p[e->slot].mtp && p[e->slot].vendor_id==e->vendor_id && p[e->slot].product_id==e->product_id?(int)e->slot:-1;
}
+93
View File
@@ -0,0 +1,93 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#include "psmtp/metadata.h"
#include "psmtp/mtp.h"
#include "psmtp/ui_format.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#if defined(__clang__)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-function"
#pragma clang diagnostic ignored "-Wunused-but-set-variable"
#endif
#define STB_IMAGE_IMPLEMENTATION
#define STB_IMAGE_STATIC
#define STBI_NO_SIMD
#define STBI_ONLY_PNG
#define STBI_ONLY_JPEG
#define STBI_NO_STDIO
#define STBI_NO_HDR
#define STBI_NO_LINEAR
#define STBI_MAX_DIMENSIONS 8192
#include <stb_image.h>
#if defined(__clang__)
#pragma clang diagnostic pop
#endif
static uint32_t be32(const uint8_t *p){return (uint32_t)p[0]<<24|(uint32_t)p[1]<<16|(uint32_t)p[2]<<8|p[3];}
static uint64_t be64(const uint8_t *p){return (uint64_t)be32(p)<<32|be32(p+4);}
static uint32_t le32(const uint8_t *p){return (uint32_t)p[3]<<24|(uint32_t)p[2]<<16|(uint32_t)p[1]<<8|p[0];}
static unsigned ex16(const uint8_t *p,int little){return little?p[0]|(unsigned)p[1]<<8:(unsigned)p[0]<<8|p[1];}
static unsigned orientation(const uint8_t *p,size_t n){
size_t at=2;if(n<4 || p[0]!=255 || p[1]!=216)return 1;
while(at+4<=n && p[at]==255){unsigned tag=p[at+1],length=(unsigned)p[at+2]*256+p[at+3];if(length<2 || length>n-at-2)break;
if(tag==225 && length>=16 && !memcmp(p+at+4,"Exif\0\0",6)){const uint8_t *t=p+at+10;size_t size=length-8,off;unsigned count,i;int little=t[0]=='I' && t[1]=='I';if(!little && (t[0]!='M'||t[1]!='M'))return 1;off=little?le32(t+4):be32(t+4);if(off>size || size-off<2)return 1;count=ex16(t+off,little);off+=2;for(i=0;i<count && size-off>=12;++i,off+=12)if(ex16(t+off,little)==0x112 && ex16(t+off+2,little)==3){unsigned v=ex16(t+off+8,little);return v>=1 && v<=8?v:1;}}
if(tag==218 || tag==217)break;at+=2+length;
}return 1;
}
void psmtp_metadata_free(psmtp_metadata *m){free(m->image);memset(m,0,sizeof(*m));}
int psmtp_metadata_image(const uint8_t *bytes,size_t length,psmtp_metadata *m){
int w,h,channels;unsigned orient,x,y,rw,rh,tw,th;unsigned char *rgba;
if(!length || length>12u*1024u*1024u || !stbi_info_from_memory(bytes,(int)length,&w,&h,&channels) || w<=0 || h<=0)return MTP_UNSUPPORTED;
orient=orientation(bytes,length);rw=orient>=5?(unsigned)h:(unsigned)w;rh=orient>=5?(unsigned)w:(unsigned)h;m->width=rw;m->height=rh;
if((uint64_t)w*h>24000000u)return MTP_LIMIT;
rgba=stbi_load_from_memory(bytes,(int)length,&w,&h,&channels,4);if(!rgba)return MTP_PROTOCOL;
tw=rw>256?256:rw;th=(unsigned)((uint64_t)rh*tw/rw);if(th>256){th=256;tw=(unsigned)((uint64_t)rw*th/rh);}if(!tw)tw=1;if(!th)th=1;
free(m->image);m->image=malloc((size_t)tw*th*4);if(!m->image){stbi_image_free(rgba);return MTP_MEMORY;}
m->image_width=tw;m->image_height=th;
for(y=0;y<th;++y)for(x=0;x<tw;++x){unsigned rx=(unsigned)((uint64_t)x*rw/tw),ry=(unsigned)((uint64_t)y*rh/th),sx=rx,sy=ry;const uint8_t *p;
if(orient==2)sx=w-1-rx;else if(orient==3){sx=w-1-rx;sy=h-1-ry;}else if(orient==4)sy=h-1-ry;
else if(orient==5){sx=ry;sy=rx;}else if(orient==6){sx=ry;sy=h-1-rx;}else if(orient==7){sx=w-1-ry;sy=h-1-rx;}else if(orient==8){sx=w-1-ry;sy=rx;}
p=rgba+((size_t)sy*w+sx)*4;m->image[y*tw+x]=p[0]|(uint32_t)p[1]<<8|(uint32_t)p[2]<<16|(uint32_t)p[3]<<24;
}
stbi_image_free(rgba);return 0;
}
static int read_at(psmtp_meta_reader *r,uint64_t offset,void *out,size_t n){if(offset>r->size || n>r->size-offset || n>16u*1024u*1024u-r->consumed)return MTP_LIMIT;r->consumed+=n;return r->read(r->user,offset,out,n);}
static uint8_t *block(psmtp_meta_reader *r,uint64_t offset,size_t n){uint8_t *p;if(!n || n>12u*1024u*1024u)return NULL;p=malloc(n);if(!p)return NULL;if(read_at(r,offset,p,n)){free(p);return NULL;}return p;}
static void sfo(psmtp_metadata *m,const uint8_t *p,size_t n){
uint32_t keys,values,count,i;if(n<20 || memcmp(p,"\0PSF",4))return;keys=le32(p+8);values=le32(p+12);count=le32(p+16);if(count>1024 || 20u+(uint64_t)count*16>n || keys>=n || values>=n)return;
for(i=0;i<count;++i){const uint8_t *e=p+20+i*16;size_t key=(size_t)keys+ex16(e,1),offset=(size_t)values+le32(e+12),len=le32(e+4);char *dst=NULL;size_t cap=0;if(key>=n || !memchr(p+key,0,n-key) || offset>=n || !len || len>n-offset || !memchr(p+offset,0,len))continue;
if(!strcmp((const char *)p+key,"TITLE")){dst=m->title;cap=sizeof(m->title);}else if(!strcmp((const char *)p+key,"CONTENT_ID")){dst=m->content_id;cap=sizeof(m->content_id);}else if(!strcmp((const char *)p+key,"TITLE_ID")){dst=m->title_id;cap=sizeof(m->title_id);}else if(!strcmp((const char *)p+key,"APP_VER")){dst=m->version;cap=sizeof(m->version);}if(dst)snprintf(dst,cap,"%s",p+offset);
}
}
static int pkg(psmtp_meta_reader *r,psmtp_metadata *m){
uint8_t header[32];uint32_t table,count,i;int rc=read_at(r,0,header,sizeof(header));if(rc)return rc;if(memcmp(header,"\177CNT",4))return MTP_PROTOCOL;count=be32(header+16);table=be32(header+24);if(count>8192 || table>r->size || (uint64_t)count*32>r->size-table)return MTP_PROTOCOL;
for(i=0;i<count;++i){uint8_t entry[32],*data;uint32_t id,offset,length;rc=read_at(r,(uint64_t)table+i*32,entry,32);if(rc)return rc;id=be32(entry);if(id!=0x1000 && id!=0x1200)continue;offset=be32(entry+16);length=be32(entry+20);if(length>(id==0x1000?131072u:4u*1024u*1024u))continue;data=block(r,offset,length);if(!data)continue;if(id==0x1000)sfo(m,data,length);else (void)psmtp_metadata_image(data,length,m);free(data);}
return 0;
}
static int atoms(psmtp_meta_reader *r,psmtp_metadata *m,uint64_t start,uint64_t end,unsigned depth,unsigned *visited){
uint64_t at=start;if(depth>9)return MTP_LIMIT;
while(at<end){uint8_t h[40];uint64_t size,head=8,data;int rc;if(++*visited>4096 || end-at<8)return MTP_LIMIT;rc=read_at(r,at,h,8);if(rc)return rc;size=be32(h);if(size==1){if(end-at<16 || read_at(r,at+8,h+8,8))return MTP_PROTOCOL;size=be64(h+8);head=16;}if(!size)size=end-at;if(size<head || size>end-at)return MTP_PROTOCOL;data=at+head;
if(!memcmp(h+4,"mvhd",4) && size-head>=24){size_t length=size-head<40?(size_t)(size-head):40;if(!read_at(r,data,h,length)){uint32_t scale=h[0]==1?be32(h+20):be32(h+12);uint64_t duration=h[0]==1?length>=32?be64(h+24):0:be32(h+16);if(scale && duration<=UINT64_MAX/1000)m->duration_ms=duration*1000/scale;}}
else if(!memcmp(h+4,"tkhd",4) && size-head>=8){if(!read_at(r,at+size-8,h,8)){unsigned w=be32(h)>>16,height=be32(h+4)>>16;if(w && height){m->width=w;m->height=height;}}}
else if(!memcmp(h+4,"stsd",4) && size-head>=16){if(!read_at(r,data,h,16)){memcpy(m->codec,h+12,4);m->codec[4]=0;}}
else if(!memcmp(h+4,"data",4) && depth && size-head>8 && size-head<=4u*1024u*1024u){uint8_t *p=block(r,data+8,(size_t)(size-head-8));if(p){unsigned w=m->width,height=m->height;(void)psmtp_metadata_image(p,(size_t)(size-head-8),m);if(w && height){m->width=w;m->height=height;}free(p);}}
else if(!memcmp(h+4,"moov",4)||!memcmp(h+4,"trak",4)||!memcmp(h+4,"mdia",4)||!memcmp(h+4,"minf",4)||!memcmp(h+4,"stbl",4)||!memcmp(h+4,"udta",4)||!memcmp(h+4,"ilst",4)||!memcmp(h+4,"covr",4)||!memcmp(h+4,"meta",4)){
uint64_t skip=!memcmp(h+4,"meta",4)?4:0;if(size-head>=skip){rc=atoms(r,m,data+skip,at+size,depth+1,visited);if(rc && rc!=MTP_UNSUPPORTED)return rc;}}
at+=size;
}return 0;
}
int psmtp_metadata_load(psmtp_meta_reader *r,const char *name,psmtp_metadata *m){
const char *ext=strrchr(name,'.');int rc=MTP_UNSUPPORTED;memset(m,0,sizeof(*m));r->consumed=0;
if(ext && !psmtp_ui_name_compare(ext,".pkg"))rc=pkg(r,m);
else if(ext && (!psmtp_ui_name_compare(ext,".jpg") || !psmtp_ui_name_compare(ext,".jpeg") || !psmtp_ui_name_compare(ext,".png"))){uint8_t *data=block(r,0,(size_t)(r->size>SIZE_MAX?SIZE_MAX:r->size));if(data){rc=psmtp_metadata_image(data,(size_t)r->size,m);free(data);}else rc=MTP_LIMIT;}
else if(ext && (!psmtp_ui_name_compare(ext,".mp4") || !psmtp_ui_name_compare(ext,".mov") || !psmtp_ui_name_compare(ext,".m4v"))){unsigned visited=0;rc=atoms(r,m,0,r->size,0,&visited);}
m->loaded=1;m->error=rc;return rc;
}
+412
View File
@@ -0,0 +1,412 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#include "psmtp/mtp.h"
#include <stdlib.h>
#include "psmtp/sha256.h"
#include <string.h>
static uint16_t u16(const uint8_t *p) { return (uint16_t)(p[0] | (uint16_t)p[1] << 8); }
static uint32_t u32(const uint8_t *p) { return (uint32_t)u16(p) | (uint32_t)u16(p+2) << 16; }
static uint64_t u64(const uint8_t *p) { return (uint64_t)u32(p) | (uint64_t)u32(p+4) << 32; }
static void put16(uint8_t *p, uint16_t v) { p[0]=(uint8_t)v; p[1]=(uint8_t)(v>>8); }
static void put32(uint8_t *p, uint32_t v) { put16(p,(uint16_t)v); put16(p+2,(uint16_t)(v>>16)); }
const char *mtp_error_string(int e) {
switch(e) {
case MTP_OK:return "ok"; case MTP_IO:return "transport error";
case MTP_TIMEOUT:return "deadline/timeout"; case MTP_DISCONNECTED:return "disconnected";
case MTP_PROTOCOL:return "malformed or unexpected protocol data";
case MTP_LIMIT:return "explicit resource limit"; case MTP_MEMORY:return "allocation failed";
case MTP_RESPONSE:return "device rejected operation"; case MTP_STATE:return "invalid session/stream state";
case MTP_UNSUPPORTED:return "unsupported";
case MTP_CANCELLED:return "cancelled"; case MTP_VERIFY:return "verification failed";
case MTP_FILESYSTEM:return "filesystem error";
case MTP_EXISTS:return "destination name already exists";
case MTP_NO_SPACE:return "insufficient storage space";
case MTP_SPACE_UNKNOWN:return "available storage space could not be checked";
default:return "unknown error";
}
}
int mtp_decode_string(const uint8_t *p, size_t n, char *out, size_t cap, size_t *used) {
size_t count, i, j=0;
if (!n || !cap) return MTP_PROTOCOL;
count=p[0];
if (1+count*2>n) return MTP_PROTOCOL;
*used=1+count*2;
if (count && u16(p+1+(count-1)*2)!=0) return MTP_PROTOCOL;
for(i=0; i+1<count; ++i) {
uint32_t c=u16(p+1+i*2);
size_t bytes;
if (!c) return MTP_PROTOCOL;
if (c>=0xd800 && c<=0xdbff) {
uint16_t lo;
if (++i+1>=count) return MTP_PROTOCOL;
lo=u16(p+1+i*2);
if (lo<0xdc00 || lo>0xdfff) return MTP_PROTOCOL;
c=0x10000+((c-0xd800)<<10)+(lo-0xdc00);
} else if (c>=0xdc00 && c<=0xdfff) return MTP_PROTOCOL;
bytes=c<0x80?1:c<0x800?2:c<0x10000?3:4;
if (j+bytes>=cap) return MTP_LIMIT;
if (bytes==1) out[j++]=(char)c;
else {
if(bytes==2) out[j++]=(char)(0xc0|(c>>6));
if(bytes==3) { out[j++]=(char)(0xe0|(c>>12)); out[j++]=(char)(0x80|((c>>6)&63)); }
if(bytes==4) { out[j++]=(char)(0xf0|(c>>18)); out[j++]=(char)(0x80|((c>>12)&63)); out[j++]=(char)(0x80|((c>>6)&63)); }
out[j++]=(char)(0x80|(c&63));
}
}
out[j]=0;
return MTP_OK;
}
typedef struct { const uint8_t *p; size_t n; } cursor;
static int take(cursor *c, size_t n, const uint8_t **p) {
if(n>c->n) return MTP_PROTOCOL;
*p=c->p; c->p+=n; c->n-=n; return MTP_OK;
}
static int str(cursor *c, char *out) {
size_t used=0; int e=mtp_decode_string(c->p,c->n,out,MTP_STRING_BYTES,&used);
if(e) return e;
c->p+=used; c->n-=used; return MTP_OK;
}
static int array16(cursor *c, uint16_t *out, size_t cap, size_t *count) {
const uint8_t *p; uint32_t n; size_t i; int e;
if((e=take(c,4,&p))) return e;
n=u32(p); if(n>cap) return MTP_LIMIT;
if((e=take(c,(size_t)n*2,&p))) return e;
for(i=0;out && i<n;++i) out[i]=u16(p+i*2);
if(count) *count=n;
return MTP_OK;
}
#define TRY(x) do { int err_=(x); if(err_) return err_; } while(0)
int mtp_parse_device(const uint8_t *p, size_t n, mtp_device_info *v) {
cursor c={p,n}; const uint8_t *q; char discarded[MTP_STRING_BYTES]; int i;
memset(v,0,sizeof(*v)); TRY(take(&c,8,&q));
v->standard_version=u16(q); v->vendor_extension=u32(q+2); v->vendor_version=u16(q+6);
TRY(str(&c,v->vendor_description)); TRY(take(&c,2,&q)); v->functional_mode=u16(q);
TRY(array16(&c,v->operations,MTP_MAX_OPS,&v->operation_count));
for(i=0;i<4;++i) TRY(array16(&c,NULL,MTP_MAX_OPS,NULL));
TRY(str(&c,v->manufacturer)); TRY(str(&c,v->model)); TRY(str(&c,v->version));
TRY(str(&c,discarded));
if(discarded[0]) {psmtp_sha256 hash;psmtp_sha256_init(&hash);psmtp_sha256_update(&hash,v->manufacturer,strlen(v->manufacturer)+1);psmtp_sha256_update(&hash,v->model,strlen(v->model)+1);psmtp_sha256_update(&hash,discarded,strlen(discarded));psmtp_sha256_final(&hash,v->identity);v->identity_valid=1;}
memset(discarded,0,sizeof(discarded));
return c.n?MTP_PROTOCOL:MTP_OK;
}
int mtp_parse_storage(const uint8_t *p,size_t n,mtp_storage_info *v) {
cursor c={p,n}; const uint8_t *q;
memset(v,0,sizeof(*v)); TRY(take(&c,26,&q));
v->type=u16(q); v->filesystem=u16(q+2); v->access=u16(q+4);
v->capacity=u64(q+6); v->free_bytes=u64(q+14); v->free_objects=u32(q+22);
TRY(str(&c,v->description)); TRY(str(&c,v->label));
return c.n?MTP_PROTOCOL:MTP_OK;
}
int mtp_parse_object(const uint8_t *p,size_t n,mtp_object_info *v) {
cursor c={p,n}; const uint8_t *q; char discarded[MTP_STRING_BYTES];
memset(v,0,sizeof(*v)); TRY(take(&c,52,&q));
v->storage=u32(q); v->format=u16(q+4); v->protection=u16(q+6); v->size32=u32(q+8);
v->image_width=u32(q+26);v->image_height=u32(q+30);
v->parent=u32(q+38); v->association=u16(q+42); v->association_desc=u32(q+44); v->sequence=u32(q+48);
TRY(str(&c,v->name)); TRY(str(&c,discarded)); TRY(str(&c,v->modified)); TRY(str(&c,discarded));
return c.n?MTP_PROTOCOL:MTP_OK;
}
int mtp_parse_ids(const uint8_t *p,size_t n,uint32_t *ids,size_t cap,size_t *count) {
uint32_t k; size_t i; *count=0;
if(n<4) return MTP_PROTOCOL;
k=u32(p); if(k>MTP_MAX_ITEMS || k>cap) return MTP_LIMIT;
if(n!=4+(size_t)k*4) return MTP_PROTOCOL;
for(i=0;i<k;++i) ids[i]=u32(p+4+i*4);
*count=k; return MTP_OK;
}
void mtp_init(mtp_client *c,mtp_transport io) {
memset(c,0,sizeof(*c)); c->io=io; c->timeout_ms=5000;c->out_max_packet=512;
}
static uint32_t remaining(mtp_client *c,uint64_t start) {
uint64_t elapsed=c->io.now_ms(c->io.user)-start;
return elapsed>=c->timeout_ms?0:c->timeout_ms-(uint32_t)elapsed;
}
static int exact_profile(mtp_client *c,uint8_t *out,size_t n,uint64_t start,mtp_download_stats *stats) {
size_t got, k; unsigned zeros=0; int e; uint32_t ms;
while(n) {
if(c->rx_pos==c->rx_len) {
ms=remaining(c,start); if(!ms) return MTP_TIMEOUT;
got=0;
{uint64_t before=stats?c->io.now_ms(c->io.user):0;
e=c->io.read(c->io.user,c->rx,sizeof(c->rx),&got,ms);
if(stats){uint64_t after=c->io.now_ms(c->io.user);++stats->read_calls;stats->read_bytes+=got;if(after>=before)stats->io_ms+=after-before;}}
if(e) return e;
if(!remaining(c,start)) return MTP_TIMEOUT;
if(got>sizeof(c->rx)) return MTP_PROTOCOL;
c->rx_pos=0; c->rx_len=got;
if(!got) { if(++zeros>8) return MTP_TIMEOUT; continue; }
}
k=c->rx_len-c->rx_pos; if(k>n) k=n;
memcpy(out,c->rx+c->rx_pos,k); c->rx_pos+=k; out+=k; n-=k;
}
return MTP_OK;
}
static int exact(mtp_client *c,uint8_t *out,size_t n,uint64_t start) {return exact_profile(c,out,n,start,NULL);}
static int container(mtp_client *c,uint64_t start,int response_only,uint16_t *type,uint16_t *code,uint8_t **payload,size_t *n) {
uint8_t h[12]; uint32_t len; int e;
*payload=NULL; *n=0;
TRY(exact(c,h,12,start)); len=u32(h);
if(len<12) return MTP_PROTOCOL;
if(len>MTP_MAX_CONTAINER) return MTP_LIMIT;
*type=u16(h+4); *code=u16(h+6);
if(u32(h+8)!=c->last_tid) return MTP_PROTOCOL;
if(*type!=2 && *type!=3) return MTP_PROTOCOL;
if(response_only && *type!=3) return MTP_PROTOCOL;
if(*type==2 && *code!=c->last_op) return MTP_PROTOCOL;
if(*type==3 && ((*code&0xf000)!=0x2000 || len>32 || (len-12)%4)) return MTP_PROTOCOL;
*n=len-12;
if(*n) {
*payload=(uint8_t *)malloc(*n); if(!*payload) return MTP_MEMORY;
e=exact(c,*payload,*n,start);
if(e) { free(*payload); *payload=NULL; return e; }
}
return MTP_OK;
}
static int transaction_ex(mtp_client *c,uint16_t op,const uint32_t *params,size_t np,int data,const uint8_t *outgoing,size_t outgoing_size,uint8_t **out,size_t *n) {
uint8_t cmd[32], *p=NULL, *response=NULL; size_t size=0,rn=0,sent=0,i;
uint16_t type=0,code=0; uint64_t start; int e,had_data;
*out=NULL; *n=0;
if(c->poisoned || !c->io.read || !c->io.write || !c->io.now_ms || np>5) return MTP_STATE;
if(op!=0x1001 && op!=0x1002 && !c->session) return MTP_STATE;
if(c->next_tid==UINT32_MAX) return MTP_LIMIT;
if(c->rx_len!=c->rx_pos) { c->poisoned=1; return MTP_PROTOCOL; }
c->last_tid=c->next_tid++; c->last_op=op; c->response=0; c->response_param_count=0;
put32(cmd,(uint32_t)(12+np*4)); put16(cmd+4,1); put16(cmd+6,op); put32(cmd+8,c->last_tid);
for(i=0;i<np;++i) put32(cmd+12+i*4,params[i]);
start=c->io.now_ms(c->io.user);
e=c->io.write(c->io.user,cmd,12+np*4,&sent,c->timeout_ms);
if(!e && sent!=12+np*4) e=MTP_IO;
if(e) goto fail;
if(outgoing) {
uint8_t packet[1024];uint32_t ms=remaining(c,start);if(!ms){e=MTP_TIMEOUT;goto fail;}if((op!=0x9804 && op!=0x100c) || outgoing_size>sizeof(packet)-12){e=MTP_STATE;goto fail;}
put32(packet,(uint32_t)(12+outgoing_size));put16(packet+4,2);put16(packet+6,op);put32(packet+8,c->last_tid);memcpy(packet+12,outgoing,outgoing_size);
sent=0;e=c->io.write(c->io.user,packet,12+outgoing_size,&sent,ms);if(!e && sent!=12+outgoing_size)e=MTP_IO;if(e)goto fail;
if(c->out_max_packet && (12+outgoing_size)%c->out_max_packet==0){sent=0;e=c->io.write(c->io.user,packet,0,&sent,remaining(c,start));if(!e && sent)e=MTP_IO;if(e)goto fail;}
}
e=container(c,start,0,&type,&code,&p,&size); if(e) goto fail;
had_data=type==2;
if(had_data) {
if(!data) { e=MTP_PROTOCOL; goto fail; }
e=container(c,start,1,&type,&code,&response,&rn); if(e) goto fail;
if(type!=3) { e=MTP_PROTOCOL; goto fail; }
} else if(code==0x2001 && data) { e=MTP_PROTOCOL; goto fail; }
c->response=code;
c->response_param_count=(had_data?rn:size)/4;
for(i=0;i<c->response_param_count;++i) c->response_params[i]=u32((had_data?response:p)+i*4);
free(response); response=NULL;
if(c->rx_pos!=c->rx_len) { e=MTP_PROTOCOL; goto fail; }
if(code!=0x2001) { free(p); return MTP_RESPONSE; }
*out=p; *n=size; return MTP_OK;
fail:
free(p); free(response); c->poisoned=1; return e;
}
static int transaction(mtp_client *c,uint16_t op,const uint32_t *params,size_t np,int data,uint8_t **out,size_t *n) {return transaction_ex(c,op,params,np,data,NULL,0,out,n);}
int mtp_supports(const mtp_device_info *v,uint16_t op) {
size_t i; for(i=0;i<v->operation_count;++i) if(v->operations[i]==op) return 1;
return 0;
}
int mtp_open_session(mtp_client *c,uint32_t id) {
uint8_t *p; size_t n; int e;
if(c->session || !id) return MTP_STATE;
c->session_recovered=0;
c->next_tid=0;
e=transaction(c,0x1002,&id,1,0,&p,&n); free(p);
if(e==MTP_RESPONSE && c->response==0x201e && !c->poisoned){
c->session=1;e=mtp_close_session(c);
if(e && !(e==MTP_RESPONSE && c->response==0x2003 && !c->poisoned))return e;
c->next_tid=0;e=transaction(c,0x1002,&id,1,0,&p,&n);free(p);if(!e)c->session_recovered=1;
}
if(e==MTP_RESPONSE && c->response==0x201e)c->session_cleanup_required=1;
if(!e) { c->session=1; c->next_tid=1; c->session_cleanup_required=0; }
return e;
}
int mtp_close_session(mtp_client *c) {
uint8_t *p; size_t n; int e=MTP_OK;
if(c->session) {
e=transaction(c,0x1003,NULL,0,0,&p,&n);free(p);c->session=0;
if(e==MTP_RESPONSE && c->response==0x2003 && !c->poisoned)e=0;
c->session_cleanup_required=e!=0;
}
return e;
}
#define DATA_CALL(op,params,np,parse) \
uint8_t *p; size_t n; int e=transaction(c,op,params,np,1,&p,&n); \
if(!e) e=(parse); free(p); if(e==MTP_PROTOCOL || e==MTP_LIMIT) c->poisoned=1; return e
int mtp_get_device_info(mtp_client *c,mtp_device_info *v) {
DATA_CALL(0x1001,NULL,0,mtp_parse_device(p,n,v));
}
int mtp_get_storage_ids(mtp_client *c,uint32_t *ids,size_t cap,size_t *count) {
*count=0; if(cap>MTP_MAX_STORAGES) cap=MTP_MAX_STORAGES;
DATA_CALL(0x1004,NULL,0,mtp_parse_ids(p,n,ids,cap,count));
}
int mtp_get_storage_info(mtp_client *c,uint32_t id,mtp_storage_info *v) {
DATA_CALL(0x1005,&id,1,mtp_parse_storage(p,n,v));
}
int mtp_get_handles(mtp_client *c,uint32_t storage,uint32_t parent,uint32_t *ids,size_t cap,size_t *count) {
uint32_t params[3]={storage,0,parent}; *count=0;
if(!parent || !storage || storage==UINT32_MAX) return MTP_STATE;
DATA_CALL(0x1007,params,3,mtp_parse_ids(p,n,ids,cap,count));
}
int mtp_get_object_info(mtp_client *c,uint32_t id,mtp_object_info *v) {
if(!id || id==UINT32_MAX) return MTP_STATE;
DATA_CALL(0x1008,&id,1,mtp_parse_object(p,n,v));
}
int mtp_get_object_size(mtp_client *c,uint32_t id,uint64_t *size) {
uint32_t params[2]={id,0xdc04};
uint8_t *p; size_t n; int e;
if(!id || id==MTP_ROOT) return MTP_STATE;
e=transaction(c,0x9803,params,2,1,&p,&n);
if(!e) { if(n!=8) { e=MTP_PROTOCOL; c->poisoned=1; } else *size=u64(p); }
free(p); return e;
}
int mtp_get_partial(mtp_client *c,uint32_t id,uint64_t offset,size_t length,int extended,uint8_t *out,size_t *got) {
uint32_t params[4]; uint8_t *p; size_t n; int e;
*got=0;
if(!id || id==MTP_ROOT || !length || length>MTP_COPY_CHUNK || !out) return MTP_STATE;
if(!extended && (offset>UINT32_MAX || length>UINT32_MAX-offset)) return MTP_LIMIT;
if(length>UINT64_MAX-offset)return MTP_LIMIT;
params[0]=id; params[1]=(uint32_t)offset;
if(extended) { params[2]=(uint32_t)(offset>>32); params[3]=(uint32_t)length; }
else params[2]=(uint32_t)length;
c->content_read=1;
e=transaction(c,extended?0x95c1:0x101b,params,extended?4:3,1,&p,&n);
c->content_read=0;
if(!e) {
if(n>length || c->response_param_count!=1 || c->response_params[0]!=n) { e=MTP_PROTOCOL; c->poisoned=1; }
else { if(n) memcpy(out,p,n); *got=n; }
}
free(p); return e;
}
int mtp_get_object_stream(mtp_client *c,uint32_t id,uint64_t size,uint8_t *buffer,size_t capacity,mtp_download_consume consume,mtp_download_progress progress,void *user,mtp_download_stats *stats) {
uint8_t command[16],header[12],*reply=NULL;uint64_t offset=0,start;uint32_t length,wanted;size_t sent=0,n=0,got=0;uint16_t type,code;int rc=0;unsigned zeros=0;
if(stats)memset(stats,0,sizeof(*stats));
if(!id || id==MTP_ROOT || !buffer || !consume || capacity<MTP_RX_BYTES || capacity>1024u*1024u || capacity%1024 || !c->session || c->poisoned || !c->io.read || !c->io.write || !c->io.now_ms)return MTP_STATE;
if(size>UINT64_MAX-12 || c->next_tid==UINT32_MAX)return MTP_LIMIT;
if(c->rx_pos!=c->rx_len){c->poisoned=1;return MTP_PROTOCOL;}
if(progress && progress(user,0,size))return MTP_CANCELLED;
c->last_op=0x1009;c->last_tid=c->next_tid++;c->response=0;c->response_param_count=0;c->content_read=1;
put32(command,16);put16(command+4,1);put16(command+6,0x1009);put32(command+8,c->last_tid);put32(command+12,id);
start=c->io.now_ms(c->io.user);rc=c->io.write(c->io.user,command,sizeof(command),&sent,c->timeout_ms);if(!rc && sent!=sizeof(command))rc=MTP_IO;if(rc)goto fail;
rc=exact_profile(c,header,sizeof(header),start,stats);if(rc)goto fail;
length=u32(header);type=u16(header+4);code=u16(header+6);
if(u32(header+8)!=c->last_tid){rc=MTP_PROTOCOL;goto fail;}
if(type==3){
if(length<12 || length>32 || (length-12)%4 || (code&0xf000)!=0x2000 || code==0x2001){rc=MTP_PROTOCOL;goto fail;}
if(length>12){uint8_t parameters[20];rc=exact_profile(c,parameters,length-12,start,stats);if(rc)goto fail;}
if(c->rx_pos!=c->rx_len){rc=MTP_PROTOCOL;goto fail;}
c->response=code;c->content_read=0;return MTP_RESPONSE;
}
wanted=size+12>=UINT32_MAX?UINT32_MAX:(uint32_t)(size+12);
if(type!=2 || code!=0x1009 || length!=wanted){rc=MTP_PROTOCOL;goto fail;}
while(offset<size){
if(offset && progress && progress(user,offset,size)){rc=MTP_CANCELLED;goto fail;}
if(c->rx_pos<c->rx_len){
n=c->rx_len-c->rx_pos;if(n>size-offset)n=(size_t)(size-offset);if(n>capacity)n=capacity;
memcpy(buffer,c->rx+c->rx_pos,n);c->rx_pos+=n;
}else{
uint64_t before,after;c->rx_pos=c->rx_len=0;start=c->io.now_ms(c->io.user);
do{uint32_t ms=remaining(c,start);if(!ms || zeros>8){rc=MTP_TIMEOUT;goto fail;}
got=0;before=c->io.now_ms(c->io.user);rc=c->io.read(c->io.user,buffer,capacity,&got,ms);after=c->io.now_ms(c->io.user);
if(stats){++stats->read_calls;stats->read_bytes+=got;if(after>=before)stats->io_ms+=after-before;}
if(rc)goto fail;if(got>capacity){rc=MTP_PROTOCOL;goto fail;}if(!remaining(c,start)){rc=MTP_TIMEOUT;goto fail;}
if(!got)++zeros;
}while(!got);
zeros=0;n=got;
if(n>size-offset){size_t extra=n-(size_t)(size-offset);n=(size_t)(size-offset);if(extra>sizeof(c->rx)){rc=MTP_PROTOCOL;goto fail;}memcpy(c->rx,buffer+n,extra);c->rx_len=extra;}
}
rc=consume(user,buffer,n);if(rc)goto fail;offset+=n;
}
start=c->io.now_ms(c->io.user);rc=container(c,start,1,&type,&code,&reply,&n);if(rc)goto fail;
if(type!=3 || n || c->rx_pos!=c->rx_len){rc=MTP_PROTOCOL;goto fail;}
c->response=code;c->content_read=0;free(reply);
if(code!=0x2001)return MTP_RESPONSE;
return progress && progress(user,size,size)?MTP_CANCELLED:0;
fail:
free(reply);c->poisoned=1;c->content_read=0;return rc;
}
int mtp_delete_object(mtp_client *c,uint32_t id) {
uint32_t params[2]={id,0};uint8_t *p;size_t n;int e;
if(!id || id==MTP_ROOT) return MTP_STATE;
e=transaction(c,0x100b,params,2,0,&p,&n);free(p);return e;
}
#include "psmtp/name.h"
int mtp_rename_object(mtp_client *c,uint32_t id,uint16_t format,const char *name) {
uint32_t params[2]={0xdc07,format};uint8_t encoded[511],*p;size_t n,length;int rc;
if(!id || id==MTP_ROOT || psmtp_name_encode(name,encoded,sizeof(encoded),&length))return MTP_STATE;
rc=transaction(c,0x9802,params,2,1,&p,&n);
if(!rc && (n<5 || u16(p)!=0xdc07 || u16(p+2)!=0xffff || p[4]!=1))rc=MTP_UNSUPPORTED;free(p);if(rc)return rc;
params[0]=id;params[1]=0xdc07;
rc=transaction_ex(c,0x9804,params,2,0,encoded,length,&p,&n);free(p);return rc;
}
int mtp_get_thumbnail(mtp_client *c,uint32_t id,uint8_t **bytes,size_t *size) {
int rc;*bytes=NULL;*size=0;if(!id || id==MTP_ROOT)return MTP_STATE;
c->content_read=1;rc=transaction(c,0x100a,&id,1,1,bytes,size);c->content_read=0;return rc;
}
int mtp_send_object_info(mtp_client *c,uint32_t storage,uint32_t parent,const char *name,uint64_t size,int folder,uint32_t *handle) {
uint8_t data[600],*reply=NULL;size_t encoded,n;uint32_t params[2]={storage,parent};int rc;
*handle=0;
if(!storage || storage==MTP_ROOT || !parent || size>MTP_UPLOAD_MAX_SIZE || (folder && size) || psmtp_name_encode(name,data+52,sizeof(data)-55,&encoded))return MTP_LIMIT;
memset(data,0,52);put32(data,storage);put16(data+4,folder?0x3001:0x3000);put32(data+8,size>=UINT32_MAX?UINT32_MAX:(uint32_t)size);put32(data+38,parent==MTP_ROOT?0:parent);put16(data+42,folder?1:0);
memset(data+52+encoded,0,3);
rc=transaction_ex(c,0x100c,params,2,0,data,55+encoded,&reply,&n);free(reply);if(rc)return rc;
if(c->response_param_count!=3 || c->response_params[0]!=storage ||
(c->response_params[1]!=parent && !(parent==MTP_ROOT && !c->response_params[1])) || !c->response_params[2] || c->response_params[2]==MTP_ROOT){c->poisoned=1;return MTP_PROTOCOL;}
*handle=c->response_params[2];return 0;
}
int mtp_send_object(mtp_client *c,uint64_t size,mtp_upload_read source,mtp_upload_progress progress,void *user) {
uint8_t cmd[12],*packet=NULL,*reply=NULL;uint64_t offset=0,start;size_t sent,n,header=12,chunk;uint16_t type,code;int rc=0;uint32_t saved_timeout=c->timeout_ms;
if(!source || !c->session || c->poisoned || !c->io.read || !c->io.write || !c->io.now_ms)return MTP_STATE;
if(size>MTP_UPLOAD_MAX_SIZE || c->next_tid==UINT32_MAX || !c->out_max_packet || c->out_max_packet>1024)return MTP_LIMIT;
if(c->rx_pos!=c->rx_len){c->poisoned=1;return MTP_PROTOCOL;}
packet=malloc(MTP_COPY_CHUNK);if(!packet){c->poisoned=1;return MTP_MEMORY;}
c->last_op=0x100d;c->last_tid=c->next_tid++;c->response=0;c->response_param_count=0;c->content_read=1;
put32(cmd,12);put16(cmd+4,1);put16(cmd+6,0x100d);put32(cmd+8,c->last_tid);
start=c->io.now_ms(c->io.user);sent=0;rc=c->io.write(c->io.user,cmd,12,&sent,c->timeout_ms);if(!rc && sent!=12)rc=MTP_IO;if(!rc && !remaining(c,start))rc=MTP_TIMEOUT;if(rc)goto fail;
put32(packet,size>MTP_UPLOAD_STANDARD_MAX_SIZE?UINT32_MAX:(uint32_t)(size+12));put16(packet+4,2);put16(packet+6,0x100d);put32(packet+8,c->last_tid);
do{
if(progress && progress(user,offset,size)){rc=MTP_CANCELLED;goto fail;}
chunk=(size_t)(size-offset>MTP_COPY_CHUNK-header?MTP_COPY_CHUNK-header:size-offset);
if(offset+chunk<size)chunk=(chunk+header)/c->out_max_packet*c->out_max_packet-header;
if(chunk){rc=source(user,packet+header,chunk);if(rc)goto fail;}
start=c->io.now_ms(c->io.user);sent=0;rc=c->io.write(c->io.user,packet,chunk+header,&sent,c->timeout_ms);
if(!rc && sent!=chunk+header)rc=MTP_IO;if(!rc && !remaining(c,start))rc=MTP_TIMEOUT;if(rc)goto fail;
offset+=chunk;header=0;
}while(offset<size);
if((size+12)%c->out_max_packet==0){sent=0;start=c->io.now_ms(c->io.user);rc=c->io.write(c->io.user,packet,0,&sent,c->timeout_ms);if(!rc && sent)rc=MTP_IO;if(rc)goto fail;}
if(progress && progress(user,size,size)){rc=MTP_CANCELLED;goto fail;}
if(size>MTP_UPLOAD_STANDARD_MAX_SIZE && c->timeout_ms<30000)c->timeout_ms=30000;
start=c->io.now_ms(c->io.user);rc=container(c,start,1,&type,&code,&reply,&n);if(rc)goto fail;
if(c->rx_pos!=c->rx_len || type!=3 || n){rc=MTP_PROTOCOL;goto fail;}
c->response=code;if(code!=0x2001)rc=code==0x200c?MTP_NO_SPACE:MTP_RESPONSE;
c->timeout_ms=saved_timeout;c->content_read=0;free(reply);free(packet);return rc;
fail:
c->timeout_ms=saved_timeout;c->poisoned=1;c->content_read=0;free(reply);free(packet);return rc;
}
+26
View File
@@ -0,0 +1,26 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#include "psmtp/name.h"
#include <string.h>
int psmtp_name_encode(const char *name,uint8_t *out,size_t cap,size_t *length) {
const unsigned char *p=(const unsigned char *)name;size_t units=0,n;uint8_t data[511];
*length=0;if(!name || !(n=strlen(name)) || n>255 || name[n-1]==' ' || name[n-1]=='.' || !strcmp(name,".") || !strcmp(name,".."))return -1;
while(*p){uint32_t cp=*p++;unsigned extra=0,i;if(cp<32 || cp==127 || (cp<128 && strchr("/\\:*?\"<>|",(int)cp)))return -1;
if(cp>=128){if(cp>=0xc2 && cp<=0xdf){extra=1;cp&=31;}else if(cp>=0xe0 && cp<=0xef){extra=2;cp&=15;}else if(cp>=0xf0 && cp<=0xf4){extra=3;cp&=7;}else return -1;
if((size_t)(p-(const unsigned char *)name)+extra>n)return -1;
for(i=0;i<extra;++i){if((p[i]&0xc0)!=0x80)return -1;cp=(cp<<6)|(p[i]&63);}p+=extra;
if((extra==1 && cp<128)||(extra==2 && cp<2048)||(extra==3 && cp<65536)||cp>0x10ffff||(cp>=0xd800 && cp<=0xdfff))return -1;}
if(cp>=0x10000){uint32_t high=0xd800+((cp-0x10000)>>10);if(units>=252)return -1;data[1+units*2]=(uint8_t)high;data[2+units*2]=(uint8_t)(high>>8);++units;cp=0xdc00+((cp-0x10000)&1023);}
if(units>=254)return -1;data[1+units*2]=(uint8_t)cp;data[2+units*2]=(uint8_t)(cp>>8);++units;
}
data[0]=(uint8_t)(units+1);data[1+units*2]=data[2+units*2]=0;*length=3+units*2;
if(out){if(cap<*length)return -1;memcpy(out,data,*length);}return 0;
}
int psmtp_name_valid(const char *name){size_t n;return !psmtp_name_encode(name,NULL,0,&n);}
+190
View File
@@ -0,0 +1,190 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#include "psmtp/navigator.h"
#include <inttypes.h>
#include <string.h>
#include <stdlib.h>
#include "psmtp/ui_format.h"
int mtp_is_folder(const mtp_object_info *info) {
return info->format==0x3001 && info->association<=1;
}
static int stage(mtp_navigator *nav,const char *name,int rc) {
mtp_client *c=nav->client;
fprintf(nav->log,"stage=browser-%s status=%d op=%04x tid=%" PRIu32 " response=%04x poisoned=%d\n",
name,rc,c->last_op,c->last_tid,c->response,c->poisoned);
fflush(nav->log); return rc;
}
static int page(mtp_navigator *nav) {
size_t i,end; int rc;
nav->page_start=(nav->selected/MTP_NAV_PAGE)*MTP_NAV_PAGE; nav->page_count=0;
end=nav->count-nav->page_start; if(end>MTP_NAV_PAGE) end=MTP_NAV_PAGE;
for(i=0;i<end;++i) {
mtp_nav_entry *entry=&nav->entries[i];
entry->handle=nav->handles[nav->page_start+i];
fprintf(nav->log,"stage=browser-object-info begin handle=%08" PRIx32 "\n",entry->handle); fflush(nav->log);
if(nav->catalog) *entry=nav->catalog[nav->page_start+i];
else {rc=mtp_get_object_info(nav->client,entry->handle,&entry->info);
if(stage(nav,"object-info",rc)) return rc;}
if(entry->info.storage!=nav->storages[nav->storage_index] ||
(nav->parent==MTP_ROOT && entry->info.parent!=0 && entry->info.parent!=MTP_ROOT) ||
(nav->parent!=MTP_ROOT && entry->info.parent!=nav->parent))
return stage(nav,"parent-check",MTP_PROTOCOL);
if(!nav->catalog || !entry->exact_size){entry->bytes=entry->info.size32;entry->exact_size=entry->info.size32!=UINT32_MAX;}
if(!mtp_is_folder(&entry->info) && !entry->exact_size && nav->size64_supported) {
uint64_t bytes=0;rc=mtp_get_object_size(nav->client,entry->handle,&bytes);
stage(nav,"file-size",rc);
if(!rc) {entry->bytes=bytes;entry->exact_size=1;}
else if(nav->client->poisoned) return rc;
}
fprintf(nav->log,"browser-item handle=%08" PRIx32 " parent=%08" PRIx32 " kind=%s format=%04x association=%u size32=%" PRIu32 "\n",
entry->handle,entry->info.parent,mtp_is_folder(&entry->info)?"folder":"file",
entry->info.format,entry->info.association,entry->info.size32);
if(nav->catalog)nav->catalog[nav->page_start+i]=*entry;
++nav->page_count;
}
fprintf(nav->log,"browser-page first=%zu displayed=%zu total=%zu names-in-log=0\n",nav->page_start,nav->page_count,nav->count);
return 0;
}
static int nav_sort_mode;
static int nav_compare(const void *a,const void *b) {
const mtp_nav_entry *x=a,*y=b;int name=psmtp_ui_compare(nav_sort_mode,x->info.name,mtp_is_folder(&x->info),x->exact_size?x->bytes:UINT64_MAX,x->info.modified,y->info.name,mtp_is_folder(&y->info),y->exact_size?y->bytes:UINT64_MAX,y->info.modified);
return name?name:x->handle<y->handle?-1:x->handle!=y->handle;
}
static int load(mtp_navigator *nav,uint32_t parent,size_t selected) {
size_t i,j; int rc;
if(!parent) return MTP_STATE;
if(parent!=MTP_ROOT) {
mtp_object_info folder;
fprintf(nav->log,"stage=browser-folder-info begin handle=%08" PRIx32 "\n",parent); fflush(nav->log);
rc=mtp_get_object_info(nav->client,parent,&folder);
if(stage(nav,"folder-info",rc)) return rc;
if(folder.storage!=nav->storages[nav->storage_index] || !mtp_is_folder(&folder))
return stage(nav,"folder-check",MTP_PROTOCOL);
memcpy(nav->folder_name,folder.name,sizeof(nav->folder_name));
} else strcpy(nav->folder_name,"/");
free(nav->catalog);nav->catalog=NULL;nav->count=0; nav->page_count=0;
fprintf(nav->log,"stage=browser-handles begin storage=%08" PRIx32 " parent=%08" PRIx32 "\n",nav->storages[nav->storage_index],parent);
fflush(nav->log);
rc=mtp_get_handles(nav->client,nav->storages[nav->storage_index],parent,nav->handles,MTP_MAX_ITEMS,&nav->count);
if(stage(nav,"handles",rc)) return rc;
for(i=0;i<nav->count;++i) {
if(!nav->handles[i] || nav->handles[i]==MTP_ROOT) return stage(nav,"handle-check",MTP_PROTOCOL);
for(j=0;j<i;++j) if(nav->handles[i]==nav->handles[j]) return stage(nav,"duplicate-handle",MTP_PROTOCOL);
}
if(nav->scan_progress && nav->count) {
nav->catalog=calloc(nav->count,sizeof(*nav->catalog));if(!nav->catalog) return MTP_MEMORY;
for(i=0;i<nav->count;++i) {
mtp_nav_entry *e=&nav->catalog[i];
if(nav->scan_progress(nav->scan_user,i,nav->count)) return MTP_CANCELLED;
e->handle=nav->handles[i];rc=mtp_get_object_info(nav->client,e->handle,&e->info);
e->bytes=e->info.size32;e->exact_size=e->info.size32!=UINT32_MAX;
if(stage(nav,"catalog-object",rc)) return rc;
if(e->info.storage!=nav->storages[nav->storage_index] ||
(parent==MTP_ROOT?e->info.parent!=0 && e->info.parent!=MTP_ROOT:e->info.parent!=parent)) return stage(nav,"catalog-parent",MTP_PROTOCOL);
}
nav_sort_mode=nav->sort_mode;qsort(nav->catalog,nav->count,sizeof(*nav->catalog),nav_compare);
for(i=0;i<nav->count;++i) nav->handles[i]=nav->catalog[i].handle;
if(nav->scan_progress(nav->scan_user,nav->count,nav->count)) return MTP_CANCELLED;
}
nav->parent=parent;
nav->selected=nav->count?(selected<nav->count?selected:nav->count-1):0;
return page(nav);
}
int mtp_nav_storage(mtp_navigator *nav,size_t index) {
int rc;
if(index>=nav->storage_count) return MTP_STATE;
fprintf(nav->log,"stage=browser-storage-info begin storage=%08" PRIx32 "\n",nav->storages[index]); fflush(nav->log);
rc=mtp_get_storage_info(nav->client,nav->storages[index],&nav->storage);
if(stage(nav,"storage-info",rc)) return rc;
nav->storage_index=index; nav->depth=0;
return load(nav,MTP_ROOT,0);
}
int mtp_nav_open(mtp_navigator *nav,mtp_client *client,FILE *out) {return mtp_nav_open_ex(nav,client,out,NULL,NULL);}
int mtp_nav_open_ex(mtp_navigator *nav,mtp_client *client,FILE *out,int (*scan)(void *,size_t,size_t),void *user) {
mtp_device_info device; size_t i; int rc;
const uint16_t ops[]={0x1002,0x1003,0x1004,0x1005,0x1007,0x1008};
memset(nav,0,sizeof(*nav)); nav->client=client; nav->log=out;nav->scan_progress=scan;nav->scan_user=user;
fputs("stage=browser-device-info begin; metadata only, names never logged\n",out); fflush(out);
rc=mtp_get_device_info(client,&device); if(stage(nav,"device-info",rc)) return rc;
for(i=0;i<sizeof(ops)/sizeof(ops[0]);++i) if(!mtp_supports(&device,ops[i])) return stage(nav,"capability",MTP_UNSUPPORTED);
snprintf(nav->device_name,sizeof(nav->device_name),"%.95s",device.model[0]?device.model:"Connected phone");
nav->identity_valid=device.identity_valid;memcpy(nav->device_identity,device.identity,32);
nav->size64_supported=mtp_supports(&device,0x9803);
nav->rename_supported=mtp_supports(&device,0x9802) && mtp_supports(&device,0x9804);
nav->thumbnail_supported=mtp_supports(&device,0x100a);nav->partial_supported=mtp_supports(&device,0x101b);nav->partial64_supported=mtp_supports(&device,0x95c1);
fputs("stage=browser-open-session begin\n",out); fflush(out);
rc=mtp_open_session(client,1); if(stage(nav,"open-session",rc)) return rc;
fprintf(out,"stage=browser-session fresh=1 recovered-stale=%d\n",client->session_recovered);
fputs("stage=browser-storage-ids begin\n",out); fflush(out);
rc=mtp_get_storage_ids(client,nav->storages,MTP_MAX_STORAGES,&nav->storage_count);
if(stage(nav,"storage-ids",rc)) return rc;
if(!nav->storage_count) return stage(nav,"no-storage",MTP_UNSUPPORTED);
for(i=0;i<nav->storage_count;++i) if(!nav->storages[i] || nav->storages[i]==MTP_ROOT) return stage(nav,"storage-check",MTP_PROTOCOL);
return mtp_nav_storage(nav,0);
}
const mtp_nav_entry *mtp_nav_selected(const mtp_navigator *nav) {
if(!nav->count || nav->selected<nav->page_start || nav->selected-nav->page_start>=nav->page_count) return NULL;
return &nav->entries[nav->selected-nav->page_start];
}
int mtp_nav_move(mtp_navigator *nav,int delta) {
size_t selected=nav->selected,step;
if(!nav->count) return 0;
if(delta<0) { step=(size_t)(-(int64_t)delta); selected=step>selected?0:selected-step; }
else { step=(size_t)delta; selected=step>nav->count-1-selected?nav->count-1:selected+step; }
if(selected==nav->selected) return 0;
nav->selected=selected;
if(selected<nav->page_start || selected-nav->page_start>=nav->page_count) return page(nav);
return 0;
}
int mtp_nav_enter(mtp_navigator *nav) {
const mtp_nav_entry *entry=mtp_nav_selected(nav); size_t i; uint32_t handle; int rc;
mtp_nav_position saved;
if(!entry || !mtp_is_folder(&entry->info)) return MTP_STATE;
if(nav->depth==MTP_NAV_DEPTH) return MTP_LIMIT;
handle=entry->handle;
if(handle==nav->parent) return stage(nav,"cycle-check",MTP_PROTOCOL);
for(i=0;i<nav->depth;++i) if(nav->history[i].parent==handle) return stage(nav,"cycle-check",MTP_PROTOCOL);
saved.parent=nav->parent; saved.selected=nav->selected;
snprintf(saved.folder,sizeof(saved.folder),"%s",nav->folder_name);
fprintf(nav->log,"browser-enter handle=%08" PRIx32 " depth=%zu\n",handle,nav->depth+1);
rc=load(nav,handle,0);
if(!rc) nav->history[nav->depth++]=saved;
return rc;
}
int mtp_nav_back(mtp_navigator *nav) {
mtp_nav_position previous; int rc;
if(!nav->depth) return MTP_STATE;
previous=nav->history[nav->depth-1];
rc=load(nav,previous.parent,previous.selected); if(!rc) --nav->depth;
return rc;
}
int mtp_nav_refresh(mtp_navigator *nav) {
uint32_t selected=nav->count?nav->handles[nav->selected]:0;size_t i;int rc=load(nav,nav->parent,nav->selected);
if(!rc && selected)for(i=0;i<nav->count;++i)if(nav->handles[i]==selected){nav->selected=i;return page(nav);}
return rc;
}
int mtp_nav_sort(mtp_navigator *nav,int mode) {
size_t i;uint32_t selected=nav->count?nav->handles[nav->selected]:0;
if(mode<0 || mode>=PSMTP_SORT_COUNT)return MTP_STATE;
nav->sort_mode=mode;if(!nav->catalog)return mode==0?0:MTP_UNSUPPORTED;
nav_sort_mode=mode;qsort(nav->catalog,nav->count,sizeof(*nav->catalog),nav_compare);
for(i=0;i<nav->count;++i){nav->handles[i]=nav->catalog[i].handle;if(nav->handles[i]==selected)nav->selected=i;}
return page(nav);
}
int mtp_nav_close(mtp_navigator *nav) {
free(nav->catalog);nav->catalog=NULL;
fputs("stage=browser-close-session begin (no command if stream unusable)\n",nav->log); fflush(nav->log);
return stage(nav,"close-session",mtp_close_session(nav->client));
}
+77
View File
@@ -0,0 +1,77 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#include "psmtp/pkg.h"
#include "psmtp/sha256.h"
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <inttypes.h>
#include <ctype.h>
static uint32_t pkg32(const uint8_t *p){return (uint32_t)p[0]<<24|(uint32_t)p[1]<<16|(uint32_t)p[2]<<8|p[3];}
static uint64_t pkg64(const uint8_t *p){return (uint64_t)pkg32(p)<<32|pkg32(p+4);}
int psmtp_pkg_name(const char *name){size_t n=name?strlen(name):0;return n>=4 && name[n-4]=='.' && tolower((unsigned char)name[n-3])=='p' && tolower((unsigned char)name[n-2])=='k' && tolower((unsigned char)name[n-1])=='g';}
int psmtp_pkg_inspect(const psmtp_pkg_source *s,psmtp_pkg_info *p){
uint8_t h[0x440];unsigned i;uint64_t table,count;psmtp_sha256 hash;int rc;
memset(p,0,sizeof(*p));if(!s || !s->read || s->size<0x2000 || s->size>INT64_MAX)return MTP_UNSUPPORTED;
rc=s->read(s->user,0,h,sizeof(h));if(rc)return rc;
if(memcmp(h,"\177CNT",4) || pkg64(h+0x430)!=s->size)return MTP_VERIFY;
table=pkg32(h+0x18);count=pkg32(h+0x10);if(!count || table<0x440 || table>s->size || count>(s->size-table)/32)return MTP_VERIFY;
for(i=0;i<36;++i){unsigned c=h[0x40+i];if(i==6 || i==19){if(c!='-')return MTP_VERIFY;}else if(i==16){if(c!='_')return MTP_VERIFY;}else if(!((c>='A' && c<='Z') || (c>='0' && c<='9') || (i>=20 && c=='_')))return MTP_VERIFY;}
if(h[0x40+36])return MTP_VERIFY;
memcpy(p->content_id,h+0x40,36);memcpy(p->title_id,h+0x47,9);
p->size=s->size;p->type=pkg32(h+0x74);p->flags=pkg32(h+0x78);
if(p->type==0x1a)strcpy(p->package_type,"PS4GD");else if(p->type==0x1b)strcpy(p->package_type,"PS4AC");else if(p->type==0x1c)strcpy(p->package_type,"PS4AL");else if(p->type==0x1e)strcpy(p->package_type,"PS4DP");else return MTP_UNSUPPORTED;
p->patch=(p->flags&UINT32_C(0x60100000))!=0 || p->type==0x1e;
p->install_bytes=pkg64(h+0x418);if(p->install_bytes<p->size)p->install_bytes=p->size;if(p->install_bytes>INT64_MAX)return MTP_LIMIT;
psmtp_sha256_init(&hash);psmtp_sha256_update(&hash,h,sizeof(h));psmtp_sha256_final(&hash,p->header_digest);return 0;
}
static int lower_equal(const char *p,size_t n,const char *s){size_t i;if(strlen(s)!=n)return 0;for(i=0;i<n;++i)if(tolower((unsigned char)p[i])!=s[i])return 0;return 1;}
static int number(const char **p,const char *end,uint64_t *v){int any=0;*v=0;while(*p<end && **p>='0' && **p<='9'){unsigned d=(unsigned)(*(*p)++-'0');if(*v>(UINT64_MAX-d)/10)return 0;*v=*v*10+d;any=1;}return any;}
static const char *line_end(const char *p,const char *end){while(end-p>=2){if(p[0]=='\r' && p[1]=='\n')return p;++p;}return NULL;}
static int http_error(psmtp_pkg_send send,void *user,int code,uint64_t size){char h[256];int n=snprintf(h,sizeof(h),"HTTP/1.1 %d Error\r\nContent-Length: 0\r\nContent-Range: bytes */%" PRIu64 "\r\nConnection: close\r\n\r\n",code,size);return send(user,(const uint8_t *)h,(size_t)n);}
int psmtp_pkg_http(const psmtp_pkg_source *s,const char *request,size_t length,const char *path,psmtp_pkg_send send,void *user){
const char *end,*line,*next,*at,*colon;uint64_t first=0,last,n;int head=0,ranged=0,rc;char header[512];uint8_t *data;size_t k;
if(!send || !s || !request || !path || !s->read)return MTP_STATE;end=request+length;last=s->size?s->size-1:0;
if(!s->size || length<4 || length>8192 || memchr(request,0,length) || memcmp(end-4,"\r\n\r\n",4))return http_error(send,user,400,s->size);
line=request;next=line_end(line,end);if(!next)return http_error(send,user,400,s->size);
if(!strncmp(line,"HEAD ",5)){head=1;at=line+5;}else if(!strncmp(line,"GET ",4))at=line+4;else return http_error(send,user,405,s->size);
if(next-at>=7 && !memcmp(at,"http://",7)){
const char *authority=at+7,*slash=memchr(authority,'/',(size_t)(next-authority));
if(!slash || slash-authority<9 || memcmp(authority,"127.0.0.1",9))return http_error(send,user,404,s->size);
if(slash-authority>9){const char *port=authority+9;uint64_t value;
if(*port++!=':' || !number(&port,slash,&value) || port!=slash || !value || value>65535)return http_error(send,user,404,s->size);}
at=slash;
}
k=strlen(path);if((size_t)(next-at)<k+9 || memcmp(at,path,k))return http_error(send,user,404,s->size);
at+=k;
if(at<next && *at=='?'){while(at<next && *at!=' '){unsigned char c=(unsigned char)*at;if(c<0x21 || c>0x7e || c=='#')return http_error(send,user,400,s->size);++at;}}
if(at==next || *at!=' ')return http_error(send,user,404,s->size);
++at;if(next-at!=8 || (memcmp(at,"HTTP/1.1",8) && memcmp(at,"HTTP/1.0",8)))return http_error(send,user,400,s->size);
for(line=next+2;line<end-2;line=next+2){
next=line_end(line,end);if(!next)return http_error(send,user,400,s->size);colon=memchr(line,':',(size_t)(next-line));if(!colon)return http_error(send,user,400,s->size);
at=colon+1;while(at<next && (*at==' ' || *at=='\t'))++at;
if(lower_equal(line,(size_t)(colon-line),"range")){
if(ranged++ || next-at<6 || memcmp(at,"bytes=",6))return http_error(send,user,416,s->size);at+=6;
if(at<next && *at=='-'){++at;if(!number(&at,next,&n) || !n)return http_error(send,user,416,s->size);first=n>=s->size?0:s->size-n;}
else {if(!number(&at,next,&first) || at==next || *at++!='-' || first>=s->size)return http_error(send,user,416,s->size);
if(at<next){if(!number(&at,next,&last) || last<first)return http_error(send,user,416,s->size);if(last>=s->size)last=s->size-1;}}
while(at<next && (*at==' ' || *at=='\t'))++at;if(at!=next)return http_error(send,user,416,s->size);
}else if(lower_equal(line,(size_t)(colon-line),"transfer-encoding") || (lower_equal(line,(size_t)(colon-line),"content-length") && (next-at!=1 || *at!='0')))return http_error(send,user,400,s->size);
}
n=last-first+1;
k=(size_t)snprintf(header,sizeof(header),"HTTP/1.1 %s\r\nContent-Type: application/octet-stream\r\nContent-Length: %" PRIu64 "\r\nAccept-Ranges: bytes\r\nConnection: close\r\n",ranged?"206 Partial Content":"200 OK",n);
if(ranged)k+=(size_t)snprintf(header+k,sizeof(header)-k,"Content-Range: bytes %" PRIu64 "-%" PRIu64 "/%" PRIu64 "\r\n",first,last,s->size);
memcpy(header+k,"\r\n",2);k+=2;rc=send(user,(const uint8_t *)header,k);if(rc || head)return rc;
data=malloc(MTP_STREAM_CHUNK);if(!data)return MTP_MEMORY;
while(n){k=n>MTP_STREAM_CHUNK?MTP_STREAM_CHUNK:(size_t)n;rc=s->read(s->user,first,data,k);if(rc)break;rc=send(user,data,k);if(rc)break;first+=k;n-=k;}
free(data);return rc;
}
+309
View File
@@ -0,0 +1,309 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#include "console_render.h"
#include "psmtp/copy.h"
#include "ui_draw.h"
#include "ui_font.h"
#include "psmtp/ui_format.h"
#include "psmtp/copy_paths.h"
#include "psmtp/version.h"
#include "psmtp/keyboard.h"
#include <stdio.h>
#include <string.h>
#include <inttypes.h>
#include <time.h>
#define WHITE UI_WHITE
#define MUTED UI_MUTED
#define LINE UI_LINE
#define RGB UI_RGB
#define PANEL UI_PANEL
static const psmtp_console_selection *panel;
static const mtp_navigator *phone;
static const psmtp_local_browser *disk;
static const psmtp_locations *devices;
static int show_names;
static const psmtp_location *active(void) {return devices && devices->count?&devices->entries[devices->active]:NULL;}
static void bytes_text(char *out,size_t cap,uint64_t bytes) {char *p;psmtp_ui_size(out,cap,bytes);p=strstr(out,".0 ");if(p) memmove(p,p+2,strlen(p+2)+1);}
static void chevron(int x,int y,int down,uint32_t c) {
if(down) {ui_line(x,y,x+8,y+8,2,c,255);ui_line(x+8,y+8,x+16,y,2,c,255);}
else {ui_line(x,y,x+7,y+8,2,c,255);ui_line(x+7,y+8,x,y+16,2,c,255);}
}
static void text_lines(int x,int y,int size,int width,int lines,const char *s,uint32_t color) {
char copy[512];size_t n=0;int line=0;if(!s) return;
while(*s && line<lines) {
size_t next=1;unsigned char c=(unsigned char)*s;
if((c&0xe0)==0xc0)next=2;else if((c&0xf0)==0xe0)next=3;else if((c&0xf8)==0xf0)next=4;
if(next>strlen(s)) next=1;if(n+next>=sizeof(copy)-1) break;
memcpy(copy+n,s,next);copy[n+next]=0;
if(n && psmtp_font_width(copy,size)>width) {copy[n]=0;if(line==lines-1) {if(n+3<sizeof(copy)) strcat(copy,"...");ui_text(x,y+line*(size+7),size,width,copy,color);return;}ui_text(x,y+line*(size+7),size,width,copy,color);++line;n=0;if(line==lines) return;}
memcpy(copy+n,s,next);n+=next;s+=next;
}
copy[n]=0;if(n && line<lines) ui_text(x,y+line*(size+7),size,width,copy,color);
}
static void control_hint(int x,int y,int px,int width,const char *s,uint32_t color){
static const char *const words[]={"Cross","Circle","Triangle","Square","D-pad","L1","R1","L2","R2","L3","R3","Touchpad","Options","X"};
static const int kinds[]={UI_BTN_CROSS,UI_BTN_CIRCLE,UI_BTN_TRIANGLE,UI_BTN_SQUARE,UI_BTN_DPAD,UI_BTN_L1,UI_BTN_R1,UI_BTN_L2,UI_BTN_R2,UI_BTN_L3,UI_BTN_R3,UI_BTN_TOUCH,UI_BTN_OPTIONS,UI_BTN_CROSS};
int start=x;size_t at=0,plain=0;if(!s)return;
while(s[at]){unsigned i;int found=-1;size_t n=0;
for(i=0;i<sizeof(words)/sizeof(words[0]);++i){size_t z=strlen(words[i]);if((!at || !((s[at-1]>='A' && s[at-1]<='Z') || (s[at-1]>='a' && s[at-1]<='z'))) && !strncmp(s+at,words[i],z) && !((s[at+z]>='A' && s[at+z]<='Z') || (s[at+z]>='a' && s[at+z]<='z') || (s[at+z]>='0' && s[at+z]<='9'))){found=(int)i;n=z;break;}}
if(found>=0){char text[512];size_t length=at-plain;if(length>=sizeof(text))length=sizeof(text)-1;memcpy(text,s+plain,length);text[length]=0;ui_text(x,y,px,width-(x-start),text,color);x+=psmtp_font_width(text,px);if(x-start+px+5>width)return;ui_button(kinds[found],x,y+2,px+2,color);x+=px+7;at+=n;plain=at;}else ++at;
if(x-start>=width)return;
}ui_text(x,y,px,width-(x-start),s+plain,color);
}
static void controller(int x,int y,int kind,const char *text) {
uint32_t c=kind==1?RGB(212,38,65):kind==2?RGB(61,177,162):WHITE;
ui_ring(x+19,y+19,19,1,c,255);ui_button(kind,x+2,y+2,34,c);ui_text(x+58,y+2,28,180,text,WHITE);
}
static void footer(int progress,const char *status) {
if(progress==3)controller(78,986,1,"Cancel");
else if(progress) {controller(78,986,0,progress==2?"Show":"Hide");controller(244,986,1,"Cancel");}
else {controller(78,979,0,"Open");controller(244,979,1,"Back");controller(398,979,2,"Options");}
if(status && *status) control_hint(78,1040,18,1750,status,MUTED);
}
static void header(void) {
char clock_text[48],release[128];time_t now;struct tm *clock;
snprintf(release,sizeof(release),"v%s",panel && panel->display_version?panel->display_version:PSMTP_VERSION);
ui_art(UI_WORDMARK,64,43,570,48,0);ui_text(651,49,31,350,release,MUTED);ui_text(651+psmtp_font_width(release,31)+10,54,25,160,"by nueex",MUTED);ui_art(UI_PLATFORM,1514,48,176,39,0);
if(panel && panel->clock_text) snprintf(clock_text,sizeof(clock_text),"%s",panel->clock_text);
else {now=time(NULL);clock=localtime(&now);if(clock) {strftime(clock_text,sizeof(clock_text),"%I:%M %p",clock);if(clock_text[0]=='0') memmove(clock_text,clock_text+1,strlen(clock_text));}else strcpy(clock_text,"--:--");}
ui_text(1737,47,28,170,clock_text,WHITE);
if(panel && panel->clipboard_label) ui_text(1020,54,23,450,panel->clipboard_label,WHITE);
}
static void sidebar(void) {
size_t count=devices?devices->count:0,first=0,i,cursor=devices?(panel && panel->drawer_focus?devices->selected:devices->active):0;int y,settings_y;
if(count>6 && cursor>4) first=cursor-4;
for(i=first;i<count && i<first+6;++i) {
const psmtp_location *location=&devices->entries[i];int current=!(panel && panel->drawer_focus) && i==devices->active,focus=panel && panel->drawer_focus && !panel->drawer_settings && i==devices->selected;
const char *name=location->label;uint32_t c=current||focus?WHITE:RGB(218,211,214);y=164+(int)(i-first)*82;
if(current || focus) ui_box(30,y,307,82,14,RGB(58,55,57),focus?WHITE:RGB(197,190,192),focus?3:2);
if(location->kind==PSMTP_LOCATION_INTERNAL) {name="Internal Storage";ui_art(UI_DRIVE,57,y+22,35,35,c);}
else if(location->kind==PSMTP_LOCATION_USB) {ui_art(UI_USB,59,y+20,31,40,c);if(count<=3) name="USB Storage";}
else ui_art(UI_PHONE,59,y+18,32,44,c);
ui_text(124,y+23,26,228,name,c);
}
if(!count) ui_text(48,190,25,280,"Finding devices...",MUTED);
settings_y=164+(int)(count<6?count:6)*82+40;ui_rect(46,settings_y-28,289,1,RGB(70,64,67));
ui_art(UI_SETTINGS,56,settings_y+12,38,38,RGB(100,95,100));ui_text(124,settings_y+12,28,210,"Settings",RGB(100,95,100));
if(first) ui_text(48,132,17,280,"Earlier devices above",MUTED);
if(first+6<count) ui_text(48,settings_y+86,17,280,"More devices below",MUTED);
}
static void toolbar(void) {
char path[256]="";const char *leaf;
if(!(panel && panel->display_items)) {
if(phone && phone->parent!=MTP_ROOT) snprintf(path,sizeof(path),"%s",phone->folder_name);
else if(disk && strcmp(disk->path,disk->root)) {leaf=strrchr(disk->path,'/');snprintf(path,sizeof(path),"%s",leaf?leaf+1:disk->path);}
if(path[0]) ui_text(411,160,25,580,path,MUTED);
}
ui_box(panel && panel->list_view?1106:1033,154,55,52,3,RGB(30,29,33),RGB(47,44,49),1);
ui_rect(1049,168,10,10,WHITE);ui_rect(1063,168,10,10,WHITE);ui_rect(1049,182,10,10,WHITE);ui_rect(1063,182,10,10,WHITE);
{int i;for(i=0;i<3;++i) {ui_rect(1120,168+i*10,4,4,WHITE);ui_rect(1130,168+i*10,17,3,WHITE);}}
{char label[64];snprintf(label,sizeof(label),"Sort: %s",psmtp_ui_sort_label(panel?panel->sort_mode:0));ui_text(1193,162,22,240,label,WHITE);}chevron(1425,176,1,WHITE);
if(panel && panel->toolbar_focus){int focus=panel->toolbar_focus;ui_box(focus==1?1033:focus==2?1106:1180,150,focus==3?266:55,60,5,0,WHITE,3);}
}
static size_t item_count(void) {return panel && panel->display_items?panel->display_count:phone?phone->count:disk?disk->count:0;}
static size_t item_selected(void) {return panel && panel->display_items?panel->display_selected:phone?phone->selected:disk?disk->selected:0;}
static int get_item(size_t index,psmtp_ui_item *out) {
memset(out,0,sizeof(*out));out->thumbnail=-1;if(index>=item_count()) return 0;
if(panel && panel->display_items) {*out=panel->display_items[index];return 1;}
if(phone) {
const mtp_nav_entry *e;
if(phone->catalog) e=&phone->catalog[index];else {if(index<phone->page_start || index-phone->page_start>=phone->page_count) return 0;e=&phone->entries[index-phone->page_start];}
out->name=e->info.name;out->modified=e->info.modified;out->folder=mtp_is_folder(&e->info);out->bytes=e->exact_size?e->bytes:e->info.size32;out->unknown=e->info.size32==UINT32_MAX && !e->exact_size;
} else if(disk) {const psmtp_local_entry *e=&disk->entries[index];out->name=e->name;out->modified=e->modified;out->bytes=e->bytes;out->folder=e->folder;}
if(!show_names) out->name="Name hidden";return out->name!=NULL;
}
static const psmtp_metadata *current_metadata;
static const psmtp_metadata *metadata_at(size_t i){return panel && panel->metadata?panel->metadata(panel->metadata_user,i):NULL;}
static void file_art(const psmtp_ui_item *item,int x,int y,int width,int height) {
if(current_metadata && current_metadata->image){ui_image(current_metadata->image,current_metadata->image_width,current_metadata->image_height,x+8,y+4,width-16,height-8);return;}
if(item->folder) ui_art(UI_FOLDER,x+(width-124)/2,y+(height-101)/2,124,101,0);
else if(item->thumbnail>=UI_THUMB_PKG && item->thumbnail<=UI_THUMB_VIDEO) {int w=item->thumbnail==UI_THUMB_PKG?164:218;ui_art(item->thumbnail,x+(width-w)/2,y+(height-120)/2,w,120,0);}
else ui_art(UI_DOCUMENT,x+(width-80)/2,y+(height-108)/2,80,108,0);
}
static void grid_tile(size_t position,int selected,const psmtp_ui_item *item) {
int x=404+(int)(position%4)*264,y=226+(int)(position/4)*232;char meta[80];
int height=item->folder?216:228;
ui_box(x,y,248,height,selected?7:3,RGB(34,31,32),selected?RGB(214,203,206):RGB(44,41,43),selected?3:1);
if(selected) ui_box(x+4,y+4,240,height-8,3,RGB(36,32,33),0,0);
file_art(item,x,y+(item->folder?22:17),248,120);ui_center(x+14,y+(item->folder?135:151),26,220,item->name,WHITE);
if(item->folder) {if(item->items_known) snprintf(meta,sizeof(meta),"%zu items",item->items);else strcpy(meta,"Folder");}
else if(item->unknown) strcpy(meta,"Size unavailable");else bytes_text(meta,sizeof(meta),item->bytes);
ui_text(x+20,y+(item->folder?175:193),22,214,meta,MUTED);
}
static void list_row(size_t position,int selected,const psmtp_ui_item *item) {
int y=266+(int)position*39;char size[48];if(selected) ui_box(404,y-3,1040,38,3,RGB(63,58,61),RGB(205,198,202),2);
ui_art(item->folder?UI_FOLDER:UI_DOCUMENT,419,y+1,29,28,0);ui_text(466,y,25,590,item->name,WHITE);ui_text(1081,y+2,21,160,psmtp_ui_kind(item->name,item->folder),MUTED);
if(item->folder) strcpy(size,"--");else if(item->unknown) strcpy(size,"Unavailable");else bytes_text(size,sizeof(size),item->bytes);ui_text(1284,y+2,22,150,size,WHITE);
}
static void modified_text(const char *value,char *date,size_t date_cap,char *clock,size_t clock_cap) {
static const char *months[]={"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"};int year,month,day,hour,minute;
strcpy(date,"Unavailable");clock[0]=0;
if(!value || sscanf(value,"%4d%2d%2dT%2d%2d",&year,&month,&day,&hour,&minute)!=5 || month<1 || month>12 || day<1 || day>31 || hour<0 || hour>23 || minute<0 || minute>59) return;
snprintf(date,date_cap,"%s %d, %d",months[month-1],day,year);snprintf(clock,clock_cap,"%d:%02d %s",hour%12?hour%12:12,minute,hour>=12?"PM":"AM");
}
static void details(const psmtp_ui_item *input) {
psmtp_ui_item value=*input;const psmtp_ui_item *item=&value;const psmtp_metadata *m=current_metadata;
if(m && m->items_known){value.items_known=1;value.items=(size_t)m->items;}
char count[64],size[64],date[64],clock[48];int offset=psmtp_font_width(item->name,33)>342?42:0;ui_box(1516,166,360,264,1,RGB(31,29,30),RGB(44,40,42),1);
if(item->folder) ui_art(UI_FOLDER,1600,235,192,156,0);else file_art(item,1516,190,360,218);
text_lines(1532,438,33,342,2,item->name,WHITE);ui_rect(1516,509+offset,360,1,RGB(64,59,63));
ui_text(1535,534+offset,25,160,"Type",WHITE);ui_text(1703,534+offset,25,178,psmtp_ui_kind(item->name,item->folder),WHITE);
if(item->folder) {ui_text(1535,582+offset,25,160,"Items",WHITE);if(item->items_known) snprintf(count,sizeof(count),"%zu",item->items);else strcpy(count,"Not counted");ui_text(1703,582+offset,25,175,count,item->items_known?WHITE:MUTED);}
ui_text(1535,(item->folder?628:587)+offset,25,160,"Size",WHITE);
if(item->folder && (!(panel && panel->display_items) || !item->items_known)) strcpy(size,"--");else if(item->unknown) strcpy(size,"Unavailable");else bytes_text(size,sizeof(size),item->bytes);ui_text(1703,(item->folder?628:587)+offset,25,175,size,WHITE);
if(item->folder && panel && panel->display_items && item->items_known) ui_text(1703,660+offset,24,175,"(on disk)",WHITE);
modified_text(item->modified,date,sizeof(date),clock,sizeof(clock));ui_text(1535,724+offset,25,160,"Modified",WHITE);ui_text(1703,724+offset,24,175,date,strcmp(date,"Unavailable")?WHITE:MUTED);ui_text(1703,756+offset,24,175,clock,WHITE);
if(m){char extra[128];int y=804+offset;
if(m->title[0]){ui_text(1535,y,23,342,m->title,WHITE);y+=34;}
if(m->title_id[0]){snprintf(extra,sizeof(extra),"%s %s",m->title_id,m->version);ui_text(1535,y,21,342,extra,MUTED);y+=32;}
if(m->width && m->height){snprintf(extra,sizeof(extra),"%u x %u%s%s",m->width,m->height,m->codec[0]?" ":"",m->codec);ui_text(1535,y,23,342,extra,WHITE);y+=32;}
if(m->duration_ms){snprintf(extra,sizeof(extra),"Duration %llu:%02llu",(unsigned long long)(m->duration_ms/60000),(unsigned long long)(m->duration_ms/1000%60));ui_text(1535,y,23,342,extra,WHITE);}
}
}
static void explorer(const char *status) {
size_t count=item_count(),selected=item_selected(),first,i,limit=panel && panel->list_view?16:12;psmtp_ui_item item;toolbar();first=(selected/limit)*limit;
if(panel && panel->list_view) {ui_text(466,228,20,500,"Name",MUTED);ui_text(1081,228,20,160,"Type",MUTED);ui_text(1284,228,20,150,"Size",MUTED);ui_rect(404,258,1040,1,LINE);}
for(i=first;i<count && i<first+limit;++i) if(get_item(i,&item)) {current_metadata=metadata_at(i);if(panel && panel->list_view) list_row(i-first,i==selected,&item);else grid_tile(i-first,i==selected,&item);}
if(!count) {ui_art(UI_FOLDER,860,420,112,91,0);ui_center(420,547,31,1000,disk && disk->error?"Could not open this folder":active()?"This folder is empty":"Choose a storage device",WHITE);control_hint(420,602,24,1000,disk && disk->error?"Check storage access, or choose another device.":active()?"Press Circle to go back.":"Connect a phone or USB drive to begin.",MUTED);}
current_metadata=metadata_at(selected);
if(panel && panel->detail_override) details(panel->detail_override);else if(get_item(selected,&item)) details(&item);else {ui_box(1516,166,360,264,1,RGB(31,29,30),RGB(44,40,42),1);ui_art(UI_FOLDER,1600,235,192,156,0);ui_text(1532,446,31,344,"Nothing selected",MUTED);}footer(panel && panel->transfer_active?2:0,status);
}
static void menu_overlay(int settings) {
static const char *const context[]={"Open","New Folder","Copy","Move","Paste","Rename","Delete","Properties","Exit PSMTP"};static const int icons[]={UI_FOLDER,UI_FOLDER,UI_COPY,UI_MOVE,UI_COPY,UI_RENAME,UI_DELETE,UI_INFO,UI_SETTINGS};
static const char *const preferences[]={"View: Grid / List","Refresh folder","Refresh devices","Clear clipboard","Diagnostics","Controls","Exit PSMTP"};
size_t i,count=settings?7:PSMTP_MENU_COUNT;int x=settings?690:630,y=settings?228:204,w=settings?540:336,h=88+(int)count*67;
ui_shadow(x,y,w,h,10);ui_box(x,y,w,h,10,0xe1100f12u,RGB(60,56,62),2);ui_text(x+34,y+22,28,w-68,settings?"Settings":"Options",WHITE);ui_rect(x+26,y+70,w-54,1,LINE);
for(i=0;i<count;++i) {
size_t item=i;
int yy=y+88+(int)i*67,enabled=settings || item==PSMTP_MENU_EXIT || (item==PSMTP_MENU_RENAME?panel && panel->rename_allowed:item==PSMTP_MENU_NEW_FOLDER?panel && panel->can_create:item==PSMTP_MENU_PASTE?panel && panel->can_paste:panel && panel->can_select);int selected=panel && panel->index==i;uint32_t c=!enabled?RGB(108,103,109):selected?RGB(38,33,38):WHITE;
if(selected) {ui_box(x+3,yy-8,w-6,70,10,0,0x26ffffffu,4);ui_box(x+7,yy-4,w-14,62,7,RGB(218,213,219),WHITE,2);}
if(!settings) ui_art(icons[item],x+36,yy+10,31,33,c);
ui_text(x+(settings?34:104),yy+10,27,w-(settings?68:120),settings?preferences[i]:context[item],c);if(i+1<count) ui_rect(x+26,yy+62,w-54,1,LINE);
}
if(panel && !settings && panel->index==PSMTP_MENU_RENAME && !panel->rename_allowed) ui_text(78,1040,19,1750,"Select an item on writable storage to rename it.",MUTED);
}
static unsigned percent(uint64_t done,uint64_t total) {double p=total?(double)done/(double)total*100:0;return p>100?100:(unsigned)(p+0.5);}
static void transfer_overlay(void) {
char size[64],total[64],progress[192],pct[24];unsigned p=panel->view==PSMTP_VIEW_INSTALL_PROGRESS && panel->install_known?(unsigned)panel->install_percent:percent(panel->done,panel->total);
const char *phase=panel->view==PSMTP_VIEW_INSTALL_PROGRESS?(!panel->install_preparing && (panel->install_known || panel->total)?"Installing package...":"Preparing installation..."):panel->phase==7?"Deleting items...":panel->phase==6?"Scanning selected items...":panel->phase==5?"Preparing storage...":panel->phase==2?"Verifying...":panel->phase==3?"Checking source...":panel->phase==4?"Finishing transfer...":panel->action==1?"Moving 1 item...":"Copying 1 item...";
ui_shadow(612,398,712,286,13);ui_box(612,398,712,286,13,RGB(29,27,28),RGB(56,51,56),2);ui_text(650,430,29,630,phase,WHITE);ui_text(650,479,26,630,panel->name?panel->name:"Selected items",MUTED);
if(panel->phase==MTP_COPY_LOCAL || panel->phase==MTP_COPY_SOURCE || panel->phase==MTP_COPY_PUBLISH) {
ui_text(650,548,24,630,"Please keep both devices connected.",MUTED);
ui_rect(0,951,1920,129,RGB(21,18,19));footer(1,"Integrity check in progress.");return;
}
ui_box(650,527,563,22,5,RGB(57,53,61),RGB(66,61,69),1);if(panel->view==PSMTP_VIEW_INSTALL_PROGRESS && !panel->install_known && !panel->total)ui_box(650+(int)((panel->elapsed_ms/10)%440),527,120,22,4,RGB(202,198,208),0,0);else if(p) ui_box(650,527,(int)(563*p/100),22,4,RGB(202,198,208),0,0);if(panel->view==PSMTP_VIEW_INSTALL_PROGRESS && !panel->install_known && !panel->total)strcpy(pct,"...");else snprintf(pct,sizeof(pct),"%u%%",p);ui_text(1241,521,25,74,pct,WHITE);
if(panel->phase==7 || panel->phase==6) snprintf(progress,sizeof(progress),"%" PRIu64 " of %" PRIu64 " items",panel->done,panel->total);else {bytes_text(size,sizeof(size),panel->done);bytes_text(total,sizeof(total),panel->total);snprintf(progress,sizeof(progress),"%s of %s",size,total);}
if(panel->phase==MTP_COPY_RECEIVE && panel->view!=PSMTP_VIEW_INSTALL_PROGRESS){char rate[128];psmtp_transfer_text(&panel->transfer,rate,sizeof(rate));ui_text(650,612,22,480,rate,MUTED);}
if(panel->view==PSMTP_VIEW_INSTALL_PROGRESS && !panel->total)snprintf(progress,sizeof(progress),"%s",panel->install_known?"System installer progress":"Waiting for system progress...");
ui_text(650,565,24,630,progress,MUTED);ui_box(1140,612,144,50,7,RGB(49,45,50),RGB(77,70,80),2);ui_center(1140,620,26,144,"Cancel",WHITE);
ui_rect(0,951,1920,129,RGB(21,18,19));footer(panel->view==PSMTP_VIEW_INSTALL_PROGRESS?3:1,panel->view==PSMTP_VIEW_INSTALL_PROGRESS?"Keep PSMTP open and the source connected. Circle stops this installation.":panel->display_items?"":"Circle cancels at a completed operation. Keep both devices connected.");
}
static void install_confirmation(void){
char size[64];bytes_text(size,sizeof(size),panel->source_total);
ui_shadow(535,288,850,506,12);ui_box(535,288,850,506,12,PANEL,LINE,2);ui_text(577,318,31,766,"Install this package?",WHITE);
text_lines(577,376,27,766,2,panel->name,WHITE);ui_text(577,467,24,766,size,MUTED);
ui_text(577,521,22,150,"Source",MUTED);ui_text(745,521,25,598,panel->destination?panel->destination:"Selected device",WHITE);
ui_text(577,565,21,766,panel->notice_text?panel->notice_text:"",MUTED);
if(panel->action==3){
ui_text(577,611,23,766,"Copy from phone, then install.",MUTED);
ui_text(577,648,22,766,"The temporary copy is removed when the attempt ends.",MUTED);
}else ui_text(577,623,23,766,"The source PKG stays on your device.",MUTED);
ui_box(1020,708,300,51,7,RGB(204,198,207),WHITE,1);ui_button(UI_BTN_CROSS,1052,718,28,RGB(34,29,34));ui_center(1094,716,26,200,"Install",RGB(34,29,34));control_hint(582,718,25,400,"Circle Cancel",WHITE);
ui_rect(0,951,1920,129,RGB(21,18,19));controller(78,986,0,"Install");controller(290,986,1,"Cancel");
}
static void review_overlay(int operation) {
char size[64],counts[160];const char *title=operation?(panel->action==2?"Delete selected items":panel->action==1?"Move selected items":"Copy selected items"):"Copy selected items";
ui_shadow(535,288,850,506,12);ui_box(535,288,850,506,12,RGB(29,27,28),RGB(60,54,61),2);ui_text(577,318,31,766,title,WHITE);text_lines(577,376,27,766,2,panel->name,WHITE);
bytes_text(size,sizeof(size),panel->source_total);snprintf(counts,sizeof(counts),"%zu files / %zu folders / %s",panel->files,panel->folders,size);ui_text(577,467,24,766,counts,MUTED);
if(operation && panel->action==2) {ui_text(577,531,26,766,"These items will be deleted permanently.",WHITE);ui_text(577,578,23,766,"There is no recycle bin. Verify your selection before continuing.",MUTED);}
else {ui_text(577,521,22,766,"Destination",MUTED);text_lines(577,556,25,766,2,panel->destination?panel->destination:PSMTP_COPY_DIRECTORY,WHITE);ui_text(577,639,21,766,panel->action==1?"Source removal follows successful integrity verification.":"Existing files are preserved. Every copied file is verified.",MUTED);}
ui_box(1020,708,300,51,7,RGB(204,198,207),WHITE,1);ui_button(UI_BTN_CROSS,1052,718,28,RGB(34,29,34));ui_center(1094,716,26,200,operation && panel->action==2?"Delete":panel->action==1?"Move":"Copy",RGB(34,29,34));control_hint(582,718,25,400,"Circle Cancel",WHITE);
ui_rect(0,951,1920,129,RGB(21,18,19));controller(78,986,0,"Confirm");controller(290,986,1,"Cancel");
}
static void notice_overlay(void) {
ui_shadow(535,340,850,360,12);ui_box(535,340,850,360,12,PANEL,LINE,2);
ui_text(577,378,31,766,panel->notice_title?panel->notice_title:"Transfer unavailable",WHITE);
{char text[512],*line,*next;int row=0;snprintf(text,sizeof(text),"%s",panel->notice_text?panel->notice_text:"Please check the destination and try again.");
line=text;do{next=strchr(line,'\n');if(next)*next++=0;ui_text(577,447+row*38,25,766,line,MUTED);line=next;}while(line && ++row<3);}
ui_box(1120,610,210,52,7,RGB(49,45,50),LINE,2);ui_button(UI_BTN_CROSS,1140,621,27,WHITE);ui_center(1178,618,25,130,"OK",WHITE);
ui_rect(0,951,1920,129,RGB(21,18,19));controller(78,986,0,"OK");controller(244,986,1,"Back");
}
static void properties_overlay(void) {
psmtp_ui_item item={0};char size[64],date[64],clock[48];
if(panel && panel->name) {item.name=panel->name;item.bytes=panel->source_total;item.folder=panel->folder;item.unknown=panel->size_unknown;item.modified=panel->modified;item.thumbnail=-1;}else if(!get_item(item_selected(),&item)) return;
ui_shadow(540,220,840,660,13);ui_box(540,220,840,660,13,RGB(29,27,28),RGB(60,54,61),2);ui_art(UI_INFO,578,250,36,37,WHITE);ui_text(639,248,31,680,"Properties",WHITE);text_lines(580,308,30,758,2,item.name,WHITE);
ui_rect(580,398,760,1,LINE);ui_text(580,420,25,200,"Type",MUTED);ui_text(830,420,25,510,psmtp_ui_kind(item.name,item.folder),WHITE);ui_text(580,466,25,200,"Size",MUTED);if(item.folder || item.unknown) strcpy(size,"Unavailable");else bytes_text(size,sizeof(size),item.bytes);ui_text(830,466,25,510,size,WHITE);
modified_text(item.modified,date,sizeof(date),clock,sizeof(clock));ui_text(580,512,25,200,"Modified",MUTED);ui_text(830,512,25,510,date,WHITE);ui_text(1100,512,25,240,clock,WHITE);
{const psmtp_metadata *m=metadata_at(item_selected());char extra[160];
if(m && m->title[0]){ui_text(580,558,22,190,"Title",MUTED);ui_text(830,558,22,510,m->title,WHITE);ui_text(580,594,22,190,"Content ID",MUTED);ui_text(830,594,19,510,m->content_id,WHITE);}
else if(m && m->width && m->height){snprintf(extra,sizeof(extra),"%u x %u %s",m->width,m->height,m->codec);ui_text(580,558,22,190,"Dimensions",MUTED);ui_text(830,558,22,510,extra,WHITE);if(m->duration_ms){snprintf(extra,sizeof(extra),"%llu:%02llu",(unsigned long long)(m->duration_ms/60000),(unsigned long long)(m->duration_ms/1000%60));ui_text(580,594,22,190,"Duration",MUTED);ui_text(830,594,22,510,extra,WHITE);}}
}
ui_text(580,642,22,758,"Full path",MUTED);text_lines(580,678,20,758,3,panel && panel->path?panel->path:disk?disk->path:"/",WHITE);
ui_box(1140,800,180,49,7,RGB(49,45,50),RGB(77,70,80),2);ui_center(1140,806,26,180,"Close",WHITE);
ui_rect(0,951,1920,129,RGB(21,18,19));controller(78,986,0,"Close");controller(244,986,1,"Back");
}
static void rename_overlay(void) {
size_t i;ui_shadow(360,220,1200,650,12);ui_box(360,220,1200,650,12,PANEL,LINE,2);ui_text(405,248,32,1110,panel->action==4?"New Folder":"Rename",WHITE);
ui_box(405,312,1110,62,5,RGB(18,16,18),WHITE,2);ui_text(423,322,28,1074,panel->edit_name?panel->edit_name:"",WHITE);
for(i=0;i<PSMTP_KEY_COUNT;++i){int x,y,w,h,selected=panel->index==i,action=psmtp_key_action(i);uint32_t c=selected?RGB(30,26,30):WHITE;char text[2]={psmtp_key_character(i,panel->keyboard_shift),0};psmtp_key_rect(i,&x,&y,&w,&h);
ui_box(x,y,w,h,5,selected?RGB(218,213,219):RGB(44,40,45),selected?WHITE:LINE,2);
if(action==PSMTP_KEY_BACKSPACE){int cx=x+w/2;ui_line(cx-21,y+28,cx-9,y+14,2,c,255);ui_line(cx-9,y+14,cx+23,y+14,2,c,255);ui_line(cx+23,y+14,cx+23,y+42,2,c,255);ui_line(cx+23,y+42,cx-9,y+42,2,c,255);ui_line(cx-9,y+42,cx-21,y+28,2,c,255);ui_button(UI_BTN_CROSS,cx-6,y+18,20,c);}
else if(action==PSMTP_KEY_SHIFT){ui_line(x+w/2,y+12,x+w/2-13,y+27,2,c,255);ui_line(x+w/2,y+12,x+w/2+13,y+27,2,c,255);ui_line(x+w/2,y+12,x+w/2,y+43,3,c,255);}
else ui_center(x,y+8,27,w,action==PSMTP_KEY_CLEAR?"Clear":action==PSMTP_KEY_SAVE?(panel->action==4?"Create":"Save"):i==42?"Space":text,c);
}
control_hint(408,774,22,1110,panel->action==4?"Cross Add Square Erase Triangle Shift L1 Clear R1 Create Circle Cancel":"Cross Add Square Erase Triangle Shift L1 Clear R1 Save Circle Cancel",WHITE);
if(panel->edit_error)ui_text(408,821,21,1110,panel->edit_error,RGB(244,173,55));
}
static void sort_overlay(void){
size_t i;ui_shadow(640,214,640,638,12);ui_box(640,214,640,638,12,PANEL,LINE,2);ui_text(680,246,32,560,"Sort by",WHITE);
for(i=0;i<PSMTP_SORT_COUNT;++i){int y=316+(int)i*65,selected=panel->index==i;uint32_t c=selected?RGB(30,26,30):WHITE;
ui_box(666,y-6,588,58,7,selected?RGB(218,213,219):0,selected?WHITE:LINE,selected?2:1);ui_ring(698,y+22,10,2,c,255);if((int)i==panel->sort_mode)ui_box(693,y+17,10,10,5,c,0,0);ui_text(733,y+5,26,488,psmtp_ui_sort_label((int)i),c);
}control_hint(681,798,22,560,"Cross Apply Circle Cancel",WHITE);
}
#ifndef PSMTP_PRODUCTION
static void settings_page(const char *status){
static const char *const labels[]={"View","Refresh folder","Refresh devices","Clear clipboard","Diagnostics","Controller guide","Test connection sound","Exit PSMTP"};
static const char *const details[]={"Choose a grid or list for your files.","Update the files in your current folder.","Find connected phones and USB storage.","Remove the copied or moved selection.","Read the app's saved diagnostic log.","View navigation and file action controls.","Play both connection cues.","Return to the PlayStation home screen."};
size_t i,index=panel->index<8?panel->index:0;ui_art(UI_SETTINGS,80,160,42,42,WHITE);ui_text(146,154,38,1550,"Settings",WHITE);ui_text(84,212,24,1700,"Explorer preferences",MUTED);
for(i=0;i<8;++i){int y=270+(int)i*75,selected=index==i;uint32_t c=selected?RGB(30,26,30):WHITE;ui_box(80,y,1050,64,8,selected?RGB(218,213,219):RGB(31,28,31),selected?WHITE:LINE,selected?2:1);ui_text(111,y+13,27,805,labels[i],c);if(!i)ui_text(905,y+15,24,180,panel->list_view?"List":"Grid",c);}
ui_rect(1190,270,1,590,LINE);ui_text(1240,287,30,585,labels[index],WHITE);text_lines(1240,356,25,585,5,details[index],MUTED);
controller(78,979,0,"Select");controller(282,979,1,"Back");if(status)control_hint(78,1040,18,1750,status,MUTED);
}
#endif
static void loading_overlay(void) {
char count[96];ui_shadow(612,398,712,286,13);ui_box(612,398,712,286,13,RGB(29,27,28),RGB(56,51,56),2);ui_text(650,432,29,630,"Reading folder...",WHITE);snprintf(count,sizeof(count),"%" PRIu64 " of %" PRIu64 " items",panel->done,panel->total);ui_text(650,493,26,630,count,MUTED);
ui_box(650,549,630,8,4,RGB(57,53,61),0,0);if(panel->total) ui_box(650,549,(int)(630*percent(panel->done,panel->total)/100),8,4,RGB(202,198,208),0,0);control_hint(650,607,24,630,"Circle Cancel",WHITE);
ui_rect(0,951,1920,129,RGB(21,18,19));controller(78,986,1,"Cancel");
}
#ifndef PSMTP_PRODUCTION
static void logs_view(const diag_log *log,size_t scroll) {
uint64_t end=diag_log_end(log),start,i;toolbar();ui_text(405,221,30,1040,"Diagnostics",WHITE);if(scroll>end-log->first)scroll=(size_t)(end-log->first);end-=scroll;start=end>24?end-24:0;if(start<log->first)start=log->first;
ui_box(404,278,1040,638,3,RGB(25,23,25),LINE,1);for(i=start;i<end;++i) ui_text(422,289+(int)(i-start)*25,19,1006,diag_log_line(log,i),RGB(194,186,195));
ui_text(1529,223,29,345,"Saved diagnostics",WHITE);text_lines(1529,293,24,345,5,"/data/psmtp/psmtp-scan.log",MUTED);control_hint(1529,450,24,345,"D-pad Scroll",WHITE);control_hint(1529,500,24,345,"L1 / R1 Pages",WHITE);control_hint(1529,550,24,345,"Circle Back to files",WHITE);footer(0,"Circle / Options Back to files");
}
#endif
void psmtp_console_render(uint32_t *frame,const diag_log *log,size_t scroll,const char *status,int names,const char *save,const psmtp_console_selection *s,const mtp_navigator *browser) {
(void)save;show_names=names;panel=s;phone=browser?browser:s?s->browser:NULL;disk=s?s->local:NULL;devices=s?s->locations:NULL;ui_draw_begin(frame);
if(psmtp_font_init()) {ui_rect(0,0,1920,1080,RGB(120,20,20));return;}ui_backdrop();header();
#ifndef PSMTP_PRODUCTION
if(s && s->view==PSMTP_VIEW_SETTINGS){settings_page(status);return;}
#else
(void)log;(void)scroll;
#endif
sidebar();
#ifndef PSMTP_PRODUCTION
if(s && s->view==PSMTP_VIEW_LOGS) logs_view(log,scroll);
else
#endif
{explorer(status);if(s && s->view==PSMTP_VIEW_CONTROLS) {static const char *const help[]={"D-pad: select tiles. Up from first row opens view / sort.","X: open. Circle: back. Triangle / Options: context menu.","Copy and Delete are available in the Options menu.","L3: Grid / List. L1 / R1: previous / next page.","L2 / R2: previous / next phone storage. Touchpad: refresh.","Move uses the clipboard. Choose Paste at the destination.","Options: create folders, manage files and exit the app."};int i;ui_box(500,252,920,560,12,PANEL,LINE,2);ui_text(540,285,32,840,"Controller guide",WHITE);for(i=0;i<7;++i) control_hint(540,366+i*54,23,840,help[i],WHITE);}
else if(s && s->view==PSMTP_VIEW_INSTALL_CONFIRM) install_confirmation();else if(s && s->view==PSMTP_VIEW_NOTICE) notice_overlay();else if(s && s->view==PSMTP_VIEW_RENAME) rename_overlay();else if(s && s->view==PSMTP_VIEW_MENU) menu_overlay(0);else if(s && s->view==PSMTP_VIEW_SORT) sort_overlay();else if(s && s->view==PSMTP_VIEW_INFO) properties_overlay();else if(s && s->view==PSMTP_VIEW_OPERATION) review_overlay(1);else if(s && s->view==PSMTP_VIEW_LOADING) loading_overlay();else if(s && s->copy_mode==1) review_overlay(0);else if(s && s->copy_mode==2) transfer_overlay();
else if(s && s->view==PSMTP_VIEW_PICK) {ui_shadow(612,398,712,286,13);ui_box(612,398,712,286,13,PANEL,LINE,2);ui_text(650,435,30,630,"Phone ready to browse",WHITE);ui_text(650,510,25,630,"Unlock your phone and choose File Transfer.",MUTED);control_hint(650,612,26,630,"Cross Open Circle Cancel",WHITE);}}
}
+49
View File
@@ -0,0 +1,49 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#ifndef PSMTP_CONSOLE_RENDER_H
#define PSMTP_CONSOLE_RENDER_H
#include "psmtp/diag_log.h"
#include "psmtp/navigator.h"
#include "file_store.h"
#include "psmtp/locations.h"
#include "psmtp/metadata.h"
#include "psmtp/transfer_meter.h"
#define PSMTP_SCREEN_WIDTH 1920
#define PSMTP_SCREEN_HEIGHT 1080
typedef struct {const char *name,*modified;uint64_t bytes;size_t items;int folder,unknown,items_known,thumbnail;} psmtp_ui_item;
typedef struct {
size_t index,count;
uint16_t vendor_id,product_id;
int copy_mode,phase;
uint32_t handle;
uint64_t done,total,source_total;
const char *name;
int view,folder;
size_t files,folders,item_index,item_count;
const psmtp_local_browser *local;
const psmtp_locations *locations;
int drawer_focus;
const char *clipboard_label,*destination;
int operation,action,can_select,can_paste;
int list_view,drawer_settings,menu_kind,size_unknown,transfer_active;
const mtp_navigator *browser;
const char *modified,*clock_text,*display_version,*path;
const psmtp_ui_item *display_items;size_t display_count,display_selected;
const psmtp_ui_item *detail_override;uint64_t elapsed_ms;
psmtp_transfer_meter transfer;
const psmtp_metadata *(*metadata)(void *,size_t);void *metadata_user;
const char *notice_title,*notice_text;
int toolbar_focus,sort_mode,rename_allowed,can_create,install_percent,install_known,install_preparing;const char *edit_name,*edit_error;int keyboard_shift;
} psmtp_console_selection;
enum { PSMTP_MENU_OPEN,PSMTP_MENU_NEW_FOLDER,PSMTP_MENU_COPY,PSMTP_MENU_MOVE,PSMTP_MENU_PASTE,PSMTP_MENU_RENAME,PSMTP_MENU_DELETE,PSMTP_MENU_PROPERTIES,PSMTP_MENU_EXIT,PSMTP_MENU_COUNT };
enum { PSMTP_VIEW_HOME,PSMTP_VIEW_PHONE,PSMTP_VIEW_LOCAL,PSMTP_VIEW_TRANSFERS,PSMTP_VIEW_LOGS,PSMTP_VIEW_PICK,PSMTP_VIEW_INFO,PSMTP_VIEW_MENU,PSMTP_VIEW_LOADING,PSMTP_VIEW_OPERATION,PSMTP_VIEW_SETTINGS,PSMTP_VIEW_CONTROLS,PSMTP_VIEW_RENAME,PSMTP_VIEW_SORT,PSMTP_VIEW_NOTICE,PSMTP_VIEW_INSTALL_CONFIRM,PSMTP_VIEW_INSTALL_PROGRESS };
void psmtp_console_render(uint32_t *, const diag_log *, size_t, const char *, int, const char *, const psmtp_console_selection *,const mtp_navigator *);
#endif
+274
View File
@@ -0,0 +1,274 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#define _GNU_SOURCE
#include "copy_file.h"
#include "storage_space.h"
#include "stat_abi.h"
#include "psmtp/ui_format.h"
#include <errno.h>
#include <string.h>
#include <inttypes.h>
#include <time.h>
#include <stdlib.h>
#ifdef PSMTP_TEST_FILE_SYSTEM
#include "ps4_copy_test_api.h"
#else
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
typedef char offset_is_64[(sizeof(off_t)==8)?1:-1];
#endif
static int failure(psmtp_copy_file *f,const char *stage,int64_t rc) {
int error=errno;
if(!f->error_stage[0]) { snprintf(f->error_stage,sizeof(f->error_stage),"%s",stage); f->error_number=error; }
fprintf(f->log,"stage=copy-fs-%s rc=%" PRId64 " errno=%d\n",stage,rc,error); return error==ENOSPC?MTP_NO_SPACE:MTP_FILESYSTEM;
}
/* The SDK typedef has a 32-bit mode_t, shifting size/blocks by eight bytes.
* Pass native storage through void*; never decode the SDK struct stat. */
static int status(psmtp_copy_file *f,int fd,const char *path,psmtp_native_stat *out) {
union { psmtp_native_stat stat; uint8_t backing[512]; } data; int rc,e=0;
if(path) {
/* lstat resolves on PS4 but returns EPERM. Open without following the
* final component, then inspect the descriptor, as for the pinned root. */
fd=open(path,O_RDONLY|O_NOFOLLOW|O_CLOEXEC|O_NONBLOCK);
if(fd<0) { fprintf(f->log,"stage=copy-fs-path-check path=%s\n",path); return failure(f,"path-open",fd); }
}
memset(&data,0,sizeof(data)); rc=fstat(fd,(void *)&data.stat);
if(rc) { fprintf(f->log,"stage=copy-fs-descriptor-query fd=%d path=%s\n",fd,path?path:"(pinned descriptor)"); e=failure(f,"fstat",rc); }
else *out=data.stat;
if(path) { rc=close(fd); if(rc && !e) e=failure(f,"path-close",rc); }
return e;
}
static int identity(psmtp_copy_file *f,const char *path,uint64_t dev,uint64_t ino,int directory) {
psmtp_native_stat s; int e=status(f,-1,path,&s); if(e) return e;
if((directory?!S_ISDIR(s.st_mode):!S_ISREG(s.st_mode)) || (uint64_t)s.st_dev!=dev || (uint64_t)s.st_ino!=ino) {
fprintf(f->log,"stage=copy-fs-identity observed-mode=%o dev=%" PRIu64 " ino=%" PRIu64 " expected-dev=%" PRIu64 " ino=%" PRIu64 "\n",
(unsigned)s.st_mode,(uint64_t)s.st_dev,(uint64_t)s.st_ino,dev,ino);
return failure(f,"identity-changed",-1);
}
if(directory && path==f->directory_path &&
(f->directory_readable ? (s.st_mode&0777)!=0755 : (s.st_mode&0077)!=0)) return failure(f,"directory-permissions",-1);
return 0;
}
static int copy_path(char *out,size_t cap,const char *parent,const char *name) {
size_t a=strlen(parent),b=strlen(name);
if(a+1+b>=cap) return MTP_LIMIT;
memcpy(out,parent,a); out[a]='/'; memcpy(out+a+1,name,b+1); return 0;
}
static int parent_check(psmtp_copy_file *f) {
char walk[256]; size_t i,n=strlen(f->parent_path),base=strlen(PSMTP_COPY_DIRECTORY); psmtp_native_stat st;
if(!n) return 0;
if(n>=sizeof(walk) || strncmp(f->parent_path,PSMTP_COPY_DIRECTORY,base) || f->parent_path[base]!='/') return MTP_STATE;
memcpy(walk,f->parent_path,n+1);
for(i=base+1;i<=n;++i) if(walk[i]=='/' || !walk[i]) {
char save=walk[i]; int e;
walk[i]=0; e=status(f,-1,walk,&st); walk[i]=save;
if(e) return e;
if(!S_ISDIR(st.st_mode)) return failure(f,"parent-type",-1);
}
return f->parent>=0?identity(f,f->parent_path,f->parent_dev,f->parent_ino,1):0;
}
static int chain(psmtp_copy_file *f) {
int e=identity(f,"/data",f->data_dev,f->data_ino,1); if(e) return e;
e=identity(f,PSMTP_LOG_DIRECTORY,f->base_dev,f->base_ino,1); if(e) return e;
e=identity(f,PSMTP_COPY_DIRECTORY,f->root_dev,f->root_ino,1); if(e) return e;
e=parent_check(f); if(e) return e;
return f->owned_directory?identity(f,f->directory_path,f->dir_dev,f->dir_ino,1):0;
}
static int begin(void *v,const mtp_copy_source *s) {
psmtp_copy_file *f=v; unsigned attempt; uint64_t stamp=(uint64_t)time(NULL); int rc,e; psmtp_native_stat st;
e=psmtp_space_require("/",PSMTP_COPY_DIRECTORY,s->size,f->log);if(e)return e;
f->expected_size=s->size;
fputs("stage=copy-fs-stat-abi bytes=120 mode-offset=8 mode-bytes=2 size-offset=72 blocks-offset=80\n",f->log);
fputs("stage=copy-fs-method private-directory absolute-open descriptor-checks; no openat/lstat/hard-link calls\n",f->log);
f->data=open("/data",O_RDONLY|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC);
if(f->data<0) return failure(f,"open-data",f->data);
e=status(f,f->data,NULL,&st); if(e) return e; f->data_dev=st.st_dev; f->data_ino=st.st_ino;
e=identity(f,"/data",f->data_dev,f->data_ino,1); if(e) return e;
rc=mkdir(PSMTP_LOG_DIRECTORY,0755); if(rc && errno!=EEXIST) return failure(f,"mkdir-base",rc);
f->base=open(PSMTP_LOG_DIRECTORY,O_RDONLY|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC);
if(f->base<0) return failure(f,"open-base",f->base);
e=status(f,f->base,NULL,&st); if(e) return e; f->base_dev=st.st_dev; f->base_ino=st.st_ino;
e=identity(f,PSMTP_LOG_DIRECTORY,f->base_dev,f->base_ino,1); if(e) return e;
rc=mkdir(PSMTP_COPY_DIRECTORY,0755); if(rc && errno!=EEXIST) return failure(f,"mkdir",rc);
f->root=open(PSMTP_COPY_DIRECTORY,O_RDONLY|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC);
if(f->root<0) return failure(f,"open-directory",f->root);
fputs("stage=copy-fs-directory-open path=" PSMTP_COPY_DIRECTORY " method=absolute-nofollow\n",f->log);
e=status(f,f->root,NULL,&st); if(e) return e; f->root_dev=st.st_dev; f->root_ino=st.st_ino;
if(f->parent_path[0]) {
e=parent_check(f); if(e) return e;
f->parent=open(f->parent_path,O_RDONLY|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC);
if(f->parent<0) return failure(f,"open-copy-parent",f->parent);
e=status(f,f->parent,NULL,&st); if(e) return e; f->parent_dev=st.st_dev; f->parent_ino=st.st_ino;
}
for(attempt=0;attempt<128;++attempt) {
e=chain(f); if(e) return e;
snprintf(f->directory_name,sizeof(f->directory_name),"psmtp-%" PRIu64 "-%08" PRIx32 "-%u",stamp,s->handle,attempt);
e=copy_path(f->directory_path,sizeof(f->directory_path),f->parent_path[0]?f->parent_path:PSMTP_COPY_DIRECTORY,f->directory_name); if(e) return e;
rc=mkdir(f->directory_path,0700);
if(rc) { if(errno==EEXIST) continue; return failure(f,"reserve-directory",rc); }
f->owned_directory=1;
e=status(f,-1,f->directory_path,&st); if(e) return e; f->dir_dev=st.st_dev; f->dir_ino=st.st_ino;
f->directory=open(f->directory_path,O_RDONLY|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC);
if(f->directory<0) return failure(f,"open-private-directory",f->directory);
e=status(f,f->directory,NULL,&st); if(e) return e;
if((uint64_t)st.st_dev!=f->dir_dev || (uint64_t)st.st_ino!=f->dir_ino) return failure(f,"directory-replaced",-1);
e=chain(f); if(e) return e;
e=copy_path(f->temporary,sizeof(f->temporary),f->directory_path,"transfer.part"); if(e) return e;
{
char extension[12],leaf[20];psmtp_ui_extension(extension,s->info.name,s->is_pkg);
snprintf(leaf,sizeof(leaf),"file.%s",extension);
e=copy_path(f->path,sizeof(f->path),f->directory_path,leaf); if(e) return e;
}
f->file=open(f->temporary,O_RDWR|O_CREAT|O_EXCL|O_NOFOLLOW|O_CLOEXEC,0600);
if(f->file<0) return failure(f,"create",f->file);
f->owned=1; e=status(f,f->file,NULL,&st); if(e) return e;
f->file_dev=st.st_dev; f->file_ino=st.st_ino;
e=identity(f,f->temporary,f->file_dev,f->file_ino,0); if(e) return e;
fprintf(f->log,"stage=copy-fs-created target=%s exclusive-directory=1 exclusive-file=1\n",f->path); return 0;
}
return failure(f,"name-limit",-1);
}
static int put(void *v,const uint8_t *p,size_t n,size_t *actual) {
psmtp_copy_file *f=v; ssize_t rc;
do { rc=write(f->file,p,n); } while(rc<0 && errno==EINTR);
*actual=rc<0?0:(size_t)rc; return rc<0?failure(f,"write",rc):0;
}
static int sync_file(void *v) { psmtp_copy_file *f=v; int rc=fsync(f->file); return rc?failure(f,"sync-file",rc):0; }
static int rewind_file(void *v) { psmtp_copy_file *f=v; int64_t rc=lseek(f->file,0,SEEK_SET); return rc?failure(f,"rewind",rc):0; }
static int get(void *v,uint8_t *p,size_t n,size_t *actual) {
psmtp_copy_file *f=v; ssize_t rc;
do { rc=read(f->file,p,n); } while(rc<0 && errno==EINTR);
*actual=rc<0?0:(size_t)rc; return rc<0?failure(f,"read-back",rc):0;
}
static int space(void *v,uint64_t *available) {
psmtp_copy_file *f=v;psmtp_native_stat st;int e;*available=0;
int fds[5]={f->data,f->base,f->root,f->parent,f->directory};
uint64_t devs[5]={f->data_dev,f->base_dev,f->root_dev,f->parent_dev,f->dir_dev};
uint64_t inos[5]={f->data_ino,f->base_ino,f->root_ino,f->parent_ino,f->dir_ino};size_t i;
for(i=0;i<5;++i)if(fds[i]>=0){e=status(f,fds[i],NULL,&st);if(e)return e;if(!S_ISDIR(st.st_mode) || (uint64_t)st.st_dev!=devs[i] || (uint64_t)st.st_ino!=inos[i])return MTP_VERIFY;
if(i==4 && (f->directory_readable?(st.st_mode&0777)!=0755:(st.st_mode&0077)!=0))return MTP_VERIFY;}
e=status(f,f->file,NULL,&st);if(e)return e;
if(!S_ISREG(st.st_mode) || (uint64_t)st.st_dev!=f->file_dev || (uint64_t)st.st_ino!=f->file_ino || st.st_size<0 || (uint64_t)st.st_size>f->expected_size)return MTP_VERIFY;
return MTP_COPY_SPACE_STREAMING;
}
static int readable(psmtp_copy_file *f,int fd,unsigned mode,int directory) {
psmtp_native_stat st; int rc=fchmod(fd,mode),e;
if(rc) return failure(f,directory?"share-directory":"share-file",rc);
e=status(f,fd,NULL,&st); if(e) return e;
if((directory?!S_ISDIR(st.st_mode):!S_ISREG(st.st_mode)) ||
(uint64_t)st.st_dev!=(directory?f->dir_dev:f->file_dev) ||
(uint64_t)st.st_ino!=(directory?f->dir_ino:f->file_ino) || (st.st_mode&0777)!=mode) {
errno=EIO; return failure(f,"share-permissions-check",-1);
}
fprintf(f->log,"stage=copy-access object=%s mode=%04o uid=%" PRIu32 " gid=%" PRIu32 "\n",
directory?"directory":"file",(unsigned)(st.st_mode&0777),st.st_uid,st.st_gid);
return 0;
}
static int publish(void *v,int *published) {
psmtp_copy_file *f=v; uint64_t ignored; psmtp_native_stat st; int target,rc,e=space(f,&ignored);
if(e!=MTP_COPY_SPACE_STREAMING) return e?e:MTP_STATE;
e=status(f,f->file,NULL,&st);if(e)return e;
if(st.st_size<0 || (uint64_t)st.st_size!=f->expected_size)return MTP_VERIFY;
e=chain(f); if(e) return e;
e=identity(f,f->temporary,f->file_dev,f->file_ino,0); if(e) return e;
errno=0; target=open(f->path,O_RDONLY|O_NOFOLLOW|O_CLOEXEC|O_NONBLOCK);
if(target>=0) { e=failure(f,"unexpected-target",target); (void)close(target); return e; }
if(errno!=ENOENT) return failure(f,"target-check",target);
e=readable(f,f->file,0644,0); if(e) return e;
rc=fsync(f->file); if(rc) return failure(f,"sync-file-permissions",rc);
rc=close(f->file); f->file=-1; if(rc) return failure(f,"close-file",rc);
rc=rename(f->temporary,f->path); if(rc) return failure(f,"publish-private",rc);
*published=f->published=1; f->owned=0;
fprintf(f->log,"stage=copy-published target=%s method=private-directory-rename; installation-not-started\n",f->path);
e=identity(f,f->path,f->file_dev,f->file_ino,0); if(e) return e;
rc=fsync(f->directory); if(rc) return failure(f,"sync-private-directory",rc);
if(f->parent>=0) { rc=fsync(f->parent); if(rc) return failure(f,"sync-copy-parent",rc); }
rc=fsync(f->root); return rc?failure(f,"sync-directory",rc):0;
}
static int dispose(void *v) {
psmtp_copy_file *f=v; int error=0,rc,e;
if(f->file>=0) { rc=close(f->file); if(rc) error=failure(f,"close-temp",rc); f->file=-1; }
if(f->owned) {
e=chain(f); if(!e) e=identity(f,f->temporary,f->file_dev,f->file_ino,0);
if(e) error=e;
else { rc=unlink(f->temporary); if(rc) error=failure(f,"remove-temp",rc); else f->owned=0; }
}
if(f->owned_directory && !f->published && !f->owned) {
e=chain(f); if(e) error=e;
else { rc=rmdir(f->directory_path); if(rc) error=failure(f,"remove-empty-directory",rc); else f->owned_directory=0; }
}
if(!error && f->published && !f->owned) {
e=chain(f); if(!e) e=identity(f,f->path,f->file_dev,f->file_ino,0);
if(!e) e=readable(f,f->directory,0755,1);
if(e) error=e;
else {
f->directory_readable=1;
e=chain(f); if(e) error=e;
if(!error) { rc=fsync(f->directory); if(rc) error=failure(f,"sync-readable-directory",rc); }
if(!error) { rc=fsync(f->root); if(rc) error=failure(f,"sync-readable-root",rc); }
if(!error) fprintf(f->log,"stage=copy-access-ready target=%s directory-mode=0755 file-mode=0644\n",f->path);
}
}
if(f->root>=0 && f->owned_directory==0) { rc=fsync(f->root); if(rc) error=failure(f,"sync-cleanup",rc); }
if(f->directory>=0) { rc=close(f->directory); if(rc) error=failure(f,"close-private-directory",rc); f->directory=-1; }
if(f->parent>=0) { rc=fsync(f->parent); if(rc) error=failure(f,"sync-copy-parent",rc); rc=close(f->parent); if(rc) error=failure(f,"close-copy-parent",rc); f->parent=-1; }
if(f->root>=0) { rc=close(f->root); if(rc) error=failure(f,"close-directory",rc); f->root=-1; }
if(f->base>=0) { rc=close(f->base); if(rc) error=failure(f,"close-base",rc); f->base=-1; }
if(f->data>=0) { rc=close(f->data); if(rc) error=failure(f,"close-data",rc); f->data=-1; }
if(error) fprintf(f->log,"stage=copy-cleanup-warning directory=%s temp=%s; inspect owned entries\n",f->directory_path,f->temporary);
return error;
}
int psmtp_copy_file_discard(psmtp_copy_file *f){
int rc,e,root;psmtp_native_stat st;const char *leaf;
if(!f || !f->owned_directory)return 0;
leaf=strrchr(f->directory_path,'/');
if(!leaf || strncmp(leaf+1,"psmtp-",6) || strncmp(f->directory_path,PSMTP_COPY_DIRECTORY "/",strlen(PSMTP_COPY_DIRECTORY)+1))return MTP_VERIFY;
e=chain(f);if(e)return e;
if(f->owned || f->published){const char *path=f->published?f->path:f->temporary;
e=identity(f,path,f->file_dev,f->file_ino,0);if(e)return e;
if(unlink(path))return failure(f,"discard-payload",-1);
f->owned=f->published=0;
}
e=chain(f);if(e)return e;
if(rmdir(f->directory_path))return failure(f,"discard-directory",-1);f->owned_directory=0;
root=open(PSMTP_COPY_DIRECTORY,O_RDONLY|O_DIRECTORY|O_NOFOLLOW|O_CLOEXEC);if(root<0)return failure(f,"discard-root",root);
e=status(f,root,NULL,&st);
if(!e && ((uint64_t)st.st_dev!=f->root_dev || (uint64_t)st.st_ino!=f->root_ino))e=MTP_VERIFY;
if(!e && fsync(root))e=failure(f,"discard-sync",-1);rc=close(root);if(rc && !e)e=failure(f,"discard-close",rc);
if(f->log)fprintf(f->log,"stage=install-staging-discard status=%d payload-removed=1 directory-removed=1\n",e);
return e;
}
void psmtp_copy_file_init(psmtp_copy_file *f,FILE *out,mtp_copy_sink *sink) {
memset(f,0,sizeof(*f)); f->parent=f->file=f->directory=f->root=f->base=f->data=-1; f->log=out;
sink->user=f; sink->begin=begin; sink->space=space; sink->write=put; sink->sync=sync_file;
sink->rewind=rewind_file; sink->read=get; sink->publish=publish; sink->dispose=dispose; sink->prepare=NULL;
}
int psmtp_copy_file_init_at(psmtp_copy_file *f,FILE *out,mtp_copy_sink *sink,const char *parent) {
size_t n=strlen(parent),base=strlen(PSMTP_COPY_DIRECTORY),i;
psmtp_copy_file_init(f,out,sink);
if(n>=sizeof(f->parent_path) || n<=base || strncmp(parent,PSMTP_COPY_DIRECTORY,base) || parent[base]!='/') return MTP_STATE;
for(i=base+1;i<n;++i) if(!((parent[i]>='a' && parent[i]<='z') || (parent[i]>='0' && parent[i]<='9') || parent[i]=='-' || parent[i]=='/' || parent[i]=='_')) return MTP_STATE;
if(parent[n-1]=='/' || strstr(parent,"//")) return MTP_STATE;
memcpy(f->parent_path,parent,n+1); return 0;
}
+27
View File
@@ -0,0 +1,27 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#ifndef PSMTP_COPY_FILE_H
#define PSMTP_COPY_FILE_H
#include "psmtp/copy.h"
#include "psmtp/copy_paths.h"
typedef struct {
int data,base,root,parent,directory,file,owned,owned_directory,published,directory_readable;
uint64_t data_dev,data_ino,base_dev,base_ino,root_dev,root_ino,dir_dev,dir_ino,file_dev,file_ino;
uint64_t expected_size;
uint64_t parent_dev,parent_ino;
char parent_path[256],directory_name[80],directory_path[256],temporary[256],path[256];
char error_stage[40]; int error_number;
FILE *log;
} psmtp_copy_file;
int psmtp_copy_file_discard(psmtp_copy_file *);
void psmtp_copy_file_init(psmtp_copy_file *,FILE *,mtp_copy_sink *);
int psmtp_copy_file_init_at(psmtp_copy_file *,FILE *,mtp_copy_sink *,const char *);
#endif
+76
View File
@@ -0,0 +1,76 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#include "device_audio.h"
#include "psmtp/device_watch.h"
#include "psmtp/device_sound.h"
#ifdef PSMTP_TEST_AUDIO
#include "ps4_audio_test_api.h"
#else
#include <orbis/libkernel.h>
#include <orbis/AudioOut.h>
#include <orbis/UserService.h>
#endif
#include <stdint.h>
#include <inttypes.h>
#include <string.h>
static struct {int port,started,reported;int stop,pending,error;unsigned played,reported_played;OrbisPthread thread;} audio={.port=-1};
static int stopped(void) {return __atomic_load_n(&audio.stop,__ATOMIC_ACQUIRE);}
static void *playback(void *unused) {
static int16_t pcm[PSMTP_CHIME_BLOCK*2];unsigned event=0;size_t frame=0;(void)unused;
while(!stopped()) {
int32_t rc;
if(!event){unsigned pending=(unsigned)__atomic_exchange_n(&audio.pending,0,__ATOMIC_ACQ_REL);if(pending){event=pending&PSMTP_DEVICE_REMOVED?PSMTP_DEVICE_REMOVED:PSMTP_DEVICE_ADDED;frame=0;pending&=~event;if(pending)__atomic_fetch_or(&audio.pending,(int)pending,__ATOMIC_RELEASE);}}
if(event)psmtp_chime_render(event==PSMTP_DEVICE_ADDED,frame,pcm,PSMTP_CHIME_BLOCK);else memset(pcm,0,sizeof(pcm));
rc=sceAudioOutOutput(audio.port,pcm);if(rc>=0)rc=sceAudioOutOutput(audio.port,NULL);
if(rc<0){__atomic_store_n(&audio.error,rc,__ATOMIC_RELEASE);return NULL;}
if(event){frame+=PSMTP_CHIME_BLOCK;if(frame>=PSMTP_CHIME_FRAMES){event=0;__atomic_fetch_add(&audio.played,1,__ATOMIC_RELEASE);}}
}return NULL;
}
void psmtp_audio_open(FILE *log) {
int32_t rc,user=255;int32_t volumes[8]={32768,32768,32768,32768,32768,32768,32768,32768};
if(audio.started) return;
rc=sceAudioOutInit();fprintf(log,"stage=audio-init status=0x%08" PRIx32 "\n",(uint32_t)rc);if(rc && (uint32_t)rc!=UINT32_C(0x8026000e)) return;
audio.port=sceAudioOutOpen(user,ORBIS_AUDIO_OUT_PORT_TYPE_MAIN,0,PSMTP_CHIME_BLOCK,PSMTP_CHIME_RATE,ORBIS_AUDIO_OUT_PARAM_FORMAT_S16_STEREO);
fprintf(log,"stage=audio-open status=%d rate=%u frames=%u\n",audio.port,PSMTP_CHIME_RATE,PSMTP_CHIME_BLOCK);if(audio.port<0) return;
rc=sceAudioOutSetVolume(audio.port,3,volumes);fprintf(log,"stage=audio-volume status=0x%08" PRIx32 " user=system route=main-stereo\n",(uint32_t)rc);
if(rc<0){(void)sceAudioOutClose(audio.port);audio.port=-1;return;}
audio.stop=audio.pending=audio.error=audio.reported=0;audio.played=audio.reported_played=0;
rc=scePthreadCreate(&audio.thread,NULL,playback,NULL,"psmtp-device-audio");
fprintf(log,"stage=audio-worker status=0x%08" PRIx32 "\n",(uint32_t)rc);
if(rc) {int32_t closed=sceAudioOutClose(audio.port);fprintf(log,"stage=audio-close status=0x%08" PRIx32 "\n",(uint32_t)closed);audio.port=-1;return;}
audio.started=1;
}
void psmtp_audio_status(FILE *log) {
int error=__atomic_load_n(&audio.error,__ATOMIC_ACQUIRE);unsigned played=__atomic_load_n(&audio.played,__ATOMIC_ACQUIRE);
if(played!=audio.reported_played){audio.reported_played=played;fprintf(log,"stage=audio-cue-completed count=%u route=main-stereo\n",played);}
if(error && !audio.reported) {audio.reported=1;fprintf(log,"stage=audio-output status=0x%08" PRIx32 " sound-disabled=1\n",(uint32_t)error);}
}
void psmtp_audio_notify(unsigned events,FILE *log) {
events&=PSMTP_DEVICE_ADDED|PSMTP_DEVICE_REMOVED;
psmtp_audio_status(log);
if(!events) return;
if(audio.started && __atomic_load_n(&audio.error,__ATOMIC_ACQUIRE))psmtp_audio_close(log);
if(!audio.started)psmtp_audio_open(log);
if(audio.started && !__atomic_load_n(&audio.error,__ATOMIC_ACQUIRE)) __atomic_fetch_or(&audio.pending,(int)events,__ATOMIC_RELEASE);
fprintf(log,"stage=device-sound added=%d removed=%d enabled=%d\n",!!(events&PSMTP_DEVICE_ADDED),!!(events&PSMTP_DEVICE_REMOVED),audio.started && !audio.reported);
}
void psmtp_audio_close(FILE *log) {
int32_t rc;if(!audio.started) return;
__atomic_store_n(&audio.stop,1,__ATOMIC_RELEASE);
rc=scePthreadJoin(audio.thread,NULL);fprintf(log,"stage=audio-join status=0x%08" PRIx32 "\n",(uint32_t)rc);
if(rc) return;
psmtp_audio_status(log);audio.started=0;
rc=sceAudioOutClose(audio.port);fprintf(log,"stage=audio-close status=0x%08" PRIx32 "\n",(uint32_t)rc);audio.port=-1;
}
+17
View File
@@ -0,0 +1,17 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#ifndef PSMTP_DEVICE_AUDIO_H
#define PSMTP_DEVICE_AUDIO_H
#include <stdio.h>
void psmtp_audio_open(FILE *);
void psmtp_audio_notify(unsigned,FILE *);
void psmtp_audio_status(FILE *);
void psmtp_audio_close(FILE *);
#endif
+21
View File
@@ -0,0 +1,21 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#ifndef PSMTP_DIRENT_CHECK_H
#define PSMTP_DIRENT_CHECK_H
/* readdir owns traversal of its variable-length native buffer. BSD permits
* the final record to absorb block slack, beyond sizeof(struct dirent).
* Check only the prefix/name we actually read; never copy d_reclen bytes.
* Reference: FreeBSD releng/9.0 sys/ufs/ufs/dir.h (directory block layout). */
static int psmtp_dirent_valid(const struct dirent *entry) {
size_t n=entry->d_namlen;
return n<sizeof(entry->d_name) && entry->d_reclen>=8u+n+1u &&
entry->d_name[n]==0 && memchr(entry->d_name,0,n)==NULL;
}
#endif
+45
View File
@@ -0,0 +1,45 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#include "exit.h"
#include <orbis/libkernel.h>
#include <orbis/Sysmodule.h>
#include <orbis/SystemService.h>
#include <inttypes.h>
#include <unistd.h>
static void sync_log(FILE *log) {
if(log) { (void)fflush(log); (void)fsync(fileno(log)); }
}
void psmtp_ps4_exit(int status,FILE *log) {
uint32_t loaded; int32_t rc;
if(log) fprintf(log,"stage=app-exit begin status=%d method=system-service\n",status);
sync_log(log);
loaded=sceSysmoduleLoadModuleInternal(ORBIS_SYSMODULE_INTERNAL_SYSTEM_SERVICE);
if(log) fprintf(log,"stage=exit-system-service-load rc=0x%08" PRIx32 "\n",loaded);
if(!loaded) {
if(log) fputs("stage=app-exit-request begin target=exit\n",log);
sync_log(log);
rc=sceSystemServiceLoadExec("exit",NULL);
if(log) fprintf(log,"stage=app-exit-request returned=1 rc=0x%08" PRIx32 "\n",(uint32_t)rc);
}
if(log) fputs("stage=app-exit-wait; use PS button / Close Application if still open; no syscall fallback\n",log);
sync_log(log);
/* A returned zero can be an asynchronous request, not completed exit.
* Never return to main/libc and never retry or invoke the known-bad _exit. */
for(;;) sceKernelUsleep(1000000);
}
_Noreturn void _exit(int status) { psmtp_ps4_exit(status,NULL); }
+15
View File
@@ -0,0 +1,15 @@
/*
* PS MTP Explorer
* Original Author: nueex
* Copyright (C) 2026 nueex
* SPDX-License-Identifier: GPL-3.0-or-later
* GNU GPL version 3 or later; see LICENSE and LICENSING.md.
* WITHOUT ANY WARRANTY; see LICENSE.
* Historical MIT permissions/notices: LICENSES/MIT-legacy.txt.
*/
#ifndef PSMTP_PS4_EXIT_H
#define PSMTP_PS4_EXIT_H
#include <stdio.h>
__attribute__((noreturn)) void psmtp_ps4_exit(int status,FILE *log);
#endif
Loaded 100 of 148 files, more files were not shown because too many files have changed in this diff. Show more