PSAR: fix the last entry of an archive, and paths that aren't on flash

Two small things I'd noticed as counters in a survey and written off as quirks
of the oldest firmwares. Both were bugs here.

The last entry of every 1.x and 2.x archive was dropped. Decoding a record hands
the decrypter 16 bytes of slack past the block, which was read out of the
archive - and the final record ends flush with the end, so there was nothing
there to read and the entry was refused. It copies what's there and zero-fills
only the remainder now. Worth noting the slack isn't decoration: filling it with
zeros unconditionally, which is what I tried first, breaks every archive that
does have those bytes, so the decrypter reads and uses them.

And 2.x names one entry "ipl:/psp_nandipl.bin", where the check for "is this
already a real path" only knew flash0: and flash1:. The file came out fine, but
it was counted unresolved, and - the part that matters - an entry that isn't a
real path is skipped whenever a prefix filter is set. Rather than collect device
names, the test is now for a "<dev>:/" shape, which 3.x's grouped short names
("com:00123") don't have.

Seven sources, firmware 1.50 through 6.61, now unpack with nothing unresolved
and nothing failed: 1.50 and 1.52 gain their missing last file, 2.81 loses its
unresolved count, and 3.95, 6.00, 6.20 and 6.61 are unchanged.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01GZq8ZtJmFY7bkX5FVkr3P9
This commit is contained in:
Henrik RydgårdandClaude Opus 5 committed 2026-08-21 10:00:57 +02:00
1 parent da9e0f0898
commit a3a67156da
1 file changed
+15 -6
+15 -6
View File
@@ -15,6 +15,7 @@
// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
#include <algorithm>
#include <cstring>
#include <map>
#include <string_view>
@@ -130,10 +131,13 @@ static bool RelativePathFromEntryName(std::string_view name, std::string *out) {
return true;
}
// An entry name is either a real path ("flash0:/...") or a short token that only the archive's
// own name tables can resolve. We can use the former directly.
// An entry name is either a real path or a short token that only the archive's own file lists can
// resolve. 1.x and 2.x archives skip the lists and name entries outright: "flash0:/...", but also
// "flash1:/..." and "ipl:/psp_nandipl.bin", so rather than keep a list of devices, take the "<dev>:/"
// shape as the tell. 3.x's grouped short names ("com:00123") have a colon but no slash after it,
// which is what keeps them out.
static bool EntryNameIsRealPath(std::string_view name) {
return startsWithNoCase(name, "flash0:/") || startsWithNoCase(name, "flash1:/");
return name.find(":/") != std::string_view::npos;
}
// 6.x file lists write "flash0:/font/x.pgf", 3.x writes "flash0/font/x.pgf" for the same thing.
@@ -526,11 +530,16 @@ int PSARReader::DecodeBlock(u32 offset, u32 cbIn, std::vector<u8> &out) {
return (int)cbIn;
}
// The decrypter reads a little past the block, so copy the extra 16 bytes it expects.
if ((size_t)cbIn + 0x10 > avail) {
if (cbIn > avail) {
return -1;
}
out.assign(in, in + cbIn + 0x10);
// The decrypter reads a little past the block and does use what it finds there, so hand it the
// real bytes whenever the archive has them. It hasn't got them for the very last entry, which
// ends flush with the last record - zero-fill only then, rather than refusing the entry, which
// is what used to lose every 1.x and 2.x archive's final file.
const size_t slack = std::min<size_t>(0x10, avail - cbIn);
out.assign(in, in + cbIn + slack);
out.resize((size_t)cbIn + 0x10, 0);
if (!oldschool_) {
// "Demangle": the 0x130 bytes at +0x20 are AES-128-CBC encrypted on top of everything