mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 19:23:24 +00:00
refactor(python): report pyc format failures through ParseFailure (#10664)
This commit is contained in:
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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user