Skip to content

Commit 89c87de

Browse files
committed
fix safeRemoveDir boundary check
1 parent a346793 commit 89c87de

File tree

2 files changed

+57
-15
lines changed

2 files changed

+57
-15
lines changed

src/deno_ral/fs.ts

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,33 +36,44 @@ export function getFileInfoType(fileInfo: Deno.FileInfo): PathType | undefined {
3636
}
3737

3838
// from https://jsr.io/@std/fs/1.0.3/_is_subdir.ts
39-
// 2024-15-11: isSubDir("foo", "foo/bar") returns true, which gets src and dest exactly backwards?!
4039
/**
41-
* Checks whether `src` is a sub-directory of `dest`.
40+
* Checks whether `path2` is a sub-directory of `path1`.
4241
*
43-
* @param src Source file path as a string or URL.
44-
* @param dest Destination file path as a string or URL.
42+
* The original function uses bad parameter names which are misleading.
43+
*
44+
* This function is such that, for all paths p:
45+
*
46+
* isSubdir(p, join(p, "foo")) === true
47+
* isSubdir(p, p) === false
48+
* isSubdir(join(p, "foo"), p) === false
49+
*
50+
* @param path1 First path, as a string or URL.
51+
* @param path2 Second path, as a string or URL.
4552
* @param sep Path separator. Defaults to `\\` for Windows and `/` for other
4653
* platforms.
4754
*
48-
* @returns `true` if `src` is a sub-directory of `dest`, `false` otherwise.
55+
* @returns `true` if `path2` is a proper sub-directory of `path1`, `false` otherwise.
4956
*/
5057
export function isSubdir(
51-
src: string | URL,
52-
dest: string | URL,
58+
path1: string | URL,
59+
path2: string | URL,
5360
sep = SEPARATOR,
5461
): boolean {
55-
src = toPathString(src);
56-
dest = toPathString(dest);
62+
path1 = toPathString(path1);
63+
path2 = toPathString(path2);
5764

58-
if (resolve(src) === resolve(dest)) {
65+
if (resolve(path1) === resolve(path2)) {
5966
return false;
6067
}
6168

62-
const srcArray = src.split(sep);
63-
const destArray = dest.split(sep);
69+
const path1Array = path1.split(sep);
70+
const path2Array = path2.split(sep);
71+
72+
// if path1Array is longer than path2Array, then at least one of the
73+
// comparisons will return false, because it will compare a string to
74+
// undefined
6475

65-
return srcArray.every((current, i) => destArray[i] === current);
76+
return path1Array.every((current, i) => path2Array[i] === current);
6677
}
6778

6879
/**
@@ -118,8 +129,7 @@ export function safeRemoveDirSync(
118129
path: string,
119130
boundary: string,
120131
) {
121-
// note the comment above about isSubdir getting src and dest backwards
122-
if (path === boundary || isSubdir(path, boundary)) {
132+
if (path === boundary || !isSubdir(boundary, path)) {
123133
throw new UnsafeRemovalError(
124134
`Refusing to remove directory ${path} that isn't a subdirectory of ${boundary}`,
125135
);
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* safe-remove-dir.test.ts
3+
*
4+
* Copyright (C) 2025 Posit Software, PBC
5+
*
6+
*/
7+
8+
import { unitTest } from "../../test.ts";
9+
import { assert, assertThrows } from "testing/asserts";
10+
11+
import { createTempContext } from "../../../src/core/temp.ts";
12+
import { ensureDirSync, safeRemoveDirSync } from "../../../src/deno_ral/fs.ts";
13+
import { join } from "../../../src/deno_ral/path.ts";
14+
15+
unitTest("safeRemoveDirSync", async () => {
16+
17+
const temp = createTempContext();
18+
19+
const d1 = temp.createDir();
20+
const path = join(d1, "do-not-touch");
21+
const boundary = join(d1, "project-root");
22+
ensureDirSync(path);
23+
ensureDirSync(boundary);
24+
25+
assertThrows(() => {
26+
safeRemoveDirSync(path, boundary);
27+
});
28+
29+
assert(Deno.statSync(path).isDirectory);
30+
31+
temp.cleanup();
32+
});

0 commit comments

Comments
 (0)