use cyberbrain_core::{Error, Result, Slash};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
const HEX_LEN: usize = 64;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModelPaths {
pub weights: PathBuf,
pub tokenizer: PathBuf,
}
impl ModelPaths {
pub fn in_dir(dir: impl AsRef<Path>) -> Self {
let dir = dir.as_ref();
Self {
weights: dir.join("model.safetensors"),
tokenizer: dir.join("tokenizer.json"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArtefactManifest {
pub weights_blake3: String,
pub tokenizer_blake3: String,
}
impl ArtefactManifest {
pub fn validate(&self) -> Result<()> {
for (what, hex) in [
("weights_blake3", &self.weights_blake3),
("tokenizer_blake3", &self.tokenizer_blake3),
] {
if hex.len() != HEX_LEN
|| !hex
.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
{
return Err(Error::Config(format!(
"model manifest: {what} must be {HEX_LEN} lowercase hex characters, \
got {:?}",
hex
)));
}
}
Ok(())
}
}
pub fn hash_bytes(bytes: &[u8]) -> String {
blake3::hash(bytes).to_hex().to_string()
}
pub fn hash_file(path: impl AsRef<Path>) -> Result<String> {
let bytes = read(path.as_ref())?;
Ok(hash_bytes(&bytes))
}
pub(crate) fn read(path: &Path) -> Result<Vec<u8>> {
std::fs::read(path).map_err(|source| Error::Io {
path: path.to_path_buf(),
source,
})
}
pub(crate) fn read_verified(path: &Path, expected_hex: &str, what: &str) -> Result<Vec<u8>> {
let bytes = read(path)?;
let actual = hash_bytes(&bytes);
if actual != expected_hex {
return Err(Error::Embed(format!(
"{what} artefact {} does not match its manifest: expected blake3 {expected_hex}, \
file has {actual}; refusing to load (SPEC §6: a hash mismatch is a hard failure)",
Slash(path)
)));
}
Ok(bytes)
}
pub const VERIFIED_FILE: &str = ".verified";
const RACY_NS: u128 = 2_000_000_000;
#[derive(Debug, Clone, PartialEq, Eq)]
struct Stamp {
len: u64,
mtime_ns: u128,
ino: u64,
}
fn stamp(path: &Path) -> Option<Stamp> {
let meta = std::fs::metadata(path).ok()?;
let mtime_ns = meta
.modified()
.ok()?
.duration_since(std::time::UNIX_EPOCH)
.ok()?
.as_nanos();
#[cfg(unix)]
let ino = std::os::unix::fs::MetadataExt::ino(&meta);
#[cfg(not(unix))]
let ino = 0;
Some(Stamp {
len: meta.len(),
mtime_ns,
ino,
})
}
pub(crate) fn map_verified(
path: &Path,
expected_hex: &str,
what: &str,
) -> Result<(memmap2::Mmap, bool)> {
let file = std::fs::File::open(path).map_err(|source| Error::Io {
path: path.to_path_buf(),
source,
})?;
let map = unsafe { memmap2::Mmap::map(&file) }.map_err(|source| Error::Io {
path: path.to_path_buf(),
source,
})?;
if recorded(path).is_some_and(|d| d == expected_hex) {
return Ok((map, true));
}
let actual = blake3::Hasher::new()
.update_rayon(&map)
.finalize()
.to_hex()
.to_string();
if actual != expected_hex {
return Err(Error::Embed(format!(
"{what} artefact {} does not match its manifest: expected blake3 {expected_hex}, \
file has {actual}; refusing to load (SPEC §6: a hash mismatch is a hard failure)",
Slash(path)
)));
}
Ok((map, false))
}
fn recorded(path: &Path) -> Option<String> {
let name = path.file_name()?.to_str()?;
let now = stamp(path)?;
let text = std::fs::read_to_string(path.with_file_name(VERIFIED_FILE)).ok()?;
text.lines().find_map(|line| {
let f: Vec<&str> = line.split('\t').collect();
let [n, digest, len, mtime, ino] = f.as_slice() else {
return None;
};
let then = Stamp {
len: len.parse().ok()?,
mtime_ns: mtime.parse().ok()?,
ino: ino.parse().ok()?,
};
(*n == name && then == now && digest.len() == HEX_LEN).then(|| digest.to_string())
})
}
pub(crate) fn record_verified(path: &Path, digest: &str) {
let (Some(name), Some(now)) = (path.file_name().and_then(|n| n.to_str()), stamp(path)) else {
return;
};
let since = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
if since.saturating_sub(now.mtime_ns) < RACY_NS {
return;
}
let record = path.with_file_name(VERIFIED_FILE);
let mut lines: Vec<String> = std::fs::read_to_string(&record)
.unwrap_or_default()
.lines()
.filter(|l| l.split('\t').next() != Some(name))
.map(str::to_string)
.collect();
lines.push(format!(
"{name}\t{digest}\t{}\t{}\t{}",
now.len, now.mtime_ns, now.ino
));
let tmp = record.with_extension("tmp");
if std::fs::write(&tmp, lines.join("\n") + "\n").is_ok() {
let _ = std::fs::rename(&tmp, &record);
}
}