From e6551617369d1f4fa1c45a11169b9fb17b212af1 Mon Sep 17 00:00:00 2001 From: Tanmay Arya Date: Thu, 19 Mar 2026 12:52:27 +0530 Subject: [PATCH 1/2] cargo-clean: add tests to show cargo deleting files as target-dir; add tests that assert cargo only deletes symlink when it is target-dir --- tests/testsuite/clean.rs | 142 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) diff --git a/tests/testsuite/clean.rs b/tests/testsuite/clean.rs index 08819d67270..a0f739df2ad 100644 --- a/tests/testsuite/clean.rs +++ b/tests/testsuite/clean.rs @@ -1058,3 +1058,145 @@ fn quiet_does_not_show_summary() { "#]]) .run(); } + +#[cargo_test] +fn target_dir_is_file() { + let p = project() + .file("Cargo.toml", &basic_bin_manifest("foo")) + .file("src/foo.rs", &main_file(r#""i am foo""#, &[])) + .file("target", "") + .build(); + + p.cargo("clean") + .with_stderr_data(str![[r#" +[REMOVED] 1 file + +"#]]) + .with_status(0) + .run(); + + assert!(!p.root().join("target").exists()); +} + +#[cargo_test] +fn explicit_target_dir_is_file() { + let p = project() + .file("Cargo.toml", &basic_bin_manifest("foo")) + .file("src/foo.rs", &main_file(r#""i am foo""#, &[])) + .file("bar", "") + .build(); + + p.cargo("clean --target-dir bar") + .with_stderr_data(str![[r#" +[REMOVED] 1 file + +"#]]) + .with_status(0) + .run(); + + assert!(!p.root().join("bar").exists()); +} + +#[cargo_test] +fn env_target_dir_is_file() { + let p = project() + .file("Cargo.toml", &basic_bin_manifest("foo")) + .file("src/foo.rs", &main_file(r#""i am foo""#, &[])) + .file("bar", "") + .build(); + + p.cargo("clean") + .env("CARGO_TARGET_DIR", "bar") + .with_stderr_data(str![[r#" +[REMOVED] 1 file + +"#]]) + .with_status(0) + .run(); + + assert!(!p.root().join("bar").exists()); +} + +#[cargo_test] +fn config_target_dir_is_file() { + let p = project() + .file("Cargo.toml", &basic_bin_manifest("foo")) + .file("src/foo.rs", &main_file(r#""i am foo""#, &[])) + .file("bar", "") + .file( + ".cargo/config.toml", + "[build] + target-dir = 'bar'", + ) + .build(); + + p.cargo("clean") + .with_stderr_data(str![[r#" +[REMOVED] 1 file + +"#]]) + .with_status(0) + .run(); + + assert!(!p.root().join("bar").exists()); +} + +#[cargo_test] +fn target_dir_is_symlink_dir() { + let p = project() + .file("Cargo.toml", &basic_bin_manifest("foo")) + .file("src/foo.rs", &main_file(r#""i am foo""#, &[])) + .symlink_dir("bar-dest", "target") + .file("bar-dest/.keep", "") + .build(); + + let mut check = p.cargo("clean"); + #[cfg(windows)] + { + check.with_stderr_data(str![[r#" +[REMOVED] 1 file + +"#]]); + } + #[cfg(not(windows))] + { + check.with_stderr_data(str![[r#" +[REMOVED] 1 file, [FILE_SIZE]B total + +"#]]); + } + check.with_status(0).run(); + + // make sure cargo has not deleted the files of the symlinked target dir + assert!(p.root().join("bar-dest/.keep").exists()); +} + +#[cargo_test] +fn target_dir_is_symlink_file() { + let p = project() + .file("Cargo.toml", &basic_bin_manifest("foo")) + .file("src/foo.rs", &main_file(r#""i am foo""#, &[])) + .symlink("bar-dest", "target") + .file("bar-dest", "") + .build(); + + let mut check = p.cargo("clean"); + #[cfg(windows)] + { + check.with_stderr_data(str![[r#" +[REMOVED] 1 file + +"#]]); + } + #[cfg(not(windows))] + { + check.with_stderr_data(str![[r#" +[REMOVED] 1 file, [FILE_SIZE]B total + +"#]]); + } + check.with_status(0).run(); + + // make sure cargo has not deleted the file of the symlinked target dir + assert!(p.root().join("bar-dest").exists()); +} From 893734865991a0a7c0e6c29c2950c260d79f6aa0 Mon Sep 17 00:00:00 2001 From: Tanmay Arya Date: Thu, 19 Mar 2026 13:57:46 +0530 Subject: [PATCH 2/2] cargo clean: validate that target-dir is not a file --- src/cargo/ops/cargo_clean.rs | 17 +++++++++++++++++ tests/testsuite/clean.rs | 32 ++++++++++++++++++++------------ 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/cargo/ops/cargo_clean.rs b/src/cargo/ops/cargo_clean.rs index 6d63db2dbbb..71526a36069 100644 --- a/src/cargo/ops/cargo_clean.rs +++ b/src/cargo/ops/cargo_clean.rs @@ -7,6 +7,7 @@ use crate::util::edit_distance; use crate::util::errors::CargoResult; use crate::util::interning::InternedString; use crate::util::{GlobalContext, Progress, ProgressStyle}; +use annotate_snippets::Level; use anyhow::bail; use cargo_util::paths; use indexmap::{IndexMap, IndexSet}; @@ -49,6 +50,22 @@ pub fn clean(ws: &Workspace<'_>, opts: &CleanOptions<'_>) -> CargoResult<()> { let mut clean_ctx = CleanContext::new(gctx); clean_ctx.dry_run = opts.dry_run; + const CLEAN_ABORT_NOTE: &str = + "cleaning has been aborted to prevent accidental deletion of unrelated files"; + + // make sure target_dir is a directory if it exists so that we don't delete files + if let Ok(meta) = fs::symlink_metadata(target_dir.as_path_unlocked()) { + // do not error if target_dir is symlink; let cargo delete it + if !meta.is_symlink() && !meta.is_dir() { + let title = format!("cannot clean `{}`: not a directory", target_dir.display()); + let report = [Level::ERROR + .primary_title(title) + .element(Level::NOTE.message(CLEAN_ABORT_NOTE))]; + gctx.shell().print_report(&report, false)?; + return Err(crate::AlreadyPrintedError::new(anyhow::anyhow!("")).into()); + } + } + if opts.doc { if !opts.spec.is_empty() { // FIXME: https://github.com/rust-lang/cargo/issues/8790 diff --git a/tests/testsuite/clean.rs b/tests/testsuite/clean.rs index a0f739df2ad..8bbe18c9dff 100644 --- a/tests/testsuite/clean.rs +++ b/tests/testsuite/clean.rs @@ -1069,13 +1069,15 @@ fn target_dir_is_file() { p.cargo("clean") .with_stderr_data(str![[r#" -[REMOVED] 1 file +[ERROR] cannot clean `[ROOT]/foo/target`: not a directory + | + = [NOTE] cleaning has been aborted to prevent accidental deletion of unrelated files "#]]) - .with_status(0) + .with_status(101) .run(); - assert!(!p.root().join("target").exists()); + assert!(p.root().join("target").exists()); } #[cargo_test] @@ -1088,13 +1090,15 @@ fn explicit_target_dir_is_file() { p.cargo("clean --target-dir bar") .with_stderr_data(str![[r#" -[REMOVED] 1 file +[ERROR] cannot clean `[ROOT]/foo/bar`: not a directory + | + = [NOTE] cleaning has been aborted to prevent accidental deletion of unrelated files "#]]) - .with_status(0) + .with_status(101) .run(); - assert!(!p.root().join("bar").exists()); + assert!(p.root().join("bar").exists()); } #[cargo_test] @@ -1108,13 +1112,15 @@ fn env_target_dir_is_file() { p.cargo("clean") .env("CARGO_TARGET_DIR", "bar") .with_stderr_data(str![[r#" -[REMOVED] 1 file +[ERROR] cannot clean `[ROOT]/foo/bar`: not a directory + | + = [NOTE] cleaning has been aborted to prevent accidental deletion of unrelated files "#]]) - .with_status(0) + .with_status(101) .run(); - assert!(!p.root().join("bar").exists()); + assert!(p.root().join("bar").exists()); } #[cargo_test] @@ -1132,13 +1138,15 @@ fn config_target_dir_is_file() { p.cargo("clean") .with_stderr_data(str![[r#" -[REMOVED] 1 file +[ERROR] cannot clean `[ROOT]/foo/bar`: not a directory + | + = [NOTE] cleaning has been aborted to prevent accidental deletion of unrelated files "#]]) - .with_status(0) + .with_status(101) .run(); - assert!(!p.root().join("bar").exists()); + assert!(p.root().join("bar").exists()); } #[cargo_test]