diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 723c293..e999bdf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,9 +49,6 @@ jobs: run: | cp LICENSE THIRD_PARTY_NOTICES.md publish/ mkdir -p publish/licenses - cp third-party/ProsperoPkgTool/LICENSE publish/licenses/ProsperoPkgTool-LICENSE - cp third-party/LibProsperoPkg12/LICENSE publish/licenses/LibProsperoPkg-LICENSE - cp third-party/LibProsperoPkg12/NOTICE publish/licenses/LibProsperoPkg-NOTICE cp src/Packwright.Ufs2/LICENSE publish/licenses/UFS2Tool-LICENSE cp third-party/licenses/* publish/licenses/ - name: Build the .deb and .rpm @@ -80,9 +77,6 @@ jobs: run: | cp LICENSE THIRD_PARTY_NOTICES.md publish/ mkdir -p publish/licenses - cp third-party/ProsperoPkgTool/LICENSE publish/licenses/ProsperoPkgTool-LICENSE - cp third-party/LibProsperoPkg12/LICENSE publish/licenses/LibProsperoPkg-LICENSE - cp third-party/LibProsperoPkg12/NOTICE publish/licenses/LibProsperoPkg-NOTICE cp src/Packwright.Ufs2/LICENSE publish/licenses/UFS2Tool-LICENSE cp third-party/licenses/* publish/licenses/ - name: Install Inno Setup @@ -124,7 +118,6 @@ jobs: chmod +x "$bundle/Contents/MacOS/$APP" cp LICENSE THIRD_PARTY_NOTICES.md "$bundle/Contents/Resources/" mkdir -p "$bundle/Contents/Resources/licenses" - cp third-party/ProsperoPkgTool/LICENSE "$bundle/Contents/Resources/licenses/ProsperoPkgTool-LICENSE" cp src/Packwright.Ufs2/LICENSE "$bundle/Contents/Resources/licenses/UFS2Tool-LICENSE" cp third-party/licenses/* "$bundle/Contents/Resources/licenses/" diff --git a/Packwright.slnx b/Packwright.slnx index 32c35f3..2fa2cf8 100644 --- a/Packwright.slnx +++ b/Packwright.slnx @@ -3,4 +3,6 @@ + + diff --git a/README.md b/README.md index 5948c1c..434ef1d 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,13 @@ One app to manage your PS5 dump and image collection, read PS5 packages, fix the ![Packwright showing a library of PS5 games as artwork cards, with the details panel open](docs/library.webp) > Packwright grew out of [PS5 PKG Tool](https://github.com/pearlxcore/PS5PkgTool) by [pearlxcore](https://github.com/pearlxcore) -> (GPL-3.0), and its image and package engines still power parts of this app. Thank you to pearlxcore. +> (GPL-3.0), and its exFAT, FFPKG and FFPFSC image code, library scanner and task queue still come from that project. +> Thank you to pearlxcore. > Details are in [License and credits](#license-and-credits). **This is not software for obtaining free PS5 games.** -*PlayStation and PS5 are trademarks of Sony Interactive Entertainment Inc. This project is independent and is not affiliated with, authorized or endorsed by Sony. No Sony software or SDK files are included.* +*PlayStation and PS5 are trademarks of Sony Interactive Entertainment Inc. This project is independent and is not affiliated with, authorized or endorsed by Sony. No Sony software, SDK files or keys are included.* > **Experimental:** Images and packages generated by this app have been validated and compared on PC, but they have not been tested on a jailbroken PS5. A successful build or validation does not guarantee that an image will mount, or that a package will install, launch, or play correctly on real hardware. The same goes for **Send to console**, which has only been tested against a simulated console. Keep backups and use generated output at your own risk. @@ -34,7 +35,7 @@ One app to manage your PS5 dump and image collection, read PS5 packages, fix the - **Library** of dumps, packages and images as artwork cards or a sortable list, with a search language, grouping, saved views and a details panel. - **Fix missing details:** autofill what it can from the files, look titles up by Title ID, and edit names, IDs, firmware and icons, in the library or inside the file. -- **Image tools:** convert between exFAT, FFPKG, FFPFSC and ZArchive, build debug packages (FPKG), extract, verify, repair and edit files inside images, with presets. +- **Image and package tools:** convert between folders, exFAT, FFPKG, FFPFSC, ZArchive and debug packages (FPKG) in every direction, verify, extract, edit and rebuild with new details, repair and edit files inside images, with presets. - **Library health** report: moved or unreadable files, duplicates, superseded updates, wasted space and more. - **Send to console** over FTP, or ask a console's installer to download a package from your PC. - **Export** the library as CSV, JSON or a web page with artwork. @@ -130,7 +131,7 @@ Packwright runs as one window: starting it again brings the open window to the f ### Library -- Scans folders for unpacked dumps, Sony `.pkg` files, `.ffpfsc`, `.ffpkg` and `.exfat` images, and ZArchive (`.zar`) files. +- Scans folders for unpacked dumps, PS5 `.pkg` files, `.ffpfsc`, `.ffpkg` and `.exfat` images, and ZArchive (`.zar`) files. - **Cards** with game artwork, or a **list**. In the list, click a column name to sort (click again to reverse) and right-click a column header to choose which columns are shown. - Group by family (base game, updates and DLC together), Title ID, category, region, format or firmware. Updates that a newer patch replaces are marked. - A [search language](#searching-your-library) and **saved views** that remember the search, filters, grouping, sort and columns. @@ -164,11 +165,17 @@ The **Health** page checks the whole library at a glance: files that were moved ### Debug packages (FPKG) -- Reads, verifies and extracts debug PS5 packages, including inner PFS browsing, trophies and PlayGo data. -- Builds a package from a dump or directly from an image. The Content ID is read from the source, and the passcode is configurable. -- Build options include inner compression (Auto, Kraken, uncompressed), Kraken level and threads, PlayGo chunks, DRM type, SDK version, a workspace folder, reproducible output, fake-signing and right.sprx injection. Free disk space is checked before a build starts. -- Retail packages are recognized but cannot be decoded without matching key material. Only debug packages built with the default or a known passcode are fully readable. +Packwright has its own package engine (`src/Packwright.Pkg`). It uses no third-party engine, ships no keys and bundles no Sony files. +- **Read, verify, extract:** debug packages built with the default passcode (all zeros) or a passcode you enter. Includes inner file browsing, trophies and PlayGo data. Extracting also writes the title's `sce_sys` entries, so the result is a complete folder. +- **Convert:** a package becomes a folder, exFAT, FFPKG, FFPFSC or ZArchive, and any of those becomes a package. +- **Build from a folder or an image:** the Content ID is read from the source. Options: inner compression (Auto, Kraken, uncompressed), compression threads, DRM type, SDK version (written into `param.json`), PlayGo chunks (only for a title without PlayGo files), reproducible output. Files are streamed into the package as it is written, so a build needs room only for the package; free space is checked first. +- **Edit and rebuild:** details and icon of a package are changed by rewriting only its metadata, which takes seconds even for a large title. If the Content ID is changed, the package is rebuilt in full. +- **Files are taken as they are.** Executables are not re-signed. The title's own `keystone` and `sce_sys/about/right.sprx` are copied when present. When one is missing, Packwright does not make one: it tells you (in the log) that the file is not present and builds the package without it. +- **Missing icon textures** (`icon0.dds`, `pic*.dds`) are generated from the PNG pictures. +- **Optional keys:** a few fields of a package are normally sealed with public RSA values. If you have them, import them under Settings > Compatibility keys (see [docs/KEYS.md](docs/KEYS.md)). Packwright ships none; without them a package is complete and readable on a PC. +- **Retail packages** are recognized but not decoded: their filesystem is encrypted with a key that is not in the file. Retail keys are for you to supply, and entering one is not available yet. +- **Not supported:** the install metadata table `naps_meta_18`, signing of raw ELF modules, and PlayGo files for titles that bring their own (those are copied unchanged). ### Send to console Right-click a game, image or package and choose **Send to console...** to put it on a console on your own network. @@ -185,7 +192,7 @@ Packwright can check GitHub for a newer version. It asks once, **About > Check f ### Settings and everyday comfort -- Light or dark theme (the moon button at the top of the sidebar), library layout, artwork on or off, output and rename defaults, preview limits, default passcode and builder, confirmation prompts. +- Light or dark theme (the moon button at the top of the sidebar), library layout, artwork on or off, output and rename defaults, preview limits, default passcode, optional compatibility keys, confirmation prompts. - Import and export settings (the passcode is left out unless you ask for it). - On Windows, an optional "Open with Packwright" entry for Explorer's right-click menu (per user, no administrator rights). @@ -233,7 +240,7 @@ Example: `id:PPSA01 size:>20GB -dlc | category:patch`. | Settings (including consoles and presets), library cache, task history | Windows `%LOCALAPPDATA%\Packwright`, Linux and macOS `~/.local/share/Packwright` | | Library-only edits (`metadata-overrides.json`, `overrides/` for custom icons) | the same folder | | Logs | `logs/` inside that folder (also shown on the **Log** page) | -| Temporary work files | your system temporary folder, or the workspace folder you choose | +| Temporary work files | your system temporary folder (only used when an image has to be unpacked first) | Delete the folder to reset the app. Setting the `PACKWRIGHT_DATA` environment variable to a folder makes a separate copy of the app use that folder instead (handy for testing). @@ -243,15 +250,34 @@ Delete the folder to reset the app. Setting the `PACKWRIGHT_DATA` environment va | --- | :---: | :---: | :---: | | Library, details, file preview | yes | yes | yes | | exFAT, FFPKG, FFPFSC, ZArchive convert, extract, verify, edit | yes | yes | yes | -| FPKG build with ProsperoPkgTool | yes | yes | yes | -| FPKG build with LibProsperoPkg | yes | - | - | -| Kraken-compressed package content | yes | yes | yes | +| FPKG build (built-in engine), with Kraken compression | yes | yes | yes | | Send to console, health report, export, command line | yes | yes | yes | | One-button update install | yes | - | - | | Explorer context menu | yes | - | - | -The filesystem, image, ZArchive and package code (including Kraken compression, which ProsperoPkgTool implements itself) is managed C# and behaves the same everywhere. LibProsperoPkg is a Windows-only library, so the other systems use ProsperoPkgTool for package builds. Linux and macOS builds are produced by the release workflow, but the Windows build has had the most testing. +The filesystem, image, ZArchive and package code (including Kraken compression) is managed C# and behaves the same everywhere. Linux and macOS builds are produced by the release workflow, but the Windows build has had the most testing. +## What works and what does not + +Tested on real titles (a 1.1 GB and an 8 GB one): every file comes out identical, and every package digest and seal is recomputed and matches. Nothing has been tested on a console, so a successful check on a PC does not promise that a loader will mount an image or install a package. + +| Operation | Folder | exFAT | FFPKG | FFPFSC | ZArchive | Debug package | +| --- | :---: | :---: | :---: | :---: | :---: | :---: | +| Read, browse, verify | yes | yes | yes | yes | yes | yes | +| Extract to a folder | - | yes | yes | yes | yes | yes | +| Convert to exFAT / FFPKG / FFPFSC / ZArchive | yes | yes | yes | yes | yes | yes | +| Build a debug package from it | yes | yes | yes | yes | yes | - | +| Edit details and icon (rebuilds with the change) | yes | yes | yes | yes | yes | yes (metadata only, fast) | +| Edit files inside | - | yes | yes | - | - | - | +| Repair / rebuild metadata | - | yes | yes | - | - | - | + +Every pair of formats converts in both directions (20 pairs tested). + +**Cannot do (yet):** +- Decode retail packages (needs a key you supply). +- Generate a missing `keystone` or `right.sprx`; the package is built without it and you are told. +- Write `naps_meta_18`, sign raw ELF modules. +- Guarantee that a console accepts an image or package. ## Build from source Requirements: the [.NET 10 SDK](https://dotnet.microsoft.com/download). No other tools or sibling repositories are needed. @@ -273,7 +299,7 @@ dotnet publish src/Packwright.App -c Release -r osx-arm64 --self-contained true dotnet publish src/Packwright.App -c Release -r osx-x64 --self-contained true -o publish/osx-x64 ``` -The Windows publish also copies the Windows-only libraries from `third-party/`. Any system can publish for any target. +Any system can publish for any target. The installers are built from a publish folder: `installer/Packwright.iss` (Inno Setup 6, Windows), `installer/linux/build-packages.sh` (`.deb` and `.rpm`, needs `dpkg-deb` and `rpmbuild`), and the macOS disk image step in `.github/workflows/release.yml`. @@ -294,10 +320,12 @@ The workflows live in `.github/workflows/`: `release.yml` and `build.yml`. src/ Packwright.App Avalonia application: pages, styles, queue glue, command line Packwright.Core Packages, parsers, library scanner, builders, FTP client, update checker, task queue + Packwright.Pkg The package engine: reads, verifies, extracts and builds PS5 packages Packwright.Containers exFAT, PFS/PFSC (FFPFSC) and ZArchive images Packwright.Ufs2 UFS2 / FFPKG filesystem installer/ Windows installer script and Linux package builder -third-party/ Vendored binaries (ProsperoPkgTool, LibProsperoPkg) and license texts +third-party/ License texts of bundled components +docs/KEYS.md The optional compatibility key file docs/ Screenshots .github/workflows/ Build and release automation Packwright.slnx Solution @@ -307,15 +335,20 @@ Packwright.slnx Solution Copyright (c) 2026 itsmemac. Packwright is licensed under the [GNU General Public License v3.0](LICENSE), with no warranty. -It started from [PS5 PKG Tool](https://github.com/pearlxcore/PS5PkgTool) by pearlxcore (Copyright (c) pearlxcore, GPL-3.0) and has since grown into a different application, with a new cross-platform interface and many new features. Parts of its image and package engines are still used, so its copyright and license notices are kept. The changes made in 2026 are listed in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md), which also lists every bundled component, its license and its copyright. The source of each release is the tag with the same version in this repository, and the license texts of the bundled components ship in the `licenses` folder of the installed app. +Packwright started from [PS5 PKG Tool](https://github.com/pearlxcore/PS5PkgTool) by pearlxcore (Copyright (c) pearlxcore, GPL-3.0) and has since grown into a different application, with a new cross-platform interface and many new features. Its exFAT, FFPKG and FFPFSC image code, the library scanner and the task queue still come from that project, so its copyright and license notices are kept. -- **[pearlxcore](https://github.com/pearlxcore/PS5PkgTool)**: author of PS5 PKG Tool, the project Packwright grew out of, and of the ProsperoPkgTool engine. -- **[SvenGDK](https://github.com/SvenGDK/LibProsperoPKG)**: LibProsperoPkg, and the UFS2Tool filesystem code the FFPKG support is based on. +The PS5 package engine (`src/Packwright.Pkg`) is Packwright's own: it reads, verifies, extracts and builds packages and uses no third-party engine and no third-party binary. It builds on the published format work of the projects below, and the source files taken from them keep their copyright notices. + +The changes made in 2026 are listed in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md), which also lists every bundled component, its license and its copyright. The source of each release is the tag with the same version in this repository, and the license texts of the bundled components ship in the `licenses` folder of the installed app. + +- **[pearlxcore](https://github.com/pearlxcore/PS5PkgTool)**: author of PS5 PKG Tool, the project Packwright grew out of. +- **[SvenGDK](https://github.com/SvenGDK/LibProsperoPKG)**: LibProsperoPKG (GPL-3.0-or-later), whose package format work the Packwright package engine builds on, and the UFS2Tool filesystem code the FFPKG support is based on. +- **[maxton](https://github.com/maxton/LibOrbisPkg)**: LibOrbisPkg (GPL-3.0), from which parts of LibProsperoPKG, and so of the package engine, descend. +- **[Powzix](https://github.com/powzix/ooz)**: ooz (GPL-3.0), the Kraken decoder that the package engine's Kraken code is translated from. - **[PSBrew / Renan Barreto](https://github.com/PSBrew/MkPFS)**: MkPFS. - **[kerrdec97](https://github.com/kerrdec97/ps5-exfat-builder)**: ps5-exfat-builder. - **[strongt1me](https://github.com/strongt1me/PS5-Dump-Image-Converter)**: PS5-Dump-Image-Converter. - **[Exzap](https://github.com/Exzap/ZArchive)**: the ZArchive format. - **[andshrew](https://github.com/andshrew/PlayStation-Titles)**: the PS5 title list that Title ID lookups use (MIT License). -- **[Avalonia UI](https://avaloniaui.net)**, **[ZstdSharp](https://github.com/oleg-st/ZstdSharp)**, **[CommunityToolkit](https://github.com/CommunityToolkit/dotnet)** (HighPerformance, used by LibProsperoPkg), **[ImageSharp](https://github.com/SixLabors/ImageSharp)** and **[BCnEncoder.Net](https://github.com/Nominom/BCnEncoder.NET)**. -- Robin Perris for [DarkUI](https://github.com/RobinPerris/DarkUI), which the original Windows interface was built on. -- Sony <3 +- **[Avalonia UI](https://avaloniaui.net)**, **[ZstdSharp](https://github.com/oleg-st/ZstdSharp)**, **[ImageSharp](https://github.com/SixLabors/ImageSharp)** and **[BCnEncoder.Net](https://github.com/Nominom/BCnEncoder.NET)**. +- Robin Perris for [DarkUI](https://github.com/RobinPerris/DarkUI), which the original Windows interface was built on. \ No newline at end of file diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 1c143a9..e378d8d 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -20,6 +20,8 @@ itsmemac: - the Windows Forms interface was replaced by a cross-platform Avalonia interface (Windows, Linux, macOS), with a new design, name and application icon; - ZArchive (`.zar`) support was added: writing, reading, library scanning, browsing and conversion; +- the package engine was replaced by one of its own (`src/Packwright.Pkg`), which reads, verifies, extracts and builds + packages, including Kraken compression, with no third-party engine and no keys (see below); - the library gained artwork cards, grouping, saved views, a search syntax, a file explorer with preview, rename/move/delete tools, task persistence and a task history with game icons; - editing of a title's details (name, IDs, region, versions, firmware, SDK, DRM, language, features and icon), in the @@ -38,42 +40,28 @@ project (Copyright (c) 2026 itsmemac, GPL-3.0). ## Source code The complete corresponding source of each release is the tag with the same version in -https://github.com/itsmemac/Packwright. The bundled LibProsperoPkg 1.2.0 files are binaries from -https://github.com/SvenGDK/LibProsperoPkg (GPL-3.0-or-later); their source is available from that -project. +https://github.com/itsmemac/Packwright. -## ProsperoPkgTool +## Packwright.Pkg (the package engine) -The PS5 PKG (FIH/CNT/PFS/NAPS) reading, verification, extraction and debug-package creation -implementation is provided by the clean-room MIT engine ProsperoPkgTool, consumed as a vendored -managed library at `third-party/ProsperoPkgTool/ProsperoPkgTool.dll`. +`src/Packwright.Pkg` is Packwright's own package engine: it reads, validates and extracts PS5 packages and builds +them (inner image, outer filesystem, metadata container, install metadata, Kraken compression). It contains no +Sony code, no Sony keys and no third-party binaries. -MIT License. Copyright (c) 2026 pearlxcore. No GPL, SDK, or decompiled code is included; the -engine is a clean-room reimplementation validated against independent oracles. +The engine builds on the format work of LibProsperoPKG by SvenGDK (https://github.com/SvenGDK/LibProsperoPKG, +Copyright (c) 2026 SvenGDK, GPL-3.0-or-later): source files of that project (the PFS and container structures, the +utility code, the Kraken encoder and decoder) are included in adapted form with their original copyright headers, and +the layout and digest rules of the writer follow it. LibProsperoPKG in turn contains code derived from LibOrbisPkg by +maxton (https://github.com/maxton/LibOrbisPkg, GPL-3.0) and an index-based translation of the Kraken decoder "ooz" by +Powzix (https://github.com/powzix/ooz, GPL-3.0). Their notices are kept in the headers of the files concerned. Every +part of the engine was checked against real packages on disk: file for file, digest for digest. -## LibProsperoPkg +The engine ships no key. A few fields of a package are normally sealed with public RSA values; the user may import them +from a file of their own (see `docs/KEYS.md`). Packwright only checks the file against fingerprints that do not reveal +the values, and refuses a private key. -The alternative package build/validate/extract backend is provided by LibProsperoPkg, loaded from a -vendored payload at `third-party/LibProsperoPkg12/` (version **1.2.0** - the version the -PPR-PKG / fpkg-gui builder ships; the newer 2.6.0 regressed the NAPS layout). It is loaded in an -isolated assembly-load context together with its bundled dependencies: `BCnEncoder.Net 2.3.0`, -`CommunityToolkit.HighPerformance 8.4.0` and `Magick.NET 14.15.0` (managed + its -`runtimes/win-x64/native/Magick.Native-Q8-x64.dll`). - -LibProsperoPkg can optionally use Sony's proprietary Publishing Tools library (`libScePubTools.dll`) -if it finds one from a Sony SDK installation. **That library is Sony's property and is not part of -this project or of any download.** Everything here works without it. - -LibProsperoPkg itself contains code derived from LibOrbisPkg by maxton -(https://github.com/maxton/LibOrbisPkg, GPL-3.0) and a translation of the "ooz" Kraken decoder by -Powzix (https://github.com/powzix/ooz, GPL-3.0); see `third-party/LibProsperoPkg12/NOTICE`. - -GNU General Public License v3.0 or later (GPL-3.0-or-later). -Copyright (c) SvenGDK 2026. https://github.com/SvenGDK/LibProsperoPkg - -Because LibProsperoPkg is GPL-3.0-or-later and Packwright loads it, the combined work is -distributed under the GPL-3.0 (see `LICENSE`). The full license text ships at -`third-party/LibProsperoPkg12/LICENSE` and, in a download, in the `licenses` folder. +GNU General Public License v3.0 or later. Because these parts are GPL-3.0-or-later, the combined work is distributed +under the GPL-3.0 (see `LICENSE`). ## UFS2Tool @@ -89,10 +77,6 @@ Avalonia.Themes.Fluent, Avalonia.Fonts.Inter), licensed under the MIT License. Copyright (c) AvaloniaUI OÜ and contributors. https://github.com/AvaloniaUI/Avalonia The Inter font is licensed under the SIL Open Font License 1.1. -## CommunityToolkit.HighPerformance - -Bundled with LibProsperoPkg (see above). MIT License. Copyright (c) .NET Foundation and Contributors. https://github.com/CommunityToolkit/dotnet - ## ZstdSharp.Port MIT License. Copyright (c) Oleg Stepanischev and contributors. https://github.com/oleg-st/ZstdSharp @@ -123,12 +107,11 @@ format that follow the reference implementation, and were checked against archiv Authors, licensed under the SIL Open Font License 1.1 (`third-party/licenses/Inter-OFL-1.1.txt`). - The application icon is original to this project. -## SixLabors ImageSharp and Magick.NET +## SixLabors ImageSharp ImageSharp (used through BCnEncoder.Net.ImageSharp) is licensed under the Six Labors Split License. This project is open source, and ImageSharp is also a transitive dependency here, so ImageSharp is used -under the Apache License 2.0 (`third-party/licenses/Apache-2.0.txt`). Magick.NET is licensed under the -Apache License 2.0 and ships with the Windows-only LibProsperoPkg payload. +under the Apache License 2.0 (`third-party/licenses/Apache-2.0.txt`). ## DarkUI @@ -139,7 +122,7 @@ this project. ## BCnEncoder.Net BCnEncoder.Net (and BCnEncoder.Net.ImageSharp) is distributed under the MIT License. It is used to -encode BC7 textures for PS5 CNT media. https://github.com/Nominom/BCnEncoder.NET +decode the BC7 textures of PS5 artwork for display. https://github.com/Nominom/BCnEncoder.NET ## PlayStation-Titles (title list, downloaded on request) @@ -155,14 +138,14 @@ documentation and validated against independent tools. Credit to the authors of document and handle these formats: - MkPFS, PSBrew / Renan Barreto (https://github.com/PSBrew/MkPFS), PFS and PFSC/FFPFSC format work. -- UFS2Tool and LibProsperoPkg, SvenGDK (https://github.com/SvenGDK/UFS2Tool), UFS2/FFPKG work. UFS2Tool is used under the BSD 2-Clause License. +- UFS2Tool and LibProsperoPKG, SvenGDK (https://github.com/SvenGDK/UFS2Tool), UFS2/FFPKG work. UFS2Tool is used under the BSD 2-Clause License. - ps5-exfat-builder, kerrdec97 (https://github.com/PSBrew/ps5-exfat-builder), exFAT format work. - PS5 Dump and Image Converter, strongt1me (https://github.com/strongt1me/PS5-Dump-Image-Converter). ## MIT License text -This text applies to every component above that is marked "MIT License" (ProsperoPkgTool, Avalonia -UI, CommunityToolkit.HighPerformance, ZstdSharp.Port, BCnEncoder.Net, DarkUI and the PlayStation-Titles list), each with the +This text applies to every component above that is marked "MIT License" (Avalonia +UI, ZstdSharp.Port, BCnEncoder.Net, DarkUI and the PlayStation-Titles list), each with the copyright notice given for it in its own section. ``` diff --git a/docs/KEYS.md b/docs/KEYS.md new file mode 100644 index 0000000..2d0190b --- /dev/null +++ b/docs/KEYS.md @@ -0,0 +1,36 @@ +# Compatibility keys (optional) + +Packwright ships **no keys** of any kind. A package it builds is complete and readable without any: the inner image and +the outer image use the public passcode scheme (the passcode is `00000000000000000000000000000000` unless you choose +another), and Packwright reads, verifies and extracts its own packages with no key at all. + +A few fields of a package are normally sealed by the original tools with public RSA values (the moduli of the public +keys), or encrypted under a fixed key. A console may check them. If you have those values you can import them, and +Packwright uses them when it builds a package. Without them those fields are filled with random bytes of the right +length, which is fine for reading the package on a PC and may or may not be accepted by a console. + +You are responsible for where the values come from. Packwright never downloads, extracts or generates them, and it +never accepts a private key: a file that contains one is refused. + +## Importing + +Settings > Compatibility keys > **Import key file...** and pick the file. Packwright checks it, keeps a copy in its data +folder (`keys.txt`) and uses it for every package it builds. **Remove** deletes the copy. + +## File format + +UTF-8 text, one `name=value` per line, `#` starts a comment. Values are hexadecimal. + +| Name | Value | Used for | +| --- | --- | --- | +| `passcode0` ... `passcode6` | seven 384-byte RSA-3072 public moduli, big-endian | the wrapped keys in the container's entry-key table | +| `mount-image` | a 384-byte RSA-3072 public modulus, big-endian | the wrapped image key | +| `metadata` | a 384-byte RSA-3072 public modulus, big-endian | the seal over the container header | +| `naps-meta-xts` | three 16-byte values (data key, tweak key, tweak), comma separated | the install metadata table (`naps_meta_18.dat`); not written by this version | + +Every value is checked against a fingerprint (its SHA-256) that is part of Packwright's source. The fingerprints do not +reveal the values; they only tell Packwright whether the file you picked is the expected one. A wrong or corrupt value +is refused with a message, and nothing is imported. + +The seven `passcode` lines have to be given together or not at all. Each line is optional otherwise: what is missing is +filled with random bytes. diff --git a/installer/Packwright.iss b/installer/Packwright.iss index 0c8c690..a2df1d1 100644 --- a/installer/Packwright.iss +++ b/installer/Packwright.iss @@ -1,10 +1,10 @@ ; Packwright installer (Inno Setup 6). Built by the release workflow: -; iscc /DAppVersion=1.0.1 /DSourceDir= /O installer\Packwright.iss +; iscc /DAppVersion=1.1.0 /DSourceDir= /O installer\Packwright.iss ; Installs for the current user by default (no administrator prompt), so a silent update is smooth. The "for all ; users" choice is offered when the installer is started as administrator. #ifndef AppVersion - #define AppVersion "1.0.1" + #define AppVersion "1.1.0" #endif #ifndef SourceDir #define SourceDir "..\publish" diff --git a/release-notes/1.1.0.md b/release-notes/1.1.0.md new file mode 100644 index 0000000..b424fb0 --- /dev/null +++ b/release-notes/1.1.0.md @@ -0,0 +1,14 @@ +### What is new + +- **Packwright now has its own package engine.** It reads, verifies, extracts and builds PS5 packages with no third-party engine and no bundled binary, and ships no keys. Builds stream the files into the package, so they need no workspace folder, only room for the package. +- **Kraken compression** for package builds (Auto, Kraken or uncompressed), with the size of the original packages. +- **Editing a package's details and icon takes seconds.** Only the metadata is rewritten; the rest of the package is copied. Changing the Content ID still rebuilds it in full. +- **Extracting a package also writes its `sce_sys` entries**, so the result is a complete folder that can be built into a package again. +- **Missing icon textures** (`icon0.dds`, `pic*.dds`) are generated, and a replaced icon updates its texture. +- **Missing `keystone` or `right.sprx`:** Packwright never makes one. The package is built without it and the log says which files are not present. +- **Compatibility keys (optional):** import public values from a file of your own under Settings; see `docs/KEYS.md`. Nothing is shipped. +- **Build options:** play-go chunks, DRM type, SDK version, reproducible output. The builder choice and the workspace, fake-sign and right.sprx options are gone with the old engines. +- **Patch packages** are described from their headers. +- A note on the build options says packages are verified on a PC but not tested on a console. + +If you already have Packwright 1.0.1, open **About** and choose **Check for updates**. diff --git a/src/Packwright.App/Cli.cs b/src/Packwright.App/Cli.cs index 6780f24..51023c9 100644 --- a/src/Packwright.App/Cli.cs +++ b/src/Packwright.App/Cli.cs @@ -29,7 +29,7 @@ internal static class Cli private static readonly HashSet ValueOptions = new(StringComparer.OrdinalIgnoreCase) { "to", "o", "output", "preset", "passcode", "level", "gain", "cluster", "block", "fragment", "density", "min-free", - "compression", "kraken-level", "threads", "playgo", "drm", "builder", "workspace", "format", "folder", "host", "port", "user", "password" + "compression", "kraken-level", "threads", "playgo", "drm", "format", "folder", "host", "port", "user", "password" }; public static bool IsCommand(string[] args) => @@ -169,8 +169,8 @@ internal static class Cli --level 1-9 --gain 0-100 FFPFSC compression --cluster 32|64 --no-ampr exFAT --block 32|64 --fragment 4|64 --density 256|512|1024 --min-free 0-50 FFPKG - --builder --compression auto|kraken|stored --kraken-level N --threads N --playgo N - --deterministic --no-fake-sign --no-right-sprx --drm upgradable|free|standard|keep --workspace + --compression auto|kraken|stored --kraken-level N --threads N --playgo N + --deterministic --drm upgradable|free|standard|keep extract/verify on a package: --passcode <32 characters> (default: the debug passcode) Exit codes: 0 done, 1 failed, 2 wrong usage, 130 cancelled. @@ -433,9 +433,10 @@ internal static class Cli { string contentId = Jobs.ReadContentId(spec.Source, format); if (contentId.Length == 0) return "The source has no content ID (sce_sys/param.json), so a package cannot be built from it."; + Console.Error.WriteLine("Note: packages are experimental. They are verified on this PC but not tested on a console; exFAT and FFPKG images are the better-tested route."); spec.Kind = JobSpec.BuildPackage; spec.ContentId = contentId; - spec.Backend = (options.Get("builder") ?? (basis.Backend.Length > 0 ? basis.Backend : AppSettingsDefaultBackend())).Trim(); + spec.Backend = BackendRegistry.DefaultId; spec.Compression = (Choice("compression") switch { "kraken" => Ps5InnerCompression.Kraken, @@ -445,14 +446,11 @@ internal static class Cli }).ToString(); spec.KrakenLevel = Math.Clamp(Pick("kraken-level", basis.KrakenLevel), -4, 9); spec.KrakenThreads = Math.Clamp(Pick("threads", basis.KrakenThreads), 0, 64); - spec.PlayGoChunks = Math.Clamp(Pick("playgo", basis.PlayGoChunks), 1, 64); + spec.PlayGoChunks = Math.Clamp(Pick("playgo", 1), 1, 64); spec.Deterministic = options.Has("deterministic") || basis.Deterministic; - spec.FakeSign = !options.Has("no-fake-sign") && basis.FakeSign; - spec.RightSprx = !options.Has("no-right-sprx") && basis.RightSprx; spec.Drm = (Choice("drm") ?? basis.DrmIndex switch { 1 => "free", 2 => "standard", 3 => "keep", _ => "upgradable" }) switch { "free" => "free", "standard" => "standard", "keep" => null, _ => "upgradable" }; spec.Sdk = basis.Sdk.Length > 0 ? SdkFromVersion(basis.Sdk) : null; - spec.TempDirectory = options.Get("workspace") ?? (basis.TempDirectory.Length > 0 ? basis.TempDirectory : null); return null; } spec.Kind = JobSpec.Convert; @@ -460,8 +458,6 @@ internal static class Cli return null; } - private static string AppSettingsDefaultBackend() => new AppStateStore().LoadSettings().BuildBackend; - private static ulong? SdkFromVersion(string version) { int index = Ps5SdkVersions.Releases.ToList().FindIndex(release => release.Version == version); diff --git a/src/Packwright.App/Infrastructure/AppStateStore.cs b/src/Packwright.App/Infrastructure/AppStateStore.cs index 5e299df..05ad840 100644 --- a/src/Packwright.App/Infrastructure/AppStateStore.cs +++ b/src/Packwright.App/Infrastructure/AppStateStore.cs @@ -128,9 +128,12 @@ public sealed class AppSettings // Build defaults public string DebugPasscode { get; set; } = string.Empty; - /// Selected package backend id ("lpp" or "ppt"). Empty/unknown falls back to the default. + /// Kept so settings written by older versions still load; the package engine is no longer selectable. public string BuildBackend { get; set; } = string.Empty; + /// Path of the imported compatibility key file (a copy kept in the app data folder), or empty. + public string KeyFile { get; set; } = string.Empty; + // Updates (nothing is checked until the user allows it) public bool CheckForUpdates { get; set; } public bool UpdateCheckAsked { get; set; } diff --git a/src/Packwright.App/Packwright.App.csproj b/src/Packwright.App/Packwright.App.csproj index 68da3c1..286b830 100644 --- a/src/Packwright.App/Packwright.App.csproj +++ b/src/Packwright.App/Packwright.App.csproj @@ -9,8 +9,8 @@ Packwright.App Assets\icon.ico false - 1.0.1 - 1.0.1.0 + 1.1.0 + 1.1.0.0 Packwright itsmemac itsmemac @@ -45,11 +45,4 @@ - - - LibProsperoPkg12\%(RecursiveDir)%(Filename)%(Extension) - PreserveNewest - PreserveNewest - - diff --git a/src/Packwright.App/Services/JobSpec.cs b/src/Packwright.App/Services/JobSpec.cs index 5b5c87b..ab11855 100644 --- a/src/Packwright.App/Services/JobSpec.cs +++ b/src/Packwright.App/Services/JobSpec.cs @@ -61,13 +61,10 @@ public sealed class JobSpec public string Compression { get; set; } = nameof(Ps5InnerCompression.Auto); public int KrakenLevel { get; set; } = 7; public int KrakenThreads { get; set; } - public int PlayGoChunks { get; set; } = 1; public bool Deterministic { get; set; } - public bool FakeSign { get; set; } = true; - public bool RightSprx { get; set; } = true; public string? Drm { get; set; } = "upgradable"; public ulong? Sdk { get; set; } - public string? TempDirectory { get; set; } + public int PlayGoChunks { get; set; } = 1; public string ToJson() => JsonSerializer.Serialize(this); @@ -89,7 +86,7 @@ public sealed class JobSpec public PackageTaskStage[] Plan() => Kind switch { Convert => FromPackage ? PackageTaskPlans.ConvertPackage : PackageTaskPlans.ConvertImage, - BuildPackage => PackageTaskPlans.BuildPackageFor(Source, Backend == BackendRegistry.LppId && !Directory.Exists(Source)), + BuildPackage => PackageTaskPlans.BuildPackageFor(Source), Extract => PackageTaskPlans.Extract, Verify => PackageTaskPlans.Verify, RefreshAmpr => PackageTaskPlans.Ampr, @@ -142,16 +139,16 @@ public sealed class JobSpec if (Output.Length == 0 || ContentId.Length == 0) return null; var settings = new PackageBuildSettings( Enum.TryParse(Compression, out Ps5InnerCompression compression) ? compression : Ps5InnerCompression.Auto, - KrakenLevel, KrakenThreads, PlayGoChunks, Deterministic, FakeSign, RightSprx, Drm, Sdk, TempDirectory); + KrakenLevel, KrakenThreads, Deterministic, Drm, Sdk, PlayGoChunks); IPackageBackend backend = Jobs.ResolveBackend(Backend); - string passcode = Passcode.Length > 0 ? Passcode : SonyDebugPackageCredentials.DefaultPasscode; + string passcode = Passcode.Length > 0 ? Passcode : Ps5DebugPackageCredentials.DefaultPasscode; return (progress, token) => Jobs.BuildPackageAsync(Source, Output, ContentId, passcode, Overwrite, settings, backend, progress, token); case Extract: if (Output.Length == 0) return null; if (FromPackage) { - string code = Passcode.Length > 0 ? Passcode : SonyDebugPackageCredentials.DefaultPasscode; + string code = Passcode.Length > 0 ? Passcode : Ps5DebugPackageCredentials.DefaultPasscode; return async (progress, token) => await Jobs.ExtractPackageAsync(Source, Output, code, progress, token).ConfigureAwait(false); } @@ -160,17 +157,17 @@ public sealed class JobSpec case Verify: if (FromPackage) { - string code = Passcode.Length > 0 ? Passcode : SonyDebugPackageCredentials.DefaultPasscode; + string code = Passcode.Length > 0 ? Passcode : Ps5DebugPackageCredentials.DefaultPasscode; return (progress, token) => { var watch = System.Diagnostics.Stopwatch.StartNew(); - SonyDebugPackageValidationResult result = Jobs.VerifyPackage(Source, code); + Ps5DebugPackageValidationResult result = Jobs.VerifyPackage(Source, code); if (!result.IsValid) throw new InvalidDataException("This package did not pass verification: " + result.Message.Trim().TrimEnd('.') + ". The file may be damaged or incomplete (compare its size and checksum with where you got it)."); Logger.Info($"Package verified: {result.IndexedFiles:N0} indexed file(s)."); progress.Report(new PackageTaskProgress("Verified", - Result: $"Verified: no problems found. {result.IndexedFiles:N0} file(s) checked in {Elapsed(watch)}.")); + Result: $"Verified: structure, metadata and file decoding are intact. {result.IndexedFiles:N0} file(s) checked in {Elapsed(watch)}.")); return Task.CompletedTask; }; } @@ -211,7 +208,7 @@ public sealed class JobSpec .ConfigureAwait(false); case EditDetails: if (Edit is not { IsEmpty: false } edit) return null; - string editCode = Passcode.Length > 0 ? Passcode : SonyDebugPackageCredentials.DefaultPasscode; + string editCode = Passcode.Length > 0 ? Passcode : Ps5DebugPackageCredentials.DefaultPasscode; return (progress, token) => MetadataEditor.ApplyAsync(Source, Output, FromPackage, edit, editCode, Backend, progress, token); case SendFtp: diff --git a/src/Packwright.App/Services/Jobs.cs b/src/Packwright.App/Services/Jobs.cs index 5c7a1a7..987ec58 100644 --- a/src/Packwright.App/Services/Jobs.cs +++ b/src/Packwright.App/Services/Jobs.cs @@ -13,13 +13,10 @@ public sealed record PackageBuildSettings( Ps5InnerCompression Compression = Ps5InnerCompression.Auto, int KrakenLevel = 7, int KrakenThreads = 0, - int PlayGoChunks = 1, bool Deterministic = false, - bool FakeSignModules = true, - bool InjectRightSprx = true, string? DrmType = "upgradable", ulong? SdkVersionOverride = null, - string? TempDirectory = null); + int PlayGoChunks = 1); /// /// The long-running operations behind the Tools tab. Each method is UI-free and reports progress @@ -43,22 +40,25 @@ public static class Jobs new Progress(value => target.Report(new PackageTaskProgress( value.Stage, 0, 0, value.Completed, value.Total, 0, 0, string.Empty))); - public static IProgress AdaptExtract(IProgress target) => - new Progress(value => target.Report(new PackageTaskProgress( + public static IProgress AdaptExtract(IProgress target) => + new Progress(value => target.Report(new PackageTaskProgress( "Extracting", 0, 0, value.CompletedBytes, value.TotalBytes, 0, 0, value.CurrentPath))); - /// The builder to use: the saved choice, falling back when LibProsperoPkg is not usable on this OS. - public static IPackageBackend ResolveBackend(string? id) + private static bool _keysLoaded; + + /// Loads the imported compatibility key file (once) so package builds use it. + public static void EnsureKeys() { - IPackageBackend backend = BackendRegistry.Get(id); - if (backend.Id == BackendRegistry.LppId && !PlatformSupportsLpp) - backend = BackendRegistry.Get(BackendRegistry.PptId); - return backend; + if (_keysLoaded) return; + _keysLoaded = true; + string path = new AppStateStore().LoadSettings().KeyFile; + if (string.IsNullOrWhiteSpace(path)) return; + string? problem = Ps5BuildKeys.TryConfigure(path); + if (problem is not null) Logger.Warn("The imported key file was not used: " + problem); } - public static bool PlatformSupportsLpp => OperatingSystem.IsWindows() && - File.Exists(Path.Combine(AppContext.BaseDirectory, "LibProsperoPkg12", "LibProsperoPkg.dll")); - + /// The package engine. There is one. + public static IPackageBackend ResolveBackend(string? id) => BackendRegistry.Get(id); public static Task ConvertAsync(string source, string output, Ps5ImageConversionTarget target, bool overwrite, bool fromPackage, IProgress progress, CancellationToken token, ExfatBuildOptions? exfatOptions = null, FfpfscBuildOptions? ffpfscOptions = null, @@ -66,7 +66,7 @@ public static class Jobs { IProgress bridge = AdaptConversion(progress); return fromPackage - ? SonyPackageImageConversion.ConvertAsync(source, output, target, overwrite, bridge, token, + ? Ps5PackageImageConversion.ConvertAsync(source, output, target, overwrite, bridge, token, exfatOptions, ffpfscOptions, ffpkgOptions) : Ps5ImageConversionService.ConvertAsync(source, output, target, overwrite, bridge, token, exfatOptions, ffpfscOptions, ffpkgOptions); @@ -125,13 +125,13 @@ public static class Jobs } } - public static async Task ExtractPackageAsync(string source, string output, + public static async Task ExtractPackageAsync(string source, string output, string passcode, IProgress progress, CancellationToken token) => - await SonyPackageExtraction.ExtractAsync(source, output, passcode, AdaptExtract(progress), token) + await Ps5PackageExtraction.ExtractAsync(source, output, passcode, AdaptExtract(progress), token) .ConfigureAwait(false); - public static SonyDebugPackageValidationResult VerifyPackage(string source, string passcode) => - SonyDebugPackageBuilder.Validate(source, passcode); + public static Ps5DebugPackageValidationResult VerifyPackage(string source, string passcode) => + Ps5DebugPackageBuilder.Validate(source, passcode); /// Reads the content ID from a dump folder or image so a package can be built from it. public static string ReadContentId(string source, Ps5ImageFormat format) @@ -207,72 +207,30 @@ public static class Jobs string jobDirectory = Path.Combine(outputDirectory, ".packwright-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(jobDirectory); string partial = Path.Combine(jobDirectory, Path.GetFileName(output) + ".partial"); - var options = new SonyDebugPackageBuildOptions + var options = new Ps5DebugPackageBuildOptions { ContentId = contentId, Passcode = passcode, SdkVersionOverride = settings.SdkVersionOverride, - TempDirectory = settings.TempDirectory, DrmTypeOverride = settings.DrmType, Log = Logger.Info, Compression = settings.Compression, KrakenLevel = settings.KrakenLevel, KrakenThreads = settings.KrakenThreads, PlayGoChunkCount = settings.PlayGoChunks, - Seed = settings.Deterministic ? DeterministicSeed(contentId, passcode) : null, - FakeSignModules = settings.FakeSignModules, - InjectRightSprx = settings.InjectRightSprx + Seed = settings.Deterministic ? DeterministicSeed(contentId, passcode) : null }; - var bridge = new Progress(value => + var bridge = new Progress(value => progress.Report(new PackageTaskProgress(value.Stage, 0, 0, value.CompletedBytes, value.TotalBytes, 0, 0, value.CurrentPath))); + EnsureKeys(); bool isDirectory = Directory.Exists(source); - Ps5ImageFormat sourceFormat = isDirectory ? Ps5ImageFormat.Unknown : Ps5ImageFormatProbe.Detect(source); - // A ZArchive, and any image built with LibProsperoPkg, is unpacked to a staging folder first. - bool stageFromImage = !isDirectory && (backend.Id == BackendRegistry.LppId || sourceFormat == Ps5ImageFormat.ZArchive); - IPackageBackend effective = isDirectory || stageFromImage ? backend : BackendRegistry.Get(BackendRegistry.PptId); - - string? staging = null; try { - string buildSource = source; - if (stageFromImage) - { - long rawBytes = sourceFormat == Ps5ImageFormat.ZArchive - ? ZArchiveLogicalBytes(source) - : VolumeDebugPackageBuilder.EstimatePayloadBytes(source); - // The extractors create the folder themselves (some refuse one that already exists). - staging = Path.Combine(Path.GetTempPath(), "Packwright-image-" + Guid.NewGuid().ToString("N")); - progress.Report(new PackageTaskProgress("Extract image", 0, 0, 0, rawBytes, 0, 0, - Path.GetFileName(source))); - await ExtractImageAsync(source, Ps5ImageFormatProbe.Detect(source), staging, progress, token) - .ConfigureAwait(false); - buildSource = staging; - } - - async Task BuildAsync() - { - if (isDirectory || staging is not null) - await effective.BuildFromDirectoryAsync(buildSource, partial, options, bridge, token) - .ConfigureAwait(false); - else - await effective.BuildFromImageAsync(source, partial, options, bridge, token) - .ConfigureAwait(false); - } - - try - { - await BuildAsync().ConfigureAwait(false); - } - catch (BackendNotSupportedException ex) when (effective.Id == BackendRegistry.LppId) - { - Logger.Info($"Builder {effective.DisplayName} could not lay out this title ({ex.Message}); " + - "retrying with ProsperoPkgTool."); - effective = BackendRegistry.Get(BackendRegistry.PptId); - TryDeleteFile(partial); - await BuildAsync().ConfigureAwait(false); - } - + if (isDirectory) + await backend.BuildFromDirectoryAsync(source, partial, options, bridge, token).ConfigureAwait(false); + else + await backend.BuildFromImageAsync(source, partial, options, bridge, token).ConfigureAwait(false); PackageReaderVerificationResult result = PackageReaderVerification.Inspect(partial, passcode); Logger.Info($"Verified built package: {result.FileCount:N0} file(s), eboot.bin " + (result.HasEboot ? "present" : "missing") + "."); @@ -290,7 +248,6 @@ public static class Jobs finally { TryDeleteDirectory(jobDirectory); - if (staging is not null) TryDeleteDirectory(staging); } } diff --git a/src/Packwright.App/Services/LibraryActions.cs b/src/Packwright.App/Services/LibraryActions.cs index f7e51e0..9e0e3e6 100644 --- a/src/Packwright.App/Services/LibraryActions.cs +++ b/src/Packwright.App/Services/LibraryActions.cs @@ -76,7 +76,7 @@ public static class LibraryActions { var plans = new List(); foreach (IGrouping group in games - .Where(game => game.SourceKind == Ps5SourceKind.SonyPackage && !string.IsNullOrWhiteSpace(game.TitleId)) + .Where(game => game.SourceKind == Ps5SourceKind.Ps5Package && !string.IsNullOrWhiteSpace(game.TitleId)) .GroupBy(game => game.TitleId, StringComparer.OrdinalIgnoreCase) .OrderBy(group => group.Key, StringComparer.OrdinalIgnoreCase)) { diff --git a/src/Packwright.App/Services/MetadataEditor.cs b/src/Packwright.App/Services/MetadataEditor.cs index ef7136e..9bd22fa 100644 --- a/src/Packwright.App/Services/MetadataEditor.cs +++ b/src/Packwright.App/Services/MetadataEditor.cs @@ -4,6 +4,8 @@ using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Nodes; using Packwright.Core.Backends; +using Packwright.Core.Builders; +using Packwright.Core.Services; using Packwright.Core.Tasks; using Packwright.Containers; using Packwright.Infrastructure; @@ -61,6 +63,7 @@ public static class MetadataEditor { private const string ParamPath = "sce_sys/param.json"; private const string IconPath = "sce_sys/icon0.png"; + private const string IconTexturePath = "sce_sys/icon0.dds"; private const int MaximumParamBytes = 4 * 1024 * 1024; private static readonly JsonSerializerOptions WriteOptions = new() @@ -206,6 +209,17 @@ public static class MetadataEditor return "0x" + head.PadRight(Math.Max(total, 4), '0'); } + + // The texture (icon0.dds) a title carries next to its icon, made from the new picture. + private static byte[] IconTextureFor(byte[] png) + { + try { return Ps5DdsEncoder.EncodePng(png); } + catch (Exception ex) when (ex is SixLabors.ImageSharp.ImageFormatException or NotSupportedException or ArgumentException) + { + throw new InvalidDataException("The icon is not a picture Packwright can read.", ex); + } + } + // ---------------------------------------------------------------- entry point /// @@ -290,7 +304,11 @@ public static class MetadataEditor byte[] patched = PatchParam(existing, edit); await WriteFileAsync(param, patched, token).ConfigureAwait(false); if (icon is not null) + { await WriteFileAsync(Path.Combine(root, "sce_sys", "icon0.png"), icon, token).ConfigureAwait(false); + string texture = Path.Combine(root, "sce_sys", "icon0.dds"); + if (File.Exists(texture)) await WriteFileAsync(texture, IconTextureFor(icon), token).ConfigureAwait(false); + } } catch when (!inPlace) { @@ -344,12 +362,13 @@ public static class MetadataEditor IProgress progress, CancellationToken token) { byte[]? param; - bool hasIcon, hasSys; + bool hasIcon, hasSys, hasTexture; using (var stream = File.OpenRead(image)) using (var volume = new ExfatVolume(stream)) { param = volume.Find(ParamPath) is null ? null : volume.ReadAllBytes(ParamPath, MaximumParamBytes); hasIcon = volume.Find(IconPath) is not null; + hasTexture = volume.Find(IconTexturePath) is not null; hasSys = volume.Find("sce_sys") is not null; } string paramFile = Path.Combine(work, "param.json"); @@ -362,6 +381,12 @@ public static class MetadataEditor string iconFile = Path.Combine(work, "icon0.png"); await File.WriteAllBytesAsync(iconFile, icon, token).ConfigureAwait(false); operations.Add(hasIcon ? ExfatEditOperation.Replace(IconPath, iconFile) : ExfatEditOperation.AddFile(IconPath, iconFile)); + if (hasTexture) + { + string textureFile = Path.Combine(work, "icon0.dds"); + await File.WriteAllBytesAsync(textureFile, IconTextureFor(icon), token).ConfigureAwait(false); + operations.Add(ExfatEditOperation.Replace(IconTexturePath, textureFile)); + } } await ExfatImageMaintenance.ApplyEditsAsync(image, operations, Jobs.Adapt(progress), token).ConfigureAwait(false); } @@ -370,13 +395,14 @@ public static class MetadataEditor IProgress progress, CancellationToken token) { byte[]? param; - bool hasIcon, hasSys; + bool hasIcon, hasSys, hasTexture; using (var volume = new Ufs2Volume(image)) { bool Has(string path, bool directory) => volume.Entries.Any(entry => entry.IsDirectory == directory && entry.Path.Trim('/').Equals(path, StringComparison.OrdinalIgnoreCase)); param = Has(ParamPath, false) ? volume.ReadAllBytes(ParamPath, MaximumParamBytes) : null; hasIcon = Has(IconPath, false); + hasTexture = Has(IconTexturePath, false); hasSys = Has("sce_sys", true); } string paramFile = Path.Combine(work, "param.json"); @@ -389,6 +415,12 @@ public static class MetadataEditor string iconFile = Path.Combine(work, "icon0.png"); await File.WriteAllBytesAsync(iconFile, icon, token).ConfigureAwait(false); operations.Add(hasIcon ? Ufs2EditOperation.Replace(IconPath, iconFile) : Ufs2EditOperation.AddFile(IconPath, iconFile)); + if (hasTexture) + { + string textureFile = Path.Combine(work, "icon0.dds"); + await File.WriteAllBytesAsync(textureFile, IconTextureFor(icon), token).ConfigureAwait(false); + operations.Add(Ufs2EditOperation.Replace(IconTexturePath, textureFile)); + } } await Ufs2Operations.ApplyEditsAsync(image, operations, Jobs.AdaptUfs2(progress), token).ConfigureAwait(false); } @@ -453,7 +485,11 @@ public static class MetadataEditor prefix = sys is null ? string.Empty : sys[..^"sce_sys".Length]; } replacements[prefix + ParamPath] = PatchParam(existing, edit); - if (icon is not null) replacements[prefix + IconPath] = icon; + if (icon is not null) + { + replacements[prefix + IconPath] = icon; + if (volume.FileExists(prefix + IconTexturePath)) replacements[prefix + IconTexturePath] = IconTextureFor(icon); + } } string partial = target + ".packwright-tmp"; @@ -477,6 +513,15 @@ public static class MetadataEditor private static async Task EditPackageAsync(string source, string target, MetadataEdit edit, byte[]? icon, string passcode, string backendId, string work, IProgress progress, CancellationToken token) { + // The quick way: copy the package and rewrite only its metadata. It applies while the content ID is unchanged. + string code = passcode.Length > 0 ? passcode : Packwright.Core.Builders.Ps5DebugPackageCredentials.DefaultPasscode; + Stage(progress, "Rewriting package metadata"); + var relay = new Progress(value => + progress.Report(new PackageTaskProgress(value.Stage, 0, 0, value.CompletedBytes, value.TotalBytes, 0, 0, value.CurrentPath))); + if (await Ps5PackageMetadataEditor.TryEditAsync(source, target, existing => PatchParam(existing, edit), icon, code, relay, token) + .ConfigureAwait(false)) + return; + string temporary = Path.Combine(work, "package.exfat"); Stage(progress, "Unpacking package"); await Jobs.ConvertAsync(source, temporary, Ps5ImageConversionTarget.Exfat, false, true, progress, token, @@ -486,7 +531,6 @@ public static class MetadataEditor string contentId = Jobs.ReadContentId(temporary, Ps5ImageFormat.Exfat); if (contentId.Length == 0) throw new InvalidDataException("The package has no content ID, so it cannot be rebuilt."); IPackageBackend backend = Jobs.ResolveBackend(backendId); - string code = passcode.Length > 0 ? passcode : Packwright.Core.Builders.SonyDebugPackageCredentials.DefaultPasscode; Stage(progress, "Rebuilding package"); await Jobs.BuildPackageAsync(temporary, target, contentId, code, true, new PackageBuildSettings(), backend, progress, token).ConfigureAwait(false); diff --git a/src/Packwright.App/Services/SourceInfo.cs b/src/Packwright.App/Services/SourceInfo.cs index 1b78e26..4226cfe 100644 --- a/src/Packwright.App/Services/SourceInfo.cs +++ b/src/Packwright.App/Services/SourceInfo.cs @@ -51,7 +51,7 @@ public static class SourceInfo return File.Exists(param) ? new Ps5ParamReader().Read(param) : null; } if (!File.Exists(path)) return null; - if (path.EndsWith(".pkg", StringComparison.OrdinalIgnoreCase)) return new SonyPkgGameReader().Read(path); + if (path.EndsWith(".pkg", StringComparison.OrdinalIgnoreCase)) return new Ps5PkgGameReader().Read(path); return Ps5ImageFormatProbeLookup(path); } diff --git a/src/Packwright.App/Views/AboutView.axaml b/src/Packwright.App/Views/AboutView.axaml index 8f94377..fbc74df 100644 --- a/src/Packwright.App/Views/AboutView.axaml +++ b/src/Packwright.App/Views/AboutView.axaml @@ -32,13 +32,12 @@ - - - + + - + diff --git a/src/Packwright.App/Views/EditDetailsWindow.cs b/src/Packwright.App/Views/EditDetailsWindow.cs index 0622f8d..292c002 100644 --- a/src/Packwright.App/Views/EditDetailsWindow.cs +++ b/src/Packwright.App/Views/EditDetailsWindow.cs @@ -598,7 +598,7 @@ public sealed class EditDetailsWindow : Window }; _modeNote.Text = _mode.SelectedIndex switch { - 1 => _item.Game.SourceKind == Ps5SourceKind.SonyPackage + 1 => _item.Game.SourceKind == Ps5SourceKind.Ps5Package ? "The package is unpacked, changed and rebuilt as a debug package (FPKG), as a queued task. This needs free " + "space for a temporary copy and works only for packages Packwright can read (not retail or encrypted ones). " + "The builder sets the required firmware from the game's SDK, so a firmware edit does not stick on packages." diff --git a/src/Packwright.App/Views/LibraryView.Autofill.cs b/src/Packwright.App/Views/LibraryView.Autofill.cs index cbe56f4..e601f47 100644 --- a/src/Packwright.App/Views/LibraryView.Autofill.cs +++ b/src/Packwright.App/Views/LibraryView.Autofill.cs @@ -296,7 +296,7 @@ public partial class LibraryView Kind = JobSpec.EditDetails, Title = item.Title, Source = item.Path, - FromPackage = item.Game.SourceKind == Ps5SourceKind.SonyPackage, + FromPackage = item.Game.SourceKind == Ps5SourceKind.Ps5Package, Passcode = _services.Settings.DebugPasscode, Backend = _services.Settings.BuildBackend, Edit = write, diff --git a/src/Packwright.App/Views/LibraryView.Details.cs b/src/Packwright.App/Views/LibraryView.Details.cs index 9113bfa..431bc88 100644 --- a/src/Packwright.App/Views/LibraryView.Details.cs +++ b/src/Packwright.App/Views/LibraryView.Details.cs @@ -484,7 +484,7 @@ public partial class LibraryView Title = item.Title, Source = item.Path, Output = outcome.Output, - FromPackage = item.Game.SourceKind == Ps5SourceKind.SonyPackage, + FromPackage = item.Game.SourceKind == Ps5SourceKind.Ps5Package, Passcode = _services.Settings.DebugPasscode, Backend = _services.Settings.BuildBackend, Edit = outcome.Edit, @@ -527,7 +527,7 @@ public partial class LibraryView private async Task RenameByInstallOrderAsync() { - List packages = SelectedGames().Where(game => game.SourceKind == Ps5SourceKind.SonyPackage).ToList(); + List packages = SelectedGames().Where(game => game.SourceKind == Ps5SourceKind.Ps5Package).ToList(); if (packages.Count == 0) { await Dialogs.ShowMessageAsync(Dialogs.OwnerOf(this), "Rename by install order", "Select one or more package (.pkg) files first."); diff --git a/src/Packwright.App/Views/SettingsView.axaml b/src/Packwright.App/Views/SettingsView.axaml index 3f1aacb..497199f 100644 --- a/src/Packwright.App/Views/SettingsView.axaml +++ b/src/Packwright.App/Views/SettingsView.axaml @@ -74,8 +74,19 @@ - - + + + + + + + + +