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"),
}
}
}