mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
fix(pyinstaller): attribute malformed TOC rows at their owner
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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")],
|
||||
|
||||
Reference in New Issue
Block a user