mod bundle;
mod dev;
mod error;
mod lock;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
pub use bundle::{
MAX_BUNDLE_BYTES, MAX_BUNDLE_ENTRIES, MAX_UNPACKED_BYTES, SUPPORTED_SCHEMA_MAJOR,
};
pub use dev::{DEV_CLASS_SOURCES, find_dev_checkout};
pub use error::ContentError;
pub use lock::{ContentLock, TAG_PREFIX, validate_tag};
use crate::integrity::Sha256Digest;
pub const LOCK_FILE_NAME: &str = "content-lock.toml";
pub fn default_cache_dir() -> Option<PathBuf> {
dirs::home_dir().map(|home| home.join(".trusty-mpm").join("content"))
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum DevOverride {
Off,
DetectFrom(PathBuf),
At(PathBuf),
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ResolveOptions {
pub cache_dir: PathBuf,
pub dev: DevOverride,
}
impl ResolveOptions {
pub fn new(cache_dir: impl Into<PathBuf>, dev: DevOverride) -> Self {
Self {
cache_dir: cache_dir.into(),
dev,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ContentSource {
DevCheckout {
root: PathBuf,
},
Installed {
tag: String,
sha256: Sha256Digest,
bundle: PathBuf,
},
}
#[derive(Debug)]
pub struct ResolvedContent {
source: ContentSource,
backing: Backing,
}
#[derive(Debug)]
enum Backing {
Memory(BTreeMap<String, Vec<u8>>),
Checkout(PathBuf),
}
pub fn resolve(options: &ResolveOptions) -> Result<ResolvedContent, ContentError> {
let dev_root = match &options.dev {
DevOverride::Off => None,
DevOverride::DetectFrom(start) => find_dev_checkout(start),
DevOverride::At(root) => {
dev::require_checkout(root)?;
Some(root.clone())
}
};
if let Some(root) = dev_root {
return Ok(ResolvedContent {
source: ContentSource::DevCheckout { root: root.clone() },
backing: Backing::Checkout(root),
});
}
let lock = ContentLock::load(&options.cache_dir.join(LOCK_FILE_NAME))?;
let files = bundle::load_verified(&options.cache_dir, &lock)?;
Ok(ResolvedContent {
source: ContentSource::Installed {
tag: lock.tag().to_owned(),
sha256: lock.sha256().clone(),
bundle: options.cache_dir.join(lock.bundle_file_name()),
},
backing: Backing::Memory(files),
})
}
const CONTENT_CLASSES: [&str; 3] = ["agents", "skills", "instructions"];
impl ResolvedContent {
pub fn source(&self) -> &ContentSource {
&self.source
}
pub fn read(&self, path: &str) -> Result<Vec<u8>, ContentError> {
let key =
bundle::relative_key(Path::new(path)).ok_or_else(|| ContentError::InvalidPath {
path: path.to_owned(),
})?;
let not_found = || ContentError::NotFound {
path: path.to_owned(),
};
match &self.backing {
Backing::Memory(files) => files.get(&key).cloned().ok_or_else(not_found),
Backing::Checkout(root) => dev::read_regular(root, &key)?.ok_or_else(not_found),
}
}
pub fn read_to_string(&self, path: &str) -> Result<String, ContentError> {
String::from_utf8(self.read(path)?).map_err(|e| ContentError::Io {
path: PathBuf::from(path),
source: std::io::Error::new(std::io::ErrorKind::InvalidData, e),
})
}
pub fn list(&self, class: &str) -> Result<Vec<String>, ContentError> {
let top = class.split('/').next().unwrap_or_default();
if !CONTENT_CLASSES.contains(&top) {
return Err(ContentError::UnknownClass {
class: class.to_owned(),
});
}
let prefix = format!("{class}/");
match &self.backing {
Backing::Memory(files) => Ok(files
.keys()
.filter(|k| k.starts_with(&prefix))
.cloned()
.collect()),
Backing::Checkout(root) => dev::list(root, class),
}
}
}
#[cfg(test)]
mod tests;