mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-01 14:58:14 +00:00
PSAR: let the caller pick which PSP model to unpack for
An updater carries one file list per hardware revision, and which one you resolve names against decides both what a file is called and whether it's part of that model's firmware at all. That was hardcoded to "first list that names it", which is right for extracting everything but wrong for reproducing what a particular console would have installed. PSARUnpackOptions::model takes a PSPModelGeneration now, and the lists are kept per model rather than merged. Any (the default) keeps the old behaviour; anything else uses only that model's list and skips what it doesn't name. --unpack-updater-model on headless takes "01g".."12g" or "any". On the 6.61 updater: any gives 411 files, 03g gives 330 with 81 belonging to other models, 01g gives 313 with 98. The difference is what it should be - 03g has the _03g.prx variants and arib.pgf, 01g has neither. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
This commit is contained in:
1 parent
d8481eab79
commit
a58f54ba53
6 files changed
+128
-16
No files matched your search
@@ -169,6 +169,8 @@ codebase first - PPSSPP already has implementations of many formats (CSO, LZRC,
|
||||
handlers, PBP, SevenZip, etc.), possibly in several places. Reuse or extend an existing one instead of
|
||||
writing a new one (e.g. there is an LZRC decompressor in Core/FileSystems/tlzrc.cpp).
|
||||
|
||||
For string sanitation, we already have SanitizeString in StringUtils.cpp - add new modes if needed.
|
||||
|
||||
## Headless and unittest builds
|
||||
|
||||
We have additional PPSSPPHeadless and unit test builds (/headless and /unittest), that have their own separate
|
||||
@@ -537,6 +539,12 @@ Concretely, as measured against the headless build (2026-08-16), per CPU backend
|
||||
| `memory.breakpoint.*` (memchecks) | works | **only for constant addresses** - see below |
|
||||
| `cpu.regBreakpoint.*` | works | never trips (as documented) |
|
||||
|
||||
## Commit message style
|
||||
|
||||
Keep commit messages focused, not overly long (although sometimes it's motivated if a single commit
|
||||
is super complex). Do not report things like 100/100 tests passed - that's a given, if tests break
|
||||
you aren't supposed to make a commit.
|
||||
|
||||
## Quick rebuild on Linux
|
||||
|
||||
You don't need to do ./b.sh --debug to verify every single little change, instead use this shortcut:
|
||||
|
||||
@@ -193,6 +193,7 @@ static const CommandLineParam g_autoParams[] = {
|
||||
{POFF(maxScreenshotError), CmdParamType::Double, "max-mse", '\0', "Maximum allowed MSE error for screenshot comparison", CmdLineMode::Headless},
|
||||
{POFF(mountIso), CmdParamType::String, "mount", 'm', "Mount ISO/CSO on umd1:", CmdLineMode::Headless},
|
||||
{POFF(unpackUpdater), CmdParamType::String, "unpack-updater", '\0', "Unpack the firmware in an updater EBOOT.PBP into DIR and exit", CmdLineMode::Headless},
|
||||
{POFF(unpackUpdaterModel), CmdParamType::String, "unpack-updater-model", '\0', "PSP model to unpack for (01g..12g, default any)", 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"},
|
||||
|
||||
@@ -69,6 +69,9 @@ struct CommandLineOptions {
|
||||
// See Core/Util/PSARUnpack.h - the API can also filter to a subfolder, which the command line
|
||||
// deliberately doesn't expose since the in-app use is specifically flash0:/font.
|
||||
std::optional<std::string> unpackUpdater;
|
||||
// Headless: which PSP model the unpacker resolves names against - "01g".."12g", or "any"
|
||||
// (the default) to take whatever file list names each file first.
|
||||
std::optional<std::string> unpackUpdaterModel;
|
||||
|
||||
std::optional<int> memReadAction;
|
||||
std::optional<int> memWriteAction;
|
||||
|
||||
+81
-12
@@ -297,8 +297,51 @@ static int TableKeyIndexForVersion(std::string_view version) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Entries "00001".."00012" are name tables rather than files.
|
||||
static bool IsNameTableEntry(std::string_view name) {
|
||||
const char *PSPModelGenerationToString(PSPModelGeneration generation) {
|
||||
switch (generation) {
|
||||
case PSPModelGeneration::Any: return "any";
|
||||
case PSPModelGeneration::PSP_1000: return "01g";
|
||||
case PSPModelGeneration::PSP_2000: return "02g";
|
||||
case PSPModelGeneration::PSP_3000: return "03g";
|
||||
case PSPModelGeneration::PSP_4000: return "04g";
|
||||
case PSPModelGeneration::PSP_N1000: return "05g";
|
||||
case PSPModelGeneration::PSP_6000: return "06g";
|
||||
case PSPModelGeneration::PSP_7000: return "07g";
|
||||
case PSPModelGeneration::PSP_9000: return "09g";
|
||||
case PSPModelGeneration::PSP_11000: return "11g";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
bool PSPModelGenerationFromString(std::string_view name, PSPModelGeneration *generation) {
|
||||
if (equalsNoCase(name, "any")) {
|
||||
*generation = PSPModelGeneration::Any;
|
||||
return true;
|
||||
}
|
||||
// "03g" and a bare "3" both work.
|
||||
std::string_view digits = name;
|
||||
if (digits.size() > 1 && (digits.back() == 'g' || digits.back() == 'G')) {
|
||||
digits.remove_suffix(1);
|
||||
}
|
||||
if (digits.empty() || digits.size() > 2) {
|
||||
return false;
|
||||
}
|
||||
int value = 0;
|
||||
for (char c : digits) {
|
||||
if (c < '0' || c > '9') {
|
||||
return false;
|
||||
}
|
||||
value = value * 10 + (c - '0');
|
||||
}
|
||||
if (value < 0 || value > (int)PSPModelGeneration::MAX) {
|
||||
return false;
|
||||
}
|
||||
*generation = (PSPModelGeneration)value;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Entries "00001".."00012" are per-model file lists rather than files, numbered by generation.
|
||||
static bool IsNameTableEntry(std::string_view name, int *generation) {
|
||||
if (name.size() != 5 || name.compare(0, 3, "000") != 0) {
|
||||
return false;
|
||||
}
|
||||
@@ -306,7 +349,11 @@ static bool IsNameTableEntry(std::string_view name) {
|
||||
return false;
|
||||
}
|
||||
const int index = (name[3] - '0') * 10 + (name[4] - '0');
|
||||
return index >= 1 && index <= 12;
|
||||
if (index < 1 || index > (int)PSPModelGeneration::MAX) {
|
||||
return false;
|
||||
}
|
||||
*generation = index;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Decrypts a name table in place and returns the length of the text in it, or <= 0 on failure.
|
||||
@@ -340,8 +387,7 @@ static int DecryptNameTable(std::vector<u8> &table, int keyIndex) {
|
||||
return pspDecryptPRX(table.data(), table.data(), (u32)table.size());
|
||||
}
|
||||
|
||||
// Table text is lines of "shortname,realpath". The tables are per PSP model, and a short name
|
||||
// that appears in several of them means the same file, so the first one to claim it wins.
|
||||
// Table text is lines of "shortname,realpath".
|
||||
static void ParseNameTable(const char *text, size_t length, std::map<std::string, std::string> *names) {
|
||||
size_t start = 0;
|
||||
while (start < length) {
|
||||
@@ -575,8 +621,9 @@ bool UnpackPSAR(const u8 *psar, size_t psarSize, const Path &outputDir, const PS
|
||||
const int tableKeyIndex = TableKeyIndexForVersion(stats->firmwareVersion);
|
||||
INFO_LOG(Log::Loader, "Unpacking firmware %s (name table key %d)", stats->firmwareVersion.c_str(), tableKeyIndex);
|
||||
|
||||
// Filled in as we go - the tables come before the files they name.
|
||||
std::map<std::string, std::string> names;
|
||||
// One file list per model, filled in as we go - they come before the files they name.
|
||||
std::map<int, std::map<std::string, std::string>> namesByModel;
|
||||
INFO_LOG(Log::Loader, "Resolving names against model %s", PSPModelGenerationToString(options.model));
|
||||
|
||||
while (true) {
|
||||
const int result = reader.NextEntry(error);
|
||||
@@ -606,33 +653,55 @@ bool UnpackPSAR(const u8 *psar, size_t psarSize, const Path &outputDir, const PS
|
||||
|
||||
// The name tables have to be read before anything they name shows up, which the archive's
|
||||
// own ordering takes care of.
|
||||
if (IsNameTableEntry(reader.entryName())) {
|
||||
int tableGeneration = 0;
|
||||
if (IsNameTableEntry(reader.entryName(), &tableGeneration)) {
|
||||
stats->nameTables++;
|
||||
std::vector<u8> table = reader.entryData();
|
||||
const int textLength = DecryptNameTable(table, tableKeyIndex);
|
||||
if (textLength <= 0 || (size_t)textLength > table.size()) {
|
||||
ERROR_LOG(Log::Loader, "PSAR: couldn't decrypt name table '%s' (%d)", reader.entryName().c_str(), textLength);
|
||||
ERROR_LOG(Log::Loader, "PSAR: couldn't decrypt the %02dg file list (%d)", tableGeneration, textLength);
|
||||
stats->failed++;
|
||||
} else {
|
||||
const size_t before = names.size();
|
||||
std::map<std::string, std::string> &names = namesByModel[tableGeneration];
|
||||
ParseNameTable((const char *)table.data(), textLength, &names);
|
||||
INFO_LOG(Log::Loader, "PSAR: name table '%s' added %d names", reader.entryName().c_str(), (int)(names.size() - before));
|
||||
INFO_LOG(Log::Loader, "PSAR: %02dg file list names %d files", tableGeneration, (int)names.size());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string realName;
|
||||
bool wrongModel = false;
|
||||
if (EntryNameIsRealPath(reader.entryName())) {
|
||||
realName = reader.entryName();
|
||||
} else if (options.model != PSPModelGeneration::Any) {
|
||||
// Only this model's list counts. A file it doesn't name belongs to some other model.
|
||||
const auto model = namesByModel.find((int)options.model);
|
||||
if (model != namesByModel.end()) {
|
||||
const auto found = model->second.find(reader.entryName());
|
||||
if (found != model->second.end()) {
|
||||
realName = found->second;
|
||||
} else {
|
||||
wrongModel = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// A short name that several lists claim is the same file, so take the first name for it.
|
||||
for (const auto &[generation, names] : namesByModel) {
|
||||
const auto found = names.find(reader.entryName());
|
||||
if (found != names.end()) {
|
||||
realName = found->second;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (wrongModel) {
|
||||
stats->otherModel++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (realName.empty()) {
|
||||
// No table claimed this one. Still worth writing out under its short name, but a
|
||||
// No list claimed this one. Still worth writing out under its short name, but a
|
||||
// filter has nothing to match it against.
|
||||
stats->unnamed++;
|
||||
realName = reader.entryName();
|
||||
|
||||
+27
-2
@@ -45,11 +45,35 @@ enum class PSARCompression {
|
||||
|
||||
const char *PSARCompressionToString(PSARCompression c);
|
||||
|
||||
// PSP hardware revisions, as the updater numbers them. An updater carries one file list per
|
||||
// model, so which one you resolve names against decides both what a file is called and whether
|
||||
// it's part of that model's firmware at all.
|
||||
enum class PSPModelGeneration {
|
||||
Any = 0, // Whichever list names a file first. Use this to extract everything.
|
||||
PSP_1000 = 1,
|
||||
PSP_2000 = 2,
|
||||
PSP_3000 = 3,
|
||||
PSP_4000 = 4,
|
||||
PSP_N1000 = 5, // PSP Go
|
||||
PSP_6000 = 6,
|
||||
PSP_7000 = 7,
|
||||
PSP_9000 = 9,
|
||||
PSP_11000 = 11,
|
||||
MAX = 12,
|
||||
};
|
||||
|
||||
const char *PSPModelGenerationToString(PSPModelGeneration generation);
|
||||
// Accepts "01g".."12g", a bare number, or "any". Returns false if it's none of those.
|
||||
bool PSPModelGenerationFromString(std::string_view name, PSPModelGeneration *generation);
|
||||
|
||||
struct PSARUnpackOptions {
|
||||
// Only unpack entries whose name starts with this, e.g. "flash0:/font/". Case insensitive.
|
||||
// Empty means everything. Entries whose real name we can't recover are never matched by a
|
||||
// non-empty filter.
|
||||
std::string prefixFilter;
|
||||
// Which model's file list to resolve names against. Anything that model's list doesn't name
|
||||
// isn't part of its firmware, and is skipped.
|
||||
PSPModelGeneration model = PSPModelGeneration::Any;
|
||||
// Walk and report, but don't write any files.
|
||||
bool listOnly = false;
|
||||
// Log a line per entry. Off by default - an updater holds well over a thousand of them.
|
||||
@@ -62,8 +86,9 @@ struct PSARUnpackStats {
|
||||
int directories = 0;
|
||||
int written = 0;
|
||||
int skippedByFilter = 0;
|
||||
int nameTables = 0; // Entries that were name tables rather than files.
|
||||
int unnamed = 0; // Short names no name table claimed.
|
||||
int nameTables = 0; // Entries that were file lists rather than files.
|
||||
int unnamed = 0; // Short names no file list claimed.
|
||||
int otherModel = 0; // Files that belong to a model other than the requested one.
|
||||
int failed = 0;
|
||||
// How many entries used each compression, indexed by PSARCompression.
|
||||
int compressionCounts[6]{};
|
||||
|
||||
@@ -623,14 +623,20 @@ int main(int argc, const char* argv[]) {
|
||||
|
||||
PSARUnpackOptions unpackOptions;
|
||||
unpackOptions.verbose = testOptions.verbose;
|
||||
if (cmdLineOptions.unpackUpdaterModel.has_value() &&
|
||||
!PSPModelGenerationFromString(cmdLineOptions.unpackUpdaterModel.value(), &unpackOptions.model)) {
|
||||
fprintf(stderr, "Unknown PSP model '%s' - expected 01g..12g or any\n", cmdLineOptions.unpackUpdaterModel.value().c_str());
|
||||
return 1;
|
||||
}
|
||||
PSARUnpackStats stats;
|
||||
std::string unpackError;
|
||||
const bool ok = UnpackUpdaterPBP(Path(cmdLineOptions.bootFilenames[0]), Path(cmdLineOptions.unpackUpdater.value()), unpackOptions, &stats, &unpackError);
|
||||
if (!ok) {
|
||||
fprintf(stderr, "Unpacking failed: %s\n", unpackError.c_str());
|
||||
}
|
||||
printf("Firmware %s: %d entries, %d files written, %d directories, %d unresolved names, %d failed\n",
|
||||
stats.firmwareVersion.c_str(), stats.entries, stats.written, stats.directories, stats.unnamed, stats.failed);
|
||||
printf("Firmware %s (model %s): %d entries, %d files written, %d directories, %d unresolved names, %d for other models, %d failed\n",
|
||||
stats.firmwareVersion.c_str(), PSPModelGenerationToString(unpackOptions.model), stats.entries, stats.written,
|
||||
stats.directories, stats.unnamed, stats.otherModel, stats.failed);
|
||||
printf("Compression: none=%d zlib=%d KL4E=%d KL3E=%d LZR=%d unknown=%d\n",
|
||||
stats.compressionCounts[(int)PSARCompression::None],
|
||||
stats.compressionCounts[(int)PSARCompression::Zlib],
|
||||
|
||||
Reference in new issue
Block a user