refactor(python): report pyc format failures through ParseFailure (#10664)

This commit is contained in:
morluto authored and GitHub committed 2026-11-15 13:45:40 +08:00
1 parent 51e9dc156f
commit b33e5c4867
5 files changed
+39 -65

No files matched your search

@@ -182,8 +182,7 @@
"src/browser/assets/WebArtifactReads.ts": 1,
"src/domain/xmlDocument.ts": 1,
"src/native/AppleMachoSelection.ts": 1,
"src/native/MachoCommandNames.ts": 1,
"src/python/PythonPyc.errors.ts": 1
"src/native/MachoCommandNames.ts": 1
},
"parser-json-parse": {
"src/android/JadxProtocol.ts": 2,
+2 -10
View File
@@ -1,15 +1,7 @@
import type { PYTHON_PYC_LIMIT_MAXIMA } from "../domain/python/pythonPyc.js";
/** Distinguish corrupt recognized pycs from magics outside parser coverage. */
export class PythonPycFormatFailure extends Error {
constructor(
readonly kind: "malformed" | "unsupported",
message: string,
) {
super(message);
this.name = "PythonPycFormatFailure";
}
}
/** The ParseFailure format of a standalone CPython pyc. */
export const PYTHON_PYC_FORMAT = "python-pyc";
/** Resource boundary exceeded while walking marshal objects. */
export class PythonPycResourceFailure extends Error {
+27 -26
View File
@@ -11,10 +11,8 @@ import {
pythonPycHeader,
} from "./PythonPyc.fixture.js";
import { parsePythonPyc } from "./PythonPycParser.js";
import {
PythonPycFormatFailure,
PythonPycResourceFailure,
} from "./PythonPyc.errors.js";
import { PythonPycResourceFailure } from "./PythonPyc.errors.js";
import { ParseFailureError } from "../domain/parseFailure.js";
const defaults = inspectPythonPycInputSchema.parse({
path: "/artifacts/sample.pyc",
@@ -133,7 +131,7 @@ it("decodes a hash-based header and a FLAG_REF top-level code object", () => {
it("rejects truncated marshal without partial success", () => {
expect(() => parse(pythonPycFixture("py311").subarray(0, 20))).toThrow(
PythonPycFormatFailure,
ParseFailureError,
);
});
@@ -169,8 +167,13 @@ it("keeps CPython 3.16 pre-release magics outside standalone decoder coverage",
it("does not qualify a magic outside the pinned identity table", () => {
expect(() => parse(pythonPycHeader(1))).toThrow(
expect.objectContaining({
kind: "unsupported",
message: expect.stringMatching(/not a pinned CPython identity.*header/iu),
failure: expect.objectContaining({
kind: "unsupported",
format: "python-pyc",
detail: expect.stringMatching(
/not a pinned CPython identity.*header/iu,
),
}),
}),
);
});
@@ -481,10 +484,10 @@ it("charges long digits to the string budget and keeps large previews bounded",
expect(root(report).consts[1]).toMatchObject({ kind: "int", value: null });
expect(pythonPycSchema.safeParse(report).success).toBe(true);
expect(() => parse(withConsts([marshalLong([1, 0])]))).toThrow(
PythonPycFormatFailure,
ParseFailureError,
);
expect(() => parse(withConsts([marshalLong([0x8000])]))).toThrow(
PythonPycFormatFailure,
ParseFailureError,
);
});
@@ -522,10 +525,10 @@ it("decodes marshal strings with CPython surrogatepass rules", () => {
});
expect(() =>
parse(withConsts([marshalUnicode(Buffer.from([0xf0, 0x90, 0x80]))])),
).toThrow(PythonPycFormatFailure);
).toThrow(ParseFailureError);
expect(() =>
parse(withConsts([marshalUnicode(Buffer.from([0xc0, 0x80]))])),
).toThrow(PythonPycFormatFailure);
).toThrow(ParseFailureError);
});
it.each([0x73, 0x74, 0x75, 0x61, 0x41, 0x28, 0x5b, 0x3c, 0x3e])(
@@ -533,7 +536,7 @@ it.each([0x73, 0x74, 0x75, 0x61, 0x41, 0x28, 0x5b, 0x3c, 0x3e])(
(tag) => {
expect(() =>
parse(withConsts([Buffer.concat([Buffer.from([tag]), i32(-1)])])),
).toThrow(PythonPycFormatFailure);
).toThrow(ParseFailureError);
},
);
@@ -589,7 +592,7 @@ it("rejects a code object whose location tables are not bytes", () => {
marshalCode("py311", { exceptiontableObject: Buffer.from([0x53]) }),
]),
),
).toThrow(PythonPycFormatFailure);
).toThrow(ParseFailureError);
});
it("hashes one shared byte string no matter how many times it is referenced", () => {
@@ -658,7 +661,7 @@ it("rejects constant tables whose copies exceed max_objects", () => {
it("rejects NULL tuple items and mismatched local kinds", () => {
expect(() => parse(withConsts([Buffer.from([0x30])]))).toThrow(
PythonPycFormatFailure,
ParseFailureError,
);
expect(
root(parse(withConsts([Buffer.from([0x7b, 0x4e, 0x54, 0x30])]))).consts[0],
@@ -676,7 +679,7 @@ it("rejects NULL tuple items and mismatched local kinds", () => {
}),
]),
),
).toThrow(PythonPycFormatFailure);
).toThrow(ParseFailureError);
const matched = parse(
Buffer.concat([
pythonPycHeader(3531),
@@ -881,7 +884,7 @@ it("reads marshal version 0 text floats inside a code object", () => {
]);
for (const text of ["1.5x", "", "0x1.8p1", " 1.5"]) {
expect(() => parse(withConsts([textFloat(text)]))).toThrow(
PythonPycFormatFailure,
ParseFailureError,
);
}
});
@@ -993,10 +996,10 @@ it("applies Python container rules before reporting lengths", () => {
]),
]),
),
).toThrow(PythonPycFormatFailure);
).toThrow(ParseFailureError);
expect(() =>
parse(withConsts([setOf(Buffer.concat([Buffer.from([0x5b]), u32(0)]))])),
).toThrow(PythonPycFormatFailure);
).toThrow(ParseFailureError);
});
it("rejects a shared tuple key before its expansion doubles without bound", () => {
@@ -1164,14 +1167,12 @@ it("rejects code keys and members whose consts are unhashable", () => {
Buffer.concat([Buffer.from([0x7b]), key, int, Buffer.from([0x30])]);
for (const key of [code(list), code(tuple(int, list)), code(code(list))]) {
expect(() => parse(withConsts([setOf(0x3c, key)]))).toThrow(
PythonPycFormatFailure,
ParseFailureError,
);
expect(() => parse(withConsts([setOf(0x3e, key)]))).toThrow(
PythonPycFormatFailure,
);
expect(() => parse(withConsts([dictKey(key)]))).toThrow(
PythonPycFormatFailure,
ParseFailureError,
);
expect(() => parse(withConsts([dictKey(key)]))).toThrow(ParseFailureError);
}
const cyclic = Buffer.concat([
Buffer.from([0x29 | 0x80, 1]),
@@ -1183,7 +1184,7 @@ it("rejects code keys and members whose consts are unhashable", () => {
}),
]);
expect(() => parse(withConsts([cyclic, setOf(0x3c, marshalRef(1))]))).toThrow(
PythonPycFormatFailure,
ParseFailureError,
);
// A set inside a tuple that reaches the unfinished tuple, directly, inside
@@ -1211,7 +1212,7 @@ it("rejects code keys and members whose consts are unhashable", () => {
]),
]),
),
).toThrow(PythonPycFormatFailure);
).toThrow(ParseFailureError);
const unhashed = parse(withConsts([tuple(code(list))]));
expect(unhashed.coverage.status).toBe("complete");
@@ -1522,7 +1523,7 @@ it("rejects negative CPython code counts and keeps them out of the schema", () =
"stacksize",
] as const) {
expect(() => parse(negative("py311", { [field]: 0xffffffff }))).toThrow(
PythonPycFormatFailure,
ParseFailureError,
);
}
expect(() => parse(negative("py37", { nlocals: 0xffffffff }))).toThrow(
+4 -3
View File
@@ -15,8 +15,9 @@ import {
type PythonPycCodeObject,
type PythonPycConst,
} from "../domain/python/pythonPyc.js";
import { malformedInput, unsupportedInput } from "../domain/parseFailure.js";
import {
PythonPycFormatFailure,
PYTHON_PYC_FORMAT,
PythonPycResourceFailure,
} from "./PythonPyc.errors.js";
import { pythonPycBytecodeIdentity } from "./PythonPycBytecodeEquality.js";
@@ -179,7 +180,7 @@ type NameTable = {
};
const malformed = (message: string): never => {
throw new PythonPycFormatFailure("malformed", message);
throw malformedInput(PYTHON_PYC_FORMAT, message);
};
/** Locals CPython's 3.11+ validator counts toward `co_varnames`. */
@@ -193,7 +194,7 @@ const fastLocalCount = (kinds: Buffer): number => {
};
const unsupported = (message: string): never => {
throw new PythonPycFormatFailure("unsupported", message);
throw unsupportedInput(PYTHON_PYC_FORMAT, message);
};
const digests = new WeakMap<Buffer, string>();
+5 -24
View File
@@ -19,21 +19,15 @@ import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import type { PythonPycPort } from "../application/python/PythonPycPort.js";
import type { ExecutionOptions } from "../application/AnalysisProvider.js";
import { AnalysisError } from "../domain/analysisErrorBase.js";
import {
AnalysisInputError,
AnalysisResourceConstraintError,
AnalysisUnsupportedTargetError,
} from "../domain/analysisErrorCore.js";
import { AnalysisResourceConstraintError } from "../domain/analysisErrorCore.js";
import {
PYTHON_PYC_LIMIT_MAXIMA,
type InspectPythonPycInput,
} from "../domain/python/pythonPyc.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import { err, ok } from "../domain/result.js";
import {
PythonPycFormatFailure,
PythonPycResourceFailure,
} from "./PythonPyc.errors.js";
import { PythonPycResourceFailure } from "./PythonPyc.errors.js";
import { parseFailureAnalysisError } from "../domain/parseFailureAnalysisError.js";
import { parsePythonPyc } from "./PythonPycParser.js";
const OPERATION = "inspect_python_pyc";
@@ -83,21 +77,8 @@ const failureForPrimary = (
cause.reported_limits,
{ cause, ...parserLimitRecovery(cause.reported_limits) },
);
if (cause instanceof PythonPycFormatFailure)
return cause.kind === "unsupported"
? new AnalysisUnsupportedTargetError(
OPERATION,
input.path,
cause.message,
{ cause },
)
: new AnalysisInputError(OPERATION, { cause }, [
{
path: ["path"],
reason: "invalid_format",
message: `Selected pyc ${input.path}: ${cause.message}`,
},
]);
const formatFailure = parseFailureAnalysisError(OPERATION, input.path, cause);
if (formatFailure !== undefined) return formatFailure;
if (
origin === "owned-or-classified" &&
cause instanceof ArtifactReaderFailure