use std::collections::HashMap;
use std::fs::File;
use std::io::{BufReader, BufWriter, Read, Write};
use std::path::PathBuf;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tracing::warn;
use zeroize::Zeroizing;
const STREAM_BUF_LEN: usize = 64 * 1024;
pub const ARTIFACT_MAGIC: [u8; 16] = *b"twasm-artifact01";
pub const ARTIFACT_VERSION: u32 = 1;
pub const ARTIFACT_HMAC_LEN: usize = 32;
pub const ARTIFACT_HEADER_LEN: usize = 16 + 4 + 32;
pub const DEFAULT_ZSTD_LEVEL: i32 = 3;
pub const MAX_PAYLOAD_LEN: usize = 256 * 1024 * 1024;
pub const MAX_DECOMPRESSED_LEN: usize = MAX_PAYLOAD_LEN + 8 * 1024;
#[derive(Debug, Error)]
pub enum ArtifactError {
#[error("artifact not found: {0}")]
NotFound(String),
#[error("magic mismatch")]
BadMagic,
#[error("unsupported version: {0}")]
BadVersion(u32),
#[error("HMAC verification failed")]
BadHmac,
#[error("content hash mismatch (expected {expected}, got {actual})")]
HashMismatch { expected: String, actual: String },
#[error("zstd decompression failed: {0}")]
Decompression(String),
#[error("artifact metadata codec error: {0}")]
Metadata(String),
#[error("artifact too large: {actual} bytes exceeds cap of {limit} bytes")]
TooLarge { actual: usize, limit: usize },
#[error("I/O error")]
Io,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ContentHash([u8; 32]);
impl std::fmt::Display for ContentHash {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for b in &self.0 {
write!(f, "{:02x}", b)?;
}
Ok(())
}
}
impl ContentHash {
pub fn of(payload: &[u8]) -> Self {
ContentHash(blake3::hash(payload).into())
}
pub fn from_bytes(bytes: [u8; 32]) -> Self {
ContentHash(bytes)
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
pub fn to_hex(&self) -> String {
format!("{self}")
}
}
pub trait ArtifactStore: Send + Sync {
fn put(&self, payload: &[u8]) -> Result<ContentHash, ArtifactError>;
fn get(&self, hash: &ContentHash) -> Result<Vec<u8>, ArtifactError>;
fn get_to(&self, hash: &ContentHash, out: &mut dyn Write) -> Result<u64, ArtifactError> {
let bytes = self.get(hash)?;
out.write_all(&bytes).map_err(|e| {
warn!(target: "tensor_wasm_artifacts", error = %e, "get_to default writer write failed");
ArtifactError::Io
})?;
Ok(bytes.len() as u64)
}
fn list(&self) -> Result<Vec<ContentHash>, ArtifactError>;
fn contains(&self, hash: &ContentHash) -> Result<bool, ArtifactError>;
fn remove(&self, hash: &ContentHash) -> Result<bool, ArtifactError>;
}
pub trait KeyProvider: Send + Sync {
fn active_key(&self) -> [u8; 32];
fn read_keys(&self) -> Vec<[u8; 32]>;
}
pub struct SingleKeyProvider {
key: Zeroizing<[u8; 32]>,
}
impl SingleKeyProvider {
pub fn new(key: [u8; 32]) -> Self {
Self {
key: Zeroizing::new(key),
}
}
}
impl KeyProvider for SingleKeyProvider {
fn active_key(&self) -> [u8; 32] {
*self.key
}
fn read_keys(&self) -> Vec<[u8; 32]> {
vec![*self.key]
}
}
pub struct RotatingKeyProvider {
active: Zeroizing<[u8; 32]>,
also_accept: Vec<Zeroizing<[u8; 32]>>,
}
impl RotatingKeyProvider {
pub fn new(active: [u8; 32], also_accept: impl IntoIterator<Item = [u8; 32]>) -> Self {
Self {
active: Zeroizing::new(active),
also_accept: also_accept.into_iter().map(Zeroizing::new).collect(),
}
}
}
impl KeyProvider for RotatingKeyProvider {
fn active_key(&self) -> [u8; 32] {
*self.active
}
fn read_keys(&self) -> Vec<[u8; 32]> {
let mut keys = Vec::with_capacity(1 + self.also_accept.len());
keys.push(*self.active);
keys.extend(self.also_accept.iter().map(|k| **k));
keys
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArtifactMetadata {
pub created_unix_ms: u64,
pub original_len: u64,
pub source_tier: String,
}
type ArtifactMac = hmac::Hmac<sha2::Sha256>;
fn new_mac(key: &[u8; 32]) -> ArtifactMac {
use hmac::Mac;
<ArtifactMac as Mac>::new_from_slice(&key[..]).expect("HMAC-SHA256 accepts any 32-byte key")
}
fn finalize_into_tag(mac: ArtifactMac) -> [u8; ARTIFACT_HMAC_LEN] {
use hmac::Mac;
let out = mac.finalize().into_bytes();
let mut tag = [0u8; ARTIFACT_HMAC_LEN];
tag.copy_from_slice(out.as_slice());
tag
}
fn key_fingerprint_hex(key: &[u8; 32]) -> String {
let h = blake3::hash(&key[..]);
h.as_bytes()[..8]
.iter()
.map(|b| format!("{:02x}", b))
.collect()
}
struct MacWriter<'a, W: Write> {
inner: W,
mac: &'a mut ArtifactMac,
}
impl<'a, W: Write> MacWriter<'a, W> {
fn new(inner: W, mac: &'a mut ArtifactMac) -> Self {
Self { inner, mac }
}
}
impl<W: Write> Write for MacWriter<'_, W> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let n = self.inner.write(buf)?;
use hmac::Mac;
self.mac.update(&buf[..n]);
Ok(n)
}
fn flush(&mut self) -> std::io::Result<()> {
self.inner.flush()
}
}
struct MacReader<'a, R: Read> {
inner: R,
mac: &'a mut ArtifactMac,
}
impl<'a, R: Read> MacReader<'a, R> {
fn new(inner: R, mac: &'a mut ArtifactMac) -> Self {
Self { inner, mac }
}
}
impl<R: Read> Read for MacReader<'_, R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let n = self.inner.read(buf)?;
use hmac::Mac;
self.mac.update(&buf[..n]);
Ok(n)
}
}
pub struct DiskArtifactStore {
dir: PathBuf,
hmac_key: Zeroizing<[u8; 32]>,
key_fp_hex: String,
key_provider: Arc<dyn KeyProvider>,
}
impl DiskArtifactStore {
pub fn new(dir: PathBuf, hmac_key: [u8; 32]) -> Self {
Self::with_key_provider(dir, Arc::new(SingleKeyProvider::new(hmac_key)))
}
pub fn with_key_provider(dir: PathBuf, key_provider: Arc<dyn KeyProvider>) -> Self {
let active = key_provider.active_key();
let key_fp_hex = key_fingerprint_hex(&active);
Self {
dir,
hmac_key: Zeroizing::new(active),
key_fp_hex,
key_provider,
}
}
fn path_for(&self, hash: &ContentHash) -> PathBuf {
self.path_for_key(hash, &self.key_fp_hex)
}
fn path_for_key(&self, hash: &ContentHash, key_fp_hex: &str) -> PathBuf {
let hash_hex = hash.to_string();
debug_assert!(
hash_hex.len() == 64
&& hash_hex
.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)),
"ContentHash rendered to an unexpected filename segment: {hash_hex:?}"
);
self.dir.join(format!("{hash_hex}.{key_fp_hex}.bin"))
}
fn meta_path_for(&self, hash: &ContentHash) -> PathBuf {
self.meta_path_for_key(hash, &self.key_fp_hex)
}
fn meta_path_for_key(&self, hash: &ContentHash, key_fp_hex: &str) -> PathBuf {
let hash_hex = hash.to_string();
self.dir.join(format!("{hash_hex}.{key_fp_hex}.meta.json"))
}
pub fn put_with_metadata(
&self,
payload: &[u8],
metadata: &ArtifactMetadata,
) -> Result<ContentHash, ArtifactError> {
let hash = self.put(payload)?;
let encoded = serde_json::to_vec(metadata).map_err(|e| {
warn!(target: "tensor_wasm_artifacts", error = %e, "metadata serialize failed");
ArtifactError::Metadata(e.to_string())
})?;
let meta_path = self.meta_path_for(&hash);
let mut tmp = tempfile::NamedTempFile::new_in(&self.dir).map_err(|e| {
warn!(target: "tensor_wasm_artifacts", error = %e, "metadata tempfile create failed");
ArtifactError::Io
})?;
tmp.write_all(&encoded).map_err(|e| {
warn!(target: "tensor_wasm_artifacts", error = %e, "metadata write failed");
ArtifactError::Io
})?;
tmp.as_file().sync_all().map_err(|e| {
warn!(target: "tensor_wasm_artifacts", error = %e, "metadata fsync (pre-persist) failed");
ArtifactError::Io
})?;
tmp.persist(&meta_path).map_err(|e| {
warn!(target: "tensor_wasm_artifacts", error = %e, "metadata persist failed");
ArtifactError::Io
})?;
self.sync_dir_best_effort();
Ok(hash)
}
pub fn metadata(&self, hash: &ContentHash) -> Result<ArtifactMetadata, ArtifactError> {
let (_blob_path, key) = match self.resolve_read_key(hash)? {
Some(found) => found,
None => return Err(ArtifactError::NotFound(hash.to_string())),
};
let fp = key_fingerprint_hex(&key);
let meta_path = self.meta_path_for_key(hash, &fp);
let bytes = match std::fs::read(&meta_path) {
Ok(b) => b,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Err(ArtifactError::NotFound(hash.to_string()));
}
Err(e) => {
warn!(
target: "tensor_wasm_artifacts",
file = %meta_path.display(),
error = %e,
"metadata read failed"
);
return Err(ArtifactError::Io);
}
};
serde_json::from_slice(&bytes).map_err(|e| {
warn!(target: "tensor_wasm_artifacts", error = %e, "metadata deserialize failed");
ArtifactError::Metadata(e.to_string())
})
}
fn verify_blob(
&self,
reader: &mut BufReader<File>,
file_len: u64,
key: &[u8; 32],
path: &std::path::Path,
) -> Result<VerifiedBlob, ArtifactError> {
let min_len = (ARTIFACT_HEADER_LEN + ARTIFACT_HMAC_LEN) as u64;
if file_len < min_len {
return Err(ArtifactError::BadMagic);
}
let prefix_end = file_len - ARTIFACT_HMAC_LEN as u64;
let body_len = prefix_end - ARTIFACT_HEADER_LEN as u64;
let mut mac = new_mac(key);
let mut header = [0u8; ARTIFACT_HEADER_LEN];
reader.read_exact(&mut header).map_err(|e| {
warn!(target: "tensor_wasm_artifacts", file = %path.display(), error = %e, "header read failed");
ArtifactError::Io
})?;
if header[..16] != ARTIFACT_MAGIC {
return Err(ArtifactError::BadMagic);
}
let version = u32::from_le_bytes([header[16], header[17], header[18], header[19]]);
if version != ARTIFACT_VERSION {
return Err(ArtifactError::BadVersion(version));
}
let mut hash_on_disk = [0u8; 32];
hash_on_disk.copy_from_slice(&header[20..52]);
{
use hmac::Mac;
mac.update(&header);
}
{
let mut body_take = Read::take(&mut *reader, body_len);
let mut scratch = [0u8; STREAM_BUF_LEN];
loop {
let n = body_take.read(&mut scratch).map_err(|e| {
warn!(target: "tensor_wasm_artifacts", file = %path.display(), error = %e, "body read failed");
ArtifactError::Io
})?;
if n == 0 {
break;
}
use hmac::Mac;
mac.update(&scratch[..n]);
}
}
let mut tag_bytes = [0u8; ARTIFACT_HMAC_LEN];
reader.read_exact(&mut tag_bytes).map_err(|e| {
warn!(target: "tensor_wasm_artifacts", file = %path.display(), error = %e, "tag read failed");
ArtifactError::Io
})?;
let expected = finalize_into_tag(mac);
use subtle::ConstantTimeEq;
if !bool::from(expected.as_slice().ct_eq(&tag_bytes[..])) {
warn!(target: "tensor_wasm_artifacts", file = %path.display(), "HMAC mismatch (get_to verify pass)");
return Err(ArtifactError::BadHmac);
}
Ok(VerifiedBlob {
hash_on_disk,
body_len,
})
}
fn decode_to_writer(
&self,
reader: &mut BufReader<File>,
verified: &VerifiedBlob,
requested: &ContentHash,
out: &mut dyn Write,
path: &std::path::Path,
) -> Result<u64, ArtifactError> {
let mut header = [0u8; ARTIFACT_HEADER_LEN];
reader.read_exact(&mut header).map_err(|e| {
warn!(target: "tensor_wasm_artifacts", error = %e, "header reread failed (decode pass)");
ArtifactError::Io
})?;
let cap = MAX_DECOMPRESSED_LEN;
let probe_limit = u64::try_from(cap)
.ok()
.and_then(|c| c.checked_add(1))
.unwrap_or(u64::MAX);
let body_take = Read::take(&mut *reader, verified.body_len);
let decoder = zstd::stream::read::Decoder::new(body_take).map_err(|e| {
warn!(target: "tensor_wasm_artifacts", error = %e, "zstd init failed (decode pass)");
ArtifactError::Decompression(e.to_string())
})?;
let mut sink = HashingWriter::new(out);
let copied = std::io::copy(&mut Read::take(decoder, probe_limit), &mut sink);
let written = match copied {
Ok(n) => n,
Err(e) => {
if sink.downstream_failed {
warn!(target: "tensor_wasm_artifacts", error = %e, "get_to writer write failed");
return Err(ArtifactError::Io);
}
warn!(target: "tensor_wasm_artifacts", error = %e, "zstd decode failed (decode pass)");
return Err(ArtifactError::Decompression(e.to_string()));
}
};
if written > cap as u64 {
warn!(
target: "tensor_wasm_artifacts",
file = %path.display(),
actual = written,
limit = cap,
"rejecting oversized decompressed payload (possible zstd bomb)"
);
return Err(ArtifactError::TooLarge {
actual: written as usize,
limit: cap,
});
}
let recomputed: [u8; 32] = sink.hasher.finalize().into();
if recomputed != verified.hash_on_disk {
return Err(ArtifactError::HashMismatch {
expected: hex_of(&verified.hash_on_disk),
actual: hex_of(&recomputed),
});
}
if recomputed != *requested.as_bytes() {
return Err(ArtifactError::HashMismatch {
expected: requested.to_string(),
actual: hex_of(&recomputed),
});
}
Ok(written)
}
fn resolve_read_key(
&self,
hash: &ContentHash,
) -> Result<Option<(PathBuf, [u8; 32])>, ArtifactError> {
for key in self.key_provider.read_keys() {
let fp = key_fingerprint_hex(&key);
let path = self.path_for_key(hash, &fp);
match std::fs::metadata(&path) {
Ok(_) => return Ok(Some((path, key))),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
Err(e) => {
warn!(
target: "tensor_wasm_artifacts",
file = %path.display(),
error = %e,
"resolve_read_key metadata probe failed"
);
return Err(ArtifactError::Io);
}
}
}
Ok(None)
}
fn sync_dir_best_effort(&self) {
match File::open(&self.dir).and_then(|d| d.sync_all()) {
Ok(()) => {}
Err(e) => {
tracing::debug!(
target: "tensor_wasm_artifacts",
dir = %self.dir.display(),
error = %e,
"directory fsync skipped (best-effort; unsupported on this platform?)"
);
}
}
}
}
struct VerifiedBlob {
hash_on_disk: [u8; 32],
body_len: u64,
}
struct HashingWriter<'a> {
inner: &'a mut dyn Write,
hasher: blake3::Hasher,
downstream_failed: bool,
}
impl<'a> HashingWriter<'a> {
fn new(inner: &'a mut dyn Write) -> Self {
Self {
inner,
hasher: blake3::Hasher::new(),
downstream_failed: false,
}
}
}
impl Write for HashingWriter<'_> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
match self.inner.write(buf) {
Ok(n) => {
self.hasher.update(&buf[..n]);
Ok(n)
}
Err(e) => {
self.downstream_failed = true;
Err(e)
}
}
}
fn flush(&mut self) -> std::io::Result<()> {
self.inner.flush()
}
}
impl ArtifactStore for DiskArtifactStore {
fn put(&self, payload: &[u8]) -> Result<ContentHash, ArtifactError> {
if payload.len() > MAX_PAYLOAD_LEN {
warn!(
target: "tensor_wasm_artifacts",
actual = payload.len(),
limit = MAX_PAYLOAD_LEN,
"rejecting oversized payload"
);
return Err(ArtifactError::TooLarge {
actual: payload.len(),
limit: MAX_PAYLOAD_LEN,
});
}
std::fs::create_dir_all(&self.dir).map_err(|e| {
warn!(
target: "tensor_wasm_artifacts",
dir = %self.dir.display(),
error = %e,
"create_dir_all failed"
);
ArtifactError::Io
})?;
let hash = ContentHash::of(payload);
let final_path = self.path_for(&hash);
let mut tmp = tempfile::NamedTempFile::new_in(&self.dir).map_err(|e| {
warn!(target: "tensor_wasm_artifacts", error = %e, "tempfile create failed");
ArtifactError::Io
})?;
let mut mac = new_mac(&self.hmac_key);
{
let buf_writer = BufWriter::with_capacity(STREAM_BUF_LEN, tmp.as_file_mut());
let mut tee = MacWriter::new(buf_writer, &mut mac);
tee.write_all(&ARTIFACT_MAGIC).map_err(|e| {
warn!(target: "tensor_wasm_artifacts", error = %e, "header magic write failed");
ArtifactError::Io
})?;
tee.write_all(&ARTIFACT_VERSION.to_le_bytes()).map_err(|e| {
warn!(target: "tensor_wasm_artifacts", error = %e, "header version write failed");
ArtifactError::Io
})?;
tee.write_all(&hash.0).map_err(|e| {
warn!(target: "tensor_wasm_artifacts", error = %e, "header hash write failed");
ArtifactError::Io
})?;
let mut encoder = zstd::stream::write::Encoder::new(&mut tee, DEFAULT_ZSTD_LEVEL)
.map_err(|e| {
warn!(target: "tensor_wasm_artifacts", error = %e, "zstd init failed");
ArtifactError::Io
})?;
encoder.write_all(payload).map_err(|e| {
warn!(target: "tensor_wasm_artifacts", error = %e, "zstd write failed");
ArtifactError::Io
})?;
encoder.finish().map_err(|e| {
warn!(target: "tensor_wasm_artifacts", error = %e, "zstd finish failed");
ArtifactError::Io
})?;
tee.flush().map_err(|e| {
warn!(target: "tensor_wasm_artifacts", error = %e, "buf flush failed");
ArtifactError::Io
})?;
}
let tag = finalize_into_tag(mac);
tmp.as_file_mut().write_all(&tag).map_err(|e| {
warn!(target: "tensor_wasm_artifacts", error = %e, "hmac tag write failed");
ArtifactError::Io
})?;
tmp.as_file().sync_all().map_err(|e| {
warn!(target: "tensor_wasm_artifacts", error = %e, "blob fsync (pre-persist) failed");
ArtifactError::Io
})?;
tmp.persist(&final_path).map_err(|e| {
warn!(target: "tensor_wasm_artifacts", error = %e, "tempfile persist failed");
ArtifactError::Io
})?;
self.sync_dir_best_effort();
Ok(hash)
}
fn get(&self, hash: &ContentHash) -> Result<Vec<u8>, ArtifactError> {
let (path, key) = match self.resolve_read_key(hash)? {
Some(found) => found,
None => return Err(ArtifactError::NotFound(hash.to_string())),
};
let file = match File::open(&path) {
Ok(f) => f,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Err(ArtifactError::NotFound(hash.to_string()));
}
Err(e) => {
warn!(
target: "tensor_wasm_artifacts",
file = %path.display(),
error = %e,
"open failed"
);
return Err(ArtifactError::Io);
}
};
let file_len = file
.metadata()
.map_err(|e| {
warn!(
target: "tensor_wasm_artifacts",
file = %path.display(),
error = %e,
"metadata failed"
);
ArtifactError::Io
})?
.len();
let min_len = (ARTIFACT_HEADER_LEN + ARTIFACT_HMAC_LEN) as u64;
if file_len < min_len {
warn!(
target: "tensor_wasm_artifacts",
file = %path.display(),
len = file_len,
"artifact too short for header+hmac"
);
return Err(ArtifactError::BadMagic);
}
let prefix_end = file_len - ARTIFACT_HMAC_LEN as u64;
let body_len = prefix_end - ARTIFACT_HEADER_LEN as u64;
let mut reader = BufReader::with_capacity(STREAM_BUF_LEN, file);
let mut mac = new_mac(&key);
let mut header = [0u8; ARTIFACT_HEADER_LEN];
reader.read_exact(&mut header).map_err(|e| {
warn!(
target: "tensor_wasm_artifacts",
file = %path.display(),
error = %e,
"header read failed"
);
ArtifactError::Io
})?;
if header[..16] != ARTIFACT_MAGIC {
return Err(ArtifactError::BadMagic);
}
let version = u32::from_le_bytes([header[16], header[17], header[18], header[19]]);
if version != ARTIFACT_VERSION {
return Err(ArtifactError::BadVersion(version));
}
let mut hash_on_disk = [0u8; 32];
hash_on_disk.copy_from_slice(&header[20..52]);
{
use hmac::Mac;
mac.update(&header);
}
let cap = MAX_DECOMPRESSED_LEN;
let probe_limit = u64::try_from(cap)
.ok()
.and_then(|c| c.checked_add(1))
.unwrap_or(u64::MAX);
let initial_capacity = usize::try_from(body_len)
.unwrap_or(cap)
.saturating_mul(2)
.min(cap);
let mut payload: Vec<u8> = Vec::with_capacity(initial_capacity);
let mut decode_result: Result<(), ArtifactError> = Ok(());
{
let body_take = Read::take(&mut reader, body_len);
let mac_reader = MacReader::new(body_take, &mut mac);
match zstd::stream::read::Decoder::new(mac_reader) {
Ok(decoder) => {
if let Err(e) = decoder.take(probe_limit).read_to_end(&mut payload) {
warn!(target: "tensor_wasm_artifacts", error = %e, "zstd decode failed");
decode_result = Err(ArtifactError::Decompression(e.to_string()));
}
}
Err(e) => {
warn!(target: "tensor_wasm_artifacts", error = %e, "zstd init failed");
decode_result = Err(ArtifactError::Decompression(e.to_string()));
}
}
}
use std::io::Seek;
let consumed = reader.stream_position().map_err(|e| {
warn!(target: "tensor_wasm_artifacts", error = %e, "stream_position failed");
ArtifactError::Io
})?;
if consumed < prefix_end {
let gap = prefix_end - consumed;
let drain_take = Read::take(&mut reader, gap);
let mut drain_mac = MacReader::new(drain_take, &mut mac);
let mut scratch = [0u8; STREAM_BUF_LEN];
loop {
let n = drain_mac.read(&mut scratch).map_err(|e| {
warn!(target: "tensor_wasm_artifacts", error = %e, "tail drain failed");
ArtifactError::Io
})?;
if n == 0 {
break;
}
}
} else if consumed > prefix_end {
reader
.seek(std::io::SeekFrom::Start(prefix_end))
.map_err(|e| {
warn!(target: "tensor_wasm_artifacts", error = %e, "seek to tag failed");
ArtifactError::Io
})?;
}
let mut tag_bytes = [0u8; ARTIFACT_HMAC_LEN];
reader.read_exact(&mut tag_bytes).map_err(|e| {
warn!(target: "tensor_wasm_artifacts", error = %e, "tag read failed");
ArtifactError::Io
})?;
let expected = finalize_into_tag(mac);
use subtle::ConstantTimeEq;
let mac_ok = bool::from(expected.as_slice().ct_eq(&tag_bytes[..]));
if !mac_ok {
warn!(
target: "tensor_wasm_artifacts",
file = %path.display(),
"HMAC mismatch (possible tampering or stale key)"
);
return Err(ArtifactError::BadHmac);
}
decode_result?;
if payload.len() > cap {
warn!(
target: "tensor_wasm_artifacts",
file = %path.display(),
actual = payload.len(),
limit = cap,
"rejecting oversized decompressed payload (possible zstd bomb)"
);
return Err(ArtifactError::TooLarge {
actual: payload.len(),
limit: cap,
});
}
let recomputed = ContentHash::of(&payload);
if recomputed.0 != hash_on_disk {
return Err(ArtifactError::HashMismatch {
expected: hex_of(&hash_on_disk),
actual: recomputed.to_string(),
});
}
if recomputed != *hash {
return Err(ArtifactError::HashMismatch {
expected: hash.to_string(),
actual: recomputed.to_string(),
});
}
Ok(payload)
}
fn get_to(&self, hash: &ContentHash, out: &mut dyn Write) -> Result<u64, ArtifactError> {
use std::io::{Seek, SeekFrom};
let (path, key) = match self.resolve_read_key(hash)? {
Some(found) => found,
None => return Err(ArtifactError::NotFound(hash.to_string())),
};
let file = match File::open(&path) {
Ok(f) => f,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Err(ArtifactError::NotFound(hash.to_string()));
}
Err(e) => {
warn!(target: "tensor_wasm_artifacts", file = %path.display(), error = %e, "open failed (get_to)");
return Err(ArtifactError::Io);
}
};
let file_len = file
.metadata()
.map_err(|e| {
warn!(target: "tensor_wasm_artifacts", file = %path.display(), error = %e, "metadata failed (get_to)");
ArtifactError::Io
})?
.len();
let mut reader = BufReader::with_capacity(STREAM_BUF_LEN, file);
let verified = self.verify_blob(&mut reader, file_len, &key, &path)?;
reader.seek(SeekFrom::Start(0)).map_err(|e| {
warn!(target: "tensor_wasm_artifacts", file = %path.display(), error = %e, "rewind failed (get_to decode pass)");
ArtifactError::Io
})?;
self.decode_to_writer(&mut reader, &verified, hash, out, &path)
}
fn list(&self) -> Result<Vec<ContentHash>, ArtifactError> {
let mut seen: std::collections::HashSet<ContentHash> = std::collections::HashSet::new();
let mut fps: Vec<String> = self
.key_provider
.read_keys()
.iter()
.map(key_fingerprint_hex)
.collect();
fps.sort();
fps.dedup();
for fp in fps {
self.list_one_key(&fp, &mut seen)?;
}
Ok(seen.into_iter().collect())
}
fn contains(&self, hash: &ContentHash) -> Result<bool, ArtifactError> {
Ok(self.resolve_read_key(hash)?.is_some())
}
fn remove(&self, hash: &ContentHash) -> Result<bool, ArtifactError> {
let (path, key) = match self.resolve_read_key(hash)? {
Some(found) => found,
None => return Ok(false),
};
let removed = match std::fs::remove_file(&path) {
Ok(()) => true,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => false,
Err(e) => {
warn!(
target: "tensor_wasm_artifacts",
file = %path.display(),
error = %e,
"remove unlink failed"
);
return Err(ArtifactError::Io);
}
};
let fp = key_fingerprint_hex(&key);
let meta_path = self.meta_path_for_key(hash, &fp);
if let Err(e) = std::fs::remove_file(&meta_path) {
if e.kind() != std::io::ErrorKind::NotFound {
warn!(
target: "tensor_wasm_artifacts",
file = %meta_path.display(),
error = %e,
"remove sidecar unlink failed (blob already removed)"
);
}
}
Ok(removed)
}
}
impl DiskArtifactStore {
fn list_one_key(
&self,
fp: &str,
seen: &mut std::collections::HashSet<ContentHash>,
) -> Result<(), ArtifactError> {
let suffix = format!(".{fp}.bin");
let entries = match std::fs::read_dir(&self.dir) {
Ok(e) => e,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(e) => {
warn!(
target: "tensor_wasm_artifacts",
dir = %self.dir.display(),
error = %e,
"read_dir failed"
);
return Err(ArtifactError::Io);
}
};
for entry in entries {
let entry = entry.map_err(|e| {
warn!(
target: "tensor_wasm_artifacts",
dir = %self.dir.display(),
error = %e,
"read_dir entry failed"
);
ArtifactError::Io
})?;
let name = match entry.file_name().into_string() {
Ok(n) => n,
Err(_) => continue,
};
if !name.ends_with(&suffix) {
continue;
}
let hash_hex = &name[..name.len() - suffix.len()];
if hash_hex.len() != 64 {
continue;
}
let mut bytes = [0u8; 32];
let mut ok = true;
for (i, chunk) in hash_hex.as_bytes().chunks(2).enumerate() {
let s = match std::str::from_utf8(chunk) {
Ok(s) => s,
Err(_) => {
ok = false;
break;
}
};
match u8::from_str_radix(s, 16) {
Ok(b) => bytes[i] = b,
Err(_) => {
ok = false;
break;
}
}
}
if ok {
seen.insert(ContentHash::from_bytes(bytes));
}
}
Ok(())
}
}
fn hex_of(bytes: &[u8; 32]) -> String {
bytes.iter().map(|b| format!("{:02x}", b)).collect()
}
pub fn encode_envelope_to_vec(
payload: &[u8],
hmac_key: &[u8; 32],
) -> Result<Vec<u8>, ArtifactError> {
if payload.len() > MAX_PAYLOAD_LEN {
warn!(
target: "tensor_wasm_artifacts",
actual = payload.len(),
limit = MAX_PAYLOAD_LEN,
"rejecting oversized payload (encode_envelope_to_vec)"
);
return Err(ArtifactError::TooLarge {
actual: payload.len(),
limit: MAX_PAYLOAD_LEN,
});
}
let hash = ContentHash::of(payload);
let mut mac = new_mac(hmac_key);
let mut buf: Vec<u8> =
Vec::with_capacity(ARTIFACT_HEADER_LEN + payload.len() / 4 + ARTIFACT_HMAC_LEN);
buf.extend_from_slice(&ARTIFACT_MAGIC);
buf.extend_from_slice(&ARTIFACT_VERSION.to_le_bytes());
buf.extend_from_slice(&hash.0);
{
use hmac::Mac;
mac.update(&buf[..ARTIFACT_HEADER_LEN]);
}
{
let mut tee = MacWriter::new(&mut buf, &mut mac);
let mut encoder = zstd::stream::write::Encoder::new(&mut tee, DEFAULT_ZSTD_LEVEL)
.map_err(|e| {
warn!(target: "tensor_wasm_artifacts", error = %e, "zstd init failed (encode_envelope_to_vec)");
ArtifactError::Io
})?;
encoder.write_all(payload).map_err(|e| {
warn!(target: "tensor_wasm_artifacts", error = %e, "zstd write failed (encode_envelope_to_vec)");
ArtifactError::Io
})?;
encoder.finish().map_err(|e| {
warn!(target: "tensor_wasm_artifacts", error = %e, "zstd finish failed (encode_envelope_to_vec)");
ArtifactError::Io
})?;
}
let tag = finalize_into_tag(mac);
buf.extend_from_slice(&tag);
Ok(buf)
}
pub fn decode_envelope_from_bytes(
bytes: &[u8],
hmac_key: &[u8; 32],
) -> Result<Vec<u8>, ArtifactError> {
decode_envelope_from_bytes_with_cap(bytes, hmac_key, MAX_DECOMPRESSED_LEN)
}
pub fn decode_envelope_from_bytes_with_cap(
bytes: &[u8],
hmac_key: &[u8; 32],
max_decompressed: usize,
) -> Result<Vec<u8>, ArtifactError> {
if bytes.len() < ARTIFACT_HEADER_LEN + ARTIFACT_HMAC_LEN {
return Err(ArtifactError::BadMagic);
}
if bytes[..16] != ARTIFACT_MAGIC {
return Err(ArtifactError::BadMagic);
}
let version = u32::from_le_bytes([bytes[16], bytes[17], bytes[18], bytes[19]]);
if version != ARTIFACT_VERSION {
return Err(ArtifactError::BadVersion(version));
}
let mut hash_on_disk = [0u8; 32];
hash_on_disk.copy_from_slice(&bytes[20..52]);
let hmac_start = bytes.len() - ARTIFACT_HMAC_LEN;
let (prefix, tag_bytes) = bytes.split_at(hmac_start);
let mut mac = new_mac(hmac_key);
{
use hmac::Mac;
mac.update(prefix);
}
let expected = finalize_into_tag(mac);
use subtle::ConstantTimeEq;
let mac_ok = bool::from(expected.as_slice().ct_eq(tag_bytes));
if !mac_ok {
warn!(
target: "tensor_wasm_artifacts",
"HMAC mismatch (decode_envelope_from_bytes; possible tampering or stale key)"
);
return Err(ArtifactError::BadHmac);
}
let body = &prefix[ARTIFACT_HEADER_LEN..];
let cap = max_decompressed;
let probe_limit = u64::try_from(cap)
.ok()
.and_then(|c| c.checked_add(1))
.unwrap_or(u64::MAX);
let initial_capacity = body.len().saturating_mul(2).min(cap);
let mut payload: Vec<u8> = Vec::with_capacity(initial_capacity);
let decoder = zstd::stream::read::Decoder::new(body).map_err(|e| {
warn!(target: "tensor_wasm_artifacts", error = %e, "zstd init failed (decode_envelope_from_bytes)");
ArtifactError::Decompression(e.to_string())
})?;
decoder
.take(probe_limit)
.read_to_end(&mut payload)
.map_err(|e| {
warn!(target: "tensor_wasm_artifacts", error = %e, "zstd decode failed (decode_envelope_from_bytes)");
ArtifactError::Decompression(e.to_string())
})?;
if payload.len() > cap {
return Err(ArtifactError::TooLarge {
actual: payload.len(),
limit: cap,
});
}
let recomputed = ContentHash::of(&payload);
if recomputed.0 != hash_on_disk {
return Err(ArtifactError::HashMismatch {
expected: hex_of(&hash_on_disk),
actual: recomputed.to_string(),
});
}
Ok(payload)
}
pub struct InMemoryArtifactStore {
entries: parking_lot::Mutex<HashMap<ContentHash, Vec<u8>>>,
#[allow(dead_code)]
hmac_key: Zeroizing<[u8; 32]>,
}
impl InMemoryArtifactStore {
pub fn new(hmac_key: [u8; 32]) -> Self {
Self {
entries: parking_lot::Mutex::new(HashMap::new()),
hmac_key: Zeroizing::new(hmac_key),
}
}
}
impl ArtifactStore for InMemoryArtifactStore {
fn put(&self, payload: &[u8]) -> Result<ContentHash, ArtifactError> {
let hash = ContentHash::of(payload);
self.entries.lock().insert(hash, payload.to_vec());
Ok(hash)
}
fn get(&self, hash: &ContentHash) -> Result<Vec<u8>, ArtifactError> {
self.entries
.lock()
.get(hash)
.cloned()
.ok_or_else(|| ArtifactError::NotFound(hash.to_string()))
}
fn get_to(&self, hash: &ContentHash, out: &mut dyn Write) -> Result<u64, ArtifactError> {
let guard = self.entries.lock();
let bytes = guard
.get(hash)
.ok_or_else(|| ArtifactError::NotFound(hash.to_string()))?;
out.write_all(bytes).map_err(|e| {
warn!(target: "tensor_wasm_artifacts", error = %e, "in-memory get_to writer write failed");
ArtifactError::Io
})?;
Ok(bytes.len() as u64)
}
fn list(&self) -> Result<Vec<ContentHash>, ArtifactError> {
Ok(self.entries.lock().keys().copied().collect())
}
fn contains(&self, hash: &ContentHash) -> Result<bool, ArtifactError> {
Ok(self.entries.lock().contains_key(hash))
}
fn remove(&self, hash: &ContentHash) -> Result<bool, ArtifactError> {
Ok(self.entries.lock().remove(hash).is_some())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn content_hash_is_blake3() {
let h = ContentHash::of(b"hello");
let expected: [u8; 32] = blake3::hash(b"hello").into();
assert_eq!(h.0, expected);
assert_eq!(h.to_hex().len(), 64);
}
#[test]
fn in_memory_put_get_round_trip() {
let store = InMemoryArtifactStore::new([7u8; 32]);
let hash = store.put(b"payload").unwrap();
assert_eq!(store.get(&hash).unwrap(), b"payload");
assert_eq!(store.list().unwrap().len(), 1);
}
#[test]
fn in_memory_contains_and_remove() {
let store = InMemoryArtifactStore::new([9u8; 32]);
let hash = store.put(b"x").unwrap();
assert!(store.contains(&hash).unwrap());
assert!(store.remove(&hash).unwrap(), "first remove deletes");
assert!(!store.contains(&hash).unwrap());
assert!(!store.remove(&hash).unwrap(), "second remove is a no-op");
}
#[test]
fn in_memory_get_to_streams_payload() {
let store = InMemoryArtifactStore::new([5u8; 32]);
let hash = store.put(b"streamed").unwrap();
let mut out: Vec<u8> = Vec::new();
let n = store.get_to(&hash, &mut out).unwrap();
assert_eq!(n, 8);
assert_eq!(out, b"streamed");
}
#[test]
fn single_key_provider_active_is_only_read_key() {
let p = SingleKeyProvider::new([3u8; 32]);
assert_eq!(p.active_key(), [3u8; 32]);
assert_eq!(p.read_keys(), vec![[3u8; 32]]);
}
#[test]
fn rotating_provider_active_first_then_accepted() {
let p = RotatingKeyProvider::new([1u8; 32], [[2u8; 32], [3u8; 32]]);
assert_eq!(p.active_key(), [1u8; 32]);
assert_eq!(p.read_keys(), vec![[1u8; 32], [2u8; 32], [3u8; 32]]);
}
#[test]
fn metadata_serde_round_trips() {
let m = ArtifactMetadata {
created_unix_ms: 123,
original_len: 456,
source_tier: "test".to_string(),
};
let json = serde_json::to_vec(&m).unwrap();
let back: ArtifactMetadata = serde_json::from_slice(&json).unwrap();
assert_eq!(m, back);
}
#[test]
fn key_fingerprint_is_8_bytes_hex() {
let fp = key_fingerprint_hex(&[0u8; 32]);
assert_eq!(fp.len(), 16); }
#[test]
fn disk_format_constants() {
assert_eq!(ARTIFACT_HEADER_LEN, 16 + 4 + 32);
assert_eq!(ARTIFACT_HMAC_LEN, 32);
assert_eq!(ARTIFACT_MAGIC.len(), 16);
}
}