summaryrefslogtreecommitdiffstats
path: root/src/dracut-cpio
diff options
context:
space:
mode:
Diffstat (limited to 'src/dracut-cpio')
-rw-r--r--src/dracut-cpio/Cargo.lock12
-rw-r--r--src/dracut-cpio/Cargo.toml11
-rw-r--r--src/dracut-cpio/src/main.rs1813
-rw-r--r--src/dracut-cpio/third_party/crosvm/Cargo.toml8
-rw-r--r--src/dracut-cpio/third_party/crosvm/LICENSE27
-rw-r--r--src/dracut-cpio/third_party/crosvm/argument.rs549
-rw-r--r--src/dracut-cpio/third_party/crosvm/crosvm.rs2
7 files changed, 2422 insertions, 0 deletions
diff --git a/src/dracut-cpio/Cargo.lock b/src/dracut-cpio/Cargo.lock
new file mode 100644
index 0000000..75dca6f
--- /dev/null
+++ b/src/dracut-cpio/Cargo.lock
@@ -0,0 +1,12 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+[[package]]
+name = "crosvm"
+version = "0.1.0"
+
+[[package]]
+name = "dracut-cpio"
+version = "0.1.0"
+dependencies = [
+ "crosvm",
+]
diff --git a/src/dracut-cpio/Cargo.toml b/src/dracut-cpio/Cargo.toml
new file mode 100644
index 0000000..28cfb15
--- /dev/null
+++ b/src/dracut-cpio/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "dracut-cpio"
+description = "cpio archive generator for Dracut"
+authors = ["David Disseldorp"]
+license = "GPL-2.0"
+version = "0.1.0"
+edition = "2018"
+
+[dependencies]
+crosvm = { path = "third_party/crosvm" }
+# please avoid adding any more dependencies
diff --git a/src/dracut-cpio/src/main.rs b/src/dracut-cpio/src/main.rs
new file mode 100644
index 0000000..c3bfe90
--- /dev/null
+++ b/src/dracut-cpio/src/main.rs
@@ -0,0 +1,1813 @@
+// 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 reproducibility. 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());
+ }
+ }
+}
diff --git a/src/dracut-cpio/third_party/crosvm/Cargo.toml b/src/dracut-cpio/third_party/crosvm/Cargo.toml
new file mode 100644
index 0000000..a56ff2d
--- /dev/null
+++ b/src/dracut-cpio/third_party/crosvm/Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "crosvm"
+version = "0.1.0"
+authors = ["The Chromium OS Authors"]
+edition = "2018"
+
+[lib]
+path = "crosvm.rs"
diff --git a/src/dracut-cpio/third_party/crosvm/LICENSE b/src/dracut-cpio/third_party/crosvm/LICENSE
new file mode 100644
index 0000000..8bafca3
--- /dev/null
+++ b/src/dracut-cpio/third_party/crosvm/LICENSE
@@ -0,0 +1,27 @@
+// Copyright 2017 The Chromium OS Authors. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/src/dracut-cpio/third_party/crosvm/argument.rs b/src/dracut-cpio/third_party/crosvm/argument.rs
new file mode 100644
index 0000000..a2dbbbd
--- /dev/null
+++ b/src/dracut-cpio/third_party/crosvm/argument.rs
@@ -0,0 +1,549 @@
+// Copyright 2017 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+//! Handles argument parsing.
+//!
+//! # Example
+//!
+//! ```
+//! # use crosvm::argument::{Argument, Error, print_help, set_arguments};
+//! # let args: std::slice::Iter<String> = [].iter();
+//! let arguments = &[
+//! Argument::positional("FILES", "files to operate on"),
+//! Argument::short_value('p', "program", "PROGRAM", "Program to apply to each file"),
+//! Argument::short_value('c', "cpus", "N", "Number of CPUs to use. (default: 1)"),
+//! Argument::flag("unmount", "Unmount the root"),
+//! Argument::short_flag('h', "help", "Print help message."),
+//! ];
+//!
+//! let match_res = set_arguments(args, arguments, |name, value| {
+//! match name {
+//! "" => println!("positional arg! {}", value.unwrap()),
+//! "program" => println!("gonna use program {}", value.unwrap()),
+//! "cpus" => {
+//! let v: u32 = value.unwrap().parse().map_err(|_| {
+//! Error::InvalidValue {
+//! value: value.unwrap().to_owned(),
+//! expected: String::from("this value for `cpus` needs to be integer"),
+//! }
+//! })?;
+//! }
+//! "unmount" => println!("gonna unmount"),
+//! "help" => return Err(Error::PrintHelp),
+//! _ => unreachable!(),
+//! }
+//! unreachable!();
+//! });
+//!
+//! match match_res {
+//! Ok(_) => println!("running with settings"),
+//! Err(Error::PrintHelp) => print_help("best_program", "FILES", arguments),
+//! Err(e) => println!("{}", e),
+//! }
+//! ```
+
+use std::fmt::{self, Display};
+use std::result;
+
+/// An error with argument parsing.
+#[derive(Debug)]
+pub enum Error {
+ /// There was a syntax error with the argument.
+ Syntax(String),
+ /// The argument's name is unused.
+ UnknownArgument(String),
+ /// The argument was required.
+ ExpectedArgument(String),
+ /// The argument's given value is invalid.
+ InvalidValue { value: String, expected: String },
+ /// The argument was already given and none more are expected.
+ TooManyArguments(String),
+ /// The argument expects a value.
+ ExpectedValue(String),
+ /// The argument does not expect a value.
+ UnexpectedValue(String),
+ /// The help information was requested
+ PrintHelp,
+}
+
+impl Display for Error {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ use self::Error::*;
+
+ match self {
+ Syntax(s) => write!(f, "syntax error: {}", s),
+ UnknownArgument(s) => write!(f, "unknown argument: {}", s),
+ ExpectedArgument(s) => write!(f, "expected argument: {}", s),
+ InvalidValue { value, expected } => {
+ write!(f, "invalid value {:?}: {}", value, expected)
+ }
+ TooManyArguments(s) => write!(f, "too many arguments: {}", s),
+ ExpectedValue(s) => write!(f, "expected parameter value: {}", s),
+ UnexpectedValue(s) => write!(f, "unexpected parameter value: {}", s),
+ PrintHelp => write!(f, "help was requested"),
+ }
+ }
+}
+
+/// Result of a argument parsing.
+pub type Result<T> = result::Result<T, Error>;
+
+#[derive(Debug, PartialEq)]
+pub enum ArgumentValueMode {
+ /// Specifies that an argument requires a value and that an error should be generated if
+ /// no value is provided during parsing.
+ Required,
+
+ /// Specifies that an argument does not allow a value and that an error should be returned
+ /// if a value is provided during parsing.
+ Disallowed,
+
+ /// Specifies that an argument may have a value during parsing but is not required to.
+ Optional,
+}
+
+/// Information about an argument expected from the command line.
+///
+/// # Examples
+///
+/// To indicate a flag style argument:
+///
+/// ```
+/// # use crosvm::argument::Argument;
+/// Argument::short_flag('f', "flag", "enable awesome mode");
+/// ```
+///
+/// To indicate a parameter style argument that expects a value:
+///
+/// ```
+/// # use crosvm::argument::Argument;
+/// // "VALUE" and "NETMASK" are placeholder values displayed in the help message for these
+/// // arguments.
+/// Argument::short_value('v', "val", "VALUE", "how much do you value this usage information");
+/// Argument::value("netmask", "NETMASK", "hides your netface");
+/// ```
+///
+/// To indicate an argument with no short version:
+///
+/// ```
+/// # use crosvm::argument::Argument;
+/// Argument::flag("verbose", "this option is hard to type quickly");
+/// ```
+///
+/// To indicate a positional argument:
+///
+/// ```
+/// # use crosvm::argument::Argument;
+/// Argument::positional("VALUES", "these are positional arguments");
+/// ```
+pub struct Argument {
+ /// The name of the value to display in the usage information.
+ pub value: Option<&'static str>,
+ /// Specifies how values should be handled for this this argument.
+ pub value_mode: ArgumentValueMode,
+ /// Optional single character shortened argument name.
+ pub short: Option<char>,
+ /// The long name of this argument.
+ pub long: &'static str,
+ /// Helpful usage information for this argument to display to the user.
+ pub help: &'static str,
+}
+
+impl Argument {
+ pub fn positional(value: &'static str, help: &'static str) -> Argument {
+ Argument {
+ value: Some(value),
+ value_mode: ArgumentValueMode::Required,
+ short: None,
+ long: "",
+ help,
+ }
+ }
+
+ pub fn value(long: &'static str, value: &'static str, help: &'static str) -> Argument {
+ Argument {
+ value: Some(value),
+ value_mode: ArgumentValueMode::Required,
+ short: None,
+ long,
+ help,
+ }
+ }
+
+ pub fn short_value(
+ short: char,
+ long: &'static str,
+ value: &'static str,
+ help: &'static str,
+ ) -> Argument {
+ Argument {
+ value: Some(value),
+ value_mode: ArgumentValueMode::Required,
+ short: Some(short),
+ long,
+ help,
+ }
+ }
+
+ pub fn flag(long: &'static str, help: &'static str) -> Argument {
+ Argument {
+ value: None,
+ value_mode: ArgumentValueMode::Disallowed,
+ short: None,
+ long,
+ help,
+ }
+ }
+
+ pub fn short_flag(short: char, long: &'static str, help: &'static str) -> Argument {
+ Argument {
+ value: None,
+ value_mode: ArgumentValueMode::Disallowed,
+ short: Some(short),
+ long,
+ help,
+ }
+ }
+
+ pub fn flag_or_value(long: &'static str, value: &'static str, help: &'static str) -> Argument {
+ Argument {
+ value: Some(value),
+ value_mode: ArgumentValueMode::Optional,
+ short: None,
+ long,
+ help,
+ }
+ }
+}
+
+fn parse_arguments<I, R, F>(args: I, mut f: F) -> Result<()>
+where
+ I: Iterator<Item = R>,
+ R: AsRef<str>,
+ F: FnMut(&str, Option<&str>) -> Result<()>,
+{
+ enum State {
+ // Initial state at the start and after finishing a single argument/value.
+ Top,
+ // The remaining arguments are all positional.
+ Positional,
+ // The next string is the value for the argument `name`.
+ Value { name: String },
+ }
+ let mut s = State::Top;
+ for arg in args {
+ let arg = arg.as_ref();
+ loop {
+ let mut arg_consumed = true;
+ s = match s {
+ State::Top => {
+ if arg == "--" {
+ State::Positional
+ } else if arg.starts_with("--") {
+ let param = arg.trim_start_matches('-');
+ if param.contains('=') {
+ let mut iter = param.splitn(2, '=');
+ let name = iter.next().unwrap();
+ let value = iter.next().unwrap();
+ if name.is_empty() {
+ return Err(Error::Syntax(
+ "expected parameter name before `=`".to_owned(),
+ ));
+ }
+ if value.is_empty() {
+ return Err(Error::Syntax(
+ "expected parameter value after `=`".to_owned(),
+ ));
+ }
+ f(name, Some(value))?;
+ State::Top
+ } else {
+ State::Value {
+ name: param.to_owned(),
+ }
+ }
+ } else if arg.starts_with('-') {
+ if arg.len() == 1 {
+ return Err(Error::Syntax(
+ "expected argument short name after `-`".to_owned(),
+ ));
+ }
+ let name = &arg[1..2];
+ let value = if arg.len() > 2 { Some(&arg[2..]) } else { None };
+ if let Err(e) = f(name, value) {
+ if let Error::ExpectedValue(_) = e {
+ State::Value {
+ name: name.to_owned(),
+ }
+ } else {
+ return Err(e);
+ }
+ } else {
+ State::Top
+ }
+ } else {
+ f("", Some(&arg))?;
+ State::Positional
+ }
+ }
+ State::Positional => {
+ f("", Some(&arg))?;
+ State::Positional
+ }
+ State::Value { name } => {
+ if arg.starts_with('-') {
+ arg_consumed = false;
+ f(&name, None)?;
+ } else if let Err(e) = f(&name, Some(&arg)) {
+ arg_consumed = false;
+ f(&name, None).map_err(|_| e)?;
+ }
+ State::Top
+ }
+ };
+
+ if arg_consumed {
+ break;
+ }
+ }
+ }
+
+ // If we ran out of arguments while parsing the last parameter, which may be either a
+ // value parameter or a flag, try to parse it as a flag. This will produce "missing value"
+ // error if the parameter is in fact a value parameter, which is the desired outcome.
+ match s {
+ State::Value { name } => f(&name, None),
+ _ => Ok(()),
+ }
+}
+
+/// Parses the given `args` against the list of know arguments `arg_list` and calls `f` with each
+/// present argument and value if required.
+///
+/// This function guarantees that only valid long argument names from `arg_list` are sent to the
+/// callback `f`. It is also guaranteed that if an arg requires a value (i.e.
+/// `arg.value.is_some()`), the value will be `Some` in the callbacks arguments. If the callback
+/// returns `Err`, this function will end parsing and return that `Err`.
+///
+/// See the [module level](index.html) example for a usage example.
+pub fn set_arguments<I, R, F>(args: I, arg_list: &[Argument], mut f: F) -> Result<()>
+where
+ I: Iterator<Item = R>,
+ R: AsRef<str>,
+ F: FnMut(&str, Option<&str>) -> Result<()>,
+{
+ parse_arguments(args, |name, value| {
+ let mut matches = None;
+ for arg in arg_list {
+ if let Some(short) = arg.short {
+ if name.len() == 1 && name.starts_with(short) {
+ if value.is_some() != arg.value.is_some() {
+ return Err(Error::ExpectedValue(short.to_string()));
+ }
+ matches = Some(arg.long);
+ }
+ }
+ if matches.is_none() && arg.long == name {
+ if value.is_none() && arg.value_mode == ArgumentValueMode::Required {
+ return Err(Error::ExpectedValue(arg.long.to_owned()));
+ }
+ if value.is_some() && arg.value_mode == ArgumentValueMode::Disallowed {
+ return Err(Error::UnexpectedValue(arg.long.to_owned()));
+ }
+ matches = Some(arg.long);
+ }
+ }
+ match matches {
+ Some(long) => f(long, value),
+ None => Err(Error::UnknownArgument(name.to_owned())),
+ }
+ })
+}
+
+/// Prints command line usage information to stdout.
+///
+/// Usage information is printed according to the help fields in `args` with a leading usage line.
+/// The usage line is of the format "`program_name` \[ARGUMENTS\] `required_arg`".
+pub fn print_help(program_name: &str, required_arg: &str, args: &[Argument]) {
+ println!(
+ "Usage: {} {}{}\n",
+ program_name,
+ if args.is_empty() { "" } else { "[ARGUMENTS] " },
+ required_arg
+ );
+ if args.is_empty() {
+ return;
+ }
+ println!("Argument{}:", if args.len() > 1 { "s" } else { "" });
+ for arg in args {
+ match arg.short {
+ Some(s) => print!(" -{}, ", s),
+ None => print!(" "),
+ }
+ if arg.long.is_empty() {
+ print!(" ");
+ } else {
+ print!("--");
+ }
+ print!("{:<12}", arg.long);
+ if let Some(v) = arg.value {
+ if arg.long.is_empty() {
+ print!(" ");
+ } else {
+ print!("=");
+ }
+ print!("{:<10}", v);
+ } else {
+ print!("{:<11}", "");
+ }
+ println!("{}", arg.help);
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn request_help() {
+ let arguments = [Argument::short_flag('h', "help", "Print help message.")];
+
+ let match_res = set_arguments(["-h"].iter(), &arguments[..], |name, _| {
+ match name {
+ "help" => return Err(Error::PrintHelp),
+ _ => unreachable!(),
+ };
+ });
+ match match_res {
+ Err(Error::PrintHelp) => {}
+ _ => unreachable!(),
+ }
+ }
+
+ #[test]
+ fn mixed_args() {
+ let arguments = [
+ Argument::positional("FILES", "files to operate on"),
+ Argument::short_value('p', "program", "PROGRAM", "Program to apply to each file"),
+ Argument::short_value('c', "cpus", "N", "Number of CPUs to use. (default: 1)"),
+ Argument::flag("unmount", "Unmount the root"),
+ Argument::short_flag('h', "help", "Print help message."),
+ ];
+
+ let mut unmount = false;
+ let match_res = set_arguments(
+ ["--cpus", "3", "--program", "hello", "--unmount", "file"].iter(),
+ &arguments[..],
+ |name, value| {
+ match name {
+ "" => assert_eq!(value.unwrap(), "file"),
+ "program" => assert_eq!(value.unwrap(), "hello"),
+ "cpus" => {
+ let c: u32 = value.unwrap().parse().map_err(|_| Error::InvalidValue {
+ value: value.unwrap().to_owned(),
+ expected: String::from("this value for `cpus` needs to be integer"),
+ })?;
+ assert_eq!(c, 3);
+ }
+ "unmount" => unmount = true,
+ "help" => return Err(Error::PrintHelp),
+ _ => unreachable!(),
+ };
+ Ok(())
+ },
+ );
+ assert!(match_res.is_ok());
+ assert!(unmount);
+ }
+
+ #[test]
+ fn name_value_pair() {
+ let arguments = [Argument::short_value(
+ 'c',
+ "cpus",
+ "N",
+ "Number of CPUs to use. (default: 1)",
+ )];
+ let match_res = set_arguments(
+ ["-c", "5", "--cpus", "5", "-c5", "--cpus=5"].iter(),
+ &arguments[..],
+ |name, value| {
+ assert_eq!(name, "cpus");
+ assert_eq!(value, Some("5"));
+ Ok(())
+ },
+ );
+ assert!(match_res.is_ok());
+ let not_match_res = set_arguments(
+ ["-c", "5", "--cpus"].iter(),
+ &arguments[..],
+ |name, value| {
+ assert_eq!(name, "cpus");
+ assert_eq!(value, Some("5"));
+ Ok(())
+ },
+ );
+ assert!(not_match_res.is_err());
+ }
+
+ #[test]
+ fn flag_or_value() {
+ let run_case = |args| -> Option<String> {
+ let arguments = [
+ Argument::positional("FILES", "files to operate on"),
+ Argument::flag_or_value("gpu", "[2D|3D]", "Enable or configure gpu"),
+ Argument::flag("foo", "Enable foo."),
+ Argument::value("bar", "stuff", "Configure bar."),
+ ];
+
+ let mut gpu_value: Option<String> = None;
+ let match_res =
+ set_arguments(args, &arguments[..], |name: &str, value: Option<&str>| {
+ match name {
+ "" => assert_eq!(value.unwrap(), "file1"),
+ "foo" => assert!(value.is_none()),
+ "bar" => assert_eq!(value.unwrap(), "stuff"),
+ "gpu" => match value {
+ Some(v) => match v {
+ "2D" | "3D" => {
+ gpu_value = Some(v.to_string());
+ }
+ _ => {
+ return Err(Error::InvalidValue {
+ value: v.to_string(),
+ expected: String::from("2D or 3D"),
+ })
+ }
+ },
+ None => {
+ gpu_value = None;
+ }
+ },
+ _ => unreachable!(),
+ };
+ Ok(())
+ });
+
+ assert!(match_res.is_ok());
+ gpu_value
+ };
+
+ // Used as flag and followed by positional
+ assert_eq!(run_case(["--gpu", "file1"].iter()), None);
+ // Used as flag and followed by flag
+ assert_eq!(run_case(["--gpu", "--foo", "file1",].iter()), None);
+ // Used as flag and followed by value
+ assert_eq!(run_case(["--gpu", "--bar=stuff", "file1"].iter()), None);
+
+ // Used as value and followed by positional
+ assert_eq!(run_case(["--gpu=2D", "file1"].iter()).unwrap(), "2D");
+ // Used as value and followed by flag
+ assert_eq!(run_case(["--gpu=2D", "--foo"].iter()).unwrap(), "2D");
+ // Used as value and followed by value
+ assert_eq!(
+ run_case(["--gpu=2D", "--bar=stuff", "file1"].iter()).unwrap(),
+ "2D"
+ );
+ }
+}
diff --git a/src/dracut-cpio/third_party/crosvm/crosvm.rs b/src/dracut-cpio/third_party/crosvm/crosvm.rs
new file mode 100644
index 0000000..ddccbfe
--- /dev/null
+++ b/src/dracut-cpio/third_party/crosvm/crosvm.rs
@@ -0,0 +1,2 @@
+// ensure that existing crosvm::argument imports work without modification...
+pub mod argument;