Skip to content

Commit b54fe55

Browse files
authored
cargo clean: Add target directory validation (#16712)
*[View all comments](https://triagebot.infra.rust-lang.org/gh-comments/rust-lang/cargo/pull/16712)* ### What does this PR try to resolve? Fixes #9192 Implements the checks mentioned in [this comment](#9192 (comment)) To summarise, when `cargo clean` is run with a specified target directory: - If a target directory is explicitly passed via `--target-dir`: check if a valid `CACHEDIR.TAG` exists in the target directory and hard error otherwise. - In other cases where target directory is specified(via env vars or build config): emit a future incompat warning if the target directory does not contain a valid `CACHEDIR.TAG` ### Tests I've added 3 sets of unit tests for: - When `--target-dir` is used explicitly - When target directory is specified via the build config - When target directory is specified via the `CARGO_TARGET_DIR` env variable Let me know if there is a case I've missed or if i need to merge multiple tests into a single one.
2 parents 710cce5 + ac8856a commit b54fe55

3 files changed

Lines changed: 291 additions & 1 deletion

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: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ use cargo_util_terminal::report::Level;
1313
use indexmap::{IndexMap, IndexSet};
1414

1515
use std::ffi::OsString;
16-
use std::fs;
16+
use std::io::Read;
1717
use std::path::{Path, PathBuf};
1818
use std::rc::Rc;
19+
use std::{fs, io};
1920

2021
pub struct CleanOptions<'gctx> {
2122
pub gctx: &'gctx GlobalContext,
@@ -31,6 +32,8 @@ pub struct CleanOptions<'gctx> {
3132
pub doc: bool,
3233
/// If set, doesn't delete anything.
3334
pub dry_run: bool,
35+
/// true if target-dir was was explicitly specified via --target-dir
36+
pub explicit_target_dir_arg: bool,
3437
}
3538

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

72+
// do some validation on target_dir if it was specified via --target-dir
73+
if opts.explicit_target_dir_arg {
74+
let target_dir_path = target_dir.as_path_unlocked();
75+
76+
// check if the target directory has a valid CACHEDIR.TAG
77+
if let Err(err) = validate_target_dir_tag(target_dir_path) {
78+
// if target_dir was passed explicitly via --target-dir, then hard error if validation fails
79+
let title = format!("cannot clean `{}`: {err}", target_dir_path.display());
80+
let report = [Level::ERROR
81+
.primary_title(title)
82+
.element(Level::NOTE.message(CLEAN_ABORT_NOTE))];
83+
gctx.shell().print_report(&report, false)?;
84+
return Err(crate::AlreadyPrintedError::new(anyhow::anyhow!("")).into());
85+
}
86+
}
87+
6988
if opts.doc {
7089
if !opts.spec.is_empty() {
7190
// FIXME: https://github.com/rust-lang/cargo/issues/8790
@@ -122,6 +141,37 @@ pub fn clean(ws: &Workspace<'_>, opts: &CleanOptions<'_>) -> CargoResult<()> {
122141
Ok(())
123142
}
124143

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

tests/testsuite/clean.rs

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1208,3 +1208,242 @@ fn target_dir_is_symlink_file() {
12081208
// make sure cargo has not deleted the file of the symlinked target dir
12091209
assert!(p.root().join("bar-dest").exists());
12101210
}
1211+
1212+
#[cargo_test]
1213+
fn explicit_target_dir_tag_not_present() {
1214+
// invalid target dir explicitly specified via --target-dir cli arg
1215+
1216+
let p = project()
1217+
.file("Cargo.toml", &basic_bin_manifest("foo"))
1218+
.file("src/foo.rs", &main_file(r#""i am foo""#, &[]))
1219+
.file("bar/.keep", "")
1220+
.build();
1221+
1222+
p.cargo("clean --target-dir bar")
1223+
.with_stdout_data("")
1224+
.with_stderr_data(str![[r#"
1225+
[ERROR] cannot clean `[ROOT]/foo/bar`: missing or invalid `CACHEDIR.TAG` file
1226+
|
1227+
= [NOTE] cleaning has been aborted to prevent accidental deletion of unrelated files
1228+
1229+
"#]])
1230+
.with_status(101)
1231+
.run();
1232+
}
1233+
1234+
#[cargo_test]
1235+
fn explicit_target_dir_tag_invalid_signature() {
1236+
let p = project()
1237+
.file("Cargo.toml", &basic_bin_manifest("foo"))
1238+
.file("src/foo.rs", &main_file(r#""i am foo""#, &[]))
1239+
.file("bar/CACHEDIR.TAG", "Signature: 1234")
1240+
.build();
1241+
1242+
p.cargo("clean --target-dir bar")
1243+
.with_stdout_data("")
1244+
.with_stderr_data(str![[r#"
1245+
[ERROR] cannot clean `[ROOT]/foo/bar`: invalid signature in `CACHEDIR.TAG` file
1246+
|
1247+
= [NOTE] cleaning has been aborted to prevent accidental deletion of unrelated files
1248+
1249+
"#]])
1250+
.with_status(101)
1251+
.run();
1252+
}
1253+
1254+
#[cargo_test]
1255+
fn explicit_target_dir_tag_symlink() {
1256+
let p = project()
1257+
.file("Cargo.toml", &basic_bin_manifest("foo"))
1258+
.file("src/foo.rs", &main_file(r#""i am foo""#, &[]))
1259+
.file(
1260+
"src/CACHEDIR.TAG",
1261+
"Signature: 8a477f597d28d172789f06886806bc55",
1262+
)
1263+
.symlink("src/CACHEDIR.TAG", "bar/CACHEDIR.TAG")
1264+
.build();
1265+
1266+
p.cargo("clean --target-dir bar")
1267+
.with_stdout_data("")
1268+
.with_stderr_data(str![[r#"
1269+
[ERROR] cannot clean `[ROOT]/foo/bar`: expect `CACHEDIR.TAG` to be a regular file, got a symlink
1270+
|
1271+
= [NOTE] cleaning has been aborted to prevent accidental deletion of unrelated files
1272+
1273+
"#]])
1274+
.with_status(101)
1275+
.run();
1276+
}
1277+
1278+
#[cargo_test]
1279+
fn explicit_target_dir_tag_valid() {
1280+
let p = project()
1281+
.file("Cargo.toml", &basic_bin_manifest("foo"))
1282+
.file("src/foo.rs", &main_file(r#""i am foo""#, &[]))
1283+
.file(
1284+
"bar/CACHEDIR.TAG",
1285+
"Signature: 8a477f597d28d172789f06886806bc55",
1286+
)
1287+
.build();
1288+
1289+
p.cargo("clean --target-dir bar").run();
1290+
}
1291+
1292+
#[cargo_test]
1293+
fn env_target_dir_tag_not_present() {
1294+
// invalid target dir specified via env var
1295+
1296+
let p = project()
1297+
.file("Cargo.toml", &basic_bin_manifest("foo"))
1298+
.file("bar/.keep", "")
1299+
.file("src/foo.rs", &main_file(r#""i am foo""#, &[]))
1300+
.build();
1301+
1302+
p.cargo("clean")
1303+
.env("CARGO_TARGET_DIR", "bar")
1304+
.with_stderr_data(str![[r#"
1305+
[REMOVED] [FILE_NUM] files, [FILE_SIZE]B total
1306+
1307+
"#]])
1308+
.run();
1309+
}
1310+
1311+
#[cargo_test]
1312+
fn env_target_dir_tag_invalid_signature() {
1313+
let p = project()
1314+
.file("Cargo.toml", &basic_bin_manifest("foo"))
1315+
.file("src/foo.rs", &main_file(r#""i am foo""#, &[]))
1316+
.file("bar/CACHEDIR.TAG", "Signature: 1234")
1317+
.build();
1318+
1319+
p.cargo("clean")
1320+
.env("CARGO_TARGET_DIR", "bar")
1321+
.with_stderr_data(str![[r#"
1322+
[REMOVED] [FILE_NUM] files, [FILE_SIZE]B total
1323+
1324+
"#]])
1325+
.run();
1326+
}
1327+
1328+
#[cargo_test]
1329+
fn env_target_dir_tag_symlink() {
1330+
let p = project()
1331+
.file("Cargo.toml", &basic_bin_manifest("foo"))
1332+
.file("src/foo.rs", &main_file(r#""i am foo""#, &[]))
1333+
.file(
1334+
"src/CACHEDIR.TAG",
1335+
"Signature: 8a477f597d28d172789f06886806bc55",
1336+
)
1337+
.symlink("src/CACHEDIR.TAG", "bar/CACHEDIR.TAG")
1338+
.build();
1339+
1340+
p.cargo("clean")
1341+
.env("CARGO_TARGET_DIR", "bar")
1342+
.with_stderr_data(str![[r#"
1343+
[REMOVED] [FILE_NUM] files, [FILE_SIZE]B total
1344+
1345+
"#]])
1346+
.run();
1347+
}
1348+
1349+
#[cargo_test]
1350+
fn env_target_dir_tag_valid() {
1351+
let p = project()
1352+
.file("Cargo.toml", &basic_bin_manifest("foo"))
1353+
.file("src/foo.rs", &main_file(r#""i am foo""#, &[]))
1354+
.file(
1355+
"bar/CACHEDIR.TAG",
1356+
"Signature: 8a477f597d28d172789f06886806bc55",
1357+
)
1358+
.build();
1359+
1360+
p.cargo("clean").env("CARGO_TARGET_DIR", "bar").run();
1361+
}
1362+
1363+
#[cargo_test]
1364+
fn config_target_dir_tag_not_present() {
1365+
// invalid target dir specified via build config
1366+
1367+
let p = project()
1368+
.file("Cargo.toml", &basic_bin_manifest("foo"))
1369+
.file("bar/.keep", "")
1370+
.file("src/foo.rs", "")
1371+
.file(
1372+
".cargo/config.toml",
1373+
"[build]
1374+
target-dir = 'bar'",
1375+
)
1376+
.build();
1377+
1378+
p.cargo("clean")
1379+
.with_stderr_data(str![[r#"
1380+
[REMOVED] [FILE_NUM] files, [FILE_SIZE]B total
1381+
1382+
"#]])
1383+
.run();
1384+
}
1385+
1386+
#[cargo_test]
1387+
fn config_target_dir_tag_invalid_signature() {
1388+
let p = project()
1389+
.file("Cargo.toml", &basic_bin_manifest("foo"))
1390+
.file("src/foo.rs", &main_file(r#""i am foo""#, &[]))
1391+
.file("bar/CACHEDIR.TAG", "Signature: 1234")
1392+
.file(
1393+
".cargo/config.toml",
1394+
"[build]
1395+
target-dir = 'bar'",
1396+
)
1397+
.build();
1398+
1399+
p.cargo("clean")
1400+
.with_stderr_data(str![[r#"
1401+
[REMOVED] [FILE_NUM] files, [FILE_SIZE]B total
1402+
1403+
"#]])
1404+
.run();
1405+
}
1406+
1407+
#[cargo_test]
1408+
fn config_target_dir_tag_symlink() {
1409+
let p = project()
1410+
.file("Cargo.toml", &basic_bin_manifest("foo"))
1411+
.file("src/foo.rs", &main_file(r#""i am foo""#, &[]))
1412+
.file(
1413+
"src/CACHEDIR.TAG",
1414+
"Signature: 8a477f597d28d172789f06886806bc55",
1415+
)
1416+
.symlink("src/CACHEDIR.TAG", "bar/CACHEDIR.TAG")
1417+
.file(
1418+
".cargo/config.toml",
1419+
"[build]
1420+
target-dir = 'bar'",
1421+
)
1422+
.build();
1423+
1424+
p.cargo("clean")
1425+
.with_stderr_data(str![[r#"
1426+
[REMOVED] [FILE_NUM] files, [FILE_SIZE]B total
1427+
1428+
"#]])
1429+
.run();
1430+
}
1431+
1432+
#[cargo_test]
1433+
fn config_target_dir_tag_valid() {
1434+
let p = project()
1435+
.file("Cargo.toml", &basic_bin_manifest("foo"))
1436+
.file("src/foo.rs", &main_file(r#""i am foo""#, &[]))
1437+
.file(
1438+
"bar/CACHEDIR.TAG",
1439+
"Signature: 8a477f597d28d172789f06886806bc55",
1440+
)
1441+
.file(
1442+
".cargo/config.toml",
1443+
"[build]
1444+
target-dir = 'bar'",
1445+
)
1446+
.build();
1447+
1448+
p.cargo("clean").run();
1449+
}

0 commit comments

Comments
 (0)