diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6b48e2422..47b727ecf 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -48,7 +48,7 @@ jobs: uses: Swatinem/rust-cache@v2 - name: Build - run: just check && just build + run: just validate && just build - name: Run unit tests run: just unit diff --git a/Cargo.toml b/Cargo.toml index 7bf87a8d7..8f5fd0713 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ unused_must_use = "forbid" missing_docs = "deny" missing_debug_implementations = "deny" # Feel free to comment this one out locally during development of a patch. +unused_imports = "deny" dead_code = "deny" [workspace.lints.clippy] diff --git a/Justfile b/Justfile index fe3deb3cc..2ee76d4c4 100644 --- a/Justfile +++ b/Justfile @@ -2,10 +2,9 @@ build: make -# Quick checks -check: - cargo t --workspace --no-run - cargo fmt --check +# Static checks +validate: + make validate # Run unit tests (excludes integration tests) unit *ARGS: diff --git a/Makefile b/Makefile index fa4ef82ed..8b93389c1 100644 --- a/Makefile +++ b/Makefile @@ -36,6 +36,19 @@ manpages: sync-cli-options $(MAN8_TARGETS) sync-cli-options: @cargo xtask sync-manpages >/dev/null 2>&1 || true +# This gates CI by default. Note that for clippy, we gate on +# only the clippy correctness and suspicious lints, plus a select +# set of default rustc warnings. +# We intentionally don't gate on this for local builds in cargo.toml +# because it impedes iteration speed. +CLIPPY_CONFIG = -A clippy::all -D clippy::correctness -D clippy::suspicious -D clippy::disallowed-methods -Dunused_imports -Ddead_code +validate: + cargo fmt -- --check -l + cargo test --no-run --workspace + cargo clippy -- $(CLIPPY_CONFIG) + env RUSTDOCFLAGS='-D warnings' cargo doc --lib +.PHONY: validate + install: install -D -m 0755 -t $(DESTDIR)$(prefix)/bin target/release/bcvk if [ -n "$(MAN8_TARGETS)" ]; then \ diff --git a/crates/integration-tests/Cargo.toml b/crates/integration-tests/Cargo.toml index 1425d852a..49259f80a 100644 --- a/crates/integration-tests/Cargo.toml +++ b/crates/integration-tests/Cargo.toml @@ -33,3 +33,6 @@ uuid = { version = "1.18.1", features = ["v4"] } camino = "1.1.12" regex = "1" linkme = "0.3.30" + +[lints] +workspace = true diff --git a/crates/integration-tests/src/bin/cleanup.rs b/crates/integration-tests/src/bin/cleanup.rs index bea3c9c48..c41857418 100644 --- a/crates/integration-tests/src/bin/cleanup.rs +++ b/crates/integration-tests/src/bin/cleanup.rs @@ -1,3 +1,7 @@ +//! Cleanup utility for integration test resources +//! +//! This binary removes integration test containers and libvirt VMs that were created during testing. + use std::process::Command; // Import shared constants from the library diff --git a/crates/integration-tests/src/lib.rs b/crates/integration-tests/src/lib.rs index 6f599e9c5..62571cafa 100644 --- a/crates/integration-tests/src/lib.rs +++ b/crates/integration-tests/src/lib.rs @@ -20,11 +20,14 @@ pub type TestFn = fn() -> color_eyre::Result<()>; /// Metadata for a registered integration test #[derive(Debug)] pub struct IntegrationTest { + /// Name of the integration test pub name: &'static str, + /// Test function to execute pub f: TestFn, } impl IntegrationTest { + /// Create a new integration test with the given name and function pub const fn new(name: &'static str, f: TestFn) -> Self { Self { name, f } } diff --git a/crates/integration-tests/src/main.rs b/crates/integration-tests/src/main.rs index b6245087b..dcf1080c2 100644 --- a/crates/integration-tests/src/main.rs +++ b/crates/integration-tests/src/main.rs @@ -1,3 +1,5 @@ +//! Integration tests for bcvk + use camino::Utf8Path; use std::process::Output; diff --git a/crates/kit/Cargo.toml b/crates/kit/Cargo.toml index 2e0b2e01b..969427d22 100644 --- a/crates/kit/Cargo.toml +++ b/crates/kit/Cargo.toml @@ -57,3 +57,5 @@ similar-asserts = "1.5" # Implementation detail of man page generation. docgen = ["clap_mangen"] +[lints] +workspace = true diff --git a/crates/kit/src/domain_list.rs b/crates/kit/src/domain_list.rs index 4e3a0ce03..691aed69a 100644 --- a/crates/kit/src/domain_list.rs +++ b/crates/kit/src/domain_list.rs @@ -199,7 +199,9 @@ impl DomainLister { .unwrap_or_default(); // Extract memory and vcpu from domain XML - let memory_mb = dom.find("memory").and_then(|node| node.parse_memory_mb()); + let memory_mb = dom + .find("memory") + .and_then(|node| crate::libvirt::parse_memory_mb(node)); let vcpus = dom .find("vcpu") diff --git a/crates/kit/src/lib.rs b/crates/kit/src/lib.rs index ed4a54ae0..310efb3c4 100644 --- a/crates/kit/src/lib.rs +++ b/crates/kit/src/lib.rs @@ -1,5 +1,4 @@ //! bcvk library - exposes internal modules for testing pub mod qemu_img; -pub mod utils; pub mod xml_utils; diff --git a/crates/kit/src/libvirt/domain.rs b/crates/kit/src/libvirt/domain.rs index 746dc9756..0e3baaea8 100644 --- a/crates/kit/src/libvirt/domain.rs +++ b/crates/kit/src/libvirt/domain.rs @@ -338,7 +338,7 @@ impl DomainBuilder { writer.end_element("interface")?; } network if network.starts_with("bridge=") => { - let bridge_name = &network[7..]; // Remove "bridge=" prefix + let bridge_name = network.strip_prefix("bridge=").unwrap(); writer.start_element("interface", &[("type", "bridge")])?; writer.write_empty_element("source", &[("bridge", bridge_name)])?; writer.write_empty_element("model", &[("type", "virtio")])?; diff --git a/crates/kit/src/libvirt/list_volumes.rs b/crates/kit/src/libvirt/list_volumes.rs index d79e55f00..3a1c71326 100644 --- a/crates/kit/src/libvirt/list_volumes.rs +++ b/crates/kit/src/libvirt/list_volumes.rs @@ -344,8 +344,6 @@ impl LibvirtListVolumesOpts { } } -/// Extract value from XML element (simple string parsing) - /// Parse virsh size format (e.g., "5.00 GiB") to bytes fn parse_virsh_size(size_str: &str) -> Option { let parts: Vec<&str> = size_str.split_whitespace().collect(); @@ -356,14 +354,7 @@ fn parse_virsh_size(size_str: &str) -> Option { let number: f64 = parts[0].parse().ok()?; let unit = parts[1]; - let multiplier = match unit { - "B" | "bytes" => 1, - "KiB" | "KB" => 1024, - "MiB" | "MB" => 1024 * 1024, - "GiB" | "GB" => 1024 * 1024 * 1024, - "TiB" | "TB" => 1024u64.pow(4), - _ => return None, - }; + let multiplier = super::unit_to_bytes(unit)?; Some((number * multiplier as f64) as u64) } diff --git a/crates/kit/src/libvirt/mod.rs b/crates/kit/src/libvirt/mod.rs index bdb9eb9cd..df89e498f 100644 --- a/crates/kit/src/libvirt/mod.rs +++ b/crates/kit/src/libvirt/mod.rs @@ -61,6 +61,126 @@ impl LibvirtOptions { } } +/// Convert a unit string to bytes multiplier +/// Handles libvirt-style units distinguishing between decimal (KB, MB, GB - powers of 1000) +/// and binary (KiB, MiB, GiB - powers of 1024) units per libvirt specification +pub(crate) fn unit_to_bytes(unit: &str) -> Option { + match unit { + // Binary prefixes (powers of 1024) + "B" | "bytes" => Some(1), + "k" | "K" | "KiB" => Some(1024), + "M" | "MiB" => Some(1024u128.pow(2)), + "G" | "GiB" => Some(1024u128.pow(3)), + "T" | "TiB" => Some(1024u128.pow(4)), + + // Decimal prefixes (powers of 1000) + "KB" => Some(1_000), + "MB" => Some(1_000u128.pow(2)), + "GB" => Some(1_000u128.pow(3)), + "TB" => Some(1_000u128.pow(4)), + + _ => None, + } +} + +/// Convert memory value with unit to megabytes (MiB) +/// Handles libvirt-style units distinguishing between decimal (KB, MB, GB - powers of 1000) +/// and binary (KiB, MiB, GiB - powers of 1024) units per libvirt specification +/// Returns None if the unit is unknown or if the result overflows u32 +pub(crate) fn convert_memory_to_mb(value: u32, unit: &str) -> Option { + let value_u128 = value as u128; + let mib_u128 = 1024 * 1024; + + // Convert to bytes first, then to MiB + let bytes = value_u128 * unit_to_bytes(unit)?; + let mb = bytes / mib_u128; + + u32::try_from(mb).ok() +} + +/// Convert memory value with unit to megabytes (MiB), returning u64 +/// Handles libvirt-style units distinguishing between decimal (KB, MB, GB - powers of 1000) +/// and binary (KiB, MiB, GiB - powers of 1024) units per libvirt specification +/// Returns None if the unit is unknown or if the result overflows u64 +#[allow(dead_code)] +pub(crate) fn convert_to_mb(value: u64, unit: &str) -> Option { + let value_u128 = value as u128; + let mib_u128 = 1024 * 1024; + + // Convert to bytes first, then to MiB + let bytes = value_u128 * unit_to_bytes(unit)?; + let mb = bytes / mib_u128; + + u64::try_from(mb).ok() +} + +/// Parse memory value from a libvirt XML node with unit attribute +/// Returns the value in megabytes (MiB) +pub(crate) fn parse_memory_mb(node: &crate::xml_utils::XmlNode) -> Option { + let value = node.text_content().parse::().ok()?; + // Convert to MB based on unit attribute (default is KiB per libvirt spec) + let unit = node + .attributes + .get("unit") + .map(|s| s.as_str()) + .unwrap_or("KiB"); + convert_memory_to_mb(value, unit) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_convert_memory_to_mb() { + // Test binary units (powers of 1024) + assert_eq!(convert_memory_to_mb(4194304, "KiB"), Some(4096)); + assert_eq!(convert_memory_to_mb(2097152, "KiB"), Some(2048)); + assert_eq!(convert_memory_to_mb(2048, "MiB"), Some(2048)); + assert_eq!(convert_memory_to_mb(4096, "MiB"), Some(4096)); + assert_eq!(convert_memory_to_mb(4, "GiB"), Some(4096)); + assert_eq!(convert_memory_to_mb(2, "GiB"), Some(2048)); + + // Test short forms (binary) + assert_eq!(convert_memory_to_mb(4, "G"), Some(4096)); + assert_eq!(convert_memory_to_mb(2048, "M"), Some(2048)); + assert_eq!(convert_memory_to_mb(2097152, "K"), Some(2048)); + + // Test decimal units (powers of 1000) + assert_eq!(convert_memory_to_mb(1048576, "KB"), Some(1000)); + assert_eq!(convert_memory_to_mb(1024, "MB"), Some(976)); + assert_eq!(convert_memory_to_mb(4, "GB"), Some(3814)); + + // Test unknown unit returns None + assert_eq!(convert_memory_to_mb(4194304, "unknown"), None); + } + + #[test] + fn test_parse_memory_mb() { + use crate::xml_utils::parse_xml_dom; + + // Test KiB (default unit) + let xml = r#"4194304"#; + let dom = parse_xml_dom(xml).unwrap(); + assert_eq!(parse_memory_mb(&dom), Some(4096)); + + // Test MiB + let xml = r#"2048"#; + let dom = parse_xml_dom(xml).unwrap(); + assert_eq!(parse_memory_mb(&dom), Some(2048)); + + // Test GiB + let xml = r#"4"#; + let dom = parse_xml_dom(xml).unwrap(); + assert_eq!(parse_memory_mb(&dom), Some(4096)); + + // Test KB (decimal unit: 1000-based) + let xml = r#"1048576"#; + let dom = parse_xml_dom(xml).unwrap(); + assert_eq!(parse_memory_mb(&dom), Some(1000)); + } +} + /// libvirt subcommands for managing bootc disk images and domains #[derive(Debug, Subcommand)] pub enum LibvirtSubcommands { diff --git a/crates/kit/src/main.rs b/crates/kit/src/main.rs index 7e88a700a..c97f00545 100644 --- a/crates/kit/src/main.rs +++ b/crates/kit/src/main.rs @@ -1,3 +1,5 @@ +//! Bootc Virtualization Kit (bcvk) - A toolkit for bootc containers and local virtualization + use std::ffi::OsString; use cap_std_ext::cap_std::fs::Dir; @@ -36,6 +38,7 @@ mod to_disk; mod utils; mod xml_utils; +/// Default state directory for bcvk container data pub const CONTAINER_STATEDIR: &str = "/var/lib/bcvk"; /// A comprehensive toolkit for bootc containers and local virtualization. diff --git a/crates/kit/src/qemu.rs b/crates/kit/src/qemu.rs index ce4e6000b..96d08137a 100644 --- a/crates/kit/src/qemu.rs +++ b/crates/kit/src/qemu.rs @@ -360,6 +360,8 @@ fn allocate_vsock_cid(vhost_fd: File) -> Result<(OwnedFd, u32)> { const VHOST_VSOCK_SET_GUEST_CID: libc::c_ulong = 0x4008af60; let cid = candidate_cid as u64; + // SAFETY: ioctl is unsafe but we're passing valid file descriptor and pointer + #[allow(unsafe_code)] let result = unsafe { match libc::ioctl( vhost_fd.as_raw_fd(), @@ -409,6 +411,7 @@ fn spawn( let mut cmd = Command::new(qemu); // SAFETY: This API is safe to call in a forked child. + #[allow(unsafe_code)] unsafe { cmd.pre_exec(|| { rustix::process::set_parent_process_death_signal(Some(rustix::process::Signal::TERM)) @@ -968,6 +971,7 @@ pub async fn spawn_virtiofsd_async(config: &VirtiofsConfig) -> Result, + /// Cluster size in bytes (for formats like qcow2) pub cluster_size: Option, + /// Backing file name (if this is a snapshot) pub backing_filename: Option, + /// Full path to backing file (if this is a snapshot) pub full_backing_filename: Option, + /// Whether the image is marked as dirty pub dirty_flag: Option, } diff --git a/crates/kit/src/run_ephemeral_ssh.rs b/crates/kit/src/run_ephemeral_ssh.rs index 85575ee30..32b822102 100644 --- a/crates/kit/src/run_ephemeral_ssh.rs +++ b/crates/kit/src/run_ephemeral_ssh.rs @@ -49,6 +49,8 @@ pub fn wait_for_vm_ssh( "/var/lib/bcvk/entrypoint", "monitor-status", ]); + // SAFETY: This API is safe to call in a forked child. + #[allow(unsafe_code)] unsafe { cmd.pre_exec(|| { rustix::process::set_parent_process_death_signal(Some(rustix::process::Signal::TERM)) diff --git a/crates/kit/src/utils.rs b/crates/kit/src/utils.rs index 9ed1c2716..b6b08c498 100644 --- a/crates/kit/src/utils.rs +++ b/crates/kit/src/utils.rs @@ -106,25 +106,33 @@ pub(crate) fn parse_size(size_str: &str) -> Result { return Err(eyre!("Empty size string")); } - let (number_part, unit_part) = if let Some(pos) = size_str.rfind(|c: char| c.is_ascii_digit()) { - let (num, unit) = size_str.split_at(pos + 1); - (num, unit) + // Try to strip known unit suffixes + let (number_str, multiplier) = if let Some(num) = size_str.strip_suffix("TB") { + (num, 1024_u64.pow(4)) + } else if let Some(num) = size_str.strip_suffix("GB") { + (num, 1024 * 1024 * 1024) + } else if let Some(num) = size_str.strip_suffix("MB") { + (num, 1024 * 1024) + } else if let Some(num) = size_str.strip_suffix("KB") { + (num, 1024) + } else if let Some(num) = size_str.strip_suffix('T') { + (num, 1024_u64.pow(4)) + } else if let Some(num) = size_str.strip_suffix('G') { + (num, 1024 * 1024 * 1024) + } else if let Some(num) = size_str.strip_suffix('M') { + (num, 1024 * 1024) + } else if let Some(num) = size_str.strip_suffix('K') { + (num, 1024) + } else if let Some(num) = size_str.strip_suffix('B') { + (num, 1) } else { - return Err(eyre!("Invalid size format: {}", size_str)); + // No unit suffix, assume bytes + (&*size_str, 1) }; - let number: u64 = number_part + let number: u64 = number_str .parse() - .map_err(|_| eyre!("Invalid number in size: {}", number_part))?; - - let multiplier = match unit_part { - "" | "B" => 1, - "K" | "KB" => 1024, - "M" | "MB" => 1024 * 1024, - "G" | "GB" => 1024 * 1024 * 1024, - "T" | "TB" => 1024_u64.pow(4), - _ => return Err(eyre!("Unknown size unit: {}", unit_part)), - }; + .map_err(|_| eyre!("Invalid number in size: {}", number_str))?; Ok(number * multiplier) } @@ -137,96 +145,37 @@ pub(crate) fn parse_memory_to_mb(memory_str: &str) -> Result { return Err(eyre!("Memory string cannot be empty")); } - // Check if it ends with a unit suffix - if let Some(last_char) = memory_str.chars().last() { - match last_char.to_ascii_uppercase() { - 'G' => { - let number_part = &memory_str[..memory_str.len() - 1]; - let gb: f64 = number_part - .parse() - .context("Invalid number in memory specification")?; - Ok((gb * 1024.0) as u32) - } - 'M' => { - let number_part = &memory_str[..memory_str.len() - 1]; - let mb: u32 = number_part - .parse() - .context("Invalid number in memory specification")?; - Ok(mb) - } - 'K' => { - let number_part = &memory_str[..memory_str.len() - 1]; - let kb: u32 = number_part - .parse() - .context("Invalid number in memory specification")?; - Ok(kb / 1024) - } - _ => { - // No suffix, assume megabytes - let mb: u32 = memory_str - .parse() - .context("Invalid number in memory specification")?; - Ok(mb) - } - } + // Try to strip unit suffix, checking case-insensitively + let (number_str, unit) = if let Some(num) = memory_str + .strip_suffix('G') + .or_else(|| memory_str.strip_suffix('g')) + { + (num, "GiB") + } else if let Some(num) = memory_str + .strip_suffix('M') + .or_else(|| memory_str.strip_suffix('m')) + { + (num, "MiB") + } else if let Some(num) = memory_str + .strip_suffix('K') + .or_else(|| memory_str.strip_suffix('k')) + { + (num, "KiB") } else { - Err(eyre!("Memory specification cannot be empty - please provide a value like '2G', '1024M', or '512'")) - } -} - -/// Convert memory value with unit to megabytes (MiB) -/// Handles libvirt-style units distinguishing between decimal (KB, MB, GB - powers of 1000) -/// and binary (KiB, MiB, GiB - powers of 1024) units per libvirt specification -pub(crate) fn convert_memory_to_mb(value: u32, unit: &str) -> u32 { - // Use u128 for calculations to prevent overflow with large units like TB - let value_u128 = value as u128; - let mib_u128 = 1024 * 1024; - - let mb = match unit { - // Binary prefixes (powers of 1024), converting to MiB - "k" | "K" | "KiB" => value_u128 / 1024, - "M" | "MiB" => value_u128, - "G" | "GiB" => value_u128 * 1024, - "T" | "TiB" => value_u128 * 1024 * 1024, - - // Decimal prefixes (powers of 1000), converting to MiB - "B" | "bytes" => value_u128 / mib_u128, - "KB" => (value_u128 * 1_000) / mib_u128, - "MB" => (value_u128 * 1_000_000) / mib_u128, - "GB" => (value_u128 * 1_000_000_000) / mib_u128, - "TB" => (value_u128 * 1_000_000_000_000) / mib_u128, - - // Libvirt default is KiB for memory - _ => value_u128 / 1024, + // No suffix, assume megabytes + (memory_str, "MiB") }; - mb as u32 -} -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_convert_memory_to_mb() { - // Test binary units (powers of 1024) - assert_eq!(convert_memory_to_mb(4194304, "KiB"), 4096); - assert_eq!(convert_memory_to_mb(2097152, "KiB"), 2048); - assert_eq!(convert_memory_to_mb(2048, "MiB"), 2048); - assert_eq!(convert_memory_to_mb(4096, "MiB"), 4096); - assert_eq!(convert_memory_to_mb(4, "GiB"), 4096); - assert_eq!(convert_memory_to_mb(2, "GiB"), 2048); - - // Test short forms (binary) - assert_eq!(convert_memory_to_mb(4, "G"), 4096); - assert_eq!(convert_memory_to_mb(2048, "M"), 2048); - assert_eq!(convert_memory_to_mb(2097152, "K"), 2048); - - // Test decimal units (powers of 1000) - assert_eq!(convert_memory_to_mb(1048576, "KB"), 1000); - assert_eq!(convert_memory_to_mb(1024, "MB"), 976); - assert_eq!(convert_memory_to_mb(4, "GB"), 3814); - - // Test default/unknown unit (defaults to KiB) - assert_eq!(convert_memory_to_mb(4194304, "unknown"), 4096); - } + let number: f64 = number_str + .parse() + .context("Invalid number in memory specification")?; + + // Use libvirt helper to get bytes per unit + let bytes_per_unit = + crate::libvirt::unit_to_bytes(unit).ok_or_else(|| eyre!("Unknown unit: {}", unit))? as f64; + + let mib = 1024.0 * 1024.0; + let total_mb = (number * bytes_per_unit) / mib; + + Ok(total_mb as u32) } diff --git a/crates/kit/src/xml_utils.rs b/crates/kit/src/xml_utils.rs index 7ef137200..f02144387 100644 --- a/crates/kit/src/xml_utils.rs +++ b/crates/kit/src/xml_utils.rs @@ -15,6 +15,12 @@ pub struct XmlWriter { writer: Writer>>, } +impl std::fmt::Debug for XmlWriter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("XmlWriter").finish_non_exhaustive() + } +} + impl XmlWriter { /// Create a new XML writer pub fn new() -> Self { @@ -104,9 +110,13 @@ impl Default for XmlWriter { /// Simple DOM node for XML parsing #[derive(Debug, Clone)] pub struct XmlNode { + /// Element name (tag name) pub name: String, + /// Element attributes as key-value pairs pub attributes: HashMap, + /// Text content of the element pub text: String, + /// Child elements pub children: Vec, } @@ -140,19 +150,6 @@ impl XmlNode { pub fn text_content(&self) -> &str { &self.text } - - /// Parse memory value from an XML node with unit attribute - /// Returns the value in megabytes (MB) - pub fn parse_memory_mb(&self) -> Option { - let value = self.text_content().parse::().ok()?; - // Convert to MB based on unit attribute (default is KiB per libvirt spec) - let unit = self - .attributes - .get("unit") - .map(|s| s.as_str()) - .unwrap_or("KiB"); - Some(crate::utils::convert_memory_to_mb(value, unit)) - } } /// Parse XML string into a simple DOM structure @@ -383,27 +380,4 @@ mod tests { assert!(xml.contains("raw content")); assert!(xml.contains("")); } - - #[test] - fn test_parse_memory_mb() { - // Test KiB (default unit) - let xml = r#"4194304"#; - let dom = parse_xml_dom(xml).unwrap(); - assert_eq!(dom.parse_memory_mb(), Some(4096)); - - // Test MiB - let xml = r#"2048"#; - let dom = parse_xml_dom(xml).unwrap(); - assert_eq!(dom.parse_memory_mb(), Some(2048)); - - // Test GiB - let xml = r#"4"#; - let dom = parse_xml_dom(xml).unwrap(); - assert_eq!(dom.parse_memory_mb(), Some(4096)); - - // Test KB (decimal unit: 1000-based) - let xml = r#"1048576"#; - let dom = parse_xml_dom(xml).unwrap(); - assert_eq!(dom.parse_memory_mb(), Some(1000)); - } }