use std::path::PathBuf;
use crate::toolchain;
use crate::util;
#[derive(Debug, Clone)]
pub struct FreshnessSnapshot {
pub managed_dylib_path: PathBuf,
pub original_mtime_unix_secs: i64,
pub original_sha256: String,
pub original_rustc_release_line: String,
}
#[derive(Debug, Clone)]
pub enum FreshnessFailure {
DylibMissing {
path: PathBuf,
},
DylibMtimeBackward {
path: PathBuf,
original_mtime: i64,
observed_mtime: i64,
},
DylibShaMismatch {
path: PathBuf,
original_sha256: String,
observed_sha256: String,
},
RustcDrift {
original_release_line: String,
observed_release_line: String,
},
}
impl FreshnessFailure {
pub fn invariant_label(&self) -> &'static str {
match self {
Self::DylibMissing { .. } => "managed_dylib_path",
Self::DylibMtimeBackward { .. } => "dylib_mtime",
Self::DylibShaMismatch { .. } => "dylib_sha256",
Self::RustcDrift { .. } => "rustc_release",
}
}
pub fn detail(&self) -> String {
match self {
Self::DylibMissing { path } => {
format!("managed dylib no longer exists at {}", path.display())
}
Self::DylibMtimeBackward {
path,
original_mtime,
observed_mtime,
} => format!(
"managed dylib mtime moved backward at {} (original: {original_mtime}, observed: {observed_mtime})",
path.display()
),
Self::DylibShaMismatch {
path,
original_sha256,
observed_sha256,
} => format!(
"managed dylib SHA-256 changed at {} (original: {original_sha256}, observed: {observed_sha256})",
path.display()
),
Self::RustcDrift {
original_release_line,
observed_release_line,
} => format!(
"rustc release line changed (original: {original_release_line}, observed: {observed_release_line})"
),
}
}
}
pub fn check(snapshot: &FreshnessSnapshot) -> Result<(), FreshnessFailure> {
let path = &snapshot.managed_dylib_path;
let meta = match std::fs::metadata(path) {
Ok(m) => m,
Err(_) => {
return Err(FreshnessFailure::DylibMissing { path: path.clone() });
}
};
let observed_mtime = match meta.modified() {
Ok(t) => t
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0),
Err(_) => 0,
};
if observed_mtime < snapshot.original_mtime_unix_secs {
return Err(FreshnessFailure::DylibMtimeBackward {
path: path.clone(),
original_mtime: snapshot.original_mtime_unix_secs,
observed_mtime,
});
}
let observed_sha = match util::sha256_file(path) {
Ok(s) => s,
Err(_) => {
return Err(FreshnessFailure::DylibMissing { path: path.clone() });
}
};
if observed_sha != snapshot.original_sha256 {
return Err(FreshnessFailure::DylibShaMismatch {
path: path.clone(),
original_sha256: snapshot.original_sha256.clone(),
observed_sha256: observed_sha,
});
}
match toolchain::capture() {
Ok(t) => {
if t.release_line != snapshot.original_rustc_release_line {
return Err(FreshnessFailure::RustcDrift {
original_release_line: snapshot.original_rustc_release_line.clone(),
observed_release_line: t.release_line,
});
}
}
Err(_) => {
return Err(FreshnessFailure::RustcDrift {
original_release_line: snapshot.original_rustc_release_line.clone(),
observed_release_line: String::new(),
});
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::tempdir;
fn write_dylib_stub(dir: &std::path::Path, contents: &[u8]) -> PathBuf {
let p = dir.join("libstub.so");
let mut f = std::fs::File::create(&p).unwrap();
f.write_all(contents).unwrap();
f.sync_all().unwrap();
p
}
#[test]
fn missing_dylib_fails_invariant_1() {
let snap = FreshnessSnapshot {
managed_dylib_path: PathBuf::from("/path/that/does/not/exist.so"),
original_mtime_unix_secs: 0,
original_sha256: "deadbeef".into(),
original_rustc_release_line: "rustc 1.95.0 (abc 2026-01-01)".into(),
};
let r = check(&snap).unwrap_err();
assert_eq!(r.invariant_label(), "managed_dylib_path");
}
#[test]
fn sha_mismatch_fails_invariant_3() {
let tmp = tempdir().unwrap();
let p = write_dylib_stub(tmp.path(), b"hello world");
let mtime = std::fs::metadata(&p)
.unwrap()
.modified()
.unwrap()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
let snap = FreshnessSnapshot {
managed_dylib_path: p.clone(),
original_mtime_unix_secs: mtime,
original_sha256: "0000000000000000000000000000000000000000000000000000000000000000"
.into(),
original_rustc_release_line: "rustc 1.95.0 (abc 2026-01-01)".into(),
};
let err = check(&snap).unwrap_err();
match &err {
FreshnessFailure::DylibShaMismatch { .. } => {}
other => panic!("expected DylibShaMismatch, got {other:?}"),
}
assert_eq!(err.invariant_label(), "dylib_sha256");
}
#[test]
fn detail_messages_are_byte_deterministic() {
let f = FreshnessFailure::DylibShaMismatch {
path: PathBuf::from("/p/lib.so"),
original_sha256: "abc".into(),
observed_sha256: "def".into(),
};
let a = f.detail();
let b = f.detail();
assert_eq!(a, b);
assert!(a.contains("/p/lib.so"));
assert!(a.contains("abc"));
assert!(a.contains("def"));
}
}