Skip to content
Open
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 src/bin/cargo/commands/clean.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult {
profile_specified: args.contains_id("profile") || args.flag("release"),
doc: args.flag("doc"),
dry_run: args.dry_run(),
explicit_target_dir_arg: args.contains_id("target-dir"),
};
ops::clean(&ws, &opts)?;
Ok(())
Expand Down
52 changes: 51 additions & 1 deletion src/cargo/ops/cargo_clean.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ use cargo_util::paths;
use indexmap::{IndexMap, IndexSet};

use std::ffi::OsString;
use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::{fs, io};

pub struct CleanOptions<'gctx> {
pub gctx: &'gctx GlobalContext,
Expand All @@ -31,6 +32,8 @@ pub struct CleanOptions<'gctx> {
pub doc: bool,
/// If set, doesn't delete anything.
pub dry_run: bool,
/// true if target-dir was was explicitly specified via --target-dir
pub explicit_target_dir_arg: bool,
}

pub struct CleanContext<'gctx> {
Expand Down Expand Up @@ -66,6 +69,22 @@ pub fn clean(ws: &Workspace<'_>, opts: &CleanOptions<'_>) -> CargoResult<()> {
}
}

// do some validation on target_dir if it was specified via --target-dir
if opts.explicit_target_dir_arg {
let target_dir_path = target_dir.as_path_unlocked();

// check if the target directory has a valid CACHEDIR.TAG
if let Err(err) = validate_target_dir_tag(target_dir_path) {
// if target_dir was passed explicitly via --target-dir, then hard error if validation fails
let title = format!("cannot clean `{}`: {err}", target_dir_path.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
Expand Down Expand Up @@ -122,6 +141,37 @@ pub fn clean(ws: &Workspace<'_>, opts: &CleanOptions<'_>) -> CargoResult<()> {
Ok(())
}

fn validate_target_dir_tag(target_dir_path: &Path) -> CargoResult<()> {
const TAG_SIGNATURE: &[u8] = b"Signature: 8a477f597d28d172789f06886806bc55";

let tag_path = target_dir_path.join("CACHEDIR.TAG");

// per https://bford.info/cachedir the tag file must not be a symlink
if tag_path.is_symlink() {
bail!("expect `CACHEDIR.TAG` to be a regular file, got a symlink");
}

if !tag_path.is_file() {
bail!("missing or invalid `CACHEDIR.TAG` file");
}

let mut file = fs::File::open(&tag_path)
.map_err(|err| anyhow::anyhow!("failed to open `{}`: {}", tag_path.display(), err))?;

let mut buf = [0u8; TAG_SIGNATURE.len()];
match file.read_exact(&mut buf) {
Ok(()) if &buf[..] == TAG_SIGNATURE => {}
Err(e) if e.kind() != io::ErrorKind::UnexpectedEof => {
bail!("failed to read `{}`: {e}", tag_path.display());
}
_ => {
bail!("invalid signature in `CACHEDIR.TAG` file");
}
}

Ok(())
}

fn clean_specs(
clean_ctx: &mut CleanContext<'_>,
ws: &Workspace<'_>,
Expand Down
239 changes: 239 additions & 0 deletions tests/testsuite/clean.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1208,3 +1208,242 @@ fn target_dir_is_symlink_file() {
// make sure cargo has not deleted the file of the symlinked target dir
assert!(p.root().join("bar-dest").exists());
}

#[cargo_test]
fn explicit_target_dir_tag_not_present() {
// invalid target dir explicitly specified via --target-dir cli arg

let p = project()
.file("Cargo.toml", &basic_bin_manifest("foo"))
.file("src/foo.rs", &main_file(r#""i am foo""#, &[]))
.file("bar/.keep", "")
.build();

p.cargo("clean --target-dir bar")
.with_stdout_data("")
.with_stderr_data(str![[r#"
[ERROR] cannot clean `[ROOT]/foo/bar`: missing or invalid `CACHEDIR.TAG` file
|
= [NOTE] cleaning has been aborted to prevent accidental deletion of unrelated files

"#]])
.with_status(101)
.run();
}

#[cargo_test]
fn explicit_target_dir_tag_invalid_signature() {
let p = project()
.file("Cargo.toml", &basic_bin_manifest("foo"))
.file("src/foo.rs", &main_file(r#""i am foo""#, &[]))
.file("bar/CACHEDIR.TAG", "Signature: 1234")
.build();

p.cargo("clean --target-dir bar")
.with_stdout_data("")
.with_stderr_data(str![[r#"
[ERROR] cannot clean `[ROOT]/foo/bar`: invalid signature in `CACHEDIR.TAG` file
|
= [NOTE] cleaning has been aborted to prevent accidental deletion of unrelated files

"#]])
.with_status(101)
.run();
}

#[cargo_test]
fn explicit_target_dir_tag_symlink() {
let p = project()
.file("Cargo.toml", &basic_bin_manifest("foo"))
.file("src/foo.rs", &main_file(r#""i am foo""#, &[]))
.file(
"src/CACHEDIR.TAG",
"Signature: 8a477f597d28d172789f06886806bc55",
)
.symlink("src/CACHEDIR.TAG", "bar/CACHEDIR.TAG")
.build();

p.cargo("clean --target-dir bar")
.with_stdout_data("")
.with_stderr_data(str![[r#"
[ERROR] cannot clean `[ROOT]/foo/bar`: expect `CACHEDIR.TAG` to be a regular file, got a symlink
|
= [NOTE] cleaning has been aborted to prevent accidental deletion of unrelated files

"#]])
.with_status(101)
.run();
}

#[cargo_test]
fn explicit_target_dir_tag_valid() {
let p = project()
.file("Cargo.toml", &basic_bin_manifest("foo"))
.file("src/foo.rs", &main_file(r#""i am foo""#, &[]))
.file(
"bar/CACHEDIR.TAG",
"Signature: 8a477f597d28d172789f06886806bc55",
)
.build();

p.cargo("clean --target-dir bar").run();
}

#[cargo_test]
fn env_target_dir_tag_not_present() {
// invalid target dir specified via env var

let p = project()
.file("Cargo.toml", &basic_bin_manifest("foo"))
.file("bar/.keep", "")
.file("src/foo.rs", &main_file(r#""i am foo""#, &[]))
.build();

p.cargo("clean")
.env("CARGO_TARGET_DIR", "bar")
.with_stderr_data(str![[r#"
[REMOVED] [FILE_NUM] files, [FILE_SIZE]B total

"#]])
.run();
}

#[cargo_test]
fn env_target_dir_tag_invalid_signature() {
let p = project()
.file("Cargo.toml", &basic_bin_manifest("foo"))
.file("src/foo.rs", &main_file(r#""i am foo""#, &[]))
.file("bar/CACHEDIR.TAG", "Signature: 1234")
.build();

p.cargo("clean")
.env("CARGO_TARGET_DIR", "bar")
.with_stderr_data(str![[r#"
[REMOVED] [FILE_NUM] files, [FILE_SIZE]B total

"#]])
.run();
}

#[cargo_test]
fn env_target_dir_tag_symlink() {
let p = project()
.file("Cargo.toml", &basic_bin_manifest("foo"))
.file("src/foo.rs", &main_file(r#""i am foo""#, &[]))
.file(
"src/CACHEDIR.TAG",
"Signature: 8a477f597d28d172789f06886806bc55",
)
.symlink("src/CACHEDIR.TAG", "bar/CACHEDIR.TAG")
.build();

p.cargo("clean")
.env("CARGO_TARGET_DIR", "bar")
.with_stderr_data(str![[r#"
[REMOVED] [FILE_NUM] files, [FILE_SIZE]B total

"#]])
.run();
}

#[cargo_test]
fn env_target_dir_tag_valid() {
let p = project()
.file("Cargo.toml", &basic_bin_manifest("foo"))
.file("src/foo.rs", &main_file(r#""i am foo""#, &[]))
.file(
"bar/CACHEDIR.TAG",
"Signature: 8a477f597d28d172789f06886806bc55",
)
.build();

p.cargo("clean").env("CARGO_TARGET_DIR", "bar").run();
}

#[cargo_test]
fn config_target_dir_tag_not_present() {
// invalid target dir specified via build config

let p = project()
.file("Cargo.toml", &basic_bin_manifest("foo"))
.file("bar/.keep", "")
.file("src/foo.rs", "")
.file(
".cargo/config.toml",
"[build]
target-dir = 'bar'",
)
.build();

p.cargo("clean")
.with_stderr_data(str![[r#"
[REMOVED] [FILE_NUM] files, [FILE_SIZE]B total

"#]])
.run();
}

#[cargo_test]
fn config_target_dir_tag_invalid_signature() {
let p = project()
.file("Cargo.toml", &basic_bin_manifest("foo"))
.file("src/foo.rs", &main_file(r#""i am foo""#, &[]))
.file("bar/CACHEDIR.TAG", "Signature: 1234")
.file(
".cargo/config.toml",
"[build]
target-dir = 'bar'",
)
.build();

p.cargo("clean")
.with_stderr_data(str![[r#"
[REMOVED] [FILE_NUM] files, [FILE_SIZE]B total

"#]])
.run();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: This formatting change should be in the first commit

}

#[cargo_test]
fn config_target_dir_tag_symlink() {
let p = project()
.file("Cargo.toml", &basic_bin_manifest("foo"))
.file("src/foo.rs", &main_file(r#""i am foo""#, &[]))
.file(
"src/CACHEDIR.TAG",
"Signature: 8a477f597d28d172789f06886806bc55",
)
.symlink("src/CACHEDIR.TAG", "bar/CACHEDIR.TAG")
.file(
".cargo/config.toml",
"[build]
target-dir = 'bar'",
)
.build();

p.cargo("clean")
.with_stderr_data(str![[r#"
[REMOVED] [FILE_NUM] files, [FILE_SIZE]B total

"#]])
.run();
}

#[cargo_test]
fn config_target_dir_tag_valid() {
let p = project()
.file("Cargo.toml", &basic_bin_manifest("foo"))
.file("src/foo.rs", &main_file(r#""i am foo""#, &[]))
.file(
"bar/CACHEDIR.TAG",
"Signature: 8a477f597d28d172789f06886806bc55",
)
.file(
".cargo/config.toml",
"[build]
target-dir = 'bar'",
)
.build();

p.cargo("clean").run();
}