use std::io::Write as _;
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::process::{Command, Output, Stdio};
use assert_fs::prelude::*;
use assert_fs::TempDir;
use snapdir_core::{manifest_path, object_path, Blake3Hasher, Hasher, Store};
use snapdir_stores::{FileStore, StreamStore, WIRE_CAPS, WIRE_VERSION};
fn snapdir_bin() -> std::path::PathBuf {
assert_cmd::cargo::cargo_bin("snapdir")
}
fn snapdir(cache: &Path) -> Command {
let mut cmd = Command::new(snapdir_bin());
cmd.env("SNAPDIR_CACHE_DIR", cache);
cmd.env_remove("SNAPDIR_STORE");
cmd
}
fn run_with_stdin(cache: &Path, args: &[&str], stdin_bytes: &[u8]) -> Output {
let mut child = snapdir(cache)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn snapdir");
child
.stdin
.take()
.expect("piped stdin")
.write_all(stdin_bytes)
.expect("write stdin");
child.wait_with_output().expect("snapdir output")
}
fn stdout_ok(cache: &Path, args: &[&str]) -> String {
let out = snapdir(cache).args(args).output().expect("run snapdir");
assert!(
out.status.success(),
"snapdir {args:?} failed ({:?})\nstderr: {}",
out.status.code(),
String::from_utf8_lossy(&out.stderr),
);
String::from_utf8(out.stdout).unwrap().trim_end().to_owned()
}
fn build_tree(dir: &TempDir) {
dir.child("a.txt").write_str("plumbing alpha").unwrap();
dir.child("sub/b.txt")
.write_str("plumbing bravo!!")
.unwrap();
dir.child("sub/c.bin")
.write_str("plumbing charlie payload")
.unwrap();
for (rel, mode) in [("a.txt", 0o644), ("sub/b.txt", 0o600), ("sub/c.bin", 0o644)] {
std::fs::set_permissions(dir.child(rel).path(), PermissionsExt::from_mode(mode)).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 pushed_fixture(cache: &Path) -> (TempDir, String, Vec<String>) {
let src = TempDir::new().unwrap();
let store = TempDir::new().unwrap();
build_tree(&src);
let src_str = src.path().to_string_lossy().into_owned();
let store_url = format!("file://{}", store.path().display());
let id = stdout_ok(cache, &["push", "--store", &store_url, &src_str]);
let manifest = FileStore::from_root(store.path())
.get_manifest(&id)
.expect("manifest in pushed store");
let mut object_ids: Vec<String> = Vec::new();
for entry in manifest.entries() {
if entry.path_type == snapdir_core::PathType::File && !object_ids.contains(&entry.checksum)
{
object_ids.push(entry.checksum.clone());
}
}
assert!(object_ids.len() >= 3, "fixture must carry several objects");
(store, id, object_ids)
}
fn hex_of(bytes: &[u8]) -> String {
Blake3Hasher::new().hash_hex(bytes)
}
fn files_under(dir: &Path) -> Vec<std::path::PathBuf> {
let mut files = Vec::new();
let Ok(entries) = std::fs::read_dir(dir) else {
return files;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
files.extend(files_under(&path));
} else {
files.push(path);
}
}
files
}
#[test]
fn plumbing_capabilities_line_exact() {
let cache = TempDir::new().unwrap();
let out = snapdir(cache.path())
.args(["version", "--capabilities"])
.output()
.expect("run snapdir");
assert!(out.status.success());
let stdout = String::from_utf8(out.stdout).unwrap();
assert_eq!(
stdout,
format!(
"snapdir {} wire=1 caps=objects-needed,send-pack,receive-pack\n",
env!("CARGO_PKG_VERSION")
)
);
assert_eq!(
stdout,
format!(
"snapdir {} wire={WIRE_VERSION} caps={}\n",
env!("CARGO_PKG_VERSION"),
WIRE_CAPS.join(",")
)
);
}
#[test]
fn plumbing_plain_version_unchanged() {
let cache = TempDir::new().unwrap();
let out = snapdir(cache.path())
.arg("version")
.output()
.expect("run snapdir");
assert!(out.status.success());
assert_eq!(
String::from_utf8(out.stdout).unwrap(),
format!("snapdir {}\n", env!("CARGO_PKG_VERSION"))
);
}
#[test]
fn plumbing_objects_needed_prints_exact_complement_in_order() {
let cache = TempDir::new().unwrap();
let store_dir = TempDir::new().unwrap();
let store = FileStore::from_root(store_dir.path());
let present: Vec<String> = [b"seeded one".as_slice(), b"seeded two".as_slice()]
.iter()
.map(|payload| {
let checksum = hex_of(payload);
store
.put_object(&checksum, payload.to_vec())
.expect("seed object");
checksum
})
.collect();
let absent_a = hex_of(b"absent a");
let absent_b = hex_of(b"absent b");
let stdin = format!(
"{p0}\n{a}\n{p0}\n{p1}\n{b}\n{a}\n{p1}\n",
p0 = present[0],
p1 = present[1],
a = absent_a,
b = absent_b,
);
let store_url = format!("file://{}", store_dir.path().display());
let out = run_with_stdin(
cache.path(),
&["objects-needed", "--store", &store_url],
stdin.as_bytes(),
);
assert!(
out.status.success(),
"objects-needed failed: {}",
String::from_utf8_lossy(&out.stderr)
);
assert_eq!(
String::from_utf8(out.stdout).unwrap(),
format!("{absent_a}\n{absent_b}\n"),
"stdout must be the exact absent complement in first-occurrence order"
);
}
#[test]
fn plumbing_objects_needed_malformed_line_fails_closed() {
let cache = TempDir::new().unwrap();
let store_dir = TempDir::new().unwrap();
let store_url = format!("file://{}", store_dir.path().display());
let valid_absent = hex_of(b"valid but absent");
let hex = "0123456789abcdef".repeat(4);
for bad in [
hex.to_uppercase(), hex[..63].to_owned(), format!("g{}", &hex[1..]), ] {
let stdin = format!("{valid_absent}\n{bad}\n");
let out = run_with_stdin(
cache.path(),
&["objects-needed", "--store", &store_url],
stdin.as_bytes(),
);
assert!(
!out.status.success(),
"malformed line {bad:?} must fail the request"
);
assert!(
out.stdout.is_empty(),
"malformed line {bad:?} must print NOTHING to stdout, got: {}",
String::from_utf8_lossy(&out.stdout)
);
}
}
#[test]
fn plumbing_objects_needed_empty_input_is_ok() {
let cache = TempDir::new().unwrap();
let store_dir = TempDir::new().unwrap();
let store_url = format!("file://{}", store_dir.path().display());
let out = run_with_stdin(
cache.path(),
&["objects-needed", "--store", &store_url],
b"",
);
assert!(out.status.success(), "empty input must succeed");
assert!(out.stdout.is_empty(), "empty input must print nothing");
}
#[test]
fn plumbing_pack_roundtrip_via_pipe() {
let cache = TempDir::new().unwrap();
let (store_a, id, object_ids) = pushed_fixture(cache.path());
let store_b = TempDir::new().unwrap();
let url_a = format!("file://{}", store_a.path().display());
let url_b = format!("file://{}", store_b.path().display());
let mut send = snapdir(cache.path())
.args([
"send-pack",
"--store",
&url_a,
"--ids",
"-",
"--manifest-id",
&id,
])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn send-pack");
send.stdin
.take()
.expect("send-pack stdin")
.write_all(format!("{}\n", object_ids.join("\n")).as_bytes())
.expect("write id list");
let pack_pipe = send.stdout.take().expect("send-pack stdout");
let recv = snapdir(cache.path())
.args(["receive-pack", "--store", &url_b, "--require-manifest", &id])
.stdin(Stdio::from(pack_pipe))
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.expect("run receive-pack");
let send_out = send.wait_with_output().expect("send-pack exit");
assert!(
send_out.status.success(),
"send-pack failed: {}",
String::from_utf8_lossy(&send_out.stderr)
);
assert!(
recv.status.success(),
"receive-pack failed: {}",
String::from_utf8_lossy(&recv.stderr)
);
assert!(
recv.stdout.is_empty(),
"receive-pack must print nothing to stdout"
);
let man_rel = manifest_path(&id);
assert_eq!(
std::fs::read(store_b.path().join(&man_rel)).expect("manifest in B"),
std::fs::read(store_a.path().join(&man_rel)).expect("manifest in A"),
"manifest must be byte-equal at the identical sharded path"
);
for checksum in &object_ids {
let rel = object_path(checksum);
assert_eq!(
std::fs::read(store_b.path().join(&rel)).expect("object in B"),
std::fs::read(store_a.path().join(&rel)).expect("object in A"),
"object {rel} must be byte-equal"
);
}
}
#[test]
fn plumbing_receive_pack_rejects_hash_mismatch() {
let cache = TempDir::new().unwrap();
let store_dir = TempDir::new().unwrap();
let store_url = format!("file://{}", store_dir.path().display());
let claimed = hex_of(b"good bytes");
let evil = b"evil bytes"; let mut stream = b"SNAPPACK 1\n".to_vec();
stream.extend_from_slice(format!("obj {claimed} {}\n", evil.len()).as_bytes());
stream.extend_from_slice(evil);
stream.extend_from_slice(b"end\n");
let out = run_with_stdin(
cache.path(),
&["receive-pack", "--store", &store_url],
&stream,
);
assert!(!out.status.success(), "mismatched payload must be rejected");
assert!(out.stdout.is_empty(), "stdout must stay silent");
assert!(
!store_dir.path().join(object_path(&claimed)).exists(),
"nothing may be filed at the claimed checksum's sharded path"
);
assert_eq!(
files_under(store_dir.path()),
Vec::<std::path::PathBuf>::new(),
"no file (object, manifest, or temp) may survive the rejected stream"
);
}
#[test]
fn plumbing_receive_pack_rejects_traversal_and_non_hex_headers() {
let cache = TempDir::new().unwrap();
let store_dir = TempDir::new().unwrap();
let store_url = format!("file://{}", store_dir.path().display());
for bad_checksum in [
"../../../../etc/passwd",
"..%2f..%2fescape00000000000000000000000000000000000000000000000000",
&format!("{}/../x", &"0123456789abcdef".repeat(4)[..32]),
] {
let stream = format!("SNAPPACK 1\nobj {bad_checksum} 0\nend\n");
let out = run_with_stdin(
cache.path(),
&["receive-pack", "--store", &store_url],
stream.as_bytes(),
);
assert!(
!out.status.success(),
"header checksum {bad_checksum:?} must be rejected"
);
assert_eq!(
files_under(store_dir.path()),
Vec::<std::path::PathBuf>::new(),
"rejected header {bad_checksum:?} must file nothing"
);
}
}
#[test]
fn plumbing_receive_pack_truncation_then_full_rerun_completes() {
let cache = TempDir::new().unwrap();
let (store_a, id, object_ids) = pushed_fixture(cache.path());
let store_b = TempDir::new().unwrap();
let url_a = format!("file://{}", store_a.path().display());
let url_b = format!("file://{}", store_b.path().display());
let ids_stdin = format!("{}\n", object_ids.join("\n"));
let send = run_with_stdin(
cache.path(),
&[
"send-pack",
"--store",
&url_a,
"--ids",
"-",
"--manifest-id",
&id,
],
ids_stdin.as_bytes(),
);
assert!(send.status.success(), "fixture send-pack must succeed");
let pack = send.stdout;
assert!(pack.ends_with(b"end\n"), "full pack ends with the trailer");
let cut = &pack[..pack.len() - b"end\n".len()];
let out = run_with_stdin(
cache.path(),
&["receive-pack", "--store", &url_b, "--require-manifest", &id],
cut,
);
assert!(!out.status.success(), "truncated stream must fail");
for checksum in &object_ids {
let rel = object_path(checksum);
assert!(
store_b.path().join(&rel).exists(),
"verified object {rel} must be filed despite the truncation"
);
}
assert!(
!store_b.path().join(manifest_path(&id)).exists(),
"truncated stream must never commit the manifest"
);
let out = run_with_stdin(
cache.path(),
&["receive-pack", "--store", &url_b, "--require-manifest", &id],
&pack,
);
assert!(
out.status.success(),
"full re-run must complete the interrupted push: {}",
String::from_utf8_lossy(&out.stderr)
);
let man_rel = manifest_path(&id);
assert_eq!(
std::fs::read(store_b.path().join(&man_rel)).expect("manifest in B"),
std::fs::read(store_a.path().join(&man_rel)).expect("manifest in A"),
"completed push must land the byte-equal manifest"
);
}
#[test]
fn plumbing_receive_pack_require_manifest_mismatch_fails() {
let cache = TempDir::new().unwrap();
let (store_a, id, object_ids) = pushed_fixture(cache.path());
let store_b = TempDir::new().unwrap();
let url_a = format!("file://{}", store_a.path().display());
let url_b = format!("file://{}", store_b.path().display());
let send = run_with_stdin(
cache.path(),
&[
"send-pack",
"--store",
&url_a,
"--ids",
"-",
"--manifest-id",
&id,
],
format!("{}\n", object_ids.join("\n")).as_bytes(),
);
assert!(send.status.success());
let other = hex_of(b"some other manifest id");
assert_ne!(other, id);
let out = run_with_stdin(
cache.path(),
&[
"receive-pack",
"--store",
&url_b,
"--require-manifest",
&other,
],
&send.stdout,
);
assert!(
!out.status.success(),
"manifest id mismatch must fail receive-pack"
);
}
#[test]
fn plumbing_send_pack_missing_object_aborts_before_end() {
let cache = TempDir::new().unwrap();
let (store_a, _id, mut object_ids) = pushed_fixture(cache.path());
let url_a = format!("file://{}", store_a.path().display());
object_ids.push(hex_of(b"never stored anywhere"));
let ids_file = TempDir::new().unwrap();
ids_file
.child("ids.txt")
.write_str(&format!("{}\n", object_ids.join("\n")))
.unwrap();
let ids_path = ids_file
.child("ids.txt")
.path()
.to_string_lossy()
.into_owned();
let out = snapdir(cache.path())
.args(["send-pack", "--store", &url_a, "--ids", &ids_path])
.output()
.expect("run send-pack");
assert!(!out.status.success(), "missing object must fail send-pack");
assert!(
!out.stdout.ends_with(b"end\n"),
"the partial stream must NOT carry the end trailer"
);
}
#[test]
fn plumbing_send_pack_malformed_id_emits_nothing() {
let cache = TempDir::new().unwrap();
let (store_a, _id, object_ids) = pushed_fixture(cache.path());
let url_a = format!("file://{}", store_a.path().display());
let stdin = format!("{}\nNOT-A-CHECKSUM\n", object_ids[0]);
let out = run_with_stdin(
cache.path(),
&["send-pack", "--store", &url_a, "--ids", "-"],
stdin.as_bytes(),
);
assert!(!out.status.success(), "malformed id must fail send-pack");
assert!(
out.stdout.is_empty(),
"fail closed: not a single pack byte may be emitted"
);
}