Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0eab5f7f6c | ||
|
|
c00f689c49 | ||
|
|
164e3fb007 | ||
|
|
a15ef5d9d0 | ||
|
|
4b051d6fae |
No files matched your search
@@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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()
|
||||
@@ -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.
|
||||
@@ -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.**
|
||||
@@ -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>.
|
||||
@@ -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.
|
||||
@@ -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."
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -55,11 +60,13 @@
|
||||
- For phone access: an **MTP-capable Android phone** and a **USB data cable**. Unlock the phone and choose **File Transfer / Android Auto (MTP)** from its USB options. No companion phone app is required.
|
||||
- For USB storage: a drive that your PS4 exposes as a mounted USB storage device.
|
||||
|
||||
**Tested on firmware 11.00.**
|
||||
**Tested on PS4 firmware 9.00, 10.01 and 11.00 with GoldHEN active.**
|
||||
|
||||
### Install and open
|
||||
|
||||
Download **`PS-MTP-Explorer-1.02.pkg`** from [Releases](https://github.com/nueex/ps-mtp-explorer/releases/latest) and install it using GoldHEN's package installer. Then:
|
||||
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.
|
||||
2. **Launch PS MTP Explorer.** Choose your phone, USB drive or internal storage.
|
||||
@@ -108,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>
|
||||
@@ -131,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.
|
||||
@@ -152,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>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Third-party notices
|
||||
|
||||
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.00-Third-Party.zip` alongside the compiled release. Application source is not included in that archive.
|
||||
**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.
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
|
After Width: | Height: | Size: 2.4 KiB |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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.
|
||||
|
After Width: | Height: | Size: 852 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 945 B |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 707 B |
|
After Width: | Height: | Size: 5.4 KiB |
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 738 B |
|
After Width: | Height: | Size: 18 KiB |
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);}}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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); }
|
||||
@@ -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
|
||||