Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 5 additions & 27 deletions test/archive-portable-collisions.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fs from "node:fs/promises";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { tarFixture } from "./helpers/archive-fuzz.js";
import { extractArchive } from "../src/archive.js";
import {
__resetFsSafeNativeConfigForTest,
Expand All @@ -22,32 +23,6 @@ try {
// JS-only jobs intentionally exercise the fallback without a built binding.
}

function writeOctal(block: Buffer, offset: number, length: number, value: number): void {
block.write(`${value.toString(8).padStart(length - 1, "0")}\0`, offset, length, "ascii");
}

function tarFixture(paths: string[]): Buffer {
const blocks: Buffer[] = [];
for (const [index, entryPath] of paths.entries()) {
const body = Buffer.from(index === 0 ? "first" : "second");
const header = Buffer.alloc(512);
header.write(entryPath, 0, 100, "utf8");
writeOctal(header, 100, 8, 0o644);
writeOctal(header, 108, 8, 0);
writeOctal(header, 116, 8, 0);
writeOctal(header, 124, 12, body.length);
writeOctal(header, 136, 12, 0);
header.fill(0x20, 148, 156);
header[156] = 0x30;
header.write("ustar\0", 257, 6, "ascii");
header.write("00", 263, 2, "ascii");
const checksum = header.reduce((sum, byte) => sum + byte, 0);
header.write(`${checksum.toString(8).padStart(6, "0")}\0 `, 148, 8, "ascii");
blocks.push(header, body, Buffer.alloc((512 - (body.length % 512)) % 512));
}
return Buffer.concat([...blocks, Buffer.alloc(1024)]);
}

function useBackend(backend: "native" | "javascript"): void {
if (backend === "native") {
__setNativeLoaderForTest(() => native!);
Expand All @@ -73,7 +48,10 @@ describe.each(backends)("%s portable archive collisions", (backend) => {
const root = await tempRoot("fs-safe-archive-portable-tar-");
const archivePath = path.join(root, "payload.tar");
const destDir = path.join(root, "dest");
await fs.writeFile(archivePath, tarFixture([firstName, secondName]));
await fs.writeFile(archivePath, tarFixture([
{ path: firstName, body: "first" },
{ path: secondName, body: "second" },
]));
await fs.mkdir(destDir);

await expect(
Expand Down
40 changes: 1 addition & 39 deletions test/archive-read-boundaries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import path from "node:path";
import { gzipSync } from "node:zlib";
import JSZip from "jszip";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { tarFixture } from "./helpers/archive-fuzz.js";
import { useTempDirs } from "./helpers/vitest.js";
import {
ARCHIVE_LIMIT_ERROR_CODE,
Expand All @@ -23,45 +24,6 @@ import {

const { tempRoot } = useTempDirs();

type TarEntry = {
path: string;
body?: Buffer | string;
type?: string;
mutateHeader?: (header: Buffer) => void;
};

function writeString(block: Buffer, offset: number, length: number, value: string): void {
block.write(value, offset, Math.min(length, Buffer.byteLength(value)), "utf8");
}

function writeOctal(block: Buffer, offset: number, length: number, value: number): void {
writeString(block, offset, length, `${value.toString(8).padStart(length - 1, "0")}\0`);
}

function tarFixture(entries: TarEntry[], endBlocks = true): Buffer {
const blocks: Buffer[] = [];
for (const entry of entries) {
const body = Buffer.isBuffer(entry.body) ? entry.body : Buffer.from(entry.body ?? "");
const header = Buffer.alloc(512);
writeString(header, 0, 100, entry.path);
writeOctal(header, 100, 8, 0o644);
writeOctal(header, 108, 8, 0);
writeOctal(header, 116, 8, 0);
writeOctal(header, 124, 12, body.length);
writeOctal(header, 136, 12, 0);
header.fill(0x20, 148, 156);
writeString(header, 156, 1, entry.type ?? "0");
writeString(header, 257, 6, "ustar\0");
writeString(header, 263, 2, "00");
entry.mutateHeader?.(header);
const checksum = header.reduce((sum, byte) => sum + byte, 0);
writeString(header, 148, 8, `${checksum.toString(8).padStart(6, "0")}\0 `);
blocks.push(header, body, Buffer.alloc((512 - (body.length % 512)) % 512));
}
if (endBlocks) blocks.push(Buffer.alloc(1024));
return Buffer.concat(blocks);
}

function rawHeader(type = "0", size = 0): Buffer {
return tarFixture([{ path: "entry", type, body: Buffer.alloc(size) }], false).subarray(0, 512);
}
Expand Down
62 changes: 27 additions & 35 deletions test/file-lock-sync-exit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,41 +7,33 @@ import { acquireFileLockSync } from "../src/file-lock.js";
const { tempRoot } = useTempDirs();

describe("synchronous file-lock process-exit cleanup", () => {
it("registers identity-checked cleanup for held locks", async () => {
const base = await tempRoot("fs-safe-sync-lock-exit-");
const targetPath = path.join(base, "state.json");
const lock = acquireFileLockSync(targetPath, {
staleMs: 60_000,
payload: () => ({ pid: process.pid, createdAt: new Date().toISOString() }),
});
expect(fs.existsSync(lock.lockPath)).toBe(true);
it.each([false, true])(
"registers identity-checked cleanup (replacement=%s)",
async (replaceLock) => {
const base = await tempRoot("fs-safe-sync-lock-exit-");
const targetPath = path.join(base, "state.json");
const lock = acquireFileLockSync(targetPath, {
staleMs: 60_000,
payload: () => ({ pid: process.pid, createdAt: new Date().toISOString() }),
});
expect(fs.existsSync(lock.lockPath)).toBe(true);
if (replaceLock) {
fs.renameSync(lock.lockPath, `${lock.lockPath}.displaced`);
fs.writeFileSync(lock.lockPath, "replacement");
}

const cleanup = Reflect.get(
globalThis,
Symbol.for("fsSafe.syncSidecarLockCleanupHandler"),
) as (() => void) | undefined;
expect(cleanup).toBeTypeOf("function");
cleanup?.();
const cleanup = Reflect.get(
globalThis,
Symbol.for("fsSafe.syncSidecarLockCleanupHandler"),
) as () => void;
expect(cleanup).toBeTypeOf("function");
cleanup();

expect(fs.existsSync(lock.lockPath)).toBe(false);
});

it("preserves a replacement during process-exit cleanup", async () => {
const base = await tempRoot("fs-safe-sync-lock-exit-swap-");
const targetPath = path.join(base, "state.json");
const lock = acquireFileLockSync(targetPath, {
staleMs: 60_000,
payload: () => ({ pid: process.pid, createdAt: new Date().toISOString() }),
});
fs.renameSync(lock.lockPath, `${lock.lockPath}.displaced`);
fs.writeFileSync(lock.lockPath, "replacement");

const cleanup = Reflect.get(
globalThis,
Symbol.for("fsSafe.syncSidecarLockCleanupHandler"),
) as () => void;
cleanup();

expect(fs.readFileSync(lock.lockPath, "utf8")).toBe("replacement");
});
if (replaceLock) {
expect(fs.readFileSync(lock.lockPath, "utf8")).toBe("replacement");
} else {
expect(fs.existsSync(lock.lockPath)).toBe(false);
}
},
);
});
51 changes: 47 additions & 4 deletions test/helpers/archive-fuzz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ export type TarSizeEncoding =
| "base256-negative"
| "invalid-octal";

export type TarFixtureEntry = {
path: string;
body?: Buffer | string;
mode?: number;
type?: string;
linkPath?: string;
base256Size?: number;
mutateHeader?: (header: Buffer) => void;
};

function writeString(block: Buffer, offset: number, length: number, value: string): void {
block.write(value, offset, Math.min(length, Buffer.byteLength(value)), "utf8");
}
Expand All @@ -23,6 +33,42 @@ function updateTarChecksum(header: Buffer): void {
writeString(header, 148, 8, `${checksum.toString(8).padStart(6, "0")}\0 `);
}

export function tarFixture(
entries: readonly TarFixtureEntry[],
endBlocks = true,
): Buffer {
const blocks: Buffer[] = [];
for (const entry of entries) {
const body = Buffer.isBuffer(entry.body) ? entry.body : Buffer.from(entry.body ?? "");
const type = entry.type ?? "0";
const hasBody = ["0", "7", "K", "L", "g", "x"].includes(type);
const header = Buffer.alloc(512);
writeString(header, 0, 100, entry.path);
writeOctal(header, 100, 8, entry.mode ?? (type === "5" ? 0o755 : 0o644));
writeOctal(header, 108, 8, 0);
writeOctal(header, 116, 8, 0);
if (entry.base256Size === undefined) {
writeOctal(header, 124, 12, hasBody ? body.length : 0);
} else {
header[124] = 0x80;
header.writeBigUInt64BE(BigInt(entry.base256Size), 128);
}
writeOctal(header, 136, 12, 0);
writeString(header, 156, 1, type);
writeString(header, 157, 100, entry.linkPath ?? "");
writeString(header, 257, 6, "ustar\0");
writeString(header, 263, 2, "00");
entry.mutateHeader?.(header);
updateTarChecksum(header);
blocks.push(header);
if (hasBody && entry.base256Size === undefined) {
blocks.push(body, Buffer.alloc((512 - (body.length % 512)) % 512));
}
}
if (endBlocks) blocks.push(Buffer.alloc(1024));
return Buffer.concat(blocks);
}

export function tarBytes(params: {
name: string;
body?: Buffer;
Expand Down Expand Up @@ -76,10 +122,7 @@ export function tarBytes(params: {
export function tarEntriesBytes(
entries: ReadonlyArray<{ name: string; body?: Buffer }>,
): Buffer {
return Buffer.concat([
...entries.map((entry) => tarBytes(entry).subarray(0, -1024)),
Buffer.alloc(1024),
]);
return tarFixture(entries.map(({ name, body }) => ({ path: name, body })));
}

export async function zipBytes(params: {
Expand Down
73 changes: 5 additions & 68 deletions test/native-archive-equivalence.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { expectFsSafeErrorSync } from "./helpers/security.js";
import { tarFixture, type TarFixtureEntry } from "./helpers/archive-fuzz.js";
import { useTempDirs } from "./helpers/vitest.js";
import {
ARCHIVE_LIMIT_ERROR_CODE,
extractArchive,
Expand All @@ -25,71 +26,8 @@ try {
} catch {
// JS-only jobs intentionally exercise the fallback without a built binding.
}
const tempDirs: string[] = [];

type TarFixtureEntry = {
path: string;
body?: string;
mode?: number;
type?: "0" | "1" | "2" | "5" | "7" | "K" | "L" | "S" | "g" | "x";
linkPath?: string;
base256Size?: number;
};

function writeString(block: Buffer, offset: number, length: number, value: string): void {
block.write(value, offset, Math.min(length, Buffer.byteLength(value)), "utf8");
}

function writeOctal(block: Buffer, offset: number, length: number, value: number): void {
writeString(block, offset, length, `${value.toString(8).padStart(length - 1, "0")}\0`);
}

function tarFixture(entries: TarFixtureEntry[]): Buffer {
const blocks: Buffer[] = [];
for (const fixture of entries) {
const body = Buffer.from(fixture.body ?? "");
const type = fixture.type ?? "0";
const header = Buffer.alloc(512);
writeString(header, 0, 100, fixture.path);
writeOctal(header, 100, 8, fixture.mode ?? (type === "5" ? 0o755 : 0o644));
writeOctal(header, 108, 8, 0);
writeOctal(header, 116, 8, 0);
const hasBody =
type === "0" ||
type === "7" ||
type === "K" ||
type === "L" ||
type === "g" ||
type === "x";
const size = fixture.base256Size ?? (hasBody ? body.length : 0);
if (fixture.base256Size === undefined) {
writeOctal(header, 124, 12, size);
} else {
header[124] = 0x80;
header.writeBigUInt64BE(BigInt(size), 128);
}
writeOctal(header, 136, 12, 0);
header.fill(0x20, 148, 156);
writeString(header, 156, 1, type);
writeString(header, 157, 100, fixture.linkPath ?? "");
writeString(header, 257, 6, "ustar\0");
writeString(header, 263, 2, "00");
const checksum = header.reduce((sum, byte) => sum + byte, 0);
writeString(header, 148, 8, `${checksum.toString(8).padStart(6, "0")}\0 `);
blocks.push(header);
if (hasBody && fixture.base256Size === undefined) {
blocks.push(body, Buffer.alloc((512 - (body.length % 512)) % 512));
}
}
blocks.push(Buffer.alloc(1024));
return Buffer.concat(blocks);
}

async function tempRoot(): Promise<string> {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "fs-safe-native-archive-"));
tempDirs.push(root);
return root;
}
const { tempRoot: createTempRoot } = useTempDirs();
const tempRoot = () => createTempRoot("fs-safe-native-archive-");

function useBackend(backend: "native" | "javascript"): void {
if (backend === "native") {
Expand Down Expand Up @@ -117,10 +55,9 @@ async function settleWithin<T>(promise: Promise<T>, milliseconds = 2_000): Promi
}
}

afterEach(async () => {
afterEach(() => {
__resetFsSafeNativeConfigForTest();
__resetNativeLoaderForTest();
await Promise.all(tempDirs.splice(0).map((root) => fs.rm(root, { recursive: true, force: true })));
});

const archiveBackends = native
Expand Down
Loading