refactor: split main.rs into registry, build, and qemu modules
main.rs keeps only the CLI definition and command dispatch: - registry.rs: image-name validation + XDG registry path handling (tests) - build.rs: podman mount/unmount, vmlinuz extraction, cpio/gzip initrd - qemu.rs: direct-kernel-boot VM launch No behavior change; cargo fmt/clippy/test pass, build/run paths verified against the mock-vm image.
This commit was merged in pull request #5.
This commit is contained in:
1 parent
5b80885207
commit
37f537184c
4 files changed
+273
-252
No files matched your search
@@ -0,0 +1,95 @@
|
||||
//! Registry: local storage of built VM artifacts under
|
||||
//! `$XDG_DATA_HOME/slim-rs/registry/<image>/`.
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
use std::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/slim-rs/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 registry_dir(image: &str) -> Result<PathBuf> {
|
||||
validate_image_name(image)?;
|
||||
let base = xdg::BaseDirectories::with_prefix("slim-rs");
|
||||
let initrd_path = base
|
||||
.place_data_file(format!("registry/{image}/initrd"))
|
||||
.context("Failed to write to XDG_DATA_HOME")?;
|
||||
let reg_dir = initrd_path
|
||||
.parent()
|
||||
.expect("data file has a parent")
|
||||
.to_owned();
|
||||
|
||||
// Defense in depth: the constructed path must stay inside the registry.
|
||||
let registry_root = base
|
||||
.place_data_file("registry/.root-check")
|
||||
.context("Failed to write to XDG_DATA_HOME")?
|
||||
.parent()
|
||||
.expect("data file has a parent")
|
||||
.to_owned();
|
||||
if !reg_dir.starts_with(®istry_root) {
|
||||
bail!(
|
||||
"invalid image name '{image}': registry path escapes {}",
|
||||
registry_root.display()
|
||||
);
|
||||
}
|
||||
Ok(reg_dir)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::validate_image_name;
|
||||
|
||||
#[test]
|
||||
fn accepts_valid_image_names() {
|
||||
for name in [
|
||||
"alpine:3.15",
|
||||
"slim",
|
||||
"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"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user