mod backends;
mod query;
mod registry;
pub use backends::FileSystemStorage;
#[cfg(feature = "sqlite")]
pub use backends::SqliteStorage;
pub use query::ModelQuery;
pub use registry::ModelRegistry;
use crate::error::{Result, TuneError};
use sha2::{Digest, Sha256};
use std::path::Path;
use super::model::RegisteredModel;
pub trait StorageBackend: Send + Sync {
fn save(&mut self, model: &RegisteredModel, weights: &[u8]) -> Result<String>;
fn load(&self, path: &str) -> Result<Vec<u8>>;
fn delete(&mut self, path: &str) -> Result<()>;
fn exists(&self, path: &str) -> bool;
fn list(&self) -> Vec<String>;
}
pub(crate) fn validate_path(path: &str) -> Result<()> {
if path.contains('\0') {
return Err(TuneError::Storage("Path contains null byte".to_string()));
}
if Path::new(path).is_absolute() {
return Err(TuneError::Storage(format!(
"Path traversal attempt: absolute path not allowed: {path}"
)));
}
for component in Path::new(path).components() {
if let std::path::Component::ParentDir = component {
return Err(TuneError::Storage(format!(
"Path traversal attempt: '..' not allowed in path: {path}"
)));
}
}
Ok(())
}
pub(crate) fn validate_model_identity(name: &str, version: &str) -> Result<()> {
validate_path(name)?;
validate_path(version)?;
Ok(())
}
pub fn sha256_hash(data: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(data);
let result = hasher.finalize();
format!("{result:x}")
}
#[cfg(test)]
mod tests;