103 lines
2.9 KiB
Rust
103 lines
2.9 KiB
Rust
//! Registry: local storage of built VM artifacts under
|
|
//! `$XDG_DATA_HOME/boco/registry/<image>/`.
|
|
|
|
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<PathBuf> {
|
|
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<PathBuf> {
|
|
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<PathBuf> {
|
|
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<PathBuf> {
|
|
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"
|
|
);
|
|
}
|
|
}
|
|
}
|