From 0e0d51920a6ab19e69fa38d24947eec04d51fb60 Mon Sep 17 00:00:00 2001 From: Mac Date: Fri, 2 Oct 2026 01:36:32 +0530 Subject: [PATCH] Packwright 1.0.0 A cross-platform (Windows, Linux, macOS) manager for PS5 dumps, images and packages, grown out of PS5 PKG Tool by pearlxcore (GPL-3.0). - Library of dumps, packages and images (cards or list), search language, grouping, saved views, health report and export (CSV, JSON, web page) - Edit and autofill details (including region, IDs, versions, features and icon), in the library or written into the file; optional Title ID lookup - Convert, build, extract, verify, repair and edit exFAT, FFPKG, FFPFSC and ZArchive images and debug packages, with presets - Send to console over FTP or by URL on the local network - Command-line mode - Installers for Windows, macOS and Linux, and an opt-in update check See README.md and THIRD_PARTY_NOTICES.md for credits, licenses and the list of changes made to the original project. --- .gitattributes | 7 + .github/ISSUE_TEMPLATE/bug_report.yml | 83 + .github/ISSUE_TEMPLATE/config.yml | 5 + .github/ISSUE_TEMPLATE/feature_request.yml | 29 + .github/workflows/build.yml | 22 + .github/workflows/release.yml | 222 + .gitignore | 9 + LICENSE | 675 +++ Packwright.slnx | 6 + README.md | 321 ++ THIRD_PARTY_NOTICES.md | 183 + docs/library.webp | Bin 0 -> 155064 bytes installer/Packwright.iss | 58 + installer/linux/build-packages.sh | 96 + installer/linux/packwright.desktop | 11 + src/Packwright.App/App.axaml | 10 + src/Packwright.App/App.axaml.cs | 29 + src/Packwright.App/Assets/icon.ico | Bin 0 -> 52159 bytes src/Packwright.App/Assets/icon.png | Bin 0 -> 69339 bytes src/Packwright.App/Cli.cs | 502 +++ .../Infrastructure/AppSettingsNormalizer.cs | 191 + .../Infrastructure/AppStateStore.cs | 247 + src/Packwright.App/Infrastructure/Logger.cs | 97 + .../Infrastructure/ShellIntegration.cs | 56 + .../Infrastructure/SingleInstance.cs | 97 + src/Packwright.App/Models/FileNode.cs | 142 + src/Packwright.App/Models/LibraryItem.cs | 208 + src/Packwright.App/Packwright.App.csproj | 55 + src/Packwright.App/Program.cs | 37 + src/Packwright.App/Services/AppServices.cs | 212 + src/Packwright.App/Services/ConsoleSender.cs | 176 + src/Packwright.App/Services/Dialogs.cs | 250 ++ src/Packwright.App/Services/IconTools.cs | 70 + src/Packwright.App/Services/JobSpec.cs | 194 + src/Packwright.App/Services/Jobs.cs | 325 ++ src/Packwright.App/Services/LibraryActions.cs | 310 ++ src/Packwright.App/Services/LibraryExport.cs | 152 + src/Packwright.App/Services/LibraryQuery.cs | 182 + src/Packwright.App/Services/MemoryTrimmer.cs | 49 + src/Packwright.App/Services/MetadataEditor.cs | 494 ++ .../Services/MetadataOverrides.cs | 117 + src/Packwright.App/Services/SourceInfo.cs | 84 + .../Services/ThumbnailService.cs | 80 + src/Packwright.App/Services/UpdateService.cs | 74 + src/Packwright.App/Styles/Theme.axaml | 457 ++ src/Packwright.App/Views/AboutView.axaml | 55 + src/Packwright.App/Views/AboutView.axaml.cs | 64 + src/Packwright.App/Views/ConsolesWindow.cs | 246 + src/Packwright.App/Views/EditDetailsWindow.cs | 726 +++ .../Views/FileBrowserWindow.axaml | 15 + .../Views/FileBrowserWindow.axaml.cs | 22 + src/Packwright.App/Views/FilesPanel.axaml | 91 + src/Packwright.App/Views/FilesPanel.axaml.cs | 456 ++ src/Packwright.App/Views/HealthView.axaml | 102 + src/Packwright.App/Views/HealthView.axaml.cs | 248 ++ .../Views/ImageEditorWindow.axaml | 41 + .../Views/ImageEditorWindow.axaml.cs | 239 + .../Views/LibraryView.Autofill.cs | 318 ++ .../Views/LibraryView.Columns.cs | 103 + .../Views/LibraryView.Details.cs | 639 +++ .../Views/LibraryView.Navigation.cs | 46 + src/Packwright.App/Views/LibraryView.axaml | 311 ++ src/Packwright.App/Views/LibraryView.axaml.cs | 616 +++ src/Packwright.App/Views/LogView.axaml | 21 + src/Packwright.App/Views/LogView.axaml.cs | 43 + src/Packwright.App/Views/MainWindow.axaml | 106 + src/Packwright.App/Views/MainWindow.axaml.cs | 298 ++ .../Views/SendToConsoleWindow.cs | 146 + src/Packwright.App/Views/SettingsView.axaml | 123 + .../Views/SettingsView.axaml.cs | 200 + src/Packwright.App/Views/TasksView.axaml | 73 + src/Packwright.App/Views/TasksView.axaml.cs | 167 + src/Packwright.App/Views/ToolsView.Presets.cs | 164 + src/Packwright.App/Views/ToolsView.axaml | 169 + src/Packwright.App/Views/ToolsView.axaml.cs | 508 +++ src/Packwright.App/Views/UpdateWindow.cs | 198 + src/Packwright.App/packwright.cmd | 4 + src/Packwright.Containers/AmprIndex.cs | 148 + src/Packwright.Containers/ExfatAmprPatcher.cs | 779 ++++ src/Packwright.Containers/ExfatImage.cs | 767 ++++ .../ExfatImageMaintenance.cs | 496 +++ src/Packwright.Containers/ExfatUpcaseTable.cs | 70 + src/Packwright.Containers/ExfatVolume.cs | 303 ++ src/Packwright.Containers/FfpfscImage.cs | 730 +++ src/Packwright.Containers/FfpfscModels.cs | 67 + src/Packwright.Containers/FfpfscVolume.cs | 142 + .../ImageConversionService.cs | 297 ++ src/Packwright.Containers/ImageFormatProbe.cs | 83 + .../Packwright.Containers.csproj | 14 + src/Packwright.Containers/PfscCodec.cs | 284 ++ src/Packwright.Containers/PfscModels.cs | 39 + src/Packwright.Containers/PfscReadStream.cs | 125 + src/Packwright.Containers/README.md | 21 + src/Packwright.Containers/ZArchiveImage.cs | 322 ++ src/Packwright.Containers/ZArchiveVolume.cs | 270 ++ src/Packwright.Containers/ZArchiveWriter.cs | 317 ++ .../Backends/BackendCapabilities.cs | 17 + .../Backends/BackendNotSupportedException.cs | 18 + .../Backends/BackendRegistry.cs | 38 + .../Backends/IPackageBackend.cs | 41 + .../Backends/LibProsperoPkg12.cs | 216 + src/Packwright.Core/Backends/LppBackend.cs | 377 ++ src/Packwright.Core/Backends/PptBackend.cs | 47 + .../Builders/ProsperoDebugPackageBuilder.cs | 195 + .../Builders/Ps5InnerCompression.cs | 26 + .../Builders/SonyDebugPackageBuilder.cs | 136 + .../Builders/SonyDebugPackageCredentials.cs | 49 + .../Builders/VolumeDebugPackageBuilder.cs | 256 ++ src/Packwright.Core/Models/Ps5Details.cs | 268 ++ src/Packwright.Core/Models/Ps5GameInfo.cs | 99 + src/Packwright.Core/Models/SectionStatus.cs | 48 + src/Packwright.Core/Models/SonyPkgModels.cs | 108 + src/Packwright.Core/Packwright.Core.csproj | 21 + src/Packwright.Core/Parsers/BigEndian.cs | 9 + src/Packwright.Core/Parsers/Ps5ParamReader.cs | 229 + src/Packwright.Core/Parsers/Ps5SelfReader.cs | 256 ++ .../Parsers/Ps5TrophyReader.cs | 155 + src/Packwright.Core/Parsers/Ps5UdsReader.cs | 235 + .../Parsers/SonyEnginePackageAccess.cs | 476 ++ src/Packwright.Core/Parsers/SonyPfsCrypto.cs | 95 + .../Parsers/SonyPfsEntryDataReader.cs | 74 + src/Packwright.Core/Parsers/SonyPkgReader.cs | 212 + src/Packwright.Core/Parsers/UcpReader.cs | 155 + .../Properties/AssemblyInfo.cs | 3 + .../Services/ConsoleNetwork.cs | 202 + .../Services/FfpfscGameReader.cs | 57 + .../Services/FfpkgGameReader.cs | 56 + .../Services/FilesystemImageGameReader.cs | 68 + src/Packwright.Core/Services/FtpClient.cs | 288 ++ .../Services/GameFileSystem.cs | 562 +++ .../Services/LibraryFileMover.cs | 102 + .../Services/PackageReaderVerification.cs | 43 + .../Services/Ps5DetailsLoader.cs | 338 ++ src/Packwright.Core/Services/Ps5DiskSpace.cs | 82 + .../Services/Ps5DumpLocator.cs | 209 + src/Packwright.Core/Services/Ps5ImageCodec.cs | 61 + .../Services/Ps5LaunchReadiness.cs | 45 + .../Services/Ps5LibraryHealth.cs | 290 ++ .../Services/Ps5LibraryScanner.cs | 182 + .../Services/Ps5MetadataSuggester.cs | 301 ++ .../Services/Ps5RenameFormats.cs | 29 + .../Services/Ps5RenameFormatter.cs | 117 + .../Services/Ps5SdkVersions.cs | 22 + .../Services/Ps5StoreLookup.cs | 258 ++ .../Services/Ps5TitleDatabase.cs | 153 + .../Services/SonyPackageExtraction.cs | 114 + .../Services/SonyPackageImageConversion.cs | 97 + .../Services/SonyPkgGameReader.cs | 132 + .../Services/SourceStructure.cs | 58 + src/Packwright.Core/Services/UpdateChecker.cs | 307 ++ .../Services/ZArchiveGameReader.cs | 57 + src/Packwright.Core/Tasks/PackageTaskQueue.cs | 398 ++ src/Packwright.Core/Tasks/PackageTaskTypes.cs | 22 + .../Tasks/QueuedPackageTask.cs | 288 ++ src/Packwright.Ufs2/AlignedStream.cs | 147 + src/Packwright.Ufs2/DriveIO.cs | 319 ++ src/Packwright.Ufs2/LICENSE | 25 + src/Packwright.Ufs2/Packwright.Ufs2.csproj | 11 + src/Packwright.Ufs2/Ufs1Inode.cs | 119 + src/Packwright.Ufs2/Ufs2Constants.cs | 98 + src/Packwright.Ufs2/Ufs2DirectoryEntry.cs | 69 + src/Packwright.Ufs2/Ufs2Image.cs | 3966 +++++++++++++++++ src/Packwright.Ufs2/Ufs2ImageCreator.cs | 3571 +++++++++++++++ src/Packwright.Ufs2/Ufs2Inode.cs | 154 + src/Packwright.Ufs2/Ufs2Superblock.cs | 535 +++ src/Packwright.Ufs2/Ufs2Volume.cs | 831 ++++ third-party/LibProsperoPkg12/BCnEncoder.dll | Bin 0 -> 264704 bytes .../CommunityToolkit.HighPerformance.dll | Bin 0 -> 123176 bytes third-party/LibProsperoPkg12/LICENSE | 674 +++ .../LibProsperoPkg12/LibProsperoPkg.dll | Bin 0 -> 1057280 bytes .../LibProsperoPkg12/Magick.NET-Q8-AnyCPU.dll | Bin 0 -> 1156272 bytes .../LibProsperoPkg12/Magick.NET.Core.dll | Bin 0 -> 1538736 bytes third-party/LibProsperoPkg12/NOTICE | 48 + third-party/LibProsperoPkg12/VERSION.txt | 21 + .../win-x64/native/Magick.Native-Q8-x64.dll | Bin 0 -> 23965360 bytes third-party/ProsperoPkgTool/LICENSE | 22 + .../ProsperoPkgTool/ProsperoPkgTool.dll | Bin 0 -> 678912 bytes .../ProsperoPkgTool/ProsperoPkgTool.pdb | Bin 0 -> 201796 bytes third-party/ProsperoPkgTool/VERSION.txt | 10 + third-party/licenses/ANGLE-BSD-3-Clause.txt | 32 + third-party/licenses/Apache-2.0.txt | 202 + third-party/licenses/Inter-OFL-1.1.txt | 92 + third-party/licenses/Skia-BSD-3-Clause.txt | 29 + 183 files changed, 38228 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Packwright.slnx create mode 100644 README.md create mode 100644 THIRD_PARTY_NOTICES.md create mode 100644 docs/library.webp create mode 100644 installer/Packwright.iss create mode 100644 installer/linux/build-packages.sh create mode 100644 installer/linux/packwright.desktop create mode 100644 src/Packwright.App/App.axaml create mode 100644 src/Packwright.App/App.axaml.cs create mode 100644 src/Packwright.App/Assets/icon.ico create mode 100644 src/Packwright.App/Assets/icon.png create mode 100644 src/Packwright.App/Cli.cs create mode 100644 src/Packwright.App/Infrastructure/AppSettingsNormalizer.cs create mode 100644 src/Packwright.App/Infrastructure/AppStateStore.cs create mode 100644 src/Packwright.App/Infrastructure/Logger.cs create mode 100644 src/Packwright.App/Infrastructure/ShellIntegration.cs create mode 100644 src/Packwright.App/Infrastructure/SingleInstance.cs create mode 100644 src/Packwright.App/Models/FileNode.cs create mode 100644 src/Packwright.App/Models/LibraryItem.cs create mode 100644 src/Packwright.App/Packwright.App.csproj create mode 100644 src/Packwright.App/Program.cs create mode 100644 src/Packwright.App/Services/AppServices.cs create mode 100644 src/Packwright.App/Services/ConsoleSender.cs create mode 100644 src/Packwright.App/Services/Dialogs.cs create mode 100644 src/Packwright.App/Services/IconTools.cs create mode 100644 src/Packwright.App/Services/JobSpec.cs create mode 100644 src/Packwright.App/Services/Jobs.cs create mode 100644 src/Packwright.App/Services/LibraryActions.cs create mode 100644 src/Packwright.App/Services/LibraryExport.cs create mode 100644 src/Packwright.App/Services/LibraryQuery.cs create mode 100644 src/Packwright.App/Services/MemoryTrimmer.cs create mode 100644 src/Packwright.App/Services/MetadataEditor.cs create mode 100644 src/Packwright.App/Services/MetadataOverrides.cs create mode 100644 src/Packwright.App/Services/SourceInfo.cs create mode 100644 src/Packwright.App/Services/ThumbnailService.cs create mode 100644 src/Packwright.App/Services/UpdateService.cs create mode 100644 src/Packwright.App/Styles/Theme.axaml create mode 100644 src/Packwright.App/Views/AboutView.axaml create mode 100644 src/Packwright.App/Views/AboutView.axaml.cs create mode 100644 src/Packwright.App/Views/ConsolesWindow.cs create mode 100644 src/Packwright.App/Views/EditDetailsWindow.cs create mode 100644 src/Packwright.App/Views/FileBrowserWindow.axaml create mode 100644 src/Packwright.App/Views/FileBrowserWindow.axaml.cs create mode 100644 src/Packwright.App/Views/FilesPanel.axaml create mode 100644 src/Packwright.App/Views/FilesPanel.axaml.cs create mode 100644 src/Packwright.App/Views/HealthView.axaml create mode 100644 src/Packwright.App/Views/HealthView.axaml.cs create mode 100644 src/Packwright.App/Views/ImageEditorWindow.axaml create mode 100644 src/Packwright.App/Views/ImageEditorWindow.axaml.cs create mode 100644 src/Packwright.App/Views/LibraryView.Autofill.cs create mode 100644 src/Packwright.App/Views/LibraryView.Columns.cs create mode 100644 src/Packwright.App/Views/LibraryView.Details.cs create mode 100644 src/Packwright.App/Views/LibraryView.Navigation.cs create mode 100644 src/Packwright.App/Views/LibraryView.axaml create mode 100644 src/Packwright.App/Views/LibraryView.axaml.cs create mode 100644 src/Packwright.App/Views/LogView.axaml create mode 100644 src/Packwright.App/Views/LogView.axaml.cs create mode 100644 src/Packwright.App/Views/MainWindow.axaml create mode 100644 src/Packwright.App/Views/MainWindow.axaml.cs create mode 100644 src/Packwright.App/Views/SendToConsoleWindow.cs create mode 100644 src/Packwright.App/Views/SettingsView.axaml create mode 100644 src/Packwright.App/Views/SettingsView.axaml.cs create mode 100644 src/Packwright.App/Views/TasksView.axaml create mode 100644 src/Packwright.App/Views/TasksView.axaml.cs create mode 100644 src/Packwright.App/Views/ToolsView.Presets.cs create mode 100644 src/Packwright.App/Views/ToolsView.axaml create mode 100644 src/Packwright.App/Views/ToolsView.axaml.cs create mode 100644 src/Packwright.App/Views/UpdateWindow.cs create mode 100644 src/Packwright.App/packwright.cmd create mode 100644 src/Packwright.Containers/AmprIndex.cs create mode 100644 src/Packwright.Containers/ExfatAmprPatcher.cs create mode 100644 src/Packwright.Containers/ExfatImage.cs create mode 100644 src/Packwright.Containers/ExfatImageMaintenance.cs create mode 100644 src/Packwright.Containers/ExfatUpcaseTable.cs create mode 100644 src/Packwright.Containers/ExfatVolume.cs create mode 100644 src/Packwright.Containers/FfpfscImage.cs create mode 100644 src/Packwright.Containers/FfpfscModels.cs create mode 100644 src/Packwright.Containers/FfpfscVolume.cs create mode 100644 src/Packwright.Containers/ImageConversionService.cs create mode 100644 src/Packwright.Containers/ImageFormatProbe.cs create mode 100644 src/Packwright.Containers/Packwright.Containers.csproj create mode 100644 src/Packwright.Containers/PfscCodec.cs create mode 100644 src/Packwright.Containers/PfscModels.cs create mode 100644 src/Packwright.Containers/PfscReadStream.cs create mode 100644 src/Packwright.Containers/README.md create mode 100644 src/Packwright.Containers/ZArchiveImage.cs create mode 100644 src/Packwright.Containers/ZArchiveVolume.cs create mode 100644 src/Packwright.Containers/ZArchiveWriter.cs create mode 100644 src/Packwright.Core/Backends/BackendCapabilities.cs create mode 100644 src/Packwright.Core/Backends/BackendNotSupportedException.cs create mode 100644 src/Packwright.Core/Backends/BackendRegistry.cs create mode 100644 src/Packwright.Core/Backends/IPackageBackend.cs create mode 100644 src/Packwright.Core/Backends/LibProsperoPkg12.cs create mode 100644 src/Packwright.Core/Backends/LppBackend.cs create mode 100644 src/Packwright.Core/Backends/PptBackend.cs create mode 100644 src/Packwright.Core/Builders/ProsperoDebugPackageBuilder.cs create mode 100644 src/Packwright.Core/Builders/Ps5InnerCompression.cs create mode 100644 src/Packwright.Core/Builders/SonyDebugPackageBuilder.cs create mode 100644 src/Packwright.Core/Builders/SonyDebugPackageCredentials.cs create mode 100644 src/Packwright.Core/Builders/VolumeDebugPackageBuilder.cs create mode 100644 src/Packwright.Core/Models/Ps5Details.cs create mode 100644 src/Packwright.Core/Models/Ps5GameInfo.cs create mode 100644 src/Packwright.Core/Models/SectionStatus.cs create mode 100644 src/Packwright.Core/Models/SonyPkgModels.cs create mode 100644 src/Packwright.Core/Packwright.Core.csproj create mode 100644 src/Packwright.Core/Parsers/BigEndian.cs create mode 100644 src/Packwright.Core/Parsers/Ps5ParamReader.cs create mode 100644 src/Packwright.Core/Parsers/Ps5SelfReader.cs create mode 100644 src/Packwright.Core/Parsers/Ps5TrophyReader.cs create mode 100644 src/Packwright.Core/Parsers/Ps5UdsReader.cs create mode 100644 src/Packwright.Core/Parsers/SonyEnginePackageAccess.cs create mode 100644 src/Packwright.Core/Parsers/SonyPfsCrypto.cs create mode 100644 src/Packwright.Core/Parsers/SonyPfsEntryDataReader.cs create mode 100644 src/Packwright.Core/Parsers/SonyPkgReader.cs create mode 100644 src/Packwright.Core/Parsers/UcpReader.cs create mode 100644 src/Packwright.Core/Properties/AssemblyInfo.cs create mode 100644 src/Packwright.Core/Services/ConsoleNetwork.cs create mode 100644 src/Packwright.Core/Services/FfpfscGameReader.cs create mode 100644 src/Packwright.Core/Services/FfpkgGameReader.cs create mode 100644 src/Packwright.Core/Services/FilesystemImageGameReader.cs create mode 100644 src/Packwright.Core/Services/FtpClient.cs create mode 100644 src/Packwright.Core/Services/GameFileSystem.cs create mode 100644 src/Packwright.Core/Services/LibraryFileMover.cs create mode 100644 src/Packwright.Core/Services/PackageReaderVerification.cs create mode 100644 src/Packwright.Core/Services/Ps5DetailsLoader.cs create mode 100644 src/Packwright.Core/Services/Ps5DiskSpace.cs create mode 100644 src/Packwright.Core/Services/Ps5DumpLocator.cs create mode 100644 src/Packwright.Core/Services/Ps5ImageCodec.cs create mode 100644 src/Packwright.Core/Services/Ps5LaunchReadiness.cs create mode 100644 src/Packwright.Core/Services/Ps5LibraryHealth.cs create mode 100644 src/Packwright.Core/Services/Ps5LibraryScanner.cs create mode 100644 src/Packwright.Core/Services/Ps5MetadataSuggester.cs create mode 100644 src/Packwright.Core/Services/Ps5RenameFormats.cs create mode 100644 src/Packwright.Core/Services/Ps5RenameFormatter.cs create mode 100644 src/Packwright.Core/Services/Ps5SdkVersions.cs create mode 100644 src/Packwright.Core/Services/Ps5StoreLookup.cs create mode 100644 src/Packwright.Core/Services/Ps5TitleDatabase.cs create mode 100644 src/Packwright.Core/Services/SonyPackageExtraction.cs create mode 100644 src/Packwright.Core/Services/SonyPackageImageConversion.cs create mode 100644 src/Packwright.Core/Services/SonyPkgGameReader.cs create mode 100644 src/Packwright.Core/Services/SourceStructure.cs create mode 100644 src/Packwright.Core/Services/UpdateChecker.cs create mode 100644 src/Packwright.Core/Services/ZArchiveGameReader.cs create mode 100644 src/Packwright.Core/Tasks/PackageTaskQueue.cs create mode 100644 src/Packwright.Core/Tasks/PackageTaskTypes.cs create mode 100644 src/Packwright.Core/Tasks/QueuedPackageTask.cs create mode 100644 src/Packwright.Ufs2/AlignedStream.cs create mode 100644 src/Packwright.Ufs2/DriveIO.cs create mode 100644 src/Packwright.Ufs2/LICENSE create mode 100644 src/Packwright.Ufs2/Packwright.Ufs2.csproj create mode 100644 src/Packwright.Ufs2/Ufs1Inode.cs create mode 100644 src/Packwright.Ufs2/Ufs2Constants.cs create mode 100644 src/Packwright.Ufs2/Ufs2DirectoryEntry.cs create mode 100644 src/Packwright.Ufs2/Ufs2Image.cs create mode 100644 src/Packwright.Ufs2/Ufs2ImageCreator.cs create mode 100644 src/Packwright.Ufs2/Ufs2Inode.cs create mode 100644 src/Packwright.Ufs2/Ufs2Superblock.cs create mode 100644 src/Packwright.Ufs2/Ufs2Volume.cs create mode 100644 third-party/LibProsperoPkg12/BCnEncoder.dll create mode 100644 third-party/LibProsperoPkg12/CommunityToolkit.HighPerformance.dll create mode 100644 third-party/LibProsperoPkg12/LICENSE create mode 100644 third-party/LibProsperoPkg12/LibProsperoPkg.dll create mode 100644 third-party/LibProsperoPkg12/Magick.NET-Q8-AnyCPU.dll create mode 100644 third-party/LibProsperoPkg12/Magick.NET.Core.dll create mode 100644 third-party/LibProsperoPkg12/NOTICE create mode 100644 third-party/LibProsperoPkg12/VERSION.txt create mode 100644 third-party/LibProsperoPkg12/runtimes/win-x64/native/Magick.Native-Q8-x64.dll create mode 100644 third-party/ProsperoPkgTool/LICENSE create mode 100644 third-party/ProsperoPkgTool/ProsperoPkgTool.dll create mode 100644 third-party/ProsperoPkgTool/ProsperoPkgTool.pdb create mode 100644 third-party/ProsperoPkgTool/VERSION.txt create mode 100644 third-party/licenses/ANGLE-BSD-3-Clause.txt create mode 100644 third-party/licenses/Apache-2.0.txt create mode 100644 third-party/licenses/Inter-OFL-1.1.txt create mode 100644 third-party/licenses/Skia-BSD-3-Clause.txt diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c02dae0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +# Keep the Windows launcher and installer script CRLF and the Linux packaging script LF, whatever the checkout settings. +*.cmd text eol=crlf +*.iss text eol=crlf +*.sh text eol=lf +*.webp binary +*.png binary +*.ico binary diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..9b51ecb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,83 @@ +name: Bug report +description: Something does not work as expected +labels: [bug] +body: + - type: markdown + attributes: + value: | + Thanks for reporting. Please search existing issues first. + Packwright does not provide games, keys or Sony SDK files, and issues asking for them will be closed. + - type: input + id: version + attributes: + label: Packwright version + description: Shown on the About page and in the sidebar, for example 1.0.0 + validations: + required: true + - type: dropdown + id: os + attributes: + label: Operating system + options: + - Windows + - Linux + - macOS (Apple Silicon) + - macOS (Intel) + validations: + required: true + - type: input + id: osversion + attributes: + label: OS version + placeholder: Windows 11 24H2, Ubuntu 24.04, macOS 15.1 + - type: dropdown + id: area + attributes: + label: Area + options: + - Library / scanning / list view + - Edit details / autofill / online lookup + - Tools (build, convert, extract, presets) + - Image editor / file browser + - Tasks + - Library health + - Send to console + - Export + - Command line + - Installer / updates + - ZArchive (.zar) + - Packages (.pkg) + - Other + validations: + required: true + - type: textarea + id: what + attributes: + label: What happened? + description: What you expected and what you saw instead. + validations: + required: true + - type: textarea + id: steps + attributes: + label: Steps to reproduce + placeholder: | + 1. Open Tools + 2. Choose a game folder as the source + 3. ... + validations: + required: true + - type: input + id: format + attributes: + label: Input type and size + description: Folder dump, .pkg, .ffpkg, .exfat, .ffpfs, .zar and roughly how large it is. + - type: textarea + id: log + attributes: + label: Log + description: | + Copy the relevant lines from the Log page (Log > Open log folder), or attach the latest file from + `%LOCALAPPDATA%\Packwright\logs` (Windows) or `~/.local/share/Packwright/logs` (Linux/macOS). + Remove personal paths if you want to. + render: text diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..8d1624a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Questions and ideas + url: https://github.com/itsmemac/Packwright/discussions + about: Ask usage questions here (enable Discussions in the repository settings). diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..afffc5e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,29 @@ +name: Feature request +description: Suggest an improvement or a new capability +labels: [enhancement] +body: + - type: textarea + id: problem + attributes: + label: What problem would this solve? + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed solution + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + - type: dropdown + id: os + attributes: + label: Platforms affected + multiple: true + options: + - Windows + - Linux + - macOS diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..c1cd8ef --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,22 @@ +name: Build + +# Pull requests are built on all four targets to catch breakage. Pushes to main are built and released +# by release.yml. +on: + pull_request: + workflow_dispatch: + +jobs: + build: + strategy: + fail-fast: false + matrix: + rid: [win-x64, linux-x64, osx-arm64, osx-x64] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + - name: Publish + run: dotnet publish src/Packwright.App/Packwright.App.csproj -c Release -r ${{ matrix.rid }} --self-contained true -o publish diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..34e74f0 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,222 @@ +name: Release + +# Every push to main builds the app for Windows, Linux and macOS and publishes (or refreshes) a GitHub +# release named after the in src/Packwright.App/Packwright.App.csproj. To cut a new +# release, bump that version and push. +on: + push: + branches: [main] + paths-ignore: + - '**.md' + - 'LICENSE' + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: release + cancel-in-progress: false + +env: + PROJECT: src/Packwright.App/Packwright.App.csproj + APP: Packwright + +jobs: + version: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.read.outputs.version }} + steps: + - uses: actions/checkout@v4 + - id: read + run: | + version=$(sed -n 's:.*\(.*\).*:\1:p' "$PROJECT" | head -n1) + if [ -z "$version" ]; then echo "No found in $PROJECT" >&2; exit 1; fi + echo "version=$version" >> "$GITHUB_OUTPUT" + + build-linux: + needs: version + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + - name: Publish + run: dotnet publish "$PROJECT" -c Release -r linux-x64 --self-contained true -o publish + - name: Add the licenses + 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 + run: | + sudo apt-get update -qq + sudo apt-get install -y -qq rpm + bash installer/linux/build-packages.sh "${{ needs.version.outputs.version }}" publish dist + - uses: actions/upload-artifact@v4 + with: + name: release-linux-packages + path: dist/* + + build-windows-installer: + needs: version + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + - name: Publish + shell: bash + run: dotnet publish "$PROJECT" -c Release -r win-x64 --self-contained true -o publish + - name: Add the licenses + shell: bash + 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 + run: choco install innosetup -y --no-progress + - name: Build the installer + shell: pwsh + run: | + New-Item -ItemType Directory -Force dist | Out-Null + $iscc = Join-Path ${env:ProgramFiles(x86)} 'Inno Setup 6\ISCC.exe' + & $iscc "/DAppVersion=${{ needs.version.outputs.version }}" "/DSourceDir=$env:GITHUB_WORKSPACE\publish" "/O$env:GITHUB_WORKSPACE\dist" installer\Packwright.iss + if ($LASTEXITCODE -ne 0) { throw "Inno Setup failed ($LASTEXITCODE)" } + - uses: actions/upload-artifact@v4 + with: + name: release-windows-installer + path: dist/*.exe + + build-macos: + needs: version + runs-on: macos-latest + strategy: + fail-fast: true + matrix: + include: + - { rid: osx-arm64, label: macos-arm64 } + - { rid: osx-x64, label: macos-x64 } + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + - name: Publish + run: dotnet publish "$PROJECT" -c Release -r ${{ matrix.rid }} --self-contained true -o publish + - name: Create the app bundle + run: | + version="${{ needs.version.outputs.version }}" + bundle="Packwright.app" + mkdir -p "$bundle/Contents/MacOS" "$bundle/Contents/Resources" + cp -R publish/. "$bundle/Contents/MacOS/" + 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/" + + # Icon: build an .icns from the PNG + iconset=icon.iconset + mkdir "$iconset" + for size in 16 32 128 256 512; do + double=$((size * 2)) + sips -z $size $size src/Packwright.App/Assets/icon.png --out "$iconset/icon_${size}x${size}.png" > /dev/null + sips -z $double $double src/Packwright.App/Assets/icon.png --out "$iconset/icon_${size}x${size}@2x.png" > /dev/null + done + iconutil -c icns "$iconset" -o "$bundle/Contents/Resources/icon.icns" + + cat > "$bundle/Contents/Info.plist" < + + + + CFBundleNamePackwright + CFBundleDisplayNamePackwright + CFBundleIdentifierio.github.itsmemac.packwright + CFBundleVersion${version} + CFBundleShortVersionString${version} + CFBundleExecutable${APP} + CFBundleIconFileicon + CFBundlePackageTypeAPPL + LSMinimumSystemVersion13.0 + NSHighResolutionCapable + + + PLIST + - name: Sign ad hoc + # Apple Silicon refuses to start unsigned code. An ad-hoc signature is enough to run locally; + # the app is not notarized, so the first launch still needs right click, then Open. + run: codesign --force --deep --sign - "Packwright.app" + - name: Build the disk image + # The disk image holds the app and a shortcut to Applications: open it and drag the app across. + run: | + mkdir dist dmgroot + name="$APP-v${{ needs.version.outputs.version }}-${{ matrix.label }}" + ditto "Packwright.app" "dmgroot/Packwright.app" + ln -s /Applications dmgroot/Applications + hdiutil create -volname "Packwright" -srcfolder dmgroot -ov -format UDZO "dist/$name.dmg" + - uses: actions/upload-artifact@v4 + with: + name: release-${{ matrix.label }} + path: dist/* + + release: + needs: [version, build-linux, build-windows-installer, build-macos] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + pattern: release-* + merge-multiple: true + path: dist + - name: Checksums and notes + run: | + version="${{ needs.version.outputs.version }}" + (cd dist && sha256sum * > SHA256SUMS.txt) + cat > notes.md <.deb\` / \`sudo dnf install ./.rpm\`. It adds a menu entry and a \`packwright\` command, and is removed with your package manager. + + Verify a download with \`SHA256SUMS.txt\`. + + **License and source:** GPL-3.0, with no warranty. Packwright grew out of PS5 PKG Tool by pearlxcore (GPL-3.0), whose engines still power parts of it; the changes and every bundled component are listed in THIRD_PARTY_NOTICES.md, and the license texts are in the \`licenses\` folder of the installed app. The source for this release is the \`v${version}\` tag of this repository. + NOTES + - name: Publish the release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + tag="v${{ needs.version.outputs.version }}" + # Replace an earlier release of the same version so the tag points at this build. + gh release delete "$tag" --cleanup-tag --yes 2>/dev/null || true + gh release create "$tag" dist/* \ + --target "$GITHUB_SHA" \ + --title "Packwright $tag" \ + --notes-file notes.md \ + --latest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cc1c2ac --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.vs/ +**/bin/ +**/obj/ +*.user +*.suo +publish/ + +# Locally built installers +installer/out/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a0ca524 --- /dev/null +++ b/LICENSE @@ -0,0 +1,675 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + 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. + + + Copyright (C) + + 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 . + +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: + + Copyright (C) + 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 +. + + 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 +. + diff --git a/Packwright.slnx b/Packwright.slnx new file mode 100644 index 0000000..32c35f3 --- /dev/null +++ b/Packwright.slnx @@ -0,0 +1,6 @@ + + + + + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..412cc86 --- /dev/null +++ b/README.md @@ -0,0 +1,321 @@ +# Packwright + +One app to manage your PS5 dump and image collection, read PS5 packages, fix their details, and build, convert and send them. For **Windows, Linux and macOS**. + +![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. +> 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.* + +> **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. + +## Contents + +- [Highlights](#highlights) +- [Download and install](#download-and-install) +- [macOS help](#macos-help) +- [Getting started](#getting-started) +- [Features](#features) +- [Command line](#command-line) +- [Searching your library](#searching-your-library) +- [Where your data lives](#where-your-data-lives) +- [Platform support](#platform-support) +- [Build from source](#build-from-source) +- [Releases](#releases) +- [Project layout](#project-layout) +- [License and credits](#license-and-credits) + +## Highlights + +- **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. +- **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. +- **Command line** for scripts and servers. +- **Real installers** for every system, with a built-in update check that you control. + +## Download and install + +Download the installer for your system from the [latest release](https://github.com/itsmemac/Packwright/releases/latest). Everything is self-contained, so no .NET install is needed. + +| System | Download | Install | +| --- | --- | --- | +| Windows 10/11 (64-bit) | `Packwright-Setup-v-windows-x64.exe` | run it (no administrator rights needed) | +| macOS, Apple silicon | `Packwright-v-macos-arm64.dmg` | open it, drag **Packwright** onto **Applications** | +| macOS, Intel | `Packwright-v-macos-x64.dmg` | open it, drag **Packwright** onto **Applications** | +| Linux (64-bit), Debian, Ubuntu, Mint | `Packwright-v-linux-x64.deb` | your software installer, or `sudo apt install ./.deb` | +| Linux (64-bit), Fedora, openSUSE, RHEL | `Packwright-v-linux-x64.rpm` | your software installer, or `sudo dnf install ./.rpm` | + +- **Uninstall:** Windows, Settings > Apps (or the Start menu entry); macOS, delete Packwright from Applications; Linux, `sudo apt remove packwright` or `sudo dnf remove packwright`. +- **Windows:** the installer is not code-signed, so Windows may say the publisher is unknown. +- **macOS:** the app is not notarized by Apple, so macOS asks for your approval the first time you open it. See [macOS help](#macos-help) for the steps. +- **Linux:** the package adds a menu entry and a `packwright` command (see [Command line](#command-line)). +- Each release also has `SHA256SUMS.txt` so you can verify a download. + +Packwright only writes to your user profile (settings, library cache and thumbnails), never to the install folder. + +## macOS help + +Packwright needs **macOS 13 (Ventura) or later**. The macOS builds are less tested than the Windows one, so please report anything odd. + +**1. Pick the right download** + +Open the Apple menu, choose **About This Mac** and look at the line **Chip** or **Processor**: + +- **Apple M1, M2, M3, M4...** (Apple silicon): `Packwright-v-macos-arm64.dmg` +- **Intel**: `Packwright-v-macos-x64.dmg` + +**2. Install** + +1. Double-click the downloaded `.dmg`. +2. Drag **Packwright** onto the **Applications** shortcut in the window that opens. +3. Eject the disk image (the eject button next to it in Finder's sidebar), then you can delete the `.dmg`. + +**3. Open it the first time** + +Packwright is not notarized by Apple (that needs a paid Apple developer account), so macOS shows a warning such as "Apple could not verify Packwright is free of malware" the first time. This is expected, and you only have to do it once. Following Apple's own steps ([Safely open apps on your Mac](https://support.apple.com/en-us/102445)): + +1. Open **Packwright** from Applications. When the warning appears, click **Done** (or **OK**). +2. Open the Apple menu, then **System Settings**, then **Privacy & Security**. +3. Scroll down to the **Security** section. You will see a note that "Packwright" was blocked. Click **Open Anyway** and enter your password or use Touch ID. +4. Click **Open** in the next prompt. + +From then on, Packwright opens with a normal double-click. + +On macOS 14 (Sonoma) and earlier you can instead **Control-click** (or right-click) Packwright in Applications, choose **Open**, then click **Open** again. + +Only do this for downloads you got from this project's [releases page](https://github.com/itsmemac/Packwright/releases). You can check a download against `SHA256SUMS.txt` in Terminal with `shasum -a 256 .dmg`. + +**If it still will not open** + +- *"Packwright is damaged and can't be opened"* is macOS's wording for a blocked download. Run this once in Terminal, then open the app again: + + ``` + xattr -cr /Applications/Packwright.app + ``` + +- The window does not appear: open **Terminal** and run `/Applications/Packwright.app/Contents/MacOS/Packwright version`. If it prints a version, the app is fine and the problem is only the permission step above. If you report a bug, include the log (**Log** page, then **Open log folder**). +- Files and folders: the first time Packwright reads a folder on an external drive, Downloads or Desktop, macOS asks whether to allow it. Choose **Allow**, or add Packwright under **System Settings, Privacy & Security, Files and Folders** (or **Full Disk Access**). + +**Updating and removing** + +- **Update:** Packwright can check for new versions (**About**, then **Check for updates**). It downloads and verifies the new `.dmg`; open it and drag Packwright onto **Applications** again, choosing **Replace**. +- **Uninstall:** quit Packwright and drag it from Applications to the Bin. Its settings and library cache live in `~/.local/share/Packwright`; delete that folder too if you want a clean removal. +- **Command line:** `/Applications/Packwright.app/Contents/MacOS/Packwright help` (see [Command line](#command-line)). + +## Getting started + +1. Open **Library** and choose **Add folder**. Point it at a folder with PS5 dumps, `.pkg` files or images. Use **More > Add a package or image file** for a single item. +2. Browse your collection as artwork cards or as a list. Select a game to see its details on the right. +3. Right-click a game for the things you can do with it: convert or build it, edit its details, send it to a console, rename, move or delete it. +4. Open **Tools** to convert, extract, verify or repair something. The source can come from the library, the file pickers, drag and drop onto the window, or the command line: + + ``` + Packwright "D:\Games\MyGame" + Packwright --open "D:\Games\MyGame.pkg" + ``` + +5. Every job is queued on the **Tasks** page, where you can follow progress, cancel, retry or clear it. Queued work survives a restart. + +Packwright runs as one window: starting it again brings the open window to the front. It remembers the window's size, position and whether it was maximized. + +## Features + +### Library + +- Scans folders for unpacked dumps, Sony `.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. +- Rename with a preview (presets or your own format), rename packages by install order, move, delete, find duplicates, and list updates or DLC that are missing a base game. +- **Export** what is shown (**More > Export what is shown**) as a CSV spreadsheet, a JSON file, or a single web page with artwork, summary figures, search and sortable columns that loads nothing from the internet. For JSON and the web page you choose whether file paths are included. +- The layout adapts to the window: the details panel becomes a slide-over and the sidebar shrinks to icons when the window is narrow. + +### Game details + +- Overview, artwork (with save), trophies (with icon export), activities, executable, package data, statistics and raw `param.json`. +- A file explorer for dumps, images and packages: a folder tree with icons, sizes and file counts, filtering, extraction of single files, whole folders or everything, and a preview for images (including DDS), text and raw bytes. It can also open in its own window with the preview beside the tree. + +### Fix and complete details + +- **Edit details and icon** of any title (right click it, or use **Edit details** in the details panel). Every detail the Overview shows can be changed: title, Title ID, Content ID, region, Concept ID, category, version, master version, required firmware, SDK version, DRM, default language, created date, tool version and the declared features (HDR, VRR, 120 Hz, PS VR2, PS5 Pro and so on), plus the icon. The region is the first two letters of the Content ID, so choosing one rewrites it. Format, size and path describe the file itself and cannot be edited. Keep the change in the library only, or write it into the file itself or a new copy: dump folders, exFAT, FFPKG, FFPFSC, ZArchive and debug packages are supported. If a dump or archive has no `param.json`, one is created. Packages are rebuilt as debug packages (retail or encrypted ones cannot be), and the builder sets their required firmware from the game's SDK. +- **Autofill missing details:** titles that lack a `param.json` are marked "Missing details" (search them with `missing:yes`). **Autofill missing** in Edit details, or **More > Fix missing details** for the whole library, works out what it can from the title itself (`nptitle.dat`, trophy data, the executable header, artwork) and from its file and folder names, shows you what it found and where it came from, and saves it only when you confirm. It never uses the network. Dumps that were stripped of their metadata may have little to go on. +- **Search PlayStation Network:** in Edit details, one button looks up the title's Title ID (read from the file or its name) in a public PS5 title list (andshrew's list, a 2 MB file downloaded once from GitHub and kept on your computer) and shows the matching entries to pick from. Picking one fills the title, Title ID, content ID and, from the PlayStation Store, the icon. You can also search by game name. **More > Look up missing details online** does this for the whole library for every title that has a Title ID. The only things sent are that one file download and, for icons, the game's name to store.playstation.com, about one request per second and only when you ask. The store search is not an official API and can change. + +### Library health + +The **Health** page checks the whole library at a glance: files that were moved or deleted, files that cannot be read, missing details, file names that disagree with their Title IDs, updates without their base game, exact duplicates, older copies and superseded updates (with the space they take), and drives that are nearly full. **Check files too** also looks inside every title for a missing icon and checks unpacked dumps for launch problems. Each issue has buttons to show the title in the Library, fix its details, or open its folder, and the report can be exported as Markdown. Nothing is deleted for you. + +### Image tools + +- **Convert** between dump folders and **exFAT**, **FFPKG**, **FFPFSC** and **ZArchive (`.zar`)** images, or build a debug package (**FPKG**) from a dump or an image. A `.zar` can be used as a source too: extract it, verify it, convert it to another image, or build a package from it. +- **Presets:** save the target and all its options under a name and apply them again in one click. A few presets are built in (for example the smallest FFPFSC, or a reproducible package). A preset never contains the source or output path. +- **Extract** a dump tree out of an image or package, and **verify** images and packages. +- **Edit files** inside exFAT and FFPKG images (replace, add, import a folder, create folders, delete). Changes are rebuilt beside the original and swapped in only after verification. +- **Repair** a damaged exFAT image, **refresh the AMPR index**, and **rebuild** FFPKG metadata. +- ZArchive files are written and read by a managed implementation of the [ZArchive](https://github.com/Exzap/ZArchive) format (zstd blocks, SHA-256 integrity). A new archive is verified after the build, and a `.zar` in your library is browsed in place, without unpacking it. + +### 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. + +### 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. + +- **FTP upload:** large files are streamed, a partly sent file is continued, a copy that is already there is skipped, the size is checked afterwards, and dropped connections are retried. Dump folders upload file by file. It runs as a background task with progress on the Tasks page. +- **Install by URL** (for a `.pkg`): Packwright shares the file from your PC for the length of the download and asks the console's package installer to fetch it. Installer payloads differ in the request they expect, so the request is a template you can adjust; its defaults follow the common "remote package installer" style and are **untested** on a real console. +- **Consoles** are set up in **Settings > Manage consoles** (address, FTP port, default folder, **Test connection**). +- **Privacy and safety:** only addresses on your local network are accepted, nothing is contacted until you press Send or Test, and no account or password is stored. Your firewall may ask you to allow Packwright while it shares a file. +- Also available as `packwright send` (see [Command line](#command-line)). + +### Updates + +Packwright can check GitHub for a newer version. It asks once, **About > Check for updates** works any time, and automatic checks run at most once a day and can be turned off in **Settings**. When a new version is out you see what is new and the download is checked against the release's `SHA256SUMS.txt`. On Windows, one button then installs it and restarts Packwright; on Linux and macOS it downloads the right package and tells you how to install it. Nothing is installed without you pressing a button, and the only server contacted is github.com. + +### 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. +- 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). + +## Command line + +Everything heavy can also run without the window, for scripts and servers. On Windows use `packwright.cmd` (installed beside `Packwright.exe`) so the prompt waits for the result; on Linux the package adds a `packwright` command, and on macOS run `/Applications/Packwright.app/Contents/MacOS/Packwright`. + +``` +packwright info "D:\Games\PS5\Quake\PPSA01880-app0.zar" +packwright scan D:\Games\PS5 --csv -o library.csv # or --json +packwright health D:\Games\PS5 --md # exit code 1 when a real problem is found +packwright verify game.ffpkg +packwright extract game.pkg -o out-folder --passcode 00000000000000000000000000000000 +packwright convert dump-folder --to ffpfsc --level 9 +packwright convert game.zar --to exfat --preset "exFAT image with AMPR index" +packwright send big.pkg --to "My PS5" --folder /data/games # FTP; add --install to let the console download a .pkg +packwright consoles # the consoles saved in Settings +packwright help +``` + +`convert` takes the same options as the Tools page (and its presets, saved or built in). Exit codes: 0 done, 1 failed, 2 wrong usage, 130 cancelled. Apart from `send`, which talks to a console on your local network, the command line only uses local files and never connects to the internet. + +## Searching your library + +Type in the search box. Words are combined with AND, `|` means OR, and a leading `-` excludes. + +| Term | Matches | +| --- | --- | +| `bloodborne` | any field containing the text | +| `"spider man"` | an exact phrase | +| `title:` `id:` `content:` | name, Title ID, Content ID | +| `category:` `role:` `region:` `format:` | category (Game, Patch, DLC), role, region, format | +| `version:` `fw:` `path:` `feature:` | version, required firmware, path, declared feature | +| `missing:yes` | titles with missing details | +| `size:>50GB` `version:>=01.05` | comparisons (`>` `>=` `<` `<=`) on size and version | +| `id:=PPSA12345` | `=` after the colon requires an exact match | +| `-dlc` | hide anything matching | + +Example: `id:PPSA01 size:>20GB -dlc | category:patch`. + +## Where your data lives + +| What | Location | +| --- | --- | +| 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 | + +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). + +## Platform support + +| Feature | Windows | Linux | macOS | +| --- | :---: | :---: | :---: | +| 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 | +| 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. + +## Build from source + +Requirements: the [.NET 10 SDK](https://dotnet.microsoft.com/download). No other tools or sibling repositories are needed. + +``` +git clone https://github.com/itsmemac/Packwright.git +cd Packwright + +dotnet build Packwright.slnx -c Release +dotnet run --project src/Packwright.App +``` + +Publish a self-contained build for one system (run from the repository root): + +``` +dotnet publish src/Packwright.App -c Release -r win-x64 --self-contained true -o publish/win-x64 +dotnet publish src/Packwright.App -c Release -r linux-x64 --self-contained true -o publish/linux-x64 +dotnet publish src/Packwright.App -c Release -r osx-arm64 --self-contained true -o publish/osx-arm64 +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. + +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`. + +## Releases + +Releases are automatic. Every push to `main` builds the app for Windows, Linux and macOS and publishes a GitHub release named after the `` in `src/Packwright.App/Packwright.App.csproj`. Every download is an installer (a Windows setup program, macOS disk images, and Linux `.deb` and `.rpm` packages), attached with `SHA256SUMS.txt`. There are no zip or tar archives. + +- To publish a new version, raise `` and push to `main`. +- Pushing again without changing the version refreshes that version's release with the new build. +- Changes that only touch Markdown files or `LICENSE` do not trigger a release. +- Pull requests are built on all four targets (without publishing) to catch breakage early. + +The workflows live in `.github/workflows/`: `release.yml` and `build.yml`. + +## Project layout + +``` +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.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 +docs/ Screenshots +.github/workflows/ Build and release automation +Packwright.slnx Solution +``` + +## License and credits + +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. + +- **[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. +- **[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 diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..1c143a9 --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,183 @@ +# Third-party notices + +Packwright, Copyright (c) 2026 itsmemac, is licensed under the GNU General Public License v3.0 +(see `LICENSE`). It is based on the original application listed first below. The components after +it are used under their own terms. + +## PS5 PKG Tool (original application) + +Packwright is based on PS5 PKG Tool by pearlxcore (https://github.com/pearlxcore/PS5PkgTool), +licensed under the GNU General Public License v3.0. Copyright (c) pearlxcore. The exFAT, UFS2/FFPKG, +PFSC/FFPFSC and package code, the library scanner and the task queue originate from that project, and +the cross-platform application logic is ported from its Windows interface. The original copyright +notice and license are retained in this repository. + +### Modifications (GPL-3.0 section 5) + +Packwright is a **modified version** of PS5 PKG Tool. The changes below were made in 2026 by +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 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 + library or written into dumps, images and packages, with offline autofill and an optional lookup of Title IDs in a + public title list; +- a library health report, export of the library (CSV, JSON, web page), saved presets for conversions, a command-line + mode, and sorting and column choice in the list view; +- "Send to console": an FTP client and a local file server for installers on the user's own network; +- an update check against this repository's GitHub releases, and installers for Windows (Inno Setup), macOS (disk image) + and Linux (.deb and .rpm); +- the projects and folders were renamed and reorganised, and build and release automation was added. + +Everything else remains under the original GPL-3.0 terms. The application icon is original to this +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. + +## ProsperoPkgTool + +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`. + +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. + +## LibProsperoPkg + +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. + +## UFS2Tool + +The `src/Packwright.Ufs2` filesystem implementation is based on +[SvenGDK/UFS2Tool](https://github.com/SvenGDK/UFS2Tool), used and modified under the BSD 2-Clause +License. Copyright (c) 2026, SvenGDK. The original copyright and license text are retained in +`src/Packwright.Ufs2/LICENSE` and in the imported source files. + +## Avalonia UI + +The cross-platform interface is built on Avalonia UI (and Avalonia.Controls.DataGrid, +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 +It provides the zstd compression used for ZArchive output. zstd itself is BSD-licensed, Copyright +(c) Meta Platforms, Inc. and affiliates. + +## ZArchive + +The `.zar` container format was designed by Exzap (https://github.com/Exzap/ZArchive, Copyright 2022 +Exzap, "MIT No Attribution" license). The writer and reader in `src/Packwright.Containers` +(`ZArchiveWriter.cs`, `ZArchiveVolume.cs`, `ZArchiveImage.cs`) are managed C# implementations of the +format that follow the reference implementation, and were checked against archives made by it. + +## Graphics libraries bundled with Avalonia + +- **SkiaSharp** (MIT License, Copyright (c) Microsoft Corporation and contributors) with the **Skia** + graphics library (BSD 3-Clause, Copyright (c) 2011 Google Inc.): `third-party/licenses/Skia-BSD-3-Clause.txt`. +- **HarfBuzzSharp** (MIT License) with **HarfBuzz** (Old MIT License, Copyright (c) the HarfBuzz authors). +- **ANGLE** (`av_libglesv2.dll`, BSD 3-Clause, Copyright 2018 The ANGLE Project Authors): + `third-party/licenses/ANGLE-BSD-3-Clause.txt`. +- **MicroCom.Runtime**, **Tmds.DBus.Protocol** and the other Avalonia platform packages are MIT licensed. + +## Icons and fonts + +- Interface icons (sidebar and file-type symbols) are paths from **Material Design Icons** by Google, + licensed under the Apache License 2.0 (`third-party/licenses/Apache-2.0.txt`). +- The **Inter** font (bundled through Avalonia.Fonts.Inter) is Copyright (c) 2016 The Inter Project + 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 + +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. + +## DarkUI + +Earlier Windows-only versions of the original application were built on DarkUI (MIT License, +Copyright (c) 2017 Robin Perris, https://github.com/RobinPerris/DarkUI). DarkUI is no longer part of +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 + +## PlayStation-Titles (title list, downloaded on request) + +When you ask Packwright to look a title up by its Title ID, it downloads the PS5 title list kept by andshrew, +https://github.com/andshrew/PlayStation-Titles (MIT License, Copyright (c) 2022 andshrew), from GitHub and keeps +a copy in its data folder. The list is not part of Packwright or of any download; it is fetched only when you use +that feature. Names and IDs in it belong to their owners. + +## Image format references + +The managed exFAT, UFS2/FFPKG, PFSC and PFS implementations were written from public format +documentation and validated against independent tools. Credit to the authors of the tools that +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. +- 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 +copyright notice given for it in its own section. + +``` +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. +``` diff --git a/docs/library.webp b/docs/library.webp new file mode 100644 index 0000000000000000000000000000000000000000..b57c0dda6832a64c06b955a8a764025320f5957e GIT binary patch literal 155064 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 #Gt%h6c5SR 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 /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.0" +#endif +#ifndef SourceDir + #define SourceDir "..\publish" +#endif + +[Setup] +AppId={{B7C1F3A2-5D1E-4C3B-9A0E-6F5D2E8C7A41} +AppName=Packwright +AppVersion={#AppVersion} +AppVerName=Packwright {#AppVersion} +AppPublisher=itsmemac +AppPublisherURL=https://github.com/itsmemac/Packwright +AppSupportURL=https://github.com/itsmemac/Packwright/issues +AppUpdatesURL=https://github.com/itsmemac/Packwright/releases +VersionInfoVersion={#AppVersion} +DefaultDirName={autopf}\Packwright +DefaultGroupName=Packwright +DisableProgramGroupPage=yes +PrivilegesRequired=lowest +PrivilegesRequiredOverridesAllowed=dialog +ArchitecturesAllowed=x64compatible +ArchitecturesInstallIn64BitMode=x64compatible +OutputBaseFilename=Packwright-Setup-v{#AppVersion}-windows-x64 +Compression=lzma2/max +SolidCompression=yes +WizardStyle=modern +SetupIconFile=..\src\Packwright.App\Assets\icon.ico +UninstallDisplayIcon={app}\Packwright.exe +LicenseFile=..\LICENSE +; Close a running Packwright before replacing its files, and start it again afterwards. +CloseApplications=yes +RestartApplications=no +CloseApplicationsFilter=Packwright.exe + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Tasks] +Name: "desktopicon"; Description: "Create a desktop shortcut"; GroupDescription: "Shortcuts:"; Flags: unchecked + +[Files] +Source: "{#SourceDir}\*"; DestDir: "{app}"; Flags: recursesubdirs createallsubdirs ignoreversion + +[Icons] +Name: "{autoprograms}\Packwright"; Filename: "{app}\Packwright.exe" +Name: "{autodesktop}\Packwright"; Filename: "{app}\Packwright.exe"; Tasks: desktopicon + +[Run] +Filename: "{app}\Packwright.exe"; Description: "Start Packwright"; Flags: nowait postinstall skipifsilent +; A silent update (started by the app's "Install now") has no finish page, so start Packwright again here. +Filename: "{app}\Packwright.exe"; Flags: nowait runasoriginaluser; Check: WizardSilent diff --git a/installer/linux/build-packages.sh b/installer/linux/build-packages.sh new file mode 100644 index 0000000..a4f0624 --- /dev/null +++ b/installer/linux/build-packages.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# Builds the Linux installers from a linux-x64 publish folder: +# Packwright-v-linux-x64.deb (Debian, Ubuntu, Mint, ...) +# Packwright-v-linux-x64.rpm (Fedora, RHEL family, openSUSE, ...) +# +# Usage: installer/linux/build-packages.sh +# Needs dpkg-deb and rpmbuild (apt install dpkg rpm). Both packages put the app in /opt/packwright, add a +# `packwright` command and a menu entry, and are removed again by the normal package manager. +set -euo pipefail + +version="${1:?version}" +publish="$(cd "${2:?publish folder}" && pwd)" +mkdir -p "${3:?output folder}" +out="$(cd "$3" && pwd)" +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo="$(cd "$here/../.." && pwd)" +name="Packwright-v${version}-linux-x64" + +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT +stage="$work/stage" + +install -d "$stage/opt/packwright" "$stage/usr/bin" "$stage/usr/share/applications" \ + "$stage/usr/share/icons/hicolor/512x512/apps" "$stage/usr/share/doc/packwright" +cp -a "$publish/." "$stage/opt/packwright/" +chmod 755 "$stage/opt/packwright/Packwright" +ln -s /opt/packwright/Packwright "$stage/usr/bin/packwright" +install -m 644 "$here/packwright.desktop" "$stage/usr/share/applications/packwright.desktop" +install -m 644 "$repo/src/Packwright.App/Assets/icon.png" "$stage/usr/share/icons/hicolor/512x512/apps/packwright.png" +install -m 644 "$repo/LICENSE" "$stage/usr/share/doc/packwright/copyright" +install -m 644 "$repo/THIRD_PARTY_NOTICES.md" "$stage/usr/share/doc/packwright/THIRD_PARTY_NOTICES.md" +find "$stage" -type d -exec chmod 755 {} + +find "$stage" -type f ! -perm /111 -exec chmod 644 {} + + +# ------------------------------------------------------------------ .deb +debroot="$work/deb" +cp -a "$stage" "$debroot" +install -d "$debroot/DEBIAN" +size_kb="$(du -sk "$debroot" | cut -f1)" +cat > "$debroot/DEBIAN/control" < +Homepage: https://github.com/itsmemac/Packwright +Description: PS5 package, dump and image tool + Manage a collection of PS5 dumps and images, read PS5 packages, and build or + convert exFAT, FFPKG, FFPFSC, ZArchive images and debug packages. + Starts from the menu, or from a terminal as "packwright" (try "packwright help"). +CONTROL +chmod 644 "$debroot/DEBIAN/control" +dpkg-deb --root-owner-group -Zxz --build "$debroot" "$out/$name.deb" + +# ------------------------------------------------------------------ .rpm +top="$work/rpm" +mkdir -p "$top"/{BUILD,RPMS,SOURCES,SPECS,SRPMS} +cat > "$top/SPECS/packwright.spec" < + + + + + + diff --git a/src/Packwright.App/App.axaml.cs b/src/Packwright.App/App.axaml.cs new file mode 100644 index 0000000..7af079f --- /dev/null +++ b/src/Packwright.App/App.axaml.cs @@ -0,0 +1,29 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using Packwright.App.Views; + +namespace Packwright.App; + +public partial class App : Application +{ + public override void Initialize() => AvaloniaXamlLoader.Load(this); + + public override void OnFrameworkInitializationCompleted() + { + // A failure in a timer or event handler is logged and the app keeps running. + Avalonia.Threading.Dispatcher.UIThread.UnhandledException += (_, e) => + { + Packwright.Infrastructure.Logger.Exception("UI thread", e.Exception); + e.Handled = true; + }; + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + var window = new MainWindow(desktop.Args ?? []); + desktop.MainWindow = window; + // Another launch of the app brings this window forward instead of opening a second copy. + Program.Current?.Listen(paths => Avalonia.Threading.Dispatcher.UIThread.Post(() => window.BringToFront(paths))); + } + base.OnFrameworkInitializationCompleted(); + } +} diff --git a/src/Packwright.App/Assets/icon.ico b/src/Packwright.App/Assets/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..7047bf18124997d2ca758baa1c997d542d04b32f GIT binary patch literal 52159 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
^| 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 literal 0 HcmV?d00001 diff --git a/src/Packwright.App/Assets/icon.png b/src/Packwright.App/Assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ad6dc13064c4960916061b0e05d20c677ed57e84 GIT binary patch literal 69339 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 literal 0 HcmV?d00001 diff --git a/src/Packwright.App/Cli.cs b/src/Packwright.App/Cli.cs new file mode 100644 index 0000000..21083ff --- /dev/null +++ b/src/Packwright.App/Cli.cs @@ -0,0 +1,502 @@ +using System.Runtime.InteropServices; +using System.Text; +using System.Text.Json; +using Packwright.App.Models; +using Packwright.App.Services; +using Packwright.App.Views; +using Packwright.Containers; +using Packwright.Core.Backends; +using Packwright.Core.Builders; +using Packwright.Core.Models; +using Packwright.Core.Parsers; +using Packwright.Core.Services; +using Packwright.Core.Tasks; +using Packwright.Infrastructure; + +namespace Packwright.App; + +/// +/// Command-line mode: Packwright info|scan|convert|extract|verify|health .... It runs the same jobs as the +/// window, without starting the interface, and never touches the network. +/// +internal static class Cli +{ + private const int Ok = 0, Failed = 1, Usage = 2, Cancelled = 130; + + private static readonly string[] Commands = ["info", "scan", "convert", "extract", "verify", "health", "send", "consoles", "version", "help"]; + + // Options that take a value; everything else starting with '-' is a switch. + 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" + }; + + public static bool IsCommand(string[] args) => + args.Length > 0 && (Commands.Contains(args[0], StringComparer.OrdinalIgnoreCase) || + args[0] is "--help" or "-h" or "/?" or "--version" or "-v" || + // A bare word that is not a file or folder is a mistyped command, not something to open. + (!args[0].StartsWith('-') && args[0].IndexOfAny(['/', '\\', '.', ':']) < 0 && + !File.Exists(args[0]) && !Directory.Exists(args[0]))); + + [DllImport("kernel32.dll")] + private static extern bool AttachConsole(int processId); + [DllImport("kernel32.dll")] + private static extern nint GetStdHandle(int handle); + [DllImport("kernel32.dll")] + private static extern bool SetStdHandle(int handle, nint value); + + private sealed class Options + { + public List Positional { get; } = []; + public Dictionary Values { get; } = new(StringComparer.OrdinalIgnoreCase); + public HashSet Switches { get; } = new(StringComparer.OrdinalIgnoreCase); + public string? Error { get; set; } + + public string? Get(params string[] names) => names.Select(name => Values.GetValueOrDefault(name)).FirstOrDefault(value => value is not null); + public bool Has(string name) => Switches.Contains(name); + public int? Int(string name) => Values.TryGetValue(name, out string? text) && int.TryParse(text, out int number) ? number : null; + } + + private static Options Parse(string[] args) + { + var options = new Options(); + for (int index = 0; index < args.Length; index++) + { + string arg = args[index]; + if (arg == "--") { options.Positional.AddRange(args.Skip(index + 1)); break; } + if (!arg.StartsWith('-') || arg.Length == 1) { options.Positional.Add(arg); continue; } + string name = arg.TrimStart('-'); + string? inline = null; + int equals = name.IndexOf('='); + if (equals > 0) { inline = name[(equals + 1)..]; name = name[..equals]; } + if (ValueOptions.Contains(name)) + { + if (inline is null && index + 1 < args.Length) inline = args[++index]; + if (inline is null) options.Error = $"The option --{name} needs a value."; + else options.Values[name] = inline; + } + else options.Switches.Add(name); + } + return options; + } + + public static int Run(string[] args) + { + if (OperatingSystem.IsWindows()) + { + // The window build has no console of its own: borrow the one it was started from. + // AttachConsole swaps the standard handles for the console's, which would drop '>' and '|' redirection: + // remember what was passed in and put it back. + nint output = GetStdHandle(-11), error = GetStdHandle(-12); + AttachConsole(-1); + if (output != 0 && output != -1) SetStdHandle(-11, output); + if (error != 0 && error != -1) SetStdHandle(-12, error); + Console.SetOut(new StreamWriter(Console.OpenStandardOutput(), new UTF8Encoding(false)) { AutoFlush = true }); + Console.SetError(new StreamWriter(Console.OpenStandardError(), new UTF8Encoding(false)) { AutoFlush = true }); + } + string command = args[0].ToLowerInvariant(); + Options options = Parse(args.Skip(1).ToArray()); + if (options.Error is not null) return Fail(options.Error, Usage); + if (options.Has("help") || options.Has("h")) return Help(command); + using var cancel = new CancellationTokenSource(); + Console.CancelKeyPress += (_, e) => { e.Cancel = true; cancel.Cancel(); }; + try + { + return command switch + { + "version" or "--version" or "-v" => Version(), + "help" or "--help" or "-h" or "/?" => Help(options.Positional.FirstOrDefault()), + "info" => Info(options), + "scan" => ScanAsync(options, cancel.Token).GetAwaiter().GetResult(), + "health" => HealthAsync(options, cancel.Token).GetAwaiter().GetResult(), + "send" => SendAsync(options, cancel.Token).GetAwaiter().GetResult(), + "consoles" => Consoles(), + "convert" or "extract" or "verify" => JobAsync(command, options, cancel.Token).GetAwaiter().GetResult(), + _ => Fail("Unknown command: " + command, Usage) + }; + } + catch (OperationCanceledException) + { + Console.Error.WriteLine("Cancelled."); + return Cancelled; + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidDataException or NotSupportedException + or ArgumentException or InvalidOperationException or JsonException) + { + Logger.Exception("CLI " + command, ex); + return Fail(ex.Message, Failed); + } + } + + private static int Fail(string message, int code) + { + Console.Error.WriteLine("Error: " + message); + if (code == Usage) Console.Error.WriteLine("Run 'Packwright help' for the commands."); + return code; + } + + private static int Version() + { + Console.WriteLine("Packwright " + UpdateService.CurrentVersion); + return Ok; + } + + private static int Help(string? topic) + { + Console.WriteLine($""" + Packwright {UpdateService.CurrentVersion} command line + + Usage: Packwright [options] + (On Windows use the packwright.cmd next to Packwright.exe so the prompt waits for the result.) + + Commands: + info Show the title, IDs, version and size of a dump folder, image or package. + scan ... List every title found. --json | --csv | --no-recursive | -o + health ... Check a library for problems. --md | --json | -o + convert --to Convert or build. is exfat, ffpkg, ffpfsc, zar or pkg. + extract Extract an image or package to a folder. -o + verify Check an image or package for damage. + send --to Send to a console on your network (FTP). --folder | --install (a .pkg: the + console downloads it from this PC) | --host --port instead of --to + consoles List the consoles saved in Settings. + version Print the version. + + convert options: + -o, --output Where to write it (default: next to the source). --overwrite replaces an existing file. + --preset Use a saved or built-in preset from the Tools page (its target and options). + --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 + extract/verify on a package: --passcode <32 characters> (default: the debug passcode) + + Exit codes: 0 done, 1 failed, 2 wrong usage, 130 cancelled. + Only local files are used; the command line never connects to the internet. + """); + return Ok; + } + + // ------------------------------------------------------------------ info / scan / health + + private static string Describe(Ps5GameInfo game, string path) + { + var item = new LibraryItem(game); + var text = new StringBuilder(); + void Line(string label, string value) { if (value.Length > 0) text.AppendLine($"{label,-12}{value}"); } + Line("Title", item.Title); + Line("Title ID", item.TitleId); + Line("Content ID", item.ContentId); + Line("Category", item.Category); + Line("Version", item.Version); + Line("Firmware", item.Firmware); + Line("Region", item.Region); + Line("Format", item.Format); + Line("Size", item.SizeText); + Line("DRM", game.DrmType); + Line("Path", path); + if (item.HasMissing) text.AppendLine($"{"Missing",-12}{string.Join(", ", item.MissingFields)}"); + return text.ToString(); + } + + private static int Info(Options options) + { + if (options.Positional.Count != 1) return Fail("info needs one path.", Usage); + string path = Path.GetFullPath(options.Positional[0]); + Ps5GameInfo? game = SourceInfo.Read(path); + if (game is null) return Fail("That is not a PS5 dump folder, image or package: " + path, Failed); + Console.Write(Describe(game, path)); + return Ok; + } + + private static async Task> ScanFoldersAsync(Options options, CancellationToken token) + { + if (options.Positional.Count == 0) throw new ArgumentException("Give at least one folder."); + var folders = options.Positional.Select(Path.GetFullPath).ToList(); + foreach (string folder in folders.Where(folder => !Directory.Exists(folder))) + throw new ArgumentException("Folder not found: " + folder); + var progress = new Progress(value => + { + if (!Console.IsErrorRedirected && value.Total > 0) Console.Error.Write($"\rScanning {value.Processed:N0} of {value.Total:N0} "); + }); + Ps5ScanResult result = await new Ps5LibraryScanner().ScanAsync(folders, !options.Has("no-recursive"), null, progress, token); + if (!Console.IsErrorRedirected) Console.Error.Write("\r" + new string(' ', 40) + "\r"); + foreach (string error in result.Errors) Console.Error.WriteLine("Warning: " + error); + return result.Games.Select(game => new LibraryItem(game)).OrderBy(item => item.Title, StringComparer.CurrentCultureIgnoreCase).ToList(); + } + + private static async Task WriteOutputAsync(Options options, string text) + { + if (options.Get("o", "output") is { } file) + { + await File.WriteAllTextAsync(Path.GetFullPath(file), text, new UTF8Encoding(false)); + Console.Error.WriteLine("Wrote " + Path.GetFullPath(file)); + } + else Console.Write(text); + return Ok; + } + + private static async Task ScanAsync(Options options, CancellationToken token) + { + List items; + try { items = await ScanFoldersAsync(options, token); } + catch (ArgumentException ex) { return Fail(ex.Message, Usage); } + if (options.Has("json")) return await WriteOutputAsync(options, LibraryExport.ToJson(items, includePaths: true) + Environment.NewLine); + if (options.Has("csv")) return await WriteOutputAsync(options, LibraryActions.ToCsv(items)); + var text = new StringBuilder(); + int width = Math.Clamp(items.Count == 0 ? 10 : items.Max(item => item.Title.Length), 10, 48); + text.AppendLine($"{"Title".PadRight(width)} {"Title ID",-10} {"Type",-6} {"Version",-8} {"Size",10} Format"); + foreach (LibraryItem item in items) + text.AppendLine($"{Trim(item.Title, width).PadRight(width)} {item.TitleId,-10} {item.Category,-6} {item.Version,-8} {item.SizeText,10} {item.Format}"); + text.AppendLine().AppendLine($"{items.Count:N0} title(s), {Ps5LibraryHealth.FormatBytes(items.Sum(item => item.SizeBytes))}."); + return await WriteOutputAsync(options, text.ToString()); + } + + private static string Trim(string text, int width) => text.Length <= width ? text : text[..(width - 1)] + "…"; + + private static async Task HealthAsync(Options options, CancellationToken token) + { + List items; + try { items = await ScanFoldersAsync(options, token); } + catch (ArgumentException ex) { return Fail(ex.Message, Usage); } + HealthReport report = Ps5LibraryHealth.Analyze(items.Select(HealthView.ToHealthItem).ToList(), []); + Ps5LibraryHealth.Sort(report); + string text = options.Has("json") + ? JsonSerializer.Serialize(report, new JsonSerializerOptions { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }) + Environment.NewLine + : Ps5LibraryHealth.ToMarkdown(report, issue => issue.ItemPath); + await WriteOutputAsync(options, text); + // A non-zero code when something is actually wrong makes this usable in scripts. + return report.Count(HealthSeverity.Problem) > 0 ? Failed : Ok; + } + + // ------------------------------------------------------------------ send to console + + private static int Consoles() + { + List consoles = new AppStateStore().LoadSettings().Consoles; + if (consoles.Count == 0) { Console.WriteLine("No consoles are saved yet. Add one in Settings, or use --host."); return Ok; } + foreach (ConsoleProfile console in consoles) + Console.WriteLine($"{console.Name,-24}{console.Host}:{console.FtpPort} folder {console.DefaultFolder}"); + return Ok; + } + + private static async Task SendAsync(Options options, CancellationToken token) + { + if (options.Positional.Count != 1) return Fail("send needs one file or folder.", Usage); + string source = Path.GetFullPath(options.Positional[0]); + if (!File.Exists(source) && !Directory.Exists(source)) return Fail("Not found: " + source, Failed); + ConsoleProfile? profile; + if (options.Get("to") is { } name) + { + profile = new AppStateStore().LoadSettings().Consoles.FirstOrDefault(console => console.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); + if (profile is null) return Fail($"No console named '{name}'. Run 'Packwright consoles' to see the saved ones.", Usage); + } + else if (options.Get("host") is { } host) + profile = new ConsoleProfile { Name = host, Host = host, FtpPort = options.Int("port") ?? 1337 }; + else + return Fail("send needs --to or --host
.", Usage); + var progress = new ConsoleProgress(); + Console.Error.WriteLine($"send: {source}"); + if (options.Has("install")) + { + Console.Error.WriteLine($" -> {profile.Name}: install by URL"); + await ConsoleSender.InstallByUrlAsync(profile, source, progress, token); + } + else + { + string folder = options.Get("folder") ?? profile.DefaultFolder; + Console.Error.WriteLine($" -> {profile.Name}: {ConsoleSender.RemotePathFor(folder, source)}"); + await ConsoleSender.UploadAsync(profile, source, folder, progress, token); + } + progress.Finish(); + Console.Error.WriteLine("Done."); + return Ok; + } + + // ------------------------------------------------------------------ convert / extract / verify + + private static async Task JobAsync(string command, Options options, CancellationToken token) + { + if (options.Positional.Count != 1) return Fail($"{command} needs exactly one source path.", Usage); + string source = Path.GetFullPath(options.Positional[0]); + bool isDirectory = Directory.Exists(source); + if (!isDirectory && !File.Exists(source)) return Fail("Not found: " + source, Failed); + bool isPackage = !isDirectory && source.EndsWith(".pkg", StringComparison.OrdinalIgnoreCase); + Ps5ImageFormat format = isDirectory || isPackage ? Ps5ImageFormat.Unknown : Ps5ImageFormatProbe.Detect(source); + if (!isDirectory && !isPackage && format == Ps5ImageFormat.Unknown) + return Fail("That file is not a PS5 image or package: " + source, Failed); + + var spec = new JobSpec + { + Source = source, + Format = format.ToString(), + FromPackage = isPackage, + Overwrite = options.Has("overwrite"), + Passcode = options.Get("passcode") ?? string.Empty + }; + if (spec.Passcode.Length > 0 && spec.Passcode.Length != 32) return Fail("The passcode must be 32 characters.", Usage); + + switch (command) + { + case "verify": + spec.Kind = JobSpec.Verify; + break; + case "extract": + spec.Kind = JobSpec.Extract; + spec.Output = Path.GetFullPath(options.Get("o", "output") ?? SiblingName(source, isDirectory, "-files")); + if (!spec.Overwrite) spec.Output = Jobs.FindAvailablePath(spec.Output); + break; + default: + if (BuildConvertSpec(spec, options, isDirectory, isPackage, format) is { } problem) return Fail(problem, Usage); + break; + } + + Func, CancellationToken, Task>? run = spec.Executor(); + if (run is null) return Fail("That job cannot run with these arguments.", Usage); + var printer = new ConsoleProgress(); + Console.Error.WriteLine($"{command}: {source}"); + if (spec.Output.Length > 0) Console.Error.WriteLine(" -> " + spec.Output); + await run(printer, token); + printer.Finish(); + Console.Error.WriteLine(command == "verify" ? "Verified: no problems found." : "Done."); + return Ok; + } + + private static string SiblingName(string source, bool isDirectory, string suffix) + { + string trimmed = source.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + string name = isDirectory ? Path.GetFileName(trimmed) : Path.GetFileNameWithoutExtension(trimmed); + string parent = (isDirectory ? Path.GetDirectoryName(trimmed) : Path.GetDirectoryName(source)) ?? "."; + return Path.Combine(parent, name + suffix); + } + + /// Fills a convert or build job from the options; returns a message when they do not make sense. + private static string? BuildConvertSpec(JobSpec spec, Options options, bool isDirectory, bool isPackage, Ps5ImageFormat format) + { + ToolPreset? preset = null; + if (options.Get("preset") is { } presetName) + { + AppSettings settings = new AppStateStore().LoadSettings(); + preset = ToolsView.BuiltInPresets.Concat(settings.Presets) + .FirstOrDefault(item => item.Name.Equals(presetName, StringComparison.OrdinalIgnoreCase)); + if (preset is null) + return $"No preset named '{presetName}'. Built-in: {string.Join(", ", ToolsView.BuiltInPresets.Select(item => "\"" + item.Name + "\""))}."; + } + string? to = options.Get("to", "format") ?? preset?.Target; + if (string.IsNullOrWhiteSpace(to)) return "convert needs --to exfat|ffpkg|ffpfsc|zar|pkg (or a preset that has a target)."; + string key = to.Trim().ToLowerInvariant(); + (string Name, string Extension)? target = key switch + { + "exfat" => ("Exfat", ".exfat"), + "ffpkg" => ("Ffpkg", ".ffpkg"), + "ffpfsc" => ("Ffpfsc", ".ffpfsc"), + "zar" or "zarchive" => ("ZArchive", ".zar"), + "pkg" or "fpkg" => ("Fpkg", ".pkg"), + _ => null + }; + if (target is null) return $"Unknown target '{to}'. Use exfat, ffpkg, ffpfsc, zar or pkg."; + if (target.Value.Name != "Fpkg") + { + if (!isDirectory && !isPackage && !Enum.TryParse(target.Value.Name, out Ps5ImageConversionTarget parsed)) + return "Unsupported target."; + if (!isDirectory && !isPackage && Enum.TryParse(target.Value.Name, out Ps5ImageConversionTarget check) && + !Ps5ImageConversionService.IsSupported(format, check)) + return $"A {format} image cannot be converted to {target.Value.Name}."; + if (isPackage && target.Value.Name == "Fpkg") return "A package cannot be converted to a package."; + } + + spec.Output = Path.GetFullPath(options.Get("o", "output") ?? SiblingName(spec.Source, isDirectory, target.Value.Extension)); + if (!spec.Overwrite && (File.Exists(spec.Output) || Directory.Exists(spec.Output))) + return $"The output already exists: {spec.Output}. Use --overwrite or -o ."; + + // Defaults come from the preset (if any); explicit options win. + ToolPreset basis = preset ?? new ToolPreset(); + int Pick(string name, int fallback) => options.Int(name) ?? fallback; + string? Choice(string name) => options.Get(name)?.Trim().ToLowerInvariant(); + + spec.ClusterSize = Choice("cluster") switch { "32" => 32768, "64" => 65536, null => basis.ClusterIndex switch { 1 => 32768, 2 => 65536, _ => 0 }, _ => 0 }; + spec.GenerateAmpr = !options.Has("no-ampr") && basis.GenerateAmpr; + spec.CompressionLevel = Math.Clamp(Pick("level", basis.Level), 1, 9); + spec.MinimumGain = Math.Clamp(Pick("gain", basis.Gain), 0, 100); + spec.BlockSize = (Choice("block") ?? (basis.BlockIndex == 1 ? "64" : "32")) == "64" ? 65536 : 32768; + spec.FragmentSize = (Choice("fragment") ?? (basis.FragmentIndex == 1 ? "64" : "4")) == "64" ? 65536 : 4096; + if (spec.FragmentSize > spec.BlockSize) spec.FragmentSize = spec.BlockSize; + spec.BytesPerInode = (Choice("density") ?? basis.DensityIndex switch { 1 => "512", 2 => "1024", _ => "256" }) switch + { "512" => 524288, "1024" => 1048576, _ => 262144 }; + spec.MinFreePercent = Math.Clamp(Pick("min-free", basis.MinFree), 0, 50); + + if (target.Value.Name == "Fpkg") + { + 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."; + spec.Kind = JobSpec.BuildPackage; + spec.ContentId = contentId; + spec.Backend = (options.Get("builder") ?? (basis.Backend.Length > 0 ? basis.Backend : AppSettingsDefaultBackend())).Trim(); + spec.Compression = (Choice("compression") switch + { + "kraken" => Ps5InnerCompression.Kraken, + "stored" or "none" => Ps5InnerCompression.Stored, + "auto" => Ps5InnerCompression.Auto, + _ => basis.CompressionIndex switch { 1 => Ps5InnerCompression.Kraken, 2 => Ps5InnerCompression.Stored, _ => Ps5InnerCompression.Auto } + }).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.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; + spec.Target = target.Value.Name; + 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); + return index >= 0 ? Ps5SdkVersions.ExecutableVersionAt(index) : null; + } + + /// Prints the current step and a percentage to the error stream, a line at a time when it is redirected. + private sealed class ConsoleProgress : IProgress + { + private string _stage = string.Empty; + private int _lastPercent = -1; + private bool _lineOpen; + + public void Report(PackageTaskProgress value) + { + int percent = value.StagePercent >= 0 ? (int)value.StagePercent + : value.TotalBytes > 0 ? (int)(value.CurrentBytes * 100 / value.TotalBytes) : -1; + bool newStage = value.Stage.Length > 0 && value.Stage != _stage; + if (newStage) + { + Finish(); + _stage = value.Stage; + _lastPercent = -1; + Console.Error.Write(_stage); + _lineOpen = true; + if (Console.IsErrorRedirected) Console.Error.WriteLine(); + } + if (percent < 0 || percent == _lastPercent) return; + if (Console.IsErrorRedirected ? percent % 10 != 0 : false) return; + _lastPercent = percent; + if (Console.IsErrorRedirected) Console.Error.WriteLine($" {percent}%"); + else Console.Error.Write($"\r{_stage} {percent}% "); + } + + public void Finish() + { + if (_lineOpen && !Console.IsErrorRedirected) Console.Error.WriteLine(); + _lineOpen = false; + } + } +} diff --git a/src/Packwright.App/Infrastructure/AppSettingsNormalizer.cs b/src/Packwright.App/Infrastructure/AppSettingsNormalizer.cs new file mode 100644 index 0000000..2a06bdf --- /dev/null +++ b/src/Packwright.App/Infrastructure/AppSettingsNormalizer.cs @@ -0,0 +1,191 @@ +namespace Packwright.Infrastructure; + +/// +/// The single normalization/validation contract for settings loaded from disk, imported from a file or +/// edited in the dialog. It never throws and never leaves null collections or out-of-range numbers, so +/// every consumer can assume a consistent shape regardless of where the settings came from. +/// +public static class AppSettingsNormalizer +{ + public const int MinRowHeight = 16; + public const int MaxRowHeight = 60; + public const int MinPreviewMb = 1; + public const int MaxPreviewMb = 1024; + public const int MinHexPageKb = 1; + public const int MaxHexPageKb = 256; + public const int MinThumbnailCache = 1; + public const int MaxThumbnailCache = 100000; + + /// Grouping keys the library understands; anything else normalizes to "None". + public static readonly string[] GroupKeys = ["", "family", "titleid", "category", "region", "source", "firmware"]; + + public const string DefaultRenameFormat = "{TITLE} [{TITLE_ID}]"; + public const string DefaultTheme = "Default (Charcoal)"; + + public static AppSettings Normalize(AppSettings? settings) + { + settings ??= new AppSettings(); + + settings.LibraryFolders = CleanPaths(settings.LibraryFolders); + settings.RecentFolders = CleanList(settings.RecentFolders, 10); + settings.ManualSources = CleanPaths(settings.ManualSources); + + settings.LibraryColumnOrder = CleanList(settings.LibraryColumnOrder); + settings.LibraryHiddenColumns = CleanList(settings.LibraryHiddenColumns); + settings.LibraryColumnWeights = CleanWeights(settings.LibraryColumnWeights); + settings.LibrarySortKeys = CleanSortKeys(settings.LibrarySortKeys); + settings.SavedViews = CleanViews(settings.SavedViews); + settings.Presets = CleanPresets(settings.Presets); + settings.Consoles = CleanConsoles(settings.Consoles); + settings.LibrarySortColumn = string.IsNullOrWhiteSpace(settings.LibrarySortColumn) + ? "Title" + : settings.LibrarySortColumn.Trim(); + + settings.Theme = string.IsNullOrWhiteSpace(settings.Theme) ? DefaultTheme : settings.Theme.Trim(); + settings.GridRowHeight = Math.Clamp(settings.GridRowHeight, MinRowHeight, MaxRowHeight); + settings.DefaultGroupBy = Array.IndexOf(GroupKeys, settings.DefaultGroupBy) >= 0 ? settings.DefaultGroupBy : string.Empty; + + settings.RenameFormat = string.IsNullOrWhiteSpace(settings.RenameFormat) ? DefaultRenameFormat : settings.RenameFormat; + + settings.MaxPreviewMb = Math.Clamp(settings.MaxPreviewMb, MinPreviewMb, MaxPreviewMb); + settings.HexPageKb = Math.Clamp(settings.HexPageKb, MinHexPageKb, MaxHexPageKb); + settings.ThumbnailCacheCount = Math.Clamp(settings.ThumbnailCacheCount, MinThumbnailCache, MaxThumbnailCache); + + settings.OutputDirectory = (settings.OutputDirectory ?? string.Empty).Trim(); + + // A passcode is either blank (meaning the built-in all-zero default) or exactly 32 printable + // ASCII characters. Anything else is discarded rather than carried into a build. + settings.DebugPasscode ??= string.Empty; + if (settings.DebugPasscode.Length > 0 && !IsValidPasscode(settings.DebugPasscode)) + settings.DebugPasscode = string.Empty; + + settings.BuildBackend = (settings.BuildBackend ?? string.Empty).Trim(); + + settings.WindowWidth = ClampNonNegative(settings.WindowWidth); + settings.WindowHeight = ClampNonNegative(settings.WindowHeight); + settings.TaskSplitterDistance = ClampNonNegative(settings.TaskSplitterDistance); + + return settings; + } + + public static bool IsValidPasscode(string value) => + value.Length == 32 && value.All(character => character is >= (char)0x20 and <= (char)0x7E); + + private static int ClampNonNegative(int value) => Math.Clamp(value, 0, 32767); + + private static List CleanList(List? values, int max = int.MaxValue) + { + var result = new List(); + if (values is null) return result; + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (string? value in values) + { + if (string.IsNullOrWhiteSpace(value)) continue; + if (!seen.Add(value.Trim())) continue; + result.Add(value.Trim()); + if (result.Count >= max) break; + } + return result; + } + + private static List CleanPaths(List? values) + { + var result = new List(); + if (values is null) return result; + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (string? value in values) + { + if (string.IsNullOrWhiteSpace(value)) continue; + string path = NormalizePath(value); + if (!seen.Add(path)) continue; + result.Add(path); + } + return result; + } + + /// Returns a full path when possible, otherwise the trimmed original string. + public static string NormalizePath(string value) + { + string trimmed = value.Trim(); + try + { + return Path.GetFullPath(trimmed); + } + catch (Exception ex) when (ex is ArgumentException or NotSupportedException or PathTooLongException) + { + return trimmed; + } + } + + private static Dictionary CleanWeights(Dictionary? weights) + { + var result = new Dictionary(StringComparer.Ordinal); + if (weights is null) return result; + foreach ((string key, float value) in weights) + if (!string.IsNullOrWhiteSpace(key) && value > 0 && float.IsFinite(value)) + result[key] = value; + return result; + } + + private static List CleanSortKeys(List? values) => CleanList(values); + + private static List CleanConsoles(List? consoles) + { + var result = new List(); + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (ConsoleProfile? console in consoles ?? []) + { + if (console is null || string.IsNullOrWhiteSpace(console.Name) || !seen.Add(console.Name.Trim())) continue; + console.Name = console.Name.Trim(); + console.Host = (console.Host ?? string.Empty).Trim(); + console.FtpPort = console.FtpPort is > 0 and <= 65535 ? console.FtpPort : 1337; + console.DefaultFolder = string.IsNullOrWhiteSpace(console.DefaultFolder) ? "/data" : console.DefaultFolder.Trim(); + console.InstallUrl ??= string.Empty; + console.InstallBody ??= string.Empty; + console.InstallContentType = string.IsNullOrWhiteSpace(console.InstallContentType) ? "application/json" : console.InstallContentType.Trim(); + result.Add(console); + } + return result; + } + + private static List CleanPresets(List? presets) + { + var result = new List(); + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (ToolPreset? preset in presets ?? []) + { + if (preset is null || string.IsNullOrWhiteSpace(preset.Name) || !seen.Add(preset.Name.Trim())) continue; + preset.Name = preset.Name.Trim(); + preset.Target ??= string.Empty; + preset.Backend ??= string.Empty; + preset.Sdk ??= string.Empty; + preset.TempDirectory ??= string.Empty; + result.Add(preset); + } + return result; + } + + private static List CleanViews(List? views) + { + var result = new List(); + if (views is null) return result; + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (SavedLibraryView? view in views) + { + if (view is null || string.IsNullOrWhiteSpace(view.Name)) continue; + string name = view.Name.Trim(); + if (!seen.Add(name)) continue; + view.Name = name; + view.Query ??= string.Empty; + view.GroupBy = Array.IndexOf(GroupKeys, view.GroupBy) >= 0 ? view.GroupBy : string.Empty; + view.Categories = CleanList(view.Categories); + view.Regions = CleanList(view.Regions); + view.Formats = CleanList(view.Formats); + view.SortKeys = CleanList(view.SortKeys); + view.HiddenColumns = CleanList(view.HiddenColumns); + view.ColumnOrder = CleanList(view.ColumnOrder); + result.Add(view); + } + return result; + } +} diff --git a/src/Packwright.App/Infrastructure/AppStateStore.cs b/src/Packwright.App/Infrastructure/AppStateStore.cs new file mode 100644 index 0000000..c0a11dd --- /dev/null +++ b/src/Packwright.App/Infrastructure/AppStateStore.cs @@ -0,0 +1,247 @@ +using System.Text.Json; +using Packwright.Core.Models; + +namespace Packwright.Infrastructure; + +/// A named snapshot of the library layout: filters, grouping, sorting and columns. +public sealed class SavedLibraryView +{ + public string Name { get; set; } = string.Empty; + public string Query { get; set; } = string.Empty; + public List Categories { get; set; } = []; + public List Regions { get; set; } = []; + public List Formats { get; set; } = []; + public string GroupBy { get; set; } = string.Empty; + public List SortKeys { get; set; } = []; + public List HiddenColumns { get; set; } = []; + public List ColumnOrder { get; set; } = []; +} + +/// A named set of Image tools options (target format and its settings). The source and output paths are never part of it. +public sealed class ToolPreset +{ + public string Name { get; set; } = string.Empty; + /// Exfat, Ffpkg, Ffpfsc, ZArchive or Fpkg; empty keeps whatever is chosen. + public string Target { get; set; } = string.Empty; + public int ClusterIndex { get; set; } + public bool GenerateAmpr { get; set; } = true; + public int Level { get; set; } = 7; + public int Gain { get; set; } = 1; + public int BlockIndex { get; set; } + public int FragmentIndex { get; set; } + public int DensityIndex { get; set; } + public int MinFree { get; set; } + public string Backend { get; set; } = string.Empty; + public int CompressionIndex { get; set; } + 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 int DrmIndex { get; set; } + public string Sdk { get; set; } = string.Empty; + public string TempDirectory { get; set; } = string.Empty; +} + +/// A console on the local network that files can be sent to. Nothing is contacted until the user sends something. +public sealed class ConsoleProfile +{ + public string Name { get; set; } = string.Empty; + /// The console's address on the local network, such as 192.168.1.50. + public string Host { get; set; } = string.Empty; + public int FtpPort { get; set; } = 1337; + /// Where files go on the console unless another folder is chosen when sending. + public string DefaultFolder { get; set; } = "/data"; + + // "Install by URL": the console's installer is asked to download a package from this PC. Different installers + // use different requests, so the request is a template. {host} is the console, {url} the address of the file. + public string InstallUrl { get; set; } = "http://{host}:12800/api/install"; + public string InstallBody { get; set; } = "{\"type\":\"direct\",\"packages\":[\"{url}\"]}"; + public string InstallContentType { get; set; } = "application/json"; +} + +public sealed class AppSettings +{ + public List LibraryFolders { get; set; } = []; + public bool RecursiveScan { get; set; } = true; + public List RecentFolders { get; set; } = []; + public List ManualSources { get; set; } = []; + public int WindowWidth { get; set; } + public int WindowHeight { get; set; } + public bool WindowMaximized { get; set; } + public bool WindowHasPosition { get; set; } + public int WindowX { get; set; } + public int WindowY { get; set; } + public string LibrarySortColumn { get; set; } = "Title"; + public bool LibrarySortAscending { get; set; } = true; + /// Ordered sort keys ("Title:asc", "Size:desc") for multi-column sorting. + public List LibrarySortKeys { get; set; } = []; + /// Named saved library views (filters, grouping, sorting and column layout). + public List SavedViews { get; set; } = []; + /// Named Image tools presets saved by the user. + public List Presets { get; set; } = []; + /// Consoles that files can be sent to. + public List Consoles { get; set; } = []; + public List LibraryColumnOrder { get; set; } = []; + public List LibraryHiddenColumns { get; set; } = []; + /// Remembered column fill weights by column name so user-resized widths survive a restart. + public Dictionary LibraryColumnWeights { get; set; } = []; + + // Appearance + public string Theme { get; set; } = "Default (Charcoal)"; + /// Library presentation: "Cards" (artwork tiles) or "List". + public string LibraryLayout { get; set; } = "Cards"; + public int GridRowHeight { get; set; } = 22; + public bool ShowThumbnails { get; set; } = true; + public bool ShowGridLines { get; set; } = true; + /// Show the inline file preview pane in the Files tab (list-only when disabled). + public bool ShowFilePreview { get; set; } = true; + public string DefaultGroupBy { get; set; } = string.Empty; + + // Library & scanning + public bool RefreshOnStartup { get; set; } + public string RenameFormat { get; set; } = "{TITLE} [{TITLE_ID}]"; + public bool LogAutoScroll { get; set; } = true; + + // Files & preview + public int MaxPreviewMb { get; set; } = 16; + public int HexPageKb { get; set; } = 16; + + // Performance + public int ThumbnailCacheCount { get; set; } = 512; + + // Paths & outputs + public string OutputDirectory { get; set; } = string.Empty; + public bool OpenOutputAfterTask { get; set; } + + // Tasks workspace + /// Remembered vertical split between the task list and the details panel. + public int TaskSplitterDistance { get; set; } + /// True when the details panel is collapsed to give the list more height. + public bool TaskDetailsCollapsed { get; set; } + + // Build defaults + public string DebugPasscode { get; set; } = string.Empty; + + /// Selected package backend id ("lpp" or "ppt"). Empty/unknown falls back to the default. + public string BuildBackend { get; set; } = string.Empty; + + // Updates (nothing is checked until the user allows it) + public bool CheckForUpdates { get; set; } + public bool UpdateCheckAsked { get; set; } + public DateTime LastUpdateCheckUtc { get; set; } + public string SkippedUpdateVersion { get; set; } = string.Empty; + + // Safety + public bool ConfirmDelete { get; set; } = true; + public bool ConfirmMove { get; set; } = true; + public bool PermanentDelete { get; set; } +} + +public sealed class LibraryManifest +{ + public DateTime CreatedUtc { get; set; } + public List Games { get; set; } = []; +} + +/// Result of loading settings, including a non-fatal warning worth surfacing to the user. +public sealed record SettingsLoadResult(AppSettings Settings, string? Warning); + +public sealed class AppStateStore +{ + private static readonly JsonSerializerOptions JsonOptions = new() + { + WriteIndented = true, + PropertyNameCaseInsensitive = true + }; + + public AppStateStore() + { + // PACKWRIGHT_DATA lets a test copy use its own folder instead of the real settings and library. + string? custom = Environment.GetEnvironmentVariable("PACKWRIGHT_DATA"); + AppDataDirectory = !string.IsNullOrWhiteSpace(custom) + ? Path.GetFullPath(custom) + : Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Packwright"); + Directory.CreateDirectory(AppDataDirectory); + } + + public string AppDataDirectory { get; } + public string SettingsPath => Path.Combine(AppDataDirectory, "settings.json"); + public string ManifestPath => Path.Combine(AppDataDirectory, "manifest.json"); + + public AppSettings LoadSettings() => LoadSettingsWithDiagnostics().Settings; + + /// + /// Loads and normalizes settings. A missing file simply yields defaults; an unreadable file is + /// quarantined (renamed) rather than silently overwritten by a later automatic save, and a warning + /// describing what happened is returned. + /// + public SettingsLoadResult LoadSettingsWithDiagnostics() + { + if (!File.Exists(SettingsPath)) return new SettingsLoadResult(new AppSettings(), null); + try + { + AppSettings settings = JsonSerializer.Deserialize(File.ReadAllText(SettingsPath), JsonOptions) + ?? new AppSettings(); + return new SettingsLoadResult(AppSettingsNormalizer.Normalize(settings), null); + } + catch (JsonException ex) + { + string quarantine = SettingsPath + ".invalid-" + DateTime.Now.ToString("yyyyMMddHHmmss"); + try { File.Move(SettingsPath, quarantine, overwrite: true); } + catch (Exception moveEx) when (moveEx is IOException or UnauthorizedAccessException) { } + return new SettingsLoadResult(new AppSettings(), + $"The settings file could not be read and was moved to {Path.GetFileName(quarantine)}. " + + $"Defaults are in use. ({ex.Message})"); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + return new SettingsLoadResult(new AppSettings(), + $"Settings could not be read: {ex.Message}. Defaults are in use for this session."); + } + } + + public LibraryManifest LoadManifest() + { + if (!File.Exists(ManifestPath)) return new LibraryManifest(); + try + { + LibraryManifest manifest = JsonSerializer.Deserialize( + File.ReadAllText(ManifestPath), JsonOptions) ?? new LibraryManifest(); + manifest.Games ??= []; + return manifest; + } + catch (JsonException ex) + { + // Quarantine rather than silently presenting an empty library (which looks like data + // loss) and then overwriting the file on the next save. + string quarantine = ManifestPath + ".invalid-" + DateTime.Now.ToString("yyyyMMddHHmmss"); + try { File.Move(ManifestPath, quarantine, overwrite: true); } + catch (Exception moveEx) when (moveEx is IOException or UnauthorizedAccessException) { } + Logger.Warn($"The library manifest could not be read and was moved to " + + $"{Path.GetFileName(quarantine)}. The library will be rebuilt on the next refresh. ({ex.Message})"); + return new LibraryManifest(); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + Logger.Warn($"The library manifest could not be read: {ex.Message}. " + + "The library may appear empty until a refresh."); + return new LibraryManifest(); + } + } + + public void SaveSettings(AppSettings settings) => Save(SettingsPath, AppSettingsNormalizer.Normalize(settings)); + public void SaveManifest(IReadOnlyCollection games) => Save(ManifestPath, new LibraryManifest + { + CreatedUtc = DateTime.UtcNow, + Games = games.ToList() + }); + + private static void Save(string path, T value) + { + string tempPath = path + ".tmp"; + File.WriteAllText(tempPath, JsonSerializer.Serialize(value, JsonOptions)); + File.Move(tempPath, path, true); + } +} diff --git a/src/Packwright.App/Infrastructure/Logger.cs b/src/Packwright.App/Infrastructure/Logger.cs new file mode 100644 index 0000000..67b100e --- /dev/null +++ b/src/Packwright.App/Infrastructure/Logger.cs @@ -0,0 +1,97 @@ +using System.Text; + +namespace Packwright.Infrastructure; + +public enum LogLevel +{ + Info, + Warn, + Error +} + +/// One structured log record, raised through . +public readonly record struct LogEntry(DateTime Time, LogLevel Level, string Message); + +/// +/// Minimal thread-safe file logger. Writes to %LocalAppData%\Packwright\logs, rotates once at +/// a soft size cap, and never throws because logging must not break the application. The file is +/// the full record of every log; feeds the in-app Log tab. +/// +public static class Logger +{ + private static readonly object Gate = new(); + private const long MaximumBytes = 5 * 1024 * 1024; + + public static string LogDirectory { get; } = Path.Combine( + Environment.GetEnvironmentVariable("PACKWRIGHT_DATA") is { Length: > 0 } custom + ? Path.GetFullPath(custom) + : Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Packwright"), + "logs"); + + public static string LogPath { get; } = Path.Combine(LogDirectory, "Packwright.log"); + + public static event Action? Logged; + + public static void Info(string message) => Write(LogLevel.Info, message); + public static void Warn(string message) => Write(LogLevel.Warn, message); + public static void Error(string message) => Write(LogLevel.Error, message); + public static void Exception(string context, Exception exception) => Write(LogLevel.Error, $"{context}: {exception}"); + + private static void Write(LogLevel level, string message) + { + var entry = new LogEntry(DateTime.Now, level, message); + string line = $"{entry.Time:yyyy-MM-dd HH:mm:ss.fff} [{LevelText(level)}] {message}"; + try + { + lock (Gate) + { + System.IO.Directory.CreateDirectory(LogDirectory); + if (File.Exists(LogPath) && new FileInfo(LogPath).Length > MaximumBytes) + File.Move(LogPath, LogPath + ".1", true); + // Shared write access, so a second running copy (or a viewer) never blocks logging. + using var stream = new FileStream(LogPath, FileMode.Append, FileAccess.Write, + FileShare.ReadWrite | FileShare.Delete); + byte[] bytes = Encoding.UTF8.GetBytes(line + Environment.NewLine); + stream.Write(bytes, 0, bytes.Length); + } + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + // Logging is best-effort only. + } + Logged?.Invoke(entry); + } + + /// Formats an entry exactly as it is written to the log file. + public static string Format(LogEntry entry) => + $"{entry.Time:yyyy-MM-dd HH:mm:ss.fff} [{LevelText(entry.Level)}] {entry.Message}"; + + /// The last lines of the log file, oldest first, for showing history in the app. + public static IReadOnlyList ReadTail(int maximumLines) + { + try + { + if (!File.Exists(LogPath)) return []; + using var stream = new FileStream(LogPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); + // Read only the end of the file; 64 KiB per 200 lines is ample for typical entries. + long window = Math.Min(stream.Length, Math.Max(64 * 1024, maximumLines * 320L)); + stream.Position = stream.Length - window; + using var reader = new StreamReader(stream, Encoding.UTF8); + string text = reader.ReadToEnd(); + List lines = text.Split('\n').Select(line => line.TrimEnd('\r')).Where(line => line.Length > 0).ToList(); + if (window < stream.Length && lines.Count > 0) lines.RemoveAt(0); // the first line is usually cut off + return lines.Count <= maximumLines ? lines : lines.GetRange(lines.Count - maximumLines, maximumLines); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + return []; + } + } + + private static string LevelText(LogLevel level) => level switch + { + LogLevel.Warn => "WARN", + LogLevel.Error => "ERROR", + _ => "INFO" + }; +} diff --git a/src/Packwright.App/Infrastructure/ShellIntegration.cs b/src/Packwright.App/Infrastructure/ShellIntegration.cs new file mode 100644 index 0000000..34c1e53 --- /dev/null +++ b/src/Packwright.App/Infrastructure/ShellIntegration.cs @@ -0,0 +1,56 @@ +using System.Runtime.Versioning; +using Microsoft.Win32; + +namespace Packwright.Infrastructure; + +/// +/// Per-user (HKCU, no admin) File Explorer context menu integration for PS5 package and image files +/// and for folders. Adds an "Open with Packwright" verb that launches the app with +/// --open "<path>". Windows only. +/// +[SupportedOSPlatform("windows")] +public static class ShellIntegration +{ + private static readonly string[] FileKeys = + [ + @"Software\Classes\SystemFileAssociations\.pkg\shell\Packwright", + @"Software\Classes\SystemFileAssociations\.ffpfsc\shell\Packwright", + @"Software\Classes\SystemFileAssociations\.ffpkg\shell\Packwright", + @"Software\Classes\SystemFileAssociations\.exfat\shell\Packwright", + @"Software\Classes\Directory\shell\Packwright" + ]; + + private static string ExecutablePath => + Environment.ProcessPath ?? throw new InvalidOperationException("The application path is unknown."); + + public static bool IsInstalled() + { + foreach (string path in FileKeys) + { + using RegistryKey? key = Registry.CurrentUser.OpenSubKey(path); + if (key is not null) return true; + } + return false; + } + + public static void Install() + { + string executable = ExecutablePath; + foreach (string path in FileKeys) + { + using RegistryKey verb = Registry.CurrentUser.CreateSubKey(path, true) + ?? throw new InvalidOperationException($"Unable to create {path}."); + verb.SetValue("MUIVerb", "Open with Packwright"); + verb.SetValue("Icon", $"\"{executable}\""); + using RegistryKey command = verb.CreateSubKey("command", true) + ?? throw new InvalidOperationException("Unable to create the shell command key."); + command.SetValue(null, $"\"{executable}\" --open \"%1\""); + } + } + + public static void Uninstall() + { + foreach (string path in FileKeys) + Registry.CurrentUser.DeleteSubKeyTree(path, throwOnMissingSubKey: false); + } +} diff --git a/src/Packwright.App/Infrastructure/SingleInstance.cs b/src/Packwright.App/Infrastructure/SingleInstance.cs new file mode 100644 index 0000000..8a764a6 --- /dev/null +++ b/src/Packwright.App/Infrastructure/SingleInstance.cs @@ -0,0 +1,97 @@ +using System.IO.Pipes; +using System.Text; + +namespace Packwright.Infrastructure; + +/// +/// Keeps one Packwright running per user. A second launch hands its arguments to the first copy, which brings +/// its window to the front, and then exits, so two copies never fight over the same settings and library cache. +/// +public sealed class SingleInstance : IDisposable +{ + private readonly Mutex _mutex; + private readonly string _pipeName; + private readonly CancellationTokenSource _stop = new(); + + private SingleInstance(Mutex mutex, string pipeName) + { + _mutex = mutex; + _pipeName = pipeName; + } + + // A test copy started with its own PACKWRIGHT_DATA folder is a separate "installation" and may run alongside. + private static string Suffix => + new string(Environment.UserName.Where(char.IsLetterOrDigit).ToArray()) + + (Environment.GetEnvironmentVariable("PACKWRIGHT_DATA") is { Length: > 0 } custom + ? Convert.ToHexString(System.Security.Cryptography.SHA256.HashData( + System.Text.Encoding.UTF8.GetBytes(Path.GetFullPath(custom))))[..12] + : string.Empty); + + /// Returns the lock when this is the first copy, or null after passing on. + public static SingleInstance? TryAcquire(string[] args) + { + string suffix = Suffix; + string pipe = "Packwright.Activate." + suffix; + Mutex mutex; + try + { + mutex = new Mutex(true, "Packwright.SingleInstance." + suffix, out bool created); + if (created) return new SingleInstance(mutex, pipe); + } + catch (Exception ex) when (ex is UnauthorizedAccessException or IOException or WaitHandleCannotBeOpenedException) + { + // If the lock cannot be used at all, start normally rather than refuse to run. + return new SingleInstance(new Mutex(), pipe); + } + + mutex.Dispose(); + try + { + using var client = new NamedPipeClientStream(".", pipe, PipeDirection.Out); + client.Connect(4000); + byte[] message = Encoding.UTF8.GetBytes(string.Join('\n', args.Where(arg => arg.Length > 0))); + client.Write(message, 0, message.Length); + } + catch (Exception ex) when (ex is IOException or TimeoutException or UnauthorizedAccessException) + { + Logger.Warn("Packwright is already running but could not be reached: " + ex.Message); + } + return null; + } + + /// Calls (with any paths given) each time another launch is attempted. + public void Listen(Action onActivate) + { + _ = Task.Run(async () => + { + while (!_stop.IsCancellationRequested) + { + try + { + await using var server = new NamedPipeServerStream(_pipeName, PipeDirection.In, 1, + PipeTransmissionMode.Byte, PipeOptions.Asynchronous); + await server.WaitForConnectionAsync(_stop.Token).ConfigureAwait(false); + using var reader = new StreamReader(server, Encoding.UTF8); + string text = await reader.ReadToEndAsync(_stop.Token).ConfigureAwait(false); + onActivate(text.Split('\n', StringSplitOptions.RemoveEmptyEntries)); + } + catch (OperationCanceledException) + { + return; + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or ObjectDisposedException) + { + await Task.Delay(500).ConfigureAwait(false); + } + } + }); + } + + public void Dispose() + { + _stop.Cancel(); + try { _mutex.ReleaseMutex(); } + catch (ApplicationException) { } + _mutex.Dispose(); + } +} diff --git a/src/Packwright.App/Models/FileNode.cs b/src/Packwright.App/Models/FileNode.cs new file mode 100644 index 0000000..c8aacca --- /dev/null +++ b/src/Packwright.App/Models/FileNode.cs @@ -0,0 +1,142 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; +using Avalonia.Media; +using Packwright.Core.Models; + +namespace Packwright.App.Models; + +/// One folder or file in the game file browser tree. +public sealed class FileNode : INotifyPropertyChanged +{ + private static readonly Geometry FolderIcon = Geometry.Parse("M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"); + private static readonly Geometry FileIcon = Geometry.Parse("M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"); + private static readonly Geometry ImageIcon = Geometry.Parse("M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"); + private static readonly Geometry CodeIcon = Geometry.Parse("M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"); + private static readonly Geometry MediaIcon = Geometry.Parse("M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"); + private static readonly Geometry BinaryIcon = Geometry.Parse("M4 4h6v6H4zm10 0h6v6h-6zM4 14h6v6H4zm10 0h6v6h-6z"); + + private static readonly IBrush FolderBrush = new SolidColorBrush(Color.Parse("#F5B642")); + private static readonly IBrush ImageBrush = new SolidColorBrush(Color.Parse("#3DD68C")); + private static readonly IBrush CodeBrush = new SolidColorBrush(Color.Parse("#7C8CFF")); + private static readonly IBrush MediaBrush = new SolidColorBrush(Color.Parse("#C084FC")); + private static readonly IBrush PlainBrush = new SolidColorBrush(Color.Parse("#8E94AE")); + + private static readonly HashSet ImageTypes = new(StringComparer.OrdinalIgnoreCase) + { ".png", ".jpg", ".jpeg", ".bmp", ".gif", ".webp", ".dds", ".tga", ".ico" }; + private static readonly HashSet CodeTypes = new(StringComparer.OrdinalIgnoreCase) + { ".txt", ".json", ".xml", ".cfg", ".ini", ".log", ".lua", ".js", ".md", ".csv", ".yml", ".yaml", ".html", ".css", ".py", ".sh", ".conf", ".sfo", ".meta", ".self", ".elf", ".sprx", ".prx" }; + private static readonly HashSet MediaTypes = new(StringComparer.OrdinalIgnoreCase) + { ".at9", ".mp3", ".ogg", ".wav", ".mp4", ".webm", ".mkv", ".avi", ".bk2", ".usm", ".wem", ".opus", ".m4a" }; + + private bool _isExpanded; + + public event PropertyChangedEventHandler? PropertyChanged; + + public string Name { get; init; } = string.Empty; + public string FullPath { get; init; } = string.Empty; + public bool IsDirectory { get; init; } + public long Size { get; set; } + public int FileCount { get; set; } + public Ps5FileInfo? Info { get; init; } + public List Children { get; } = []; + + public Geometry Icon { get; private set; } = FileIcon; + public IBrush IconBrush { get; private set; } = PlainBrush; + + public string Detail => IsDirectory + ? $"{FileCount:N0} file{(FileCount == 1 ? string.Empty : "s")} · {LibraryItem.FormatBytes(Size)}" + : LibraryItem.FormatBytes(Size); + + public bool IsExpanded + { + get => _isExpanded; + set + { + if (_isExpanded == value) return; + _isExpanded = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsExpanded))); + } + } + + public void ApplyIcon() + { + if (IsDirectory) { Icon = FolderIcon; IconBrush = FolderBrush; return; } + string extension = Path.GetExtension(Name); + (Icon, IconBrush) = extension switch + { + _ when ImageTypes.Contains(extension) => (ImageIcon, ImageBrush), + _ when CodeTypes.Contains(extension) => (CodeIcon, CodeBrush), + _ when MediaTypes.Contains(extension) => (MediaIcon, MediaBrush), + ".bin" or ".dat" or ".pak" or ".pkg" or ".psarc" => (BinaryIcon, PlainBrush), + _ => (FileIcon, PlainBrush) + }; + } + + /// Builds the folder tree for a set of files and returns its top-level nodes. + public static List BuildTree(IEnumerable files, bool expandAll) + { + var roots = new List(); + var folders = new Dictionary(StringComparer.Ordinal); + + FileNode Folder(string path) + { + if (folders.TryGetValue(path, out FileNode? existing)) return existing; + int slash = path.LastIndexOf('/'); + var node = new FileNode { Name = slash < 0 ? path : path[(slash + 1)..], FullPath = path, IsDirectory = true, _isExpanded = expandAll }; + node.ApplyIcon(); + folders[path] = node; + (slash < 0 ? roots : Folder(path[..slash]).Children).Add(node); + return node; + } + + foreach (Ps5FileInfo file in files) + { + string path = file.RelativePath.Replace('\\', '/').Trim('/'); + if (path.Length == 0) continue; + int slash = path.LastIndexOf('/'); + var leaf = new FileNode + { + Name = slash < 0 ? path : path[(slash + 1)..], + FullPath = path, + Size = file.Size, + FileCount = 1, + Info = file + }; + leaf.ApplyIcon(); + (slash < 0 ? roots : Folder(path[..slash]).Children).Add(leaf); + } + + Finish(roots); + return roots; + } + + private static void Finish(List nodes) + { + foreach (FileNode node in nodes.Where(node => node.IsDirectory)) + { + Finish(node.Children); + node.Size = node.Children.Sum(child => child.Size); + node.FileCount = node.Children.Sum(child => child.FileCount); + } + nodes.Sort((a, b) => a.IsDirectory != b.IsDirectory + ? a.IsDirectory ? -1 : 1 + : string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase)); + } + + public IEnumerable SelfAndDescendants() + { + yield return this; + foreach (FileNode child in Children) + foreach (FileNode inner in child.SelfAndDescendants()) + yield return inner; + } + + public IEnumerable Files() => SelfAndDescendants().Where(node => !node.IsDirectory); + + public static void SetExpanded(IEnumerable nodes, bool expanded) + { + foreach (FileNode node in nodes) + foreach (FileNode inner in node.SelfAndDescendants().Where(inner => inner.IsDirectory)) + inner.IsExpanded = expanded; + } +} diff --git a/src/Packwright.App/Models/LibraryItem.cs b/src/Packwright.App/Models/LibraryItem.cs new file mode 100644 index 0000000..66d6bfe --- /dev/null +++ b/src/Packwright.App/Models/LibraryItem.cs @@ -0,0 +1,208 @@ +using System.ComponentModel; +using System.Runtime.CompilerServices; +using Avalonia.Media.Imaging; +using Packwright.App.Services; +using Packwright.Core.Models; + +namespace Packwright.App.Models; + +/// A library row: display-ready values derived from a scanned . +public sealed class LibraryItem : INotifyPropertyChanged +{ + private Bitmap? _thumbnail; + private bool _thumbnailRequested; + private string _groupKey = string.Empty; + private string _role = "Unknown"; + + public LibraryItem(Ps5GameInfo game) + { + Game = game; + Format = game.SourceDescription; + SizeText = game.SourceSize > 0 ? FormatBytes(game.SourceSize) : string.Empty; + ApplyOverride(MetadataOverrides.Get(game.RootPath)); + } + + public event PropertyChangedEventHandler? PropertyChanged; + + public Ps5GameInfo Game { get; } + public string Title { get; private set; } = string.Empty; + public string TitleId { get; private set; } = string.Empty; + public string ContentId { get; private set; } = string.Empty; + public string Category { get; private set; } = string.Empty; + public string Version { get; private set; } = string.Empty; + public string Region { get; private set; } = string.Empty; + public string ConceptId { get; private set; } = string.Empty; + public string MasterVersion { get; private set; } = string.Empty; + public string SdkVersion { get; private set; } = string.Empty; + public string Drm { get; private set; } = string.Empty; + public string DefaultLanguage { get; private set; } = string.Empty; + public string Created { get; private set; } = string.Empty; + public string ToolVersion { get; private set; } = string.Empty; + public IReadOnlyList Features { get; private set; } = []; + /// True when the user edited this item's details. + public bool IsEdited { get; private set; } + + /// Details the title still lacks (after the user's edits), such as "Title ID" or "Firmware". + public IReadOnlyList MissingFields { get; private set; } = []; + public bool HasMissing => MissingFields.Count > 0; + public string MissingText => "Missing: " + string.Join(", ", MissingFields); + public bool HasTitleId => TitleId.Length > 0; + public string Format { get; } + public long SizeBytes => Game.SourceSize; + public string SizeText { get; } + public string Firmware { get; private set; } = string.Empty; + public string Path => Game.RootPath; + public string FileName => + System.IO.Path.GetFileName(Game.RootPath.TrimEnd(System.IO.Path.DirectorySeparatorChar, + System.IO.Path.AltDirectorySeparatorChar)); + + /// Recomputes the displayed values from the file's own metadata plus the user's edits. + public void ApplyOverride(MetadataOverride? edit) + { + string Pick(string custom, string detected) => string.IsNullOrWhiteSpace(custom) ? detected : custom.Trim(); + Title = Pick(edit?.Title ?? string.Empty, string.IsNullOrWhiteSpace(Game.Title) ? FileName : Game.Title); + TitleId = Pick(edit?.TitleId ?? string.Empty, Game.TitleId); + ContentId = Pick(edit?.ContentId ?? string.Empty, Game.ContentId); + Version = Pick(edit?.Version ?? string.Empty, Game.DisplayVersion); + Firmware = Pick(edit?.Firmware ?? string.Empty, Game.RequiredSystemSoftware); + Category = Pick(edit?.Category ?? string.Empty, CategoryOf(Game)); + ConceptId = Pick(edit?.ConceptId ?? string.Empty, Game.ConceptId); + MasterVersion = Pick(edit?.MasterVersion ?? string.Empty, Game.MasterVersion); + SdkVersion = Pick(edit?.Sdk ?? string.Empty, Game.SdkVersion); + Drm = Pick(edit?.Drm ?? string.Empty, Game.DrmType); + DefaultLanguage = Pick(edit?.DefaultLanguage ?? string.Empty, Game.DefaultLanguage); + Created = Pick(edit?.Created ?? string.Empty, Game.CreationDate); + ToolVersion = Pick(edit?.ToolVersion ?? string.Empty, Game.ToolVersion); + Features = edit?.Features is { } features ? features : Game.DeclaredFeatures; + Region = RegionOfId(ContentId.Length > 0 ? ContentId : TitleId); + IsEdited = edit is { IsEmpty: false }; + var missing = new List(); + if (Game.LocalizedTitles.Count == 0 && string.IsNullOrWhiteSpace(edit?.Title)) missing.Add("Title"); + if (TitleId.Length == 0) missing.Add("Title ID"); + if (ContentId.Length == 0) missing.Add("Content ID"); + if (Version.Length == 0) missing.Add("Version"); + if (Firmware.Length == 0) missing.Add("Firmware"); + MissingFields = missing; + _role = Category switch + { + "Game" => "Base", + "Patch" => "Update", + "DLC" or "Add-on" => "DLC", + "App" => "App", + _ => "Unknown" + }; + Raise(string.Empty); + } + + /// The item's role in its title family: Base, Update, DLC or App. + public string Role + { + get => _role; + set { if (_role != value) { _role = value; Raise(); } } + } + + /// Key of the group this row currently sits in (empty when grouping is off). + public string GroupKey + { + get => _groupKey; + set { if (_groupKey != value) { _groupKey = value; Raise(); } } + } + + public string Subtitle + { + get + { + var parts = new List(); + if (TitleId.Length > 0) parts.Add(TitleId); + if (Version.Length > 0) parts.Add("v" + Version.TrimStart('v', 'V')); + return string.Join(" · ", parts); + } + } + + /// Artwork, loaded lazily the first time a view asks for it. + public Bitmap? Thumbnail + { + get + { + if (!_thumbnailRequested) + { + _thumbnailRequested = true; + ThumbnailService.Request(this); + } + return _thumbnail; + } + } + + public bool HasThumbnail => _thumbnail is not null; + + public void SetThumbnail(Bitmap? bitmap) + { + _thumbnail = bitmap; + Raise(nameof(Thumbnail)); + Raise(nameof(HasThumbnail)); + } + + /// Forgets a cached bitmap so it can be loaded again after eviction. + public void ResetThumbnail() + { + _thumbnail = null; + _thumbnailRequested = false; + Raise(nameof(Thumbnail)); + Raise(nameof(HasThumbnail)); + } + + private void Raise([CallerMemberName] string? name = null) => + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); + + public static string RegionOf(Ps5GameInfo game) => + RegionOfId(!string.IsNullOrWhiteSpace(game.ContentId) ? game.ContentId : game.TitleId); + + /// The regions a Content ID can name, with the two letters it starts with. + public static readonly (string Name, string Prefix)[] Regions = + [ + ("Americas", "UP"), ("Europe", "EP"), ("Japan", "JP"), ("Korea", "KP"), ("Asia", "AP"), ("Hong Kong", "HP") + ]; + + public static string RegionOfId(string id) + { + if (id.Length < 1) return "Unknown"; + return char.ToUpperInvariant(id[0]) switch + { + 'U' => "Americas", + 'E' => "Europe", + 'J' => "Japan", + 'K' => "Korea", + 'A' => "Asia", + 'H' => "Hong Kong", + _ => "Other" + }; + } + + public static string CategoryOf(Ps5GameInfo game) + { + string raw = game.ApplicationCategory; + if (!string.IsNullOrWhiteSpace(raw)) + { + string token = raw.Split(' ', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault() ?? raw; + if (int.TryParse(token, out int number)) + return number switch { 1 => "DLC", 2 => "Patch", 3 => "App", _ => "Game" }; + return raw; + } + return game.Package?.ContentType switch + { + 0x20 => "Game", + 0x21 => "DLC", + 0x22 => "DLC", + _ => game.SourceKind == Ps5SourceKind.LooseDump ? "Game" : "Unknown" + }; + } + + public static string FormatBytes(long bytes) + { + string[] units = ["B", "KB", "MB", "GB", "TB"]; + double value = Math.Max(0, bytes); + int unit = 0; + while (value >= 1024 && unit < units.Length - 1) { value /= 1024; unit++; } + return $"{value:0.##} {units[unit]}"; + } +} diff --git a/src/Packwright.App/Packwright.App.csproj b/src/Packwright.App/Packwright.App.csproj new file mode 100644 index 0000000..5338051 --- /dev/null +++ b/src/Packwright.App/Packwright.App.csproj @@ -0,0 +1,55 @@ + + + WinExe + net10.0 + enable + enable + latest + Packwright + Packwright.App + Assets\icon.ico + false + 1.0.0 + 1.0.0.0 + Packwright + itsmemac + itsmemac + Copyright (c) 2026 itsmemac + Cross-platform PS5 dump, image and package manager. Based on PS5 PKG Tool by pearlxcore (GPL-3.0). + false + en + + false + true + false + + + + + + + + + + + + + + + + + + + + + + + + + + LibProsperoPkg12\%(RecursiveDir)%(Filename)%(Extension) + PreserveNewest + PreserveNewest + + + diff --git a/src/Packwright.App/Program.cs b/src/Packwright.App/Program.cs new file mode 100644 index 0000000..309cfbb --- /dev/null +++ b/src/Packwright.App/Program.cs @@ -0,0 +1,37 @@ +using Avalonia; +using Packwright.Infrastructure; + +namespace Packwright.App; + +internal static class Program +{ + [STAThread] + public static void Main(string[] args) + { + AppDomain.CurrentDomain.UnhandledException += (_, e) => + Logger.Exception("AppDomain", e.ExceptionObject as Exception ?? new Exception("Unknown error")); + TaskScheduler.UnobservedTaskException += (_, e) => + { + Logger.Exception("Unobserved task", e.Exception); + e.SetObserved(); + }; + if (Cli.IsCommand(args)) Environment.Exit(Cli.Run(args)); + using SingleInstance? instance = SingleInstance.TryAcquire(args); + if (instance is null) return; + Current = instance; + Logger.Info($"Packwright {System.Reflection.Assembly.GetExecutingAssembly().GetName().Version?.ToString(3)} started on {System.Runtime.InteropServices.RuntimeInformation.OSDescription}."); + BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); + Logger.Info("Packwright exited."); + } + + /// The lock that keeps this the only running copy. + internal static SingleInstance? Current { get; private set; } + + public static AppBuilder BuildAvaloniaApp() => + AppBuilder.Configure().UsePlatformDetect().WithInterFont().LogToTrace() + // Draw into a normal window surface. The default DirectComposition surface has no redirection + // bitmap, which some screenshot and screen-recording tools capture only partly. + .With(new Win32PlatformOptions { CompositionMode = [Win32CompositionMode.RedirectionSurface] }) + // The default lets Skia keep ~100 MB of GPU resources cached; this UI needs far less. + .With(new SkiaOptions { MaxGpuResourceSizeBytes = 32 * 1024 * 1024 }); +} diff --git a/src/Packwright.App/Services/AppServices.cs b/src/Packwright.App/Services/AppServices.cs new file mode 100644 index 0000000..069d1d8 --- /dev/null +++ b/src/Packwright.App/Services/AppServices.cs @@ -0,0 +1,212 @@ +using Packwright.Core.Models; +using Packwright.Core.Services; +using Packwright.Core.Tasks; +using Packwright.Infrastructure; + +namespace Packwright.App.Services; + +/// Process-wide state shared by every view: settings, the library and the task queue. +public sealed class AppServices +{ + public static AppServices Instance { get; } = new(); + + private readonly Ps5LibraryScanner _scanner = new(); + private readonly object _gameGate = new(); + + private AppServices() + { + Store = new AppStateStore(); + SettingsLoadResult loaded = Store.LoadSettingsWithDiagnostics(); + Settings = loaded.Settings; + SettingsWarning = loaded.Warning; + Queue = new PackageTaskQueue(); + Queue.TasksChanged += (_, _) => LogTaskChanges(); + string taskFile = Path.Combine(Store.AppDataDirectory, "studio-tasks.json"); + Queue.SetPersistencePath(taskFile); + Queue.RestoreFromDisk(RestoreTask); + foreach (QueuedPackageTask restored in Queue.Tasks) _loggedStatus[restored.Id] = restored.Status; + Queue.EnablePersistence(taskFile); + Games = Store.LoadManifest().Games; + Logger.Info($"Loaded {Games.Count:N0} cached library item(s); {Settings.LibraryFolders.Count} library folder(s), " + + $"{Settings.ManualSources.Count} added item(s)."); + } + + private readonly Dictionary _loggedStatus = []; + + /// Writes one log line whenever a task starts, finishes, fails or is cancelled. + private void LogTaskChanges() + { + foreach (QueuedPackageTask task in Queue.Tasks) + { + PackageTaskStatus previous; + bool known; + lock (_loggedStatus) + { + known = _loggedStatus.TryGetValue(task.Id, out previous); + if (known && previous == task.Status) continue; + _loggedStatus[task.Id] = task.Status; + } + string name = $"{task.DisplayName} ({task.Operation}{(task.FormatRoute.Length > 0 ? ", " + task.FormatRoute : string.Empty)})"; + switch (task.Status) + { + case PackageTaskStatus.Queued when !known: + Logger.Info($"Task queued: {name}. From {task.SourcePath}" + + (task.OutputPath.Length > 0 && task.OutputPath != task.SourcePath ? $" to {task.OutputPath}" : string.Empty)); + break; + case PackageTaskStatus.Running: + Logger.Info($"Task started: {name}"); + break; + case PackageTaskStatus.Completed: + Logger.Info($"Task completed: {name}"); + break; + case PackageTaskStatus.Failed: + Logger.Error($"Task failed: {name}: {task.Failure?.Message ?? task.Message}"); + if (task.Failure is { } failure) Logger.Error(failure.ToString()); + break; + case PackageTaskStatus.Cancelled: + Logger.Warn($"Task cancelled: {name}"); + break; + case PackageTaskStatus.Interrupted when !known: + Logger.Warn($"Task interrupted in an earlier session: {name}"); + break; + } + } + } + + public AppStateStore Store { get; } + public AppSettings Settings { get; private set; } + public string? SettingsWarning { get; } + public PackageTaskQueue Queue { get; } + public List Games { get; private set; } + + public event Action? LibraryChanged; + + /// Files the last scan found but could not read ("path: reason"). + public IReadOnlyList ScanProblems { get; private set; } = []; + + /// Raised when another page wants the Library to show (and optionally edit) one title. + public event Action? ShowTitleRequested; + + public void RequestShowTitle(string path, bool edit) => ShowTitleRequested?.Invoke(path, edit); + + /// Raised after settings were saved so views can re-read appearance options. + public event Action? SettingsChanged; + + public void NotifySettingsChanged() => SettingsChanged?.Invoke(); + + /// Raised when another view wants the Tools tab to work on a path. + public event Action? ToolsSourceRequested; + + public void RequestToolsSource(string path) => ToolsSourceRequested?.Invoke(path); + + /// Queues a job described by ; it is saved and restored across restarts. + public QueuedPackageTask EnqueueJob(JobSpec spec, string displayName, string operation, string sourceFormat, + string targetFormat, string qualifier = "") + { + var executor = spec.Executor() ?? throw new InvalidOperationException("This job cannot be run."); + var task = new QueuedPackageTask + { + Id = Guid.NewGuid().ToString("N"), + Type = spec.TaskType, + DisplayName = displayName, + SourcePath = spec.Source, + OutputPath = spec.Output.Length > 0 ? spec.Output : spec.Source, + PersistencePayload = spec.ToJson(), + Operation = operation, + SourceFormat = sourceFormat, + TargetFormat = targetFormat, + TargetQualifier = qualifier, + StagePlan = spec.Plan(), + Execute = executor + }; + // Each task reports its own state changes (started, finished, failed); log them as they happen. + task.Changed += (_, _) => LogTaskChanges(); + return Queue.Enqueue(task); + } + + private QueuedPackageTask? RestoreTask(PersistedPackageTask entry) + { + JobSpec? spec = JobSpec.FromJson(entry.Payload); + var task = new QueuedPackageTask + { + Id = entry.Id, + Type = entry.Type, + DisplayName = entry.DisplayName, + SourcePath = entry.SourcePath, + OutputPath = entry.OutputPath, + PersistencePayload = entry.Payload, + Operation = entry.Operation, + SourceFormat = entry.SourceFormat, + TargetFormat = entry.TargetFormat, + TargetQualifier = entry.TargetQualifier, + StagePlan = spec?.Plan() ?? [], + Execute = spec?.Executor(), + CreatedUtc = entry.CreatedUtc == default ? DateTime.UtcNow : entry.CreatedUtc + }; + task.Changed += (_, _) => LogTaskChanges(); + return task; + } + + public void SaveSettings() + { + try { Store.SaveSettings(Settings); } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + Logger.Warn("Settings could not be saved: " + ex.Message); + } + } + + public void ReplaceSettings(AppSettings settings) + { + Settings = AppSettingsNormalizer.Normalize(settings); + SaveSettings(); + } + + /// Library folders plus single items added by path (manual sources). + public IReadOnlyList ScanSources => + Settings.LibraryFolders.Concat(Settings.ManualSources).Distinct(StringComparer.OrdinalIgnoreCase).ToList(); + + public void AddManualSource(string path) + { + if (string.IsNullOrWhiteSpace(path)) return; + if (!Settings.ManualSources.Contains(path, StringComparer.OrdinalIgnoreCase)) + Settings.ManualSources.Add(path); + SaveSettings(); + } + + public void AddLibraryFolder(string path) + { + if (string.IsNullOrWhiteSpace(path)) return; + if (!Settings.LibraryFolders.Contains(path, StringComparer.OrdinalIgnoreCase)) + Settings.LibraryFolders.Add(path); + Settings.RecentFolders.RemoveAll(existing => string.Equals(existing, path, StringComparison.OrdinalIgnoreCase)); + Settings.RecentFolders.Insert(0, path); + SaveSettings(); + } + + public void RemoveLibraryFolder(string path) + { + Settings.LibraryFolders.RemoveAll(existing => string.Equals(existing, path, StringComparison.OrdinalIgnoreCase)); + SaveSettings(); + } + + /// Rescans every library folder (reusing cached entries for unchanged sources). + public async Task RefreshLibraryAsync(IProgress? progress, + CancellationToken cancellationToken) + { + Logger.Info($"Scanning {ScanSources.Count} source(s){(Settings.RecursiveScan ? " including sub-folders" : string.Empty)}..."); + Ps5ScanResult result = await _scanner.ScanAsync(ScanSources, Settings.RecursiveScan, + Games, progress, cancellationToken).ConfigureAwait(false); + lock (_gameGate) Games = result.Games; + try { Store.SaveManifest(result.Games); } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + Logger.Warn("The library manifest could not be saved: " + ex.Message); + } + ScanProblems = result.Errors.ToList(); + foreach (string error in result.Errors) Logger.Warn("Scan: " + error); + Logger.Info($"Scan finished: {result.Games.Count:N0} item(s), {result.Errors.Count} problem(s)."); + LibraryChanged?.Invoke(); + return result; + } +} diff --git a/src/Packwright.App/Services/ConsoleSender.cs b/src/Packwright.App/Services/ConsoleSender.cs new file mode 100644 index 0000000..e2cc325 --- /dev/null +++ b/src/Packwright.App/Services/ConsoleSender.cs @@ -0,0 +1,176 @@ +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Text.Json; +using Packwright.Core.Services; +using Packwright.Core.Tasks; +using Packwright.Infrastructure; + +namespace Packwright.App.Services; + +/// +/// Sends files to a console on the local network: FTP upload, or "install by URL" where the console's package installer +/// downloads a .pkg from this PC. Only addresses on the local network are used, and only when the user sends something. +/// +public static class ConsoleSender +{ + private const int Attempts = 3; + + /// The remote folder a source will end up in: the chosen folder plus the file or dump folder name. + public static string RemotePathFor(string remoteFolder, string source) => + FtpClient.Normalize(FtpClient.Normalize(remoteFolder) + "/" + + Path.GetFileName(source.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar))); + + /// Connects and reports what the console's FTP server says, so a profile can be checked before sending. + public static async Task TestFtpAsync(ConsoleProfile profile, CancellationToken token) + { + IPAddress address = await ConsoleNetwork.ResolveLocalAsync(profile.Host, token).ConfigureAwait(false); + await using FtpClient client = await ConnectAsync(address, profile, token).ConfigureAwait(false); + string system = await client.SystemAsync(token).ConfigureAwait(false); + (int code, string text) = await client.CommandAsync("PWD", token).ConfigureAwait(false); + int open = text.IndexOf('"'), close = text.IndexOf('"', open + 1); + string folder = code == 257 && open >= 0 && close > open ? text[(open + 1)..close] : "unknown"; + return $"Connected to {address}:{profile.FtpPort}. {(system.Length > 0 ? system + ". " : string.Empty)}Start folder: {folder}."; + } + + private static Task ConnectAsync(IPAddress address, ConsoleProfile profile, CancellationToken token) => + // Console FTP payloads accept anyone, so no account is used (and no password is stored). + FtpClient.ConnectAsync(address.ToString(), profile.FtpPort, "anonymous", "packwright@localhost", token); + + // ------------------------------------------------------------------ FTP upload + + public static async Task UploadAsync(ConsoleProfile profile, string source, string remoteFolder, + IProgress progress, CancellationToken token) + { + IPAddress address = await ConsoleNetwork.ResolveLocalAsync(profile.Host, token).ConfigureAwait(false); + string remoteRoot = RemotePathFor(remoteFolder, source); + List<(string Local, string Remote, long Size)> files = []; + if (Directory.Exists(source)) + { + string root = Path.GetFullPath(source); + foreach (string file in Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories).Order(StringComparer.Ordinal)) + files.Add((file, remoteRoot + "/" + Path.GetRelativePath(root, file).Replace('\\', '/'), new FileInfo(file).Length)); + } + else if (File.Exists(source)) + files.Add((source, remoteRoot, new FileInfo(source).Length)); + else + throw new FileNotFoundException("The file to send was not found.", source); + if (files.Count == 0) throw new InvalidDataException("There is nothing in that folder to send."); + + long total = files.Sum(file => file.Size), done = 0; + var watch = Stopwatch.StartNew(); + long lastReport = 0; + FtpClient? client = null; + try + { + for (int index = 0; index < files.Count; index++) + { + (string local, string remote, long size) = files[index]; + for (int attempt = 1; ; attempt++) + { + token.ThrowIfCancellationRequested(); + try + { + client ??= await ConnectAsync(address, profile, token).ConfigureAwait(false); + await client.EnsureDirectoryAsync(remote[..remote.LastIndexOf('/')], token).ConfigureAwait(false); + long before = done; + await client.UploadAsync(local, remote, sent => + { + long now = watch.ElapsedMilliseconds; + if (now - lastReport < 250 && sent < size) return; + lastReport = now; + progress.Report(new PackageTaskProgress("Sending to " + profile.Name, 1, 1, before + sent, total, + index + 1, files.Count, remote, StagePercent: total > 0 ? (before + sent) * 100.0 / total : -1)); + }, token).ConfigureAwait(false); + done += size; + break; + } + catch (Exception ex) when (attempt < Attempts && IsRetryable(ex)) + { + // The link dropped or the console was busy: reconnect and carry on (a partly sent file is continued). + Logger.Warn($"Send to {profile.Name}: attempt {attempt} failed ({ex.Message}); retrying."); + if (client is not null) await client.DisposeAsync().ConfigureAwait(false); + client = null; + await Task.Delay(TimeSpan.FromSeconds(2), token).ConfigureAwait(false); + } + } + } + progress.Report(new PackageTaskProgress("Sent to " + profile.Name, 1, 1, total, total, files.Count, files.Count, string.Empty, 100)); + } + finally + { + if (client is not null) await client.DisposeAsync().ConfigureAwait(false); + } + Logger.Info($"Sent {files.Count:N0} file(s), {total:N0} bytes, to {profile.Name} ({address}) at {remoteRoot}."); + } + + /// Dropped links and busy servers are worth another try; refusals (5xx) are not. + private static bool IsRetryable(Exception ex) => ex switch + { + FtpException ftp => ftp.IsTemporary, + IOException or SocketException => true, + _ => false + }; + + // ------------------------------------------------------------------ install by URL + + private static readonly HttpClient Http = new() { Timeout = TimeSpan.FromSeconds(20) }; + + /// Fills the placeholders of the install request. {url} is escaped for JSON bodies. + public static (string Url, string Body) BuildInstallRequest(ConsoleProfile profile, string fileUrl) + { + string url = profile.InstallUrl.Replace("{host}", profile.Host, StringComparison.Ordinal) + .Replace("{url}", Uri.EscapeDataString(fileUrl), StringComparison.Ordinal); + bool json = profile.InstallContentType.Contains("json", StringComparison.OrdinalIgnoreCase); + string value = json ? JsonEncodedText.Encode(fileUrl).ToString() : fileUrl; + return (url, profile.InstallBody.Replace("{url}", value, StringComparison.Ordinal)); + } + + public static async Task InstallByUrlAsync(ConsoleProfile profile, string package, + IProgress progress, CancellationToken token) + { + if (!File.Exists(package) || !package.EndsWith(".pkg", StringComparison.OrdinalIgnoreCase)) + throw new InvalidDataException("Install by URL needs a .pkg file."); + if (string.IsNullOrWhiteSpace(profile.InstallUrl)) + throw new InvalidOperationException("This console has no install address. Set it in the console's settings."); + IPAddress console = await ConsoleNetwork.ResolveLocalAsync(profile.Host, token).ConfigureAwait(false); + IPAddress local = ConsoleNetwork.LocalAddressFor(console); + await using SingleFileServer server = SingleFileServer.Start(package, local); + Logger.Info($"Serving {Path.GetFileName(package)} at {local}:{server.Port} for {profile.Name}."); + + (string requestUrl, string body) = BuildInstallRequest(profile, server.Url); + if (!Uri.TryCreate(requestUrl, UriKind.Absolute, out Uri? target) || target.Scheme != Uri.UriSchemeHttp) + throw new InvalidOperationException("The install address is not a valid http:// address: " + requestUrl); + await ConsoleNetwork.ResolveLocalAsync(target.Host, token).ConfigureAwait(false); + + progress.Report(new PackageTaskProgress("Asking " + profile.Name + " to install", 1, 3, 0, server.Length, 0, 0, string.Empty, 0)); + using var content = new StringContent(body, Encoding.UTF8); + content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(profile.InstallContentType); + using HttpResponseMessage response = await Http.PostAsync(target, content, token).ConfigureAwait(false); + if (!response.IsSuccessStatusCode) + { + string text = (await response.Content.ReadAsStringAsync(token).ConfigureAwait(false)).Trim(); + throw new IOException($"The console's installer answered {(int)response.StatusCode} {response.ReasonPhrase}. {(text.Length > 200 ? text[..200] : text)}"); + } + + // Keep the file available while the console downloads it: wait until it has taken the whole file and has been quiet. + DateTime asked = DateTime.UtcNow; + while (true) + { + token.ThrowIfCancellationRequested(); + await Task.Delay(500, token).ConfigureAwait(false); + double percent = server.Length > 0 ? Math.Min(100.0, server.BytesServed * 100.0 / server.Length) : 0; + bool started = server.Requests > 0; + progress.Report(new PackageTaskProgress(started ? profile.Name + " is downloading the package" : "Waiting for " + profile.Name + " to start the download", + 2, 3, Math.Min(server.BytesServed, server.Length), server.Length, 0, 0, string.Empty, percent)); + if (!started && DateTime.UtcNow - asked > TimeSpan.FromMinutes(5)) + throw new IOException("The console never asked for the file. Check the install address in the console's settings, and that the firewall allows Packwright on your network."); + if (server.FullyServed && DateTime.UtcNow - server.LastRequestUtc > TimeSpan.FromSeconds(20)) break; + if (started && DateTime.UtcNow - server.LastRequestUtc > TimeSpan.FromMinutes(5)) + throw new IOException("The console stopped downloading before it had the whole file."); + } + progress.Report(new PackageTaskProgress("The console has the package", 3, 3, server.Length, server.Length, 0, 0, string.Empty, 100)); + Logger.Info($"{profile.Name} downloaded {Path.GetFileName(package)}."); + } +} diff --git a/src/Packwright.App/Services/Dialogs.cs b/src/Packwright.App/Services/Dialogs.cs new file mode 100644 index 0000000..88f0c6f --- /dev/null +++ b/src/Packwright.App/Services/Dialogs.cs @@ -0,0 +1,250 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Layout; +using Avalonia.Platform.Storage; + +namespace Packwright.App.Services; + +/// Small helpers for message boxes and the platform file/folder pickers. +public static class Dialogs +{ + public static Window? OwnerOf(Control control) => TopLevel.GetTopLevel(control) as Window; + + public static async Task ShowMessageAsync(Window? owner, string title, string message) + { + await ShowAsync(owner, title, message, ["OK"]); + } + + public static async Task ConfirmAsync(Window? owner, string title, string message, + string yes = "Yes", string no = "Cancel") => + await ShowAsync(owner, title, message, [yes, no]) == 0; + + private static async Task ShowAsync(Window? owner, string title, string message, string[] buttons) + { + int result = -1; + var window = new Window + { + Title = title, + Width = 460, + SizeToContent = SizeToContent.Height, + MinHeight = 140, + CanResize = false, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + ShowInTaskbar = false + }; + var row = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, Spacing = 8 }; + for (int i = 0; i < buttons.Length; i++) + { + int index = i; + var button = new Button { Content = buttons[i], MinWidth = 80, HorizontalContentAlignment = HorizontalAlignment.Center }; + button.Click += (_, _) => { result = index; window.Close(); }; + if (i == 0) button.Classes.Add("accent"); + row.Children.Add(button); + } + window.Content = new StackPanel + { + Margin = new Thickness(20), + Spacing = 18, + Children = + { + new TextBlock { Text = message, TextWrapping = Avalonia.Media.TextWrapping.Wrap }, + row + } + }; + if (owner is not null) await window.ShowDialog(owner); + else window.Show(); + return result; + } + + /// Shows long text in a scrollable box; returns true when the user picks the first button. + public static async Task ConfirmTextAsync(Window? owner, string title, string header, string body, + string yes = "Apply", string no = "Cancel") + { + bool result = false; + var window = new Window + { + Title = title, + Width = 720, + Height = 520, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + ShowInTaskbar = false + }; + var apply = new Button { Content = yes, MinWidth = 90 }; + apply.Classes.Add("accent"); + apply.Click += (_, _) => { result = true; window.Close(); }; + var cancel = new Button { Content = no, MinWidth = 90 }; + cancel.Click += (_, _) => window.Close(); + var buttons = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8, HorizontalAlignment = HorizontalAlignment.Right }; + if (yes.Length > 0) buttons.Children.Add(apply); + buttons.Children.Add(cancel); + var grid = new Grid { Margin = new Thickness(16), RowDefinitions = new RowDefinitions("Auto,*,Auto") }; + grid.Children.Add(new TextBlock { Text = header, TextWrapping = Avalonia.Media.TextWrapping.Wrap, Margin = new Thickness(0, 0, 0, 10) }); + var box = new TextBox + { + Text = body, + IsReadOnly = true, + AcceptsReturn = true, + FontFamily = new Avalonia.Media.FontFamily("Consolas,Menlo,monospace") + }; + Grid.SetRow(box, 1); + grid.Children.Add(box); + Grid.SetRow(buttons, 2); + buttons.Margin = new Thickness(0, 10, 0, 0); + grid.Children.Add(buttons); + window.Content = grid; + if (owner is not null) await window.ShowDialog(owner); + else window.Show(); + return result; + } + + /// Asks for one line of text; returns null when cancelled. + public static async Task PromptAsync(Window? owner, string title, string label, string initial = "") + { + string? result = null; + var window = new Window + { + Title = title, + Width = 420, + SizeToContent = SizeToContent.Height, + CanResize = false, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + ShowInTaskbar = false + }; + var box = new TextBox { Text = initial }; + var ok = new Button { Content = "OK", MinWidth = 80 }; + ok.Classes.Add("accent"); + ok.Click += (_, _) => { result = box.Text; window.Close(); }; + var cancel = new Button { Content = "Cancel", MinWidth = 80 }; + cancel.Click += (_, _) => window.Close(); + box.KeyDown += (_, e) => { if (e.Key == Avalonia.Input.Key.Enter) { result = box.Text; window.Close(); } }; + window.Content = new StackPanel + { + Margin = new Thickness(20), + Spacing = 12, + Children = + { + new TextBlock { Text = label }, + box, + new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8, HorizontalAlignment = HorizontalAlignment.Right, Children = { ok, cancel } } + } + }; + if (owner is not null) await window.ShowDialog(owner); + else window.Show(); + return string.IsNullOrWhiteSpace(result) ? null : result.Trim(); + } + + /// Shows a list of checkboxes; returns the new states or null when cancelled. + public static async Task ChecklistAsync(Window? owner, string title, IReadOnlyList<(string Label, bool Checked)> items) + { + bool[]? result = null; + var window = new Window + { + Title = title, + Width = 360, + SizeToContent = SizeToContent.Height, + CanResize = false, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + ShowInTaskbar = false + }; + var boxes = items.Select(item => new CheckBox { Content = item.Label, IsChecked = item.Checked }).ToList(); + var ok = new Button { Content = "Apply", MinWidth = 80 }; + ok.Classes.Add("accent"); + ok.Click += (_, _) => { result = boxes.Select(box => box.IsChecked == true).ToArray(); window.Close(); }; + var cancel = new Button { Content = "Cancel", MinWidth = 80 }; + cancel.Click += (_, _) => window.Close(); + var list = new StackPanel { Spacing = 4 }; + foreach (CheckBox box in boxes) list.Children.Add(box); + window.Content = new StackPanel + { + Margin = new Thickness(20), + Spacing = 14, + Children = + { + list, + new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8, HorizontalAlignment = HorizontalAlignment.Right, Children = { ok, cancel } } + } + }; + if (owner is not null) await window.ShowDialog(owner); + else window.Show(); + return result; + } + + public static async Task PickFolderAsync(Visual anchor, string title) + { + TopLevel? top = TopLevel.GetTopLevel(anchor); + if (top is null) return null; + IReadOnlyList folders = await top.StorageProvider.OpenFolderPickerAsync( + new FolderPickerOpenOptions { Title = title, AllowMultiple = false }); + return folders.Count > 0 ? folders[0].TryGetLocalPath() : null; + } + + public static async Task PickFileAsync(Visual anchor, string title, string[]? patterns = null) + { + TopLevel? top = TopLevel.GetTopLevel(anchor); + if (top is null) return null; + IReadOnlyList files = await top.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions + { + Title = title, + AllowMultiple = false, + FileTypeFilter = + [ + new FilePickerFileType(patterns is null ? "PS5 images and packages" : "Files") + { + Patterns = patterns ?? ["*.pkg", "*.exfat", "*.ffpkg", "*.ffpfsc", "*.zar"] + }, + FilePickerFileTypes.All + ] + }); + return files.Count > 0 ? files[0].TryGetLocalPath() : null; + } + + public static async Task SaveFileAsync(Visual anchor, string title, string suggestedName, string extension) + { + TopLevel? top = TopLevel.GetTopLevel(anchor); + if (top is null) return null; + IStorageFile? file = await top.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions + { + Title = title, + SuggestedFileName = suggestedName, + DefaultExtension = extension.TrimStart('.'), + ShowOverwritePrompt = false + }); + return file?.TryGetLocalPath(); + } + + public static void OpenPath(string path) + { + try + { + if (OperatingSystem.IsWindows()) + System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(path) { UseShellExecute = true }); + else if (OperatingSystem.IsMacOS()) + System.Diagnostics.Process.Start("open", [path]); + else + System.Diagnostics.Process.Start("xdg-open", [path]); + } + catch (Exception ex) + { + Packwright.Infrastructure.Logger.Warn("Could not open " + path + ": " + ex.Message); + } + } + + /// Shows a file or folder in the platform file manager. + public static void Reveal(string path) + { + try + { + string folder = Directory.Exists(path) ? path : Path.GetDirectoryName(path) ?? path; + if (OperatingSystem.IsWindows() && File.Exists(path)) + System.Diagnostics.Process.Start("explorer.exe", $"/select,\"{path}\""); + else if (OperatingSystem.IsMacOS() && File.Exists(path)) + System.Diagnostics.Process.Start("open", ["-R", path]); + else + OpenPath(folder); + } + catch (Exception ex) + { + Packwright.Infrastructure.Logger.Warn("Could not reveal " + path + ": " + ex.Message); + } + } +} diff --git a/src/Packwright.App/Services/IconTools.cs b/src/Packwright.App/Services/IconTools.cs new file mode 100644 index 0000000..98044f1 --- /dev/null +++ b/src/Packwright.App/Services/IconTools.cs @@ -0,0 +1,70 @@ +using Avalonia; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Threading; +using Packwright.Core.Models; + +namespace Packwright.App.Services; + +/// Turns artwork into the 512x512 PNG a title uses as its icon (sce_sys/icon0.png). +public static class IconTools +{ + public const int IconSize = 512; + + /// Centre-crops to a square and scales it to 512x512. Runs on the UI thread. + public static Task SquarePngAsync(Ps5ImageData source) => + Dispatcher.UIThread.InvokeAsync(() => SquarePng(source)).GetTask(); + + public static byte[]? SquarePng(Ps5ImageData source) + { + try + { + using Bitmap decoded = Decode(source); + return Square(decoded); + } + catch (Exception ex) when (ex is ArgumentException or InvalidOperationException or IOException) + { + return null; + } + } + + public static byte[]? SquarePng(string file) + { + try + { + using var decoded = new Bitmap(file); + return Square(decoded); + } + catch (Exception ex) when (ex is ArgumentException or InvalidOperationException or IOException or UnauthorizedAccessException) + { + return null; + } + } + + private static byte[] Square(Bitmap decoded) + { + using var square = new RenderTargetBitmap(new PixelSize(IconSize, IconSize)); + using (DrawingContext context = square.CreateDrawingContext()) + { + double side = Math.Min(decoded.PixelSize.Width, decoded.PixelSize.Height); + var crop = new Rect((decoded.PixelSize.Width - side) / 2, (decoded.PixelSize.Height - side) / 2, side, side); + context.DrawImage(decoded, crop, new Rect(0, 0, IconSize, IconSize)); + } + using var memory = new MemoryStream(); + square.Save(memory); + return memory.ToArray(); + } + + private static Bitmap Decode(Ps5ImageData data) + { + if (!data.IsRgba) return new Bitmap(new MemoryStream(data.Bytes, writable: false)); + var bitmap = new WriteableBitmap(new PixelSize(data.Width, data.Height), new Vector(96, 96), + PixelFormat.Rgba8888, AlphaFormat.Unpremul); + using ILockedFramebuffer buffer = bitmap.Lock(); + int rowBytes = data.Width * 4; + for (int y = 0; y < data.Height; y++) + System.Runtime.InteropServices.Marshal.Copy(data.Bytes, y * rowBytes, buffer.Address + y * buffer.RowBytes, rowBytes); + return bitmap; + } +} diff --git a/src/Packwright.App/Services/JobSpec.cs b/src/Packwright.App/Services/JobSpec.cs new file mode 100644 index 0000000..a374c59 --- /dev/null +++ b/src/Packwright.App/Services/JobSpec.cs @@ -0,0 +1,194 @@ +using System.Text.Json; +using Packwright.Core.Backends; +using Packwright.Core.Builders; +using Packwright.Core.Tasks; +using Packwright.Containers; +using Packwright.Infrastructure; +using UFS2Tool; + +namespace Packwright.App.Services; + +/// +/// A serializable description of one queued job. The same spec drives the job when it is first queued +/// and when it is restored after a restart, so there is one place that knows how to run each kind. +/// +public sealed class JobSpec +{ + public const string Convert = "convert"; + public const string BuildPackage = "build"; + public const string Extract = "extract"; + public const string Verify = "verify"; + public const string Repair = "repair"; + public const string RefreshAmpr = "ampr"; + public const string RebuildFfpkg = "rebuild"; + public const string EditDetails = "editmeta"; + public const string SendFtp = "sendftp"; + public const string SendInstall = "sendurl"; + + public string Kind { get; set; } = string.Empty; + + /// The game's name and a small PNG icon (base64), shown on the task card. + public string Title { get; set; } = string.Empty; + public string? Icon { get; set; } + public string Source { get; set; } = string.Empty; + public string Output { get; set; } = string.Empty; + public string Format { get; set; } = string.Empty; // Ps5ImageFormat name of the source + public string Target { get; set; } = string.Empty; // Ps5ImageConversionTarget name + public bool Overwrite { get; set; } + public bool FromPackage { get; set; } + public string Passcode { get; set; } = string.Empty; + + // Image options + public int ClusterSize { get; set; } + public bool GenerateAmpr { get; set; } = true; + public int CompressionLevel { get; set; } = 7; + public int MinimumGain { get; set; } = 1; + public int BlockSize { get; set; } = 32768; + public int FragmentSize { get; set; } = 4096; + public int BytesPerInode { get; set; } = 262144; + public int MinFreePercent { get; set; } + + /// For the send kinds: a copy of the console's settings at the time, and the folder on the console. + public ConsoleProfile? Console { get; set; } + public string RemoteFolder { get; set; } = string.Empty; + + /// For : the details to write. Output empty means edit in place. + public MetadataEdit? Edit { get; set; } + + // Package build + public string ContentId { get; set; } = string.Empty; + public string Backend { get; set; } = string.Empty; + 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 string ToJson() => JsonSerializer.Serialize(this); + + public static JobSpec? FromJson(string? json) + { + if (string.IsNullOrWhiteSpace(json)) return null; + try { return JsonSerializer.Deserialize(json); } + catch (JsonException) { return null; } + } + + public PackageTaskStage[] Plan() => Kind switch + { + Convert => FromPackage ? PackageTaskPlans.ConvertPackage : PackageTaskPlans.ConvertImage, + BuildPackage => PackageTaskPlans.BuildPackageFor(Source, Backend == BackendRegistry.LppId && !Directory.Exists(Source)), + Extract => PackageTaskPlans.Extract, + Verify => PackageTaskPlans.Verify, + RefreshAmpr => PackageTaskPlans.Ampr, + _ => PackageTaskPlans.Single + }; + + public string TaskType => Kind switch + { + Convert => PackageTaskTypes.ImageConvert, + BuildPackage => PackageTaskTypes.ImageBuildPackage, + Extract => PackageTaskTypes.PackageExtract, + Verify => FromPackage ? PackageTaskTypes.PackageVerify : PackageTaskTypes.ImageVerify, + Repair => PackageTaskTypes.ExfatRepair, + RefreshAmpr => PackageTaskTypes.ExfatAmpr, + EditDetails => PackageTaskTypes.EditDetails, + SendFtp or SendInstall => PackageTaskTypes.SendToConsole, + _ => PackageTaskTypes.FfpkgRebuild + }; + + private Ps5ImageFormat ImageFormat => Enum.TryParse(Format, out Ps5ImageFormat parsed) ? parsed : Ps5ImageFormat.Unknown; + + /// The delegate that runs this job inside the task queue. + public Func, CancellationToken, Task>? Executor() + { + if (Source.Length == 0) return null; + switch (Kind) + { + case Convert: + if (Output.Length == 0 || !Enum.TryParse(Target, out Ps5ImageConversionTarget target)) return null; + return (progress, token) => Jobs.ConvertAsync(Source, Output, target, Overwrite, FromPackage, progress, token, + target is Ps5ImageConversionTarget.Ffpkg or Ps5ImageConversionTarget.ZArchive + ? null + : new ExfatBuildOptions { ClusterSize = ClusterSize > 0 ? ClusterSize : null, GenerateAmprIndex = GenerateAmpr }, + target == Ps5ImageConversionTarget.Ffpfsc + ? new FfpfscBuildOptions + { + Compression = new PfscCompressionOptions { CompressionLevel = CompressionLevel, MinimumGainPercent = MinimumGain } + } + : null, + target == Ps5ImageConversionTarget.Ffpkg + ? new FfpkgBuildOptions + { + BlockSize = BlockSize, + FragmentSize = FragmentSize, + BytesPerInode = BytesPerInode, + MinFreePercent = MinFreePercent + } + : null); + case BuildPackage: + 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); + IPackageBackend backend = Jobs.ResolveBackend(Backend); + string passcode = Passcode.Length > 0 ? Passcode : SonyDebugPackageCredentials.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; + return async (progress, token) => + await Jobs.ExtractPackageAsync(Source, Output, code, progress, token).ConfigureAwait(false); + } + Ps5ImageFormat extractFormat = ImageFormat; + return (progress, token) => Jobs.ExtractImageAsync(Source, extractFormat, Output, progress, token); + case Verify: + if (FromPackage) + { + string code = Passcode.Length > 0 ? Passcode : SonyDebugPackageCredentials.DefaultPasscode; + return (progress, token) => + { + SonyDebugPackageValidationResult result = Jobs.VerifyPackage(Source, code); + if (!result.IsValid) throw new InvalidDataException(result.Message); + Logger.Info($"Package verified: {result.IndexedFiles:N0} indexed file(s)."); + return Task.CompletedTask; + }; + } + Ps5ImageFormat verifyFormat = ImageFormat; + return (progress, token) => Jobs.VerifyImageAsync(Source, verifyFormat, token); + case Repair: + return async (progress, token) => + await ExfatImageMaintenance.RepairAsync(Source, Jobs.Adapt(progress), token).ConfigureAwait(false); + case RefreshAmpr: + return async (progress, token) => + await ExfatAmprPatcher.RefreshAsync(Source, Jobs.Adapt(progress), token).ConfigureAwait(false); + case RebuildFfpkg: + return async (progress, token) => + await Ufs2Operations.RebuildWithEditsAsync(Source, static _ => { }, Jobs.AdaptUfs2(progress), token) + .ConfigureAwait(false); + case EditDetails: + if (Edit is not { IsEmpty: false } edit) return null; + string editCode = Passcode.Length > 0 ? Passcode : SonyDebugPackageCredentials.DefaultPasscode; + return (progress, token) => MetadataEditor.ApplyAsync(Source, Output, FromPackage, edit, editCode, + Backend, progress, token); + case SendFtp: + if (Console is null) return null; + ConsoleProfile ftpTarget = Console; + string folder = RemoteFolder.Length > 0 ? RemoteFolder : ftpTarget.DefaultFolder; + return (progress, token) => ConsoleSender.UploadAsync(ftpTarget, Source, folder, progress, token); + case SendInstall: + if (Console is null) return null; + ConsoleProfile installTarget = Console; + return (progress, token) => ConsoleSender.InstallByUrlAsync(installTarget, Source, progress, token); + default: + return null; + } + } +} diff --git a/src/Packwright.App/Services/Jobs.cs b/src/Packwright.App/Services/Jobs.cs new file mode 100644 index 0000000..5d7ffd9 --- /dev/null +++ b/src/Packwright.App/Services/Jobs.cs @@ -0,0 +1,325 @@ +using Packwright.Core.Backends; +using Packwright.Core.Builders; +using Packwright.Core.Services; +using Packwright.Core.Tasks; +using Packwright.Containers; +using Packwright.Infrastructure; +using UFS2Tool; + +namespace Packwright.App.Services; + +/// Settings for a debug package (FPKG) build. +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); + +/// +/// The long-running operations behind the Tools tab. Each method is UI-free and reports progress +/// through , so it can run inside the shared task queue. +/// +public static class Jobs +{ + public static IProgress NoProgress { get; } = new Progress(); + + public static IProgress Adapt(IProgress target) => + new Progress(value => target.Report(new PackageTaskProgress( + value.Stage, 0, 0, value.BytesProcessed, value.TotalBytes, 0, 0, string.Empty))); + + public static IProgress AdaptUfs2(IProgress target) => + new Progress(value => target.Report(new PackageTaskProgress( + value.Stage, 0, 0, value.BytesProcessed, value.TotalBytes, + checked((int)Math.Min(value.Completed, int.MaxValue)), + checked((int)Math.Min(value.Total, int.MaxValue)), value.Unit))); + + public static IProgress AdaptConversion(IProgress target) => + 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( + "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) + { + IPackageBackend backend = BackendRegistry.Get(id); + if (backend.Id == BackendRegistry.LppId && !PlatformSupportsLpp) + backend = BackendRegistry.Get(BackendRegistry.PptId); + return backend; + } + + public static bool PlatformSupportsLpp => OperatingSystem.IsWindows() && + File.Exists(Path.Combine(AppContext.BaseDirectory, "LibProsperoPkg12", "LibProsperoPkg.dll")); + + public static Task ConvertAsync(string source, string output, Ps5ImageConversionTarget target, bool overwrite, + bool fromPackage, IProgress progress, CancellationToken token, + ExfatBuildOptions? exfatOptions = null, FfpfscBuildOptions? ffpfscOptions = null, + FfpkgBuildOptions? ffpkgOptions = null) + { + IProgress bridge = AdaptConversion(progress); + return fromPackage + ? SonyPackageImageConversion.ConvertAsync(source, output, target, overwrite, bridge, token, + exfatOptions, ffpfscOptions, ffpkgOptions) + : Ps5ImageConversionService.ConvertAsync(source, output, target, overwrite, bridge, token, + exfatOptions, ffpfscOptions, ffpkgOptions); + } + + public static async Task ExtractImageAsync(string source, Ps5ImageFormat format, string output, + IProgress progress, CancellationToken token) + { + switch (format) + { + case Ps5ImageFormat.Exfat: + await ExfatImage.ExtractDirectoryAsync(source, output, Adapt(progress), token).ConfigureAwait(false); + break; + case Ps5ImageFormat.Ufs2: + await Ufs2Operations.ExtractAsync(source, output, AdaptUfs2(progress), token).ConfigureAwait(false); + break; + case Ps5ImageFormat.Pfs: + await FfpfscImage.ExtractToDirectoryAsync(source, output, overwrite: true, Adapt(progress), token) + .ConfigureAwait(false); + break; + case Ps5ImageFormat.ZArchive: + await ZArchiveImage.ExtractToDirectoryAsync(source, output, overwrite: true, Adapt(progress), token) + .ConfigureAwait(false); + break; + default: + throw new InvalidDataException("Unsupported source image format."); + } + } + + public static async Task VerifyImageAsync(string source, Ps5ImageFormat format, CancellationToken token) + { + switch (format) + { + case Ps5ImageFormat.Exfat: + await ExfatImage.VerifyAsync(source, null, token).ConfigureAwait(false); + break; + case Ps5ImageFormat.Ufs2: + await Ufs2Operations.VerifyAsync(source, null, token).ConfigureAwait(false); + break; + case Ps5ImageFormat.Pfs: + FfpfscVerificationResult result = await FfpfscImage.TryVerifyAsync(source, null, null, token) + .ConfigureAwait(false); + if (!result.StructureValid) + throw new InvalidDataException("The FFPFSC structure is invalid. " + (result.Error ?? string.Empty)); + if (!result.EveryPfscBlockDecodes) + throw new InvalidDataException("The FFPFSC block decode failed. " + (result.Error ?? string.Empty)); + break; + case Ps5ImageFormat.ZArchive: + await ZArchiveImage.VerifyAsync(source, null, token).ConfigureAwait(false); + break; + default: + throw new InvalidDataException("Unsupported image format."); + } + } + + public static async Task ExtractPackageAsync(string source, string output, + string passcode, IProgress progress, CancellationToken token) => + await SonyPackageExtraction.ExtractAsync(source, output, passcode, AdaptExtract(progress), token) + .ConfigureAwait(false); + + public static SonyDebugPackageValidationResult VerifyPackage(string source, string passcode) => + SonyDebugPackageBuilder.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) + { + byte[]? bytes = TryReadParamBytes(source, format); + if (bytes is null || bytes.Length == 0) return string.Empty; + try + { + using System.Text.Json.JsonDocument document = System.Text.Json.JsonDocument.Parse(bytes); + return document.RootElement.ValueKind == System.Text.Json.JsonValueKind.Object && + document.RootElement.TryGetProperty("contentId", out System.Text.Json.JsonElement element) && + element.ValueKind == System.Text.Json.JsonValueKind.String + ? element.GetString() ?? string.Empty + : string.Empty; + } + catch (System.Text.Json.JsonException) + { + return string.Empty; + } + } + + private static byte[]? TryReadParamBytes(string source, Ps5ImageFormat format) + { + try + { + if (Directory.Exists(source)) + { + string path = Path.Combine(source, "sce_sys", "param.json"); + return File.Exists(path) ? File.ReadAllBytes(path) : null; + } + switch (format) + { + case Ps5ImageFormat.Exfat: + using (var volume = new ExfatVolume(File.OpenRead(source))) + return volume.ReadAllBytes("sce_sys/param.json", 4 * 1024 * 1024); + case Ps5ImageFormat.Ufs2: + using (var volume = new Ufs2Volume(source)) + return volume.ReadAllBytes("sce_sys/param.json", 4 * 1024 * 1024); + case Ps5ImageFormat.Pfs: + using (var volume = FfpfscVolume.Open(source)) + return volume.ReadAllBytes("sce_sys/param.json", 4 * 1024 * 1024); + case Ps5ImageFormat.ZArchive: + using (var volume = new ZArchiveVolume(source)) + { + string param = volume.Entries.FirstOrDefault(entry => !entry.IsDirectory && + (entry.Path.Equals("sce_sys/param.json", StringComparison.OrdinalIgnoreCase) || + entry.Path.EndsWith("/sce_sys/param.json", StringComparison.OrdinalIgnoreCase)))?.Path ?? string.Empty; + return param.Length == 0 ? null : volume.ReadAllBytes(param, 4 * 1024 * 1024); + } + default: + return null; + } + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidDataException or + ArgumentException or NotSupportedException) + { + return null; + } + } + + private static byte[] DeterministicSeed(string contentId, string passcode) => + System.Security.Cryptography.SHA256.HashData( + System.Text.Encoding.UTF8.GetBytes(contentId + "\0" + passcode))[..16]; + + public static async Task BuildPackageAsync(string source, string output, string contentId, string passcode, + bool overwrite, PackageBuildSettings settings, IPackageBackend backend, + IProgress progress, CancellationToken token) + { + if (!overwrite && File.Exists(output)) + throw new IOException($"The output file already exists: {output}"); + string outputDirectory = Path.GetDirectoryName(Path.GetFullPath(output)) ?? Directory.GetCurrentDirectory(); + Directory.CreateDirectory(outputDirectory); + 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 + { + 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 + }; + var bridge = new Progress(value => + progress.Report(new PackageTaskProgress(value.Stage, 0, 0, value.CompletedBytes, value.TotalBytes, + 0, 0, value.CurrentPath))); + 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); + } + + PackageReaderVerificationResult result = PackageReaderVerification.Inspect(partial, passcode); + Logger.Info($"Verified built package: {result.FileCount:N0} file(s), eboot.bin " + + (result.HasEboot ? "present" : "missing") + "."); + if (result.FileCount == 0 || !result.HasEboot) + throw new InvalidDataException("The built package failed verification: " + + (result.FileCount == 0 ? "the filesystem is empty." : "eboot.bin is missing.")); + token.ThrowIfCancellationRequested(); + File.Move(partial, output, overwrite); + } + catch + { + TryDeleteFile(partial); + throw; + } + finally + { + TryDeleteDirectory(jobDirectory); + if (staging is not null) TryDeleteDirectory(staging); + } + } + + private static long ZArchiveLogicalBytes(string path) + { + using var volume = new ZArchiveVolume(path); + return volume.TotalFileBytes; + } + + public static string FindAvailablePath(string preferredPath) + { + string fullPath = Path.GetFullPath(preferredPath); + if (!Directory.Exists(fullPath) && !File.Exists(fullPath)) return fullPath; + for (int index = 2; index < 10_000; index++) + { + string candidate = fullPath + $" ({index})"; + if (!Directory.Exists(candidate) && !File.Exists(candidate)) return candidate; + } + throw new IOException("Unable to find an available output name."); + } + + private static void TryDeleteFile(string path) + { + try { if (File.Exists(path)) File.Delete(path); } + catch (IOException) { } + catch (UnauthorizedAccessException) { } + } + + private static void TryDeleteDirectory(string path) + { + try { if (Directory.Exists(path)) Directory.Delete(path, recursive: true); } + catch (IOException) { } + catch (UnauthorizedAccessException) { } + } +} diff --git a/src/Packwright.App/Services/LibraryActions.cs b/src/Packwright.App/Services/LibraryActions.cs new file mode 100644 index 0000000..f7e51e0 --- /dev/null +++ b/src/Packwright.App/Services/LibraryActions.cs @@ -0,0 +1,310 @@ +using System.Collections; +using System.Reflection; +using System.Security.Cryptography; +using System.Text; +using Packwright.App.Models; +using Packwright.Core.Models; +using Packwright.Core.Services; +using Packwright.Infrastructure; + +namespace Packwright.App.Services; + +/// One planned rename: the source, where it would land and whether a numbered name was needed. +public sealed record RenamePlan(Ps5GameInfo Game, string Source, string Target, bool Conflict, string? Error) +{ + public bool Unchanged => Error is null && Target.Length == 0; +} + +/// File operations behind the library context menu: rename, move, delete, export and duplicates. +public static class LibraryActions +{ + public static string DefaultRenameFormat(AppSettings settings) => + string.IsNullOrWhiteSpace(settings.RenameFormat) ? AppSettingsNormalizer.DefaultRenameFormat : settings.RenameFormat.Trim(); + + private static Ps5RenameTokens Tokens(Ps5GameInfo game) => new( + Title: game.Title, + TitleId: game.TitleId, + ContentId: game.ContentId, + ConceptId: game.ConceptId, + Version: game.DisplayVersion, + ContentVersion: game.ContentVersion, + MasterVersion: game.MasterVersion, + Category: LibraryItem.CategoryOf(game), + Region: LibraryItem.RegionOf(game), + Platform: game.Platform, + SystemVersion: game.RequiredSystemSoftware, + SdkVersion: game.SdkVersion, + Source: game.SourceDescription, + Size: game.SourceSize > 0 ? LibraryItem.FormatBytes(game.SourceSize) : string.Empty, + Language: game.DefaultLanguage, + Drm: game.DrmType, + Date: game.CreationDate, + Tool: game.ToolVersion); + + public static IReadOnlyList PlanRenames(IEnumerable games, string format) => + games.Select(game => Resolve(game, Ps5RenameFormatter.Expand(format, Tokens(game), FallbackName(game)))).ToList(); + + private static string FallbackName(Ps5GameInfo game) + { + string name = Path.GetFileNameWithoutExtension(game.RootPath); + return string.IsNullOrWhiteSpace(name) ? "PS5_GAME" : name; + } + + private static RenamePlan Resolve(Ps5GameInfo game, string baseName) + { + string source = game.RootPath; + bool isDirectory = Directory.Exists(source); + if (!isDirectory && !File.Exists(source)) + return new RenamePlan(game, source, string.Empty, false, "source path no longer exists"); + string safe = Ps5RenameFormatter.Sanitize(baseName); + if (safe.Length == 0) return new RenamePlan(game, source, string.Empty, false, "empty name"); + string? parent = Path.GetDirectoryName(source.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)); + if (string.IsNullOrEmpty(parent)) return new RenamePlan(game, source, string.Empty, false, "no parent folder"); + string extension = isDirectory ? string.Empty : Path.GetExtension(source); + string raw = Path.Combine(parent, safe + extension); + if (string.Equals(raw, source, StringComparison.OrdinalIgnoreCase)) + return new RenamePlan(game, source, string.Empty, false, null); + bool conflict = File.Exists(raw) || Directory.Exists(raw); + return new RenamePlan(game, source, conflict ? MakeUnique(raw, isDirectory) : raw, conflict, null); + } + + /// + /// Numbers packages per Title ID in install order (base game, then updates, then add-ons), so sorting + /// the files by name installs them in the right order. + /// + public static IReadOnlyList PlanInstallOrder(IEnumerable games, string format) + { + var plans = new List(); + foreach (IGrouping group in games + .Where(game => game.SourceKind == Ps5SourceKind.SonyPackage && !string.IsNullOrWhiteSpace(game.TitleId)) + .GroupBy(game => game.TitleId, StringComparer.OrdinalIgnoreCase) + .OrderBy(group => group.Key, StringComparer.OrdinalIgnoreCase)) + { + List ordered = group + .OrderBy(game => CategoryPriority(LibraryItem.CategoryOf(game))) + .ThenBy(game => game.DisplayVersion, StringComparer.OrdinalIgnoreCase) + .ThenBy(game => game.RootPath, StringComparer.OrdinalIgnoreCase) + .ToList(); + for (int index = 0; index < ordered.Count; index++) + { + Ps5GameInfo game = ordered[index]; + string name = Ps5RenameFormatter.Expand(format, Tokens(game), FallbackName(game)); + plans.Add(Resolve(game, $"{index:D2} - {name}")); + } + } + return plans; + } + + private static int CategoryPriority(string category) => category switch + { + "Game" => 0, + "Patch" => 1, + "DLC" or "Add-on" => 2, + "App" => 3, + _ => 4 + }; + + /// Lists updates and add-ons whose Title ID has no base game in the library. + public static string MissingBaseReport(IEnumerable items) + { + List all = items.ToList(); + HashSet bases = all.Where(item => item.Category == "Game" && item.TitleId.Length > 0) + .Select(item => item.TitleId).ToHashSet(StringComparer.OrdinalIgnoreCase); + var text = new StringBuilder(); + foreach (LibraryItem item in all + .Where(item => item.Category is "Patch" or "DLC" or "Add-on" && item.TitleId.Length > 0 && + !bases.Contains(item.TitleId)) + .OrderBy(item => item.TitleId, StringComparer.OrdinalIgnoreCase)) + text.AppendLine($"{item.TitleId} {item.Category,-6} v{item.Version,-9} {item.Title}\n {item.Path}"); + return text.Length == 0 ? "Every update and add-on has a base game in the library." : text.ToString(); + } + + public static string MakeUnique(string target, bool isDirectory) + { + if (!File.Exists(target) && !Directory.Exists(target)) return target; + string parent = Path.GetDirectoryName(target) ?? string.Empty; + string name = isDirectory ? Path.GetFileName(target) : Path.GetFileNameWithoutExtension(target); + string extension = isDirectory ? string.Empty : Path.GetExtension(target); + for (int index = 2; index < 10000; index++) + { + string candidate = Path.Combine(parent, $"{name} ({index}){extension}"); + if (!File.Exists(candidate) && !Directory.Exists(candidate)) return candidate; + } + return target; + } + + public static string PreviewText(IReadOnlyList plans) + { + var text = new StringBuilder(); + foreach (RenamePlan plan in plans) + { + string name = Path.GetFileName(plan.Source.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)); + if (plan.Error is not null) text.AppendLine($"x {name} ({plan.Error})"); + else if (plan.Unchanged) text.AppendLine($"= {name} (unchanged)"); + else + { + text.AppendLine($" {name}"); + text.AppendLine($"-> {Path.GetFileName(plan.Target)}{(plan.Conflict ? " [name already taken]" : string.Empty)}"); + } + } + return text.ToString(); + } + + /// Applies the plans and returns how many items were renamed. + public static int ApplyRenames(IEnumerable plans) + { + int renamed = 0; + foreach (RenamePlan plan in plans.Where(plan => plan.Error is null && plan.Target.Length > 0)) + { + try + { + if (Directory.Exists(plan.Source)) Directory.Move(plan.Source, plan.Target); + else File.Move(plan.Source, plan.Target); + renamed++; + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or ArgumentException or NotSupportedException) + { + Logger.Warn($"Rename skipped for '{plan.Source}': {ex.Message}"); + } + } + return renamed; + } + + public static int MoveAll(IEnumerable games, string destination, CancellationToken token) + { + Directory.CreateDirectory(destination); + int moved = 0; + foreach (Ps5GameInfo game in games) + { + string source = game.RootPath; + bool isDirectory = Directory.Exists(source); + if (!isDirectory && !File.Exists(source)) continue; + string name = Path.GetFileName(source.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)); + string target = MakeUnique(Path.Combine(destination, name), isDirectory); + try + { + LibraryFileMover.Move(source, target, token); + moved++; + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + Logger.Warn($"Move skipped for '{source}': {ex.Message}"); + } + } + return moved; + } + + public static int DeleteAll(IEnumerable games) + { + int deleted = 0; + foreach (Ps5GameInfo game in games) + { + try + { + if (Directory.Exists(game.RootPath)) Directory.Delete(game.RootPath, recursive: true); + else if (File.Exists(game.RootPath)) File.Delete(game.RootPath); + else continue; + deleted++; + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + Logger.Warn($"Delete skipped for '{game.RootPath}': {ex.Message}"); + } + } + return deleted; + } + + public static string ToCsv(IEnumerable items) + { + var text = new StringBuilder(); + text.AppendLine("Title,TitleId,ContentId,Category,Region,Format,SizeBytes,Version,RequiredFirmware,DRM,FileName,Path"); + foreach (LibraryItem item in items) + text.AppendLine(string.Join(",", new[] + { + item.Title, item.TitleId, item.ContentId, item.Category, item.Region, item.Format, + item.SizeBytes.ToString(), item.Version, item.Firmware, item.Game.DrmType, item.FileName, item.Path + }.Select(Csv))); + return text.ToString(); + } + + private static string Csv(string value) + { + if (value.Length > 0 && value[0] is '=' or '+' or '-' or '@') value = "'" + value; + return value.IndexOfAny([',', '"', '\n', '\r']) >= 0 ? "\"" + value.Replace("\"", "\"\"") + "\"" : value; + } + + /// Groups items that share the same param.json bytes (exact copies) or the same content ID. + public static string DuplicatesReport(IEnumerable items) + { + var text = new StringBuilder(); + int groups = 0; + foreach (IGrouping group in items + .Where(item => item.Game.RawParamJson.Length > 0) + .GroupBy(item => Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(item.Game.RawParamJson)))) + .Where(group => group.Count() > 1)) + { + groups++; + text.AppendLine($"Identical param.json ({group.Count()} copies): {group.First().Title}"); + foreach (LibraryItem item in group) text.AppendLine(" " + item.Path); + } + var identical = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (IGrouping group in items + .Where(item => item.ContentId.Length > 0) + .GroupBy(item => item.ContentId + "|" + item.Version, StringComparer.OrdinalIgnoreCase) + .Where(group => group.Count() > 1)) + { + if (group.Select(item => item.Path).All(identical.Contains)) continue; + groups++; + text.AppendLine($"Same content ID and version ({group.Count()}): {group.First().Title}"); + foreach (LibraryItem item in group) text.AppendLine(" " + item.Path); + } + return groups == 0 ? "No duplicates found." : text.ToString(); + } + + /// Renders a parsed model as readable "name: value" lines (lists are summarised and truncated). + public static string Describe(object? value, int maxListItems = 40) + { + if (value is null) return "Nothing to show."; + var text = new StringBuilder(); + Write(text, value, 0, maxListItems); + return text.ToString(); + } + + private static void Write(StringBuilder text, object value, int depth, int maxItems) + { + string indent = new(' ', depth * 2); + foreach (PropertyInfo property in value.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + if (property.GetIndexParameters().Length > 0) continue; + object? item; + try { item = property.GetValue(value); } + catch (TargetInvocationException) { continue; } + if (item is null || item is byte[]) continue; + if (item is string or bool or IFormattable || item.GetType().IsEnum) + { + text.AppendLine($"{indent}{property.Name}: {item}"); + } + else if (item is IEnumerable sequence) + { + List list = sequence.Cast().ToList(); + text.AppendLine($"{indent}{property.Name}: {list.Count} item(s)"); + if (depth >= 1) continue; + foreach (object entry in list.Take(maxItems)) + { + if (entry is string or IFormattable) text.AppendLine($"{indent} - {entry}"); + else + { + text.AppendLine($"{indent} -"); + Write(text, entry, depth + 2, maxItems); + } + } + if (list.Count > maxItems) text.AppendLine($"{indent} ... {list.Count - maxItems} more"); + } + else if (depth < 1) + { + text.AppendLine($"{indent}{property.Name}:"); + Write(text, item, depth + 1, maxItems); + } + } + } +} diff --git a/src/Packwright.App/Services/LibraryExport.cs b/src/Packwright.App/Services/LibraryExport.cs new file mode 100644 index 0000000..21aa31b --- /dev/null +++ b/src/Packwright.App/Services/LibraryExport.cs @@ -0,0 +1,152 @@ +using System.Net; +using System.Text; +using System.Text.Json; +using Avalonia.Media.Imaging; +using Packwright.App.Models; +using Packwright.Core.Services; + +namespace Packwright.App.Services; + +/// Writes the library as a JSON file or as one self-contained web page. Nothing here uses the network. +public static class LibraryExport +{ + private const int IconWidth = 96; + + private static readonly JsonSerializerOptions JsonOptions = new() + { + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull + }; + + private sealed record Row(string Title, string TitleId, string ContentId, string Category, string Role, string Region, + string Format, long SizeBytes, string Version, string Firmware, string Drm, string Sdk, string FileName, + bool Edited, string[] Missing, string? Path); + + private static Row ToRow(LibraryItem item, bool includePaths) => new( + item.Title, item.TitleId, item.ContentId, item.Category, item.Role, item.Region, item.Format, item.SizeBytes, + item.Version, item.Firmware, item.Game.DrmType, item.Game.SdkVersion, item.FileName, item.IsEdited, + [.. item.MissingFields], includePaths ? item.Path : null); + + public static string ToJson(IReadOnlyCollection items, bool includePaths) + { + var document = new + { + generator = "Packwright " + UpdateService.CurrentVersion, + exportedUtc = DateTime.UtcNow, + count = items.Count, + totalBytes = items.Sum(item => item.SizeBytes), + items = items.Select(item => ToRow(item, includePaths)).ToList() + }; + return JsonSerializer.Serialize(document, JsonOptions); + } + + /// A small PNG of the title's artwork for the web page, or null when it has none. + public static byte[]? ReadSmallIcon(LibraryItem item) + { + try + { + if (ThumbnailService.ReadIcon(item) is not { Length: > 0 } bytes) return null; + using var input = new MemoryStream(bytes); + using Bitmap bitmap = Bitmap.DecodeToWidth(input, IconWidth, BitmapInterpolationMode.MediumQuality); + using var output = new MemoryStream(); + bitmap.Save(output); + return output.ToArray(); + } + catch (Exception ex) when (ex is IOException or InvalidDataException or UnauthorizedAccessException + or NotSupportedException or ArgumentException or InvalidOperationException) + { + return null; + } + } + + public static string ToHtml(IReadOnlyCollection items, bool includePaths, + IReadOnlyDictionary? icons) + { + static string H(string? text) => WebUtility.HtmlEncode(text ?? string.Empty); + long total = items.Sum(item => item.SizeBytes); + var html = new StringBuilder(64 * 1024); + html.Append(""" + + + + + PS5 library +
+ """); + html.Append("

PS5 library

") + .Append(H($"Exported {DateTime.Now:d MMMM yyyy, HH:mm} with Packwright {UpdateService.CurrentVersion}")) + .Append("
"); + Stat(html, items.Count.ToString("N0"), "titles"); + Stat(html, Ps5LibraryHealth.FormatBytes(total), "total size"); + foreach (IGrouping group in items.GroupBy(item => item.Category.Length > 0 ? item.Category : "Other") + .OrderByDescending(group => group.Count())) + Stat(html, group.Count().ToString("N0"), group.Key); + int missing = items.Count(item => item.HasMissing); + if (missing > 0) Stat(html, missing.ToString("N0"), "with missing details"); + html.Append("
"); + html.Append("
") + .Append(""); + if (includePaths) html.Append(""); + html.Append(""); + foreach (LibraryItem item in items.OrderBy(item => item.Title, StringComparer.CurrentCultureIgnoreCase)) + { + html.Append(""); + if (includePaths) html.Append(""); + html.Append(""); + } + html.Append(""" +
TitleCategoryRegionFormatVersionFirmwareSizePath
"); + if (icons is not null && icons.TryGetValue(item.Path, out byte[]? png)) + html.Append("\"\""); + else html.Append("
"); + html.Append("
").Append(H(item.Title)).Append("
") + .Append(H(item.TitleId.Length > 0 ? item.TitleId : item.FileName)); + if (item.ContentId.Length > 0) html.Append(" · ").Append(H(item.ContentId)); + html.Append("
"); + if (item.HasMissing) html.Append("
").Append(H(item.MissingText)).Append("
"); + html.Append("
").Append(H(item.Category)).Append("").Append(H(item.Region)) + .Append("").Append(H(item.Format)).Append("").Append(H(item.Version)).Append("") + .Append(H(item.Firmware)).Append("") + .Append(H(item.SizeText)).Append("").Append(H(item.Path)).Append("
+
Made with Packwright. This page is a single file; it loads nothing from the internet.
+
+ """); + return html.ToString(); + } + + private static void Stat(StringBuilder html, string value, string label) => + html.Append("
").Append(WebUtility.HtmlEncode(value)).Append("") + .Append(WebUtility.HtmlEncode(label)).Append("
"); +} diff --git a/src/Packwright.App/Services/LibraryQuery.cs b/src/Packwright.App/Services/LibraryQuery.cs new file mode 100644 index 0000000..15dbfab --- /dev/null +++ b/src/Packwright.App/Services/LibraryQuery.cs @@ -0,0 +1,182 @@ +using System.Globalization; +using Packwright.App.Models; + +namespace Packwright.App.Services; + +/// +/// The library search grammar: plain words, quoted phrases, field:value terms +/// (title id content category role region version fw feature path size), -exclude, +/// | for OR and = after the colon for an exact match (id:=PPSA12345). +/// Comparisons work on size and version (size:>50GB, version:>=01.05). +/// +public sealed class LibraryQuery +{ + private sealed record Term(string Field, string Value, bool Exclude, bool Exact, string Op); + + private readonly List> _alternatives = []; + + public List Problems { get; } = []; + + public bool IsEmpty => _alternatives.Count == 0 || _alternatives.All(group => group.Count == 0); + + public static LibraryQuery Parse(string? text) + { + var query = new LibraryQuery(); + var current = new List(); + query._alternatives.Add(current); + foreach (string token in Tokenize(text ?? string.Empty)) + { + if (token == "|") + { + current = []; + query._alternatives.Add(current); + continue; + } + string body = token; + bool exclude = body.Length > 1 && body[0] == '-'; + if (exclude) body = body[1..]; + string field = string.Empty; + int colon = body.IndexOf(':'); + if (colon > 0 && IsField(body[..colon])) + { + field = body[..colon].ToLowerInvariant(); + body = body[(colon + 1)..]; + } + else if (colon > 0 && body[..colon].All(char.IsLetter) && !body.Contains(' ')) + { + query.Problems.Add(body[..colon]); + } + string op = string.Empty; + bool exact = false; + if (body.StartsWith('=')) { exact = true; body = body[1..]; } + else + { + foreach (string candidate in new[] { ">=", "<=", ">", "<" }) + if (body.StartsWith(candidate, StringComparison.Ordinal)) { op = candidate; body = body[candidate.Length..]; break; } + } + if (body.Length == 0) continue; + current.Add(new Term(field, body, exclude, exact, op)); + } + return query; + } + + public bool Matches(LibraryItem item) + { + if (IsEmpty) return true; + return _alternatives.Any(group => group.All(term => Evaluate(term, item))); + } + + private bool Evaluate(Term term, LibraryItem item) + { + bool result = term.Field switch + { + "" => Any(term, item.Title, item.TitleId, item.ContentId, item.Version, item.Category, item.Path, + item.Region, item.Format), + "title" => Text(term, item.Title), + "id" => Text(term, item.TitleId), + "content" => Text(term, item.ContentId), + "category" => Text(term, item.Category), + "role" => Text(term, item.Role), + "region" => Text(term, item.Region), + "format" or "source" => Text(term, item.Format), + "path" => Text(term, item.Path), + "version" => Compare(term, item.Version), + "fw" => Compare(term, item.Firmware), + "feature" => item.Game.DeclaredFeatures.Any(feature => Text(term, feature)), + "missing" => item.HasMissing, + "size" => CompareSize(term, item.SizeBytes), + _ => true + }; + return term.Exclude ? !result : result; + } + + private static bool Any(Term term, params string[] values) => values.Any(value => Text(term, value)); + + private static bool Text(Term term, string value) => term.Exact + ? value.Equals(term.Value, StringComparison.OrdinalIgnoreCase) + : value.Contains(term.Value, StringComparison.OrdinalIgnoreCase); + + private static bool Compare(Term term, string value) + { + if (term.Op.Length == 0) return Text(term, value); + int order = CompareVersions(value, term.Value); + return Apply(term.Op, order); + } + + private static bool CompareSize(Term term, long bytes) + { + if (!TryParseSize(term.Value, out long wanted)) return false; + return term.Op.Length == 0 ? bytes == wanted : Apply(term.Op, bytes.CompareTo(wanted)); + } + + private static bool Apply(string op, int order) => op switch + { + ">" => order > 0, + ">=" => order >= 0, + "<" => order < 0, + "<=" => order <= 0, + _ => order == 0 + }; + + private static int CompareVersions(string left, string right) + { + string[] a = left.Split('.', StringSplitOptions.RemoveEmptyEntries); + string[] b = right.Split('.', StringSplitOptions.RemoveEmptyEntries); + for (int i = 0; i < Math.Max(a.Length, b.Length); i++) + { + bool okA = i < a.Length && double.TryParse(a[i], NumberStyles.Float, CultureInfo.InvariantCulture, out _); + bool okB = i < b.Length && double.TryParse(b[i], NumberStyles.Float, CultureInfo.InvariantCulture, out _); + double x = okA ? double.Parse(a[i], CultureInfo.InvariantCulture) : 0; + double y = okB ? double.Parse(b[i], CultureInfo.InvariantCulture) : 0; + if (x != y) return x.CompareTo(y); + } + return 0; + } + + private static bool TryParseSize(string text, out long bytes) + { + bytes = 0; + text = text.Trim(); + int split = text.Length; + while (split > 0 && !char.IsDigit(text[split - 1]) && text[split - 1] != '.') split--; + if (!double.TryParse(text[..split], NumberStyles.Float, CultureInfo.InvariantCulture, out double number)) return false; + double factor = text[split..].Trim().ToUpperInvariant() switch + { + "" or "B" => 1, + "K" or "KB" or "KIB" => 1024d, + "M" or "MB" or "MIB" => 1024d * 1024, + "G" or "GB" or "GIB" => 1024d * 1024 * 1024, + "T" or "TB" or "TIB" => 1024d * 1024 * 1024 * 1024, + _ => -1 + }; + if (factor < 0) return false; + bytes = (long)(number * factor); + return true; + } + + private static bool IsField(string name) => name.ToLowerInvariant() is "title" or "id" or "content" or "category" + or "role" or "region" or "format" or "source" or "path" or "version" or "fw" or "feature" or "size"; + + private static IEnumerable Tokenize(string text) + { + var token = new System.Text.StringBuilder(); + bool quoted = false; + foreach (char ch in text) + { + if (ch == '"') { quoted = !quoted; continue; } + if (!quoted && char.IsWhiteSpace(ch)) + { + if (token.Length > 0) { yield return token.ToString(); token.Clear(); } + continue; + } + if (!quoted && ch == '|') + { + if (token.Length > 0) { yield return token.ToString(); token.Clear(); } + yield return "|"; + continue; + } + token.Append(ch); + } + if (token.Length > 0) yield return token.ToString(); + } +} diff --git a/src/Packwright.App/Services/MemoryTrimmer.cs b/src/Packwright.App/Services/MemoryTrimmer.cs new file mode 100644 index 0000000..52cb90c --- /dev/null +++ b/src/Packwright.App/Services/MemoryTrimmer.cs @@ -0,0 +1,49 @@ +using System.Runtime; +using System.Runtime.InteropServices; +using Avalonia.Threading; + +namespace Packwright.App.Services; + +/// +/// Gives memory back after bursts of work (loading a game's details, scanning, switching pages): a short +/// idle delay, then a full collection that also compacts the large-object heap, and on Windows a working-set +/// trim so the figure Task Manager shows reflects what the app really needs. +/// +public static class MemoryTrimmer +{ + private static DispatcherTimer? _timer; + + /// Trims once the app has been quiet for ; repeated calls restart the wait. + public static void Schedule(int seconds = 4) + { + if (!Dispatcher.UIThread.CheckAccess()) + { + Dispatcher.UIThread.Post(() => Schedule(seconds)); + return; + } + _timer?.Stop(); + _timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(seconds) }; + _timer.Tick += (_, _) => + { + _timer?.Stop(); + _ = Task.Run(TrimNow); + }; + _timer.Start(); + } + + public static void TrimNow() + { + GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce; + GC.Collect(2, GCCollectionMode.Forced, blocking: true, compacting: true); + GC.WaitForPendingFinalizers(); + GC.Collect(2, GCCollectionMode.Forced, blocking: true, compacting: true); + if (OperatingSystem.IsWindows()) + { + try { SetProcessWorkingSetSize(System.Diagnostics.Process.GetCurrentProcess().Handle, -1, -1); } + catch (Exception ex) when (ex is DllNotFoundException or EntryPointNotFoundException) { } + } + } + + [DllImport("kernel32.dll")] + private static extern bool SetProcessWorkingSetSize(IntPtr process, nint minimum, nint maximum); +} diff --git a/src/Packwright.App/Services/MetadataEditor.cs b/src/Packwright.App/Services/MetadataEditor.cs new file mode 100644 index 0000000..ef7136e --- /dev/null +++ b/src/Packwright.App/Services/MetadataEditor.cs @@ -0,0 +1,494 @@ +using System.Globalization; +using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Nodes; +using Packwright.Core.Backends; +using Packwright.Core.Tasks; +using Packwright.Containers; +using Packwright.Infrastructure; +using UFS2Tool; + +namespace Packwright.App.Services; + +/// The values to write into a title's param.json and icon. Empty values are left as they are. +/// The feature flags a title can declare, and which bit of param.json each one is. +public static class DeclaredFeatureFlags +{ + public static readonly (string Name, string Key, int Bit)[] All = + [ + ("HDR", "attribute", 29), ("120 Hz", "attribute3", 6), ("PS VR2 supported", "attribute3", 10), + ("PS VR2 required", "attribute3", 11), ("VRR", "attribute3", 18), ("VRR 120 Hz", "attribute3", 19), + ("VRR disabled", "attribute3", 20), ("PS5 Pro", "attribute3", 22), ("PS5 Pro 8K", "attribute3", 23), + ("Power Saver", "attribute4", 0) + ]; + + public static IEnumerable Names => All.Select(flag => flag.Name); +} + +public sealed class MetadataEdit +{ + public string Title { get; set; } = string.Empty; + public string TitleId { get; set; } = string.Empty; + public string ContentId { get; set; } = string.Empty; + public string Version { get; set; } = string.Empty; + public string Firmware { get; set; } = string.Empty; + /// A PNG file that becomes sce_sys/icon0.png. + public string IconPath { get; set; } = string.Empty; + public string Category { get; set; } = string.Empty; + public string ConceptId { get; set; } = string.Empty; + public string MasterVersion { get; set; } = string.Empty; + public string Sdk { get; set; } = string.Empty; + public string Drm { get; set; } = string.Empty; + public string DefaultLanguage { get; set; } = string.Empty; + public string Created { get; set; } = string.Empty; + public string ToolVersion { get; set; } = string.Empty; + /// The features to declare (see ). Null leaves them as they are. + public List? Features { get; set; } + + public bool IsEmpty => Title.Length == 0 && TitleId.Length == 0 && ContentId.Length == 0 && Version.Length == 0 && + Firmware.Length == 0 && IconPath.Length == 0 && Category.Length == 0 && ConceptId.Length == 0 && + MasterVersion.Length == 0 && Sdk.Length == 0 && Drm.Length == 0 && DefaultLanguage.Length == 0 && + Created.Length == 0 && ToolVersion.Length == 0 && Features is null; +} + +/// +/// Writes edited details into the file itself: param.json and icon0.png inside a dump folder, an exFAT or +/// FFPKG image, an FFPFSC image, a ZArchive or a debug package. Nothing is left half-written: images are +/// rebuilt beside the original and swapped in only after they verify. +/// +public static class MetadataEditor +{ + private const string ParamPath = "sce_sys/param.json"; + private const string IconPath = "sce_sys/icon0.png"; + private const int MaximumParamBytes = 4 * 1024 * 1024; + + private static readonly JsonSerializerOptions WriteOptions = new() + { + WriteIndented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + // ---------------------------------------------------------------- param.json + + /// + /// Returns with the edited fields changed and everything else kept, or a new + /// minimal param.json when there is none. + /// + public static byte[] PatchParam(byte[]? original, MetadataEdit edit) + { + JsonObject root; + if (original is null) + { + // The title has no param.json (a bare dump or archive): start a minimal one. + root = new JsonObject { ["applicationCategoryType"] = 0 }; + } + else + { + string text = Encoding.UTF8.GetString(original).TrimStart('\uFEFF').TrimEnd('\0'); + root = JsonNode.Parse(text, null, new JsonDocumentOptions + { + AllowTrailingCommas = true, + CommentHandling = JsonCommentHandling.Skip + }) as JsonObject ?? throw new InvalidDataException("param.json is not a JSON object."); + } + + if (edit.TitleId.Length > 0) root["titleId"] = edit.TitleId; + if (edit.ContentId.Length > 0) root["contentId"] = edit.ContentId; + if (edit.Version.Length > 0) + { + if (root.ContainsKey("contentVersion") || !root.ContainsKey("masterVersion") || edit.MasterVersion.Length > 0) + root["contentVersion"] = edit.Version; + else + root["masterVersion"] = edit.Version; + } + if (edit.Firmware.Length > 0) root["requiredSystemSoftwareVersion"] = FirmwareToParam(edit.Firmware, root["requiredSystemSoftwareVersion"]?.ToString()); + if (edit.Title.Length > 0) SetTitle(root, edit.Title); + if (edit.ConceptId.Length > 0) root["conceptId"] = edit.ConceptId; + if (edit.MasterVersion.Length > 0) root["masterVersion"] = edit.MasterVersion; + if (edit.Sdk.Length > 0) root["sdkVersion"] = FirmwareToParam(edit.Sdk, root["sdkVersion"]?.ToString()); + if (edit.Drm.Length > 0) root["applicationDrmType"] = edit.Drm; + if (edit.Category.Length > 0) root["applicationCategoryType"] = CategoryToParam(edit.Category); + if (edit.Created.Length > 0) PubTools(root)["creationDate"] = edit.Created; + if (edit.ToolVersion.Length > 0) PubTools(root)["toolVersion"] = edit.ToolVersion; + if (edit.DefaultLanguage.Length > 0) SetDefaultLanguage(root, edit.DefaultLanguage, edit.Title); + if (edit.Features is not null) ApplyFeatures(root, edit.Features); + return Encoding.UTF8.GetBytes(root.ToJsonString(WriteOptions)); + } + + private static JsonObject PubTools(JsonObject root) + { + if (root["pubtools"] is JsonObject existing) return existing; + var created = new JsonObject(); + root["pubtools"] = created; + return created; + } + + /// The number the app reads back as the category (the same mapping the library uses). + private static int CategoryToParam(string category) => category switch { "DLC" => 1, "Patch" => 2, "App" => 3, _ => 0 }; + + /// Makes the default language, adding an entry for it (with the title) when it has none. + private static void SetDefaultLanguage(JsonObject root, string language, string title) + { + if (root["localizedParameters"] is not JsonObject localized) + { + localized = new JsonObject(); + root["localizedParameters"] = localized; + } + string previous = localized["defaultLanguage"]?.ToString() ?? string.Empty; + localized["defaultLanguage"] = language; + if (localized[language] is JsonObject) return; + string name = title; + if (name.Length == 0 && previous.Length > 0 && localized[previous] is JsonObject old) name = old["titleName"]?.ToString() ?? string.Empty; + if (name.Length == 0) + foreach ((string key, JsonNode? value) in localized) + if (value is JsonObject other && other["titleName"]?.ToString() is { Length: > 0 } found) { name = found; break; } + localized[language] = new JsonObject { ["titleName"] = name }; + } + + /// Sets or clears the known feature bits and leaves every other bit as it was. + private static void ApplyFeatures(JsonObject root, IReadOnlyCollection features) + { + foreach (IGrouping word in DeclaredFeatureFlags.All.GroupBy(flag => flag.Key)) + { + long value = 0; + if (root[word.Key] is JsonValue existing && existing.TryGetValue(out long number)) value = number; + else if (root[word.Key]?.ToString() is { Length: > 0 } text && long.TryParse(text, out long parsed)) value = parsed; + long updated = value; + foreach ((string name, _, int bit) in word) + updated = features.Contains(name, StringComparer.OrdinalIgnoreCase) ? updated | (1L << bit) : updated & ~(1L << bit); + if (updated != value || root.ContainsKey(word.Key)) root[word.Key] = updated; + } + } + + private static void SetTitle(JsonObject root, string title) + { + if (root["localizedParameters"] is not JsonObject localized) + { + root["localizedParameters"] = new JsonObject + { + ["defaultLanguage"] = "en-US", + ["en-US"] = new JsonObject { ["titleName"] = title } + }; + return; + } + bool any = false; + foreach ((string name, JsonNode? value) in localized.ToList()) + { + if (name.Equals("defaultLanguage", StringComparison.OrdinalIgnoreCase) || value is not JsonObject language) continue; + language["titleName"] = title; + any = true; + } + if (!any) + { + string defaultLanguage = localized["defaultLanguage"]?.ToString() is { Length: > 0 } configured ? configured : "en-US"; + localized["defaultLanguage"] = defaultLanguage; + localized[defaultLanguage] = new JsonObject { ["titleName"] = title }; + } + } + + /// "4.50" becomes "0x0450000000000000". A value that already starts with 0x is used as written. + internal static string FirmwareToParam(string text, string? existing) + { + text = text.Trim(); + if (text.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) return text; + string[] parts = text.Split('.', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 0 || !int.TryParse(parts[0], NumberStyles.None, CultureInfo.InvariantCulture, out int major)) + throw new InvalidDataException($"'{text}' is not a firmware version such as 4.50."); + string minor = parts.Length > 1 ? parts[1] : "00"; + if (minor.Length == 1) minor += "0"; + if (minor.Length != 2 || !minor.All(char.IsAsciiHexDigit)) + throw new InvalidDataException($"'{text}' is not a firmware version such as 4.50."); + string head = $"{major:D2}{minor}"; + int total = existing is { Length: > 2 } && existing.StartsWith("0x", StringComparison.OrdinalIgnoreCase) + ? existing.Length - 2 + : 16; + return "0x" + head.PadRight(Math.Max(total, 4), '0'); + } + + // ---------------------------------------------------------------- entry point + + /// + /// Applies to . When is empty or the + /// same as the source the file is changed in place, otherwise a new file or folder is written. + /// + public static async Task ApplyAsync(string source, string output, bool fromPackage, MetadataEdit edit, + string passcode, string backendId, IProgress progress, CancellationToken token) + { + if (edit.IsEmpty) throw new InvalidOperationException("There is nothing to write."); + bool inPlace = output.Length == 0 || + string.Equals(Path.GetFullPath(output), Path.GetFullPath(source), StringComparison.OrdinalIgnoreCase); + string target = inPlace ? Path.GetFullPath(source) : Path.GetFullPath(output); + if (!inPlace && (File.Exists(target) || Directory.Exists(target))) + throw new IOException("The output already exists: " + target); + + string work = Path.Combine(Path.GetTempPath(), "Packwright-edit-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(work); + try + { + byte[]? icon = edit.IconPath.Length > 0 ? await File.ReadAllBytesAsync(edit.IconPath, token).ConfigureAwait(false) : null; + Logger.Info($"Writing edited details into {source}" + (inPlace ? string.Empty : " -> " + target)); + if (Directory.Exists(source)) + { + await EditFolderAsync(source, target, inPlace, edit, icon, progress, token).ConfigureAwait(false); + } + else if (fromPackage) + { + await EditPackageAsync(source, target, edit, icon, passcode, backendId, work, progress, token).ConfigureAwait(false); + } + else + { + switch (Ps5ImageFormatProbe.Detect(source)) + { + case Ps5ImageFormat.Exfat: + await EditCopyOrInPlaceAsync(source, target, inPlace, progress, + path => EditExfatAsync(path, edit, icon, work, progress, token)).ConfigureAwait(false); + break; + case Ps5ImageFormat.Ufs2: + await EditCopyOrInPlaceAsync(source, target, inPlace, progress, + path => EditUfs2Async(path, edit, icon, work, progress, token)).ConfigureAwait(false); + break; + case Ps5ImageFormat.Pfs: + await EditPfsAsync(source, target, edit, icon, work, progress, token).ConfigureAwait(false); + break; + case Ps5ImageFormat.ZArchive: + await EditZArchiveAsync(source, target, edit, icon, progress, token).ConfigureAwait(false); + break; + default: + throw new InvalidDataException("This file type cannot be edited."); + } + } + Logger.Info("Edited details written."); + } + finally + { + try { Directory.Delete(work, true); } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { } + } + } + + private static void Stage(IProgress progress, string text) => + progress.Report(new PackageTaskProgress(text, 0, 0, 0, 0, 0, 0, string.Empty)); + + // ---------------------------------------------------------------- dump folder + + private static async Task EditFolderAsync(string source, string target, bool inPlace, MetadataEdit edit, + byte[]? icon, IProgress progress, CancellationToken token) + { + string root = source; + if (!inPlace) + { + Stage(progress, "Copying folder"); + await Task.Run(() => CopyDirectory(source, target, token), token).ConfigureAwait(false); + root = target; + } + try + { + Stage(progress, "Writing details"); + string param = Path.Combine(root, "sce_sys", "param.json"); + byte[]? existing = File.Exists(param) ? await File.ReadAllBytesAsync(param, token).ConfigureAwait(false) : null; + 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); + } + catch when (!inPlace) + { + try { Directory.Delete(target, true); } catch (IOException) { } + throw; + } + } + + private static async Task WriteFileAsync(string path, byte[] data, CancellationToken token) + { + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + string temp = path + ".packwright-tmp"; + await File.WriteAllBytesAsync(temp, data, token).ConfigureAwait(false); + File.Move(temp, path, true); + } + + private static void CopyDirectory(string source, string target, CancellationToken token) + { + Directory.CreateDirectory(target); + foreach (string file in Directory.EnumerateFiles(source)) + { + token.ThrowIfCancellationRequested(); + File.Copy(file, Path.Combine(target, Path.GetFileName(file))); + } + foreach (string directory in Directory.EnumerateDirectories(source)) + CopyDirectory(directory, Path.Combine(target, Path.GetFileName(directory)), token); + } + + // ---------------------------------------------------------------- exFAT and FFPKG (UFS2) + + private static async Task EditCopyOrInPlaceAsync(string source, string target, bool inPlace, + IProgress progress, Func edit) + { + if (!inPlace) + { + Stage(progress, "Copying image"); + await Task.Run(() => File.Copy(source, target)).ConfigureAwait(false); + } + try + { + await edit(target).ConfigureAwait(false); + } + catch when (!inPlace) + { + try { File.Delete(target); } catch (IOException) { } + throw; + } + } + + private static async Task EditExfatAsync(string image, MetadataEdit edit, byte[]? icon, string work, + IProgress progress, CancellationToken token) + { + byte[]? param; + bool hasIcon, hasSys; + 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; + hasSys = volume.Find("sce_sys") is not null; + } + string paramFile = Path.Combine(work, "param.json"); + await File.WriteAllBytesAsync(paramFile, PatchParam(param, edit), token).ConfigureAwait(false); + var operations = new List(); + if (!hasSys) operations.Add(ExfatEditOperation.AddDirectory("sce_sys")); + operations.Add(param is null ? ExfatEditOperation.AddFile(ParamPath, paramFile) : ExfatEditOperation.Replace(ParamPath, paramFile)); + if (icon is not null) + { + 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)); + } + await ExfatImageMaintenance.ApplyEditsAsync(image, operations, Jobs.Adapt(progress), token).ConfigureAwait(false); + } + + private static async Task EditUfs2Async(string image, MetadataEdit edit, byte[]? icon, string work, + IProgress progress, CancellationToken token) + { + byte[]? param; + bool hasIcon, hasSys; + 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); + hasSys = Has("sce_sys", true); + } + string paramFile = Path.Combine(work, "param.json"); + await File.WriteAllBytesAsync(paramFile, PatchParam(param, edit), token).ConfigureAwait(false); + var operations = new List(); + if (!hasSys) operations.Add(Ufs2EditOperation.AddDirectory("sce_sys")); + operations.Add(param is null ? Ufs2EditOperation.AddFile(ParamPath, paramFile) : Ufs2EditOperation.Replace(ParamPath, paramFile)); + if (icon is not null) + { + 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)); + } + await Ufs2Operations.ApplyEditsAsync(image, operations, Jobs.AdaptUfs2(progress), token).ConfigureAwait(false); + } + + // ---------------------------------------------------------------- FFPFSC + + private static async Task EditPfsAsync(string source, string target, MetadataEdit edit, byte[]? icon, string work, + IProgress progress, CancellationToken token) + { + string temporary = Path.Combine(work, "image.exfat"); + Stage(progress, "Unpacking FFPFSC"); + await Jobs.ConvertAsync(source, temporary, Ps5ImageConversionTarget.Exfat, false, false, progress, token, + new ExfatBuildOptions { GenerateAmprIndex = true }).ConfigureAwait(false); + await EditExfatAsync(temporary, edit, icon, work, progress, token).ConfigureAwait(false); + + string partial = target + ".packwright-tmp"; + try + { + Stage(progress, "Building FFPFSC"); + await Jobs.ConvertAsync(temporary, partial, Ps5ImageConversionTarget.Ffpfsc, true, false, progress, token, + null, new FfpfscBuildOptions + { + Compression = new PfscCompressionOptions { CompressionLevel = 7, MinimumGainPercent = 1 } + }).ConfigureAwait(false); + token.ThrowIfCancellationRequested(); + await Jobs.VerifyImageAsync(partial, Ps5ImageFormat.Pfs, token).ConfigureAwait(false); + File.Move(partial, target, true); + } + finally + { + try { File.Delete(partial); } catch (IOException) { } + } + } + + // ---------------------------------------------------------------- ZArchive + + private static async Task EditZArchiveAsync(string source, string target, MetadataEdit edit, byte[]? icon, + IProgress progress, CancellationToken token) + { + var replacements = new Dictionary(StringComparer.OrdinalIgnoreCase); + using (var volume = new ZArchiveVolume(source)) + { + ZArchiveEntry[] found = volume.Entries.Where(entry => !entry.IsDirectory && + (entry.Path.Equals(ParamPath, StringComparison.OrdinalIgnoreCase) || + entry.Path.EndsWith("/" + ParamPath, StringComparison.OrdinalIgnoreCase))).ToArray(); + if (found.Length > 1) + throw new InvalidDataException("The archive contains several game roots; it cannot be edited as one title."); + string prefix; + byte[]? existing = null; + if (found.Length == 1) + { + prefix = found[0].Path[..^ParamPath.Length]; + existing = volume.ReadAllBytes(found[0].Path, MaximumParamBytes); + } + else + { + // No param.json yet: put one in the archive's sce_sys folder, or at the top. + string? sys = volume.Entries.Where(entry => entry.IsDirectory && + (entry.Path.Equals("sce_sys", StringComparison.OrdinalIgnoreCase) || + entry.Path.EndsWith("/sce_sys", StringComparison.OrdinalIgnoreCase))) + .OrderBy(entry => entry.Path.Length).FirstOrDefault()?.Path; + 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; + } + + string partial = target + ".packwright-tmp"; + try + { + await ZArchiveImage.RewriteWithReplacementsAsync(source, partial, replacements, Jobs.Adapt(progress), token) + .ConfigureAwait(false); + Stage(progress, "Verifying ZArchive"); + await ZArchiveImage.VerifyAsync(partial, null, token).ConfigureAwait(false); + token.ThrowIfCancellationRequested(); + File.Move(partial, target, true); + } + finally + { + try { File.Delete(partial); } catch (IOException) { } + } + } + + // ---------------------------------------------------------------- packages + + private static async Task EditPackageAsync(string source, string target, MetadataEdit edit, byte[]? icon, + string passcode, string backendId, string work, IProgress progress, CancellationToken token) + { + string temporary = Path.Combine(work, "package.exfat"); + Stage(progress, "Unpacking package"); + await Jobs.ConvertAsync(source, temporary, Ps5ImageConversionTarget.Exfat, false, true, progress, token, + new ExfatBuildOptions { GenerateAmprIndex = true }).ConfigureAwait(false); + await EditExfatAsync(temporary, edit, icon, work, progress, token).ConfigureAwait(false); + + 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/MetadataOverrides.cs b/src/Packwright.App/Services/MetadataOverrides.cs new file mode 100644 index 0000000..1c3501c --- /dev/null +++ b/src/Packwright.App/Services/MetadataOverrides.cs @@ -0,0 +1,117 @@ +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using Packwright.Infrastructure; + +namespace Packwright.App.Services; + +/// +/// User-supplied details for one library item (a package, image or dump). Empty fields mean "use what the +/// file says". The file itself is never modified. +/// +public sealed class MetadataOverride +{ + public string Title { get; set; } = string.Empty; + public string TitleId { get; set; } = string.Empty; + public string ContentId { get; set; } = string.Empty; + public string Version { get; set; } = string.Empty; + public string Firmware { get; set; } = string.Empty; + /// Game, Patch, DLC or App. Empty keeps the detected category. + public string Category { get; set; } = string.Empty; + public string ConceptId { get; set; } = string.Empty; + public string MasterVersion { get; set; } = string.Empty; + public string Sdk { get; set; } = string.Empty; + public string Drm { get; set; } = string.Empty; + public string DefaultLanguage { get; set; } = string.Empty; + public string Created { get; set; } = string.Empty; + public string ToolVersion { get; set; } = string.Empty; + /// The declared features (HDR, VRR, ...). Null keeps what the file declares; an empty list means none. + public List? Features { get; set; } + /// Name of the custom icon file inside the overrides folder (empty = none). + public string IconFile { get; set; } = string.Empty; + + public bool IsEmpty => + Title.Length == 0 && TitleId.Length == 0 && ContentId.Length == 0 && Version.Length == 0 && + Firmware.Length == 0 && Category.Length == 0 && IconFile.Length == 0 && ConceptId.Length == 0 && + MasterVersion.Length == 0 && Sdk.Length == 0 && Drm.Length == 0 && DefaultLanguage.Length == 0 && + Created.Length == 0 && ToolVersion.Length == 0 && Features is null; +} + +/// Per-item detail overrides, saved next to the settings so they survive rescans and restarts. +public static class MetadataOverrides +{ + private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; + private static readonly object Gate = new(); + private static Dictionary? _items; + + private static string Folder => Path.Combine(AppServices.Instance.Store.AppDataDirectory, "overrides"); + private static string FilePath => Path.Combine(AppServices.Instance.Store.AppDataDirectory, "metadata-overrides.json"); + + private static Dictionary Items + { + get + { + if (_items is not null) return _items; + var loaded = new Dictionary(StringComparer.OrdinalIgnoreCase); + try + { + if (File.Exists(FilePath)) + loaded = new Dictionary( + JsonSerializer.Deserialize>(File.ReadAllText(FilePath)) ?? [], + StringComparer.OrdinalIgnoreCase); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or JsonException) + { + Logger.Warn("Edited details could not be read: " + ex.Message); + } + return _items = loaded; + } + } + + public static MetadataOverride? Get(string path) + { + lock (Gate) return Items.TryGetValue(path, out MetadataOverride? value) ? value : null; + } + + /// Saves (or, when empty, removes) the override for . + public static void Set(string path, MetadataOverride value) + { + lock (Gate) + { + if (value.IsEmpty) Items.Remove(path); + else Items[path] = value; + try + { + File.WriteAllText(FilePath, JsonSerializer.Serialize(Items, JsonOptions)); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + Logger.Warn("Edited details could not be saved: " + ex.Message); + } + } + } + + public static string? IconPath(MetadataOverride? value) + { + if (value is null || value.IconFile.Length == 0) return null; + string path = Path.Combine(Folder, value.IconFile); + return File.Exists(path) ? path : null; + } + + /// Copies an image into the overrides folder and returns the stored file name. + public static string StoreIcon(string itemPath, byte[] imageBytes) + { + Directory.CreateDirectory(Folder); + string name = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(itemPath.ToLowerInvariant())))[..24] + + "-" + DateTime.UtcNow.Ticks + ".png"; + File.WriteAllBytes(Path.Combine(Folder, name), imageBytes); + return name; + } + + public static void DeleteIcon(string? fileName) + { + if (string.IsNullOrEmpty(fileName)) return; + try { File.Delete(Path.Combine(Folder, fileName)); } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { } + } +} diff --git a/src/Packwright.App/Services/SourceInfo.cs b/src/Packwright.App/Services/SourceInfo.cs new file mode 100644 index 0000000..1b78e26 --- /dev/null +++ b/src/Packwright.App/Services/SourceInfo.cs @@ -0,0 +1,84 @@ +using Avalonia.Media.Imaging; +using Packwright.Core.Models; +using Packwright.Core.Parsers; +using Packwright.Core.Services; +using Packwright.Infrastructure; + +namespace Packwright.App.Services; + +/// A game's display name and a small icon, used to label tasks and the selected source. +public sealed record SourceSummary(string Title, byte[]? Icon) +{ + public static SourceSummary Empty { get; } = new(string.Empty, null); +} + +/// Finds the title and artwork of a dump folder, image or package. +public static class SourceInfo +{ + private const int IconWidth = 96; + + public static Task DescribeAsync(string path) => Task.Run(() => Describe(path)); + + private static SourceSummary Describe(string path) + { + try + { + Ps5GameInfo? game = FromLibrary(path) ?? Read(path); + if (game is null) return new SourceSummary(NameOf(path), null); + string title = string.IsNullOrWhiteSpace(game.Title) ? NameOf(path) : game.Title; + return new SourceSummary(title, ReadIcon(game)); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or InvalidDataException or + NotSupportedException or ArgumentException or System.Text.Json.JsonException) + { + Logger.Warn($"Could not describe {path}: {ex.Message}"); + return new SourceSummary(NameOf(path), null); + } + } + + private static string NameOf(string path) => + Path.GetFileName(path.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)); + + private static Ps5GameInfo? FromLibrary(string path) => + AppServices.Instance.Games.FirstOrDefault(game => + string.Equals(game.RootPath, path, StringComparison.OrdinalIgnoreCase)); + + internal static Ps5GameInfo? Read(string path) + { + if (Directory.Exists(path)) + { + string param = Path.Combine(path, "sce_sys", "param.json"); + 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); + return Ps5ImageFormatProbeLookup(path); + } + + private static Ps5GameInfo? Ps5ImageFormatProbeLookup(string path) + { + Packwright.Containers.Ps5ImageFormat format = Packwright.Containers.Ps5ImageFormatProbe.Detect(path); + return format switch + { + Packwright.Containers.Ps5ImageFormat.Exfat => new FilesystemImageGameReader().Read(path), + Packwright.Containers.Ps5ImageFormat.Ufs2 => new FfpkgGameReader().Read(path), + Packwright.Containers.Ps5ImageFormat.Pfs => new FfpfscGameReader().Read(path), + Packwright.Containers.Ps5ImageFormat.ZArchive => new ZArchiveGameReader().Read(path), + _ => null + }; + } + + private static byte[]? ReadIcon(Ps5GameInfo game) + { + using IReadOnlyGameFileSystem files = GameFileSystem.Open(game); + if (!files.FileExists("sce_sys/icon0.png")) return null; + using Stream stream = files.OpenRead("sce_sys/icon0.png"); + using var memory = new MemoryStream(); + stream.CopyTo(memory); + memory.Position = 0; + using Bitmap small = Bitmap.DecodeToWidth(memory, IconWidth, BitmapInterpolationMode.HighQuality); + using var output = new MemoryStream(); + small.Save(output); + return output.ToArray(); + } +} diff --git a/src/Packwright.App/Services/ThumbnailService.cs b/src/Packwright.App/Services/ThumbnailService.cs new file mode 100644 index 0000000..0a92f2f --- /dev/null +++ b/src/Packwright.App/Services/ThumbnailService.cs @@ -0,0 +1,80 @@ +using System.Collections.Concurrent; +using Avalonia.Media.Imaging; +using Avalonia.Threading; +using Packwright.App.Models; +using Packwright.Core.Services; +using Packwright.Infrastructure; + +namespace Packwright.App.Services; + +/// +/// Loads game artwork (sce_sys/icon0.png) in the background, a couple at a time, decoded to a small +/// size. The number of cached bitmaps is capped; the oldest are released and loaded again on demand. +/// +public static class ThumbnailService +{ + private const int DecodeWidth = 168; + private static readonly SemaphoreSlim Gate = new(2); + private static readonly ConcurrentQueue Loaded = new(); + private static int _loadedCount; + + public static void Request(LibraryItem item) + { + if (!AppServices.Instance.Settings.ShowThumbnails) return; + _ = Task.Run(() => LoadAsync(item)); + } + + private static async Task LoadAsync(LibraryItem item) + { + await Gate.WaitAsync().ConfigureAwait(false); + try + { + byte[]? bytes = ReadIcon(item); + if (bytes is null || bytes.Length == 0) return; + Bitmap? bitmap = Decode(bytes); + if (bitmap is null) return; + await Dispatcher.UIThread.InvokeAsync(() => item.SetThumbnail(bitmap)); + Loaded.Enqueue(item); + if (Interlocked.Increment(ref _loadedCount) > AppServices.Instance.Settings.ThumbnailCacheCount && + Loaded.TryDequeue(out LibraryItem? oldest)) + { + Interlocked.Decrement(ref _loadedCount); + await Dispatcher.UIThread.InvokeAsync(oldest.ResetThumbnail); + } + } + catch (Exception ex) when (ex is IOException or InvalidDataException or UnauthorizedAccessException + or NotSupportedException or ArgumentException or InvalidOperationException) + { + Logger.Warn($"No artwork for {item.FileName}: {ex.Message}"); + } + finally + { + Gate.Release(); + } + } + + internal static byte[]? ReadIcon(LibraryItem item) + { + if (MetadataOverrides.IconPath(MetadataOverrides.Get(item.Path)) is { } custom) + return File.ReadAllBytes(custom); + using IReadOnlyGameFileSystem files = GameFileSystem.Open(item.Game); + if (!files.FileExists("sce_sys/icon0.png")) return null; + using Stream stream = files.OpenRead("sce_sys/icon0.png"); + using var memory = new MemoryStream(); + stream.CopyTo(memory); + return memory.ToArray(); + } + + private static Bitmap? Decode(byte[] png) + { + try + { + using var stream = new MemoryStream(png); + return Bitmap.DecodeToWidth(stream, DecodeWidth, BitmapInterpolationMode.MediumQuality); + } + catch (Exception ex) when (ex is ArgumentException or InvalidOperationException or IOException) + { + return null; + } + } +} diff --git a/src/Packwright.App/Services/UpdateService.cs b/src/Packwright.App/Services/UpdateService.cs new file mode 100644 index 0000000..d722759 --- /dev/null +++ b/src/Packwright.App/Services/UpdateService.cs @@ -0,0 +1,74 @@ +using System.Reflection; +using Packwright.Core.Services; +using Packwright.Infrastructure; + +namespace Packwright.App.Services; + +public enum UpdateState { UpToDate, Available, NoReleases, Failed } + +public sealed record UpdateResult(UpdateState State, ReleaseInfo? Release, string Message); + +/// +/// Remembers whether a newer Packwright is available. A check happens only when the user asks, or once a day when +/// they allowed automatic checks; it talks to GitHub and nowhere else. +/// +public static class UpdateService +{ + public static string CurrentVersion { get; } = + Assembly.GetExecutingAssembly().GetName().Version?.ToString(3) ?? "1.0.0"; + + /// The newer release found by the last check, or null. + public static ReleaseInfo? Available { get; private set; } + + public static event Action? Changed; + + /// True when a newer release is known and the user has not chosen to skip it. + public static bool HasVisibleUpdate => + Available is { } release && + !string.Equals(AppServices.Instance.Settings.SkippedUpdateVersion, release.Version, StringComparison.OrdinalIgnoreCase); + + public static bool ShouldCheckOnStartup() + { + AppSettings settings = AppServices.Instance.Settings; + return settings.CheckForUpdates && DateTime.UtcNow - settings.LastUpdateCheckUtc > TimeSpan.FromHours(20); + } + + public static async Task CheckAsync(CancellationToken cancellationToken = default) + { + AppServices services = AppServices.Instance; + try + { + ReleaseInfo? latest = await UpdateChecker.GetLatestAsync(cancellationToken).ConfigureAwait(false); + services.Settings.LastUpdateCheckUtc = DateTime.UtcNow; + services.SaveSettings(); + if (latest is null) + { + Available = null; + Changed?.Invoke(); + return new UpdateResult(UpdateState.NoReleases, null, "No release has been published yet."); + } + if (!UpdateChecker.IsNewer(latest.Version, CurrentVersion)) + { + Available = null; + Changed?.Invoke(); + return new UpdateResult(UpdateState.UpToDate, latest, $"You have the latest version ({CurrentVersion})."); + } + Available = latest; + Logger.Info($"Update available: {latest.Version} (you have {CurrentVersion})."); + Changed?.Invoke(); + return new UpdateResult(UpdateState.Available, latest, $"Version {latest.Version} is available (you have {CurrentVersion})."); + } + catch (OnlineLookupException ex) + { + Logger.Warn("Update check: " + ex.Message); + return new UpdateResult(UpdateState.Failed, null, ex.Message); + } + } + + public static void Skip(ReleaseInfo release) + { + AppServices.Instance.Settings.SkippedUpdateVersion = release.Version; + AppServices.Instance.SaveSettings(); + Changed?.Invoke(); + } +} diff --git a/src/Packwright.App/Styles/Theme.axaml b/src/Packwright.App/Styles/Theme.axaml new file mode 100644 index 0000000..57b77dc --- /dev/null +++ b/src/Packwright.App/Styles/Theme.axaml @@ -0,0 +1,457 @@ + + + + + + + + #0D0F17 + #131622 + #1A1E2E + #222741 + #10131D + #2A2F47 + #8E94AE + #EEF0FA + #7C8CFF + #5B6BF0 + #267C8CFF + #3DD68C + #F5B642 + #FF6B7A + + + + #F3F4FA + #FFFFFF + #FFFFFF + #EEF0FB + #F6F7FC + #DDE0EE + #6A7090 + #161A2B + #4F5FE8 + #3B4BD6 + #1F4F5FE8 + #1FA46A + #C98A10 + #D9414F + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Packwright.App/Views/AboutView.axaml b/src/Packwright.App/Views/AboutView.axaml new file mode 100644 index 0000000..8f94377 --- /dev/null +++ b/src/Packwright.App/Views/AboutView.axaml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + +