use std::path::{Path, PathBuf};
pub fn find_best_base(
snapshots_dir: &Path,
fresh_snap_dir: &Path,
snap_file: &str,
image: &str,
memory_mib: u32,
baked_by_version: &str,
) -> std::io::Result<Option<PathBuf>> {
let fresh_canonical = std::fs::canonicalize(fresh_snap_dir).ok();
let Some(fresh_size) = std::fs::metadata(fresh_snap_dir.join(snap_file))
.map(|m| m.len())
.ok()
else {
return Ok(None);
};
let mut best: Option<(std::time::SystemTime, PathBuf)> = None;
for entry in std::fs::read_dir(snapshots_dir)?.flatten() {
let path = entry.path();
if !path.is_dir() {
continue;
}
if fresh_canonical
.as_ref()
.and_then(|c| std::fs::canonicalize(&path).ok().map(|p| p == *c))
.unwrap_or(false)
{
continue;
}
let snap = path.join(snap_file);
let Ok(stat) = std::fs::metadata(&snap) else {
continue;
};
if stat.len() != fresh_size {
continue;
}
let Ok(text) = std::fs::read_to_string(path.join("metadata.json")) else {
continue;
};
let Ok(meta) = serde_json::from_str::<serde_json::Value>(&text) else {
continue;
};
if meta.get("image").and_then(|v| v.as_str()) != Some(image)
|| meta.get("memory_mib").and_then(|v| v.as_u64()) != Some(memory_mib as u64)
|| meta.get("baked_by_version").and_then(|v| v.as_str()) != Some(baked_by_version)
{
continue;
}
let mtime = stat.modified().unwrap_or(std::time::UNIX_EPOCH);
if best.as_ref().is_none_or(|(m, _)| mtime > *m) {
best = Some((mtime, snap));
}
}
Ok(best.map(|(_, p)| p))
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::sync::atomic::{AtomicU64, Ordering};
fn unique_root(tag: &str) -> PathBuf {
static N: AtomicU64 = AtomicU64::new(0);
let d = std::env::temp_dir().join(format!(
"sm-snapdedup-{tag}-{}-{}",
std::process::id(),
N.fetch_add(1, Ordering::Relaxed)
));
let _ = fs::remove_dir_all(&d);
fs::create_dir_all(&d).unwrap();
d
}
fn make_snap(
root: &Path,
name: &str,
snap_file: &str,
size: usize,
image: &str,
mem: u32,
ver: &str,
) -> PathBuf {
let d = root.join(name);
fs::create_dir_all(&d).unwrap();
fs::write(d.join(snap_file), vec![0u8; size]).unwrap();
let meta = serde_json::json!({
"image": image, "memory_mib": mem, "baked_by_version": ver,
});
fs::write(d.join("metadata.json"), serde_json::to_vec(&meta).unwrap()).unwrap();
d
}
#[test]
fn picks_only_a_fully_matching_sibling() {
let sf = "vm.snap";
let root = unique_root("match");
let fresh = make_snap(&root, "fresh", sf, 1000, "img:1", 512, "v1");
make_snap(&root, "wrong_size", sf, 999, "img:1", 512, "v1");
make_snap(&root, "wrong_image", sf, 1000, "img:2", 512, "v1");
make_snap(&root, "wrong_mem", sf, 1000, "img:1", 256, "v1");
make_snap(&root, "wrong_ver", sf, 1000, "img:1", 512, "v2");
let good = make_snap(&root, "good", sf, 1000, "img:1", 512, "v1");
let r = find_best_base(&root, &fresh, sf, "img:1", 512, "v1").unwrap();
assert_eq!(r, Some(good.join(sf)));
let _ = fs::remove_dir_all(&root);
}
#[test]
fn snap_file_name_is_respected() {
let root = unique_root("filename");
let fresh = make_snap(&root, "fresh", "vm.snap", 500, "i", 128, "v");
make_snap(&root, "hvf_style", "restore.snap", 500, "i", 128, "v");
assert_eq!(
find_best_base(&root, &fresh, "vm.snap", "i", 128, "v").unwrap(),
None
);
let _ = fs::remove_dir_all(&root);
}
#[test]
fn excludes_self_and_none_without_sibling() {
let root = unique_root("self");
let fresh = make_snap(&root, "fresh", "vm.snap", 500, "i", 128, "v");
assert_eq!(
find_best_base(&root, &fresh, "vm.snap", "i", 128, "v").unwrap(),
None
);
let _ = fs::remove_dir_all(&root);
}
#[test]
fn none_when_fresh_snap_missing() {
let root = unique_root("nofresh");
let fresh = root.join("fresh");
fs::create_dir_all(&fresh).unwrap();
make_snap(&root, "other", "vm.snap", 100, "i", 64, "v");
assert_eq!(
find_best_base(&root, &fresh, "vm.snap", "i", 64, "v").unwrap(),
None
);
let _ = fs::remove_dir_all(&root);
}
#[test]
fn newest_sibling_wins_at_subsecond_resolution() {
let sf = "vm.snap";
let root = unique_root("newest");
let fresh = make_snap(&root, "fresh", sf, 800, "i", 256, "v");
make_snap(&root, "older", sf, 800, "i", 256, "v");
std::thread::sleep(std::time::Duration::from_millis(30));
let newer = make_snap(&root, "newer", sf, 800, "i", 256, "v");
let r = find_best_base(&root, &fresh, sf, "i", 256, "v").unwrap();
assert_eq!(
r,
Some(newer.join(sf)),
"must pick the most-recent matching sibling (sub-second mtime)"
);
let _ = fs::remove_dir_all(&root);
}
}