mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-21 05:50:55 +08:00
* 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
70 lines
4.9 KiB
TypeScript
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 }); }
|
|
}
|