Files
scriptc/tests/corpus/node24-fs-links.ts
Chris Tate 46188eefe1 Add Node 24 filesystem link support (#664)
* Add Node 24 filesystem link support

* Use executable extensions in Windows link fixtures

* Remove Windows links without walking their targets

* Match Windows symlink inference and Buffer validation

* Match Node Windows symlink paths and fallback state

* Preserve Windows errors when following filesystem links
2026-10-04 09:46:00 -05:00

70 lines
4.9 KiB
TypeScript

// Relative targets stay relative to the link's parent; hard links share contents.
import * as fs from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
const root = fs.mkdtempSync(join(tmpdir(), "scriptc-links-"));
const outside = fs.mkdtempSync(join(tmpdir(), "scriptc-link-target-"));
fs.writeFileSync(join(outside, "keep"), "preserved");
const file = join(root, "target.txt");
const hard = join(root, "hard.txt");
const nested = join(root, "nested");
fs.writeFileSync(file, "first");
fs.mkdirSync(nested);
try {
fs.linkSync(pathToFileURL(file), Buffer.from(hard));
const storedLink = fs.linkSync;
storedLink(Buffer.from(file), join(root, "stored-hard"));
fs.writeFileSync(hard, "shared");
console.log("hard", fs.readFileSync(file, "utf8"), fs.statSync(file).ino === fs.statSync(hard).ino);
try { fs.linkSync(file, hard); } catch (e) { const err = e as NodeJS.ErrnoException; console.log("existing", err.name, err.code, err.message.includes("link")); }
try { fs.linkSync(file + ".missing", hard + ".missing"); } catch (e) { const err = e as NodeJS.ErrnoException; console.log("missing", err.name, err.code, err.message.includes(" -> ")); }
const relative = join(nested, "relative");
let canSymlink = true;
try { fs.symlinkSync("../target.txt", relative); }
catch (e) { const err = e as NodeJS.ErrnoException; if (process.platform === "win32" && err.code === "EPERM") { canSymlink = false; console.log("symlink privilege boundary"); } else throw e; }
if (canSymlink) {
const text = fs.readlinkSync(pathToFileURL(relative));
const bytes = fs.readlinkSync(Buffer.from(relative), { encoding: "buffer" });
console.log("relative", text === (process.platform === "win32" ? "..\\target.txt" : "../target.txt"), fs.readFileSync(relative, "utf8"), bytes.toString("utf8") === text);
console.log("encodings", fs.readlinkSync(relative, "hex") === bytes.toString("hex"), fs.readlinkSync(relative, { encoding: "base64" }) === bytes.toString("base64"), fs.readlinkSync(relative, "latin1") === bytes.toString("latin1"));
fs.symlinkSync(pathToFileURL(file), join(root, "absolute"), "file");
console.log("absolute", fs.readlinkSync(join(root, "absolute")) === file);
fs.symlinkSync("missing-target", join(root, "dangling"), null);
console.log("dangling", fs.readlinkSync(join(root, "dangling")), fs.lstatSync(join(root, "dangling")).isSymbolicLink());
try { fs.statSync(join(root, "dangling")); } catch (e) { const err = e as NodeJS.ErrnoException; console.log("dangling stat", err.name, err.code); }
fs.symlinkSync("nested", join(root, "directory"), "dir");
console.log("directory", fs.statSync(join(root, "directory")).isDirectory());
fs.symlinkSync("nested", join(root, "file-directory"), "file");
console.log("file-directory link", fs.lstatSync(join(root, "file-directory")).isSymbolicLink(), fs.readlinkSync(join(root, "file-directory")) === "nested");
try { console.log("file-directory stat", fs.statSync(join(root, "file-directory")).isDirectory()); }
catch (e) { const err = e as NodeJS.ErrnoException; console.log("file-directory stat", err.name, err.code); }
fs.symlinkSync("target.txt", join(root, "directory-file"), "dir");
console.log("directory-file link", fs.lstatSync(join(root, "directory-file")).isSymbolicLink());
try { console.log("directory-file stat", fs.statSync(join(root, "directory-file")).isFile()); }
catch (e) { const err = e as NodeJS.ErrnoException; console.log("directory-file stat", err.name, err.code); }
if (process.platform !== "win32") {
fs.symlinkSync("x".repeat(600), join(root, "long"), "file");
console.log("long", fs.readlinkSync(join(root, "long")).length);
fs.symlinkSync(Buffer.from([0x61, 0xff, 0x62]), Buffer.from(join(root, "raw")));
console.log("raw", fs.readlinkSync(join(root, "raw"), "buffer").toString("hex"), fs.readlinkSync(join(root, "raw")));
}
}
// Junctions do not require Windows symlink privileges. POSIX ignores type.
fs.symlinkSync("nested", join(root, "junction"), "junction");
console.log("junction", fs.statSync(join(root, "junction")).isDirectory(), fs.readlinkSync(join(root, "junction")) === (process.platform === "win32" ? nested : "nested"));
fs.unlinkSync(join(root, "junction"));
console.log("unlink directory link", fs.statSync(nested).isDirectory());
fs.symlinkSync("nested", join(root, "remove"), "junction");
fs.rmSync(join(root, "remove"));
console.log("remove directory link", fs.statSync(nested).isDirectory());
fs.symlinkSync(outside, join(root, "outside"), "junction");
fs.unlinkSync(file);
console.log("survives", fs.readFileSync(hard, "utf8"));
} finally {
try {
fs.rmSync(root, { recursive: true, force: true });
console.log("no-follow cleanup", fs.readFileSync(join(outside, "keep"), "utf8"));
} finally { fs.rmSync(outside, { recursive: true, force: true }); }
}