Skip to content

Commit 2507116

Browse files
authored
Merge pull request #11455 from quarto-dev/bugfix/7998
use safeRemoveDirSync when possible to avoid unexpected catastrophic directory deletions
2 parents 80beea1 + e042c96 commit 2507116

File tree

30 files changed

+131
-60
lines changed

30 files changed

+131
-60
lines changed

news/changelog-1.6.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ All changes included in 1.6:
9191

9292
## Projects
9393

94+
- ([#7988](https://github.com/quarto-dev/quarto-cli/issues/7988)): Do not allow `lib-dir` to cause an accidental cleanup of the project directory when its value points to a parent of the project directory.
9495
- ([#10125](https://github.com/quarto-dev/quarto-cli/issues/10125)): Show path to the project when project YAML validation fails.
9596
- ([#10268](https://github.com/quarto-dev/quarto-cli/issues/10268)): `quarto create` supports opening project in Positron, in addition to VS Code and RStudio IDE.
9697
- ([#10285](https://github.com/quarto-dev/quarto-cli/issues/10285)): Include text from before the first chapter sections in search indices. In addition, include text of every element with `.quarto-include-in-search-index` class in search indices.

src/command/render/cleanup.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Copyright (C) 2020-2022 Posit Software, PBC
55
*/
66

7-
import { existsSync } from "../../deno_ral/fs.ts";
7+
import { existsSync, safeRemoveDirSync } from "../../deno_ral/fs.ts";
88
import { dirname, extname, isAbsolute, join } from "../../deno_ral/path.ts";
99

1010
import * as ld from "../../core/lodash.ts";
@@ -22,11 +22,13 @@ import { isHtmlFileOutput, isLatexOutput } from "../../config/format.ts";
2222
import { kKeepMd, kKeepTex, kKeepTyp } from "../../config/constants.ts";
2323

2424
import { filesDirLibDir, filesDirMediabagDir } from "./render-paths.ts";
25+
import { ProjectContext } from "../../project/types.ts";
2526

2627
export function renderCleanup(
2728
input: string,
2829
output: string,
2930
format: Format,
31+
project: ProjectContext,
3032
supporting?: string[],
3133
keepMd?: string,
3234
) {
@@ -90,7 +92,7 @@ export function renderCleanup(
9092
// clean supporting
9193
ld.uniq(supporting).forEach((path) => {
9294
if (existsSync(path)) {
93-
safeRemoveSync(path, { recursive: true });
95+
safeRemoveDirSync(path, project.dir);
9496
}
9597
});
9698
}

src/command/render/latexmk/latex.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66

77
import { basename, join } from "../../../deno_ral/path.ts";
8-
import { existsSync } from "../../../deno_ral/fs.ts";
8+
import { existsSync, safeRemoveSync } from "../../../deno_ral/fs.ts";
99
import { error, info } from "../../../deno_ral/log.ts";
1010

1111
import { PdfEngine } from "../../../config/types.ts";
@@ -67,7 +67,7 @@ export async function runPdfEngine(
6767
// Clean any log file or output from previous runs
6868
[log, output].forEach((file) => {
6969
if (existsSync(file)) {
70-
Deno.removeSync(file);
70+
safeRemoveSync(file);
7171
}
7272
});
7373

@@ -141,7 +141,7 @@ export async function runIndexEngine(
141141

142142
// Clean any log file from previous runs
143143
if (existsSync(log)) {
144-
Deno.removeSync(log);
144+
safeRemoveSync(log);
145145
}
146146

147147
const result = await runLatexCommand(
@@ -176,7 +176,7 @@ export async function runBibEngine(
176176

177177
// Clean any log file from previous runs
178178
if (existsSync(log)) {
179-
Deno.removeSync(log);
179+
safeRemoveSync(log);
180180
}
181181

182182
const result = await runLatexCommand(

src/command/render/latexmk/pdf.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66

77
import { dirname, join } from "../../../deno_ral/path.ts";
8-
import { existsSync } from "../../../deno_ral/fs.ts";
8+
import { existsSync, safeRemoveSync } from "../../../deno_ral/fs.ts";
99

1010
import { PdfEngine } from "../../../config/types.ts";
1111
import { LatexmkOptions } from "./types.ts";
@@ -594,7 +594,7 @@ function cleanup(workingDir: string, stem: string) {
594594

595595
auxFiles.forEach((auxFile) => {
596596
if (existsSync(auxFile)) {
597-
Deno.removeSync(auxFile);
597+
safeRemoveSync(auxFile);
598598
}
599599
});
600600
}

src/command/render/output-tex.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66

77
import { dirname, join, normalize, relative } from "../../deno_ral/path.ts";
8-
import { ensureDirSync } from "../../deno_ral/fs.ts";
8+
import { ensureDirSync, safeRemoveSync } from "../../deno_ral/fs.ts";
99

1010
import { writeFileToStdout } from "../../core/console.ts";
1111
import { dirAndStem, expandPath } from "../../core/path.ts";
@@ -78,14 +78,14 @@ export function texToPdfOutputRecipe(
7878
// keep tex if requested
7979
const compileTex = join(inputDir, output);
8080
if (!format.render[kKeepTex]) {
81-
Deno.removeSync(compileTex);
81+
safeRemoveSync(compileTex);
8282
}
8383

8484
// copy (or write for stdout) compiled pdf to final output location
8585
if (finalOutput) {
8686
if (finalOutput === kStdOut) {
8787
writeFileToStdout(pdfOutput);
88-
Deno.removeSync(pdfOutput);
88+
safeRemoveSync(pdfOutput);
8989
} else {
9090
const outputPdf = expandPath(finalOutput);
9191

@@ -99,9 +99,10 @@ export function texToPdfOutputRecipe(
9999

100100
// Clean the output directory if it is empty
101101
if (pdfOutputDir) {
102+
console.log({ pdfOutputDir });
102103
try {
103104
// Remove the outputDir if it is empty
104-
Deno.removeSync(pdfOutputDir, { recursive: false });
105+
safeRemoveSync(pdfOutputDir, { recursive: false });
105106
} catch {
106107
// This is ok, just means the directory wasn't empty
107108
}

src/command/render/output-typst.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66

77
import { dirname, join, normalize, relative } from "../../deno_ral/path.ts";
8-
import { ensureDirSync } from "../../deno_ral/fs.ts";
8+
import { ensureDirSync, safeRemoveSync } from "../../deno_ral/fs.ts";
99

1010
import {
1111
kFontPaths,
@@ -81,14 +81,14 @@ export function typstPdfOutputRecipe(
8181

8282
// keep typ if requested
8383
if (!format.render[kKeepTyp]) {
84-
Deno.removeSync(input);
84+
safeRemoveSync(input);
8585
}
8686

8787
// copy (or write for stdout) compiled pdf to final output location
8888
if (finalOutput) {
8989
if (finalOutput === kStdOut) {
9090
writeFileToStdout(pdfOutput);
91-
Deno.removeSync(pdfOutput);
91+
safeRemoveSync(pdfOutput);
9292
} else {
9393
const outputPdf = expandPath(finalOutput);
9494

src/command/render/output.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
} from "../../deno_ral/path.ts";
1616

1717
import { writeFileToStdout } from "../../core/console.ts";
18-
import { dirAndStem, expandPath } from "../../core/path.ts";
18+
import { dirAndStem, expandPath, safeRemoveSync } from "../../core/path.ts";
1919
import {
2020
parse as parseYaml,
2121
partitionYamlFrontMatter,
@@ -209,7 +209,7 @@ export function outputRecipe(
209209
recipe.isOutputTransient = true;
210210
completeActions.push(() => {
211211
writeFileToStdout(join(inputDir, recipe.output));
212-
Deno.removeSync(join(inputDir, recipe.output));
212+
safeRemoveSync(join(inputDir, recipe.output));
213213
});
214214
} else if (!isAbsolute(recipe.output)) {
215215
// relatve output file on the command line: make it relative to the input dir

src/command/render/project.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@
44
* Copyright (C) 2020-2022 Posit Software, PBC
55
*/
66

7-
import { ensureDirSync, existsSync, safeMoveSync } from "../../deno_ral/fs.ts";
7+
import {
8+
ensureDirSync,
9+
existsSync,
10+
safeMoveSync,
11+
safeRemoveDirSync,
12+
safeRemoveSync,
13+
UnsafeRemovalError,
14+
} from "../../deno_ral/fs.ts";
815
import { dirname, isAbsolute, join, relative } from "../../deno_ral/path.ts";
916
import { info, warning } from "../../deno_ral/log.ts";
1017
import { mergeProjectMetadata } from "../../config/metadata.ts";
@@ -481,7 +488,18 @@ export async function renderProject(
481488
return;
482489
}
483490
if (existsSync(targetDir)) {
484-
Deno.removeSync(targetDir, { recursive: true });
491+
try {
492+
safeRemoveDirSync(targetDir, context.dir);
493+
} catch (e) {
494+
if (e instanceof UnsafeRemovalError) {
495+
warning(
496+
`Refusing to remove directory ${targetDir} since it is not a subdirectory of the main project directory.`,
497+
);
498+
warning(
499+
`Quarto did not expect the path configuration being used in this project, and strange behavior may result.`,
500+
);
501+
}
502+
}
485503
}
486504
ensureDirSync(dirname(targetDir));
487505
if (copy) {
@@ -494,7 +512,7 @@ export async function renderProject(
494512
// because src and target are in different file systems.
495513
// In that case, try to recursively copy from src
496514
copyTo(srcDir, targetDir);
497-
Deno.removeSync(srcDir, { recursive: true });
515+
safeRemoveDirSync(targetDir, context.dir);
498516
}
499517
}
500518
};
@@ -906,7 +924,7 @@ export async function renderProject(
906924
if (projectRenderConfig.options.forceClean) {
907925
const scratchDir = join(projDir, kQuartoScratch);
908926
if (existsSync(scratchDir)) {
909-
Deno.removeSync(scratchDir, { recursive: true });
927+
safeRemoveSync(scratchDir, { recursive: true });
910928
}
911929
}
912930

src/command/render/render.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,7 @@ export async function renderPandoc(
377377
context.target.input,
378378
finalOutput!,
379379
format,
380+
file.context.project,
380381
cleanupSelfContained,
381382
executionEngineKeepMd(context),
382383
));

src/core/copy.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
existsSync,
1919
getFileInfoType,
2020
isSubdir,
21+
safeRemoveSync,
2122
walkSync,
2223
} from "../deno_ral/fs.ts";
2324

@@ -146,7 +147,7 @@ function copyFileSync(
146147
// multiple users/owners in play). see this code for where this occurs:
147148
// https://github.com/denoland/deno/blob/1c05e41f37da022971f0090b2a92e6340d230055/runtime/ops/fs.rs#L914-L916
148149
if (existsSync(dest)) {
149-
Deno.removeSync(dest);
150+
safeRemoveSync(dest);
150151
}
151152
Deno.copyFileSync(src, dest);
152153

@@ -196,7 +197,7 @@ function copySymlinkSync(
196197
ensureValidCopySync(src, dest, options);
197198
// remove dest if it exists
198199
if (existsSync(dest)) {
199-
Deno.removeSync(dest);
200+
safeRemoveSync(dest);
200201
}
201202
const originSrcFilePath = Deno.readLinkSync(src);
202203
const type = getFileInfoType(Deno.lstatSync(src));

0 commit comments

Comments
 (0)