diff --git a/src/extract.rs b/src/extract.rs index 174042f..ffeef67 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -137,7 +137,11 @@ fn extract_oci_layer(layer_path: &Path, dest: &Path) -> Result<()> { "Extracting OCI layer", ) } else if layer_name.ends_with(".tar.xz") || layer_name.ends_with(".txz") { - extract_with_progress(tar::Archive::new(xz2::read::XzDecoder::new(reader)), dest, "Extracting OCI layer") + extract_with_progress( + tar::Archive::new(xz2::read::XzDecoder::new(reader)), + dest, + "Extracting OCI layer", + ) } else if layer_name.ends_with(".tar.zst") || layer_name.ends_with(".tar.zstd") { extract_with_progress( tar::Archive::new(zstd::stream::read::Decoder::new(reader)?), @@ -217,26 +221,33 @@ fn extract_tar(reader: R, dest: &Path) -> Result<()> { } /// Extract tar archive with progress bar, handling whiteout files for OCI layers -fn extract_with_progress(mut archive: tar::Archive, dest: &Path, msg: &str) -> Result<()> { +fn extract_with_progress( + mut archive: tar::Archive, + dest: &Path, + msg: &str, +) -> Result<()> { let pb = ProgressBar::new_spinner(); - pb.set_style(ProgressStyle::default_spinner() - .template("{spinner:.green} {msg} ({pos} files)") - .unwrap()); + pb.set_style( + ProgressStyle::default_spinner() + .template("{spinner:.green} {msg} ({pos} files)") + .unwrap(), + ); pb.set_message(msg.to_string()); archive.set_preserve_permissions(true); archive.set_preserve_ownerships(false); archive.set_unpack_xattrs(false); - let entries = archive.entries() + let entries = archive + .entries() .context("Failed to read archive entries")?; - + for entry in entries { let mut entry = entry.context("Failed to read archive entry")?; - + // Clone the path before any mutable borrow of entry let path = entry.path().context("Invalid tar entry path")?.into_owned(); - + let filename = path .file_name() .map(|n| n.to_string_lossy().into_owned()) @@ -269,10 +280,11 @@ fn extract_with_progress(mut archive: tar::Archive, dest: & } // Do not extract the .wh.* marker itself } else { - entry.unpack_in(dest) + entry + .unpack_in(dest) .with_context(|| format!("Failed to extract {}", path.display()))?; } - + pb.inc(1); } diff --git a/src/main.rs b/src/main.rs index 9c22e4a..a85fc3b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,4 @@ mod chroot; -mod utils; mod cli; mod config; mod distro; @@ -9,6 +8,7 @@ mod mount; mod namespace; mod qemu; mod qemu_vm; +mod utils; mod verbose; /// Print to stderr only when --verbose / -v is active. @@ -160,11 +160,7 @@ fn main() -> Result<()> { } /// Run in namespace/chroot mode -fn namespace_mode( - args: Args, - rootfs: std::path::PathBuf, - config: Config, -) -> Result<()> { +fn namespace_mode(args: Args, rootfs: std::path::PathBuf, config: Config) -> Result<()> { // Check user namespace availability namespace::check_user_namespace()?; diff --git a/src/namespace.rs b/src/namespace.rs index 0bff928..9a6d618 100644 --- a/src/namespace.rs +++ b/src/namespace.rs @@ -435,7 +435,7 @@ mod tests { fn test_hostname_format() { // Test that hostname generation produces valid format use std::collections::HashSet; - + let mut hostnames = HashSet::new(); for _ in 0..100 { let mut hasher = std::collections::hash_map::DefaultHasher::new(); @@ -455,24 +455,30 @@ mod tests { .collect(); let hostname = format!("ecr-test-{}", random_suffix); - + // Verify hostname format assert!(hostname.starts_with("ecr-test-")); assert!(hostname.len() > 9); // "ecr-test-" + at least 1 char - assert!(hostname.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')); - + assert!(hostname + .chars() + .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')); + hostnames.insert(hostname); } - + // With 100 iterations and good entropy, we should get many unique hostnames - assert!(hostnames.len() > 50, "Expected many unique hostnames, got {}", hostnames.len()); + assert!( + hostnames.len() > 50, + "Expected many unique hostnames, got {}", + hostnames.len() + ); } #[test] fn test_set_hostname_uniqueness() { // Verify that rapid consecutive calls produce different hostnames use std::collections::HashSet; - + let mut hostnames = Vec::new(); for _ in 0..10 { // Simulate the hostname generation logic @@ -494,7 +500,7 @@ mod tests { hostnames.push(format!("ecr-test-{}", random_suffix)); } - + let unique: HashSet<_> = hostnames.iter().collect(); // Most hostnames should be unique (high entropy) assert!(unique.len() >= 8, "Expected mostly unique hostnames"); diff --git a/src/qemu_vm.rs b/src/qemu_vm.rs index fc59ff3..9f85cf3 100644 --- a/src/qemu_vm.rs +++ b/src/qemu_vm.rs @@ -55,7 +55,10 @@ pub fn launch_qemu(config: QemuConfig) -> Result<()> { Ubuntu/Debian: sudo apt install qemu-system-{}\n\ Arch: sudo pacman -S qemu-system-{}\n\ Alpine: sudo apk add qemu-system-{}", - qemu_bin, get_arch_package_suffix(&config.arch), get_arch_package_suffix(&config.arch), get_arch_package_suffix(&config.arch) + qemu_bin, + get_arch_package_suffix(&config.arch), + get_arch_package_suffix(&config.arch), + get_arch_package_suffix(&config.arch) ))?; // Check if we can use KVM acceleration @@ -68,16 +71,20 @@ pub fn launch_qemu(config: QemuConfig) -> Result<()> { // Detect the best available shell in the rootfs let shell = crate::utils::detect_shell(&config.rootfs_path); - + // Generate a unique hostname like "ecr-vm-a1b2c3" // Use VM_HOSTNAME_SUFFIX_BITS constant for entropy - let hostname_suffix = format!("{:x}", (std::process::id() as u64) - .wrapping_mul(std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_nanos() as u64) % crate::utils::VM_HOSTNAME_SUFFIX_BITS); + let hostname_suffix = format!( + "{:x}", + (std::process::id() as u64).wrapping_mul( + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_nanos() as u64 + ) % crate::utils::VM_HOSTNAME_SUFFIX_BITS + ); let hostname = format!("ecr-vm-{}", hostname_suffix); - + // Build kernel command line // For initramfs boot, use rdinit= instead of init= // No root= needed as initramfs becomes the rootfs @@ -168,15 +175,15 @@ fn get_arch_package_suffix(arch: &str) -> &'static str { /// Check if KVM acceleration can be used for the target architecture fn can_use_kvm(target_arch: &str) -> bool { use crate::utils::Arch; - + // Normalize both to canonical form (uname -m style) and compare let host_arch = crate::utils::get_host_arch(); let target_enum = Arch::from_str(target_arch); - + if host_arch != target_enum { return false; } - + // Check if /dev/kvm exists and is accessible (read+write required for VM execution) match std::fs::OpenOptions::new() .read(true) @@ -195,7 +202,7 @@ fn can_use_kvm(target_arch: &str) -> bool { /// Check if KVM capabilities are actually functional fn check_kvm_capabilities() -> bool { use std::os::unix::io::AsRawFd; - + // Try to open /dev/kvm and check KVM_GET_API_VERSION match std::fs::OpenOptions::new() .read(true) @@ -227,9 +234,11 @@ fn create_initramfs(rootfs: &Path) -> Result { // Create progress bar let pb = ProgressBar::new_spinner(); - pb.set_style(ProgressStyle::default_spinner() - .template("{spinner:.green} {msg} ({pos} files)") - .unwrap()); + pb.set_style( + ProgressStyle::default_spinner() + .template("{spinner:.green} {msg} ({pos} files)") + .unwrap(), + ); pb.set_message("Creating initramfs..."); // Create the cpio archive with progress @@ -238,12 +247,15 @@ fn create_initramfs(rootfs: &Path) -> Result { let file_count = pb.position(); // Write directly to file (no compression) - std::fs::write(&initramfs_path, &cpio_data) - .context("Failed to write initramfs file")?; + std::fs::write(&initramfs_path, &cpio_data).context("Failed to write initramfs file")?; // Finish progress bar pb.finish_and_clear(); - veprintln!("Initramfs created: {} bytes, {} files", total_bytes, file_count); + veprintln!( + "Initramfs created: {} bytes, {} files", + total_bytes, + file_count + ); Ok(initramfs_path) } @@ -251,41 +263,41 @@ fn create_initramfs(rootfs: &Path) -> Result { /// Create a newc-format cpio archive from a directory using the cpio crate fn create_cpio_archive(rootfs: &Path, pb: &ProgressBar) -> Result> { let mut archive = Vec::new(); - + // Track seen inodes to handle hard links properly let mut seen_inodes = std::collections::HashMap::new(); - + // Collect all entries with their data let entries = collect_entries(rootfs, rootfs, pb, &mut seen_inodes)?; - + // Collect entry names for checking existence later let entry_names: Vec<&str> = entries.iter().map(|(n, _, _, _, _)| n.as_str()).collect(); - + pb.set_message("Writing initramfs..."); - + // Write each entry using the cpio crate for (name, mode, mtime, nlink, data) in &entries { let file_size = data.len() as u32; - + let builder = NewcBuilder::new(name) .mode(*mode) .uid(0) .gid(0) .nlink(*nlink) .mtime(*mtime); - + // Write header and get a writer let mut writer = builder.write(&mut archive, file_size); - + // Write the file content - writer.write_all(data) + writer + .write_all(data) .context("Failed to write file content to cpio archive")?; - + // Finish this entry (returns the underlying writer) - writer.finish() - .context("Failed to finish cpio entry")?; + writer.finish().context("Failed to finish cpio entry")?; } - + // Add essential device nodes for serial console // These are character devices (mode 0o020xxx) let device_nodes = [ @@ -296,13 +308,13 @@ fn create_cpio_archive(rootfs: &Path, pb: &ProgressBar) -> Result> { // /dev/tty - controlling terminal (major 5, minor 0) ("dev/tty", 0o020666, 5, 0), ]; - + for (name, mode, major, minor) in device_nodes { // Check if this device node already exists in the archive if entry_names.contains(&name) { continue; } - + let builder = NewcBuilder::new(name) .mode(mode) .uid(0) @@ -311,13 +323,14 @@ fn create_cpio_archive(rootfs: &Path, pb: &ProgressBar) -> Result> { .mtime(0) .rdev_major(major) .rdev_minor(minor); - + // Device nodes have zero size let writer = builder.write(&mut archive, 0); - writer.finish() + writer + .finish() .context("Failed to finish device node entry")?; } - + // Add the /init script that will be run as PID 1 // This script handles hostname setup, shell execution, and poweroff on exit // Uses /proc/sysrq-trigger for poweroff since poweroff command may not be available @@ -369,16 +382,17 @@ fi .gid(0) .nlink(1) .mtime(0); - + let mut init_writer = init_builder.write(&mut archive, init_data.len() as u32); - init_writer.write_all(init_data) + init_writer + .write_all(init_data) .context("Failed to write init script to cpio archive")?; - init_writer.finish() + init_writer + .finish() .context("Failed to finish init script entry")?; - + // Write the trailer (takes ownership and returns the writer) - archive = newc::trailer(archive) - .context("Failed to write cpio trailer")?; + archive = newc::trailer(archive).context("Failed to write cpio trailer")?; Ok(archive) } @@ -393,33 +407,41 @@ fn collect_entries( ) -> Result> { let mut entries = Vec::new(); let mut total_data: u64 = 0; - + // Read directory entries let dir_entries: Vec<_> = match std::fs::read_dir(current) { Ok(entries) => entries.collect::>()?, Err(e) => { - veprintln!("Warning: cannot read directory {}: {}", current.display(), e); + veprintln!( + "Warning: cannot read directory {}: {}", + current.display(), + e + ); return Ok(entries); } }; - + for entry in dir_entries { let path = entry.path(); - + // Get metadata let metadata = match std::fs::symlink_metadata(&path) { Ok(m) => m, Err(e) => { - veprintln!("Warning: skipping {} due to metadata error: {}", path.display(), e); + veprintln!( + "Warning: skipping {} due to metadata error: {}", + path.display(), + e + ); continue; } }; - + // Increment progress counter pb.inc(1); - + let file_type = metadata.file_type(); - + // Determine mode (file type + permissions from filesystem) let mode = if file_type.is_dir() { // Directory: preserve permissions, ensure at least rwx for owner @@ -432,16 +454,16 @@ fn collect_entries( } else { continue; // Skip other types (sockets, fifos, etc.) }; - + // Build the entry name (relative path from base) let relative = path.strip_prefix(base).unwrap(); let entry_name = relative.to_string_lossy().into_owned(); - + // Handle hard links: only store data for first occurrence let (data, nlink) = if file_type.is_file() && metadata.nlink() > 1 { // This file has multiple hard links - check if we've seen it before let inode_key = (metadata.dev(), metadata.ino()); - + if let Some(_first_path) = seen_inodes.get(&inode_key) { // We've seen this inode before - create a hard link entry with no data (Vec::new(), metadata.nlink() as u32) @@ -479,10 +501,10 @@ fn collect_entries( // Directory (Vec::new(), 2) }; - + total_data += data.len() as u64; entries.push((entry_name, mode, metadata.mtime() as u32, nlink, data)); - + // Recurse into directories if file_type.is_dir() { let mut sub_entries = collect_entries(base, &path, pb, seen_inodes)?; @@ -492,10 +514,14 @@ fn collect_entries( entries.append(&mut sub_entries); } } - + // Only print summary for the root directory if current == base { - veprintln!("Collected {} entries, {} bytes total data", entries.len(), total_data); + veprintln!( + "Collected {} entries, {} bytes total data", + entries.len(), + total_data + ); } Ok(entries) } diff --git a/src/utils.rs b/src/utils.rs index d849dec..5952a0d 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -196,11 +196,7 @@ pub fn validate_memory_string(s: &str) -> Result<()> { let suffix = s.chars().last().unwrap(); let has_suffix = suffix.is_ascii_alphabetic(); - let numeric_part = if has_suffix { - &s[..s.len() - 1] - } else { - s - }; + let numeric_part = if has_suffix { &s[..s.len() - 1] } else { s }; // Check for negative numbers if numeric_part.starts_with('-') {