fix(python): preserve marshal names and packaged bytecode

Use one marshal surrogatepass decoder for PYZ directories and Python bytecode.
Validate ordinary UTF-8 spans with the native fatal decoder, preserving BOMs,
lone surrogates, raw bytes and marshal reference slots.

Distinguish legacy full-pyc CArchive modules from modern marshal-only modules
before reconstructing headers. Share typed source/codec failures between stream
output and completion while retaining native-read drain and cleanup ownership.

Validated focused producer, reader, lifecycle, CLI and MCP regressions, plus
public inventory/extraction of CPython 3.14-generated marshal tables and code.
No packaged application was executed. Document original versus derived bytes.

Fixes #6800
Fixes #6789
This commit is contained in:
morluto committed 2026-10-24 01:24:43 +08:00
1 parent 2443b949a4
commit f8f52f205a
18 files changed
+986 -109

No files matched your search

+37
View File
@@ -0,0 +1,37 @@
import { expect, it } from "vitest";
import { decodeMarshalUtf8 } from "./marshalUtf8.js";
it.each([
["efbbbfeda080efbbbfedbfbfefbbbf", "\ufeff\ud800\ufeff\udfff\ufeff"],
["ed9fbfeda080ee8080", "\ud7ff\ud800\ue000"],
["f0908080eda080f48fbfbf", "\ud800\udc00\ud800\udbff\udfff"],
["eda080edbfbf", "\ud800\udfff"],
["616263eda080646566edbfbf676869", "abc\ud800def\udfffghi"],
])(
"preserves ordinary UTF-8 spans around marshal surrogates (%s)",
(hex, value) => {
expect(decodeMarshalUtf8(Buffer.from(hex, "hex"))).toEqual({
ok: true,
value,
});
},
);
it.each([
"c2eda080",
"e282eda080",
"f09080eda080",
"80eda080",
"eda08080",
"eda080c2",
"eda080e08080",
"eda080f4908080",
"eda080eda0",
"eda0eda080",
])("does not hide malformed bytes beside a marshal surrogate (%s)", (hex) => {
expect(decodeMarshalUtf8(Buffer.from(hex, "hex"))).toEqual({
ok: false,
error: { kind: "invalid-marshal-utf8" },
});
});
+45
View File
@@ -0,0 +1,45 @@
import { err, ok, type Result } from "../result.js";
const UTF8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
/** CPython marshal uses UTF-8 surrogatepass; all other malformed bytes fail. */
export const decodeMarshalUtf8 = (
bytes: Buffer,
): Result<string, { readonly kind: "invalid-marshal-utf8" }> => {
const chunks: string[] = [];
let start = 0;
try {
for (
let offset = bytes.indexOf(0xed);
offset !== -1;
offset = bytes.indexOf(0xed, offset + 1)
) {
const second = bytes[offset + 1];
const third = bytes[offset + 2];
if (
second === undefined ||
second < 0xa0 ||
second > 0xbf ||
third === undefined ||
third < 0x80 ||
third > 0xbf
)
continue;
// Only ED A0..BF 80..BF encodes a surrogate. The fatal decoder owns
// every unchanged span, including incomplete scalars on either side.
chunks.push(
UTF8.decode(bytes.subarray(start, offset)),
String.fromCharCode(0xd000 | ((second & 0x3f) << 6) | (third & 0x3f)),
);
start = offset + 3;
offset += 2;
}
if (start === 0) return ok(UTF8.decode(bytes));
chunks.push(UTF8.decode(bytes.subarray(start)));
return ok(chunks.join(""));
} catch (cause: unknown) {
if (cause instanceof TypeError)
return err({ kind: "invalid-marshal-utf8" });
throw cause;
}
};