Skip to content

Commit 1251871

Browse files
authored
Merge pull request #12782 from quarto-dev/bugfix/directory-relocator-bug
fix bad directory relocation fallback code
2 parents 9b71e9f + 92c266c commit 1251871

File tree

4 files changed

+69
-22
lines changed

4 files changed

+69
-22
lines changed

news/changelog-1.8.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,7 @@ All changes included in 1.8:
5757
### `inspect`
5858

5959
- ([#12733](https://github.com/quarto-dev/quarto-cli/issues/12733)): Add installed extensions to `quarto inspect` project report.
60+
61+
## Other fixes and improvements
62+
63+
- ([#12782](https://github.com/quarto-dev/quarto-cli/pull/12782)): fix bug on `safeRemoveDirSync`'s detection of safe directory boundaries.

src/command/render/project.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,7 @@ export async function renderProject(
513513
// because src and target are in different file systems.
514514
// In that case, try to recursively copy from src
515515
copyTo(srcDir, targetDir);
516-
safeRemoveDirSync(targetDir, context.dir);
516+
safeRemoveDirSync(srcDir, context.dir);
517517
}
518518
}
519519
};
@@ -632,13 +632,11 @@ export async function renderProject(
632632
const sortedOperations = uniqOps.sort((a, b) => {
633633
if (a.src === b.src) {
634634
return 0;
635-
} else {
636-
if (isSubdir(a.src, b.src)) {
637-
return -1;
638-
} else {
639-
return a.src.localeCompare(b.src);
640-
}
641635
}
636+
if (isSubdir(a.src, b.src)) {
637+
return -1;
638+
}
639+
return a.src.localeCompare(b.src);
642640
});
643641

644642
// Before file move

src/deno_ral/fs.ts

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,33 +36,47 @@ 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+
path1 = resolve(path1);
66+
path2 = resolve(path2);
67+
68+
if (path1 === path2) {
5969
return false;
6070
}
6171

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

65-
return srcArray.every((current, i) => destArray[i] === current);
79+
return path1Array.every((current, i) => path2Array[i] === current);
6680
}
6781

6882
/**
@@ -118,8 +132,7 @@ export function safeRemoveDirSync(
118132
path: string,
119133
boundary: string,
120134
) {
121-
// note the comment above about isSubdir getting src and dest backwards
122-
if (path === boundary || isSubdir(path, boundary)) {
135+
if (path === boundary || !isSubdir(boundary, path)) {
123136
throw new UnsafeRemovalError(
124137
`Refusing to remove directory ${path} that isn't a subdirectory of ${boundary}`,
125138
);
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)