Skip to content

Commit 94a4d66

Browse files
committed
implement checks for target dir in cargo clean using CACHEDIR.TAG
1 parent c8b4c91 commit 94a4d66

3 files changed

Lines changed: 143 additions & 10 deletions

File tree

src/bin/cargo/commands/clean.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult {
163163
profile_specified: args.contains_id("profile") || args.flag("release"),
164164
doc: args.flag("doc"),
165165
dry_run: args.dry_run(),
166+
explicit_target_dir_arg: args.contains_id("target-dir"),
166167
};
167168
ops::clean(&ws, &opts)?;
168169
Ok(())

src/cargo/ops/cargo_clean.rs

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@ use crate::util::edit_distance;
77
use crate::util::errors::CargoResult;
88
use crate::util::interning::InternedString;
99
use crate::util::{GlobalContext, Progress, ProgressStyle};
10+
use annotate_snippets::Level;
1011
use anyhow::bail;
1112
use cargo_util::paths;
1213
use indexmap::{IndexMap, IndexSet};
1314

1415
use std::ffi::OsString;
15-
use std::fs;
16+
use std::io::Read;
1617
use std::path::{Path, PathBuf};
1718
use std::rc::Rc;
19+
use std::{fs, io};
1820

1921
pub struct CleanOptions<'gctx> {
2022
pub gctx: &'gctx GlobalContext,
@@ -30,6 +32,8 @@ pub struct CleanOptions<'gctx> {
3032
pub doc: bool,
3133
/// If set, doesn't delete anything.
3234
pub dry_run: bool,
35+
/// true if target-dir was was explicitly specified via --target-dir
36+
pub explicit_target_dir_arg: bool,
3337
}
3438

3539
pub struct CleanContext<'gctx> {
@@ -49,6 +53,35 @@ pub fn clean(ws: &Workspace<'_>, opts: &CleanOptions<'_>) -> CargoResult<()> {
4953
let mut clean_ctx = CleanContext::new(gctx);
5054
clean_ctx.dry_run = opts.dry_run;
5155

56+
if opts.explicit_target_dir_arg {
57+
// if target_dir was passed explicitly via --target-dir, then hard error if validation fails
58+
if let Err(err) = validate_target_dir_tag(target_dir.as_path_unlocked()) {
59+
let title = format!("cannot clean `{}`: {err:#}", target_dir.display());
60+
let note =
61+
"cleaning has been aborted to prevent accidental deletion of unrelated files";
62+
63+
let report = [Level::ERROR
64+
.primary_title(title)
65+
.element(Level::NOTE.message(note))];
66+
gctx.shell().print_report(&report, false)?;
67+
return Err(crate::AlreadyPrintedError::new(anyhow::anyhow!("")).into());
68+
}
69+
} else if gctx.target_dir()?.is_some() {
70+
// target_dir was set via env or build config
71+
if let Err(err) = validate_target_dir_tag(target_dir.as_path_unlocked()) {
72+
let title = format!(
73+
"`{}` does not appear to be a valid Cargo target directory: {err:#}",
74+
target_dir.display()
75+
);
76+
let note = "this may become a hard error in the future; see <https://github.com/rust-lang/cargo/issues/9192>";
77+
78+
let report = [Level::WARNING
79+
.primary_title(title)
80+
.element(Level::NOTE.message(note))];
81+
gctx.shell().print_report(&report, false)?;
82+
}
83+
}
84+
5285
if opts.doc {
5386
if !opts.spec.is_empty() {
5487
// FIXME: https://github.com/rust-lang/cargo/issues/8790
@@ -105,6 +138,42 @@ pub fn clean(ws: &Workspace<'_>, opts: &CleanOptions<'_>) -> CargoResult<()> {
105138
Ok(())
106139
}
107140

141+
fn validate_target_dir_tag(target_dir_path: &Path) -> CargoResult<()> {
142+
const TAG_SIGNATURE: &[u8] = b"Signature: 8a477f597d28d172789f06886806bc55";
143+
144+
// if the path is not a dir then don't do anything
145+
if !target_dir_path.is_dir() {
146+
return Ok(());
147+
}
148+
149+
let tag_path = target_dir_path.join("CACHEDIR.TAG");
150+
151+
// per https://bford.info/cachedir the tag file must not be a symlink
152+
if tag_path.is_symlink() {
153+
bail!("expect `CACHEDIR.TAG` to be a regular file, got a symlink");
154+
}
155+
156+
if !tag_path.is_file() {
157+
bail!("missing or invalid `CACHEDIR.TAG` file");
158+
}
159+
160+
let mut file = fs::File::open(&tag_path)
161+
.map_err(|err| anyhow::anyhow!("failed to open `{}`: {}", tag_path.display(), err))?;
162+
163+
let mut buf = [0u8; TAG_SIGNATURE.len()];
164+
match file.read_exact(&mut buf) {
165+
Ok(()) if &buf[..] == TAG_SIGNATURE => {}
166+
Err(e) if e.kind() != io::ErrorKind::UnexpectedEof => {
167+
bail!("failed to read `{}`: {e}", tag_path.display());
168+
}
169+
_ => {
170+
bail!("invalid signature in `CACHEDIR.TAG` file");
171+
}
172+
}
173+
174+
Ok(())
175+
}
176+
108177
fn clean_specs(
109178
clean_ctx: &mut CleanContext<'_>,
110179
ws: &Workspace<'_>,

tests/testsuite/clean.rs

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1071,7 +1071,13 @@ fn explicit_target_dir_tag_not_present() {
10711071

10721072
p.cargo("clean --target-dir bar")
10731073
.with_stdout_data("")
1074-
.with_status(0)
1074+
.with_stderr_data(str![[r#"
1075+
[ERROR] cannot clean `[ROOT]/foo/bar`: missing or invalid `CACHEDIR.TAG` file
1076+
|
1077+
= [NOTE] cleaning has been aborted to prevent accidental deletion of unrelated files
1078+
1079+
"#]])
1080+
.with_status(101)
10751081
.run();
10761082
}
10771083

@@ -1085,7 +1091,13 @@ fn explicit_target_dir_tag_invalid_signature() {
10851091

10861092
p.cargo("clean --target-dir bar")
10871093
.with_stdout_data("")
1088-
.with_status(0)
1094+
.with_stderr_data(str![[r#"
1095+
[ERROR] cannot clean `[ROOT]/foo/bar`: invalid signature in `CACHEDIR.TAG` file
1096+
|
1097+
= [NOTE] cleaning has been aborted to prevent accidental deletion of unrelated files
1098+
1099+
"#]])
1100+
.with_status(101)
10891101
.run();
10901102
}
10911103

@@ -1103,7 +1115,13 @@ fn explicit_target_dir_tag_symlink() {
11031115

11041116
p.cargo("clean --target-dir bar")
11051117
.with_stdout_data("")
1106-
.with_status(0)
1118+
.with_stderr_data(str![[r#"
1119+
[ERROR] cannot clean `[ROOT]/foo/bar`: expect `CACHEDIR.TAG` to be a regular file, got a symlink
1120+
|
1121+
= [NOTE] cleaning has been aborted to prevent accidental deletion of unrelated files
1122+
1123+
"#]])
1124+
.with_status(101)
11071125
.run();
11081126
}
11091127

@@ -1131,7 +1149,15 @@ fn env_target_dir_tag_not_present() {
11311149
.file("src/foo.rs", &main_file(r#""i am foo""#, &[]))
11321150
.build();
11331151

1134-
p.cargo("clean").env("CARGO_TARGET_DIR", "bar").run();
1152+
p.cargo("clean")
1153+
.env("CARGO_TARGET_DIR", "bar")
1154+
.with_stderr_data(str![[r#"
1155+
[WARNING] `[ROOT]/foo/bar` does not appear to be a valid Cargo target directory: missing or invalid `CACHEDIR.TAG` file
1156+
|
1157+
= [NOTE] this may become a hard error in the future; see <https://github.com/rust-lang/cargo/issues/9192>
1158+
[REMOVED] [FILE_NUM] files, [FILE_SIZE]B total
1159+
1160+
"#]]).run();
11351161
}
11361162

11371163
#[cargo_test]
@@ -1142,7 +1168,15 @@ fn env_target_dir_tag_invalid_signature() {
11421168
.file("bar/CACHEDIR.TAG", "Signature: 1234")
11431169
.build();
11441170

1145-
p.cargo("clean").env("CARGO_TARGET_DIR", "bar").run();
1171+
p.cargo("clean")
1172+
.env("CARGO_TARGET_DIR", "bar")
1173+
.with_stderr_data(str![[r#"
1174+
[WARNING] `[ROOT]/foo/bar` does not appear to be a valid Cargo target directory: invalid signature in `CACHEDIR.TAG` file
1175+
|
1176+
= [NOTE] this may become a hard error in the future; see <https://github.com/rust-lang/cargo/issues/9192>
1177+
[REMOVED] [FILE_NUM] files, [FILE_SIZE]B total
1178+
1179+
"#]]).run();
11461180
}
11471181

11481182
#[cargo_test]
@@ -1157,7 +1191,15 @@ fn env_target_dir_tag_symlink() {
11571191
.symlink("src/CACHEDIR.TAG", "bar/CACHEDIR.TAG")
11581192
.build();
11591193

1160-
p.cargo("clean").env("CARGO_TARGET_DIR", "bar").run();
1194+
p.cargo("clean")
1195+
.env("CARGO_TARGET_DIR", "bar")
1196+
.with_stderr_data(str![[r#"
1197+
[WARNING] `[ROOT]/foo/bar` does not appear to be a valid Cargo target directory: expect `CACHEDIR.TAG` to be a regular file, got a symlink
1198+
|
1199+
= [NOTE] this may become a hard error in the future; see <https://github.com/rust-lang/cargo/issues/9192>
1200+
[REMOVED] [FILE_NUM] files, [FILE_SIZE]B total
1201+
1202+
"#]]).run();
11611203
}
11621204

11631205
#[cargo_test]
@@ -1189,7 +1231,14 @@ fn config_target_dir_tag_not_present() {
11891231
)
11901232
.build();
11911233

1192-
p.cargo("clean").run();
1234+
p.cargo("clean")
1235+
.with_stderr_data(str![[r#"
1236+
[WARNING] `[ROOT]/foo/bar` does not appear to be a valid Cargo target directory: missing or invalid `CACHEDIR.TAG` file
1237+
|
1238+
= [NOTE] this may become a hard error in the future; see <https://github.com/rust-lang/cargo/issues/9192>
1239+
[REMOVED] [FILE_NUM] files, [FILE_SIZE]B total
1240+
1241+
"#]]).run();
11931242
}
11941243

11951244
#[cargo_test]
@@ -1205,7 +1254,14 @@ fn config_target_dir_tag_invalid_signature() {
12051254
)
12061255
.build();
12071256

1208-
p.cargo("clean").run();
1257+
p.cargo("clean")
1258+
.with_stderr_data(str![[r#"
1259+
[WARNING] `[ROOT]/foo/bar` does not appear to be a valid Cargo target directory: invalid signature in `CACHEDIR.TAG` file
1260+
|
1261+
= [NOTE] this may become a hard error in the future; see <https://github.com/rust-lang/cargo/issues/9192>
1262+
[REMOVED] [FILE_NUM] files, [FILE_SIZE]B total
1263+
1264+
"#]]).run();
12091265
}
12101266

12111267
#[cargo_test]
@@ -1225,7 +1281,14 @@ fn config_target_dir_tag_symlink() {
12251281
)
12261282
.build();
12271283

1228-
p.cargo("clean").run();
1284+
p.cargo("clean")
1285+
.with_stderr_data(str![[r#"
1286+
[WARNING] `[ROOT]/foo/bar` does not appear to be a valid Cargo target directory: expect `CACHEDIR.TAG` to be a regular file, got a symlink
1287+
|
1288+
= [NOTE] this may become a hard error in the future; see <https://github.com/rust-lang/cargo/issues/9192>
1289+
[REMOVED] [FILE_NUM] files, [FILE_SIZE]B total
1290+
1291+
"#]]).run();
12291292
}
12301293

12311294
#[cargo_test]

0 commit comments

Comments
 (0)