mirror of
https://github.com/morluto/rea
synced 2026-10-27 06:33:32 -04:00
e8da7e1edd
Parse Pickle extents once at the ASAR boundary and carry finalized signature facts through root and nested classification. Scan JSON whitespace within its declared extent and decode budget without retaining the whitespace prefix. Keep prefix storage bounded independently of stream chunk ownership. Fill fixed ASAR prefixes and DMG trailers across short reads, preserve cancellation through admission and stream EOF, and retain existing cleanup ownership. Cover public CLI/MCP selection, root/nested inventory, malformed JSON whitespace and padding, limits, native/XML consumers and descriptor faults. Validation: 24 focused files / 170 tests; full check; docs check; test discovery. Official inert ASAR fixtures and controlled descriptor faults were exercised; real Electron execution, Windows and natural OS short-read frequency remain unverified. Equivalent compact CLI tasks returned identical results and reply token estimates; repeated timings do not establish a speedup. Fixes #3574 Fixes #5188
132 lines
4.7 KiB
TypeScript
132 lines
4.7 KiB
TypeScript
import { mkdir, open, readFile, truncate, writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { createPackage } from "@electron/asar";
|
|
import { expect, it, vi } from "vitest";
|
|
|
|
import {
|
|
admitAsarHeader,
|
|
MAX_ASAR_HEADER_BYTES,
|
|
} from "../../../src/artifacts/AsarHeader.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
|
|
const createFixture = async () => {
|
|
const root = await createTestTempDirectory("rea-asar-header-admission-");
|
|
const source = join(root, "source");
|
|
const archive = join(root, "fixture.asar");
|
|
await mkdir(source);
|
|
await writeFile(join(source, "main.js"), "export const answer = 42;\n");
|
|
await createPackage(source, archive);
|
|
return { archive, original: await readFile(archive) };
|
|
};
|
|
|
|
const admit = async (path: string) => {
|
|
const handle = await open(path, "r");
|
|
const read = vi.spyOn(handle, "read");
|
|
try {
|
|
return await admitAsarHeader(handle);
|
|
} finally {
|
|
// Admission must inspect only the fixed prefix, even for forged extents.
|
|
const calls = [...read.mock.calls];
|
|
const results = await Promise.all(
|
|
read.mock.results.map(
|
|
({ value }) => value as Promise<{ readonly bytesRead: number }>,
|
|
),
|
|
);
|
|
read.mockRestore();
|
|
await handle.close();
|
|
expect(handle.fd).toBe(-1);
|
|
expect(calls.length).toBeGreaterThan(0);
|
|
let filled = 0;
|
|
for (const [index, call] of calls.entries()) {
|
|
const [buffer, offset, length, position]: readonly unknown[] =
|
|
call as unknown as readonly unknown[];
|
|
expect(buffer).toBeInstanceOf(Buffer);
|
|
expect(offset).toBe(filled);
|
|
expect(position).toBe(filled);
|
|
expect(length).toBe(16 - filled);
|
|
filled += results[index]!.bytesRead;
|
|
}
|
|
expect(filled).toBeLessThanOrEqual(16);
|
|
}
|
|
};
|
|
|
|
it("admits a header produced by the upstream ASAR serializer", async () => {
|
|
const { archive, original } = await createFixture();
|
|
await expect(admit(archive)).resolves.toEqual({
|
|
archiveBytes: original.length,
|
|
headerBytes: original.readUInt32LE(4),
|
|
jsonBytes: original.readUInt32LE(12),
|
|
});
|
|
expect(await readFile(archive)).toEqual(original);
|
|
});
|
|
|
|
it("rejects every truncated prefix of a real serialized header", async () => {
|
|
const { archive, original } = await createFixture();
|
|
const headerEnd = 8 + original.readUInt32LE(4);
|
|
for (let length = 0; length < headerEnd; length++) {
|
|
await writeFile(archive, original.subarray(0, length));
|
|
await expect(
|
|
admit(archive),
|
|
`truncated at byte ${length}`,
|
|
).rejects.toMatchObject({
|
|
reason: "format",
|
|
});
|
|
}
|
|
// Member data is outside header admission; an intact header still passes.
|
|
await writeFile(archive, original.subarray(0, headerEnd));
|
|
await expect(admit(archive)).resolves.toMatchObject({
|
|
archiveBytes: headerEnd,
|
|
});
|
|
});
|
|
|
|
it("rejects contradictory and unsigned-boundary extents before decoding", async () => {
|
|
const { archive, original } = await createFixture();
|
|
const mutations = [
|
|
{ offset: 0, values: [0, 1, 3, 5, 0xffffffff] },
|
|
{ offset: 4, values: [0, 4, 7, original.length, 0x80000000, 0xffffffff] },
|
|
{ offset: 8, values: [0, 1, original.readUInt32LE(8) + 4, 0xffffffff] },
|
|
{ offset: 12, values: [original.readUInt32LE(8), 0x80000000, 0xffffffff] },
|
|
];
|
|
for (const { offset, values } of mutations) {
|
|
for (const value of values) {
|
|
const bytes = Buffer.from(original);
|
|
bytes.writeUInt32LE(value, offset);
|
|
await writeFile(archive, bytes);
|
|
await expect(
|
|
admit(archive),
|
|
`uint32 at ${offset} = ${value}`,
|
|
).rejects.toMatchObject({
|
|
reason: "format",
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
it("separates the header decode budget from container validity at its exact boundary", async () => {
|
|
const root = await createTestTempDirectory("rea-asar-header-budget-");
|
|
const archive = join(root, "budget.asar");
|
|
expect(MAX_ASAR_HEADER_BYTES).toBe(16 * 1024 * 1024);
|
|
for (const headerBytes of [
|
|
MAX_ASAR_HEADER_BYTES,
|
|
MAX_ASAR_HEADER_BYTES + 4,
|
|
]) {
|
|
const prefix = Buffer.alloc(16);
|
|
prefix.writeUInt32LE(4, 0);
|
|
prefix.writeUInt32LE(headerBytes, 4);
|
|
prefix.writeUInt32LE(headerBytes - 4, 8);
|
|
prefix.writeUInt32LE(2, 12);
|
|
await writeFile(archive, prefix);
|
|
// Supply the declared extent without allocating a header-sized JS buffer.
|
|
// This tests extent admission only; the padding is not valid archive JSON.
|
|
await truncate(archive, headerBytes + 8);
|
|
if (headerBytes === MAX_ASAR_HEADER_BYTES)
|
|
await expect(admit(archive)).resolves.toEqual({
|
|
archiveBytes: headerBytes + 8,
|
|
headerBytes,
|
|
jsonBytes: 2,
|
|
});
|
|
else
|
|
await expect(admit(archive)).rejects.toMatchObject({ reason: "limit" });
|
|
}
|
|
});
|