use anyhow::{Result, anyhow};
use std::borrow::Cow;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use tar::{Builder, EntryType, Header};
#[derive(Clone, Debug)]
pub struct CanonicalTarHeader {
pub header: Header,
pub pax_extensions: Vec<(String, Vec<u8>)>,
}
impl CanonicalTarHeader {
pub fn from_entry<R: Read>(entry: &mut tar::Entry<'_, R>) -> Result<Self> {
let header = entry.header().clone();
let pax_extensions = match entry.pax_extensions() {
Err(e) => return Err(anyhow!("failed to read PAX extensions: {e}")),
Ok(None) => vec![],
Ok(Some(exts)) => {
let mut pairs = Vec::new();
for ext in exts {
let ext = ext.map_err(|e| anyhow!("invalid PAX extension: {e}"))?;
let key = ext
.key()
.map_err(|e| anyhow!("invalid PAX key: {e}"))?
.to_string();
let val = ext.value_bytes().to_vec();
pairs.push((key, val));
}
pairs
}
};
Ok(Self {
header,
pax_extensions,
})
}
pub fn path(&self) -> Result<Cow<'_, Path>> {
if let Some((_, v)) = self.pax_extensions.iter().find(|(k, _)| k == "path") {
let s = std::str::from_utf8(v)
.map_err(|e| anyhow!("PAX 'path' extension is not valid UTF-8: {e}"))?;
return Ok(Cow::Owned(PathBuf::from(s)));
}
self.header
.path()
.map_err(|e| anyhow!("reading path from header: {e}"))
}
pub fn entry_type(&self) -> EntryType {
self.header.entry_type()
}
pub fn link_name(&self) -> Result<Option<PathBuf>> {
if let Some((_, v)) = self.pax_extensions.iter().find(|(k, _)| k == "linkpath") {
let s = std::str::from_utf8(v)
.map_err(|e| anyhow!("PAX 'linkpath' extension is not valid UTF-8: {e}"))?;
return Ok(Some(PathBuf::from(s)));
}
Ok(self
.header
.link_name()
.map_err(|e| anyhow!("reading link_name from header: {e}"))?
.map(|p| p.into_owned()))
}
pub fn clone_as_regular(&self) -> Self {
let pax_extensions = self
.pax_extensions
.iter()
.filter(|(k, _)| k != "path" && k != "linkpath" && !k.starts_with("GNU.sparse."))
.cloned()
.collect();
let mut header = self.header.clone();
header.set_entry_type(EntryType::Regular);
Self {
header,
pax_extensions,
}
}
pub fn write_hardlink_to_tar<W: Write>(
&self,
link_path: &Path,
target_path: &Path,
builder: &mut Builder<W>,
) -> Result<()> {
let link_path_str = link_path.to_string_lossy();
let target_str = target_path.to_string_lossy();
let mut pax: Vec<(&str, Vec<u8>)> = Vec::new();
if link_path_str.len() > 100 {
pax.push(("path", link_path_str.as_bytes().to_vec()));
}
if target_str.len() > 100 {
pax.push(("linkpath", target_str.as_bytes().to_vec()));
}
if !pax.is_empty() {
builder
.append_pax_extensions(pax.iter().map(|(k, v)| (*k, v.as_slice())))
.map_err(|e| anyhow!("failed to append PAX extensions for hardlink: {e}"))?;
}
let mut header = self.header.clone();
header.set_entry_type(EntryType::Link);
header.set_size(0);
{
let raw = header.as_mut_bytes();
let field = &mut raw[0..100];
field.fill(0);
let bytes = link_path_str.as_bytes();
let len = bytes.len().min(99);
field[..len].copy_from_slice(&bytes[..len]);
}
{
let raw = header.as_mut_bytes();
let field = &mut raw[157..257];
field.fill(0);
let bytes = target_str.as_bytes();
let len = bytes.len().min(100);
field[..len].copy_from_slice(&bytes[..len]);
}
header.set_cksum();
builder
.append(&header, &[] as &[u8])
.map_err(|e| anyhow!("failed to append hardlink entry: {e}"))?;
Ok(())
}
pub fn write_to_tar<W: Write, R: Read>(
&self,
path: &Path,
data: R,
builder: &mut Builder<W>,
) -> Result<()> {
if !self.pax_extensions.is_empty() {
builder
.append_pax_extensions(
self.pax_extensions
.iter()
.map(|(k, v)| (k.as_str(), v.as_slice())),
)
.map_err(|e| anyhow!("failed to append PAX extensions: {e}"))?;
}
let mut header = self.header.clone();
builder
.append_data(&mut header, path, data)
.map_err(|e| anyhow!("failed to append entry: {e}"))?;
Ok(())
}
}