mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 11:13:21 +00:00
refactor(rust): report cargo-auditable section failures through ParseFailure (#10652)
* refactor(rust): report cargo-auditable section failures through ParseFailure RustAuditableFormatFailure restated the malformed/unsupported algebra and the provider translated it by hand; container BinaryFormatFailures were re-wrapped and lost their kind. The reader now throws ParseFailureError (format rust-auditable), container failures propagate unchanged, and the provider maps both through parseFailureAnalysisError. A counted guard keeps new per-format failure classes from returning. * test(rust): pin the container's truncated failure facts through the Rust reader
This commit is contained in:
1 parent
3e0dc29041
commit
29fb608559
7 files changed
+115
-95
No files matched your search
@@ -205,6 +205,19 @@
|
||||
"src/wasm/WabtArtifactProvider.ts": 2,
|
||||
"src/wasm/WabtFailures.ts": 2
|
||||
},
|
||||
"parser-format-failure-class": {
|
||||
"src/browser/assets/WebArtifactReads.ts": 1,
|
||||
"src/browser/history/CaptureFormatError.ts": 1,
|
||||
"src/domain/xmlDocument.ts": 1,
|
||||
"src/il2cpp/Il2cppBinaryFormat.ts": 1,
|
||||
"src/il2cpp/Il2cppMetadataFormat.ts": 1,
|
||||
"src/javascript/sourceMaps/SourceMapFormat.ts": 1,
|
||||
"src/model/ModelFormat.ts": 1,
|
||||
"src/native/AppleMachoSelection.ts": 1,
|
||||
"src/native/MachoCommandNames.ts": 1,
|
||||
"src/native/apple/AppleIps.errors.ts": 1,
|
||||
"src/python/PythonPyc.errors.ts": 1
|
||||
},
|
||||
"parser-json-parse": {
|
||||
"src/android/JadxProtocol.ts": 2,
|
||||
"src/android/JadxWorkspaceRoot.ts": 1,
|
||||
|
||||
@@ -283,6 +283,17 @@ checkCounted(
|
||||
[],
|
||||
);
|
||||
|
||||
// A per-format failure class restates the ParseFailure algebra under its own
|
||||
// spelling and needs its own catch-site translation; parsers throw a
|
||||
// ParseFailureError (or a subclass of it) instead.
|
||||
checkCounted(
|
||||
"per-format failure class extending Error (throw a ParseFailureError from src/domain/parseFailure)",
|
||||
"parser-format-failure-class",
|
||||
files,
|
||||
/class \w*(?:Format|Parse)(?:Failure|Error) extends Error\b/gu,
|
||||
["src/domain/parseFailure.ts"],
|
||||
);
|
||||
|
||||
// JSON.parse keeps the last of repeated member names without saying so; a
|
||||
// parser of artifact content must state its duplicate-member policy
|
||||
// (src/domain/jsonDuplicateMember.ts finds a repeated name).
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { deflateSync } from "node:zlib";
|
||||
import { expect, it } from "vitest";
|
||||
import { rustAuditableMetadataSchema } from "../domain/rust/rustBinary.js";
|
||||
import { ParseFailureError } from "../domain/parseFailure.js";
|
||||
|
||||
import {
|
||||
readAuditableSection,
|
||||
RustAuditableFormatFailure,
|
||||
RustAuditableResourceFailure,
|
||||
} from "./RustAuditableReader.js";
|
||||
|
||||
@@ -112,7 +112,7 @@ it("preserves historical cargo-auditable feature lists and rejects bad values",
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toThrow(RustAuditableFormatFailure);
|
||||
).toThrow(ParseFailureError);
|
||||
});
|
||||
|
||||
it("reports dependency cycles and keeps an omitted format version unknown", () => {
|
||||
@@ -163,7 +163,7 @@ it("rejects out-of-range indices, multiple roots, and schema drift", () => {
|
||||
{ name: "a", version: "1", source: "local", dependencies: [2] },
|
||||
],
|
||||
}),
|
||||
).toThrow(RustAuditableFormatFailure);
|
||||
).toThrow(ParseFailureError);
|
||||
expect(() =>
|
||||
section({
|
||||
packages: [
|
||||
@@ -196,7 +196,7 @@ it("rejects invalid JSON, trailing zlib bytes, and an empty section", () => {
|
||||
);
|
||||
expect(() =>
|
||||
readAuditableSection(".dep-v0", 0, Buffer.from([1, 2, 3])),
|
||||
).toThrow(RustAuditableFormatFailure);
|
||||
).toThrow(ParseFailureError);
|
||||
});
|
||||
|
||||
it("rejects duplicate decoded JSON fields before object conversion", () => {
|
||||
@@ -353,8 +353,11 @@ it.each([-1, 1.5, 0x1_0000_0000])(
|
||||
(format) => {
|
||||
expect(() => section({ format, packages: [] })).toThrow(
|
||||
expect.objectContaining({
|
||||
name: "RustAuditableFormatFailure",
|
||||
kind: "malformed",
|
||||
name: "ParseFailureError",
|
||||
failure: expect.objectContaining({
|
||||
kind: "malformed",
|
||||
format: "rust-auditable",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
type RustPackage,
|
||||
} from "../domain/rust/rustBinary.js";
|
||||
import { jsonValueByteLength } from "../domain/jsonValueByteLength.js";
|
||||
import { malformedInput, ParseFailureError } from "../domain/parseFailure.js";
|
||||
|
||||
/** Compressed zlib stream limit. */
|
||||
const RUST_SECTION_BYTES = 1024 * 1024;
|
||||
@@ -36,20 +37,15 @@ const documentSchema = documentVersionSchema
|
||||
})
|
||||
.strict();
|
||||
|
||||
/** Malformed or unsupported dependency-section bytes. */
|
||||
export class RustAuditableFormatFailure extends Error {
|
||||
/** The image holds the metadata, in a form this reader cannot decode. */
|
||||
readonly undecodedContent: boolean;
|
||||
constructor(
|
||||
readonly kind: "malformed" | "unsupported",
|
||||
message: string,
|
||||
options?: ErrorOptions & { readonly undecodedContent?: boolean },
|
||||
) {
|
||||
super(message, options);
|
||||
this.undecodedContent = options?.undecodedContent ?? false;
|
||||
this.name = "RustAuditableFormatFailure";
|
||||
}
|
||||
}
|
||||
/** The ParseFailure format of `cargo auditable` dependency sections. */
|
||||
export const RUST_AUDITABLE_FORMAT = "rust-auditable";
|
||||
|
||||
/** Malformed section bytes, keeping the decoder or schema failure that showed it. */
|
||||
const malformedSection = (detail: string, cause: unknown) =>
|
||||
new ParseFailureError(
|
||||
{ kind: "malformed", format: RUST_AUDITABLE_FORMAT, detail },
|
||||
{ cause },
|
||||
);
|
||||
|
||||
/** A resource admission limit with explicit semantic units. */
|
||||
export type RustAuditableConstraint =
|
||||
@@ -85,10 +81,7 @@ export const readAuditableSection = (
|
||||
} = {},
|
||||
): RustAuditableMetadata => {
|
||||
if (bytes.length === 0)
|
||||
throw new RustAuditableFormatFailure(
|
||||
"malformed",
|
||||
"Dependency section is empty",
|
||||
);
|
||||
throw malformedInput(RUST_AUDITABLE_FORMAT, "Dependency section is empty");
|
||||
if (
|
||||
bytes.length > RUST_SECTION_BYTES &&
|
||||
options.peFileAlignment === undefined
|
||||
@@ -172,13 +165,13 @@ const inflateSection = (
|
||||
const alignedRawSize =
|
||||
Math.ceil(compressedBytes / peFileAlignment) * peFileAlignment;
|
||||
if (bytes.length !== alignedRawSize || padding.some((byte) => byte !== 0))
|
||||
throw new RustAuditableFormatFailure(
|
||||
"malformed",
|
||||
throw malformedInput(
|
||||
RUST_AUDITABLE_FORMAT,
|
||||
"Dependency section has bytes after the zlib stream",
|
||||
);
|
||||
} else if (compressedBytes !== bytes.length) {
|
||||
throw new RustAuditableFormatFailure(
|
||||
"malformed",
|
||||
throw malformedInput(
|
||||
RUST_AUDITABLE_FORMAT,
|
||||
"Dependency section has bytes after the zlib stream",
|
||||
);
|
||||
}
|
||||
@@ -219,8 +212,8 @@ const inflateFailure = (
|
||||
code.startsWith("Z_") ||
|
||||
code === "ERR_INVALID_ARG_TYPE"
|
||||
)
|
||||
return new RustAuditableFormatFailure(
|
||||
"malformed",
|
||||
return malformedInput(
|
||||
RUST_AUDITABLE_FORMAT,
|
||||
"Dependency section is not a complete zlib stream",
|
||||
);
|
||||
return cause instanceof Error ? cause : new Error(String(cause));
|
||||
@@ -234,11 +227,7 @@ const parseDocument = (
|
||||
try {
|
||||
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
||||
} catch (cause) {
|
||||
throw new RustAuditableFormatFailure(
|
||||
"malformed",
|
||||
"Dependency JSON is not valid UTF-8",
|
||||
{ cause },
|
||||
);
|
||||
throw malformedSection("Dependency JSON is not valid UTF-8", cause);
|
||||
}
|
||||
assertDepth(text);
|
||||
let invalidJson = false;
|
||||
@@ -269,42 +258,36 @@ const parseDocument = (
|
||||
},
|
||||
);
|
||||
if (invalidJson) {
|
||||
throw new RustAuditableFormatFailure(
|
||||
"malformed",
|
||||
throw malformedInput(
|
||||
RUST_AUDITABLE_FORMAT,
|
||||
"Dependency section JSON is invalid",
|
||||
);
|
||||
}
|
||||
if (duplicate !== undefined)
|
||||
throw new RustAuditableFormatFailure(
|
||||
"malformed",
|
||||
throw malformedInput(
|
||||
RUST_AUDITABLE_FORMAT,
|
||||
`Dependency JSON contains duplicate field ${JSON.stringify(duplicate)}`,
|
||||
);
|
||||
let value: unknown;
|
||||
try {
|
||||
value = JSON.parse(text);
|
||||
} catch (cause) {
|
||||
throw new RustAuditableFormatFailure(
|
||||
"malformed",
|
||||
"Dependency section JSON is invalid",
|
||||
{ cause },
|
||||
);
|
||||
throw malformedSection("Dependency section JSON is invalid", cause);
|
||||
}
|
||||
const version = documentVersionSchema.safeParse(value);
|
||||
if (!version.success)
|
||||
throw new RustAuditableFormatFailure(
|
||||
"malformed",
|
||||
throw malformedSection(
|
||||
"Dependency JSON does not match the format schema",
|
||||
{ cause: version.error },
|
||||
version.error,
|
||||
);
|
||||
admitGraphExpansion(value, maxOutputBytes);
|
||||
// One malformed package must fail before Zod retains an issue for every field.
|
||||
rejectMalformedPackages(value);
|
||||
const parsed = documentSchema.safeParse(value);
|
||||
if (!parsed.success)
|
||||
throw new RustAuditableFormatFailure(
|
||||
"malformed",
|
||||
throw malformedSection(
|
||||
"Dependency JSON does not match the package schema",
|
||||
{ cause: parsed.error },
|
||||
parsed.error,
|
||||
);
|
||||
return parsed.data;
|
||||
};
|
||||
@@ -404,8 +387,8 @@ const rejectMalformedPackages = (value: unknown): void => {
|
||||
return;
|
||||
for (const [index, pkg] of value.packages.entries()) {
|
||||
if (!packageRecord(pkg))
|
||||
throw new RustAuditableFormatFailure(
|
||||
"malformed",
|
||||
throw malformedInput(
|
||||
RUST_AUDITABLE_FORMAT,
|
||||
`Dependency package ${String(index)} does not match the package schema`,
|
||||
);
|
||||
}
|
||||
@@ -475,8 +458,8 @@ const assertDepth = (text: string): void => {
|
||||
if (char === "{" || char === "[") {
|
||||
depth += 1;
|
||||
if (depth > JSON_DEPTH)
|
||||
throw new RustAuditableFormatFailure(
|
||||
"malformed",
|
||||
throw malformedInput(
|
||||
RUST_AUDITABLE_FORMAT,
|
||||
"Dependency JSON nesting exceeds 32 levels",
|
||||
);
|
||||
} else if ((char === "}" || char === "]") && depth > 0) depth -= 1;
|
||||
@@ -499,8 +482,8 @@ const normalizePackages = (
|
||||
const rootIndex = (packages: readonly RustPackage[]): number | null => {
|
||||
const roots = packages.flatMap((pkg, index) => (pkg.root ? [index] : []));
|
||||
if (roots.length > 1)
|
||||
throw new RustAuditableFormatFailure(
|
||||
"malformed",
|
||||
throw malformedInput(
|
||||
RUST_AUDITABLE_FORMAT,
|
||||
"Dependency JSON declares more than one root package",
|
||||
);
|
||||
for (const [index, pkg] of packages.entries()) {
|
||||
@@ -508,8 +491,8 @@ const rootIndex = (packages: readonly RustPackage[]): number | null => {
|
||||
(target) => target >= packages.length,
|
||||
);
|
||||
if (outside !== undefined)
|
||||
throw new RustAuditableFormatFailure(
|
||||
"malformed",
|
||||
throw malformedInput(
|
||||
RUST_AUDITABLE_FORMAT,
|
||||
`Dependency index ${String(outside)} from package ${String(index)} is outside the package list`,
|
||||
);
|
||||
}
|
||||
|
||||
+10
-14
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
BinaryFormatFailure,
|
||||
BinaryResourceFailure,
|
||||
type BinaryContainer,
|
||||
type ImageSection,
|
||||
} from "../native/metadata/BinaryContainer.js";
|
||||
import { malformedInput, unsupportedInput } from "../domain/parseFailure.js";
|
||||
import { readBinaryContainer } from "../native/metadata/readBinaryContainer.js";
|
||||
import type {
|
||||
RustAuditableMetadata,
|
||||
@@ -13,7 +13,7 @@ import { DEFAULT_RUSTC_HASH_LIMIT } from "../domain/rust/rustBinary.js";
|
||||
import { RUST_PRODUCER_DEFAULTS } from "../domain/rust/rustResourceLimits.js";
|
||||
import {
|
||||
readAuditableSection,
|
||||
RustAuditableFormatFailure,
|
||||
RUST_AUDITABLE_FORMAT,
|
||||
RustAuditableResourceFailure,
|
||||
} from "./RustAuditableReader.js";
|
||||
|
||||
@@ -59,12 +59,8 @@ const readContainer = (bytes: Buffer) => {
|
||||
try {
|
||||
return readBinaryContainer(bytes);
|
||||
} catch (cause) {
|
||||
if (cause instanceof BinaryFormatFailure)
|
||||
throw new RustAuditableFormatFailure(
|
||||
cause.kind === "unsupported" ? "unsupported" : "malformed",
|
||||
cause.message,
|
||||
{ cause },
|
||||
);
|
||||
// A container format failure is already a ParseFailureError; it keeps
|
||||
// its own kind and facts instead of being restated as a Rust failure.
|
||||
if (cause instanceof BinaryResourceFailure)
|
||||
throw new RustAuditableResourceFailure(
|
||||
{
|
||||
@@ -87,21 +83,21 @@ const dependencySection = (
|
||||
const expected = format === "macho" ? "__DATA,.dep-v0" : ".dep-v0";
|
||||
const matches = sections.filter((section) => section.name === expected);
|
||||
if (matches.length > 1)
|
||||
throw new RustAuditableFormatFailure(
|
||||
"malformed",
|
||||
throw malformedInput(
|
||||
RUST_AUDITABLE_FORMAT,
|
||||
"Image contains more than one dependency section",
|
||||
);
|
||||
const section = matches[0];
|
||||
if (section === undefined) return null;
|
||||
if (section.compressed)
|
||||
throw new RustAuditableFormatFailure(
|
||||
"unsupported",
|
||||
throw unsupportedInput(
|
||||
RUST_AUDITABLE_FORMAT,
|
||||
"Compressed dependency sections are unsupported",
|
||||
{ undecodedContent: true },
|
||||
);
|
||||
if (!section.file_backed)
|
||||
throw new RustAuditableFormatFailure(
|
||||
"malformed",
|
||||
throw malformedInput(
|
||||
RUST_AUDITABLE_FORMAT,
|
||||
"Dependency section is not file-backed",
|
||||
);
|
||||
return section;
|
||||
|
||||
@@ -23,10 +23,8 @@ import { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import {
|
||||
AnalysisArtifactChangedError,
|
||||
AnalysisCancelledError,
|
||||
AnalysisInputError,
|
||||
AnalysisOutputError,
|
||||
AnalysisResourceConstraintError,
|
||||
AnalysisUnsupportedTargetError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import {
|
||||
RUST_AUDITABLE_FORMAT_VERSIONS,
|
||||
@@ -42,10 +40,8 @@ import {
|
||||
type RustBinaryImage,
|
||||
} from "./RustBinaryImage.js";
|
||||
import { requireSelectedNativeImage } from "../native/metadata/MachoSliceSelection.js";
|
||||
import {
|
||||
RustAuditableFormatFailure,
|
||||
RustAuditableResourceFailure,
|
||||
} from "./RustAuditableReader.js";
|
||||
import { RustAuditableResourceFailure } from "./RustAuditableReader.js";
|
||||
import { parseFailureAnalysisError } from "../domain/parseFailureAnalysisError.js";
|
||||
|
||||
import {
|
||||
resolveRustProducerLimits,
|
||||
@@ -347,20 +343,10 @@ const failureForPrimary = (
|
||||
)),
|
||||
},
|
||||
);
|
||||
if (cause instanceof RustAuditableFormatFailure)
|
||||
return cause.kind === "unsupported"
|
||||
? new AnalysisUnsupportedTargetError(OPERATION, path, cause.message, {
|
||||
cause,
|
||||
undecodedContent: cause.undecodedContent,
|
||||
})
|
||||
: new AnalysisInputError(OPERATION, { cause }, [
|
||||
{
|
||||
path: ["path"],
|
||||
reason: "invalid_format",
|
||||
message: `Selected artifact ${path}: ${cause.message}`,
|
||||
},
|
||||
]);
|
||||
return artifactFailure(facts, path, limits);
|
||||
return (
|
||||
parseFailureAnalysisError(OPERATION, path, cause) ??
|
||||
artifactFailure(facts, path, limits)
|
||||
);
|
||||
};
|
||||
|
||||
const failureFor = (
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { readRustBinaryImage } from "../../../src/rust/RustBinaryImage.js";
|
||||
import { readBinaryContainer } from "../../../src/native/metadata/readBinaryContainer.js";
|
||||
import { readGoBinaryImage } from "../../../src/go/GoBinaryImage.js";
|
||||
import { RustAuditableFormatFailure } from "../../../src/rust/RustAuditableReader.js";
|
||||
import { ParseFailureError } from "../../../src/domain/parseFailure.js";
|
||||
|
||||
const packages = [
|
||||
{
|
||||
@@ -306,11 +306,39 @@ it("finds rustc paths in read-only ELF load data without section headers", () =>
|
||||
|
||||
it("refuses a universal Mach-O container", () => {
|
||||
expect(() => readRustBinaryImage(fat([0x0100000c]))).toThrow(
|
||||
RustAuditableFormatFailure,
|
||||
ParseFailureError,
|
||||
);
|
||||
expect(() => readRustBinaryImage(fat([0x0100000c]))).toThrow(/thin slice/);
|
||||
});
|
||||
|
||||
it("keeps the container's truncated failure and its facts unchanged", () => {
|
||||
const bytes = createRustImage({ format: "elf" });
|
||||
// The fixture's first program header is at byte 128; p_filesz and
|
||||
// p_memsz are at +32 and +40.
|
||||
bytes.writeBigUInt64LE(BigInt(bytes.length) * 2n, 128 + 32);
|
||||
bytes.writeBigUInt64LE(BigInt(bytes.length) * 2n, 128 + 40);
|
||||
const thrown = (read: () => unknown): unknown => {
|
||||
try {
|
||||
read();
|
||||
} catch (cause: unknown) {
|
||||
return cause;
|
||||
}
|
||||
throw new Error("expected a container failure");
|
||||
};
|
||||
const container = thrown(() => readBinaryContainer(bytes));
|
||||
const rust = thrown(() => readRustBinaryImage(bytes));
|
||||
expect(rust).toBeInstanceOf(ParseFailureError);
|
||||
expect((rust as ParseFailureError).failure).toEqual(
|
||||
(container as ParseFailureError).failure,
|
||||
);
|
||||
expect((rust as ParseFailureError).failure).toMatchObject({
|
||||
kind: "truncated",
|
||||
format: "elf",
|
||||
offset: expect.any(Number),
|
||||
field: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it("does not read SHT_NOBITS dependency sections as stored bytes", () => {
|
||||
const bytes = createRustImage({ format: "elf" });
|
||||
// The fixture writes the .dep-v0 section header at byte 384.
|
||||
@@ -319,7 +347,7 @@ it("does not read SHT_NOBITS dependency sections as stored bytes", () => {
|
||||
try {
|
||||
metadata = readRustBinaryImage(bytes).auditable_metadata;
|
||||
} catch (cause: unknown) {
|
||||
expect(cause).toBeInstanceOf(RustAuditableFormatFailure);
|
||||
expect(cause).toBeInstanceOf(ParseFailureError);
|
||||
return;
|
||||
}
|
||||
expect(metadata).toBeNull();
|
||||
|
||||
Reference in new issue
Block a user