genpac 0.1.0

Sandbox for Gentoo ebuild development using bubblewrap
// Copyright (C) 2023 Gokul Das B
// SPDX-License-Identifier: GPL-3.0-or-later
//! Header analysis
//!
//! This module deals with identifying the type of tar header and associated metadata. It is also
//! able to extract and choose from PAX key-value extensions for stage3.

use anyhow::Result as AResult;
use std::collections::HashMap;
use std::fmt;
use std::io;
use std::path::{Path, PathBuf};
use tar::{Entry, Header};

pub(super) enum EntityType {
    Directory(PathBuf),
    NonEmptyFile(PathBuf),
    EmptyFile(PathBuf),
    FileLink { path: PathBuf, target: PathBuf },
    DirectoryLink { path: PathBuf, target: PathBuf },
}

#[derive(Default)]
pub(super) struct ExtraMeta {
    pub(super) mtime: u64,
    pub(super) atime: Option<u64>,
    pub(super) ctime: Option<u64>,
}

pub(super) struct ArchiveEntity {
    pub(super) entity: EntityType,
    pub(super) meta: ExtraMeta,
    pub(super) header: Header,
}

const KNOWNKEYS: [&str; 5] = ["path", "linkpath", "mtime", "atime", "ctime"];

impl ArchiveEntity {
    pub(super) fn identify(entry: &mut Entry<impl io::Read>) -> AResult<Self> {
        let mut meta = ExtraMeta::default();

        let mut paxextns = HashMap::new();
        if let Some(paxes) = entry.pax_extensions()? {
            for paxe in paxes {
                let paxe = paxe?;
                let key = paxe.key()?;
                let val = paxe.value()?;
                paxextns.insert(key, val);
                anyhow::ensure!(KNOWNKEYS.contains(&key), "Unforeseen PAX key '{key}'");
            }
        }

        let pathpax = paxextns
            .get("path")
            .map(|x| PathBuf::from(*x))
            .unwrap_or_default();
        let linkpax = paxextns
            .get("linkpath")
            .map(|x| PathBuf::from(*x))
            .unwrap_or_default();
        let mtimepax = paxextns
            .get("mtime")
            .map(|x| (*x).parse::<f64>().unwrap() as u64);
        meta.atime = paxextns
            .get("atime")
            .map(|x| (*x).parse::<f64>().unwrap() as u64);
        meta.ctime = paxextns
            .get("ctime")
            .map(|x| (*x).parse::<f64>().unwrap() as u64);

        let sizebuf = entry.size();

        let header = entry.header().clone();

        let sizehdr = header.size()?;
        let mtimehdr = header.mtime()?;
        let pathhdr = header.path()?;
        let linkhdr = header.link_name()?.unwrap_or_default();

        {
            let mtimepax = mtimepax.unwrap_or_default();
            anyhow::ensure!(
                mtimepax == 0 || mtimepax == mtimehdr,
                "Mtime mismatch. Header: {mtimehdr}, PAX: {mtimepax}"
            );
        }
        meta.mtime = mtimepax.unwrap_or(mtimehdr);

        if sizehdr != sizebuf {
            anyhow::bail!("Size mismatch. Header: {sizehdr}, Buffer {sizebuf}");
        }
        let empty = sizehdr == 0;

        let path = {
            let pax = pathpax.to_str().unwrap();
            let hdr = pathhdr.to_str().unwrap();
            match (pax.is_empty(), hdr.is_empty()) {
                (true, true) => anyhow::bail!("Path missing entirely"),
                (true, false) => PathBuf::from(pathhdr),
                (false, true) => pathpax,
                (false, false) => {
                    if pax.starts_with(hdr) {
                        pathpax
                    } else {
                        anyhow::bail!("Path mismatch. Header: {hdr}, PAX: {pax}");
                    }
                }
            }
        };

        let link = {
            let pax = linkpax.to_str().unwrap();
            let hdr = linkhdr.to_str().unwrap();
            match (pax.is_empty(), hdr.is_empty()) {
                (true, true) => None,
                (true, false) => Some(PathBuf::from(linkhdr)),
                (false, true) => Some(linkpax),
                (false, false) => {
                    anyhow::ensure!(
                        pax.starts_with(hdr),
                        "Link mismatch. Header: {hdr}, PAX: {pax}"
                    );
                    Some(linkpax)
                }
            }
        };

        anyhow::ensure!(link.is_none() || empty, "Non-empty link");

        let direc = {
            let path = path.to_str().unwrap();
            let link = match link {
                Some(ref x) => x.to_str().unwrap(),
                None => path,
            };
            let pathdir = path.ends_with('/');
            let linkdir = link.ends_with('/');
            anyhow::ensure!(
                !(pathdir ^ linkdir),
                "Directory link mismatch. Path: {path}, Link: {link}"
            );
            pathdir
        };

        anyhow::ensure!(!direc || empty, "Non-zero sized directory");

        let entity = {
            use EntityType::*;
            match (direc, empty, path, link) {
                (true, true, path, None) => Directory(path),
                (false, false, path, None) => NonEmptyFile(path),
                (false, true, path, None) => EmptyFile(path),
                (false, true, path, Some(target)) => FileLink { path, target },
                (true, true, path, Some(target)) => DirectoryLink { path, target },
                _ => unreachable!(),
            }
        };

        Ok(Self {
            entity,
            meta,
            header,
        })
    }

    pub(super) fn path(&self) -> &Path {
        use EntityType::*;
        match &self.entity {
            Directory(p) | NonEmptyFile(p) | EmptyFile(p) => p,
            FileLink { path: p, .. } | DirectoryLink { path: p, .. } => p,
        }
    }

    pub(super) fn target(&self) -> Option<&Path> {
        use EntityType::*;
        match &self.entity {
            Directory(_) | NonEmptyFile(_) | EmptyFile(_) => None,
            FileLink { target: t, .. } | DirectoryLink { target: t, .. } => Some(t),
        }
    }

    pub(super) fn is_link(&self) -> bool {
        use EntityType::*;
        match &self.entity {
            FileLink { .. } | DirectoryLink { .. } => true,
            Directory(_) | NonEmptyFile(_) | EmptyFile(_) => false,
        }
    }
}

impl fmt::Display for ArchiveEntity {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        use EntityType::*;
        match &self.entity {
            Directory(_) => write!(f, "Directory"),
            NonEmptyFile(_) => write!(f, "Non-empty file"),
            EmptyFile(_) => write!(f, "Empty file"),
            FileLink { .. } => write!(f, "File link"),
            DirectoryLink { .. } => write!(f, "Directory link"),
        }
    }
}