From c26b88bdf7fb52452c5cb66503938225f8cc8c4a Mon Sep 17 00:00:00 2001 From: Kamil Jopek Date: Wed, 9 Sep 2026 22:02:14 -0500 Subject: [PATCH 1/3] fix(tests): accept npm tar parser constructor names --- .../s3-http-exports/archive-parser.test.mjs | 36 +++++++++++++++++++ .../s3-http-exports/committed-archive.mjs | 3 +- 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 packages/safe-bash/tests/integration/s3-http-exports/archive-parser.test.mjs diff --git a/packages/safe-bash/tests/integration/s3-http-exports/archive-parser.test.mjs b/packages/safe-bash/tests/integration/s3-http-exports/archive-parser.test.mjs new file mode 100644 index 000000000..e8d5bce16 --- /dev/null +++ b/packages/safe-bash/tests/integration/s3-http-exports/archive-parser.test.mjs @@ -0,0 +1,36 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { digest, readArchive, resolveTools } from "./committed-archive.mjs"; + +// Exercise both public constructor names with the actual npm-bundled parser. +// In-memory archive bytes keep these controls independent of package staging. +for (const exportName of ["Parse", "Parser"]) { + test(`archive admission preserves safety with the ${exportName} tar API`, async () => { + const { tar } = resolveTools(); + const Parser = tar.Parser ?? tar.Parse; + const parserApi = { [exportName]: Parser }; + const archive = (path, type = "File") => { + const payload = Buffer.from("admitted bytes"); + const header = new tar.Header({ path, type, size: type === "File" ? payload.length : 0, linkpath: type === "SymbolicLink" ? "allowed.txt" : "", mode: 0o644 }); + const bytes = Buffer.alloc(2048); + header.encode(bytes); + if (type === "File") payload.copy(bytes, 512); + return bytes; + }; + const read = (bytes, admit = () => {}, expectedHash = digest(bytes)) => readArchive(parserApi, "/control.tar", expectedHash, admit, { + lstatSync: () => ({ isFile: () => true, size: bytes.length }), + readFileSync: () => bytes, + }); + const bytes = archive("allowed.txt"); + const admitted = []; + const files = await read(bytes, path => admitted.push(path)); + assert.deepEqual(admitted, ["allowed.txt"]); + assert.equal(files.get("allowed.txt").toString(), "admitted bytes"); + await assert.rejects(read(bytes, () => assert.fail("must authenticate first"), "0".repeat(64)), /identity changed/); + await assert.rejects(read(bytes, () => assert.fail("unbound entry")), /unbound entry/); + await assert.rejects(read(archive("../escape")), /nonliteral input path/); + await assert.rejects(read(archive("link.txt", "SymbolicLink")), /nonregular archive entry/); + const duplicate = Buffer.concat([bytes.subarray(0, 1024), bytes]); + await assert.rejects(read(duplicate), /duplicate or excessive archive entries/); + }); +} diff --git a/packages/safe-bash/tests/integration/s3-http-exports/committed-archive.mjs b/packages/safe-bash/tests/integration/s3-http-exports/committed-archive.mjs index 492935191..5e7dcde4b 100644 --- a/packages/safe-bash/tests/integration/s3-http-exports/committed-archive.mjs +++ b/packages/safe-bash/tests/integration/s3-http-exports/committed-archive.mjs @@ -551,7 +551,8 @@ export async function readArchive(tar, filename, expectedHash, admit, fileSystem const files = new Map(); let expanded = 0; await new Promise((resolvePromise, reject) => { - const parser = new tar.Parser({ strict: true, maxMetaEntrySize: 1024 * 1024 }); + const Parser = tar.Parser ?? tar.Parse; + const parser = new Parser({ strict: true, maxMetaEntrySize: 1024 * 1024 }); parser.on("error", reject); parser.on("end", resolvePromise); parser.on("entry", entry => { From 9a5d1a8a18131179b7f20f0461bda8ff966c71fd Mon Sep 17 00:00:00 2001 From: Kamil Jopek Date: Thu, 10 Sep 2026 01:35:41 -0500 Subject: [PATCH 2/3] test(safe-bash): discover archive parser compatibility regressions Import the tar Parse/Parser regression from the maintained integration entrypoint so normal test discovery executes both compatibility cases. Validation: maintained entrypoint discovers and passes both parser cases (2/2); guarded repository ESLint passes 10,528 files with zero errors or warnings. Tests: archive admission preserves safety with the Parse and Parser tar APIs; both verify exact archived bytes and archive safety checks. --- .../safe-bash/tests/integration/s3-http-exports/exports.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/safe-bash/tests/integration/s3-http-exports/exports.test.ts b/packages/safe-bash/tests/integration/s3-http-exports/exports.test.ts index 51fecdd39..924a36799 100644 --- a/packages/safe-bash/tests/integration/s3-http-exports/exports.test.ts +++ b/packages/safe-bash/tests/integration/s3-http-exports/exports.test.ts @@ -7,6 +7,7 @@ import { test } from "node:test"; import { fileURLToPath } from "node:url"; await import(new URL("./archive-controls.test.mjs", import.meta.url).href); +await import(new URL("./archive-parser.test.mjs", import.meta.url).href); const { cleanEnvironment } = await import(new URL("./committed-archive.mjs", import.meta.url).href); test("S3 HTTP root/subpath exports work from a clean packed revision without source fallback", { timeout: 300_000 }, () => { From 1d53107830cebb7f3e045f1eb60bcc248bd840c9 Mon Sep 17 00:00:00 2001 From: Kamil Jopek Date: Thu, 10 Sep 2026 02:48:07 -0500 Subject: [PATCH 3/3] test(safe-bash): complete isolated archive launcher fixture Include the parser-test module beside the copied outer launcher so its startup-environment control reaches the synthetic verifier after parser regressions are registered by the maintained test entrypoint. ### Tests Updated | Test | Verifies | | --- | --- | | maintained outer launcher rejects inherited startup settings before the verifier starts | Both baseline and poisoned environments reach the verifier without executing inherited startup code. | Validation: exact missing-module reproduction failed before the fixture addition; launcher and actual Parse/Parser archive-safety controls pass 3/3. --- .../tests/integration/s3-http-exports/archive-controls.test.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/safe-bash/tests/integration/s3-http-exports/archive-controls.test.mjs b/packages/safe-bash/tests/integration/s3-http-exports/archive-controls.test.mjs index 8ba07012c..6df60cd07 100644 --- a/packages/safe-bash/tests/integration/s3-http-exports/archive-controls.test.mjs +++ b/packages/safe-bash/tests/integration/s3-http-exports/archive-controls.test.mjs @@ -1018,6 +1018,7 @@ test("maintained outer launcher rejects inherited startup settings before the ve writeFileSync(join(directory, "exports.test.mjs"), launcher); assert.deepEqual(readFileSync(join(directory, "exports.test.mjs")), launcher); writeFileSync(join(directory, "archive-controls.test.mjs"), "export {};\n"); + writeFileSync(join(directory, "archive-parser.test.mjs"), "export {};\n"); writeFileSync(join(directory, "committed-archive.mjs"), `export { cleanEnvironment } from ${JSON.stringify(new URL("./committed-archive.mjs", import.meta.url).href)};\n`); const startupMarker = join(directory, "startup-ran"); const verifierMarker = join(directory, "synthetic-verifier-ran");