use std::fmt;
use std::fs;
use std::path::Path;
use crate::error::{Error, Result};
const MAGIC_GENERATION_3: u32 = 0x05E1_2A84;
#[derive(Clone, PartialEq, Eq)]
pub struct Weights {
bytes: Vec<u8>,
generation: u8,
}
impl Weights {
pub fn from_bytes<B>(bytes: B) -> Result<Self>
where
B: Into<Vec<u8>>,
{
let bytes = bytes.into();
let Some(header) = bytes.get(..4) else {
return Err(Error::UnsupportedWeights { tag: 0 });
};
let tag = u32::from_le_bytes([header[0], header[1], header[2], header[3]]);
if tag == MAGIC_GENERATION_3 {
Ok(Weights {
bytes,
generation: 3,
})
} else {
Err(Error::UnsupportedWeights { tag })
}
}
pub fn from_file<P>(path: P) -> Result<Self>
where
P: AsRef<Path>,
{
Self::from_bytes(fs::read(path.as_ref())?)
}
#[inline]
#[must_use]
pub fn len(&self) -> usize {
self.bytes.len()
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.bytes.is_empty()
}
#[inline]
#[must_use]
pub const fn generation(&self) -> u8 {
self.generation
}
pub(crate) fn as_bytes(&self) -> &[u8] {
&self.bytes
}
}
impl fmt::Debug for Weights {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Weights")
.field("generation", &self.generation)
.field("len", &self.bytes.len())
.finish()
}
}
#[cfg(feature = "download")]
#[cfg_attr(docsrs, doc(cfg(feature = "download")))]
pub use fetch::WEIGHTS_SHA256;
#[cfg(feature = "download")]
mod fetch {
use std::env;
use std::fmt::Write as _;
use std::fs;
use std::io::Read as _;
use std::path::PathBuf;
use sha2::{Digest as _, Sha256};
use super::Weights;
use crate::error::{Error, Result};
const ARCHIVE: &str = "needle3.cact";
const WEIGHTS_ENV: &str = "CACTUS_NEEDLE_WEIGHTS";
pub const WEIGHTS_SHA256: &str =
"c9d915eca282ed42d1a09b143b592adb4cc6744ffe2d294adf5cfc5548170c38";
const MAX_BODY: u64 = 256 * 1024 * 1024;
impl Weights {
#[cfg_attr(docsrs, doc(cfg(feature = "download")))]
pub fn fetch() -> Result<Self> {
if let Some(path) = env::var_os(WEIGHTS_ENV) {
return Weights::from_file(path);
}
let cached = cache_path();
if let Ok(bytes) = fs::read(&cached) {
if digest(&bytes) == WEIGHTS_SHA256 {
return Weights::from_bytes(bytes);
}
}
let url = format!(
"https://huggingface.co/Cactus-Compute/needle3/resolve/{}/{ARCHIVE}",
cactus_sys::NEEDLE_ENGINE_COMMIT
);
let bytes = download(&url)?;
let actual = digest(&bytes);
if actual != WEIGHTS_SHA256 {
return Err(Error::ChecksumMismatch {
expected: WEIGHTS_SHA256.to_owned(),
actual,
});
}
let _ = install(&cached, &bytes);
Weights::from_bytes(bytes)
}
}
fn cache_path() -> PathBuf {
let base = if cfg!(windows) {
env::var_os("LOCALAPPDATA").map(PathBuf::from)
} else {
env::var_os("XDG_CACHE_HOME")
.map(PathBuf::from)
.or_else(|| env::var_os("HOME").map(|home| PathBuf::from(home).join(".cache")))
}
.unwrap_or_else(env::temp_dir);
let commit = cactus_sys::NEEDLE_ENGINE_COMMIT;
base.join("cactus-rs")
.join("needle3")
.join(commit.get(..12).unwrap_or(commit))
.join(ARCHIVE)
}
fn install(path: &std::path::Path, bytes: &[u8]) -> Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |elapsed| elapsed.as_nanos());
let partial = path.with_extension(format!("cact.{}.{nanos}.partial", std::process::id()));
let written = fs::write(&partial, bytes).and_then(|()| fs::rename(&partial, path));
if written.is_err() {
let _ = fs::remove_file(&partial);
}
Ok(written?)
}
fn download(url: &str) -> Result<Vec<u8>> {
let config = ureq::Agent::config_builder()
.user_agent(concat!("cactus-rs/", env!("CARGO_PKG_VERSION")))
.build();
let agent = ureq::Agent::new_with_config(config);
let mut response = agent.get(url).call().map_err(|error| Error::Download {
url: url.to_owned(),
detail: error.to_string(),
})?;
let mut bytes = Vec::new();
response
.body_mut()
.with_config()
.limit(MAX_BODY)
.reader()
.read_to_end(&mut bytes)
.map_err(|error| Error::Download {
url: url.to_owned(),
detail: error.to_string(),
})?;
Ok(bytes)
}
fn digest(bytes: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(bytes);
let mut hex = String::with_capacity(64);
for byte in hasher.finalize() {
let _ = write!(hex, "{byte:02x}");
}
hex
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn digest_matches_a_known_vector() {
assert_eq!(
digest(b"abc"),
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
);
}
#[test]
fn cache_path_carries_the_pin() {
let text = cache_path().display().to_string();
assert!(text.contains("cactus-rs"));
assert!(text.contains("needle3"));
assert!(text.contains(&cactus_sys::NEEDLE_ENGINE_COMMIT[..12]));
assert!(text.ends_with(ARCHIVE));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generation_three_is_accepted() {
let weights = Weights::from_bytes(vec![0x84, 0x2A, 0xE1, 0x05, 0x45]).expect("gen 3 tag");
assert_eq!(weights.generation(), 3);
assert_eq!(weights.len(), 5);
}
const MAGIC_GENERATION_2: u32 = 0x05E1_2A83;
#[test]
fn generation_two_is_rejected_by_tag() {
let error = Weights::from_bytes(vec![0x83, 0x2A, 0xE1, 0x05]).expect_err("gen 2 tag");
assert!(matches!(
error,
Error::UnsupportedWeights {
tag: MAGIC_GENERATION_2
}
));
}
#[test]
fn a_short_file_is_not_an_archive() {
let error = Weights::from_bytes(vec![0x84, 0x2A]).expect_err("truncated header");
assert!(matches!(error, Error::UnsupportedWeights { tag: 0 }));
}
#[test]
fn debug_does_not_dump_the_bytes() {
let weights = Weights::from_bytes(vec![0x84, 0x2A, 0xE1, 0x05, 0xAB]).expect("gen 3 tag");
let text = format!("{weights:?}");
assert_eq!(text, "Weights { generation: 3, len: 5 }");
}
}