fix(pyinstaller): attribute malformed TOC rows at their owner

This commit is contained in:
morluto
2026-10-24 02:31:44 +08:00
parent f6a1e2c892
commit 8425c36da2
3 changed files with 56 additions and 23 deletions
+3
View File
@@ -44,6 +44,9 @@ they do not claim a destination directory. Their `relative_offset` and
`compressed_size` preserve any payload declared by older producers, while
`declared_size` is zero because no bytecode is materialized for a namespace.
Every declared span must end before the PYZ table of contents.
Malformed CArchive TOC rows identify their zero-based table index and byte offset
within the TOC, including truncated headers, invalid names, compression flags,
and type codes. The failure retains its original format reason.
PYZ `compression_flag` is derived from storage: nonempty payloads are compressed,
including legacy namespaces; an empty namespace has no compressed payload.
+14 -23
View File
@@ -137,6 +137,13 @@ export const parseCArchiveToc = (
): Result<readonly CArchiveEntry[], PyInstallerFailure> => {
const entries: CArchiveEntry[] = [];
let offset = 0;
const malformedEntry = (message: string): Result<never, PyInstallerFailure> =>
err(
pyInstallerFailure(
"format",
`CArchive table index ${String(entries.length)} at TOC offset ${String(offset)}: ${message}`,
),
);
while (offset < bytes.length) {
if (entries.length >= MAX_TOC_ENTRIES)
return err(
@@ -146,42 +153,26 @@ export const parseCArchiveToc = (
),
);
if (offset + TOC_HEADER_BYTES > bytes.length)
return err(
pyInstallerFailure(
"format",
"CArchive table of contents entry is truncated",
),
);
return malformedEntry("CArchive table of contents entry is truncated");
const entryLength = bytes.readUInt32BE(offset);
if (entryLength < TOC_HEADER_BYTES || offset + entryLength > bytes.length)
return err(
pyInstallerFailure(
"format",
"CArchive table of contents entry extends outside the table",
),
return malformedEntry(
"CArchive table of contents entry extends outside the table",
);
const nameBytes = bytes.subarray(
offset + TOC_HEADER_BYTES,
offset + entryLength,
);
const name = tocName(nameBytes);
if (!name.ok) return name;
if (!name.ok) return malformedEntry(name.error.message);
const compressionFlag = bytes[offset + 16] ?? 0;
if (compressionFlag !== 0 && compressionFlag !== 1)
return err(
pyInstallerFailure(
"format",
`CArchive compression flag ${String(compressionFlag)} is not stored or zlib`,
),
return malformedEntry(
`CArchive compression flag ${String(compressionFlag)} is not stored or zlib`,
);
const typeCode = bytes[offset + 17] ?? 0;
if (typeCode < 0x20 || typeCode > 0x7e)
return err(
pyInstallerFailure(
"format",
"CArchive type code is not a printable character",
),
);
return malformedEntry("CArchive type code is not a printable character");
entries.push({
index: entries.length,
tocRecordOffset: offset,
@@ -26,6 +26,45 @@ import {
type ArtifactEntry,
} from "../ArtifactReader.js";
it.each([0, 1, 2])(
"identifies malformed CArchive compression at table index %i",
async (index) => {
const original = cArchiveExecutable({
layout: "2.1",
pythonVersion: 309,
members: ["a", "b", "c"].map((name) => ({
name,
typeCode: "x",
payload: Buffer.from(name),
compress: false,
})),
});
const toc = 64 + original.readUInt32BE(original.length - 88 + 12);
let record = toc;
for (let row = 0; row < index; row++)
record += original.readUInt32BE(record);
await withPyInstallerReader(original, async (reader) => {
const entries = [];
for await (const entry of reader.entries()) entries.push(entry);
expect(entries).toHaveLength(3);
});
const malformed = Buffer.from(original);
malformed[record + 16] = 2;
await expect(
withPyInstallerReader(malformed, async (reader) => {
for await (const _entry of reader.entries()) {
/* force TOC admission */
}
}),
).rejects.toMatchObject({
reason: "format",
message: expect.stringContaining(
`table index ${index} at TOC offset ${record - toc}`,
),
});
},
);
it.each([
["empty", Buffer.alloc(0)],
["short", Buffer.from("630000", "hex")],