fix: initramfs hard links, streaming archive, verbatim argv transport
- hard links in the rootfs were written as zero-size entries with no inode set (all zero), so the kernel could link them to the wrong file; assign a synthetic inode per (device, inode) group. - build the cpio archive by streaming entries to disk instead of holding the whole archive in memory. - the VM command was joined into a single string and shell-expanded in the guest, mangling quotes and nested commands; pass the argv base64-encoded (ECR_ARGV) and exec it verbatim. - parse ecr parameters from /proc/cmdline in the init script, set the hostname via procfs (arch has no hostname binary), and give the serial console a moment to drain before poweroff.
This commit is contained in:
Generated
+1
@@ -301,6 +301,7 @@ name = "ecr"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"base64",
|
||||||
"clap",
|
"clap",
|
||||||
"cpio",
|
"cpio",
|
||||||
"dirs",
|
"dirs",
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ anyhow = "1"
|
|||||||
dirs = "6"
|
dirs = "6"
|
||||||
which = "7"
|
which = "7"
|
||||||
cpio = "0.4"
|
cpio = "0.4"
|
||||||
|
base64 = "0.22"
|
||||||
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"
|
||||||
|
|||||||
+313
-120
@@ -97,10 +97,19 @@ pub fn launch_qemu(config: QemuConfig) -> Result<()> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
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(" ");
|
// The argv travels through the kernel cmdline, where quotes and
|
||||||
|
// spaces would be mangled — pass each element base64-encoded,
|
||||||
|
// comma-separated (the base64 alphabet contains neither). The init
|
||||||
|
// script decodes it back and execs the argv verbatim.
|
||||||
|
use base64::Engine as _;
|
||||||
|
let argv_b64 = cmd
|
||||||
|
.iter()
|
||||||
|
.map(|arg| base64::engine::general_purpose::STANDARD.encode(arg.as_bytes()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(",");
|
||||||
format!(
|
format!(
|
||||||
"console=ttyS0{} ECR_SHELL={} ECR_CMD=\"{}\" ECR_HOSTNAME={}",
|
"console=ttyS0{} ECR_SHELL={} ECR_ARGV={} ECR_HOSTNAME={}",
|
||||||
quiet_flag, shell, cmd_str, hostname
|
quiet_flag, shell, argv_b64, hostname
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
format!(
|
format!(
|
||||||
@@ -226,10 +235,9 @@ fn check_kvm_capabilities() -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create an uncompressed cpio initramfs from a directory
|
/// Create an uncompressed cpio initramfs from a directory.
|
||||||
/// A filesystem entry for cpio archives: (name, mode, mtime, nlink, data)
|
/// Entries are streamed straight to disk so large rootfs images never have
|
||||||
type CpioEntry = (String, u32, u32, u32, Vec<u8>);
|
/// to fit in memory as a whole archive.
|
||||||
|
|
||||||
fn create_initramfs(rootfs: &Path) -> Result<PathBuf> {
|
fn create_initramfs(rootfs: &Path) -> Result<PathBuf> {
|
||||||
// Create a temporary file for the initramfs (uncompressed cpio)
|
// Create a temporary file for the initramfs (uncompressed cpio)
|
||||||
// Use a temp file in the same directory as rootfs, or fall back to /tmp
|
// Use a temp file in the same directory as rootfs, or fall back to /tmp
|
||||||
@@ -247,16 +255,26 @@ fn create_initramfs(rootfs: &Path) -> Result<PathBuf> {
|
|||||||
);
|
);
|
||||||
pb.set_message("Creating initramfs...");
|
pb.set_message("Creating initramfs...");
|
||||||
|
|
||||||
// Create the cpio archive with progress
|
let file = std::fs::File::create(&initramfs_path)
|
||||||
let cpio_data = create_cpio_archive(rootfs, &pb)?;
|
.with_context(|| format!("Failed to create {}", initramfs_path.display()))?;
|
||||||
let total_bytes = cpio_data.len() as u64;
|
let mut writer = std::io::BufWriter::new(file);
|
||||||
let file_count = pb.position();
|
|
||||||
|
|
||||||
// Write directly to file (no compression)
|
let result = write_cpio_archive(rootfs, &mut writer, &pb);
|
||||||
std::fs::write(&initramfs_path, &cpio_data).context("Failed to write initramfs file")?;
|
let flushed = writer.flush().context("Failed to flush initramfs");
|
||||||
|
|
||||||
|
// On any error, don't leave a partial archive behind
|
||||||
|
if let Err(e) = result.or(flushed) {
|
||||||
|
std::fs::remove_file(&initramfs_path).ok();
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
|
||||||
// Finish progress bar
|
// Finish progress bar
|
||||||
|
let file_count = pb.position();
|
||||||
pb.finish_and_clear();
|
pb.finish_and_clear();
|
||||||
|
|
||||||
|
let total_bytes = std::fs::metadata(&initramfs_path)
|
||||||
|
.map(|m| m.len())
|
||||||
|
.unwrap_or(0);
|
||||||
veprintln!(
|
veprintln!(
|
||||||
"Initramfs created: {} bytes, {} files",
|
"Initramfs created: {} bytes, {} files",
|
||||||
total_bytes,
|
total_bytes,
|
||||||
@@ -266,43 +284,34 @@ fn create_initramfs(rootfs: &Path) -> Result<PathBuf> {
|
|||||||
Ok(initramfs_path)
|
Ok(initramfs_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a newc-format cpio archive from a directory using the cpio crate
|
/// Write the newc-format cpio archive for a directory tree, streaming to `writer`
|
||||||
fn create_cpio_archive(rootfs: &Path, pb: &ProgressBar) -> Result<Vec<u8>> {
|
fn write_cpio_archive<W: Write>(rootfs: &Path, writer: &mut W, pb: &ProgressBar) -> Result<()> {
|
||||||
let mut archive = Vec::new();
|
// Track hard links by (device, inode): the value is the synthetic cpio
|
||||||
|
// inode assigned to the first occurrence. The Linux initramfs loader
|
||||||
|
// turns later zero-size entries sharing that inode into hard links.
|
||||||
|
let mut seen_inodes: std::collections::HashMap<(u64, u64), u32> =
|
||||||
|
std::collections::HashMap::new();
|
||||||
|
let mut next_ino: u32 = 1;
|
||||||
|
// Names of entries already written, so we can skip duplicate device nodes
|
||||||
|
let mut written_names: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||||
|
let mut total_data: u64 = 0;
|
||||||
|
|
||||||
// Track seen inodes to handle hard links properly
|
write_dir_entries(
|
||||||
let mut seen_inodes = std::collections::HashMap::new();
|
rootfs,
|
||||||
|
rootfs,
|
||||||
|
writer,
|
||||||
|
pb,
|
||||||
|
&mut seen_inodes,
|
||||||
|
&mut next_ino,
|
||||||
|
&mut written_names,
|
||||||
|
&mut total_data,
|
||||||
|
)?;
|
||||||
|
|
||||||
// Collect all entries with their data
|
veprintln!(
|
||||||
let entries = collect_entries(rootfs, rootfs, pb, &mut seen_inodes)?;
|
"Collected {} entries, {} bytes total data",
|
||||||
|
written_names.len(),
|
||||||
// Collect entry names for checking existence later
|
total_data
|
||||||
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)
|
|
||||||
.context("Failed to write file content to cpio archive")?;
|
|
||||||
|
|
||||||
// Finish this entry (returns the underlying writer)
|
|
||||||
writer.finish().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)
|
||||||
@@ -317,7 +326,7 @@ fn create_cpio_archive(rootfs: &Path, pb: &ProgressBar) -> Result<Vec<u8>> {
|
|||||||
|
|
||||||
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 written_names.contains(name) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,8 +340,8 @@ fn create_cpio_archive(rootfs: &Path, pb: &ProgressBar) -> Result<Vec<u8>> {
|
|||||||
.rdev_minor(minor);
|
.rdev_minor(minor);
|
||||||
|
|
||||||
// Device nodes have zero size
|
// Device nodes have zero size
|
||||||
let writer = builder.write(&mut archive, 0);
|
let entry_writer = builder.write(&mut *writer, 0);
|
||||||
writer
|
entry_writer
|
||||||
.finish()
|
.finish()
|
||||||
.context("Failed to finish device node entry")?;
|
.context("Failed to finish device node entry")?;
|
||||||
}
|
}
|
||||||
@@ -348,10 +357,22 @@ mount -t proc proc /proc
|
|||||||
mount -t sysfs sysfs /sys
|
mount -t sysfs sysfs /sys
|
||||||
mount -t devtmpfs devtmpfs /dev 2>/dev/null || true
|
mount -t devtmpfs devtmpfs /dev 2>/dev/null || true
|
||||||
|
|
||||||
# Set hostname from kernel cmdline
|
# Parse our parameters straight from the kernel cmdline. More robust than
|
||||||
|
# relying on the kernel forwarding unknown key=value params to init's env.
|
||||||
|
ECR_SHELL="/bin/sh"
|
||||||
|
ECR_ARGV=""
|
||||||
|
for param in $(cat /proc/cmdline); do
|
||||||
|
case "$param" in
|
||||||
|
ECR_SHELL=*) ECR_SHELL="${param#ECR_SHELL=}" ;;
|
||||||
|
ECR_ARGV=*) ECR_ARGV="${param#ECR_ARGV=}" ;;
|
||||||
|
ECR_HOSTNAME=*) ECR_HOSTNAME="${param#ECR_HOSTNAME=}" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Set hostname from kernel cmdline (via procfs — no hostname binary needed)
|
||||||
if [ -n "$ECR_HOSTNAME" ]; then
|
if [ -n "$ECR_HOSTNAME" ]; then
|
||||||
echo "$ECR_HOSTNAME" > /etc/hostname
|
echo "$ECR_HOSTNAME" > /etc/hostname
|
||||||
hostname "$ECR_HOSTNAME"
|
echo "$ECR_HOSTNAME" > /proc/sys/kernel/hostname
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create console device if missing
|
# Create console device if missing
|
||||||
@@ -360,6 +381,9 @@ mknod -m 666 /dev/ttyS0 c 4 64 2>/dev/null || true
|
|||||||
|
|
||||||
# Function to poweroff - use sysrq-trigger which works without external binaries
|
# Function to poweroff - use sysrq-trigger which works without external binaries
|
||||||
do_poweroff() {
|
do_poweroff() {
|
||||||
|
# Give the serial console a moment to drain pending output, otherwise
|
||||||
|
# the last command output can be dropped when the VM powers off
|
||||||
|
sleep 1
|
||||||
# Silence kernel printk to suppress shutdown messages
|
# Silence kernel printk to suppress shutdown messages
|
||||||
echo 0 > /proc/sys/kernel/printk
|
echo 0 > /proc/sys/kernel/printk
|
||||||
# 'o' means power off, see Documentation/admin-guide/sysrq.rst
|
# 'o' means power off, see Documentation/admin-guide/sysrq.rst
|
||||||
@@ -371,49 +395,76 @@ do_poweroff() {
|
|||||||
# Trap exit to ensure poweroff runs
|
# Trap exit to ensure poweroff runs
|
||||||
trap do_poweroff EXIT
|
trap do_poweroff EXIT
|
||||||
|
|
||||||
# Run the shell or command
|
# Rebuild the argv: each element is base64-encoded, elements are separated
|
||||||
if [ -n "$ECR_CMD" ]; then
|
# by commas. Decoding into "$@" avoids any shell re-parsing of the command.
|
||||||
# Run command with the specified shell
|
set --
|
||||||
setsid sh -c "exec $ECR_SHELL </dev/ttyS0 >/dev/ttyS0 2>&1 -c '$ECR_CMD'"
|
if [ -n "$ECR_ARGV" ]; then
|
||||||
|
for enc in $(printf '%s' "$ECR_ARGV" | tr ',' ' '); do
|
||||||
|
dec=$(printf '%s' "$enc" | base64 -d 2>/dev/null)
|
||||||
|
set -- "$@" "$dec"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run the requested command verbatim, or an interactive shell in its own
|
||||||
|
# session (setsid enables job control on the serial console)
|
||||||
|
if [ "$#" -gt 0 ]; then
|
||||||
|
"$@"
|
||||||
else
|
else
|
||||||
# Run interactive shell
|
|
||||||
setsid sh -c "exec $ECR_SHELL </dev/ttyS0 >/dev/ttyS0 2>&1"
|
setsid sh -c "exec $ECR_SHELL </dev/ttyS0 >/dev/ttyS0 2>&1"
|
||||||
fi
|
fi
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
let init_data = init_script.as_bytes();
|
write_cpio_entry(writer, "init", 0o100755, 0, 1, 0, init_script.as_bytes())?;
|
||||||
let init_builder = NewcBuilder::new("init")
|
|
||||||
.mode(0o100755) // executable
|
|
||||||
.uid(0)
|
|
||||||
.gid(0)
|
|
||||||
.nlink(1)
|
|
||||||
.mtime(0);
|
|
||||||
|
|
||||||
let mut init_writer = init_builder.write(&mut archive, init_data.len() as u32);
|
// Write the trailer
|
||||||
init_writer
|
newc::trailer(&mut *writer).context("Failed to write cpio trailer")?;
|
||||||
.write_all(init_data)
|
|
||||||
.context("Failed to write init script to cpio archive")?;
|
|
||||||
init_writer
|
|
||||||
.finish()
|
|
||||||
.context("Failed to finish init script entry")?;
|
|
||||||
|
|
||||||
// Write the trailer (takes ownership and returns the writer)
|
Ok(())
|
||||||
archive = newc::trailer(archive).context("Failed to write cpio trailer")?;
|
|
||||||
|
|
||||||
Ok(archive)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Collect all filesystem entries recursively
|
/// Write a single newc-format cpio entry
|
||||||
/// Uses a HashMap to track hard links by (device, inode) - only stores data for first occurrence
|
fn write_cpio_entry<W: Write>(
|
||||||
fn collect_entries(
|
writer: &mut W,
|
||||||
|
name: &str,
|
||||||
|
mode: u32,
|
||||||
|
mtime: u32,
|
||||||
|
nlink: u32,
|
||||||
|
ino: u32,
|
||||||
|
data: &[u8],
|
||||||
|
) -> Result<()> {
|
||||||
|
let builder = NewcBuilder::new(name)
|
||||||
|
.mode(mode)
|
||||||
|
.uid(0)
|
||||||
|
.gid(0)
|
||||||
|
.ino(ino)
|
||||||
|
.nlink(nlink)
|
||||||
|
.mtime(mtime);
|
||||||
|
|
||||||
|
let mut entry_writer = builder.write(&mut *writer, data.len() as u32);
|
||||||
|
entry_writer
|
||||||
|
.write_all(data)
|
||||||
|
.with_context(|| format!("Failed to write {} to cpio archive", name))?;
|
||||||
|
entry_writer
|
||||||
|
.finish()
|
||||||
|
.with_context(|| format!("Failed to finish cpio entry {}", name))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Walk a directory tree, writing every entry to the cpio archive as it goes
|
||||||
|
/// Hard links are handled by assigning a synthetic inode to the first
|
||||||
|
/// occurrence of a (device, inode) pair; subsequent occurrences are written
|
||||||
|
/// as zero-size entries reusing that inode.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn write_dir_entries(
|
||||||
base: &Path,
|
base: &Path,
|
||||||
current: &Path,
|
current: &Path,
|
||||||
|
writer: &mut impl Write,
|
||||||
pb: &ProgressBar,
|
pb: &ProgressBar,
|
||||||
seen_inodes: &mut std::collections::HashMap<(u64, u64), String>,
|
seen_inodes: &mut std::collections::HashMap<(u64, u64), u32>,
|
||||||
) -> Result<Vec<CpioEntry>> {
|
next_ino: &mut u32,
|
||||||
let mut entries = Vec::new();
|
written_names: &mut std::collections::HashSet<String>,
|
||||||
let mut total_data: u64 = 0;
|
total_data: &mut u64,
|
||||||
|
) -> Result<()> {
|
||||||
// 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<_, _>>()?,
|
||||||
@@ -423,7 +474,7 @@ fn collect_entries(
|
|||||||
current.display(),
|
current.display(),
|
||||||
e
|
e
|
||||||
);
|
);
|
||||||
return Ok(entries);
|
return Ok(());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -465,25 +516,31 @@ fn collect_entries(
|
|||||||
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
|
// Determine (data, nlink, cpio inode).
|
||||||
let (data, nlink) = if file_type.is_file() && metadata.nlink() > 1 {
|
// The kernel's initramfs loader records the first entry of a hard-link
|
||||||
// This file has multiple hard links - check if we've seen it before
|
// group (the one carrying the data) and turns later zero-size entries
|
||||||
|
// with the same inode into sys_link calls.
|
||||||
|
let (data, nlink, cpio_ino) = if file_type.is_file() && metadata.nlink() > 1 {
|
||||||
let inode_key = (metadata.dev(), metadata.ino());
|
let inode_key = (metadata.dev(), metadata.ino());
|
||||||
|
match seen_inodes.get(&inode_key) {
|
||||||
if let Some(_first_path) = seen_inodes.get(&inode_key) {
|
Some(seen_cpio_ino) => {
|
||||||
// We've seen this inode before - create a hard link entry with no data
|
// Subsequent occurrence: zero-size hard-link entry
|
||||||
(Vec::new(), metadata.nlink() as u32)
|
(Vec::new(), metadata.nlink() as u32, *seen_cpio_ino)
|
||||||
} else {
|
}
|
||||||
// First occurrence - read the data and record this inode
|
None => {
|
||||||
seen_inodes.insert(inode_key, entry_name.clone());
|
// First occurrence: read the data, assign a synthetic inode
|
||||||
let data = match std::fs::read(&path) {
|
let cpio_ino = *next_ino;
|
||||||
Ok(data) => data,
|
*next_ino = next_ino.wrapping_add(1);
|
||||||
Err(e) => {
|
seen_inodes.insert(inode_key, cpio_ino);
|
||||||
veprintln!("Warning: cannot read file {}: {}", path.display(), e);
|
let data = match std::fs::read(&path) {
|
||||||
continue;
|
Ok(data) => data,
|
||||||
}
|
Err(e) => {
|
||||||
};
|
veprintln!("Warning: cannot read file {}: {}", path.display(), e);
|
||||||
(data, metadata.nlink() as u32)
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(data, metadata.nlink() as u32, cpio_ino)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if file_type.is_file() {
|
} else if file_type.is_file() {
|
||||||
// Regular file with nlink=1
|
// Regular file with nlink=1
|
||||||
@@ -494,10 +551,10 @@ fn collect_entries(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
(data, 1)
|
(data, 1, 0)
|
||||||
} else if file_type.is_symlink() {
|
} else if file_type.is_symlink() {
|
||||||
match std::fs::read_link(&path) {
|
match std::fs::read_link(&path) {
|
||||||
Ok(target) => (target.to_string_lossy().into_owned().into_bytes(), 1),
|
Ok(target) => (target.to_string_lossy().into_owned().into_bytes(), 1, 0),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
veprintln!("Warning: cannot read symlink {}: {}", path.display(), e);
|
veprintln!("Warning: cannot read symlink {}: {}", path.display(), e);
|
||||||
continue;
|
continue;
|
||||||
@@ -505,29 +562,165 @@ fn collect_entries(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Directory
|
// Directory
|
||||||
(Vec::new(), 2)
|
(Vec::new(), 2, 0)
|
||||||
};
|
};
|
||||||
|
|
||||||
total_data += data.len() as u64;
|
*total_data += data.len() as u64;
|
||||||
entries.push((entry_name, mode, metadata.mtime() as u32, nlink, data));
|
|
||||||
|
write_cpio_entry(
|
||||||
|
writer,
|
||||||
|
&entry_name,
|
||||||
|
mode,
|
||||||
|
metadata.mtime() as u32,
|
||||||
|
nlink,
|
||||||
|
cpio_ino,
|
||||||
|
&data,
|
||||||
|
)?;
|
||||||
|
written_names.insert(entry_name);
|
||||||
|
|
||||||
// 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)?;
|
write_dir_entries(
|
||||||
for (_, _, _, _, d) in &sub_entries {
|
base,
|
||||||
total_data += d.len() as u64;
|
&path,
|
||||||
}
|
writer,
|
||||||
entries.append(&mut sub_entries);
|
pb,
|
||||||
|
seen_inodes,
|
||||||
|
next_ino,
|
||||||
|
written_names,
|
||||||
|
total_data,
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only print summary for the root directory
|
Ok(())
|
||||||
if current == base {
|
}
|
||||||
veprintln!(
|
|
||||||
"Collected {} entries, {} bytes total data",
|
#[cfg(test)]
|
||||||
entries.len(),
|
mod tests {
|
||||||
total_data
|
use super::*;
|
||||||
|
use std::io::Read as _;
|
||||||
|
|
||||||
|
/// Parse a newc cpio archive into (name, ino, mode, nlink, file_size, data) tuples
|
||||||
|
fn parse_cpio(archive: Vec<u8>) -> Vec<(String, u32, u32, u32, u32, Vec<u8>)> {
|
||||||
|
let mut cursor = std::io::Cursor::new(archive);
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
loop {
|
||||||
|
let mut reader = newc::Reader::new(&mut cursor).expect("valid cpio entry");
|
||||||
|
let name = reader.entry().name().to_string();
|
||||||
|
let ino = reader.entry().ino();
|
||||||
|
let mode = reader.entry().mode();
|
||||||
|
let nlink = reader.entry().nlink();
|
||||||
|
let file_size = reader.entry().file_size();
|
||||||
|
let mut data = Vec::new();
|
||||||
|
reader.read_to_end(&mut data).expect("read entry data");
|
||||||
|
let is_trailer = reader.entry().is_trailer();
|
||||||
|
// Skip the padding after the entry data before parsing the next one
|
||||||
|
reader.finish().expect("skip entry padding");
|
||||||
|
entries.push((name, ino, mode, nlink, file_size, data));
|
||||||
|
if is_trailer {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entries
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_hard_links_share_inode() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
|
||||||
|
// Two distinct hard-link groups, each with two names
|
||||||
|
std::fs::write(dir.path().join("group1.txt"), b"hello").unwrap();
|
||||||
|
std::fs::hard_link(
|
||||||
|
dir.path().join("group1.txt"),
|
||||||
|
dir.path().join("group1b.txt"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
std::fs::write(dir.path().join("group2.txt"), b"world!").unwrap();
|
||||||
|
std::fs::hard_link(
|
||||||
|
dir.path().join("group2.txt"),
|
||||||
|
dir.path().join("group2b.txt"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let pb = ProgressBar::hidden();
|
||||||
|
let mut archive = Vec::new();
|
||||||
|
write_cpio_archive(dir.path(), &mut archive, &pb).unwrap();
|
||||||
|
|
||||||
|
let entries = parse_cpio(archive)
|
||||||
|
.into_iter()
|
||||||
|
.map(|(name, ino, _, nlink, size, data)| (name, (ino, nlink, size, data)))
|
||||||
|
.collect::<std::collections::HashMap<String, (u32, u32, u32, Vec<u8>)>>();
|
||||||
|
let get = |name: &str| entries[name].clone();
|
||||||
|
|
||||||
|
// Both names of a hard link share one synthetic (nonzero) inode.
|
||||||
|
// readdir order decides which occurrence is walked first, so exactly
|
||||||
|
// one of the two entries carries the data and the other is zero-size.
|
||||||
|
let (ino1, nlink1, size1, data1) = get("group1.txt");
|
||||||
|
let (ino1b, nlink1b, size1b, data1b) = get("group1b.txt");
|
||||||
|
assert_eq!(nlink1, 2);
|
||||||
|
assert_eq!(nlink1b, 2);
|
||||||
|
assert_eq!(ino1, ino1b, "both names of a hard link must share an inode");
|
||||||
|
assert_ne!(ino1, 0, "hard-link group must get a synthetic inode");
|
||||||
|
assert_eq!(size1 + size1b, 5);
|
||||||
|
assert_eq!(
|
||||||
|
data1
|
||||||
|
.iter()
|
||||||
|
.chain(data1b.iter())
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<u8>>(),
|
||||||
|
b"hello".to_vec()
|
||||||
|
);
|
||||||
|
|
||||||
|
// The other group gets a different inode — this is what keeps the
|
||||||
|
// kernel from linking group2 names to group1's file
|
||||||
|
let (ino2, _, size2, data2) = get("group2.txt");
|
||||||
|
let (ino2b, _, size2b, data2b) = get("group2b.txt");
|
||||||
|
assert_eq!(ino2, ino2b);
|
||||||
|
assert_ne!(
|
||||||
|
ino2, ino1,
|
||||||
|
"distinct hard-link groups must not share an inode"
|
||||||
|
);
|
||||||
|
assert_eq!(size2 + size2b, 6);
|
||||||
|
assert_eq!(
|
||||||
|
data2
|
||||||
|
.iter()
|
||||||
|
.chain(data2b.iter())
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<u8>>(),
|
||||||
|
b"world!".to_vec()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Ok(entries)
|
|
||||||
|
#[test]
|
||||||
|
fn test_regular_files_have_zero_inode_and_data() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
std::fs::create_dir(dir.path().join("sub")).unwrap();
|
||||||
|
std::fs::write(dir.path().join("sub/plain.txt"), b"plain").unwrap();
|
||||||
|
|
||||||
|
let pb = ProgressBar::hidden();
|
||||||
|
let mut archive = Vec::new();
|
||||||
|
write_cpio_archive(dir.path(), &mut archive, &pb).unwrap();
|
||||||
|
|
||||||
|
let entries = parse_cpio(archive);
|
||||||
|
let find = |name: &str| {
|
||||||
|
entries
|
||||||
|
.iter()
|
||||||
|
.find(|(n, ..)| n == name)
|
||||||
|
.map(|(_, ino, mode, nlink, size, data)| (*ino, *mode, *nlink, *size, data.clone()))
|
||||||
|
.expect("entry missing")
|
||||||
|
};
|
||||||
|
|
||||||
|
// Regular files with nlink=1 keep inode 0 and must keep their data
|
||||||
|
let (ino, _, nlink, size, data) = find("sub/plain.txt");
|
||||||
|
assert_eq!(ino, 0);
|
||||||
|
assert_eq!((nlink, size), (1, 5));
|
||||||
|
assert_eq!(data, b"plain");
|
||||||
|
|
||||||
|
// The init script is always appended, executable, and non-empty
|
||||||
|
let (_, init_mode, _, init_size, init_data) = find("init");
|
||||||
|
assert_eq!(init_mode, 0o100755);
|
||||||
|
assert!(init_size > 0);
|
||||||
|
assert!(init_data.starts_with(b"#!/bin/sh"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user