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)
}