//! Registry: local storage of built VM artifacts under //! `$XDG_DATA_HOME/boco/registry//`. use anyhow::{Context, Result, anyhow, bail}; use std::{fs, io, path::PathBuf}; /// Validate a user-supplied image name before it is used in a registry path. /// /// The name becomes a directory under `$XDG_DATA_HOME/boco/registry/`, so /// path separators and `.`/`..` would allow escaping that directory. /// Allow-list: ASCII alphanumerics plus `.`, `_`, `:`, `-` (covers image /// refs like `alpine:3.15`). pub(crate) fn validate_image_name(name: &str) -> Result<()> { let valid = !name.is_empty() && name != "." && name != ".." && name .chars() .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | ':' | '-')); if !valid { bail!( "invalid image name '{name}': must be non-empty and contain only \ alphanumerics, '.', '_', ':' and '-' (no path separators or '..')" ); } Ok(()) } pub(crate) fn vmlinuz_path() -> Result { let base = xdg::BaseDirectories::with_prefix("boco"); base.place_data_file("vmlinuz") .context("Failed to create XDG_DATA_HOME subdirectory") } pub(crate) fn registry_base_dir() -> Result { let base = xdg::BaseDirectories::with_prefix("boco"); base.create_data_directory("registry") .context("Failed to create XDG_DATA_HOME subdirectory") } pub(crate) fn init_binary_path() -> Result { let base = xdg::BaseDirectories::with_prefix("boco"); base.get_data_file("boco-init") .context("boco-init not found in XDG data dir") } pub(crate) fn registry_dir(image: &str) -> Result { validate_image_name(image)?; let base = registry_base_dir()?; let reg_dir = base.join(image); fs::create_dir(®_dir) .or_else(|e| { (e.kind() == io::ErrorKind::AlreadyExists) .then_some(()) .ok_or(e) }) .with_context(|| anyhow!("Failed to create {reg_dir:?}"))?; Ok(reg_dir) } #[cfg(test)] mod tests { use super::validate_image_name; #[test] fn accepts_valid_image_names() { for name in [ "alpine:3.15", "boco", "mock-vm", "my_vm", "a.b.c", "vm-1.2.3", ] { validate_image_name(name).unwrap_or_else(|e| panic!("{name} rejected: {e}")); } } #[test] fn rejects_traversal_and_separators() { for name in [ "", ".", "..", "../evil", "../../tmp/evil", "a/b", "/abs", "a\\b", "foo bar", "vm;rm", "vm\n", ] { assert!( validate_image_name(name).is_err(), "{name:?} should be rejected" ); } } }