feat: use cpio crate for initramfs creation
Add the `cpio` crate as dependency, removing e2fsprogs external dependency.
This commit is contained in:
Generated
+7
@@ -243,6 +243,12 @@ version = "0.8.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpio"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "938e716cb1ade5d6c8f959c13a7248b889c07491fc7e41167c3afe20f8f0de1e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crc32fast"
|
name = "crc32fast"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -296,6 +302,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
|
"cpio",
|
||||||
"dirs",
|
"dirs",
|
||||||
"flate2",
|
"flate2",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ anyhow = "1"
|
|||||||
# Utilities
|
# Utilities
|
||||||
dirs = "6"
|
dirs = "6"
|
||||||
which = "7"
|
which = "7"
|
||||||
|
cpio = "0.4"
|
||||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-util"] }
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-util"] }
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
indicatif = "0.18"
|
indicatif = "0.18"
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ ecr --arch arm64 alpine -- uname -m
|
|||||||
# Always pull a fresh image
|
# Always pull a fresh image
|
||||||
ecr --no-cache fedora
|
ecr --no-cache fedora
|
||||||
|
|
||||||
# Boot with QEMU system emulation (requires qemu-system-<arch> and e2fsprogs)
|
# Boot with QEMU system emulation (requires qemu-system-<arch>)
|
||||||
ecr --kernel /boot/vmlinuz ubuntu
|
ecr --kernel /boot/vmlinuz ubuntu
|
||||||
|
|
||||||
# Boot with custom memory
|
# Boot with custom memory
|
||||||
@@ -75,15 +75,14 @@ ecr --kernel /boot/vmlinuz alpine
|
|||||||
```
|
```
|
||||||
|
|
||||||
This mode:
|
This mode:
|
||||||
- Creates an ext4 disk image from the rootfs
|
- Creates a gzipped CPIO initramfs from the rootfs
|
||||||
- Boots QEMU with your kernel
|
- Boots QEMU with your kernel
|
||||||
- Provides full VM isolation
|
- Provides full VM isolation
|
||||||
- Works for any architecture (no binfmt_misc needed)
|
- Works for any architecture (no binfmt_misc needed)
|
||||||
|
|
||||||
Requirements:
|
Requirements:
|
||||||
- `qemu-system-<arch>` installed
|
- `qemu-system-<arch>` installed
|
||||||
- `e2fsprogs` for disk image creation
|
- Kernel with serial console support
|
||||||
- Kernel with virtio support
|
|
||||||
|
|
||||||
## Supported distributions
|
## Supported distributions
|
||||||
|
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ No action required. Modern qemu-user-static packages register binfmt_misc with t
|
|||||||
|
|
||||||
## QEMU System Emulation Mode
|
## QEMU System Emulation Mode
|
||||||
|
|
||||||
When `--kernel` is specified, ecr switches from namespace/chroot mode to QEMU system emulation. The extracted rootfs is converted to a disk image and booted with the provided kernel.
|
When `--kernel` is specified, ecr switches from namespace/chroot mode to QEMU system emulation. The extracted rootfs is converted to a gzipped CPIO initramfs and booted with the provided kernel.
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
@@ -201,24 +201,20 @@ ecr --kernel /boot/vmlinuz debian -- /bin/sh -c "echo hello"
|
|||||||
|
|
||||||
1. Download/cache rootfs tarball (same as namespace mode)
|
1. Download/cache rootfs tarball (same as namespace mode)
|
||||||
2. Extract tarball to temporary directory
|
2. Extract tarball to temporary directory
|
||||||
3. Create ext4 disk image from rootfs using `mke2fs -d` (requires `e2fsprogs`)
|
3. Create gzipped CPIO initramfs from rootfs
|
||||||
4. Launch QEMU with:
|
4. Launch QEMU with:
|
||||||
- `-kernel <path>` - provided kernel
|
- `-kernel <path>` - provided kernel
|
||||||
- `-append "root=/dev/vda rw console=ttyS0"` - kernel command line
|
- `-initrd initramfs.cpio.gz` - rootfs as initramfs
|
||||||
|
- `-append "console=ttyS0 rdinit=/bin/sh"` - kernel command line
|
||||||
- `-m <memory>` - memory size (default 2G)
|
- `-m <memory>` - memory size (default 2G)
|
||||||
- `-nographic` - console on stdio
|
- `-display none -serial mon:stdio` - console on stdio
|
||||||
- `-drive file=rootfs.img,format=raw,if=virtio` - rootfs disk
|
|
||||||
- `-netdev user,id=net0 -device virtio-net-pci,netdev=net0` - network
|
- `-netdev user,id=net0 -device virtio-net-pci,netdev=net0` - network
|
||||||
5. Wait for QEMU to exit
|
5. Wait for QEMU to exit
|
||||||
6. Cleanup temporary files
|
6. Cleanup temporary files
|
||||||
|
|
||||||
### Disk Image Creation
|
### Initramfs Creation
|
||||||
|
|
||||||
The rootfs directory is converted to an ext4 disk image using `mke2fs -t ext4 -d <rootfs>`. This requires the `e2fsprogs` package:
|
The rootfs directory is converted to a gzipped CPIO archive (newc format) using the `cpio` crate.
|
||||||
|
|
||||||
- Ubuntu/Debian: `sudo apt install e2fsprogs`
|
|
||||||
- Arch: `sudo pacman -S e2fsprogs`
|
|
||||||
- Alpine: `sudo apk add e2fsprogs`
|
|
||||||
|
|
||||||
### Architecture Support
|
### Architecture Support
|
||||||
|
|
||||||
@@ -234,8 +230,7 @@ The rootfs directory is converted to an ext4 disk image using `mke2fs -t ext4 -d
|
|||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
- QEMU system emulator installed (`qemu-system-<arch>`)
|
- QEMU system emulator installed (`qemu-system-<arch>`)
|
||||||
- `e2fsprogs` for disk image creation
|
- Kernel with required drivers (serial console, virtio-net for network)
|
||||||
- Kernel with virtio support (for disk and network drivers)
|
|
||||||
|
|
||||||
### Differences from Namespace Mode
|
### Differences from Namespace Mode
|
||||||
|
|
||||||
|
|||||||
+141
-201
@@ -1,8 +1,10 @@
|
|||||||
use crate::veprintln;
|
use crate::veprintln;
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use cpio::{newc, NewcBuilder};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::PathBuf;
|
use std::os::unix::fs::MetadataExt;
|
||||||
use std::process::{Command, Stdio};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
/// QEMU system emulation configuration
|
/// QEMU system emulation configuration
|
||||||
pub struct QemuConfig {
|
pub struct QemuConfig {
|
||||||
@@ -36,8 +38,8 @@ pub fn launch_qemu(config: QemuConfig) -> Result<()> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a disk image from the rootfs
|
// Create a gzipped cpio initramfs from the rootfs
|
||||||
let disk_image = create_disk_image(&config.rootfs_path)?;
|
let initramfs = create_initramfs(&config.rootfs_path)?;
|
||||||
|
|
||||||
// Get QEMU binary for architecture
|
// Get QEMU binary for architecture
|
||||||
let qemu_bin = qemu_binary_for_arch(&config.arch);
|
let qemu_bin = qemu_binary_for_arch(&config.arch);
|
||||||
@@ -52,22 +54,22 @@ pub fn launch_qemu(config: QemuConfig) -> Result<()> {
|
|||||||
))?;
|
))?;
|
||||||
|
|
||||||
// Build kernel command line
|
// Build kernel command line
|
||||||
// Container rootfs images don't have /sbin/init - they expect a command as PID 1
|
// For initramfs boot, use rdinit= instead of init=
|
||||||
// We use init=/bin/sh as default, and if a command is specified, we pass it to sh
|
// No root= needed as initramfs becomes the rootfs
|
||||||
let kernel_append = if let Some(ref cmd) = config.command {
|
let kernel_append = if let Some(ref cmd) = config.command {
|
||||||
let cmd_str = cmd.join(" ");
|
let cmd_str = cmd.join(" ");
|
||||||
format!(
|
format!(
|
||||||
"root=/dev/vda rw console=ttyS0 init=/bin/sh -- -c \"{}\"",
|
"console=ttyS0 rdinit=/bin/sh -- -c \"{}\"",
|
||||||
cmd_str
|
cmd_str
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// Default to interactive shell
|
// Default to interactive shell
|
||||||
"root=/dev/vda rw console=ttyS0 init=/bin/sh".to_string()
|
"console=ttyS0 rdinit=/bin/sh".to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
veprintln!("Launching QEMU: {}", qemu_bin);
|
veprintln!("Launching QEMU: {}", qemu_bin);
|
||||||
veprintln!(" Kernel: {}", config.kernel_path.display());
|
veprintln!(" Kernel: {}", config.kernel_path.display());
|
||||||
veprintln!(" Disk image: {}", disk_image.display());
|
veprintln!(" Initramfs: {}", initramfs.display());
|
||||||
veprintln!(" Memory: {}", config.memory);
|
veprintln!(" Memory: {}", config.memory);
|
||||||
veprintln!(" Kernel append: {}", kernel_append);
|
veprintln!(" Kernel append: {}", kernel_append);
|
||||||
|
|
||||||
@@ -77,6 +79,8 @@ pub fn launch_qemu(config: QemuConfig) -> Result<()> {
|
|||||||
let args = vec![
|
let args = vec![
|
||||||
"-kernel".to_string(),
|
"-kernel".to_string(),
|
||||||
config.kernel_path.to_string_lossy().to_string(),
|
config.kernel_path.to_string_lossy().to_string(),
|
||||||
|
"-initrd".to_string(),
|
||||||
|
initramfs.to_string_lossy().to_string(),
|
||||||
"-append".to_string(),
|
"-append".to_string(),
|
||||||
kernel_append,
|
kernel_append,
|
||||||
"-m".to_string(),
|
"-m".to_string(),
|
||||||
@@ -85,11 +89,6 @@ pub fn launch_qemu(config: QemuConfig) -> Result<()> {
|
|||||||
"none".to_string(),
|
"none".to_string(),
|
||||||
"-serial".to_string(),
|
"-serial".to_string(),
|
||||||
"mon:stdio".to_string(),
|
"mon:stdio".to_string(),
|
||||||
"-drive".to_string(),
|
|
||||||
format!(
|
|
||||||
"file={},format=raw,if=virtio",
|
|
||||||
disk_image.to_string_lossy()
|
|
||||||
),
|
|
||||||
"-netdev".to_string(),
|
"-netdev".to_string(),
|
||||||
"user,id=net0".to_string(),
|
"user,id=net0".to_string(),
|
||||||
"-device".to_string(),
|
"-device".to_string(),
|
||||||
@@ -102,9 +101,9 @@ pub fn launch_qemu(config: QemuConfig) -> Result<()> {
|
|||||||
.status()
|
.status()
|
||||||
.context("Failed to execute QEMU")?;
|
.context("Failed to execute QEMU")?;
|
||||||
|
|
||||||
// Cleanup disk image
|
// Cleanup initramfs
|
||||||
if let Err(e) = std::fs::remove_file(&disk_image) {
|
if let Err(e) = std::fs::remove_file(&initramfs) {
|
||||||
veprintln!("Warning: failed to cleanup disk image: {}", e);
|
veprintln!("Warning: failed to cleanup initramfs: {}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
@@ -143,203 +142,144 @@ fn get_arch_package_suffix(arch: &str) -> &str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a raw disk image from a directory
|
/// Create a gzipped cpio initramfs from a directory
|
||||||
fn create_disk_image(rootfs: &PathBuf) -> Result<PathBuf> {
|
fn create_initramfs(rootfs: &PathBuf) -> Result<PathBuf> {
|
||||||
veprintln!("Creating disk image from rootfs...");
|
veprintln!("Creating initramfs from rootfs...");
|
||||||
|
|
||||||
// Create a temporary file for the disk image
|
// Create a temporary file for the initramfs
|
||||||
let disk_image = rootfs.parent().unwrap().join("rootfs.img");
|
let initramfs_path = rootfs.parent().unwrap().join("initramfs.cpio.gz");
|
||||||
|
|
||||||
// Use mke2fs to create an ext4 filesystem image
|
// Create the cpio archive
|
||||||
// First, calculate size needed (du -sb)
|
let cpio_data = create_cpio_archive(rootfs)?;
|
||||||
let du_output = Command::new("du")
|
|
||||||
.arg("-sb")
|
|
||||||
.arg(rootfs)
|
|
||||||
.output()
|
|
||||||
.context("Failed to calculate rootfs size")?;
|
|
||||||
|
|
||||||
let size_str = String::from_utf8_lossy(&du_output.stdout);
|
// Compress with gzip
|
||||||
let size: u64 = size_str
|
let mut output_file = std::fs::File::create(&initramfs_path)
|
||||||
.split_whitespace()
|
.context("Failed to create initramfs file")?;
|
||||||
.next()
|
|
||||||
.context("Failed to parse du output")?
|
|
||||||
.parse()
|
|
||||||
.context("Failed to parse size")?;
|
|
||||||
|
|
||||||
// Add 50% overhead for filesystem metadata, journal, and some free space
|
let mut encoder = flate2::write::GzEncoder::new(&mut output_file, flate2::Compression::default());
|
||||||
// ext4 with journal can have significant overhead
|
encoder.write_all(&cpio_data)
|
||||||
let image_size = size + (size / 2);
|
.context("Failed to write compressed initramfs")?;
|
||||||
// Minimum 64MB for small rootfs to ensure enough space for metadata
|
encoder.finish()
|
||||||
let image_size = image_size.max(64 * 1024 * 1024);
|
.context("Failed to finalize gzip compression")?;
|
||||||
|
|
||||||
veprintln!("Rootfs size: {} bytes, image size: {} bytes", size, image_size);
|
veprintln!("Initramfs created: {} bytes (uncompressed)", cpio_data.len());
|
||||||
|
|
||||||
// Create the image file
|
Ok(initramfs_path)
|
||||||
let image_file = std::fs::File::create(&disk_image)
|
|
||||||
.context("Failed to create disk image file")?;
|
|
||||||
|
|
||||||
// Pre-allocate the file
|
|
||||||
image_file
|
|
||||||
.set_len(image_size)
|
|
||||||
.context("Failed to allocate disk image")?;
|
|
||||||
drop(image_file);
|
|
||||||
|
|
||||||
// Try to use mke2fs to create an ext4 image with the directory contents
|
|
||||||
// This is the most efficient way on Linux
|
|
||||||
veprintln!("Running: mke2fs -t ext4 -d {} {}", rootfs.display(), disk_image.display());
|
|
||||||
let mke2fs_result = Command::new("mke2fs")
|
|
||||||
.arg("-t")
|
|
||||||
.arg("ext4")
|
|
||||||
.arg("-d")
|
|
||||||
.arg(rootfs)
|
|
||||||
.arg(&disk_image)
|
|
||||||
.output();
|
|
||||||
|
|
||||||
match mke2fs_result {
|
|
||||||
Ok(output) => {
|
|
||||||
if output.status.success() {
|
|
||||||
veprintln!("Disk image created successfully with mke2fs");
|
|
||||||
// Verify the image has content by checking if we can list files
|
|
||||||
let verify = Command::new("debugfs")
|
|
||||||
.arg("-R")
|
|
||||||
.arg("ls -l /")
|
|
||||||
.arg(&disk_image)
|
|
||||||
.output();
|
|
||||||
if let Ok(v) = verify {
|
|
||||||
veprintln!("Root directory contents:\n{}", String::from_utf8_lossy(&v.stdout));
|
|
||||||
}
|
|
||||||
return Ok(disk_image);
|
|
||||||
} else {
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
|
||||||
veprintln!("mke2fs failed!");
|
|
||||||
veprintln!(" stdout: {}", stdout);
|
|
||||||
veprintln!(" stderr: {}", stderr);
|
|
||||||
// Don't continue if mke2fs exists but failed - it's the only reliable method
|
|
||||||
return Err(anyhow!(
|
|
||||||
"mke2fs -d failed to create disk image.\n\
|
|
||||||
stdout: {}\n\
|
|
||||||
stderr: {}",
|
|
||||||
stdout, stderr
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
veprintln!("mke2fs not available: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: create a simple ext4 image and copy files
|
|
||||||
// Try mkfs.ext4
|
|
||||||
let mkfs_result = Command::new("mkfs.ext4")
|
|
||||||
.arg("-F")
|
|
||||||
.arg(&disk_image)
|
|
||||||
.output();
|
|
||||||
|
|
||||||
match mkfs_result {
|
|
||||||
Ok(output) => {
|
|
||||||
if !output.status.success() {
|
|
||||||
return Err(anyhow!(
|
|
||||||
"mkfs.ext4 failed: {}",
|
|
||||||
String::from_utf8_lossy(&output.stderr)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
return Err(anyhow!(
|
|
||||||
"Neither mke2fs nor mkfs.ext4 available. Install e2fsprogs:\n\
|
|
||||||
Ubuntu/Debian: sudo apt install e2fsprogs\n\
|
|
||||||
Arch: sudo pacman -S e2fsprogs\n\
|
|
||||||
Alpine: sudo apk add e2fsprogs\n\
|
|
||||||
Error: {}",
|
|
||||||
e
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mount the image and copy files
|
|
||||||
veprintln!("Mounting disk image and copying files...");
|
|
||||||
|
|
||||||
// Use debugfs to copy files (doesn't require root/mount)
|
|
||||||
let debugfs_result = copy_with_debugfs(rootfs, &disk_image)?;
|
|
||||||
|
|
||||||
if debugfs_result {
|
|
||||||
veprintln!("Disk image created successfully");
|
|
||||||
return Ok(disk_image);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If debugfs failed, we need to try mounting (requires root or fuse)
|
|
||||||
// This is a last resort
|
|
||||||
Err(anyhow!(
|
|
||||||
"Could not create disk image. Please ensure e2fsprogs is installed with mke2fs support.\n\
|
|
||||||
The mke2fs -d option is required for non-root disk image creation."
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Copy files to the disk image using debugfs
|
/// Create a newc-format cpio archive from a directory using the cpio crate
|
||||||
fn copy_with_debugfs(rootfs: &PathBuf, disk_image: &PathBuf) -> Result<bool> {
|
fn create_cpio_archive(rootfs: &Path) -> Result<Vec<u8>> {
|
||||||
// Use debugfs to write files - this doesn't require mounting
|
let mut archive = Vec::new();
|
||||||
let mut debugfs = match Command::new("debugfs")
|
|
||||||
.arg("-w")
|
// Collect all entries with their data
|
||||||
.arg(disk_image)
|
let entries = collect_entries(rootfs, rootfs)?;
|
||||||
.stdin(Stdio::piped())
|
|
||||||
.stdout(Stdio::piped())
|
// Write each entry using the cpio crate
|
||||||
.stderr(Stdio::piped())
|
for (name, mode, mtime, nlink, data) in entries {
|
||||||
.spawn()
|
let file_size = data.len() as u32;
|
||||||
{
|
|
||||||
Ok(child) => child,
|
let builder = NewcBuilder::new(&name)
|
||||||
Err(_) => return Ok(false),
|
.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)
|
||||||
|
.context("Failed to write file content to cpio archive")?;
|
||||||
|
|
||||||
|
// Finish this entry (returns the underlying writer)
|
||||||
|
writer.finish()
|
||||||
|
.context("Failed to finish cpio entry")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the trailer (takes ownership and returns the writer)
|
||||||
|
archive = newc::trailer(archive)
|
||||||
|
.context("Failed to write cpio trailer")?;
|
||||||
|
|
||||||
|
Ok(archive)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collect all filesystem entries recursively
|
||||||
|
fn collect_entries(base: &Path, current: &Path) -> Result<Vec<(String, u32, u32, u32, Vec<u8>)>> {
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
|
||||||
|
// Read directory entries
|
||||||
|
let dir_entries: Vec<_> = match std::fs::read_dir(current) {
|
||||||
|
Ok(entries) => entries.collect::<std::result::Result<_, _>>()?,
|
||||||
|
Err(e) => {
|
||||||
|
veprintln!("Warning: cannot read directory {}: {}", current.display(), e);
|
||||||
|
return Ok(entries);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let stdin = debugfs.stdin.as_mut().context("Failed to open debugfs stdin")?;
|
for entry in dir_entries {
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
// Write files recursively
|
// Get metadata
|
||||||
fn write_directory(
|
let metadata = match std::fs::symlink_metadata(&path) {
|
||||||
dir: &PathBuf,
|
Ok(m) => m,
|
||||||
prefix: &str,
|
Err(e) => {
|
||||||
stdin: &mut std::process::ChildStdin,
|
veprintln!("Warning: skipping {} due to metadata error: {}", path.display(), e);
|
||||||
) -> Result<()> {
|
continue;
|
||||||
for entry in std::fs::read_dir(dir)? {
|
|
||||||
let entry = entry?;
|
|
||||||
let path = entry.path();
|
|
||||||
let name = entry.file_name().to_string_lossy().into_owned();
|
|
||||||
let target = if prefix.is_empty() {
|
|
||||||
format!("/{}", name)
|
|
||||||
} else {
|
|
||||||
format!("{}/{}", prefix, name)
|
|
||||||
};
|
|
||||||
|
|
||||||
if path.is_dir() {
|
|
||||||
// Create directory
|
|
||||||
writeln!(stdin, "mkdir {}", target)?;
|
|
||||||
write_directory(&path, &target, stdin)?;
|
|
||||||
} else {
|
|
||||||
// Write file
|
|
||||||
writeln!(stdin, "write {} {}", path.display(), target)?;
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
0o040000 | (metadata.mode() & 0o7777)
|
||||||
|
} else if file_type.is_symlink() {
|
||||||
|
0o120777 // symlink with rwxrwxrwx (permissions don't matter for symlinks)
|
||||||
|
} else if file_type.is_file() {
|
||||||
|
// Regular file: preserve permissions from filesystem
|
||||||
|
0o100000 | (metadata.mode() & 0o7777)
|
||||||
|
} else {
|
||||||
|
continue; // Skip other types (sockets, fifos, etc.)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get file content or symlink target
|
||||||
|
let data: Vec<u8> = if file_type.is_file() {
|
||||||
|
match std::fs::read(&path) {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(e) => {
|
||||||
|
veprintln!("Warning: cannot read file {}: {}", path.display(), e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if file_type.is_symlink() {
|
||||||
|
match std::fs::read_link(&path) {
|
||||||
|
Ok(target) => target.to_string_lossy().into_owned().into_bytes(),
|
||||||
|
Err(e) => {
|
||||||
|
veprintln!("Warning: cannot read symlink {}: {}", path.display(), e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build the entry name (relative path from base)
|
||||||
|
let relative = path.strip_prefix(base).unwrap();
|
||||||
|
let entry_name = relative.to_string_lossy().into_owned();
|
||||||
|
|
||||||
|
// nlink: directories have 2 (. and ..), files/symlinks have 1
|
||||||
|
let nlink = if file_type.is_dir() { 2 } else { 1 };
|
||||||
|
|
||||||
|
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)?;
|
||||||
|
entries.append(&mut sub_entries);
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(e) = write_directory(rootfs, "", stdin) {
|
Ok(entries)
|
||||||
veprintln!("debugfs write failed: {}", e);
|
|
||||||
let _ = debugfs.kill();
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// stdin is implicitly dropped here when it goes out of scope
|
|
||||||
|
|
||||||
let output = debugfs
|
|
||||||
.wait_with_output()
|
|
||||||
.context("Failed to wait for debugfs")?;
|
|
||||||
|
|
||||||
if !output.status.success() {
|
|
||||||
veprintln!(
|
|
||||||
"debugfs failed: {}",
|
|
||||||
String::from_utf8_lossy(&output.stderr)
|
|
||||||
);
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(true)
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user