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