Skip to content

use safeRemoveDirSync when possible to avoid unexpected catastrophic directory deletions #11455

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions news/changelog-1.6.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ All changes included in 1.6:

## Projects

- ([#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.
- ([#10125](https://github.com/quarto-dev/quarto-cli/issues/10125)): Show path to the project when project YAML validation fails.
- ([#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.
- ([#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.
Expand Down
6 changes: 4 additions & 2 deletions src/command/render/cleanup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Copyright (C) 2020-2022 Posit Software, PBC
*/

import { existsSync } from "../../deno_ral/fs.ts";
import { existsSync, safeRemoveDirSync } from "../../deno_ral/fs.ts";
import { dirname, extname, isAbsolute, join } from "../../deno_ral/path.ts";

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

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

export function renderCleanup(
input: string,
output: string,
format: Format,
project: ProjectContext,
supporting?: string[],
keepMd?: string,
) {
Expand Down Expand Up @@ -90,7 +92,7 @@ export function renderCleanup(
// clean supporting
ld.uniq(supporting).forEach((path) => {
if (existsSync(path)) {
safeRemoveSync(path, { recursive: true });
safeRemoveDirSync(path, project.dir);
}
});
}
Expand Down
8 changes: 4 additions & 4 deletions src/command/render/latexmk/latex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import { basename, join } from "../../../deno_ral/path.ts";
import { existsSync } from "../../../deno_ral/fs.ts";
import { existsSync, safeRemoveSync } from "../../../deno_ral/fs.ts";
import { error, info } from "../../../deno_ral/log.ts";

import { PdfEngine } from "../../../config/types.ts";
Expand Down Expand Up @@ -67,7 +67,7 @@ export async function runPdfEngine(
// Clean any log file or output from previous runs
[log, output].forEach((file) => {
if (existsSync(file)) {
Deno.removeSync(file);
safeRemoveSync(file);
}
});

Expand Down Expand Up @@ -141,7 +141,7 @@ export async function runIndexEngine(

// Clean any log file from previous runs
if (existsSync(log)) {
Deno.removeSync(log);
safeRemoveSync(log);
}

const result = await runLatexCommand(
Expand Down Expand Up @@ -176,7 +176,7 @@ export async function runBibEngine(

// Clean any log file from previous runs
if (existsSync(log)) {
Deno.removeSync(log);
safeRemoveSync(log);
}

const result = await runLatexCommand(
Expand Down
4 changes: 2 additions & 2 deletions src/command/render/latexmk/pdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import { dirname, join } from "../../../deno_ral/path.ts";
import { existsSync } from "../../../deno_ral/fs.ts";
import { existsSync, safeRemoveSync } from "../../../deno_ral/fs.ts";

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

auxFiles.forEach((auxFile) => {
if (existsSync(auxFile)) {
Deno.removeSync(auxFile);
safeRemoveSync(auxFile);
}
});
}
9 changes: 5 additions & 4 deletions src/command/render/output-tex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import { dirname, join, normalize, relative } from "../../deno_ral/path.ts";
import { ensureDirSync } from "../../deno_ral/fs.ts";
import { ensureDirSync, safeRemoveSync } from "../../deno_ral/fs.ts";

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

// copy (or write for stdout) compiled pdf to final output location
if (finalOutput) {
if (finalOutput === kStdOut) {
writeFileToStdout(pdfOutput);
Deno.removeSync(pdfOutput);
safeRemoveSync(pdfOutput);
} else {
const outputPdf = expandPath(finalOutput);

Expand All @@ -99,9 +99,10 @@ export function texToPdfOutputRecipe(

// Clean the output directory if it is empty
if (pdfOutputDir) {
console.log({ pdfOutputDir });
try {
// Remove the outputDir if it is empty
Deno.removeSync(pdfOutputDir, { recursive: false });
safeRemoveSync(pdfOutputDir, { recursive: false });
} catch {
// This is ok, just means the directory wasn't empty
}
Expand Down
6 changes: 3 additions & 3 deletions src/command/render/output-typst.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import { dirname, join, normalize, relative } from "../../deno_ral/path.ts";
import { ensureDirSync } from "../../deno_ral/fs.ts";
import { ensureDirSync, safeRemoveSync } from "../../deno_ral/fs.ts";

import {
kFontPaths,
Expand Down Expand Up @@ -81,14 +81,14 @@ export function typstPdfOutputRecipe(

// keep typ if requested
if (!format.render[kKeepTyp]) {
Deno.removeSync(input);
safeRemoveSync(input);
}

// copy (or write for stdout) compiled pdf to final output location
if (finalOutput) {
if (finalOutput === kStdOut) {
writeFileToStdout(pdfOutput);
Deno.removeSync(pdfOutput);
safeRemoveSync(pdfOutput);
} else {
const outputPdf = expandPath(finalOutput);

Expand Down
4 changes: 2 additions & 2 deletions src/command/render/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
} from "../../deno_ral/path.ts";

import { writeFileToStdout } from "../../core/console.ts";
import { dirAndStem, expandPath } from "../../core/path.ts";
import { dirAndStem, expandPath, safeRemoveSync } from "../../core/path.ts";
import {
parse as parseYaml,
partitionYamlFrontMatter,
Expand Down Expand Up @@ -209,7 +209,7 @@ export function outputRecipe(
recipe.isOutputTransient = true;
completeActions.push(() => {
writeFileToStdout(join(inputDir, recipe.output));
Deno.removeSync(join(inputDir, recipe.output));
safeRemoveSync(join(inputDir, recipe.output));
});
} else if (!isAbsolute(recipe.output)) {
// relatve output file on the command line: make it relative to the input dir
Expand Down
26 changes: 22 additions & 4 deletions src/command/render/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@
* Copyright (C) 2020-2022 Posit Software, PBC
*/

import { ensureDirSync, existsSync, safeMoveSync } from "../../deno_ral/fs.ts";
import {
ensureDirSync,
existsSync,
safeMoveSync,
safeRemoveDirSync,
safeRemoveSync,
UnsafeRemovalError,
} from "../../deno_ral/fs.ts";
import { dirname, isAbsolute, join, relative } from "../../deno_ral/path.ts";
import { info, warning } from "../../deno_ral/log.ts";
import { mergeProjectMetadata } from "../../config/metadata.ts";
Expand Down Expand Up @@ -481,7 +488,18 @@ export async function renderProject(
return;
}
if (existsSync(targetDir)) {
Deno.removeSync(targetDir, { recursive: true });
try {
safeRemoveDirSync(targetDir, context.dir);
} catch (e) {
if (e instanceof UnsafeRemovalError) {
warning(
`Refusing to remove directory ${targetDir} since it is not a subdirectory of the main project directory.`,
);
warning(
`Quarto did not expect the path configuration being used in this project, and strange behavior may result.`,
);
}
}
}
ensureDirSync(dirname(targetDir));
if (copy) {
Expand All @@ -494,7 +512,7 @@ export async function renderProject(
// because src and target are in different file systems.
// In that case, try to recursively copy from src
copyTo(srcDir, targetDir);
Deno.removeSync(srcDir, { recursive: true });
safeRemoveDirSync(targetDir, context.dir);
}
}
};
Expand Down Expand Up @@ -906,7 +924,7 @@ export async function renderProject(
if (projectRenderConfig.options.forceClean) {
const scratchDir = join(projDir, kQuartoScratch);
if (existsSync(scratchDir)) {
Deno.removeSync(scratchDir, { recursive: true });
safeRemoveSync(scratchDir, { recursive: true });
}
}

Expand Down
1 change: 1 addition & 0 deletions src/command/render/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,7 @@ export async function renderPandoc(
context.target.input,
finalOutput!,
format,
file.context.project,
cleanupSelfContained,
executionEngineKeepMd(context),
));
Expand Down
5 changes: 3 additions & 2 deletions src/core/copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
existsSync,
getFileInfoType,
isSubdir,
safeRemoveSync,
walkSync,
} from "../deno_ral/fs.ts";

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

Expand Down Expand Up @@ -196,7 +197,7 @@ function copySymlinkSync(
ensureValidCopySync(src, dest, options);
// remove dest if it exists
if (existsSync(dest)) {
Deno.removeSync(dest);
safeRemoveSync(dest);
}
const originSrcFilePath = Deno.readLinkSync(src);
const type = getFileInfoType(Deno.lstatSync(src));
Expand Down
13 changes: 13 additions & 0 deletions src/core/deno/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,19 @@ export const getStackAsArray = (
col: m4[5] + (m4[1] ? 6 : 0),
};
}

// at Array.map (<anonymous>)
const m5 = s.match(
/^.*at (.*)\(<anonymous>\)$/,
);
if (m5) {
return {
pos: "",
name: `${m5[1]}`,
line: "",
col: "",
};
}
throw new Error(`Unexpected stack entry: ${s}`);
});

Expand Down
3 changes: 2 additions & 1 deletion src/core/deno/monkey-patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@

import { debug } from "../../deno_ral/log.ts";
import { normalizePath } from "../path.ts";
import { getStack } from "./debug.ts";

// Window UNC paths can be mishandled by realPathSync
// Windows UNC paths can be mishandled by realPathSync
// (see https://github.com/quarto-dev/quarto-vscode/issues/67)
// so we monkey-patch to implement the absolute path and normalize
// parts of realPathSync (we aren't interested in the symlink
Expand Down
4 changes: 2 additions & 2 deletions src/core/run/lua.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import { info } from "../../deno_ral/log.ts";

import { dirname, extname } from "../../deno_ral/path.ts";
import { normalizePath } from "../path.ts";
import { normalizePath, safeRemoveSync } from "../path.ts";
import { isWindows } from "../platform.ts";
import { execProcess } from "../process.ts";
import { pandocBinaryPath, resourcePath } from "../resources.ts";
Expand Down Expand Up @@ -109,7 +109,7 @@ setmetatable(_G, meta)
} finally {
// remove temp script
if (tempScript) {
Deno.removeSync(tempScript);
safeRemoveSync(tempScript);
}
}
}
4 changes: 2 additions & 2 deletions src/core/windows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* Copyright (C) 2020-2022 Posit Software, PBC
*/
import { existsSync } from "../deno_ral/fs.ts";
import { existsSync, safeRemoveSync } from "../deno_ral/fs.ts";
import { join } from "../deno_ral/path.ts";
import { quartoCacheDir } from "./appdirs.ts";
import { removeIfExists } from "./path.ts";
Expand Down Expand Up @@ -79,7 +79,7 @@ export async function cacheCodePage() {

export function clearCodePageCache() {
if (existsSync(tokenPath)) {
Deno.removeSync(tokenPath);
safeRemoveSync(tokenPath);
}
}

Expand Down
20 changes: 20 additions & 0 deletions src/deno_ral/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export function getFileInfoType(fileInfo: Deno.FileInfo): PathType | undefined {
}

// from https://jsr.io/@std/fs/1.0.3/_is_subdir.ts
// 2024-15-11: isSubDir("foo", "foo/bar") returns true, which gets src and dest exactly backwards?!
/**
* Checks whether `src` is a sub-directory of `dest`.
*
Expand Down Expand Up @@ -104,3 +105,22 @@ export function safeRemoveSync(
}
}
}

export class UnsafeRemovalError extends Error {
constructor(msg: string) {
super(msg);
}
}

export function safeRemoveDirSync(
path: string,
boundary: string,
) {
// note the comment above about isSubdir getting src and dest backwards
if (path === boundary || isSubdir(path, boundary)) {
throw new UnsafeRemovalError(
`Refusing to remove directory ${path} that isn't a subdirectory of ${boundary}`,
);
}
return safeRemoveSync(path, { recursive: true });
}
4 changes: 2 additions & 2 deletions src/execute/julia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import { isInteractiveSession } from "../core/platform.ts";
import { runningInCI } from "../core/ci-info.ts";
import { sleep } from "../core/async.ts";
import { JupyterNotebook } from "../core/jupyter/types.ts";
import { existsSync } from "../deno_ral/fs.ts";
import { existsSync, safeRemoveSync } from "../deno_ral/fs.ts";
import { encodeBase64 } from "encoding/base64";
import {
executeResultEngineDependencies,
Expand Down Expand Up @@ -468,7 +468,7 @@ async function getJuliaServerConnection(
options,
"Connecting to server failed, a transport file was reused so it might be stale. Delete transport file and retry.",
);
Deno.removeSync(juliaTransportFile());
safeRemoveSync(juliaTransportFile());
return await getJuliaServerConnection(options);
} else {
error(
Expand Down
Loading
Loading