You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1813 lines
64 KiB

// SPDX-License-Identifier: GPL-2.0
// Copyright (C) 2021 SUSE LLC
use std::convert::TryInto;
use std::env;
use std::ffi::OsStr;
use std::fs;
use std::io;
use std::io::prelude::*;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::FileTypeExt;
use std::os::unix::fs::MetadataExt as UnixMetadataExt;
use std::path::{Path, PathBuf};
use crosvm::argument::{self, Argument};
macro_rules! NEWC_HDR_FMT {
() => {
concat!(
"{magic}{ino:08X}{mode:08X}{uid:08X}{gid:08X}{nlink:08X}",
"{mtime:08X}{filesize:08X}{major:08X}{minor:08X}{rmajor:08X}",
"{rminor:08X}{namesize:08X}{chksum:08X}"
)
};
}
// Don't print debug messages on release builds...
#[cfg(debug_assertions)]
macro_rules! dout {
($($l:tt)*) => { println!($($l)*); }
}
#[cfg(not(debug_assertions))]
macro_rules! dout {
($($l:tt)*) => {};
}
const NEWC_HDR_LEN: u64 = 110;
const PATH_MAX: u64 = 4096;
struct HardlinkPath {
infile: PathBuf,
outfile: PathBuf,
}
struct HardlinkState {
names: Vec<HardlinkPath>,
source_ino: u64,
mapped_ino: u32,
nlink: u32,
seen: u32,
}
struct DevState {
dev: u64,
hls: Vec<HardlinkState>,
}
struct ArchiveProperties {
// first inode number to use. @ArchiveState.ino increments from this.
initial_ino: u32,
// if non-zero, then align file data segments to this offset by injecting
// extra zeros after the filename string terminator.
data_align: u32,
// When injecting extra zeros into the filename field for data alignment,
// ensure that it doesn't exceed this size. The linux kernel will ignore
// files where namesize is larger than PATH_MAX, hence the need for this.
namesize_max: u32,
// if the archive is being appended to the end of an existing file, then
// @initial_data_off is used when calculating @data_align alignment.
initial_data_off: u64,
// delimiter character for the stdin file list
list_separator: u8,
// mtime, uid and gid to use for archived inodes, instead of the value
// reported by stat.
fixed_mtime: Option<u32>,
fixed_uid: Option<u32>,
fixed_gid: Option<u32>,
// When archiving a subset of hardlinks, nlink values in the archive can
// represent the subset (renumber_nlink=true) or the original source file
// nlink values (renumber_nlink=false), where the latter matches GNU cpio.
renumber_nlink: bool,
// If OUTPUT file exists, then zero-truncate it instead of appending. The
// default append behaviour chains archives back-to-back, i.e. multiple
// archives will be separated by a TRAILER and 512-byte padding.
// See Linux's Documentation/driver-api/early-userspace/buffer-format.rst
// for details on how chained initramfs archives are handled.
truncate_existing: bool,
}
impl ArchiveProperties {
pub fn default() -> ArchiveProperties {
ArchiveProperties {
initial_ino: 0, // match GNU cpio numbering
data_align: 0,
namesize_max: PATH_MAX as u32,
initial_data_off: 0,
list_separator: b'\n',
fixed_mtime: None,
fixed_uid: None,
fixed_gid: None,
renumber_nlink: false,
truncate_existing: false,
}
}
}
struct ArchiveState {
// 2d dev + inode vector serves two purposes:
// - dev index provides reproducible major,minor values
// - inode@dev provides hardlink state tracking
ids: Vec<DevState>,
// offset from the start of this archive
off: u64,
// next mapped inode number, used instead of source file inode numbers to
// ensure reproducability. XXX: should track inode per mapped dev?
ino: u32,
}
impl ArchiveState {
pub fn new(ino_start: u32) -> ArchiveState {
ArchiveState {
ids: Vec::new(),
off: 0,
ino: ino_start,
}
}
// lookup or create DevState for @dev. Return @major/@minor based on index
pub fn dev_seen(&mut self, dev: u64) -> Option<(u32, u32)> {
let index: u64 = match self.ids.iter().position(|i| i.dev == dev) {
Some(idx) => idx.try_into().ok()?,
None => {
self.ids.push(DevState {
dev: dev,
hls: Vec::new(),
});
(self.ids.len() - 1).try_into().ok()?
}
};
let major: u32 = (index >> 32).try_into().unwrap();
let minor: u32 = (index & u64::from(u32::MAX)).try_into().unwrap();
Some((major, minor))
}
// Check whether we've already seen this hardlink's dev/inode combination.
// If already seen, fill the existing mapped_ino.
// Return true if this entry has been deferred (seen != nlinks)
pub fn hardlink_seen<W: Write + Seek>(
&mut self,
props: &ArchiveProperties,
mut writer: W,
major: u32,
minor: u32,
md: fs::Metadata,
inpath: &Path,
outpath: &Path,
mapped_ino: &mut Option<u32>,
mapped_nlink: &mut Option<u32>,
) -> std::io::Result<bool> {
assert!(md.nlink() > 1);
let index = u64::from(major) << 32 | u64::from(minor);
// reverse index->major/minor conversion that was just done
let devstate: &mut DevState = &mut self.ids[index as usize];
let (_index, hl) = match devstate
.hls
.iter_mut()
.enumerate()
.find(|(_, hl)| hl.source_ino == md.ino())
{
Some(hl) => hl,
None => {
devstate.hls.push(HardlinkState {
names: vec![HardlinkPath {
infile: inpath.to_path_buf(),
outfile: outpath.to_path_buf(),
}],
source_ino: md.ino(),
mapped_ino: self.ino,
nlink: md.nlink().try_into().unwrap(), // pre-checked
seen: 1,
});
self.ino += 1; // ino is reserved for all subsequent links
return Ok(true);
}
};
if (*hl).names.iter().any(|n| n.infile == inpath) {
println!(
"duplicate hardlink path {} for {}",
inpath.display(),
md.ino()
);
// GNU cpio doesn't swallow duplicates
}
// hl.nlink may not match md.nlink if we've come here via
// archive_flush_unseen_hardlinks() .
(*hl).seen += 1;
if (*hl).seen > (*hl).nlink {
// GNU cpio powers through if a hardlink is listed multiple times,
// exceeding nlink.
println!("hardlink seen {} exceeds nlink {}", (*hl).seen, (*hl).nlink);
}
if (*hl).seen < (*hl).nlink {
(*hl).names.push(HardlinkPath {
infile: inpath.to_path_buf(),
outfile: outpath.to_path_buf(),
});
return Ok(true);
}
// a new HardlinkPath entry isn't added, as return path handles cpio
// outpath header *and* data segment.
for path in (*hl).names.iter().rev() {
dout!("writing hardlink {}", path.outfile.display());
// length already PATH_MAX validated
let fname = path.outfile.as_os_str().as_bytes();
write!(
writer,
NEWC_HDR_FMT!(),
magic = "070701",
ino = (*hl).mapped_ino,
mode = md.mode(),
uid = match props.fixed_uid {
Some(u) => u,
None => md.uid(),
},
gid = match props.fixed_gid {
Some(g) => g,
None => md.gid(),
},
nlink = match props.renumber_nlink {
true => (*hl).nlink,
false => md.nlink().try_into().unwrap(),
},
mtime = match props.fixed_mtime {
Some(t) => t,
None => md.mtime().try_into().unwrap(),
},
filesize = 0,
major = major,
minor = major,
rmajor = 0,
rminor = 0,
namesize = fname.len() + 1,
chksum = 0
)?;
self.off += NEWC_HDR_LEN;
writer.write_all(fname)?;
self.off += fname.len() as u64;
// +1 as padding starts after fname nulterm
let seeklen = 1 + archive_padlen(self.off + 1, 4);
{
let z = vec![0u8; seeklen.try_into().unwrap()];
writer.write_all(&z)?;
}
self.off += seeklen;
}
*mapped_ino = Some((*hl).mapped_ino);
// cpio nlink may be different to stat nlink if only a subset of links
// are archived.
if props.renumber_nlink {
*mapped_nlink = Some((*hl).nlink);
}
// GNU cpio: if a name is given multiple times, exceeding nlink, then
// subsequent names continue to be packed (with a repeat data segment),
// using the same mapped inode.
dout!("resetting hl at index {}", index);
hl.seen = 0;
hl.names.clear();
return Ok(false);
}
}
fn archive_path<W: Seek + Write>(
state: &mut ArchiveState,
props: &ArchiveProperties,
path: &Path,
mut writer: W,
) -> std::io::Result<()> {
let inpath = path;
let mut outpath = path.clone();
let mut datalen: u32 = 0;
let mut rmajor: u32 = 0;
let mut rminor: u32 = 0;
let mut hardlink_ino: Option<u32> = None;
let mut hardlink_nlink: Option<u32> = None;
let mut symlink_tgt = PathBuf::new();
let mut data_align_seek: u32 = 0;
outpath = match outpath.strip_prefix("./") {
Ok(p) => {
if p.as_os_str().as_bytes().len() == 0 {
outpath // retain './' and '.' paths
} else {
p
}
}
Err(_) => outpath,
};
let fname = outpath.as_os_str().as_bytes();
if fname.len() + 1 >= PATH_MAX.try_into().unwrap() {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "path too long"));
}
let md = match fs::symlink_metadata(inpath) {
Ok(m) => m,
Err(e) => {
println!("failed to get metadata for {}: {}", inpath.display(), e);
return Err(e);
}
};
dout!("archiving {} with mode {:o}", outpath.display(), md.mode());
let (major, minor) = match state.dev_seen(md.dev()) {
Some((maj, min)) => (maj, min),
None => return Err(io::Error::new(io::ErrorKind::Other, "failed to map dev")),
};
if md.nlink() > u32::MAX as u64 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"nlink too large",
));
}
let mtime: u32 = match props.fixed_mtime {
Some(t) => t,
None => {
// check for 2106 epoch overflow
if md.mtime() > i64::from(u32::MAX) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"mtime too large for cpio",
));
}
md.mtime().try_into().unwrap()
}
};
let ftype = md.file_type();
if ftype.is_symlink() {
symlink_tgt = fs::read_link(inpath)?;
datalen = {
let d: usize = symlink_tgt.as_os_str().as_bytes().len();
if d >= PATH_MAX.try_into().unwrap() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"symlink path too long",
));
}
d.try_into().unwrap()
};
// no zero terminator for symlink target path
}
// Linux kernel uses 32-bit dev_t, encoded as mmmM MMmm. glibc uses 64-bit
// MMMM Mmmm mmmM MMmm, which is compatible with the former.
if ftype.is_block_device() || ftype.is_char_device() {
let rd = md.rdev();
rmajor = (((rd >> 32) & 0xfffff000) | ((rd >> 8) & 0x00000fff)) as u32;
rminor = (((rd >> 12) & 0xffffff00) | (rd & 0x000000ff)) as u32;
}
if ftype.is_file() {
datalen = {
if md.len() > u64::from(u32::MAX) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"file too large for newc",
));
}
md.len().try_into().unwrap()
};
if md.nlink() > 1 {
// follow GNU cpio's behaviour of attaching hardlink data only to
// the last entry in the archive.
let deferred = state.hardlink_seen(
&props,
&mut writer,
major,
minor,
md.clone(),
&inpath,
outpath,
&mut hardlink_ino,
&mut hardlink_nlink,
)?;
if deferred {
dout!("deferring hardlink {} data portion", outpath.display());
return Ok(());
}
}
if props.data_align > 0 && datalen > props.data_align {
// XXX we're "bending" the newc spec a bit here to inject zeros
// after fname to provide data segment alignment. These zeros are
// accounted for in the namesize, but some applications may only
// expect a single zero-terminator (and 4 byte alignment). GNU cpio
// and Linux initramfs handle this fine as long as PATH_MAX isn't
// exceeded.
data_align_seek = {
let len: u64 = archive_padlen(
props.initial_data_off + state.off + NEWC_HDR_LEN + fname.len() as u64 + 1,
u64::from(props.data_align),
);
let padded_namesize = len + fname.len() as u64 + 1;
if padded_namesize > u64::from(props.namesize_max) {
dout!(
"{} misaligned. Required padding {} exceeds namesize maximum {}.",
outpath.display(),
len,
props.namesize_max
);
0
} else {
len.try_into().unwrap()
}
};
}
}
write!(
writer,
NEWC_HDR_FMT!(),
magic = "070701",
ino = match hardlink_ino {
Some(i) => i,
None => {
let i = state.ino;
state.ino += 1;
i
}
},
mode = md.mode(),
uid = match props.fixed_uid {
Some(u) => u,
None => md.uid(),
},
gid = match props.fixed_gid {
Some(g) => g,
None => md.gid(),
},
nlink = match hardlink_nlink {
Some(n) => n,
None => md.nlink().try_into().unwrap(),
},
mtime = mtime,
filesize = datalen,
major = major,
minor = major,
rmajor = rmajor,
rminor = rminor,
namesize = fname.len() + 1 + data_align_seek as usize,
chksum = 0
)?;
state.off += NEWC_HDR_LEN;
writer.write_all(fname)?;
state.off += fname.len() as u64;
let mut seek_len: i64 = 1; // fname nulterm
if data_align_seek > 0 {
seek_len += data_align_seek as i64;
assert_eq!(archive_padlen(state.off + seek_len as u64, 4), 0);
} else {
let padding_len = archive_padlen(state.off + seek_len as u64, 4);
seek_len += padding_len as i64;
}
{
let z = vec![0u8; seek_len.try_into().unwrap()];
writer.write_all(&z)?;
}
state.off += seek_len as u64;
// io::copy() can reflink: https://github.com/rust-lang/rust/pull/75272 \o/
if datalen > 0 {
if ftype.is_file() {
let mut reader = io::BufReader::new(fs::File::open(inpath)?);
let copied = io::copy(&mut reader, &mut writer)?;
if copied != u64::from(datalen) {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"copy returned unexpected length",
));
}
} else if ftype.is_symlink() {
writer.write_all(symlink_tgt.as_os_str().as_bytes())?;
}
state.off += u64::from(datalen);
let dpad_len: usize = archive_padlen(state.off, 4).try_into().unwrap();
write!(writer, "{pad:.padlen$}", padlen = dpad_len, pad = "\0\0\0")?;
state.off += dpad_len as u64;
}
Ok(())
}
fn archive_padlen(off: u64, alignment: u64) -> u64 {
(alignment - (off & (alignment - 1))) % alignment
}
// this fn is inefficient, but optimizing for hardlinks isn't high priority
fn archive_flush_unseen_hardlinks<W: Write + Seek>(
state: &mut ArchiveState,
props: &ArchiveProperties,
mut writer: W,
) -> std::io::Result<()> {
let mut deferred_inpaths: Vec<PathBuf> = Vec::new();
for id in state.ids.iter_mut() {
for hl in id.hls.iter_mut() {
if hl.seen == 0 || hl.seen == hl.nlink {
dout!("HardlinkState complete with seen {}", hl.seen);
continue;
}
dout!(
"pending HardlinkState with seen {} != nlinks {}",
hl.seen,
hl.nlink
);
while hl.names.len() > 0 {
let path = hl.names.pop().unwrap();
deferred_inpaths.push(path.infile);
}
// ensure that data segment gets added on archive_path recall
hl.nlink = hl.seen;
hl.seen = 0;
// existing allocated inode should be used
}
}
if deferred_inpaths.len() > 0 {
// rotate-right to match gnu ordering
deferred_inpaths.rotate_right(1);
// .reverse() to match gnu ordering
for p in deferred_inpaths.iter().rev() {
archive_path(state, props, p.as_path(), &mut writer)?;
}
}
Ok(())
}
fn archive_trailer<W: Write>(mut writer: W, cur_off: u64) -> std::io::Result<u64> {
let fname = "TRAILER!!!";
let fname_len = fname.len() + 1;
write!(
writer,
NEWC_HDR_FMT!(),
magic = "070701",
ino = 0,
mode = 0,
uid = 0,
gid = 0,
nlink = 1,
mtime = 0,
filesize = 0,
major = 0,
minor = 0,
rmajor = 0,
rminor = 0,
namesize = fname_len,
chksum = 0
)?;
let mut off: u64 = cur_off + NEWC_HDR_LEN;
let padding_len = archive_padlen(off + fname_len as u64, 4);
write!(
writer,
"{}\0{pad:.padlen$}",
fname,
padlen = padding_len as usize,
pad = "\0\0\0"
)?;
off += fname_len as u64 + padding_len as u64;
Ok(off)
}
fn archive_loop<R: BufRead, W: Seek + Write>(
mut reader: R,
mut writer: W,
props: &ArchiveProperties,
) -> std::io::Result<u64> {
if props.data_align > 0 && (props.initial_data_off + u64::from(props.data_align)) % 4 != 0 {
// must satisfy both data_align and cpio 4-byte padding alignment
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"data alignment must be a multiple of 4",
));
}
let mut state = ArchiveState::new(props.initial_ino);
loop {
let mut linebuf: Vec<u8> = Vec::new();
let mut r = reader.by_ref().take(PATH_MAX);
match r.read_until(props.list_separator, &mut linebuf) {
Ok(l) => {
if l == 0 {
break; // EOF
}
if l >= PATH_MAX.try_into().unwrap() {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "path too long"));
}
}
Err(e) => {
println!("read_until() failed: {}", e);
return Err(e);
}
};
// trim separator. len > 0 already checked.
let last_byte = linebuf.last().unwrap();
if *last_byte == props.list_separator {
linebuf.pop().unwrap();
if linebuf.len() == 0 {
continue;
}
} else {
println!(
"\'{:0x}\' ending not separator \'{:0x}\' terminated",
last_byte, props.list_separator
);
}
let linestr = OsStr::from_bytes(linebuf.as_slice());
let path = Path::new(linestr);
archive_path(&mut state, props, path, &mut writer)?;
}
archive_flush_unseen_hardlinks(&mut state, props, &mut writer)?;
state.off = archive_trailer(&mut writer, state.off)?;
// GNU cpio pads the end of an archive out to blocklen with zeros
let block_padlen = archive_padlen(state.off, 512);
if block_padlen > 0 {
let z = vec![0u8; block_padlen.try_into().unwrap()];
writer.write_all(&z)?;
state.off += block_padlen;
}
writer.flush()?;
Ok(state.off)
}
fn params_usage(params: &[Argument]) {
argument::print_help("dracut-cpio", "OUTPUT", params);
println!("\nExample: find fs-tree/ | dracut-cpio archive.cpio\n");
}
fn params_process(props: &mut ArchiveProperties) -> argument::Result<PathBuf> {
let params = &[
Argument::positional("OUTPUT", "Write cpio archive to this file path."),
Argument::value(
"data-align",
"ALIGNMENT",
"Attempt to pad archive to achieve ALIGNMENT for file data.",
),
Argument::short_flag(
'0',
"null",
"Expect null delimeters in stdin filename list instead of newline.",
),
Argument::value(
"mtime",
"EPOCH",
"Use EPOCH for archived mtime instead of filesystem reported values.",
),
Argument::value(
"owner",
"UID:GID",
"Use UID and GID instead of filesystem reported owner values.",
),
Argument::flag(
"truncate-existing",
"Truncate and overwrite any existing OUTPUT file, instead of appending.",
),
Argument::short_flag('h', "help", "Print help message."),
];
let mut positional_args = 0;
let args = env::args().skip(1); // skip binary name
let match_res = argument::set_arguments(args, params, |name, value| {
match name {
"" => positional_args += 1,
"data-align" => {
let v: u32 = value
.unwrap()
.parse()
.map_err(|_| argument::Error::InvalidValue {
value: value.unwrap().to_owned(),
expected: String::from("data-align must be an integer"),
})?;
if v > props.namesize_max {
println!(
concat!(
"Requested data-align {} larger than namesize maximum {}.",
" This will likely result in misalignment."
),
v, props.namesize_max
);
}
props.data_align = v;
}
"null" => props.list_separator = b'\0',
"mtime" => {
let v: u32 = value
.unwrap()
.parse()
.map_err(|_| argument::Error::InvalidValue {
value: value.unwrap().to_owned(),
expected: String::from("mtime must be an integer"),
})?;
props.fixed_mtime = Some(v);
}
"owner" => {
let ugv_parsed: argument::Result<Vec<u32>> = value
.unwrap()
.split(':')
.map(|id| {
id.parse().map_err(|_| argument::Error::InvalidValue {
value: id.to_owned(),
expected: String::from("uid/gid must be an integer"),
})
})
.collect();
let ugv_parsed = ugv_parsed?;
if ugv_parsed.len() != 2 {
return Err(argument::Error::InvalidValue {
value: value.unwrap().to_owned(),
expected: String::from("owner must be UID:GID"),
});
}
props.fixed_uid = Some(ugv_parsed[0]);
props.fixed_gid = Some(ugv_parsed[1]);
}
"truncate-existing" => props.truncate_existing = true,
"help" => return Err(argument::Error::PrintHelp),
_ => unreachable!(),
};
Ok(())
});
match match_res {
Ok(_) => {
if positional_args != 1 {
params_usage(params);
return Err(argument::Error::ExpectedArgument(
"one OUTPUT parameter required".to_string(),
));
}
}
Err(e) => {
params_usage(params);
return Err(e);
}
}
let last_arg = env::args_os().last().unwrap();
Ok(PathBuf::from(&last_arg))
}
fn main() -> std::io::Result<()> {
let mut props = ArchiveProperties::default();
let output_path = match params_process(&mut props) {
Ok(p) => p,
Err(argument::Error::PrintHelp) => return Ok(()),
Err(e) => return Err(io::Error::new(io::ErrorKind::InvalidInput, e.to_string())),
};
let mut f = fs::OpenOptions::new()
.read(false)
.write(true)
.create(true)
.truncate(props.truncate_existing)
.open(&output_path)?;
if !props.truncate_existing {
props.initial_data_off = f.seek(io::SeekFrom::End(0))?;
}
let mut writer = io::BufWriter::new(f);
let stdin = std::io::stdin();
let mut reader = io::BufReader::new(stdin);
let _wrote = archive_loop(&mut reader, &mut writer, &props)?;
if props.initial_data_off > 0 {
dout!(
"appended {} bytes to archive {} at offset {}",
_wrote,
output_path.display(),
props.initial_data_off
);
} else {
dout!(
"wrote {} bytes to archive {}",
_wrote,
output_path.display()
);
}
Ok(())
}
// tests change working directory, so need to be run with:
// cargo test -- --test-threads=1 --nocapture
#[cfg(test)]
mod tests {
use super::*;
use std::cmp;
use std::os::unix::fs as unixfs;
use std::path::PathBuf;
use std::process::{Command, Stdio};
struct TempWorkDir {
prev_dir: PathBuf,
parent_tmp_dir: PathBuf,
cleanup_files: Vec<PathBuf>,
cleanup_dirs: Vec<PathBuf>,
ignore_cleanup: bool, // useful for debugging
}
impl TempWorkDir {
// create a temporary directory under CWD and cd into it.
// The directory will be cleaned up when twd goes out of scope.
pub fn new() -> TempWorkDir {
let mut buf = [0u8; 16];
let mut s = String::from("cpio-selftest-");
fs::File::open("/dev/urandom")
.unwrap()
.read_exact(&mut buf)
.unwrap();
for i in &buf {
s.push_str(&format!("{:02x}", i).to_string());
}
let mut twd = TempWorkDir {
prev_dir: env::current_dir().unwrap(),
parent_tmp_dir: {
let mut t = env::current_dir().unwrap().clone();
t.push(s);
println!("parent_tmp_dir: {}", t.display());
t
},
cleanup_files: Vec::new(),
cleanup_dirs: Vec::new(),
ignore_cleanup: false,
};
fs::create_dir(&twd.parent_tmp_dir).unwrap();
twd.cleanup_dirs.push(twd.parent_tmp_dir.clone());
env::set_current_dir(&twd.parent_tmp_dir).unwrap();
twd
}
pub fn create_tmp_file(&mut self, name: &str, len_bytes: u64) {
let mut bytes = len_bytes;
let f = fs::File::create(name).unwrap();
self.cleanup_files.push(PathBuf::from(name));
let mut writer = io::BufWriter::new(f);
let mut buf = [0u8; 512];
for (i, elem) in buf.iter_mut().enumerate() {
*elem = !(i & 0xFF) as u8;
}
while bytes > 0 {
let this_len = cmp::min(buf.len(), bytes.try_into().unwrap());
writer.write_all(&buf[0..this_len]).unwrap();
bytes -= this_len as u64;
}
writer.flush().unwrap();
}
pub fn create_tmp_dir(&mut self, name: &str) {
fs::create_dir(name).unwrap();
self.cleanup_dirs.push(PathBuf::from(name));
}
// execute coreutils mknod NAME TYPE [MAJOR MINOR]
pub fn create_tmp_mknod(&mut self, name: &str, typ: char,
maj_min: Option<(u32, u32)>) {
let t = typ.to_string();
let proc = match maj_min {
Some(maj_min) => {
let (maj, min) = maj_min;
Command::new("mknod")
.args(&[name, &t, &maj.to_string(), &min.to_string()])
.spawn()
},
None => Command::new("mknod").args(&[name, &t]).spawn()
};
let status = proc.expect("mknod failed to start").wait().unwrap();
assert!(status.success());
self.cleanup_files.push(PathBuf::from(name));
}
}
impl Drop for TempWorkDir {
fn drop(&mut self) {
for f in self.cleanup_files.iter().rev() {
if self.ignore_cleanup {
println!("ignoring cleanup of file {}", f.display());
continue;
}
println!("cleaning up test file at {}", f.display());
match fs::remove_file(f) {
Err(e) => println!("file removal failed {}", e),
Ok(_) => {}
};
}
for f in self.cleanup_dirs.iter().rev() {
if self.ignore_cleanup {
println!("ignoring cleanup of dir {}", f.display());
continue;
}
println!("cleaning up test dir at {}", f.display());
match fs::remove_dir(f) {
Err(e) => println!("dir removal failed {}", e),
Ok(_) => {}
};
}
println!("returning cwd to {}", self.prev_dir.display());
env::set_current_dir(self.prev_dir.as_path()).unwrap();
}
}
fn gnu_cpio_create(stdinput: &[u8], out: &str) {
let mut proc = Command::new("cpio")
.args(&["--quiet", "-o", "-H", "newc", "--reproducible", "-F", out])
.stdin(Stdio::piped())
.spawn()
.expect("GNU cpio failed to start");
{
let mut stdin = proc.stdin.take().unwrap();
stdin.write_all(stdinput).expect("Failed to write to stdin");
}
let status = proc.wait().unwrap();
assert!(status.success());
}
#[test]
fn test_archive_empty_file() {
let mut twd = TempWorkDir::new();
twd.create_tmp_file("file.txt", 0);
gnu_cpio_create("file.txt\n".as_bytes(), "gnu.cpio");
twd.cleanup_files.push(PathBuf::from("gnu.cpio"));
// use dracut-cpio to archive file.txt
let f = fs::File::create("dracut.cpio").unwrap();
let mut writer = io::BufWriter::new(f);
let mut reader = io::BufReader::new("file.txt\n".as_bytes());
let wrote = archive_loop(&mut reader, &mut writer, &ArchiveProperties::default()).unwrap();
twd.cleanup_files.push(PathBuf::from("dracut.cpio"));
assert_eq!(wrote, 512);
let status = Command::new("diff")
.args(&["gnu.cpio", "dracut.cpio"])
.status()
.expect("diff failed to start");
assert!(status.success());
}
#[test]
fn test_archive_small_file() {
let mut twd = TempWorkDir::new();
twd.create_tmp_file("file.txt", 33);
gnu_cpio_create("file.txt\n".as_bytes(), "gnu.cpio");
twd.cleanup_files.push(PathBuf::from("gnu.cpio"));
let f = fs::File::create("dracut.cpio").unwrap();
let mut writer = io::BufWriter::new(f);
let mut reader = io::BufReader::new("file.txt\n".as_bytes());
let wrote = archive_loop(&mut reader, &mut writer, &ArchiveProperties::default()).unwrap();
twd.cleanup_files.push(PathBuf::from("dracut.cpio"));
assert!(wrote > NEWC_HDR_LEN * 2 + 33);
let status = Command::new("diff")
.args(&["gnu.cpio", "dracut.cpio"])
.status()
.expect("diff failed to start");
assert!(status.success());
}
#[test]
fn test_archive_prefixed_path() {
let mut twd = TempWorkDir::new();
twd.create_tmp_file("file.txt", 0);
gnu_cpio_create("./file.txt\n".as_bytes(), "gnu.cpio");
twd.cleanup_files.push(PathBuf::from("gnu.cpio"));
let f = fs::File::create("dracut.cpio").unwrap();
let mut writer = io::BufWriter::new(f);
let mut reader = io::BufReader::new("./file.txt\n".as_bytes());
let wrote = archive_loop(&mut reader, &mut writer, &ArchiveProperties::default()).unwrap();
twd.cleanup_files.push(PathBuf::from("dracut.cpio"));
assert_eq!(wrote, 512);
let status = Command::new("diff")
.args(&["gnu.cpio", "dracut.cpio"])
.status()
.expect("diff failed to start");
assert!(status.success());
}
#[test]
fn test_archive_absolute_path() {
let mut twd = TempWorkDir::new();
twd.create_tmp_file("file.txt", 0);
let canon_path = fs::canonicalize("file.txt").unwrap();
let mut canon_file_list = canon_path.into_os_string();
canon_file_list.push("\n");
gnu_cpio_create(canon_file_list.as_bytes(), "gnu.cpio");
twd.cleanup_files.push(PathBuf::from("gnu.cpio"));
let f = fs::File::create("dracut.cpio").unwrap();
let mut writer = io::BufWriter::new(f);
let mut reader = io::BufReader::new(canon_file_list.as_bytes());
let wrote = archive_loop(&mut reader, &mut writer, &ArchiveProperties::default()).unwrap();
twd.cleanup_files.push(PathBuf::from("dracut.cpio"));
assert_eq!(wrote, 512);
let status = Command::new("diff")
.args(&["gnu.cpio", "dracut.cpio"])
.status()
.expect("diff failed to start");
assert!(status.success());
}
#[test]
fn test_archive_dir() {
let mut twd = TempWorkDir::new();
twd.create_tmp_dir("dir");
gnu_cpio_create("dir\n".as_bytes(), "gnu.cpio");
twd.cleanup_files.push(PathBuf::from("gnu.cpio"));
let f = fs::File::create("dracut.cpio").unwrap();
let mut writer = io::BufWriter::new(f);
let mut reader = io::BufReader::new("dir\n".as_bytes());
let wrote = archive_loop(&mut reader, &mut writer, &ArchiveProperties::default()).unwrap();
twd.cleanup_files.push(PathBuf::from("dracut.cpio"));
assert_eq!(wrote, 512);
let status = Command::new("diff")
.args(&["gnu.cpio", "dracut.cpio"])
.status()
.expect("diff failed to start");
assert!(status.success());
}
#[test]
fn test_archive_dir_file() {
let mut twd = TempWorkDir::new();
twd.create_tmp_dir("dir");
twd.create_tmp_file("dir/file.txt", 512 * 32);
let file_list: &str = "dir\n\ndir/file.txt\n"; // double separator
gnu_cpio_create(file_list.as_bytes(), "gnu.cpio");
twd.cleanup_files.push(PathBuf::from("gnu.cpio"));
let f = fs::File::create("dracut.cpio").unwrap();
let mut writer = io::BufWriter::new(f);
let mut reader = io::BufReader::new(file_list.as_bytes());
let wrote = archive_loop(&mut reader, &mut writer, &ArchiveProperties::default()).unwrap();
twd.cleanup_files.push(PathBuf::from("dracut.cpio"));
assert!(wrote > NEWC_HDR_LEN * 3 + 512 * 32);
let status = Command::new("diff")
.args(&["gnu.cpio", "dracut.cpio"])
.status()
.expect("diff failed to start");
assert!(status.success());
}
#[test]
fn test_archive_dot_path() {
let mut twd = TempWorkDir::new();
twd.create_tmp_dir("dir");
twd.create_tmp_file("dir/file.txt", 512 * 32);
let file_list: &str = ".\ndir\ndir/file.txt\n";
gnu_cpio_create(file_list.as_bytes(), "gnu.cpio");
twd.cleanup_files.push(PathBuf::from("gnu.cpio"));
let f = fs::File::create("dracut.cpio").unwrap();
let mut writer = io::BufWriter::new(f);
let mut reader = io::BufReader::new(file_list.as_bytes());
let wrote = archive_loop(&mut reader, &mut writer, &ArchiveProperties::default()).unwrap();
twd.cleanup_files.push(PathBuf::from("dracut.cpio"));
assert!(wrote > NEWC_HDR_LEN * 4 + 512 * 32);
let status = Command::new("diff")
.args(&["gnu.cpio", "dracut.cpio"])
.status()
.expect("diff failed to start");
assert!(status.success());
}
#[test]
fn test_archive_dot_slash_path() {
let mut twd = TempWorkDir::new();
twd.create_tmp_dir("dir");
twd.create_tmp_file("dir/file.txt", 512 * 32);
let file_list: &str = "./\ndir\ndir/file.txt\n";
gnu_cpio_create(file_list.as_bytes(), "gnu.cpio");
twd.cleanup_files.push(PathBuf::from("gnu.cpio"));
let f = fs::File::create("dracut.cpio").unwrap();
let mut writer = io::BufWriter::new(f);
let mut reader = io::BufReader::new(file_list.as_bytes());
let wrote = archive_loop(&mut reader, &mut writer, &ArchiveProperties::default()).unwrap();
twd.cleanup_files.push(PathBuf::from("dracut.cpio"));
assert!(wrote > NEWC_HDR_LEN * 4 + 512 * 32);
let status = Command::new("diff")
.args(&["gnu.cpio", "dracut.cpio"])
.status()
.expect("diff failed to start");
assert!(status.success());
}
#[test]
fn test_archive_symlink() {
let mut twd = TempWorkDir::new();
twd.create_tmp_file("file.txt", 0);
unixfs::symlink("file.txt", "symlink.txt").unwrap();
twd.cleanup_files.push(PathBuf::from("symlink.txt"));
gnu_cpio_create("file.txt\nsymlink.txt\n".as_bytes(), "gnu.cpio");
twd.cleanup_files.push(PathBuf::from("gnu.cpio"));
let f = fs::File::create("dracut.cpio").unwrap();
let mut writer = io::BufWriter::new(f);
let mut reader = io::BufReader::new("file.txt\nsymlink.txt\n".as_bytes());
let wrote = archive_loop(&mut reader, &mut writer, &ArchiveProperties::default()).unwrap();
twd.cleanup_files.push(PathBuf::from("dracut.cpio"));
assert_eq!(wrote, 512);
let status = Command::new("diff")
.args(&["gnu.cpio", "dracut.cpio"])
.status()
.expect("diff failed to start");
assert!(status.success());
}
#[test]
fn test_archive_fifo() {
let mut twd = TempWorkDir::new();
twd.create_tmp_mknod("fifo", 'p', None);
gnu_cpio_create("fifo\n".as_bytes(), "gnu.cpio");
twd.cleanup_files.push(PathBuf::from("gnu.cpio"));
let f = fs::File::create("dracut.cpio").unwrap();
let mut writer = io::BufWriter::new(f);
let mut reader = io::BufReader::new("fifo\n".as_bytes());
let wrote = archive_loop(&mut reader, &mut writer, &ArchiveProperties::default()).unwrap();
twd.cleanup_files.push(PathBuf::from("dracut.cpio"));
assert_eq!(wrote, 512);
let status = Command::new("diff")
.args(&["gnu.cpio", "dracut.cpio"])
.status()
.expect("diff failed to start");
assert!(status.success());
}
#[test]
fn test_archive_char() {
let mut twd = TempWorkDir::new();
gnu_cpio_create("/dev/zero\n".as_bytes(), "gnu.cpio");
twd.cleanup_files.push(PathBuf::from("gnu.cpio"));
let f = fs::File::create("dracut.cpio").unwrap();
let mut writer = io::BufWriter::new(f);
let mut reader = io::BufReader::new("/dev/zero\n".as_bytes());
let wrote = archive_loop(&mut reader, &mut writer, &ArchiveProperties::default()).unwrap();
twd.cleanup_files.push(PathBuf::from("dracut.cpio"));
assert_eq!(wrote, 512);
let status = Command::new("diff")
.args(&["gnu.cpio", "dracut.cpio"])
.status()
.expect("diff failed to start");
assert!(status.success());
}
#[test]
fn test_archive_data_align() {
let mut twd = TempWorkDir::new();
twd.create_tmp_dir("dir");
twd.create_tmp_file("dir/file.txt", 1024 * 1024); // 1M
twd.create_tmp_dir("extractor");
let f = fs::File::create("extractor/dracut.cpio").unwrap();
let mut writer = io::BufWriter::new(f);
let mut reader = io::BufReader::new("dir\ndir/file.txt\n".as_bytes());
// 4k cpio data segment alignment injects zeros after filename nullterm
let wrote = archive_loop(
&mut reader,
&mut writer,
&ArchiveProperties {
data_align: 4096,
..ArchiveProperties::default()
},
)
.unwrap();
twd.cleanup_files
.push(PathBuf::from("extractor/dracut.cpio"));
assert!(wrote > NEWC_HDR_LEN * 3 + 1024 * 1024);
// check 4k data segment alignment
let mut proc = Command::new("diff")
.args(&["dir/file.txt", "-"])
.stdin(Stdio::piped())
.spawn()
.expect("diff failed to start");
{
let f = fs::File::open("extractor/dracut.cpio").unwrap();
let mut reader = io::BufReader::new(f);
reader.seek(io::SeekFrom::Start(4096)).unwrap();
let mut take = reader.take(1024 * 1024 as u64);
let mut stdin = proc.stdin.take().unwrap();
let copied = io::copy(&mut take, &mut stdin).unwrap();
assert_eq!(copied, 1024 * 1024);
}
let status = proc.wait().unwrap();
assert!(status.success());
// confirm that GNU cpio can extract fname-zeroed paths
let status = Command::new("cpio")
.current_dir("extractor")
.args(&["--quiet", "-i", "-H", "newc", "-F", "dracut.cpio"])
.status()
.expect("GNU cpio failed to start");
assert!(status.success());
twd.cleanup_files
.push(PathBuf::from("extractor/dir/file.txt"));
twd.cleanup_dirs.push(PathBuf::from("extractor/dir"));
let status = Command::new("diff")
.args(&["dir/file.txt", "extractor/dir/file.txt"])
.status()
.expect("diff failed to start");
assert!(status.success());
}
#[test]
fn test_archive_data_align_off() {
let mut twd = TempWorkDir::new();
twd.create_tmp_dir("dir1");
twd.create_tmp_dir("dir2");
twd.create_tmp_dir("dir3");
twd.create_tmp_file("dir1/file.txt", 514 * 1024);
twd.create_tmp_dir("extractor");
let data_before_cpio = [5u8; 16384 + 4];
let f = fs::File::create("extractor/dracut.cpio").unwrap();
twd.cleanup_files
.push(PathBuf::from("extractor/dracut.cpio"));
let mut writer = io::BufWriter::new(f);
writer.write_all(&data_before_cpio).unwrap();
let mut reader = io::BufReader::new("dir1\ndir2\ndir3\ndir1/file.txt\n".as_bytes());
let wrote = archive_loop(
&mut reader,
&mut writer,
&ArchiveProperties {
data_align: 4096,
initial_data_off: data_before_cpio.len() as u64,
..ArchiveProperties::default()
},
)
.unwrap();
assert!(wrote > NEWC_HDR_LEN * 5 + 514 * 1024);
let mut proc = Command::new("diff")
.args(&["dir1/file.txt", "-"])
.stdin(Stdio::piped())
.spawn()
.expect("diff failed to start");
{
let f = fs::File::open("extractor/dracut.cpio").unwrap();
let mut reader = io::BufReader::new(f);
reader.seek(io::SeekFrom::Start(16384 + 4096)).unwrap();
let mut take = reader.take(514 * 1024 as u64);
let mut stdin = proc.stdin.take().unwrap();
let copied = io::copy(&mut take, &mut stdin).unwrap();
assert_eq!(copied, 514 * 1024);
}
let status = proc.wait().unwrap();
assert!(status.success());
}
#[test]
fn test_archive_data_align_off_bad() {
let mut twd = TempWorkDir::new();
twd.create_tmp_file("file.txt", 514 * 1024);
let data_before_cpio = [5u8; 16384 + 3];
let f = fs::File::create("dracut.cpio").unwrap();
twd.cleanup_files.push(PathBuf::from("dracut.cpio"));
let mut writer = io::BufWriter::new(f);
writer.write_all(&data_before_cpio).unwrap();
let mut reader = io::BufReader::new("file.txt\n".as_bytes());
let res = archive_loop(
&mut reader,
&mut writer,
&ArchiveProperties {
data_align: 4096,
initial_data_off: data_before_cpio.len() as u64,
..ArchiveProperties::default()
},
);
assert!(res.is_err());
assert_eq!(io::ErrorKind::InvalidInput, res.unwrap_err().kind());
}
#[test]
fn test_archive_hardlinks_order() {
let mut twd = TempWorkDir::new();
twd.create_tmp_file("file.txt", 512 * 4);
fs::hard_link("file.txt", "link1.txt").unwrap();
twd.cleanup_files.push(PathBuf::from("link1.txt"));
fs::hard_link("file.txt", "link2.txt").unwrap();
twd.cleanup_files.push(PathBuf::from("link2.txt"));
twd.create_tmp_file("another.txt", 512 * 4);
let file_list: &str = "file.txt\nanother.txt\nlink1.txt\nlink2.txt\n";
gnu_cpio_create(file_list.as_bytes(), "gnu.cpio");
twd.cleanup_files.push(PathBuf::from("gnu.cpio"));
let f = fs::File::create("dracut.cpio").unwrap();
let mut writer = io::BufWriter::new(f);
let mut reader = io::BufReader::new(file_list.as_bytes());
let wrote = archive_loop(&mut reader, &mut writer, &ArchiveProperties::default()).unwrap();
twd.cleanup_files.push(PathBuf::from("dracut.cpio"));
assert!(wrote > NEWC_HDR_LEN * 5 + 512 * 8);
let status = Command::new("diff")
.args(&["gnu.cpio", "dracut.cpio"])
.status()
.expect("diff failed to start");
assert!(status.success());
}
#[test]
fn test_archive_hardlinks_empty() {
let mut twd = TempWorkDir::new();
twd.create_tmp_file("file.txt", 0);
fs::hard_link("file.txt", "link1.txt").unwrap();
twd.cleanup_files.push(PathBuf::from("link1.txt"));
fs::hard_link("file.txt", "link2.txt").unwrap();
twd.cleanup_files.push(PathBuf::from("link2.txt"));
twd.create_tmp_file("another.txt", 512 * 4);
let file_list: &str = "file.txt\nanother.txt\nlink1.txt\nlink2.txt\n";
gnu_cpio_create(file_list.as_bytes(), "gnu.cpio");
twd.cleanup_files.push(PathBuf::from("gnu.cpio"));
let f = fs::File::create("dracut.cpio").unwrap();
let mut writer = io::BufWriter::new(f);
let mut reader = io::BufReader::new(file_list.as_bytes());
let wrote = archive_loop(&mut reader, &mut writer, &ArchiveProperties::default()).unwrap();
twd.cleanup_files.push(PathBuf::from("dracut.cpio"));
assert!(wrote > NEWC_HDR_LEN * 5 + 512 * 4);
let status = Command::new("diff")
.args(&["gnu.cpio", "dracut.cpio"])
.status()
.expect("diff failed to start");
assert!(status.success());
}
#[test]
fn test_archive_hardlinks_missing() {
let mut twd = TempWorkDir::new();
twd.create_tmp_file("file.txt", 512 * 4);
fs::hard_link("file.txt", "link1.txt").unwrap();
twd.cleanup_files.push(PathBuf::from("link1.txt"));
fs::hard_link("file.txt", "link2.txt").unwrap();
twd.cleanup_files.push(PathBuf::from("link2.txt"));
fs::hard_link("file.txt", "link3.txt").unwrap();
twd.cleanup_files.push(PathBuf::from("link3.txt"));
twd.create_tmp_file("another.txt", 512 * 4);
// link2 missing from the archive, throwing off deferrals
let file_list: &str = "file.txt\nanother.txt\nlink1.txt\nlink3.txt\n";
gnu_cpio_create(file_list.as_bytes(), "gnu.cpio");
twd.cleanup_files.push(PathBuf::from("gnu.cpio"));
let f = fs::File::create("dracut.cpio").unwrap();
let mut writer = io::BufWriter::new(f);
let mut reader = io::BufReader::new(file_list.as_bytes());
let wrote = archive_loop(&mut reader, &mut writer, &ArchiveProperties::default()).unwrap();
twd.cleanup_files.push(PathBuf::from("dracut.cpio"));
assert!(wrote > NEWC_HDR_LEN * 5 + 512 * 8);
let status = Command::new("diff")
.args(&["gnu.cpio", "dracut.cpio"])
.status()
.expect("diff failed to start");
assert!(status.success());
}
#[test]
fn test_archive_hardlinks_multi() {
let mut twd = TempWorkDir::new();
twd.create_tmp_file("file.txt", 512 * 4);
fs::hard_link("file.txt", "link1.txt").unwrap();
twd.cleanup_files.push(PathBuf::from("link1.txt"));
fs::hard_link("file.txt", "link2.txt").unwrap();
twd.cleanup_files.push(PathBuf::from("link2.txt"));
twd.create_tmp_file("another.txt", 512 * 4);
fs::hard_link("another.txt", "anotherlink.txt").unwrap();
twd.cleanup_files.push(PathBuf::from("anotherlink.txt"));
// link2 missing from the archive, throwing off deferrals
let file_list: &str = "file.txt\nanother.txt\nlink1.txt\nanotherlink.txt\n";
gnu_cpio_create(file_list.as_bytes(), "gnu.cpio");
twd.cleanup_files.push(PathBuf::from("gnu.cpio"));
let f = fs::File::create("dracut.cpio").unwrap();
let mut writer = io::BufWriter::new(f);
let mut reader = io::BufReader::new(file_list.as_bytes());
let wrote = archive_loop(&mut reader, &mut writer, &ArchiveProperties::default()).unwrap();
twd.cleanup_files.push(PathBuf::from("dracut.cpio"));
assert!(wrote > NEWC_HDR_LEN * 5 + 512 * 8);
let status = Command::new("diff")
.args(&["gnu.cpio", "dracut.cpio"])
.status()
.expect("diff failed to start");
assert!(status.success());
}
#[test]
fn test_archive_duplicates() {
let mut twd = TempWorkDir::new();
twd.create_tmp_file("file.txt", 512 * 4);
twd.create_tmp_file("another.txt", 512 * 4);
// file.txt is listed twice
let file_list: &str = "file.txt\nanother.txt\nfile.txt\n";
gnu_cpio_create(file_list.as_bytes(), "gnu.cpio");
twd.cleanup_files.push(PathBuf::from("gnu.cpio"));
let f = fs::File::create("dracut.cpio").unwrap();
let mut writer = io::BufWriter::new(f);
let mut reader = io::BufReader::new(file_list.as_bytes());
let wrote = archive_loop(&mut reader, &mut writer, &ArchiveProperties::default()).unwrap();
twd.cleanup_files.push(PathBuf::from("dracut.cpio"));
assert!(wrote > NEWC_HDR_LEN * 4 + 512 * 12);
let status = Command::new("diff")
.args(&["gnu.cpio", "dracut.cpio"])
.status()
.expect("diff failed to start");
assert!(status.success());
}
#[test]
fn test_archive_hardlink_duplicates() {
let mut twd = TempWorkDir::new();
twd.create_tmp_file("file.txt", 512 * 4);
fs::hard_link("file.txt", "ln1.txt").unwrap();
twd.cleanup_files.push(PathBuf::from("ln1.txt"));
fs::hard_link("file.txt", "ln2.txt").unwrap();
twd.cleanup_files.push(PathBuf::from("ln2.txt"));
twd.create_tmp_file("f2.txt", 512 * 4);
// ln1 listed twice
let file_list: &str = "file.txt\nf2.txt\nln1.txt\nln1.txt\nln1.txt\n";
gnu_cpio_create(file_list.as_bytes(), "gnu.cpio");
twd.cleanup_files.push(PathBuf::from("gnu.cpio"));
let f = fs::File::create("dracut.cpio").unwrap();
let mut writer = io::BufWriter::new(f);
let mut reader = io::BufReader::new(file_list.as_bytes());
let wrote = archive_loop(&mut reader, &mut writer, &ArchiveProperties::default()).unwrap();
twd.cleanup_files.push(PathBuf::from("dracut.cpio"));
assert!(wrote > NEWC_HDR_LEN * 4 + 512 * 8);
let status = Command::new("diff")
.args(&["gnu.cpio", "dracut.cpio"])
.status()
.expect("diff failed to start");
assert!(status.success());
}
#[test]
fn test_archive_list_separator() {
let mut twd = TempWorkDir::new();
twd.create_tmp_file("file1", 33);
twd.create_tmp_file("file2", 55);
let file_list_nulldelim: &str = "file1\0file2\0";
let mut proc = Command::new("cpio")
.args(&[
"--quiet",
"-o",
"-H",
"newc",
"--reproducible",
"-F",
"gnu.cpio",
"--null",
])
.stdin(Stdio::piped())
.spawn()
.expect("GNU cpio failed to start");
{
let mut stdin = proc.stdin.take().unwrap();
stdin
.write_all(file_list_nulldelim.as_bytes())
.expect("Failed to write to stdin");
}
let status = proc.wait().unwrap();
assert!(status.success());
twd.cleanup_files.push(PathBuf::from("gnu.cpio"));
let f = fs::File::create("dracut.cpio").unwrap();
let mut writer = io::BufWriter::new(f);
let mut reader = io::BufReader::new(file_list_nulldelim.as_bytes());
let wrote = archive_loop(
&mut reader,
&mut writer,
&ArchiveProperties {
list_separator: b'\0',
..ArchiveProperties::default()
},
)
.unwrap();
twd.cleanup_files.push(PathBuf::from("dracut.cpio"));
assert!(wrote > NEWC_HDR_LEN * 3 + 33 + 55);
let status = Command::new("diff")
.args(&["gnu.cpio", "dracut.cpio"])
.status()
.expect("diff failed to start");
assert!(status.success());
}
#[test]
fn test_archive_fixed_mtime() {
let mut twd = TempWorkDir::new();
twd.create_tmp_file("file1", 33);
twd.create_tmp_file("file2", 55);
let file_list: &str = "file1\nfile2\n";
twd.create_tmp_dir("extractor");
let f = fs::File::create("extractor/dracut.cpio").unwrap();
let mut writer = io::BufWriter::new(f);
let mut reader = io::BufReader::new(file_list.as_bytes());
let wrote = archive_loop(
&mut reader,
&mut writer,
&ArchiveProperties {
fixed_mtime: Some(0),
..ArchiveProperties::default()
},
)
.unwrap();
twd.cleanup_files
.push(PathBuf::from("extractor/dracut.cpio"));
assert!(wrote > NEWC_HDR_LEN * 3 + 33 + 55);
let status = Command::new("cpio")
.current_dir("extractor")
.args(&[
"--quiet",
"-i",
"--preserve-modification-time",
"-H",
"newc",
"-F",
"dracut.cpio",
])
.status()
.expect("GNU cpio failed to start");
assert!(status.success());
twd.cleanup_files.push(PathBuf::from("extractor/file1"));
twd.cleanup_files.push(PathBuf::from("extractor/file2"));
let md = fs::symlink_metadata("extractor/file1").unwrap();
assert_eq!(md.mtime(), 0);
let md = fs::symlink_metadata("extractor/file2").unwrap();
assert_eq!(md.mtime(), 0);
}
#[test]
fn test_archive_stat_mtime() {
let mut twd = TempWorkDir::new();
twd.create_tmp_file("file1", 33);
twd.create_tmp_file("file2", 55);
let file_list: &str = "file1\nfile2\n";
twd.create_tmp_dir("extractor");
let f = fs::File::create("extractor/dracut.cpio").unwrap();
let mut writer = io::BufWriter::new(f);
let mut reader = io::BufReader::new(file_list.as_bytes());
assert_eq!(ArchiveProperties::default().fixed_mtime, None);
let wrote = archive_loop(&mut reader, &mut writer, &ArchiveProperties::default()).unwrap();
twd.cleanup_files
.push(PathBuf::from("extractor/dracut.cpio"));
assert!(wrote > NEWC_HDR_LEN * 3 + 33 + 55);
let status = Command::new("cpio")
.current_dir("extractor")
.args(&[
"--quiet",
"-i",
"--preserve-modification-time",
"-H",
"newc",
"-F",
"dracut.cpio",
])
.status()
.expect("GNU cpio failed to start");
assert!(status.success());
twd.cleanup_files.push(PathBuf::from("extractor/file1"));
twd.cleanup_files.push(PathBuf::from("extractor/file2"));
let src_md = fs::symlink_metadata("file1").unwrap();
let ex_md = fs::symlink_metadata("extractor/file1").unwrap();
assert_eq!(src_md.mtime(), ex_md.mtime());
let src_md = fs::symlink_metadata("file2").unwrap();
let ex_md = fs::symlink_metadata("extractor/file2").unwrap();
assert_eq!(src_md.mtime(), ex_md.mtime());
}
#[test]
fn test_archive_fixed_owner() {
let mut twd = TempWorkDir::new();
twd.create_tmp_file("file1", 33);
twd.create_tmp_file("file2", 55);
let file_list: &str = "file1\nfile2\n";
let md = fs::symlink_metadata("file1").unwrap();
// ideally we should check the process euid, but this will do...
if md.uid() != 0 {
println!("SKIPPED: this test requires root");
return;
}
twd.create_tmp_dir("extractor");
let f = fs::File::create("extractor/dracut.cpio").unwrap();
let mut writer = io::BufWriter::new(f);
let mut reader = io::BufReader::new(file_list.as_bytes());
let wrote = archive_loop(
&mut reader,
&mut writer,
&ArchiveProperties {
fixed_uid: Some(65534),
fixed_gid: Some(65534),
..ArchiveProperties::default()
},
)
.unwrap();
twd.cleanup_files
.push(PathBuf::from("extractor/dracut.cpio"));
assert!(wrote > NEWC_HDR_LEN * 3 + 33 + 55);
let status = Command::new("cpio")
.current_dir("extractor")
.args(&["--quiet", "-i", "-H", "newc", "-F", "dracut.cpio"])
.status()
.expect("GNU cpio failed to start");
assert!(status.success());
twd.cleanup_files.push(PathBuf::from("extractor/file1"));
twd.cleanup_files.push(PathBuf::from("extractor/file2"));
let md = fs::symlink_metadata("extractor/file1").unwrap();
assert_eq!(md.uid(), 65534);
assert_eq!(md.gid(), 65534);
let md = fs::symlink_metadata("extractor/file2").unwrap();
assert_eq!(md.uid(), 65534);
assert_eq!(md.gid(), 65534);
}
#[test]
fn test_archive_stat_owner() {
let mut twd = TempWorkDir::new();
twd.create_tmp_file("file1", 33);
twd.create_tmp_file("file2", 55);
let file_list: &str = "file1\nfile2\n";
twd.create_tmp_dir("extractor");
let f = fs::File::create("extractor/dracut.cpio").unwrap();
let mut writer = io::BufWriter::new(f);
let mut reader = io::BufReader::new(file_list.as_bytes());
assert_eq!(ArchiveProperties::default().fixed_uid, None);
assert_eq!(ArchiveProperties::default().fixed_gid, None);
let wrote = archive_loop(&mut reader, &mut writer, &ArchiveProperties::default()).unwrap();
twd.cleanup_files
.push(PathBuf::from("extractor/dracut.cpio"));
assert!(wrote > NEWC_HDR_LEN * 3 + 33 + 55);
let status = Command::new("cpio")
.current_dir("extractor")
.args(&["--quiet", "-i", "-H", "newc", "-F", "dracut.cpio"])
.status()
.expect("GNU cpio failed to start");
assert!(status.success());
twd.cleanup_files.push(PathBuf::from("extractor/file1"));
twd.cleanup_files.push(PathBuf::from("extractor/file2"));
let src_md = fs::symlink_metadata("file1").unwrap();
let ex_md = fs::symlink_metadata("extractor/file1").unwrap();
assert_eq!(src_md.uid(), ex_md.uid());
assert_eq!(src_md.gid(), ex_md.gid());
let src_md = fs::symlink_metadata("file2").unwrap();
let ex_md = fs::symlink_metadata("extractor/file2").unwrap();
assert_eq!(src_md.uid(), ex_md.uid());
assert_eq!(src_md.gid(), ex_md.gid());
}
#[test]
fn test_archive_dev_maj_min() {
let mut twd = TempWorkDir::new();
twd.create_tmp_file("file1", 0);
let md = fs::symlink_metadata("file1").unwrap();
if md.uid() != 0 {
println!("SKIPPED: this test requires root");
return;
}
twd.create_tmp_mknod("bdev1", 'b', Some((0x01, 0x01)));
twd.create_tmp_mknod("bdev2", 'b', Some((0x02, 0x100)));
twd.create_tmp_mknod("bdev3", 'b', Some((0x03, 0x1000)));
twd.create_tmp_mknod("bdev4", 'b', Some((0x04, 0x10000)));
twd.create_tmp_mknod("bdev5", 'b', Some((0x100, 0x05)));
twd.create_tmp_mknod("bdev6", 'b', Some((0x100, 0x06)));
let file_list: &str = "file1\nbdev1\nbdev2\nbdev3\nbdev4\nbdev5\nbdev6\n";
// create GNU cpio archive
twd.create_tmp_dir("gnucpio_xtr");
gnu_cpio_create(file_list.as_bytes(), "gnucpio_xtr/gnu.cpio");
twd.cleanup_files.push(PathBuf::from("gnucpio_xtr/gnu.cpio"));
// create Dracut cpio archive
twd.create_tmp_dir("dracut_xtr");
let f = fs::File::create("dracut_xtr/dracut.cpio").unwrap();
let mut writer = io::BufWriter::new(f);
let mut reader = io::BufReader::new(file_list.as_bytes());
let wrote = archive_loop(
&mut reader,
&mut writer,
&ArchiveProperties::default()
)
.unwrap();
twd.cleanup_files.push(PathBuf::from("dracut_xtr/dracut.cpio"));
let file_list_count = file_list.split_terminator('\n').count() as u64;
assert!(wrote >= NEWC_HDR_LEN * file_list_count
+ (file_list.len() as u64));
let status = Command::new("cpio")
.current_dir("gnucpio_xtr")
.args(&["--quiet", "-i", "-H", "newc", "-F", "gnu.cpio"])
.status()
.expect("GNU cpio failed to start");
assert!(status.success());
for s in file_list.split_terminator('\n') {
let p = PathBuf::from("gnucpio_xtr/".to_owned() + s);
twd.cleanup_files.push(p);
}
let status = Command::new("cpio")
.current_dir("dracut_xtr")
.args(&["--quiet", "-i", "-H", "newc", "-F", "dracut.cpio"])
.status()
.expect("GNU cpio failed to start");
assert!(status.success());
for s in file_list.split_terminator('\n') {
let dp = PathBuf::from("dracut_xtr/".to_owned() + s);
twd.cleanup_files.push(dp);
}
// diff extracted major/minor between dracut and GNU cpio created archives
for s in file_list.split_terminator('\n') {
let gmd = fs::symlink_metadata("gnucpio_xtr/".to_owned() + s).unwrap();
let dmd = fs::symlink_metadata("dracut_xtr/".to_owned() + s).unwrap();
print!("{}: cpio extracted dev_t gnu: {:#x}, dracut: {:#x}\n",
s, gmd.rdev(), dmd.rdev());
assert!(gmd.rdev() == dmd.rdev());
}
}
}