fs-safe/test/new-primitives.test.ts
Sarah Fortune 3be7ba6ee3 ci+test: run check on windows and guard windows-only test behavior
Run the check job on windows-latest in addition to ubuntu so the
windows code paths (no O_NOFOLLOW, node fallbacks for fd-relative
ops, ACL inspection) are exercised on every PR rather than only
documented.

Make the test suite pass on the new windows runner by addressing
the platform-specific failures:

- Long happy-path tests that mix supported (mkdir, write, read) and
  unsupported (stat, list, move, exists) operations are guarded
  with skipIf(process.platform === "win32") since the pinned
  filesystem helper throws "unsupported-platform" on win32 by
  design (src/pinned-python.ts).
- Short focused tests where the unsupported operation is the whole
  point (pinned-python, pinned-write-fallback-coverage,
  write-boundary-bypass symlink-move) split into runIf(non-win32)
  and runIf(win32) tests, with the windows variant asserting
  unsupported-platform.
- The expectFsSafeCode helper accepts unsupported-platform on
  windows; new expectedFsSafeCode helper substitutes for
  per-rejects.toMatchObject sites where the windows code differs
  from posix (e.g. path-alias / not-found returning
  unsupported-platform via the helper layer).
- secure-file-reads test split into a posix happy-path runIf and a
  windows runIf that asserts permission-unverified, since ACL
  inspection has no portable equivalent on windows
  (src/secure-file.ts:177).
- safeFileURLToPath test uses hardcoded platform-specific input/
  output instead of building the URL via pathToFileURL+fileURLToPath
  so the assertion verifies the function directly.
- Fix expandHomePrefix to normalize path separators by splitting via
  path.normalize + path.sep and rejoining via path.join. Apply the
  same segment-based check to resolveHomeRelativePath and
  resolveOsHomeRelativePath. Drop input.trim() — whitespace is a
  valid filename character on both platforms and env-var inputs are
  already trimmed upstream via normalizeOptionalString.
- coverage-more's "normalizes empty temp names" decomposes the
  result with path.dirname/path.basename instead of regex-matching
  a path-separator literal.
- extracted-helpers' path-helpers test builds its root with
  path.resolve so the drive letter is present on windows.
- additional-boundary-bypass guards its "..\evil.txt" sanitizer
  assertion behind a non-win32 check (windows reserves "\" as a
  path separator and cannot have it in a filename).
- coverage-more's sibling temp test guards just the posix file-mode
  assertion (stat.mode & 0o777 === 0o600), which has no analog on
  windows. The syncing behaviour the test actually targets still
  runs on both platforms.
- Raise test/new-primitives.test.ts size budget to 1500 to
  accommodate the secure-file-reads test split.

After: 253 passed, 1 failed, 66 skipped on windows-11-arm64. The
single remaining failure is a separate library-side gap (a
SAFE_REJECTED_SUSPICIOUS_WRITE_PAYLOADS payload resolves on windows
instead of rejecting) and will be tracked in a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 14:59:24 -07:00

1016 lines
35 KiB
TypeScript

import fs from "node:fs/promises";
import syncFs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
appendRegularFile,
appendRegularFileSync,
readRegularFile,
readRegularFileSync,
resolveRegularFileAppendFlags,
statRegularFile,
} from "../src/regular-file.js";
import {
tempWorkspace,
tempWorkspaceSync,
withTempWorkspace,
withTempWorkspaceSync,
} from "../src/private-temp-workspace.js";
import {
assertNoSymlinkParents,
assertNoSymlinkParentsSync,
} from "../src/symlink-parents.js";
import { pathScope } from "../src/root-paths.js";
import { replaceFileAtomic, replaceFileAtomicSync } from "../src/replace-file.js";
import { movePathWithCopyFallback } from "../src/move-path.js";
import { writeSiblingTempFile } from "../src/sibling-temp.js";
import { acquireFileLock, createFileLockManager, withFileLock } from "../src/file-lock.js";
import { fileStore, fileStoreSync } from "../src/file-store.js";
import { jsonStore } from "../src/json-store.js";
import {
createIcaclsResetCommand,
formatIcaclsResetCommand,
formatPermissionDetail,
formatPermissionRemediation,
formatWindowsAclSummary,
inspectPathPermissions,
inspectWindowsAcl,
modeBits,
parseIcaclsOutput,
resolveWindowsUserPrincipal,
summarizeWindowsAcl,
} from "../src/permissions.js";
import { readSecureFile } from "../src/secure-file.js";
import { walkDirectory, walkDirectorySync } from "../src/walk.js";
let root: string;
beforeEach(async () => {
root = await fs.mkdtemp(path.join(os.tmpdir(), "fs-safe-new-"));
});
afterEach(async () => {
vi.unstubAllEnvs();
await fs.rm(root, { recursive: true, force: true });
});
describe("private temp workspaces", () => {
it("writes private files and removes the workspace", async () => {
let workspaceDir = "";
const content = await withTempWorkspace({ rootDir: root, prefix: "work-" }, async (tmp) => {
workspaceDir = tmp.dir;
const filePath = await tmp.write("input.txt", "hello");
expect(await fs.readFile(filePath, "utf8")).toBe("hello");
return await tmp.read("input.txt");
});
expect(content.toString("utf8")).toBe("hello");
await expect(fs.stat(workspaceDir)).rejects.toMatchObject({ code: "ENOENT" });
});
it("rejects path-like file names", async () => {
const tmp = await tempWorkspace({ rootDir: root, prefix: "work-" });
try {
await expect(tmp.write("../escape.txt", "nope")).rejects.toThrow(/Invalid/);
} finally {
await tmp.cleanup();
}
});
it("supports sync temp workspaces", async () => {
let workspaceDir = "";
const result = withTempWorkspaceSync({ rootDir: root, prefix: "sync-" }, (tmp) => {
workspaceDir = tmp.dir;
const filePath = tmp.write("input.txt", "hello");
expect(tmp.read("input.txt").toString("utf8")).toBe("hello");
return filePath;
});
expect(path.basename(result)).toBe("input.txt");
await expect(fs.stat(workspaceDir)).rejects.toMatchObject({ code: "ENOENT" });
const tmp = tempWorkspaceSync({ rootDir: root, prefix: "sync-" });
try {
expect(tmp.write("again.txt", "ok")).toContain("again.txt");
} finally {
tmp.cleanup();
}
});
it("supports the compact tempWorkspace factory and await using cleanup", async () => {
let workspaceDir = "";
{
await using tmp = await tempWorkspace({ rootDir: root, prefix: "compact-" });
workspaceDir = tmp.dir;
const filePath = await tmp.write("input.txt", "hello");
expect(filePath).toBe(tmp.path("input.txt"));
expect(tmp.path("input.txt")).toBe(filePath);
await tmp.store.json<{ ok: boolean }>("state.json").write({ ok: true });
await expect(tmp.store.readJson("state.json")).resolves.toEqual({ ok: true });
}
await expect(fs.stat(workspaceDir)).rejects.toMatchObject({ code: "ENOENT" });
});
it("writes JSON and copies files through temp workspace helpers", async () => {
await using tmp = await tempWorkspace({ rootDir: root, prefix: "helpers-" });
const source = path.join(root, "source.txt");
await fs.writeFile(source, "copied", "utf8");
expect(await fs.readFile(await tmp.writeText("text.txt", "hello"), "utf8")).toBe("hello");
expect(JSON.parse(await fs.readFile(await tmp.writeJson("state.json", { ok: true }), "utf8")))
.toEqual({ ok: true });
expect(await fs.readFile(await tmp.copyIn("copy.txt", source), "utf8")).toBe("copied");
});
});
describe("file store", () => {
it.skipIf(process.platform === "win32")("writes, reads, copies, and prunes files under the store root", async () => {
const store = fileStore({ rootDir: root, maxBytes: 1024 });
await store.write("media/a.txt", "hello");
await expect(store.readBytes("media/a.txt")).resolves.toEqual(Buffer.from("hello"));
await expect(store.readText("media/a.txt")).resolves.toBe("hello");
await store.writeJson("media/state.json", { ok: true });
await expect(store.readJson("media/state.json")).resolves.toEqual({ ok: true });
const source = path.join(root, "source.bin");
await fs.writeFile(source, "source", "utf8");
await store.copyIn("media/b.txt", source);
await expect(store.readBytes("media/b.txt")).resolves.toEqual(Buffer.from("source"));
await fs.utimes(store.path("media/a.txt"), new Date(0), new Date(0));
await store.pruneExpired({ ttlMs: 1, recursive: true, pruneEmptyDirs: true });
await expect(store.exists("media/a.txt")).resolves.toBe(false);
});
it("rejects escaped paths and size limit violations", async () => {
const store = fileStore({ rootDir: root, maxBytes: 3 });
await expect(store.write("../escape.txt", "nope")).rejects.toThrow();
await expect(store.write("too-large.txt", "four")).rejects.toMatchObject({
code: "too-large",
});
});
});
describe("json store", () => {
it("reads fallback, writes atomically, and updates under a lock", async () => {
const filePath = path.join(root, "state", "store.json");
const store = fileStore({ rootDir: path.dirname(filePath), private: true }).json<{
count: number;
}>(path.basename(filePath), {
lock: true,
});
await expect(store.read()).resolves.toBeUndefined();
await expect(store.readOr({ count: 10 })).resolves.toEqual({ count: 10 });
await expect(store.readRequired()).rejects.toMatchObject({ code: "not-found" });
await store.updateOr({ count: 0 }, (current) => ({ count: current.count + 1 }));
await expect(store.read()).resolves.toEqual({ count: 1 });
await expect(store.readRequired()).resolves.toEqual({ count: 1 });
const strictStore = jsonStore<{ count: number }>({ filePath: path.join(root, "missing.json") });
await expect(strictStore.read()).resolves.toBeUndefined();
await expect(strictStore.readOr({ count: 2 })).resolves.toEqual({ count: 2 });
});
});
describe("secure file reads", () => {
it.runIf(process.platform !== "win32")("reads from a validated file handle", async () => {
const filePath = path.join(root, "secret.json");
await fs.writeFile(filePath, '{"token":"ok"}', { mode: 0o600 });
await fs.chmod(filePath, 0o600).catch(() => undefined);
const result = await readSecureFile({
filePath,
label: "test secret",
io: { maxBytes: 1024 },
});
expect(result.buffer.toString("utf8")).toBe('{"token":"ok"}');
expect(result.realPath).toBe(await fs.realpath(filePath));
});
it.runIf(process.platform === "win32")(
"fails closed on windows when ACL inspection is unavailable",
async () => {
// See src/secure-file.ts:177 — readSecureFile throws permission-unverified
// on Windows because ACL inspection has no portable equivalent.
const filePath = path.join(root, "secret.json");
await fs.writeFile(filePath, '{"token":"ok"}', { mode: 0o600 });
await expect(
readSecureFile({
filePath,
label: "test secret",
io: { maxBytes: 1024 },
}),
).rejects.toMatchObject({ code: "permission-unverified" });
},
);
it("rejects symlinks and files outside trusted dirs", async () => {
const trusted = path.join(root, "trusted");
const outside = path.join(root, "outside");
await fs.mkdir(trusted);
await fs.mkdir(outside);
const trustedFile = path.join(trusted, "secret.txt");
const outsideFile = path.join(outside, "secret.txt");
const link = path.join(trusted, "link.txt");
await fs.writeFile(trustedFile, "ok", { mode: 0o600 });
await fs.writeFile(outsideFile, "no", { mode: 0o600 });
await fs.symlink(outsideFile, link);
await expect(readSecureFile({ filePath: link })).rejects.toMatchObject({ code: "symlink" });
await expect(
readSecureFile({ filePath: outsideFile, trust: { trustedDirs: [trusted] } }),
).rejects.toMatchObject({ code: "outside-workspace" });
});
it("rejects network paths unless explicitly trusted", async () => {
await expect(readSecureFile({ filePath: String.raw`\\server\share\secret.txt` })).rejects
.toMatchObject({ code: "invalid-path" });
});
it("rejects overly broad POSIX permissions", async () => {
if (process.platform === "win32") return;
const filePath = path.join(root, "too-open.txt");
await fs.writeFile(filePath, "secret", { mode: 0o644 });
await fs.chmod(filePath, 0o644);
await expect(readSecureFile({ filePath })).rejects.toMatchObject({
code: "insecure-permissions",
});
});
it("covers symlink, directory, size, and trusted-dir secure read branches", async () => {
const target = path.join(root, "target.txt");
const link = path.join(root, "link.txt");
const trusted = path.join(root, "trusted");
const outsideTrusted = path.join(root, "outside-trusted");
await fs.writeFile(target, "secret", { mode: 0o600 });
await fs.symlink(target, link);
await fs.mkdir(trusted);
await fs.mkdir(outsideTrusted);
await expect(readSecureFile({ filePath: "relative.txt" })).rejects.toMatchObject({
code: "invalid-path",
});
await expect(readSecureFile({ filePath: root })).rejects.toMatchObject({ code: "not-file" });
await expect(
readSecureFile({
filePath: link,
trust: { allowSymlink: true, trustedDirs: [outsideTrusted] },
permissions: { allowInsecure: true },
}),
).rejects.toMatchObject({ code: "outside-workspace" });
const result = await readSecureFile({
filePath: link,
trust: { allowSymlink: true, trustedDirs: [root] },
permissions: { allowInsecure: true },
io: { maxBytes: 100, timeoutMs: 1000 },
});
expect(result.buffer.toString("utf8")).toBe("secret");
await expect(
readSecureFile({
filePath: target,
permissions: { allowInsecure: true },
io: { maxBytes: 2 },
}),
).rejects.toMatchObject({ code: "too-large" });
});
it("uses Windows ACL permission checks for secure reads when requested", async () => {
const filePath = path.join(root, "windows-secret.txt");
await fs.writeFile(filePath, "secret", { mode: 0o600 });
const exec = vi.fn().mockResolvedValue({
stdout: "*S-1-5-18:(F)\n",
stderr: "",
});
const result = await readSecureFile({
filePath,
inject: { platform: "win32", exec },
permissions: { allowReadableByOthers: true },
});
expect(result.buffer.toString("utf8")).toBe("secret");
expect(result.permissions?.source).toBe("windows-acl");
const unsafeExec = vi.fn().mockResolvedValue({
stdout: "Everyone:(R)\n",
stderr: "",
});
await expect(
readSecureFile({
filePath,
inject: { platform: "win32", exec: unsafeExec },
}),
).rejects.toMatchObject({ code: "insecure-permissions" });
const failedExec = vi.fn().mockRejectedValue(new Error("icacls failed"));
await expect(
readSecureFile({
filePath,
inject: { platform: "win32", exec: failedExec },
}),
).rejects.toMatchObject({ code: "permission-unverified" });
});
it("parses icacls output into ACL entries", () => {
const entries = parseIcaclsOutput(
String.raw`C:\Users\me\secret.txt *S-1-5-18:(F)
*S-1-1-0:(R)`,
String.raw`C:\Users\me\secret.txt`,
);
expect(entries).toMatchObject([
{ principal: "*S-1-5-18", canWrite: true },
{ principal: "*S-1-1-0", canRead: true },
]);
});
it("resolves Windows system commands from trusted absolute roots", async () => {
vi.stubEnv("SystemRoot", "D:\\Windows");
const exec = vi.fn().mockResolvedValue({
stdout: String.raw`C:\Users\me\secret.txt *S-1-5-18:(F)`,
stderr: "",
});
const result = await inspectWindowsAcl(String.raw`C:\Users\me\secret.txt`, { exec });
expect(result.ok).toBe(true);
expect(exec).toHaveBeenCalledWith("D:\\Windows\\System32\\icacls.exe", [
String.raw`C:\Users\me\secret.txt`,
"/sid",
]);
const fallbackExec = vi.fn().mockResolvedValue({
stdout: String.raw`C:\Users\me\secret.txt *S-1-5-18:(F)`,
stderr: "",
});
await inspectWindowsAcl(String.raw`C:\Users\me\secret.txt`, {
exec: fallbackExec,
env: { SystemRoot: ".\\fake-root", WINDIR: "E:\\Windows" },
});
expect(fallbackExec).toHaveBeenCalledWith("E:\\Windows\\System32\\icacls.exe", [
String.raw`C:\Users\me\secret.txt`,
"/sid",
]);
const command = createIcaclsResetCommand(String.raw`C:\Users\me\secret.txt`, {
isDir: false,
env: { systemroot: ".\\fake-root", username: "me" },
userInfo: () => ({ username: "me" }),
});
expect(command?.command).toBe("C:\\Windows\\System32\\icacls.exe");
});
it("covers permission formatting and ACL classification helpers", async () => {
const missing = await inspectPathPermissions(path.join(root, "missing.txt"));
expect(missing.ok).toBe(false);
const target = path.join(root, "acl-target.txt");
const link = path.join(root, "acl-link.txt");
await fs.writeFile(target, "ok", { mode: 0o640 });
await fs.symlink(target, link);
const posix = await inspectPathPermissions(link, { platform: "linux" });
expect(posix.isSymlink).toBe(true);
expect(formatPermissionDetail(target, posix)).toContain("mode=");
expect(
formatPermissionRemediation({
targetPath: target,
perms: posix,
isDir: false,
posixMode: 0o600,
}),
).toBe(`chmod 600 ${target}`);
const entries = parseIcaclsOutput(
[
`"C:\\Secrets\\token.txt" DOMAIN\\me:(F)`,
"Everyone:(R)",
"BUILTIN\\Users:(M)",
"*S-1-5-21-123:(R)",
"Denied:(DENY)(F)",
"Successfully processed 1 files; Failed processing 0 files",
].join("\n"),
String.raw`C:\Secrets\token.txt`,
);
const summary = summarizeWindowsAcl(entries, {
USERDOMAIN: "DOMAIN",
USERNAME: "me",
USERSID: "S-1-5-21-999",
});
expect(summary.trusted.map((entry) => entry.principal)).toContain("DOMAIN\\me");
expect(summary.untrustedWorld.some((entry) => entry.principal === "Everyone")).toBe(true);
expect(summary.untrustedGroup.some((entry) => entry.principal === "*S-1-5-21-123")).toBe(true);
expect(formatWindowsAclSummary({ ok: true, entries, ...summary })).toContain("Everyone");
expect(formatWindowsAclSummary({ ok: false, entries: [], trusted: [], untrustedWorld: [], untrustedGroup: [] }))
.toBe("unknown");
expect(resolveWindowsUserPrincipal({ USERDOMAIN: "DOMAIN", USERNAME: "me" })).toBe(
"DOMAIN\\me",
);
expect(resolveWindowsUserPrincipal({}, () => ({ username: "fallback" }))).toBe("fallback");
expect(createIcaclsResetCommand(target, { isDir: true, userInfo: () => ({}) })).toBeNull();
expect(
formatIcaclsResetCommand(String.raw`C:\Secrets\token.txt`, {
isDir: true,
env: { SystemRoot: "D:\\Windows", USERNAME: "me" },
}),
).toContain('"me:(OI)(CI)F"');
expect(modeBits(0o100777)).toBe(0o777);
});
it("resolves the current user SID when ACL output only contains an unknown SID", async () => {
const target = String.raw`C:\Secrets\token.txt`;
const exec = vi
.fn()
.mockResolvedValueOnce({
stdout: `${target} *S-1-5-21-42:(F)\nEveryone:(R)\n`,
stderr: "",
})
.mockResolvedValueOnce({
stdout: '"USER","SID"\n"DOMAIN\\me","S-1-5-21-42"\n',
stderr: "",
});
const result = await inspectWindowsAcl(target, { exec, env: { SystemRoot: "C:\\Windows" } });
expect(result.ok).toBe(true);
expect(result.trusted.some((entry) => entry.principal === "*S-1-5-21-42")).toBe(true);
expect(exec).toHaveBeenCalledTimes(2);
});
});
describe("directory walking", () => {
it("walks bounded trees and reports truncation", async () => {
await fs.mkdir(path.join(root, "a", "b"), { recursive: true });
await fs.writeFile(path.join(root, "a", "one.txt"), "1");
await fs.writeFile(path.join(root, "a", "b", "two.txt"), "2");
const shallow = walkDirectorySync(root, { maxDepth: 1 });
expect(shallow.entries.map((entry) => entry.relativePath)).toEqual(["a"]);
const files = await walkDirectory(root, {
include: (entry) => entry.kind === "file",
});
expect(files.entries.map((entry) => entry.relativePath).sort()).toEqual([
path.join("a", "b", "two.txt"),
path.join("a", "one.txt"),
]);
const truncated = walkDirectorySync(root, { maxEntries: 1 });
expect(truncated.truncated).toBe(true);
expect(truncated.scannedEntryCount).toBe(1);
});
it("does not recurse forever through followed symlink cycles", async () => {
if (process.platform === "win32") return;
await fs.mkdir(path.join(root, "a"), { recursive: true });
await fs.writeFile(path.join(root, "a", "file.txt"), "1");
await fs.symlink(root, path.join(root, "a", "loop"));
const scan = await walkDirectory(root, { symlinks: "follow" });
expect(scan.truncated).toBe(false);
expect(scan.entries.map((entry) => entry.relativePath).sort()).toEqual([
"a",
path.join("a", "file.txt"),
path.join("a", "loop"),
]);
});
});
describe("private file store mode", () => {
it("writes JSON under the store root", async () => {
const store = fileStore({ rootDir: root, private: true });
await store.writeJson("nested/state.json", { ok: true }, { trailingNewline: true });
expect(await fs.readFile(path.join(root, "nested", "state.json"), "utf8")).toBe(
'{\n "ok": true\n}\n',
);
await expect(store.readJson("nested/state.json")).resolves.toEqual({ ok: true });
});
it("rejects paths outside the store root", async () => {
const store = fileStore({ rootDir: root, private: true });
await expect(store.writeText("../escape.txt", "nope")).rejects.toThrow(/relative path/);
await expect(store.readTextIfExists("../escape.txt")).rejects.toThrow(/outside workspace root/);
});
it("supports sync JSON writes", async () => {
const filePath = fileStoreSync({ rootDir: root, private: true }).writeJson("sync.json", {
ok: true,
});
expect(JSON.parse(await fs.readFile(filePath, "utf8"))).toEqual({ ok: true });
});
it("has explicit lenient read helpers", async () => {
const store = fileStore({ rootDir: root, private: true });
await store.writeText("state.txt", "hello");
await store.writeJson("state.json", { ok: true });
await expect(store.readTextIfExists("state.txt")).resolves.toBe("hello");
await expect(store.readJsonIfExists("state.json")).resolves.toEqual({ ok: true });
await expect(store.readTextIfExists("missing.txt")).resolves.toBeNull();
await expect(store.readJsonIfExists("missing.json")).resolves.toBeNull();
});
});
describe("file locks", () => {
it("supports await using cleanup", async () => {
const targetPath = path.join(root, "locked.txt");
let lockPath = "";
{
await using lock = await acquireFileLock(targetPath, {
managerKey: `test-${Date.now()}-${Math.random()}`,
staleMs: 60_000,
payload: () => ({ owner: "test" }),
});
lockPath = lock.lockPath;
await expect(fs.stat(lockPath)).resolves.toMatchObject({});
}
await expect(fs.stat(lockPath)).rejects.toMatchObject({ code: "ENOENT" });
});
it("supports manager lifecycle and top-level withFileLock", async () => {
const targetPath = path.join(root, "managed-lock.txt");
const manager = createFileLockManager(`manager-${Date.now()}-${Math.random()}`);
const lock = await manager.acquire(targetPath, {
staleMs: 60_000,
allowReentrant: true,
metadata: { suite: "new-primitives" },
payload: () => ({ owner: "manager" }),
});
const reentrant = await manager.acquire(targetPath, {
staleMs: 60_000,
allowReentrant: true,
payload: () => ({ owner: "manager" }),
});
expect(manager.heldEntries()).toHaveLength(1);
expect(manager.heldEntries()[0]?.metadata).toEqual({ suite: "new-primitives" });
await reentrant.release();
await lock.release();
expect(manager.heldEntries()).toEqual([]);
await expect(
manager.withLock(
targetPath,
{ staleMs: 60_000, payload: () => ({ owner: "manager" }) },
async () => "ok",
),
).resolves.toBe("ok");
await expect(
withFileLock(
path.join(root, "top-level-lock.txt"),
{ staleMs: 60_000, payload: () => ({ owner: "top-level" }) },
async () => "locked",
),
).resolves.toBe("locked");
manager.reset();
await manager.drain();
});
});
describe("regular file append", () => {
it("keeps append flags usable when O_NOFOLLOW is unavailable", () => {
expect(
resolveRegularFileAppendFlags({
O_APPEND: 0x01,
O_CREAT: 0x02,
O_WRONLY: 0x04,
}),
).toBe(0x07);
});
it("appends with restrictive permissions and honors max bytes", async () => {
const filePath = path.join(root, "events.jsonl");
await appendRegularFile({ filePath, content: "12345\n", maxFileBytes: 6 });
await appendRegularFile({ filePath, content: "after\n", maxFileBytes: 6 });
expect(await fs.readFile(filePath, "utf8")).toBe("12345\n");
if (process.platform !== "win32") {
expect((await fs.stat(filePath)).mode & 0o777).toBe(0o600);
}
});
it("appends synchronously with restrictive permissions and honors max bytes", async () => {
const filePath = path.join(root, "sync-events.jsonl");
appendRegularFileSync({ filePath, content: "12345\n", maxFileBytes: 6 });
appendRegularFileSync({ filePath, content: "after\n", maxFileBytes: 6 });
expect(await fs.readFile(filePath, "utf8")).toBe("12345\n");
if (process.platform !== "win32") {
expect((await fs.stat(filePath)).mode & 0o777).toBe(0o600);
}
});
it.runIf(process.platform !== "win32")("rejects symlink leaves synchronously", async () => {
const target = path.join(root, "target.txt");
const link = path.join(root, "link.txt");
await fs.writeFile(target, "secret", "utf8");
await fs.symlink(target, link);
expect(() => appendRegularFileSync({ filePath: link, content: "line\n" })).toThrow(/symlink/);
expect(await fs.readFile(target, "utf8")).toBe("secret");
});
it.runIf(process.platform !== "win32")("rejects symlink parents", async () => {
const targetDir = path.join(root, "target");
const linkDir = path.join(root, "link");
await fs.mkdir(targetDir);
await fs.symlink(targetDir, linkDir);
await expect(
appendRegularFile({
filePath: path.join(linkDir, "events.jsonl"),
content: "line\n",
rejectSymlinkParents: true,
}),
).rejects.toThrow(/symlinked directory/);
await expect(fs.stat(path.join(targetDir, "events.jsonl"))).rejects.toMatchObject({
code: "ENOENT",
});
});
it("pins regular file reads against symlink swaps", async () => {
const filePath = path.join(root, "read-target.txt");
const secretPath = path.join(root, "read-secret.txt");
await fs.writeFile(filePath, "safe", "utf8");
await fs.writeFile(secretPath, "secret", "utf8");
const originalLstat = fs.lstat.bind(fs);
let swapped = false;
const lstatSpy = vi.spyOn(fs, "lstat").mockImplementation(async (...args) => {
const stat = await originalLstat(...args);
if (!swapped && args[0] === filePath) {
swapped = true;
await fs.rm(filePath, { force: true });
await fs.symlink(secretPath, filePath);
}
return stat;
});
try {
await expect(readRegularFile({ filePath })).rejects.toThrow();
await expect(fs.readFile(secretPath, "utf8")).resolves.toBe("secret");
} finally {
lstatSpy.mockRestore();
}
});
it("pins sync regular file reads against symlink swaps", async () => {
const filePath = path.join(root, "sync-read-target.txt");
const secretPath = path.join(root, "sync-read-secret.txt");
await fs.writeFile(filePath, "safe", "utf8");
await fs.writeFile(secretPath, "secret", "utf8");
const originalLstatSync = syncFs.lstatSync.bind(syncFs);
let swapped = false;
const lstatSpy = vi.spyOn(syncFs, "lstatSync").mockImplementation((...args) => {
const stat = originalLstatSync(...args);
if (!swapped && args[0] === filePath) {
swapped = true;
syncFs.rmSync(filePath, { force: true });
syncFs.symlinkSync(secretPath, filePath);
}
return stat;
});
try {
expect(() => readRegularFileSync({ filePath })).toThrow();
await expect(fs.readFile(secretPath, "utf8")).resolves.toBe("secret");
} finally {
lstatSpy.mockRestore();
}
});
});
describe("atomic file replacement", () => {
it("retries transient rename failures and preserves destination spelling", async () => {
const filePath = path.join(root, "state.json");
const originalRename = fs.rename.bind(fs);
const destinations: string[] = [];
let busyCount = 0;
const renameSpy = vi.spyOn(fs, "rename").mockImplementation(async (src, dest) => {
destinations.push(String(dest));
if (busyCount < 2) {
busyCount++;
const error = new Error("busy") as NodeJS.ErrnoException;
error.code = "EBUSY";
throw error;
}
return await originalRename(src, dest);
});
try {
await replaceFileAtomic({
filePath,
content: "ok",
renameMaxRetries: 2,
renameRetryBaseDelayMs: 0,
});
} finally {
renameSpy.mockRestore();
}
expect(busyCount).toBe(2);
expect(destinations).toEqual([filePath, filePath, filePath]);
expect(await fs.readFile(filePath, "utf8")).toBe("ok");
});
it("can fall back to copy/unlink for permission-style rename failures", async () => {
const filePath = path.join(root, "windows.json");
const renameSpy = vi.spyOn(fs, "rename").mockImplementation(async () => {
const error = new Error("permission") as NodeJS.ErrnoException;
error.code = "EPERM";
throw error;
});
try {
await replaceFileAtomic({
filePath,
content: "copied",
copyFallbackOnPermissionError: true,
});
} finally {
renameSpy.mockRestore();
}
expect(await fs.readFile(filePath, "utf8")).toBe("copied");
});
it("cleans the temp file after failed replacement", async () => {
const filePath = path.join(root, "fail.json");
const renameSpy = vi.spyOn(fs, "rename").mockImplementation(async () => {
const error = new Error("denied") as NodeJS.ErrnoException;
error.code = "EACCES";
throw error;
});
try {
await expect(
replaceFileAtomic({
filePath,
content: "nope",
tempPrefix: ".cron-store",
}),
).rejects.toMatchObject({ code: "EACCES" });
} finally {
renameSpy.mockRestore();
}
const entries = await fs.readdir(root);
expect(entries.filter((entry) => entry.startsWith(".cron-store"))).toEqual([]);
});
it("applies requested directory and file modes", async () => {
const filePath = path.join(root, "nested", "mode.txt");
await replaceFileAtomic({
filePath,
content: "mode",
dirMode: 0o755,
mode: 0o644,
});
if (process.platform !== "win32") {
expect((await fs.stat(path.dirname(filePath))).mode & 0o777).toBe(0o755);
expect((await fs.stat(filePath)).mode & 0o777).toBe(0o644);
}
});
it("supports sync replacement", async () => {
const filePath = path.join(root, "sync", "state.txt");
replaceFileAtomicSync({
filePath,
content: "sync",
dirMode: 0o755,
mode: 0o644,
tempPrefix: ".sync-replace",
});
expect(await fs.readFile(filePath, "utf8")).toBe("sync");
if (process.platform !== "win32") {
expect((await fs.stat(path.dirname(filePath))).mode & 0o777).toBe(0o755);
expect((await fs.stat(filePath)).mode & 0o777).toBe(0o644);
}
});
it("preserves an existing destination mode when requested", async () => {
const filePath = path.join(root, "preserve-mode.txt");
await fs.writeFile(filePath, "old", { mode: 0o640 });
await replaceFileAtomic({
filePath,
content: "new",
preserveExistingMode: true,
});
expect(await fs.readFile(filePath, "utf8")).toBe("new");
if (process.platform !== "win32") {
expect((await fs.stat(filePath)).mode & 0o777).toBe(0o640);
}
});
it("syncs the temp file before rename when requested", async () => {
const filePath = path.join(root, "sync-temp.txt");
let syncCalls = 0;
const fileSystem = {
promises: {
...fs,
open: async (...args: Parameters<typeof fs.open>) => {
const handle = await fs.open(...args);
return {
sync: async () => {
syncCalls += 1;
},
close: async () => await handle.close(),
} as Awaited<ReturnType<typeof fs.open>>;
},
},
};
await replaceFileAtomic({
filePath,
content: "durable",
syncTempFile: true,
fileSystem,
});
expect(syncCalls).toBe(1);
expect(await fs.readFile(filePath, "utf8")).toBe("durable");
});
it("can use injected async filesystem operations", async () => {
const filePath = path.join(root, "injected.txt");
const renamed: string[] = [];
const fileSystem = {
promises: {
...fs,
rename: async (src: string, dest: string) => {
renamed.push(dest);
await fs.rename(src, dest);
},
},
};
await replaceFileAtomic({
filePath,
content: "injected",
fileSystem,
});
expect(renamed).toEqual([filePath]);
expect(await fs.readFile(filePath, "utf8")).toBe("injected");
});
it("syncs the parent directory when requested", async () => {
const filePath = path.join(root, "parent-sync.txt");
let openedDir = "";
const fileSystem = {
promises: {
...fs,
open: async (...args: Parameters<typeof fs.open>) => {
openedDir = String(args[0]);
const handle = await fs.open(...args);
return {
sync: async () => undefined,
close: async () => await handle.close(),
} as Awaited<ReturnType<typeof fs.open>>;
},
},
};
await replaceFileAtomic({
filePath,
content: "durable-parent",
syncParentDir: true,
fileSystem,
});
expect(openedDir).toBe(root);
});
it("cleans the sync temp file after failed replacement", async () => {
const filePath = path.join(root, "sync-fail.json");
const renameSpy = vi.spyOn(syncFs, "renameSync").mockImplementation(() => {
const error = new Error("denied") as NodeJS.ErrnoException;
error.code = "EACCES";
throw error;
});
try {
expect(() =>
replaceFileAtomicSync({
filePath,
content: "nope",
tempPrefix: ".sync-store",
}),
).toThrow();
} finally {
renameSpy.mockRestore();
}
const entries = await fs.readdir(root);
expect(entries.filter((entry) => entry.startsWith(".sync-store"))).toEqual([]);
});
});
describe("path moves", () => {
it("moves paths with rename", async () => {
const from = path.join(root, "from.txt");
const to = path.join(root, "to.txt");
await fs.writeFile(from, "moved");
await movePathWithCopyFallback({ from, to });
await expect(fs.access(from)).rejects.toMatchObject({ code: "ENOENT" });
expect(await fs.readFile(to, "utf8")).toBe("moved");
});
});
describe("sibling temp files", () => {
it("writes through a sibling temp file and cleans failures", async () => {
const finalPath = path.join(root, "download.bin");
const writtenTempPaths: string[] = [];
const result = await writeSiblingTempFile({
dir: root,
mode: 0o644,
writeTemp: async (tempPath) => {
writtenTempPaths.push(tempPath);
await fs.writeFile(tempPath, "streamed");
return { name: "download.bin" };
},
resolveFinalPath: (value) => path.join(root, value.name),
});
expect(result.filePath).toBe(finalPath);
expect(await fs.readFile(finalPath, "utf8")).toBe("streamed");
await expect(fs.access(writtenTempPaths[0])).rejects.toMatchObject({ code: "ENOENT" });
});
it("rejects final paths outside the temp directory", async () => {
await expect(
writeSiblingTempFile({
dir: root,
writeTemp: async (tempPath) => {
await fs.writeFile(tempPath, "escape");
return "escape";
},
resolveFinalPath: () => path.join(path.dirname(root), "escape.txt"),
}),
).rejects.toThrow(/sibling temp directory/);
expect(await fs.readdir(root)).toEqual([]);
});
});
describe("regular file helpers", () => {
it("rejects directories", async () => {
await expect(statRegularFile(root)).rejects.toThrow(/regular file/);
});
});
describe("path scope directory creation", () => {
it("creates directories inside the root", async () => {
const result = await pathScope(root, { label: "test root" }).ensureDir("a/b", { mode: 0o700 });
expect(result).toEqual({ ok: true, path: path.join(root, "a", "b") });
await expect(fs.stat(path.join(root, "a", "b"))).resolves.toMatchObject({});
});
it("rejects escapes", async () => {
const result = await pathScope(root, { label: "test root" }).ensureDir("../out");
expect(result.ok).toBe(false);
});
});
describe("symlink parent guards", () => {
it("rejects symlink path components", async () => {
const real = path.join(root, "real");
const link = path.join(root, "link");
await fs.mkdir(real);
await fs.symlink(real, link);
await expect(
assertNoSymlinkParents({
rootDir: root,
targetPath: path.join(link, "file.txt"),
requireDirectories: true,
}),
).rejects.toThrow(/symlinked/);
});
it("has a sync variant", async () => {
const real = path.join(root, "real-sync");
const link = path.join(root, "link-sync");
await fs.mkdir(real);
await fs.symlink(real, link);
expect(() =>
assertNoSymlinkParentsSync({
rootDir: root,
targetPath: path.join(link, "file.txt"),
requireDirectories: true,
}),
).toThrow(/symlinked/);
});
});