feat(M4): Reusable code abstractions and dead-code removal
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* safe-replace-dir.mjs — safely replace a directory within a safety-root boundary
|
||||
*
|
||||
* Exports:
|
||||
* safeReplaceDir(source, target, safetyRoot) → Promise<void>
|
||||
*
|
||||
* Usage:
|
||||
* import { safeReplaceDir } from "./lib/safe-replace-dir.mjs";
|
||||
* await safeReplaceDir("/path/to/source", "/safe/root/target", "/safe/root");
|
||||
*
|
||||
* Safety contract:
|
||||
* - `target` must be a strict descendant of `safetyRoot` (not equal to it).
|
||||
* - `target` must be a non-empty path.
|
||||
* - Throws with a descriptive message if either constraint is violated.
|
||||
*
|
||||
* Behaviour:
|
||||
* - Removes any existing content at `target` (rm -rf equivalent).
|
||||
* - Creates `target` (and any missing parent directories).
|
||||
* - Copies all files from `source` into `target`.
|
||||
*/
|
||||
|
||||
import { cp, mkdir, realpath, rm } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
/**
|
||||
* Safely replace `target` with the contents of `source`, enforcing that
|
||||
* `target` is a strict descendant of `safetyRoot`.
|
||||
*
|
||||
* @param {string} source - Directory to copy from.
|
||||
* @param {string} target - Directory to replace (will be removed then recreated).
|
||||
* @param {string} safetyRoot - Ancestor boundary; `target` must be inside this.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function safeReplaceDir(source, target, safetyRoot) {
|
||||
if (!target || target === "") {
|
||||
throw new Error(`Refusing to replace unsafe target: (empty string)`);
|
||||
}
|
||||
|
||||
const resolvedSafety = path.resolve(safetyRoot);
|
||||
const resolvedTarget = path.resolve(target);
|
||||
|
||||
// Lexical check: target must be a strict descendant of safetyRoot.
|
||||
const relative = path.relative(resolvedSafety, resolvedTarget);
|
||||
if (!relative || relative.startsWith("..") || path.isAbsolute(relative) || relative === "") {
|
||||
throw new Error(`Refusing to replace target outside safety root: ${target}`);
|
||||
}
|
||||
|
||||
// Real-path check: resolve the deepest existing ancestor of target's parent
|
||||
// and verify it lies inside the real (symlink-resolved) safety root.
|
||||
// This blocks a symlinked parent directory from redirecting outside the boundary.
|
||||
const realSafety = await realpath(resolvedSafety);
|
||||
let checkPath = path.dirname(resolvedTarget);
|
||||
for (;;) {
|
||||
try {
|
||||
const realAncestor = await realpath(checkPath);
|
||||
const realRel = path.relative(realSafety, realAncestor);
|
||||
if (realRel.startsWith("..") || path.isAbsolute(realRel)) {
|
||||
throw new Error(`Refusing to replace target outside safety root: ${target}`);
|
||||
}
|
||||
break; // validation passed
|
||||
} catch (err) {
|
||||
if (err.code === "ENOENT") {
|
||||
const parent = path.dirname(checkPath);
|
||||
if (parent === checkPath) {
|
||||
throw new Error(`Refusing to replace target outside safety root: ${target}`, { cause: err });
|
||||
}
|
||||
checkPath = parent;
|
||||
continue;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
await rm(resolvedTarget, { recursive: true, force: true });
|
||||
await mkdir(resolvedTarget, { recursive: true });
|
||||
await cp(source, resolvedTarget, { recursive: true, force: true });
|
||||
}
|
||||
Reference in New Issue
Block a user