~danyspin97/initrz

ab15e2ccc12dbcef96319942312cc6062e9f3fa5 — Danilo Spinella 5 months ago bf07ac8
mkinitrz: Add initial implementation
A src/mkinitrz/src/config.rs => src/mkinitrz/src/config.rs +21 -0
@@ 0,0 1,21 @@
use std::{fs, path::Path};

use anyhow::Result;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
pub struct Config {
    pub modules: Vec<String>,
}

impl Config {
    pub fn new(file: &Path) -> Result<Config> {
        if file.exists() {
            Ok(serde_yaml::from_slice(&fs::read(file)?)?)
        } else {
            Ok(Config {
                modules: Vec::new(),
            })
        }
    }
}

A src/mkinitrz/src/depend.rs => src/mkinitrz/src/depend.rs +205 -0
@@ 0,0 1,205 @@
// https://github.com/Farenjihn/elusive/blob/151b7e8080b75944327f949cbf2eab25490e5341/src/depend.rs

use anyhow::{bail, Result};
use log::error;
use object::elf::PT_DYNAMIC;
use object::elf::{FileHeader32, FileHeader64};
use object::elf::{DT_NEEDED, DT_STRSZ, DT_STRTAB};
use object::pod::Bytes;
use object::read::elf::{Dyn, FileHeader, ProgramHeader};
use object::read::FileKind;
use object::{Endianness, StringTable};
use std::convert::TryInto;
use std::ffi::{CStr, CString, OsStr, OsString};
use std::fs;
use std::mem::MaybeUninit;
use std::os::unix::ffi::OsStrExt;
use std::path::{Path, PathBuf};

pub fn resolve(path: &Path) -> Result<Vec<PathBuf>> {
    let data = fs::read(path)?;

    let kind = FileKind::parse(&data)?;
    let bytes = Bytes(&data);

    let needed = match kind {
        FileKind::Elf32 => {
            let elf = FileHeader32::<Endianness>::parse(bytes)?;
            elf_needed(elf, bytes)
        }
        FileKind::Elf64 => {
            let elf = FileHeader64::<Endianness>::parse(bytes)?;
            elf_needed(elf, bytes)
        }
        _ => {
            error!("Failed to parse binary");
            bail!("only elf files are supported");
        }
    }?;

    let mut resolved = Vec::new();

    for lib in needed {
        walk_linkmap(&lib, &mut resolved)?;
    }

    Ok(resolved)
}

fn elf_needed<T>(elf: &T, data: Bytes) -> Result<Vec<OsString>>
where
    T: FileHeader<Endian = Endianness>,
{
    let endian = elf.endian()?;
    let headers = elf.program_headers(endian, data)?;

    let mut strtab = 0;
    let mut strsz = 0;

    let mut offsets = Vec::new();

    for header in headers {
        if header.p_type(endian) == PT_DYNAMIC {
            if let Some(dynamic) = header.dynamic(endian, data)? {
                for entry in dynamic {
                    let d_tag = entry.d_tag(endian).into();

                    if d_tag == DT_STRTAB as u64 {
                        strtab = entry.d_val(endian).into();
                    } else if d_tag == DT_STRSZ as u64 {
                        strsz = entry.d_val(endian).into();
                    } else if d_tag == DT_NEEDED as u64 {
                        offsets.push(entry.d_val(endian).into());
                    }
                }
            }
        }
    }

    let mut needed = Vec::new();

    for header in headers {
        if let Ok(Some(data)) = header.data_range(endian, data, strtab, strsz) {
            let dynstr = StringTable::new(data);

            for offset in offsets {
                let offset = offset.try_into()?;
                let name = dynstr.get(offset).expect("offset exists in string table");
                let path = OsStr::from_bytes(name).to_os_string();

                needed.push(path);
            }

            break;
        }
    }

    Ok(needed)
}

fn walk_linkmap(lib: &OsStr, resolved: &mut Vec<PathBuf>) -> Result<()> {
    let name = CString::new(lib.as_bytes())?;
    let mut linkmap = MaybeUninit::<*mut link_map>::uninit();

    let handle = unsafe { libc::dlopen(name.as_ptr(), libc::RTLD_LAZY) };
    if handle.is_null() {
        let error = unsafe {
            CStr::from_ptr(libc::dlerror())
                .to_str()
                .expect("error should be valid utf8")
        };

        error!("Failed to open handle to dynamic dependency for {:?}", lib);
        bail!("dlopen failed: {}", error);
    }

    let ret = unsafe {
        libc::dlinfo(
            handle,
            libc::RTLD_DI_LINKMAP,
            linkmap.as_mut_ptr() as *mut libc::c_void,
        )
    };

    if ret < 0 {
        error!("Failed to get path to dynamic dependency for {:?}", lib);
        bail!("dlinfo failed");
    }

    let mut names = Vec::new();
    unsafe {
        let mut linkmap = linkmap.assume_init();

        // walk back to the beginning of the link map
        while !(*linkmap).l_prev.is_null() {
            linkmap = (*linkmap).l_prev as *mut link_map;
        }

        // skip first entry in linkmap since its name is empty
        // next entry is also skipped since it is the vDSO
        linkmap = (*linkmap).l_next as *mut link_map;

        // walk through the link map and add entries
        while !(*linkmap).l_next.is_null() {
            linkmap = (*linkmap).l_next as *mut link_map;
            names.push(CStr::from_ptr((*linkmap).l_name));
        }
    };

    for name in names {
        let path = PathBuf::from(OsStr::from_bytes(name.to_bytes()));
        resolved.push(path);
    }

    let ret = unsafe { libc::dlclose(handle) };
    if ret < 0 {
        error!("Failed to close handle to dynamic dependency for {:?}", lib);
        bail!("dlclose failed");
    }

    Ok(())
}

/// C struct used in `dlinfo` with `RTLD_DI_LINKMAP`
#[repr(C)]
struct link_map {
    l_addr: u64,
    l_name: *mut libc::c_char,
    l_ld: *mut libc::c_void,
    l_next: *mut libc::c_void,
    l_prev: *mut libc::c_void,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_resolver() -> Result<()> {
        let ls = PathBuf::from("/bin/ls");

        if ls.exists() {
            let dependencies = resolve(&ls)?;
            let mut found_libc = false;

            for lib in dependencies {
                if lib
                    .file_name()
                    .expect("library path should have filename")
                    .to_str()
                    .expect("filename should be valid utf8")
                    .starts_with("libc")
                {
                    found_libc = true;
                    break;
                }
            }

            if !found_libc {
                bail!("resolver did not list libc in dependencies")
            }
        }

        Ok(())
    }
}

A src/mkinitrz/src/initramfs.rs => src/mkinitrz/src/initramfs.rs +210 -0
@@ 0,0 1,210 @@
use std::{
    collections::HashSet,
    convert::TryInto,
    env, fs,
    os::unix::fs::MetadataExt,
    path::{Path, PathBuf},
};

use anyhow::Result;
use log::debug;

use crate::config::Config;
use crate::depend;
use crate::module::Module;
use crate::modules;
use crate::newc::{self, Archive, Entry, EntryBuilder};

const ROOT_DIRECTORIES: [&str; 9] = [
    "/dev",
    "/etc",
    "/new_root",
    "/proc",
    "/run",
    "/sys",
    "/usr",
    "/usr/bin",
    "/usr/lib",
];

const ROOT_SYMLINKS: [(&str, &str); 6] = [
    ("/bin", "usr/bin"),
    ("/lib", "usr/lib"),
    ("/lib64", "lib"),
    ("/sbin", "usr/sbin"),
    ("/usr/lib64", "lib"),
    ("/usr/sbin", "bin"),
];

const DEFAULT_DIR_MODE: u32 = 0o040_000 + 0o755;
const DEFAULT_SYMLINK_MODE: u32 = 0o120_000;
const DEFAULT_FILE_MODE: u32 = 0o644;

pub struct Initramfs {
    entries: Vec<Entry>,
    files: HashSet<PathBuf>,
}

impl Initramfs {
    pub fn new(kver: &str, config: Config) -> Result<Initramfs> {
        let mut initramfs = Initramfs::new_common_structure(kver, &config)?;

        let modules = modules::get_general_modules(kver, config.modules)?;
        initramfs.add_modules(kver, modules)?;

        Ok(initramfs)
    }

    pub fn with_host_settings(kver: &str, config: Config) -> Result<Initramfs> {
        let mut initramfs = Initramfs::new_common_structure(kver, &config)?;

        let crypttab = Path::new("/etc/crypttab.initramfs");
        if crypttab.exists() {
            initramfs.add_file(crypttab)?;
        }

        let modules = modules::get_host_modules(kver, config.modules)?;
        initramfs.add_modules(kver, modules)?;

        Ok(initramfs)
    }

    fn new_common_structure(kver: &str, config: &Config) -> Result<Initramfs> {
        let mut entries = Vec::new();
        let mut files: HashSet<PathBuf> = HashSet::new();

        ROOT_DIRECTORIES.iter().for_each(|dir| {
            files.insert((*dir).into());
            entries.push(EntryBuilder::directory(dir).mode(DEFAULT_DIR_MODE).build())
        });

        ROOT_SYMLINKS.iter().for_each(|(src, dest)| {
            files.insert((*src).into());
            entries.push(
                EntryBuilder::symlink(src, Path::new(dest))
                    .mode(DEFAULT_SYMLINK_MODE)
                    .build(),
            )
        });

        let mut initramfs = Initramfs { entries, files };

        let mut initrz: PathBuf =
            Path::new(&env::var("INITRZ").unwrap_or("target/release/initrz".to_string())).into();
        if !initrz.exists() {
            initrz = Path::new("/sbin/initrz").into();
        }
        initramfs.add_elf_with_path(&initrz, Path::new("/init"))?;

        initramfs.add_elf(Path::new("/sbin/vgchange"))?;
        initramfs.add_elf(Path::new("/sbin/vgmknodes"))?;

        initramfs.add_elf(Path::new("/bin/busybox"))?;

        let ld_conf = Path::new("/etc/ld.so.conf");
        initramfs.add_entry(
            ld_conf,
            EntryBuilder::file(ld_conf, Vec::new())
                .with_metadata(&fs::metadata(&ld_conf)?)
                .build(),
        );

        let kernel_root = Path::new("/lib/modules").join(kver);
        initramfs.add_file(&kernel_root.join("modules.dep"))?;
        initramfs.add_file(&kernel_root.join("modules.alias"))?;

        initramfs.apply_config(config);

        Ok(initramfs)
    }

    fn apply_config(&mut self, config: &Config) {}

    fn add_elf(&mut self, exe: &Path) -> Result<()> {
        self.add_elf_with_path(exe, exe)
    }

    fn add_elf_with_path(&mut self, exe: &Path, path: &Path) -> Result<()> {
        if !self.add_file_with_path(exe, path)? {
            return Ok(());
        }
        depend::resolve(Path::new(exe))?
            .iter()
            .try_for_each(|lib| -> Result<()> { self.add_library(lib) })?;

        Ok(())
    }

    fn add_library(&mut self, lib: &Path) -> Result<()> {
        let libname = lib.file_name().unwrap();
        if !self.add_file_with_path(lib, &Path::new("/usr/lib").join(libname))? {
            return Ok(());
        }

        depend::resolve(lib)?
            .iter()
            .try_for_each(|lib| -> Result<()> { self.add_library(lib) })?;

        Ok(())
    }

    fn add_file(&mut self, path: &Path) -> Result<bool> {
        self.add_file_with_path(path, path)
    }

    fn add_file_with_path(&mut self, file: &Path, path: &Path) -> Result<bool> {
        let file = fs::canonicalize(file)?;
        let path = path.to_path_buf();
        if self.files.contains(&path) {
            return Ok(false);
        }
        self.add_directory(
            path.parent()
                .expect("Files path shall contain a parent directory"),
        );
        self.add_entry(
            &path,
            EntryBuilder::file(&path, fs::read(&file)?)
                .with_metadata(&fs::metadata(file)?)
                .build(),
        );
        Ok(true)
    }

    fn add_directory(&mut self, dir: &Path) {
        if self.files.contains(dir) {
            return;
        }
        if let Some(parent) = dir.parent() {
            self.add_directory(parent);
        }

        self.add_entry(
            &dir,
            EntryBuilder::directory(&dir).mode(DEFAULT_DIR_MODE).build(),
        );
    }

    fn add_entry(&mut self, path: &Path, entry: Entry) {
        debug!("Added entry {:?}", path);
        self.files.insert(path.into());
        self.entries.push(entry);
    }

    fn add_modules(&mut self, kver: &str, modules: Vec<Module>) -> Result<()> {
        Ok(modules.iter().try_for_each(|module| -> Result<()> {
            let path = &module.path.with_extension("");
            self.add_directory(path.parent().unwrap());
            Ok(self.add_entry(
                &path,
                EntryBuilder::file(&path, module.into_bytes()?)
                    .with_metadata(&fs::metadata(&module.path)?)
                    .build(),
            ))
        })?)
    }

    pub fn into_bytes(self) -> Result<Vec<u8>> {
        Archive::new(self.entries).into_bytes()
    }
}

M src/mkinitrz/src/main.rs => src/mkinitrz/src/main.rs +72 -2
@@ 1,3 1,73 @@
fn main() {
    println!("Hello, world!");
mod config;
mod depend;
mod initramfs;
mod module;
mod modules;
mod newc;

use std::{ffi::OsString, fs::File, io::Write, path::Path};

use anyhow::Result;
use clap::Clap;
use log;
use simplelog::{ColorChoice, LevelFilter, TermLogger, TerminalMode};
use zstd::stream::write::Encoder;

use config::Config;
use initramfs::Initramfs;

#[derive(Clap)]
#[clap(version = "0.1", author = "danyspin97")]
struct Opts {
    #[clap(
        short = 'c',
        long = "config",
        default_value = "/etc/initrz/mkinitrz.conf"
    )]
    config: OsString,
    #[clap(short = 'h', long = "host-only")]
    host: bool,
    #[clap(short = 'k', long = "kver")]
    kver: String,
    #[clap(short = 'o', long = "output")]
    output: OsString,
    #[clap(short = 'q', long = "quiet")]
    quiet: bool,
    #[clap(short = 'v', long = "verbose", parse(from_occurrences))]
    verbose: u32,
}

fn main() -> Result<()> {
    let opts: Opts = Opts::parse();

    TermLogger::init(
        if opts.quiet {
            LevelFilter::Error
        } else {
            match opts.verbose {
                0 => LevelFilter::Warn,
                1 => LevelFilter::Info,
                2 => LevelFilter::Debug,
                _ => LevelFilter::Trace,
            }
        },
        simplelog::Config::default(),
        TerminalMode::Mixed,
        ColorChoice::Auto,
    )?;

    let config = Config::new(&Path::new(&opts.config))?;

    let initramfs = if opts.host {
        Initramfs::with_host_settings(&opts.kver, config)?
    } else {
        Initramfs::new(&opts.kver, config)?
    };
    let initramfs_file = File::create("initramfs.img")?;
    let mut zstd_encoder = Encoder::new(initramfs_file, 3)?;
    // zstd_encoder.multithread(1)?;
    zstd_encoder.write_all(&initramfs.into_bytes()?)?;
    zstd_encoder.finish()?;

    Ok(())
}

A src/mkinitrz/src/module.rs => src/mkinitrz/src/module.rs +29 -0
@@ 0,0 1,29 @@
use std::{
    fs::File,
    io::{BufReader, Read},
    path::{Path, PathBuf},
};

use anyhow::Result;
use xz2::bufread::XzDecoder;

#[derive(Debug)]
pub struct Module {
    pub name: String,
    pub path: PathBuf,
}

impl Module {
    pub fn new(name: String, path: &Path) -> Module {
        Module {
            name,
            path: path.into(),
        }
    }

    pub fn into_bytes(&self) -> Result<Vec<u8>> {
        let mut data = Vec::new();
        XzDecoder::new(BufReader::new(File::open(&self.path)?)).read_to_end(&mut data)?;
        Ok(data)
    }
}

A src/mkinitrz/src/modules.rs => src/mkinitrz/src/modules.rs +120 -0
@@ 0,0 1,120 @@
use std::{
    collections::HashMap,
    convert::TryFrom,
    path::{Path, PathBuf},
};

use anyhow::{Context, Result};
use dowser::Dowser;
use log::warn;
use rayon::prelude::*;

use crate::module::Module;

pub type Modules = Vec<Module>;

pub fn get_general_modules(kver: &str, additional_modules: Vec<String>) -> Result<Modules> {
    let modules: Vec<String> = get_modules_path(kver)?
        .iter_mut()
        .map(|(key, _)| key.clone())
        .collect();

    // let additional_modules: Vec<&str> = additional_modules
    //     .iter()
    //     .filter(|module| modules.contains(&module.as_str()).clone())
    //     .map(|s| s.as_str())
    //     .collect();
    // modules.extend(additional_modules);

    common_modules_gen(
        kver,
        &modules
            .iter()
            .map(|s| s.as_str())
            .collect::<Vec<&str>>()
            .as_slice(),
    )
}

pub fn get_host_modules(kver: &str, additional_modules: Vec<String>) -> Result<Modules> {
    let mut modules = Vec::new();
    let additional_modules: Vec<&str> = additional_modules
        .iter()
        .filter(|module| modules.contains(&module.as_str()).clone())
        .map(|s| s.as_str())
        .collect();
    modules.extend(additional_modules);

    common_modules_gen(kver, &modules)
}

fn common_modules_gen(kver: &str, modules: &[&str]) -> Result<Modules> {
    let modules_path = get_modules_path(kver)?;

    Ok(modules
        .par_iter()
        .map(|name| -> Result<Module> {
            let module_name = name.to_string();
            let path = modules_path
                .get(&module_name)
                .with_context(|| format!("unable to find module {}", name))?
                .clone();
            Ok(Module {
                name: module_name,
                path,
            })
        })
        .filter_map(|module| -> Option<Module> {
            if module.is_err() {
                warn!("{:?}", module);
            }
            module.ok()
        })
        .collect())
}

fn get_modules_path(kver: &str) -> Result<HashMap<String, PathBuf>> {
    Ok(Vec::<PathBuf>::try_from(
        Dowser::filtered(|p: &Path| {
            p.extension()
                .filter(|ext| ext.to_str().unwrap_or("") == "xz")
                .is_some()
                && p.file_stem()
                    .filter(|stem| {
                        Path::new(stem)
                            .extension()
                            .filter(|ext| ext.to_str().unwrap_or("") == "ko")
                            .is_some()
                    })
                    .is_some()
        })
        .with_path(Path::new("/lib/modules").join(kver).join("kernel")),
    )?
    .iter()
    .map(|path| -> Result<(String, PathBuf)> {
        Ok((
            get_module_name(
                path.as_os_str()
                    .to_str()
                    .with_context(|| "unable to convert path to string")?,
            )?,
            path.clone(),
        ))
    })
    .collect::<Result<HashMap<String, PathBuf>>>()?)
}

fn get_module_name(filename: &str) -> Result<String> {
    Ok(Path::new(filename)
        .file_stem()
        .and_then(|module| std::path::Path::new(module).file_stem())
        .with_context(|| format!("failed to get module name of file {}", filename))?
        .to_str()
        .with_context(|| {
            format!(
                "failed to convert the module name in file {} from OsStr to Str",
                filename
            )
        })?
        .to_string())
}

A src/mkinitrz/src/newc.rs => src/mkinitrz/src/newc.rs +364 -0
@@ 0,0 1,364 @@
// https://github.com/Farenjihn/elusive/blob/151b7e8080b75944327f949cbf2eab25490e5341/src/newc.rs
//! Newc cpio implementation
//!
//! This module implements the cpio newc format
//! that can be used with the Linux kernel to
//! load an initramfs.

use anyhow::Result;
use std::convert::TryInto;
use std::ffi::CString;
use std::fmt;
use std::fs::Metadata;
use std::io::Write;
use std::ops::Deref;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::MetadataExt;
use std::path::Path;

/// Magic number for newc cpio files
const MAGIC: &[u8] = b"070701";
/// Magic bytes for cpio trailer entries
const TRAILER: &str = "TRAILER!!!";

/// Offset for inode number to avoid reserved inodes (arbitrary)
const INO_OFFSET: u64 = 1337;

/// Represents a cpio archive
#[derive(PartialEq, Debug)]
pub struct Archive {
    entries: Vec<Entry>,
}

impl Archive {
    /// Create a new archive from the provided entries
    pub fn new(entries: Vec<Entry>) -> Self {
        Archive { entries }
    }

    /// Serialize this entry into cpio newc format
    pub fn into_bytes(self) -> Result<Vec<u8>> {
        let mut buf = Vec::new();

        // iterate and lazily assign new inode number
        for (index, mut entry) in self.entries.into_iter().enumerate() {
            entry.ino = INO_OFFSET + index as u64;
            entry.write(&mut buf)?;
        }

        let trailer = EntryBuilder::trailer().build();
        trailer.write(&mut buf)?;

        Ok(buf)
    }
}

/// Represent the name of a cpio entry
#[derive(PartialEq, Default)]
pub struct EntryName {
    name: Vec<u8>,
}

impl EntryName {
    /// Get a null byte terminated vector for this entry name
    pub fn into_bytes_with_nul(self) -> Result<Vec<u8>> {
        let cstr = CString::new(self.name)?;
        Ok(cstr.into_bytes_with_nul())
    }
}

impl<T> From<T> for EntryName
where
    T: AsRef<Path>,
{
    fn from(path: T) -> Self {
        let path = path.as_ref();

        let stripped = if path.has_root() {
            path.strip_prefix("/").expect("path starts with /")
        } else {
            path
        };

        EntryName {
            name: stripped.as_os_str().as_bytes().to_vec(),
        }
    }
}

impl fmt::Debug for EntryName {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("EntryName")
            .field("name", &String::from_utf8_lossy(&self.name))
            .finish()
    }
}

/// Wrapper type for data
#[derive(PartialEq)]
pub struct EntryData {
    data: Vec<u8>,
}

impl EntryData {
    fn new(data: Vec<u8>) -> Self {
        EntryData { data }
    }
}

impl Deref for EntryData {
    type Target = [u8];

    fn deref(&self) -> &Self::Target {
        &self.data
    }
}

impl fmt::Debug for EntryData {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("EntryData(<data>)")
    }
}

/// Cpio newc entry
#[derive(PartialEq, Default, Debug)]
pub struct Entry {
    /// Name of the entry (path)
    name: EntryName,
    /// Inode of the entry
    ino: u64,
    /// Mode of the entry
    mode: u32,
    /// User id of the entry
    uid: u64,
    /// Group id of the entry
    gid: u64,
    /// Number of links to the entry
    nlink: u64,
    /// Modification time of the entry
    mtime: u64,
    /// Device major number of the entry
    dev_major: u64,
    /// Device minor number of the entry
    dev_minor: u64,
    /// Rdev major number of the entry
    rdev_major: u64,
    /// Rdev minor number of the entry
    rdev_minor: u64,
    /// Data is entry is a regular file or symlink
    data: Option<EntryData>,
}

impl Entry {
    /// Create an entry with the provided name
    fn new<T>(name: T) -> Self
    where
        T: Into<EntryName>,
    {
        Entry {
            name: name.into(),
            ..Entry::default()
        }
    }

    /// Create an entry with a name and data
    fn with_data<T>(name: T, data: Vec<u8>) -> Self
    where
        T: Into<EntryName>,
    {
        Entry {
            name: name.into(),
            data: Some(EntryData::new(data)),
            ..Entry::default()
        }
    }
}

impl Entry {
    /// Serialize the entry to the passed buffer
    pub fn write(self, buf: &mut Vec<u8>) -> Result<()> {
        let file_size = match &self.data {
            Some(data) => data.len(),
            None => 0,
        };

        // serialize the header for this entry
        let filename = self.name.into_bytes_with_nul()?;

        // magic + 8 * fields + filename + file
        buf.reserve(6 + (13 * 8) + filename.len() + file_size);
        buf.write_all(MAGIC)?;
        write!(buf, "{:08x}", self.ino)?;
        write!(buf, "{:08x}", self.mode)?;
        write!(buf, "{:08x}", self.uid)?; // uid is always 0 (root)
        write!(buf, "{:08x}", self.gid)?; // gid is always 0 (root)
        write!(buf, "{:08x}", self.nlink)?;
        write!(buf, "{:08x}", self.mtime)?;
        write!(buf, "{:08x}", file_size as usize)?;
        write!(buf, "{:08x}", self.dev_major)?; // dev_major is always 0
        write!(buf, "{:08x}", self.dev_minor)?; // dev_minor is always 0
        write!(buf, "{:08x}", self.rdev_major)?;
        write!(buf, "{:08x}", self.rdev_minor)?;
        write!(buf, "{:08x}", filename.len())?;
        write!(buf, "{:08x}", 0)?; // CRC, null bytes with our MAGIC
        buf.write_all(&filename)?;
        pad_buf(buf);

        if let Some(data) = &self.data {
            buf.write_all(data)?;
            pad_buf(buf);
        }

        Ok(())
    }
}

/// Builder pattern for a cpio entry
pub struct EntryBuilder {
    /// Entry being built
    entry: Entry,
}

impl EntryBuilder {
    /// Create an entry representing a directory
    pub fn directory<T>(name: T) -> Self
    where
        T: Into<EntryName>,
    {
        EntryBuilder {
            entry: Entry::new(name),
        }
    }

    /// Create an entry representing a regular file
    pub fn file<T>(name: T, data: Vec<u8>) -> Self
    where
        T: Into<EntryName>,
    {
        EntryBuilder {
            entry: Entry::with_data(name, data),
        }
    }

    /// Create an entry representing a special file
    pub fn special_file<T>(name: T) -> Self
    where
        T: Into<EntryName>,
    {
        EntryBuilder {
            entry: Entry::new(name),
        }
    }

    /// Create an entry representing a symlink
    pub fn symlink<T>(name: T, path: &Path) -> Self
    where
        T: Into<EntryName>,
    {
        let data = path.as_os_str().as_bytes().to_vec();
        EntryBuilder {
            entry: Entry::with_data(name, data),
        }
    }

    /// Create a trailer entry
    pub fn trailer() -> Self {
        EntryBuilder {
            entry: Entry::new(TRAILER),
        }
    }

    /// Add the provided metadata to the entry
    pub fn with_metadata(self, metadata: &Metadata) -> Self {
        let rdev = metadata.rdev();

        self.mode(metadata.mode())
            .mtime(
                metadata
                    .mtime()
                    .try_into()
                    .expect("timestamp does not fit in a u64"),
            )
            .rdev_major(major(rdev))
            .rdev_minor(minor(rdev))
    }

    /// Set the mode for the entry
    pub const fn mode(mut self, mode: u32) -> Self {
        self.entry.mode = mode;
        self
    }

    /// Set the modification time for the entry
    pub const fn mtime(mut self, mtime: u64) -> Self {
        self.entry.mtime = mtime;
        self
    }

    /// Set the major rdev number for the entry
    pub const fn rdev_major(mut self, rdev_major: u64) -> Self {
        self.entry.rdev_major = rdev_major;
        self
    }

    /// Set the minor rdev number for the entry
    pub const fn rdev_minor(mut self, rdev_minor: u64) -> Self {
        self.entry.rdev_minor = rdev_minor;
        self
    }

    /// Build the entry
    pub fn build(self) -> Entry {
        self.entry
    }
}

/// Pad the buffer so entries align according to cpio requirements
pub fn pad_buf(buf: &mut Vec<u8>) {
    let rem = buf.len() % 4;

    if rem != 0 {
        buf.resize(buf.len() + (4 - rem), 0);
    }
}

/// Shamelessly taken from the `nix` crate, thanks !
pub const fn major(dev: u64) -> u64 {
    ((dev >> 32) & 0xffff_f000) | ((dev >> 8) & 0x0000_0fff)
}

/// Shamelessly taken from the `nix` crate, thanks !
pub const fn minor(dev: u64) -> u64 {
    ((dev >> 12) & 0xffff_ff00) | ((dev) & 0x0000_00ff)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_builder() -> Result<()> {
        let entry = EntryBuilder::file("/testfile", b"datadatadata".to_vec()).build();

        let mut buf = Vec::new();
        entry.write(&mut buf)?;

        assert!(buf.len() > 0);

        Ok(())
    }

    #[test]
    fn test_serialize() -> Result<()> {
        let empty = Archive::new(Vec::new());
        let trailer = EntryBuilder::trailer().build();

        let mut buf = Vec::new();
        trailer.write(&mut buf)?;

        // an empty archive is just a trailer entry
        assert_eq!(empty.into_bytes()?, buf);

        Ok(())
    }
}