From e015977f3c82267e7f3af4acf7b510bc548c59ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Fri, 25 Sep 2026 15:23:08 -0600 Subject: [PATCH] Headless: Add --dump-file to copy a file out of a disc image Co-Authored-By: Claude Opus 5.5 (1M context) --- Core/CmdLine.cpp | 2 ++ Core/CmdLine.h | 4 ++++ headless/Headless.cpp | 8 +++++++ headless/ReverseEngineer.cpp | 42 ++++++++++++++++++++++++++++++++++++ headless/ReverseEngineer.h | 4 ++++ 5 files changed, 60 insertions(+) diff --git a/Core/CmdLine.cpp b/Core/CmdLine.cpp index 8e2baab246..cb2aef5662 100644 --- a/Core/CmdLine.cpp +++ b/Core/CmdLine.cpp @@ -224,6 +224,8 @@ static const CommandLineParam g_autoParams[] = { {POFF(reRawBase), CmdParamType::String, "re-raw-base", '\0', "Treat --re-module as a raw code image loaded at this address, e.g. 0x08300000", CmdLineMode::Headless}, {POFF(reDecrypt), CmdParamType::String, "re-decrypt", '\0', "Decrypt one encrypted PSP file (PRX or ME image) and exit", CmdLineMode::Headless}, {POFF(reDecryptOut), CmdParamType::String, "re-decrypt-out", '\0', "Output file for --re-decrypt (default: decrypted.bin)", CmdLineMode::Headless}, + {POFF(dumpFile), CmdParamType::String, "dump-file", '\0', "Copy one file out of the disc image, e.g. disc0:/PSP_GAME/USRDIR/MOVIE.PMF, and exit", CmdLineMode::Headless}, + {POFF(dumpFileOut), CmdParamType::String, "dump-file-out", '\0', "Output file for --dump-file (default: the file's own name)", CmdLineMode::Headless}, {POFF(odsLog), CmdParamType::Bool, "odslog", 'o', "Also log through OutputDebugString (Windows)", CmdLineMode::Headless}, {POFF(generateInterpreterDispatch), CmdParamType::Bool, "generate-interpreter-dispatch", '\0', "Generate C++ interpreter dispatch code (ExecInstruction) to stdout and exit", CmdLineMode::Headless}, {POFF(resolutionScale), CmdParamType::Int, "resolution-scale", '\0', "Set the resolution scale factor"}, diff --git a/Core/CmdLine.h b/Core/CmdLine.h index 61040e9b60..d5dc9a53b2 100644 --- a/Core/CmdLine.h +++ b/Core/CmdLine.h @@ -126,6 +126,10 @@ struct CommandLineOptions { std::optional reDecrypt; // Where the plaintext goes. Defaults to "decrypted.bin". std::optional reDecryptOut; + // Headless: copy one file out of the disc image given as the positional argument, to + // --dump-file-out, and exit. + std::optional dumpFile; + std::optional dumpFileOut; std::optional memReadAction; std::optional memWriteAction; diff --git a/headless/Headless.cpp b/headless/Headless.cpp index 812dfa5203..d9ccb37a7f 100644 --- a/headless/Headless.cpp +++ b/headless/Headless.cpp @@ -1065,6 +1065,14 @@ int main(int argc, const char* argv[]) { if (cmdLineOptions.reDecrypt.has_value()) { return RunDecryptFile(cmdLineOptions.reDecrypt.value(), cmdLineOptions.reDecryptOut.value_or("decrypted.bin")); } + if (cmdLineOptions.dumpFile.has_value()) { + if (cmdLineOptions.bootFilenames.empty()) { + fprintf(stderr, "--dump-file needs the disc image as the positional argument\n"); + return 1; + } + const std::string &inPath = cmdLineOptions.dumpFile.value(); + return RunDumpDiscFile(cmdLineOptions.bootFilenames[0], inPath, cmdLineOptions.dumpFileOut.value_or(Path(inPath.substr(inPath.find(':') + 1)).GetFilename())); + } if (cmdLineOptions.reModule.has_value()) { ReverseEngineerOptions reOptions; reOptions.modulePath = cmdLineOptions.reModule.value(); diff --git a/headless/ReverseEngineer.cpp b/headless/ReverseEngineer.cpp index 62104ba778..0ff22333b8 100644 --- a/headless/ReverseEngineer.cpp +++ b/headless/ReverseEngineer.cpp @@ -393,6 +393,48 @@ int RunDecryptFile(const std::string &inPath, const std::string &outPath) { return 0; } +int RunDumpDiscFile(const std::string &discPath, const std::string &inPath, const std::string &outPath) { + std::string insideDisc = inPath; + const size_t colon = insideDisc.find(':'); + if (colon != std::string::npos) { + insideDisc = insideDisc.substr(colon + 1); + } + if (insideDisc.empty() || insideDisc[0] != '/') { + insideDisc = "/" + insideDisc; + } + + std::unique_ptr loader(ConstructFileLoader(Path(discPath))); + std::string blockError; + std::shared_ptr device(loader ? ConstructBlockDevice(loader.get(), &blockError) : nullptr); + if (!device) { + fprintf(stderr, "dump-file: %s isn't a disc image we can read: %s\n", discPath.c_str(), blockError.c_str()); + return 1; + } + // The ISO filesystem takes ownership of the block device, which owns the loader. + loader.release(); + ISOFileSystem isoFs(&pspFileSystem, device); + + const int handle = isoFs.OpenFile(insideDisc, FILEACCESS_READ); + if (handle < 0) { + fprintf(stderr, "dump-file: no such file on the disc: %s\n", insideDisc.c_str()); + return 1; + } + const PSPFileInfo info = isoFs.GetFileInfoByHandle(handle); + std::vector data((size_t)info.size); + const size_t readBytes = data.empty() ? 0 : isoFs.ReadFile(handle, data.data(), (s64)data.size()); + isoFs.CloseFile(handle); + if (readBytes != data.size()) { + fprintf(stderr, "dump-file: short read, %d of %d bytes\n", (int)readBytes, (int)data.size()); + return 1; + } + if (!File::WriteDataToFile(false, data.data(), data.size(), Path(outPath))) { + fprintf(stderr, "dump-file: couldn't write %s\n", outPath.c_str()); + return 1; + } + printf("dump-file: wrote %d bytes of %s to %s\n", (int)data.size(), insideDisc.c_str(), outPath.c_str()); + return 0; +} + int RunReverseEngineer(const ReverseEngineerOptions &opts) { // A module inside a disc image rather than on the host - see ReverseEngineerOptions. const bool fromDisc = startsWithNoCase(opts.modulePath, "disc0:") || startsWithNoCase(opts.modulePath, "umd0:"); diff --git a/headless/ReverseEngineer.h b/headless/ReverseEngineer.h index 8f71d8c713..6ab7c099b9 100644 --- a/headless/ReverseEngineer.h +++ b/headless/ReverseEngineer.h @@ -50,3 +50,7 @@ int RunReverseEngineer(const ReverseEngineerOptions &opts); // flash0:/kd/resource, which are ordinary tagged containers with the signature blanked, so the // normal module loader won't touch them. Returns a process exit code. int RunDecryptFile(const std::string &inPath, const std::string &outPath); + +// Copies one file out of a disc image (ISO/CSO/...) as-is, e.g. "disc0:/PSP_GAME/USRDIR/MOVIE.PMF". +// Returns a process exit code. +int RunDumpDiscFile(const std::string &discPath, const std::string &inPath, const std::string &outPath);