cyberbrain_embed/
artefact.rs1use cyberbrain_core::{Error, Result, Slash};
7use serde::{Deserialize, Serialize};
8use std::path::{Path, PathBuf};
9
10const HEX_LEN: usize = 64;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct ModelPaths {
16 pub weights: PathBuf,
18 pub tokenizer: PathBuf,
20}
21
22impl ModelPaths {
23 pub fn in_dir(dir: impl AsRef<Path>) -> Self {
25 let dir = dir.as_ref();
26 Self {
27 weights: dir.join("model.safetensors"),
28 tokenizer: dir.join("tokenizer.json"),
29 }
30 }
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
40pub struct ArtefactManifest {
41 pub weights_blake3: String,
42 pub tokenizer_blake3: String,
43}
44
45impl ArtefactManifest {
46 pub fn validate(&self) -> Result<()> {
49 for (what, hex) in [
50 ("weights_blake3", &self.weights_blake3),
51 ("tokenizer_blake3", &self.tokenizer_blake3),
52 ] {
53 if hex.len() != HEX_LEN
54 || !hex
55 .bytes()
56 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
57 {
58 return Err(Error::Config(format!(
59 "model manifest: {what} must be {HEX_LEN} lowercase hex characters, \
60 got {:?}",
61 hex
62 )));
63 }
64 }
65 Ok(())
66 }
67}
68
69pub fn hash_bytes(bytes: &[u8]) -> String {
71 blake3::hash(bytes).to_hex().to_string()
72}
73
74pub fn hash_file(path: impl AsRef<Path>) -> Result<String> {
77 let bytes = read(path.as_ref())?;
78 Ok(hash_bytes(&bytes))
79}
80
81pub(crate) fn read(path: &Path) -> Result<Vec<u8>> {
82 std::fs::read(path).map_err(|source| Error::Io {
83 path: path.to_path_buf(),
84 source,
85 })
86}
87
88pub(crate) fn read_verified(path: &Path, expected_hex: &str, what: &str) -> Result<Vec<u8>> {
91 let bytes = read(path)?;
92 let actual = hash_bytes(&bytes);
93 if actual != expected_hex {
94 return Err(Error::Embed(format!(
95 "{what} artefact {} does not match its manifest: expected blake3 {expected_hex}, \
96 file has {actual}; refusing to load (SPEC §6: a hash mismatch is a hard failure)",
97 Slash(path)
98 )));
99 }
100 Ok(bytes)
101}
102
103pub const VERIFIED_FILE: &str = ".verified";
105
106const RACY_NS: u128 = 2_000_000_000;
108
109#[derive(Debug, Clone, PartialEq, Eq)]
111struct Stamp {
112 len: u64,
113 mtime_ns: u128,
114 ino: u64,
115}
116
117fn stamp(path: &Path) -> Option<Stamp> {
118 let meta = std::fs::metadata(path).ok()?;
119 let mtime_ns = meta
120 .modified()
121 .ok()?
122 .duration_since(std::time::UNIX_EPOCH)
123 .ok()?
124 .as_nanos();
125 #[cfg(unix)]
126 let ino = std::os::unix::fs::MetadataExt::ino(&meta);
127 #[cfg(not(unix))]
128 let ino = 0;
129 Some(Stamp {
130 len: meta.len(),
131 mtime_ns,
132 ino,
133 })
134}
135
136pub(crate) fn map_verified(
151 path: &Path,
152 expected_hex: &str,
153 what: &str,
154) -> Result<(memmap2::Mmap, bool)> {
155 let file = std::fs::File::open(path).map_err(|source| Error::Io {
156 path: path.to_path_buf(),
157 source,
158 })?;
159 let map = unsafe { memmap2::Mmap::map(&file) }.map_err(|source| Error::Io {
163 path: path.to_path_buf(),
164 source,
165 })?;
166 if recorded(path).is_some_and(|d| d == expected_hex) {
167 return Ok((map, true));
168 }
169 let actual = blake3::Hasher::new()
170 .update_rayon(&map)
171 .finalize()
172 .to_hex()
173 .to_string();
174 if actual != expected_hex {
175 return Err(Error::Embed(format!(
176 "{what} artefact {} does not match its manifest: expected blake3 {expected_hex}, \
177 file has {actual}; refusing to load (SPEC §6: a hash mismatch is a hard failure)",
178 Slash(path)
179 )));
180 }
181 Ok((map, false))
182}
183
184fn recorded(path: &Path) -> Option<String> {
186 let name = path.file_name()?.to_str()?;
187 let now = stamp(path)?;
188 let text = std::fs::read_to_string(path.with_file_name(VERIFIED_FILE)).ok()?;
189 text.lines().find_map(|line| {
190 let f: Vec<&str> = line.split('\t').collect();
191 let [n, digest, len, mtime, ino] = f.as_slice() else {
192 return None;
193 };
194 let then = Stamp {
195 len: len.parse().ok()?,
196 mtime_ns: mtime.parse().ok()?,
197 ino: ino.parse().ok()?,
198 };
199 (*n == name && then == now && digest.len() == HEX_LEN).then(|| digest.to_string())
200 })
201}
202
203pub(crate) fn record_verified(path: &Path, digest: &str) {
206 let (Some(name), Some(now)) = (path.file_name().and_then(|n| n.to_str()), stamp(path)) else {
207 return;
208 };
209 let since = std::time::SystemTime::now()
215 .duration_since(std::time::UNIX_EPOCH)
216 .map(|d| d.as_nanos())
217 .unwrap_or(0);
218 if since.saturating_sub(now.mtime_ns) < RACY_NS {
219 return;
220 }
221 let record = path.with_file_name(VERIFIED_FILE);
222 let mut lines: Vec<String> = std::fs::read_to_string(&record)
223 .unwrap_or_default()
224 .lines()
225 .filter(|l| l.split('\t').next() != Some(name))
226 .map(str::to_string)
227 .collect();
228 lines.push(format!(
229 "{name}\t{digest}\t{}\t{}\t{}",
230 now.len, now.mtime_ns, now.ino
231 ));
232 let tmp = record.with_extension("tmp");
233 if std::fs::write(&tmp, lines.join("\n") + "\n").is_ok() {
234 let _ = std::fs::rename(&tmp, &record);
235 }
236}