mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 03:02:56 +00:00
* refactor(analysis): centralize resource budgets and recovery facts * feat(analysis): expose ASAR and Ghidra runtime admission controls * feat(artifacts): expose reader admission and preserve limited recognition * feat(javascript): expose recoverable semantic precision budgets * feat(web): add recoverable source-map budgets and compact retained views * feat(web): make historical capture decoding budgets recoverable * fix(desktop): enforce selected view policies across capture and export * feat(runtime): make Frida and LLDB retention budgets selectable * feat(native): expose diagnostic budgets and preserve exhaustion causes * feat(managed): expose recoverable snapshot and representation budgets * feat(metadata): make Go and Rust inspection budgets recoverable * feat(wasm): expose representation and producer admissions * feat(evm): expose per-call worker resource budgets * feat(android): make resource and string-pool omissions recoverable * feat(producers): expose command and recorded-capture budgets * docs(analysis): record recovery contracts and integrated validation * fix(docs): link calibration oracle from published verification pages * fix(docs): restore desktop task anchors and receipt links * test(analysis): align fixtures with selectable budget contracts * fix(analysis): remove source-location projector dependency cycle * test(analysis): refresh budget recovery and verifier regressions * test(analysis): preserve format and publication recovery expectations * test(windows): align conformance with owned runtime admission * fix(desktop): preserve selected alias kinds in exported state * test(analysis): align provider fixtures with recoverable budgets * test(desktop): validate emitted alias metadata structurally * fix(ghidra): classify bounded Windows endpoint refusals * test(cli): assert selectable artifact admission recovery * fix(cli): preserve capture provenance in environment builds
337 lines
10 KiB
TypeScript
337 lines
10 KiB
TypeScript
import { setImmediate } from "node:timers/promises";
|
|
|
|
import { expect, it } from "vitest";
|
|
|
|
import { DEFAULT_NODE_SEA_READER_LIMITS } from "../../domain/artifactReaderLimits.js";
|
|
import {
|
|
NODE_SEA_MAGIC,
|
|
decodeNodeSeaBlob,
|
|
observeNodeSeaFuseMatches,
|
|
} from "./NodeSeaBlob.js";
|
|
import { buildNodeSeaBlob } from "./NodeSea.fixture.js";
|
|
|
|
const header = (flags: number, extension = 1): Buffer => {
|
|
const bytes = Buffer.alloc(9);
|
|
bytes.writeUInt32LE(NODE_SEA_MAGIC, 0);
|
|
bytes.writeUInt32LE(flags, 4);
|
|
bytes[8] = extension;
|
|
return bytes;
|
|
};
|
|
|
|
it("rejects a blob that ends before the magic", async () => {
|
|
await expect(
|
|
decodeNodeSeaBlob(Buffer.from([0x20, 0xda])),
|
|
).rejects.toMatchObject({
|
|
reason: "format",
|
|
message: expect.stringMatching(/Node SEA.*truncated/iu),
|
|
});
|
|
});
|
|
|
|
it("rejects a blob whose magic is not 0x143da20", async () => {
|
|
await expect(decodeNodeSeaBlob(Buffer.alloc(9))).rejects.toMatchObject({
|
|
reason: "format",
|
|
message: expect.stringMatching(/magic.*not.*0x143da20/iu),
|
|
});
|
|
});
|
|
|
|
it("rejects a header that ends before the extension byte", async () => {
|
|
await expect(
|
|
decodeNodeSeaBlob(header(0).subarray(0, 8)),
|
|
).rejects.toMatchObject({
|
|
reason: "format",
|
|
message: expect.stringMatching(/Node SEA.*truncated/iu),
|
|
});
|
|
});
|
|
|
|
it("rejects unknown flag bits", async () => {
|
|
await expect(decodeNodeSeaBlob(header(1 << 5))).rejects.toMatchObject({
|
|
reason: "format",
|
|
message: expect.stringMatching(/flags.*unknown bits/iu),
|
|
});
|
|
});
|
|
|
|
it("rejects an unknown exec argv extension", async () => {
|
|
await expect(decodeNodeSeaBlob(header(0, 9))).rejects.toMatchObject({
|
|
reason: "format",
|
|
message: expect.stringMatching(/exec argv extension.*unknown/iu),
|
|
});
|
|
});
|
|
|
|
it("rejects an empty code path", async () => {
|
|
await expect(
|
|
decodeNodeSeaBlob(buildNodeSeaBlob({ codePath: "" })),
|
|
).rejects.toMatchObject({
|
|
reason: "format",
|
|
message: expect.stringMatching(/code path.*empty/iu),
|
|
});
|
|
});
|
|
|
|
it("rejects a truncated length-prefixed field", async () => {
|
|
const bytes = Buffer.concat([header(0), Buffer.alloc(4)]);
|
|
await expect(decodeNodeSeaBlob(bytes)).rejects.toMatchObject({
|
|
reason: "format",
|
|
message: expect.stringMatching(/Node SEA.*truncated/iu),
|
|
});
|
|
});
|
|
|
|
it("rejects colliding asset names", async () => {
|
|
await expect(
|
|
decodeNodeSeaBlob(
|
|
buildNodeSeaBlob({
|
|
assets: [
|
|
{ name: "shared", body: "one" },
|
|
{ name: "shared", body: "two" },
|
|
],
|
|
}),
|
|
),
|
|
).rejects.toMatchObject({
|
|
reason: "format",
|
|
message: expect.stringMatching(/Node SEA asset name collides.*shared/iu),
|
|
});
|
|
});
|
|
|
|
it("retains an empty asset key as a raw Node SEA key", async () => {
|
|
const decoded = await decodeNodeSeaBlob(
|
|
buildNodeSeaBlob({ assets: [{ name: "", body: "value" }] }),
|
|
);
|
|
expect(decoded.assets).toMatchObject([{ name: "", range: { length: 5 } }]);
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
field: "code path",
|
|
build: (value: string) => buildNodeSeaBlob({ codePath: value }),
|
|
read: (blob: Awaited<ReturnType<typeof decodeNodeSeaBlob>>) =>
|
|
blob.codePath,
|
|
},
|
|
{
|
|
field: "asset name",
|
|
build: (value: string) =>
|
|
buildNodeSeaBlob({ assets: [{ name: value, body: "" }] }),
|
|
read: (blob: Awaited<ReturnType<typeof decodeNodeSeaBlob>>) =>
|
|
blob.assets[0]?.name,
|
|
},
|
|
{
|
|
field: "exec argument",
|
|
build: (value: string) => buildNodeSeaBlob({ execArgv: [value] }),
|
|
read: (blob: Awaited<ReturnType<typeof decodeNodeSeaBlob>>) =>
|
|
blob.execArgv[0],
|
|
},
|
|
])(
|
|
"rejects invalid UTF-8 in $field while preserving a literal replacement character",
|
|
async ({ build, read }) => {
|
|
const valid = await decodeNodeSeaBlob(build("\ufffd"));
|
|
expect(read(valid)).toBe("\ufffd");
|
|
|
|
const malformed = build("fixture-marker");
|
|
const offset = malformed.indexOf("fixture-marker");
|
|
expect(offset).toBeGreaterThanOrEqual(0);
|
|
malformed[offset] = 0xff;
|
|
await expect(decodeNodeSeaBlob(malformed)).rejects.toMatchObject({
|
|
reason: "format",
|
|
message: expect.stringMatching(/not valid UTF-8/iu),
|
|
});
|
|
},
|
|
);
|
|
|
|
it("rejects impossible asset rows before applying resource admission", async () => {
|
|
const prefix = buildNodeSeaBlob({ assets: [{ name: "a", body: "" }] });
|
|
const count = Buffer.alloc(8);
|
|
count.writeBigUInt64LE(2n);
|
|
const bytes = Buffer.concat([prefix.subarray(0, -25), count]);
|
|
await expect(
|
|
decodeNodeSeaBlob(bytes, undefined, {
|
|
...DEFAULT_NODE_SEA_READER_LIMITS,
|
|
asset_count: 1,
|
|
}),
|
|
).rejects.toMatchObject({
|
|
reason: "format",
|
|
message: expect.stringMatching(/Node SEA.*truncated/iu),
|
|
readerLimit: undefined,
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
{ field: "asset_count" as const, limit: 1, raised: 2 },
|
|
{ field: "asset_name_bytes" as const, limit: 2, raised: 3 },
|
|
{ field: "argument_count" as const, limit: 1, raised: 2 },
|
|
{ field: "argument_bytes" as const, limit: 2, raised: 3 },
|
|
])(
|
|
"recovers the same blob after raising $field",
|
|
async ({ field, limit, raised }) => {
|
|
const bytes = buildNodeSeaBlob({
|
|
assets: [
|
|
{ name: "a", body: "one" },
|
|
{ name: "bb", body: "two" },
|
|
],
|
|
execArgv: ["x", "yz"],
|
|
});
|
|
await expect(
|
|
decodeNodeSeaBlob(bytes, undefined, {
|
|
...DEFAULT_NODE_SEA_READER_LIMITS,
|
|
[field]: limit,
|
|
}),
|
|
).rejects.toMatchObject({
|
|
reason: "limit",
|
|
readerLimit: {
|
|
parameter: `artifact_limits.node_sea.${field}`,
|
|
effective_value: limit,
|
|
},
|
|
});
|
|
const recovered = await decodeNodeSeaBlob(bytes, undefined, {
|
|
...DEFAULT_NODE_SEA_READER_LIMITS,
|
|
[field]: raised,
|
|
});
|
|
expect(recovered.assets.map((asset) => asset.name)).toEqual(["a", "bb"]);
|
|
expect(recovered.execArgv).toEqual(["x", "yz"]);
|
|
},
|
|
);
|
|
|
|
it("applies blob byte admission even when called without a carrier reader", async () => {
|
|
const bytes = buildNodeSeaBlob({ main: "hello" });
|
|
await expect(
|
|
decodeNodeSeaBlob(bytes, undefined, {
|
|
...DEFAULT_NODE_SEA_READER_LIMITS,
|
|
blob_bytes: bytes.length - 1,
|
|
}),
|
|
).rejects.toMatchObject({
|
|
reason: "limit",
|
|
readerLimit: {
|
|
parameter: "artifact_limits.node_sea.blob_bytes",
|
|
required_value: bytes.length,
|
|
},
|
|
});
|
|
expect(
|
|
(
|
|
await decodeNodeSeaBlob(bytes, undefined, {
|
|
...DEFAULT_NODE_SEA_READER_LIMITS,
|
|
blob_bytes: bytes.length,
|
|
})
|
|
).mainScript?.length,
|
|
).toBe(5);
|
|
});
|
|
|
|
it("rejects non-zero trailing bytes and keeps zero padding", async () => {
|
|
const padded = buildNodeSeaBlob({ trailing: Buffer.alloc(8) });
|
|
expect((await decodeNodeSeaBlob(padded)).codePath).toBe("sea.js");
|
|
const dirty = Buffer.concat([padded, Buffer.from([1])]);
|
|
await expect(decodeNodeSeaBlob(dirty)).rejects.toMatchObject({
|
|
reason: "format",
|
|
message: expect.stringMatching(/trailing bytes/iu),
|
|
});
|
|
});
|
|
|
|
it("decodes flags, the main script, code cache, assets, and exec argv", async () => {
|
|
const decoded = await decodeNodeSeaBlob(
|
|
buildNodeSeaBlob({
|
|
codePath: "app/main.js",
|
|
main: "console.log(1)\n",
|
|
codeCache: Buffer.from([1, 2, 3]),
|
|
assets: [{ name: "pkg/data.txt", body: "asset" }],
|
|
execArgv: ["--no-warnings", ""],
|
|
disableExperimentalSeaWarning: true,
|
|
execArgvExtension: 2,
|
|
}),
|
|
);
|
|
expect(decoded).toMatchObject({
|
|
flags: (1 << 0) | (1 << 2) | (1 << 3) | (1 << 4),
|
|
disableExperimentalSeaWarning: true,
|
|
useSnapshot: false,
|
|
useCodeCache: true,
|
|
includeAssets: true,
|
|
includeExecArgv: true,
|
|
execArgvExtension: "cli",
|
|
codePath: "app/main.js",
|
|
execArgv: ["--no-warnings", ""],
|
|
assets: [{ name: "pkg/data.txt" }],
|
|
});
|
|
expect(decoded.mainScript?.length).toBe(
|
|
Buffer.byteLength("console.log(1)\n"),
|
|
);
|
|
expect(decoded.snapshot).toBeNull();
|
|
expect(decoded.codeCache?.length).toBe(3);
|
|
});
|
|
|
|
it("treats the body as a snapshot when that flag is set", async () => {
|
|
const decoded = await decodeNodeSeaBlob(
|
|
buildNodeSeaBlob({
|
|
codePath: "sea.js",
|
|
snapshot: Buffer.from("snap"),
|
|
}),
|
|
);
|
|
expect(decoded.useSnapshot).toBe(true);
|
|
expect(decoded.mainScript).toBeNull();
|
|
expect(decoded.snapshot?.length).toBe(4);
|
|
});
|
|
|
|
it("reports an absent, unset, set, or conflicting fuse sentinel", () => {
|
|
expect(observeNodeSeaFuseMatches([])).toMatchObject({
|
|
fuseState: "absent",
|
|
fuseSentinel: null,
|
|
fuseContradiction: expect.stringMatching(
|
|
/no NODE_SEA_FUSE sentinel.*observed/iu,
|
|
),
|
|
});
|
|
const unset = `NODE_SEA_FUSE_${"a".repeat(32)}:0`;
|
|
expect(observeNodeSeaFuseMatches([unset, unset])).toMatchObject({
|
|
fuseState: "unset",
|
|
fuseSentinel: unset,
|
|
fuseContradiction: expect.stringMatching(/NODE_SEA_FUSE sentinel.*unset/iu),
|
|
});
|
|
const set = `NODE_SEA_FUSE_${"b".repeat(32)}:1`;
|
|
expect(observeNodeSeaFuseMatches([set])).toMatchObject({
|
|
fuseState: "set",
|
|
fuseSentinel: set,
|
|
fuseContradiction: null,
|
|
});
|
|
expect(observeNodeSeaFuseMatches([unset, set])).toMatchObject({
|
|
fuseState: "conflicting",
|
|
fuseSentinel: null,
|
|
fuseContradiction: expect.stringMatching(/fuse sentinels disagree/iu),
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
[
|
|
"assets",
|
|
() =>
|
|
buildNodeSeaBlob({
|
|
assets: Array.from({ length: 1024 }, (_, index) => ({
|
|
name: `asset-${String(index)}`,
|
|
body: "",
|
|
})),
|
|
}),
|
|
],
|
|
[
|
|
"exec arguments",
|
|
() =>
|
|
buildNodeSeaBlob({
|
|
execArgv: Array.from({ length: 1024 }, () => "--no-warnings"),
|
|
}),
|
|
],
|
|
[
|
|
"zero padding",
|
|
() => buildNodeSeaBlob({ trailing: Buffer.alloc(1024 * 1024) }),
|
|
],
|
|
] as const)("observes cancellation while decoding %s", async (_name, build) => {
|
|
const bytes = build();
|
|
const controller = new AbortController();
|
|
const reason = { stage: "decoding" };
|
|
const pending = decodeNodeSeaBlob(bytes, controller.signal);
|
|
// Let a batch run; cancellation must be observed before the whole table or
|
|
// padding scan finishes, without relying on elapsed-time assertions.
|
|
await setImmediate();
|
|
controller.abort(reason);
|
|
await expect(pending).rejects.toMatchObject({
|
|
reason: "cancelled",
|
|
cause: reason,
|
|
});
|
|
});
|
|
|
|
it("preserves cancellation before decoding even a malformed blob", async () => {
|
|
const reason = { stage: "before decoding" };
|
|
await expect(
|
|
decodeNodeSeaBlob(Buffer.alloc(0), AbortSignal.abort(reason)),
|
|
).rejects.toMatchObject({ reason: "cancelled", cause: reason });
|
|
});
|