mirror of
https://github.com/morluto/rea.git
synced 2026-11-06 23:06:18 +00:00
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:
1 parent
2443b949a4
commit
f8f52f205a
18 files changed
+986
-109
No files matched your search
@@ -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" },
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
Reference in new issue
Block a user