// 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;
    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());
        }
    }
}
