use std::io::Write;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::process::{Command, Stdio};
use assert_cmd::prelude::*;
use assert_fs::prelude::*;
use assert_fs::TempDir;
fn snapdir(cache: &Path) -> Command {
let mut cmd = Command::cargo_bin("snapdir").expect("snapdir binary built");
cmd.env("SNAPDIR_CACHE_DIR", cache);
cmd
}
fn build_tree(dir: &TempDir) {
dir.child("a.txt").write_str("hello").unwrap();
std::fs::set_permissions(dir.child("a.txt").path(), PermissionsExt::from_mode(0o644)).unwrap();
dir.child("sub/b.txt").write_str("world!!").unwrap();
std::fs::set_permissions(
dir.child("sub/b.txt").path(),
PermissionsExt::from_mode(0o600),
)
.unwrap();
std::fs::set_permissions(dir.child("sub").path(), PermissionsExt::from_mode(0o755)).unwrap();
std::fs::set_permissions(dir.path(), PermissionsExt::from_mode(0o755)).unwrap();
}
fn id_with_stdin(cache: &Path, cwd: &Path, args: &[&str], stdin_bytes: &[u8]) -> String {
let mut child = snapdir(cache)
.arg("id")
.args(args)
.current_dir(cwd)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.expect("spawn snapdir id");
child.stdin.take().unwrap().write_all(stdin_bytes).unwrap();
let out = child.wait_with_output().unwrap();
assert!(
out.status.success(),
"`snapdir id` (stdin) must succeed; stderr would be on null"
);
String::from_utf8(out.stdout).unwrap().trim().to_owned()
}
fn manifest_and_id(cache: &Path, tree: &Path) -> (Vec<u8>, String) {
let man = snapdir(cache).arg("manifest").arg(tree).output().unwrap();
assert!(man.status.success(), "manifest <tree> must succeed");
let id_dir = snapdir(cache).arg("id").arg(tree).output().unwrap();
assert!(id_dir.status.success(), "id <tree> must succeed");
let id = String::from_utf8(id_dir.stdout).unwrap().trim().to_owned();
(man.stdout, id)
}
#[test]
fn id_from_stdin_depends_only_on_stdin_not_cwd() {
let cache = TempDir::new().unwrap();
let tree = TempDir::new().unwrap();
build_tree(&tree);
let cwd_a = TempDir::new().unwrap();
cwd_a.child("alpha.txt").write_str("A").unwrap();
let cwd_b = TempDir::new().unwrap();
cwd_b.child("beta.txt").write_str("BBBB").unwrap();
let (manifest_bytes, _id_dir) = manifest_and_id(cache.path(), tree.path());
let c1 = TempDir::new().unwrap();
let c2 = TempDir::new().unwrap();
let from_a = id_with_stdin(c1.path(), cwd_a.path(), &[], &manifest_bytes);
let from_b = id_with_stdin(c2.path(), cwd_b.path(), &[], &manifest_bytes);
assert_eq!(
from_a, from_b,
"`snapdir id` on FIXED stdin must be CWD-independent, \
but got {from_a} (cwd_a) vs {from_b} (cwd_b) — stdin is being ignored"
);
}
#[test]
fn manifest_piped_to_id_round_trips_to_id_dir() {
let cache = TempDir::new().unwrap();
let tree = TempDir::new().unwrap();
build_tree(&tree);
let other_cwd = TempDir::new().unwrap();
other_cwd.child("noise.txt").write_str("noise").unwrap();
let (manifest_bytes, id_dir) = manifest_and_id(cache.path(), tree.path());
let c = TempDir::new().unwrap();
let piped = id_with_stdin(c.path(), other_cwd.path(), &[], &manifest_bytes);
assert_eq!(
piped, id_dir,
"`manifest <tree> | id` ({piped}) must round-trip to `id <tree>` ({id_dir})"
);
}
fn id_run_raw(
cache: &Path,
cwd: &Path,
args: &[&str],
stdin: Stdio,
stdin_bytes: Option<&[u8]>,
) -> std::process::Output {
let mut child = snapdir(cache)
.arg("id")
.args(args)
.current_dir(cwd)
.stdin(stdin)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn snapdir id");
if let Some(bytes) = stdin_bytes {
child.stdin.take().unwrap().write_all(bytes).unwrap();
}
child.wait_with_output().unwrap()
}
#[test]
fn round_trip_keystone_is_cwd_independent_and_byte_identical() {
let cache = TempDir::new().unwrap();
let tree = TempDir::new().unwrap();
build_tree(&tree);
let unrelated_cwd = TempDir::new().unwrap();
unrelated_cwd
.child("decoy/keystone.txt")
.write_str("totally different bytes")
.unwrap();
let (manifest_bytes, id_dir) = manifest_and_id(cache.path(), tree.path());
let c = TempDir::new().unwrap();
let piped = id_with_stdin(c.path(), unrelated_cwd.path(), &[], &manifest_bytes);
assert_eq!(
piped, id_dir,
"`manifest <dir> | id` from an unrelated cwd must be byte-identical to \
`id <dir>`: got {piped} vs {id_dir}"
);
assert_eq!(
id_dir.len(),
64,
"snapshot id must be 64 hex chars: {id_dir}"
);
assert!(
id_dir.bytes().all(|b| b.is_ascii_hexdigit()),
"snapshot id must be hex: {id_dir}"
);
}
#[test]
fn id_no_path_with_dev_null_is_empty_manifest_id_not_cwd_walk() {
let cache = TempDir::new().unwrap();
let cwd = TempDir::new().unwrap();
cwd.child("walked.txt")
.write_str("if you see this id, you walked the cwd")
.unwrap();
let cwd_id = {
let out = snapdir(cache.path())
.arg("id")
.arg(cwd.path())
.output()
.unwrap();
assert!(out.status.success());
String::from_utf8(out.stdout).unwrap().trim().to_owned()
};
let empty_cache = TempDir::new().unwrap();
let empty_id = id_with_stdin(empty_cache.path(), cwd.path(), &[], b"");
let c = TempDir::new().unwrap();
let out = id_run_raw(
c.path(),
cwd.path(),
&[],
Stdio::null(), None,
);
let stdout = String::from_utf8(out.stdout).unwrap();
let got = stdout.trim().to_owned();
assert!(
out.status.success(),
"`id` with /dev/null stdin currently takes the empty-manifest branch \
and succeeds; stderr: {}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(
got, empty_id,
"`id < /dev/null` must yield the deterministic EMPTY-manifest id, got {got}"
);
assert_ne!(
got, cwd_id,
"`id < /dev/null` must NOT walk the cwd (cwd id was {cwd_id})"
);
}
#[test]
fn id_empty_piped_stdin_is_deterministic_empty_manifest_id() {
let cache = TempDir::new().unwrap();
let cwd = TempDir::new().unwrap();
cwd.child("noise.txt")
.write_str("noise noise noise")
.unwrap();
let cwd_id = {
let out = snapdir(cache.path())
.arg("id")
.arg(cwd.path())
.output()
.unwrap();
assert!(out.status.success());
String::from_utf8(out.stdout).unwrap().trim().to_owned()
};
let c1 = TempDir::new().unwrap();
let c2 = TempDir::new().unwrap();
let first = id_with_stdin(c1.path(), cwd.path(), &[], b"");
let second = id_with_stdin(c2.path(), cwd.path(), &[], b"");
assert_eq!(
first, second,
"empty piped stdin must be deterministic: {first} vs {second}"
);
assert_ne!(
first, cwd_id,
"empty piped stdin must NOT produce the cwd id ({cwd_id})"
);
}
#[test]
fn id_trailing_newline_is_normalized_to_snapshot_id_contract() {
let cache = TempDir::new().unwrap();
let tree = TempDir::new().unwrap();
build_tree(&tree);
let (manifest_bytes, id_dir) = manifest_and_id(cache.path(), tree.path());
let mut no_nl = manifest_bytes.clone();
assert_eq!(
no_nl.last(),
Some(&b'\n'),
"`manifest <dir>` output is expected to end with a single newline"
);
no_nl.pop();
assert_ne!(
no_nl.last(),
Some(&b'\n'),
"fixture must not end with a blank line; stripping one '\\n' must leave a content line"
);
let with_nl = manifest_bytes;
let c1 = TempDir::new().unwrap();
let c2 = TempDir::new().unwrap();
let cwd = TempDir::new().unwrap();
let id_with = id_with_stdin(c1.path(), cwd.path(), &[], &with_nl);
let id_without = id_with_stdin(c2.path(), cwd.path(), &[], &no_nl);
assert_eq!(
id_with, id_without,
"trailing newline must be normalized: with-nl {id_with} vs without-nl {id_without}"
);
assert_eq!(
id_with, id_dir,
"the piped-manifest id must equal `id <dir>` per the frozen snapshot_id contract"
);
}
#[test]
fn id_comment_lines_are_stripped_and_do_not_affect_the_id() {
let cache = TempDir::new().unwrap();
let tree = TempDir::new().unwrap();
build_tree(&tree);
let (manifest_bytes, id_dir) = manifest_and_id(cache.path(), tree.path());
let mut commented = Vec::new();
commented.extend_from_slice(b"# snapdir manifest header comment\n");
commented.extend_from_slice(b"# generated-by: adversary review fixture\n");
commented.extend_from_slice(&manifest_bytes);
commented.extend_from_slice(b"# trailing comment after the entries\n");
let cwd = TempDir::new().unwrap();
let c1 = TempDir::new().unwrap();
let c2 = TempDir::new().unwrap();
let id_plain = id_with_stdin(c1.path(), cwd.path(), &[], &manifest_bytes);
let id_commented = id_with_stdin(c2.path(), cwd.path(), &[], &commented);
assert_eq!(
id_plain, id_commented,
"adding/removing `#`-comment lines must NOT change the id: \
plain {id_plain} vs commented {id_commented}"
);
assert_eq!(
id_commented, id_dir,
"the comment-stripped piped id must equal `id <dir>` ({id_dir})"
);
}
#[test]
fn id_malformed_stdin_errors_cleanly_without_cwd_fallback() {
let cache = TempDir::new().unwrap();
let cwd = TempDir::new().unwrap();
cwd.child("present.txt").write_str("present").unwrap();
let cwd_id = {
let out = snapdir(cache.path())
.arg("id")
.arg(cwd.path())
.output()
.unwrap();
assert!(out.status.success());
String::from_utf8(out.stdout).unwrap().trim().to_owned()
};
let c = TempDir::new().unwrap();
let out = id_run_raw(
c.path(),
cwd.path(),
&[],
Stdio::piped(),
Some(b"this is not a manifest @@@\nrandom !!! garbage\n"),
);
let stdout = String::from_utf8(out.stdout).unwrap();
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
!out.status.success(),
"malformed stdin must produce a nonzero exit; stdout={stdout:?} stderr={stderr}"
);
assert!(
out.status.code().is_some(),
"malformed stdin must exit cleanly (a code), not via a signal/panic; stderr={stderr}"
);
assert!(
stderr.contains("manifest") || stderr.contains("stdin") || stderr.contains("parse"),
"malformed stdin must emit a parse-error message; got stderr: {stderr}"
);
assert!(
stdout.trim().is_empty(),
"malformed stdin must NOT print an id on stdout; got: {stdout:?}"
);
assert!(
!stdout.contains(&cwd_id),
"malformed stdin must NOT fall back to the cwd id ({cwd_id})"
);
}