Skip to content

Commit 9491069

Browse files
committed
Bound clean script options
1 parent 9533574 commit 9491069

2 files changed

Lines changed: 60 additions & 18 deletions

File tree

scripts/clean.mjs

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,23 @@ export const DEFAULT_SKIP_NAMES = new Set([
2222
"tmp",
2323
]);
2424

25-
function assertPositiveInteger(value, label) {
26-
if (!Number.isSafeInteger(value) || value <= 0) {
27-
throw new Error(`${label} must be a positive safe integer`);
25+
function isRecord(value) {
26+
return value !== null && typeof value === "object" && !Array.isArray(value);
27+
}
28+
29+
function readCleanOption(options, key, defaultValue) {
30+
return Object.hasOwn(options, key) ? options[key] : defaultValue;
31+
}
32+
33+
function assertBoolean(value, label) {
34+
if (typeof value !== "boolean") {
35+
throw new Error(`${label} must be a boolean`);
36+
}
37+
}
38+
39+
function assertPositiveIntegerAtMost(value, label, maximum) {
40+
if (!Number.isSafeInteger(value) || value <= 0 || value > maximum) {
41+
throw new Error(`${label} must be a positive safe integer no greater than ${maximum}`);
2842
}
2943
}
3044

@@ -88,28 +102,41 @@ function assertCleanSuffixes(value, label) {
88102
}
89103

90104
function resolveCleanOptions(options) {
105+
if (!isRecord(options)) {
106+
throw new Error("clean options must be an object");
107+
}
108+
91109
return {
92-
removableNames: options.removableNames ?? DEFAULT_REMOVABLE_NAMES,
93-
removableSuffixes: options.removableSuffixes ?? DEFAULT_REMOVABLE_SUFFIXES,
94-
skipNames: options.skipNames ?? DEFAULT_SKIP_NAMES,
95-
verifyRoot: options.verifyRoot ?? true,
96-
maxDirectories: options.maxDirectories ?? MAX_CLEAN_DIRECTORIES,
97-
maxFiles: options.maxFiles ?? MAX_CLEAN_FILES,
98-
maxDirectoryEntries: options.maxDirectoryEntries ?? MAX_CLEAN_DIRECTORY_ENTRIES,
99-
maxRemovals: options.maxRemovals ?? MAX_CLEAN_REMOVALS,
100-
maxPathBytes: options.maxPathBytes ?? MAX_CLEAN_PATH_BYTES,
110+
removableNames: readCleanOption(options, "removableNames", DEFAULT_REMOVABLE_NAMES),
111+
removableSuffixes: readCleanOption(options, "removableSuffixes", DEFAULT_REMOVABLE_SUFFIXES),
112+
skipNames: readCleanOption(options, "skipNames", DEFAULT_SKIP_NAMES),
113+
verifyRoot: readCleanOption(options, "verifyRoot", true),
114+
maxDirectories: readCleanOption(options, "maxDirectories", MAX_CLEAN_DIRECTORIES),
115+
maxFiles: readCleanOption(options, "maxFiles", MAX_CLEAN_FILES),
116+
maxDirectoryEntries: readCleanOption(
117+
options,
118+
"maxDirectoryEntries",
119+
MAX_CLEAN_DIRECTORY_ENTRIES,
120+
),
121+
maxRemovals: readCleanOption(options, "maxRemovals", MAX_CLEAN_REMOVALS),
122+
maxPathBytes: readCleanOption(options, "maxPathBytes", MAX_CLEAN_PATH_BYTES),
101123
};
102124
}
103125

104126
function assertCleanOptions(options) {
105127
assertCleanNameSet(options.removableNames, "removableNames");
106128
assertCleanSuffixes(options.removableSuffixes, "removableSuffixes");
107129
assertCleanNameSet(options.skipNames, "skipNames");
108-
assertPositiveInteger(options.maxDirectories, "maxDirectories");
109-
assertPositiveInteger(options.maxFiles, "maxFiles");
110-
assertPositiveInteger(options.maxDirectoryEntries, "maxDirectoryEntries");
111-
assertPositiveInteger(options.maxRemovals, "maxRemovals");
112-
assertPositiveInteger(options.maxPathBytes, "maxPathBytes");
130+
assertBoolean(options.verifyRoot, "verifyRoot");
131+
assertPositiveIntegerAtMost(options.maxDirectories, "maxDirectories", MAX_CLEAN_DIRECTORIES);
132+
assertPositiveIntegerAtMost(options.maxFiles, "maxFiles", MAX_CLEAN_FILES);
133+
assertPositiveIntegerAtMost(
134+
options.maxDirectoryEntries,
135+
"maxDirectoryEntries",
136+
MAX_CLEAN_DIRECTORY_ENTRIES,
137+
);
138+
assertPositiveIntegerAtMost(options.maxRemovals, "maxRemovals", MAX_CLEAN_REMOVALS);
139+
assertPositiveIntegerAtMost(options.maxPathBytes, "maxPathBytes", MAX_CLEAN_PATH_BYTES);
113140
}
114141

115142
function assertPathWithinLimit(root, absolutePath, maxPathBytes) {

scripts/clean.test.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { access, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
44
import { tmpdir } from "node:os";
55
import path from "node:path";
66
import test from "node:test";
7-
import { cleanWorkspace } from "./clean.mjs";
7+
import { MAX_CLEAN_REMOVALS, cleanWorkspace } from "./clean.mjs";
88

99
async function pathExists(filePath: string): Promise<boolean> {
1010
try {
@@ -136,6 +136,21 @@ test("cleanWorkspace rejects malformed clean rules before walking", async () =>
136136
);
137137
});
138138

139+
test("cleanWorkspace rejects malformed clean options before walking", async () => {
140+
await assert.rejects(
141+
() => cleanWorkspace(process.cwd(), null),
142+
/clean options must be an object/,
143+
);
144+
await assert.rejects(
145+
() => cleanWorkspace(process.cwd(), { verifyRoot: "false" }),
146+
/verifyRoot must be a boolean/,
147+
);
148+
await assert.rejects(
149+
() => cleanWorkspace(process.cwd(), { maxRemovals: MAX_CLEAN_REMOVALS + 1 }),
150+
/maxRemovals must be a positive safe integer no greater than 2048/,
151+
);
152+
});
153+
139154
test("cleanWorkspace streams directory entries without readdir", async (t) => {
140155
const tempDir = await mkdtemp(path.join(tmpdir(), "ray-clean-stream-"));
141156
const originalReaddir = fs.readdir;

0 commit comments

Comments
 (0)