fix(domain): replace bare parser errors with typed codes (#8057)
* fix(domain): replace bare parser errors with typed codes OpenStep syntax, canonical JSON digests, managed-graph source checks, frame fingerprints, and reference-flow invariants threw Error and published the sentence. Each refusal now carries a code. Tests assert that code, including OpenStep position, instead of the previous prose. * test(domain): pin OpenStep refusal codes and positions The syntax cases accepted any code and any offset. Each container refusal now expects its code and source position, including the deep suffix that changes (x from unterminated_array to expected_separator. Scalar failures stay identical to the pinned decoder. * test(filesystem): assert OpenStep keyed-archive codes The deep-suffix keyed archive cases compared ArtifactReaderFailure message text with the pinned decoder sentence. They now require reason format and the OpenStep code and source position on the cause. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
1 parent
a4c183b750
commit
b663c79552
12 files changed
+232
-74
No files matched your search
@@ -7,6 +7,7 @@ import type {
|
||||
import type { RetainedBrowserScenarioCapture } from "./browserScenarioCapture.js";
|
||||
import {
|
||||
fingerprintScenarioNetworkTransactions,
|
||||
ScenarioNetworkFingerprintError,
|
||||
scenarioNetworkTransactions,
|
||||
scenarioNetworkCoverageReason,
|
||||
} from "./browserScenarioNetworkComparison.js";
|
||||
@@ -163,8 +164,7 @@ export const prepareScenarioEvents = (
|
||||
for (const [index, node] of nodes.entries()) {
|
||||
const step = networkByStep.get(node.step_index) ?? [];
|
||||
const fingerprint = fingerprints[index];
|
||||
if (fingerprint === undefined)
|
||||
throw new Error("Network comparison node has no fingerprint");
|
||||
if (fingerprint === undefined) throw new ScenarioNetworkFingerprintError();
|
||||
step.push(fingerprint);
|
||||
networkByStep.set(node.step_index, step);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,17 @@ import { digestCanonicalValue } from "./canonicalDigest.js";
|
||||
import { normalizeScenarioValue } from "./browserScenarioNormalization.js";
|
||||
import { indexBrowserScenarioTransactions } from "./browserScenarioTransactions.js";
|
||||
|
||||
/** A comparison node lost the fingerprint its producer should have stored. */
|
||||
export class ScenarioNetworkFingerprintError extends Error {
|
||||
readonly _tag = "ScenarioNetworkFingerprintError";
|
||||
readonly code = "missing_fingerprint" as const;
|
||||
|
||||
constructor() {
|
||||
super("missing_fingerprint");
|
||||
this.name = "ScenarioNetworkFingerprintError";
|
||||
}
|
||||
}
|
||||
|
||||
/** Numeric links are temporary local indices, never durable transaction identity. */
|
||||
export const scenarioNetworkTransactionSchema = z.strictObject({
|
||||
events: z.array(z.unknown()),
|
||||
@@ -241,8 +252,7 @@ export const fingerprintScenarioNetworkTransactions = (
|
||||
}
|
||||
return nodes.map((_, index) => {
|
||||
const fingerprint = fingerprints.get(index);
|
||||
if (fingerprint === undefined)
|
||||
throw new Error("Network comparison node has no fingerprint");
|
||||
if (fingerprint === undefined) throw new ScenarioNetworkFingerprintError();
|
||||
return fingerprint;
|
||||
});
|
||||
};
|
||||
@@ -4,7 +4,10 @@ import { fc, it } from "@fast-check/vitest";
|
||||
import canonicalize from "canonicalize";
|
||||
import { describe, expect } from "vitest";
|
||||
|
||||
import { digestCanonicalValue } from "./canonicalDigest.js";
|
||||
import {
|
||||
CanonicalDigestError,
|
||||
digestCanonicalValue,
|
||||
} from "./canonicalDigest.js";
|
||||
|
||||
const legacyDigest = (value: unknown): string => {
|
||||
const encoded = canonicalize(value);
|
||||
@@ -52,8 +55,27 @@ describe("incremental canonical digest", () => {
|
||||
const cyclic: Record<string, unknown> = {};
|
||||
cyclic.self = cyclic;
|
||||
const recursiveJson = { toJSON: (): unknown => recursiveJson };
|
||||
for (const value of [NaN, Infinity, -Infinity, 1n, cyclic, recursiveJson])
|
||||
expect(() => digestCanonicalValue(value)).toThrow();
|
||||
for (const [value, code] of [
|
||||
[Number.NaN, "nan"],
|
||||
[Number.POSITIVE_INFINITY, "infinity"],
|
||||
[Number.NEGATIVE_INFINITY, "infinity"],
|
||||
] as const) {
|
||||
expect(() => digestCanonicalValue(value)).toThrow(CanonicalDigestError);
|
||||
try {
|
||||
digestCanonicalValue(value);
|
||||
} catch (cause) {
|
||||
expect(cause).toMatchObject({ _tag: "CanonicalDigestError", code });
|
||||
}
|
||||
}
|
||||
for (const value of [cyclic, recursiveJson]) {
|
||||
expect(() => digestCanonicalValue(value)).toThrow(CanonicalDigestError);
|
||||
try {
|
||||
digestCanonicalValue(value);
|
||||
} catch (cause) {
|
||||
expect(cause).toMatchObject({ code: "circular" });
|
||||
}
|
||||
}
|
||||
expect(() => digestCanonicalValue(1n)).toThrow();
|
||||
});
|
||||
|
||||
it("preserves canonical bytes across large keys, repeated leaves and buffer flushes", () => {
|
||||
|
||||
@@ -38,22 +38,30 @@ export const digestCanonicalValue = (
|
||||
|
||||
type Emit = (part: string) => void;
|
||||
|
||||
/** Canonical-JSON refusal. The code is the stable identity. */
|
||||
export class CanonicalDigestError extends Error {
|
||||
readonly _tag = "CanonicalDigestError";
|
||||
|
||||
constructor(readonly code: "nan" | "infinity" | "circular") {
|
||||
super(code);
|
||||
this.name = "CanonicalDigestError";
|
||||
}
|
||||
}
|
||||
|
||||
const emitCanonical = (
|
||||
value: unknown,
|
||||
emit: Emit,
|
||||
ancestors: Set<object>,
|
||||
): boolean => {
|
||||
if (typeof value === "number" && !Number.isFinite(value))
|
||||
throw new Error(
|
||||
Number.isNaN(value) ? "NaN is not allowed" : "Infinity is not allowed",
|
||||
);
|
||||
throw new CanonicalDigestError(Number.isNaN(value) ? "nan" : "infinity");
|
||||
if (value === null || typeof value !== "object") {
|
||||
const encoded = JSON.stringify(value);
|
||||
if (encoded === undefined) return false;
|
||||
emit(encoded);
|
||||
return true;
|
||||
}
|
||||
if (ancestors.has(value)) throw new Error("Circular reference detected");
|
||||
if (ancestors.has(value)) throw new CanonicalDigestError("circular");
|
||||
ancestors.add(value);
|
||||
try {
|
||||
if ("toJSON" in value && typeof value.toJSON === "function") {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { fc, it } from "@fast-check/vitest";
|
||||
import { describe, expect } from "vitest";
|
||||
import { canonicalJsonDigestSteps } from "./canonicalJsonDigestSteps.js";
|
||||
import { digestCanonicalValue } from "./canonicalDigest.js";
|
||||
import {
|
||||
CanonicalDigestError,
|
||||
digestCanonicalValue,
|
||||
} from "./canonicalDigest.js";
|
||||
import { jsonValueSchema, type JsonValue } from "./jsonValue.js";
|
||||
|
||||
const complete = <Value>(steps: Iterator<void, Value>): Value => {
|
||||
@@ -78,7 +81,22 @@ describe("cooperative canonical JSON digest", () => {
|
||||
sparse.length = 2;
|
||||
const cycle: { [key: string]: JsonValue } = {};
|
||||
cycle.self = cycle;
|
||||
for (const value of [sparse, Number.NaN, cycle])
|
||||
expect(() => complete(canonicalJsonDigestSteps(value))).toThrow();
|
||||
expect(() => complete(canonicalJsonDigestSteps(sparse))).toThrow();
|
||||
expect(() => complete(canonicalJsonDigestSteps(Number.NaN))).toThrow(
|
||||
CanonicalDigestError,
|
||||
);
|
||||
expect(() => complete(canonicalJsonDigestSteps(cycle))).toThrow(
|
||||
CanonicalDigestError,
|
||||
);
|
||||
try {
|
||||
complete(canonicalJsonDigestSteps(Number.NaN));
|
||||
} catch (cause) {
|
||||
expect(cause).toMatchObject({ code: "nan" });
|
||||
}
|
||||
try {
|
||||
complete(canonicalJsonDigestSteps(cycle));
|
||||
} catch (cause) {
|
||||
expect(cause).toMatchObject({ code: "circular" });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { CanonicalDigestError } from "./canonicalDigest.js";
|
||||
import type { JsonValue } from "./jsonValue.js";
|
||||
|
||||
type DigestFrame =
|
||||
@@ -137,14 +138,12 @@ export function* canonicalJsonDigestSteps(
|
||||
stack.push({ kind: "string", value: item, index: 0 });
|
||||
} else if (item === null || typeof item !== "object") {
|
||||
if (typeof item === "number" && !Number.isFinite(item))
|
||||
throw new Error(
|
||||
Number.isNaN(item)
|
||||
? "NaN is not allowed"
|
||||
: "Infinity is not allowed",
|
||||
throw new CanonicalDigestError(
|
||||
Number.isNaN(item) ? "nan" : "infinity",
|
||||
);
|
||||
emit(JSON.stringify(item));
|
||||
} else {
|
||||
if (ancestors.has(item)) throw new Error("Circular reference detected");
|
||||
if (ancestors.has(item)) throw new CanonicalDigestError("circular");
|
||||
ancestors.add(item);
|
||||
if (Array.isArray(item)) {
|
||||
emit("[");
|
||||
|
||||
@@ -17,6 +17,18 @@ interface FlowEdge {
|
||||
remaining: number;
|
||||
}
|
||||
|
||||
/** Residual-network invariant. The code is the stable identity. */
|
||||
class ReferenceFlowError extends Error {
|
||||
readonly _tag = "ReferenceFlowError";
|
||||
|
||||
constructor(
|
||||
readonly code: "endpoint_unallocated" | "reverse_edge_unallocated",
|
||||
) {
|
||||
super(code);
|
||||
this.name = "ReferenceFlowError";
|
||||
}
|
||||
}
|
||||
|
||||
/** A local residual network: no recursive walk or pairwise adjacency matrix. */
|
||||
class ReferenceFlow {
|
||||
private readonly edges: FlowEdge[][] = [];
|
||||
@@ -30,7 +42,7 @@ class ReferenceFlow {
|
||||
const forward = this.edges[from];
|
||||
const reverse = this.edges[to];
|
||||
if (forward === undefined || reverse === undefined)
|
||||
throw new Error("Reference flow endpoint was not allocated.");
|
||||
throw new ReferenceFlowError("endpoint_unallocated");
|
||||
forward.push({ to, reverse: reverse.length, remaining: capacity });
|
||||
reverse.push({ to: from, reverse: forward.length - 1, remaining: 0 });
|
||||
}
|
||||
@@ -87,7 +99,7 @@ class ReferenceFlow {
|
||||
edge.remaining -= amount;
|
||||
const reverse = this.edges[edge.to]?.[edge.reverse];
|
||||
if (reverse === undefined)
|
||||
throw new Error("Reference flow reverse edge was not allocated.");
|
||||
throw new ReferenceFlowError("reverse_edge_unallocated");
|
||||
reverse.remaining += amount;
|
||||
}
|
||||
return amount;
|
||||
|
||||
@@ -300,6 +300,17 @@ export const readManagedGraphProjection = (
|
||||
|
||||
const OPERATION = "project_managed_application_graph";
|
||||
|
||||
/** Validated graph input still had no artifact source. */
|
||||
class ManagedGraphSourceError extends Error {
|
||||
readonly _tag = "ManagedGraphSourceError";
|
||||
readonly code = "missing_source" as const;
|
||||
|
||||
constructor() {
|
||||
super("missing_source");
|
||||
this.name = "ManagedGraphSourceError";
|
||||
}
|
||||
}
|
||||
|
||||
const chooseArtifact = (
|
||||
parsed: ParsedManagedGraphInput,
|
||||
): { readonly sha256: string; readonly path: string } => {
|
||||
@@ -307,8 +318,7 @@ const chooseArtifact = (
|
||||
parsed.artifact?.result ??
|
||||
parsed.members?.result ??
|
||||
parsed.boundaries?.result;
|
||||
if (result === undefined)
|
||||
throw new Error("Validated managed graph projection has no source");
|
||||
if (result === undefined) throw new ManagedGraphSourceError();
|
||||
return { sha256: result.artifact.sha256, path: result.artifact.path };
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { parseOpenStep } from "plist";
|
||||
import { plistNestingLimitMessage } from "./apple/plistValue.js";
|
||||
import {
|
||||
decodeOpenStepPropertyList,
|
||||
OpenStepPropertyListError,
|
||||
startsOpenStepPropertyList,
|
||||
} from "./propertyListOpenStep.js";
|
||||
|
||||
@@ -30,28 +31,53 @@ const syntaxFailure = (read: () => unknown): Error => {
|
||||
throw new Error("Expected syntax failure");
|
||||
};
|
||||
|
||||
const containerRefusal = (
|
||||
code: OpenStepPropertyListError["code"],
|
||||
position: number,
|
||||
) => ({ code, position });
|
||||
|
||||
const containerRefusals = [
|
||||
["(}", containerRefusal("unexpected_character", 1)],
|
||||
["{)", containerRefusal("unexpected_character", 1)],
|
||||
["(@", containerRefusal("unexpected_character", 1)],
|
||||
["{key @", containerRefusal("expected_equals", 5)],
|
||||
["(x @", containerRefusal("expected_separator", 3)],
|
||||
[
|
||||
"(x ",
|
||||
containerRefusal("unterminated_array", 3),
|
||||
containerRefusal("expected_separator", 3),
|
||||
],
|
||||
["{key ", containerRefusal("expected_equals", 5)],
|
||||
["{key = x,", containerRefusal("unexpected_character", 8)],
|
||||
["{ /* unterminated", containerRefusal("unterminated_block_comment", 2)],
|
||||
["(x, /* unterminated", containerRefusal("unterminated_block_comment", 4)],
|
||||
["{ { toString = text; } @", containerRefusal("expected_equals", 23)],
|
||||
] as const;
|
||||
|
||||
it.each(containerRefusals)(
|
||||
"reports %s before and after a deep suffix",
|
||||
(text, diagnosis, deep = diagnosis) => {
|
||||
expect(syntaxFailure(() => decodeOpenStepPropertyList(text))).toMatchObject(
|
||||
{ _tag: "OpenStepPropertyListError", ...diagnosis },
|
||||
);
|
||||
expect(
|
||||
syntaxFailure(() => decodeOpenStepPropertyList(text + "(".repeat(130))),
|
||||
).toMatchObject({ _tag: "OpenStepPropertyListError", ...deep });
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
"(}",
|
||||
"{)",
|
||||
"(@",
|
||||
"{key @",
|
||||
"(x @",
|
||||
"(x ",
|
||||
"{key ",
|
||||
"{key = x,",
|
||||
'{ key = "unterminated',
|
||||
'{ key = "escape\\',
|
||||
"{ key = <01",
|
||||
"{ /* unterminated",
|
||||
"(x, /* unterminated",
|
||||
"{ { toString = text; } @",
|
||||
"{ { toString = text; } = value; }",
|
||||
])(
|
||||
"preserves pinned syntax diagnosis before later deep input after %s",
|
||||
"keeps the pinned scalar decoder's diagnosis for %s before a deep suffix",
|
||||
(prefix) => {
|
||||
for (const text of [prefix, prefix + "(".repeat(130)]) {
|
||||
const pinned = syntaxFailure(() => parseOpenStep(text));
|
||||
const owned = syntaxFailure(() => decodeOpenStepPropertyList(text));
|
||||
expect(owned).not.toBeInstanceOf(OpenStepPropertyListError);
|
||||
expect(owned.constructor).toBe(pinned.constructor);
|
||||
expect(owned.message).toBe(pinned.message);
|
||||
}
|
||||
@@ -89,14 +115,16 @@ it("bounds compound keys and later siblings at their owning parse", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it.each(["(x,", "{ key ="])(
|
||||
"retains incomplete-value syntax for %s",
|
||||
(text) => {
|
||||
expect(syntaxFailure(() => decodeOpenStepPropertyList(text)).message).toBe(
|
||||
syntaxFailure(() => parseOpenStep(text)).message,
|
||||
);
|
||||
},
|
||||
);
|
||||
it.each([
|
||||
["(x,", "unexpected_end", 3],
|
||||
["{ key =", "unexpected_end", 7],
|
||||
] as const)("reports incomplete %s as %s at %s", (text, code, position) => {
|
||||
expect(syntaxFailure(() => decodeOpenStepPropertyList(text))).toMatchObject({
|
||||
_tag: "OpenStepPropertyListError",
|
||||
code,
|
||||
position,
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["\n", "\r\n", "\r", "\u2028", "\u2029"])(
|
||||
"ends OpenStep line comments at %j without dropping following values",
|
||||
@@ -134,5 +162,10 @@ it("does not treat nonterminating whitespace or quoted comment text as line ends
|
||||
);
|
||||
expect(() =>
|
||||
decodeOpenStepPropertyList("// comment without a line end"),
|
||||
).toThrow("Unexpected end of input");
|
||||
).toThrow(OpenStepPropertyListError);
|
||||
expect(
|
||||
syntaxFailure(() =>
|
||||
decodeOpenStepPropertyList("// comment without a line end"),
|
||||
),
|
||||
).toMatchObject({ code: "unexpected_end" });
|
||||
});
|
||||
@@ -3,6 +3,31 @@ import { parseOpenStep } from "plist";
|
||||
import { MAX_PLIST_DEPTH, PlistNestingLimitError } from "./apple/plistValue.js";
|
||||
import { markPlistDictionary } from "./apple/plistUid.js";
|
||||
|
||||
const OPEN_STEP_SYNTAX_CODES = [
|
||||
"unterminated_block_comment",
|
||||
"unexpected_end",
|
||||
"unterminated_dictionary",
|
||||
"expected_equals",
|
||||
"unterminated_array",
|
||||
"expected_separator",
|
||||
"unexpected_character",
|
||||
] as const;
|
||||
|
||||
export type OpenStepPropertyListCode = (typeof OPEN_STEP_SYNTAX_CODES)[number];
|
||||
|
||||
/** OpenStep syntax refusal. `code` is the contract; `position` is the source offset. */
|
||||
export class OpenStepPropertyListError extends Error {
|
||||
readonly _tag = "OpenStepPropertyListError";
|
||||
|
||||
constructor(
|
||||
readonly code: OpenStepPropertyListCode,
|
||||
readonly position?: number,
|
||||
) {
|
||||
super(code);
|
||||
this.name = "OpenStepPropertyListError";
|
||||
}
|
||||
}
|
||||
|
||||
/** Locate the first value for the XML owner's existing OpenStep autodetection. */
|
||||
export const startsOpenStepPropertyList = (text: string): boolean => {
|
||||
const cursor = skipTrivia(text, 0, false);
|
||||
@@ -25,7 +50,11 @@ const skipTrivia = (text: string, start: number, strict = true): number => {
|
||||
} else if (text.startsWith("/*", cursor)) {
|
||||
const end = text.indexOf("*/", cursor + 2);
|
||||
if (end < 0) {
|
||||
if (strict) throw new Error("Unterminated block comment");
|
||||
if (strict)
|
||||
throw new OpenStepPropertyListError(
|
||||
"unterminated_block_comment",
|
||||
cursor,
|
||||
);
|
||||
return text.length;
|
||||
}
|
||||
cursor = end + 2;
|
||||
@@ -51,7 +80,8 @@ export const decodeOpenStepPropertyList = (
|
||||
};
|
||||
const value = (depth: number, dictionaryKey = false): unknown => {
|
||||
trivia();
|
||||
if (cursor >= text.length) throw new Error("Unexpected end of input");
|
||||
if (cursor >= text.length)
|
||||
throw new OpenStepPropertyListError("unexpected_end", cursor);
|
||||
const character = text[cursor]!;
|
||||
if (character === "{") {
|
||||
assertDepth(depth);
|
||||
@@ -60,7 +90,11 @@ export const decodeOpenStepPropertyList = (
|
||||
markPlistDictionary(dictionary);
|
||||
while (true) {
|
||||
trivia();
|
||||
if (cursor >= text.length) throw new Error("Unterminated dictionary");
|
||||
if (cursor >= text.length)
|
||||
throw new OpenStepPropertyListError(
|
||||
"unterminated_dictionary",
|
||||
cursor,
|
||||
);
|
||||
if (text[cursor] === "}") {
|
||||
cursor += 1;
|
||||
return dictionary;
|
||||
@@ -68,9 +102,7 @@ export const decodeOpenStepPropertyList = (
|
||||
const key = value(depth + 1, true);
|
||||
trivia();
|
||||
if (text[cursor] !== "=")
|
||||
throw new Error(
|
||||
`Expected '=' after key "${String(key)}" at position ${cursor}`,
|
||||
);
|
||||
throw new OpenStepPropertyListError("expected_equals", cursor);
|
||||
cursor += 1;
|
||||
const entry = value(depth + 1);
|
||||
Object.defineProperty(dictionary, String(key), {
|
||||
@@ -95,13 +127,14 @@ export const decodeOpenStepPropertyList = (
|
||||
while (true) {
|
||||
array.push(value(depth + 1));
|
||||
trivia();
|
||||
if (cursor >= text.length) throw new Error("Unterminated array");
|
||||
if (cursor >= text.length)
|
||||
throw new OpenStepPropertyListError("unterminated_array", cursor);
|
||||
if (text[cursor] === ")") {
|
||||
cursor += 1;
|
||||
return array;
|
||||
}
|
||||
if (text[cursor] !== ",")
|
||||
throw new Error(`Expected ',' or ')' in array at position ${cursor}`);
|
||||
throw new OpenStepPropertyListError("expected_separator", cursor);
|
||||
cursor += 1;
|
||||
trivia();
|
||||
if (text[cursor] === ")") {
|
||||
@@ -125,9 +158,7 @@ export const decodeOpenStepPropertyList = (
|
||||
while (cursor < text.length && /[a-zA-Z0-9._/$:-]/u.test(text[cursor]!))
|
||||
cursor += 1;
|
||||
if (cursor === start)
|
||||
throw new Error(
|
||||
`Unexpected character '${character}' at position ${cursor}`,
|
||||
);
|
||||
throw new OpenStepPropertyListError("unexpected_character", cursor);
|
||||
}
|
||||
// Scalar dictionary keys do not add source-value nesting; compound keys do.
|
||||
if (!dictionaryKey) assertDepth(depth);
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import type { WebPageInspection } from "./browserObservationSchemas.js";
|
||||
import { digestCanonicalValue } from "./canonicalDigest.js";
|
||||
|
||||
/** Frame-forest invariant. The code is the stable identity. */
|
||||
class WebCaptureFrameError extends Error {
|
||||
readonly _tag = "WebCaptureFrameError";
|
||||
|
||||
constructor(readonly code: "missing_ready_frame" | "missing_parent_frame") {
|
||||
super(code);
|
||||
this.name = "WebCaptureFrameError";
|
||||
}
|
||||
}
|
||||
|
||||
/** Compare complete frame forests without treating capture-local IDs as identity. */
|
||||
export const fingerprintWebCaptureFrames = (
|
||||
frames: WebPageInspection["frames"],
|
||||
@@ -20,7 +30,8 @@ export const fingerprintWebCaptureFrames = (
|
||||
let examined = 0;
|
||||
for (let index = 0; index < ready.length; index += 1) {
|
||||
const frame = ready[index];
|
||||
if (frame === undefined) throw new Error("Missing ready frame");
|
||||
if (frame === undefined)
|
||||
throw new WebCaptureFrameError("missing_ready_frame");
|
||||
const fingerprint = digestCanonicalValue({
|
||||
url: frame.url,
|
||||
origin: frame.origin,
|
||||
@@ -40,7 +51,7 @@ export const fingerprintWebCaptureFrames = (
|
||||
if (count === 0) {
|
||||
const parent = byId.get(parentId);
|
||||
if (parent === undefined)
|
||||
throw new Error("Missing admitted parent frame");
|
||||
throw new WebCaptureFrameError("missing_parent_frame");
|
||||
ready.push(parent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@ import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { expect, it } from "vitest";
|
||||
import { parse } from "plist";
|
||||
|
||||
import { ArtifactReaderFailure } from "../../../src/artifacts/ArtifactReader.js";
|
||||
import { inspectBundleKeyedArchive } from "../../../src/artifacts/apple/KeyedArchiveReader.js";
|
||||
import { plistNestingLimitMessage } from "../../../src/domain/apple/plistValue.js";
|
||||
import type { OpenStepPropertyListCode } from "../../../src/domain/propertyListOpenStep.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { deeplyNestedXmlPlist } from "../../fixtures/keyedArchiveXmlNesting.js";
|
||||
|
||||
@@ -29,31 +30,34 @@ it.each([129, 3000])(
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["(}", "{)", "(@", "{key @", "(x @", "(x ", "{key ", "{key = x,"])(
|
||||
"preserves the pinned OpenStep syntax diagnosis before a deep suffix after %s",
|
||||
async (prefix) => {
|
||||
const text = prefix + "(".repeat(130);
|
||||
let pinned: unknown;
|
||||
try {
|
||||
parse(text);
|
||||
} catch (cause) {
|
||||
pinned = cause;
|
||||
}
|
||||
if (!(pinned instanceof Error) || pinned instanceof RangeError)
|
||||
throw new Error(
|
||||
"Malformed control must fail in the pinned syntax decoder",
|
||||
);
|
||||
it.each([
|
||||
["(}", "unexpected_character", 1],
|
||||
["{)", "unexpected_character", 1],
|
||||
["(@", "unexpected_character", 1],
|
||||
["{key @", "expected_equals", 5],
|
||||
["(x @", "expected_separator", 3],
|
||||
["(x ", "expected_separator", 3],
|
||||
["{key ", "expected_equals", 5],
|
||||
["{key = x,", "unexpected_character", 8],
|
||||
] as const)(
|
||||
"reports OpenStep %s before a deep suffix as %s at %s",
|
||||
async (prefix, code: OpenStepPropertyListCode, position) => {
|
||||
const root = await createTestTempDirectory("rea-keyed-openstep-malformed-");
|
||||
await writeFile(join(root, "Malformed.plist"), text);
|
||||
await writeFile(join(root, "Malformed.plist"), prefix + "(".repeat(130));
|
||||
const rejected = await inspectBundleKeyedArchive({
|
||||
bundlePath: root,
|
||||
targetSha256: "a".repeat(64),
|
||||
parameters: { path: "Malformed.plist" },
|
||||
platform: "linux",
|
||||
}).catch((cause: unknown) => cause);
|
||||
expect(rejected).toBeInstanceOf(ArtifactReaderFailure);
|
||||
expect(rejected).toMatchObject({
|
||||
reason: "format",
|
||||
message: `Cannot decode selected Foundation keyed archive: ${pinned.message}`,
|
||||
cause: {
|
||||
_tag: "OpenStepPropertyListError",
|
||||
code,
|
||||
position,
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user