use std::path::PathBuf;
use sui_compat::derivation::Derivation;
use sui_compat::store_path::{
compute_drv_path_with_refs, compute_fixed_output_hash, compute_output_path, StorePath,
};
const DRV_SAMPLE_SIZE: usize = 500;
fn nix_store_root() -> PathBuf {
std::env::var("NIX_STORE_ROOT")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("/nix/store"))
}
fn sample_drv_files(n: usize) -> Vec<PathBuf> {
let root = nix_store_root();
if !root.is_dir() {
return Vec::new();
}
let mut out: Vec<PathBuf> = match std::fs::read_dir(&root) {
Ok(it) => it
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("drv"))
.collect(),
Err(_) => return Vec::new(),
};
out.sort();
out.truncate(n);
out
}
fn basename_of(p: &PathBuf) -> String {
p.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.to_string()
}
fn drv_human_name(basename: &str) -> Option<String> {
let stripped = basename.strip_suffix(".drv")?;
let sp = StorePath::from_basename(stripped).ok()?;
Some(sp.name)
}
#[test]
fn aterm_round_trip_byte_identical() {
let corpus = sample_drv_files(DRV_SAMPLE_SIZE);
if corpus.is_empty() {
eprintln!("skip aterm_round_trip_byte_identical: /nix/store has no .drv files");
return;
}
let mut parsed_count = 0;
let mut roundtrip_mismatches: Vec<(PathBuf, String)> = Vec::new();
let mut parse_failures: Vec<(PathBuf, String)> = Vec::new();
for path in &corpus {
let bytes = match std::fs::read(path) {
Ok(b) => b,
Err(_) => continue,
};
let parsed = match Derivation::parse(&bytes) {
Ok(d) => d,
Err(e) => {
parse_failures.push((path.clone(), format!("{e}")));
continue;
}
};
parsed_count += 1;
let round = parsed.serialize();
if round.as_bytes() != bytes.as_slice() {
let first_diff = first_byte_difference(&bytes, round.as_bytes());
roundtrip_mismatches.push((
path.clone(),
format!(
"orig {} bytes, round {} bytes, first diff at {:?}",
bytes.len(),
round.len(),
first_diff
),
));
}
}
eprintln!(
"aterm_round_trip: parsed {} / {} (parse_failures={}, roundtrip_mismatches={})",
parsed_count,
corpus.len(),
parse_failures.len(),
roundtrip_mismatches.len()
);
if !parse_failures.is_empty() {
let summary: String = parse_failures
.iter()
.take(5)
.map(|(p, why)| format!(" {}\n {why}", p.display()))
.collect::<Vec<_>>()
.join("\n");
panic!(
"{} .drv files failed to parse. First {}:\n{}",
parse_failures.len(),
parse_failures.len().min(5),
summary
);
}
if !roundtrip_mismatches.is_empty() {
let summary: String = roundtrip_mismatches
.iter()
.take(5)
.map(|(p, why)| format!(" {}\n {why}", p.display()))
.collect::<Vec<_>>()
.join("\n");
panic!(
"{} .drv files failed ATerm round-trip. First {}:\n{}",
roundtrip_mismatches.len(),
roundtrip_mismatches.len().min(5),
summary
);
}
}
#[test]
fn drv_path_matches_filename() {
let corpus = sample_drv_files(DRV_SAMPLE_SIZE);
if corpus.is_empty() {
eprintln!("skip drv_path_matches_filename: no corpus");
return;
}
let mut checked = 0;
let mut mismatches: Vec<(PathBuf, String)> = Vec::new();
for path in &corpus {
let bytes = match std::fs::read(path) {
Ok(b) => b,
Err(_) => continue,
};
let parsed = match Derivation::parse(&bytes) {
Ok(d) => d,
Err(_) => continue,
};
let Some(name) = drv_human_name(&basename_of(path)) else {
continue;
};
let mut refs: Vec<String> = parsed.input_derivations.keys().cloned().collect();
refs.extend(parsed.input_sources.iter().cloned());
let computed = compute_drv_path_with_refs(&bytes, &name, &refs);
let computed_basename = computed
.rsplit_once('/')
.map(|(_, b)| b.to_string())
.unwrap_or(computed.clone());
let actual_basename = basename_of(path);
if computed_basename != actual_basename {
mismatches.push((
path.clone(),
format!("expected {actual_basename}, got {computed_basename}"),
));
}
checked += 1;
}
eprintln!(
"drv_path_matches_filename: checked {}, mismatches {}",
checked,
mismatches.len()
);
if !mismatches.is_empty() {
let summary: String = mismatches
.iter()
.take(5)
.map(|(p, why)| format!(" {}\n {why}", p.display()))
.collect::<Vec<_>>()
.join("\n");
panic!(
"{} / {} .drv files had drvPath mismatch. First {}:\n{}",
mismatches.len(),
checked,
mismatches.len().min(5),
summary
);
}
}
#[test]
fn fixed_output_path_matches_declared() {
let corpus = sample_drv_files(DRV_SAMPLE_SIZE);
if corpus.is_empty() {
eprintln!("skip fixed_output_path_matches_declared: no corpus");
return;
}
let mut checked = 0;
let mut mismatches: Vec<(PathBuf, String)> = Vec::new();
for path in &corpus {
let bytes = match std::fs::read(path) {
Ok(b) => b,
Err(_) => continue,
};
let parsed = match Derivation::parse(&bytes) {
Ok(d) => d,
Err(_) => continue,
};
let Some(out) = parsed.outputs.get("out") else {
continue;
};
if out.hash_algo.is_empty() || out.hash.is_empty() {
continue; }
let Some(name) = drv_human_name(&basename_of(path)) else {
continue;
};
let is_recursive = out.hash_algo.starts_with("r:");
let algo = out.hash_algo.trim_start_matches("r:");
if algo != "sha256" {
continue;
}
let computed = compute_fixed_output_hash(algo, &out.hash, is_recursive, &name);
if computed != out.path {
mismatches.push((
path.clone(),
format!(
"name={name} algo={} recursive={is_recursive}\n expected {}\n got {}",
out.hash_algo, out.path, computed
),
));
}
checked += 1;
}
eprintln!(
"fixed_output_path_matches_declared: checked {}, mismatches {}",
checked,
mismatches.len()
);
if !mismatches.is_empty() {
let summary: String = mismatches
.iter()
.take(5)
.map(|(p, why)| format!(" {}\n {why}", p.display()))
.collect::<Vec<_>>()
.join("\n");
panic!(
"{} / {} fixed-output .drv files had output path mismatch. First {}:\n{}",
mismatches.len(),
checked,
mismatches.len().min(5),
summary
);
}
}
#[test]
fn standard_output_paths_have_valid_shape() {
let corpus = sample_drv_files(DRV_SAMPLE_SIZE);
if corpus.is_empty() {
eprintln!("skip standard_output_paths_have_valid_shape: no corpus");
return;
}
let mut checked = 0;
let mut bad_shape: Vec<(PathBuf, String)> = Vec::new();
for path in &corpus {
let bytes = match std::fs::read(path) {
Ok(b) => b,
Err(_) => continue,
};
let parsed = match Derivation::parse(&bytes) {
Ok(d) => d,
Err(_) => continue,
};
for (name, output) in &parsed.outputs {
if !output.hash_algo.is_empty() {
continue; }
if StorePath::from_absolute_path(&output.path).is_err() {
bad_shape.push((
path.clone(),
format!("output {name} has invalid shape: {}", output.path),
));
}
checked += 1;
}
}
eprintln!(
"standard_output_paths_have_valid_shape: checked {}, bad_shape {}",
checked,
bad_shape.len()
);
if !bad_shape.is_empty() {
let summary: String = bad_shape
.iter()
.take(5)
.map(|(p, why)| format!(" {}\n {why}", p.display()))
.collect::<Vec<_>>()
.join("\n");
panic!(
"{} outputs had invalid store-path shape. First {}:\n{}",
bad_shape.len(),
bad_shape.len().min(5),
summary
);
}
}
#[test]
fn compute_output_path_shape_sanity() {
let inner = "a".repeat(64);
let out = compute_output_path(&inner, "out", "hello-1.0");
let parsed = StorePath::from_absolute_path(&out).unwrap();
assert_eq!(parsed.name, "hello-1.0");
}
fn first_byte_difference(a: &[u8], b: &[u8]) -> Option<usize> {
for i in 0..a.len().min(b.len()) {
if a[i] != b[i] {
return Some(i);
}
}
if a.len() != b.len() {
Some(a.len().min(b.len()))
} else {
None
}
}