#![forbid(unsafe_code)]
#![deny(missing_docs)]
#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
pub mod builder;
pub mod parse;
pub use builder::{
blocks_for_size, EntryBuilder, ExtensionMode, HeaderBuilder, PaxBuilder, LINKNAME_MAX_LEN,
NAME_MAX_LEN,
};
use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
use core::fmt;
use thiserror::Error;
use zerocopy::{FromBytes, FromZeros, Immutable, IntoBytes, KnownLayout};
pub const HEADER_SIZE: usize = 512;
pub const USTAR_MAGIC: &[u8; 6] = b"ustar\0";
pub const USTAR_VERSION: &[u8; 2] = b"00";
pub const GNU_MAGIC: &[u8; 6] = b"ustar ";
pub const GNU_VERSION: &[u8; 2] = b" \0";
#[derive(Debug, Error)]
pub enum HeaderError {
#[error("insufficient data: expected {HEADER_SIZE} bytes, got {0}")]
InsufficientData(usize),
#[error("invalid octal field: {0:?}")]
InvalidOctal(Vec<u8>),
#[error("value overflows {field_len}-byte field: {detail}")]
FieldOverflow {
field_len: usize,
detail: String,
},
#[error("checksum mismatch: expected {expected}, computed {computed}")]
ChecksumMismatch {
expected: u64,
computed: u64,
},
}
pub type Result<T> = core::result::Result<T, HeaderError>;
#[derive(Clone, Copy, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C)]
pub struct OldHeader {
pub name: [u8; 100],
pub mode: [u8; 8],
pub uid: [u8; 8],
pub gid: [u8; 8],
pub size: [u8; 12],
pub mtime: [u8; 12],
pub cksum: [u8; 8],
pub linkflag: [u8; 1],
pub linkname: [u8; 100],
pub pad: [u8; 255],
}
impl Default for OldHeader {
fn default() -> Self {
Self::new_zeroed()
}
}
impl fmt::Debug for OldHeader {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("OldHeader")
.field("name", &String::from_utf8_lossy(truncate_null(&self.name)))
.field("mode", &String::from_utf8_lossy(truncate_null(&self.mode)))
.field("linkflag", &self.linkflag[0])
.finish_non_exhaustive()
}
}
#[derive(Clone, Copy, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C)]
pub struct UstarHeader {
pub name: [u8; 100],
pub mode: [u8; 8],
pub uid: [u8; 8],
pub gid: [u8; 8],
pub size: [u8; 12],
pub mtime: [u8; 12],
pub cksum: [u8; 8],
pub typeflag: [u8; 1],
pub linkname: [u8; 100],
pub magic: [u8; 6],
pub version: [u8; 2],
pub uname: [u8; 32],
pub gname: [u8; 32],
pub dev_major: [u8; 8],
pub dev_minor: [u8; 8],
pub prefix: [u8; 155],
pub pad: [u8; 12],
}
impl Default for UstarHeader {
fn default() -> Self {
let mut header = Self::new_zeroed();
header.magic.copy_from_slice(USTAR_MAGIC);
header.version.copy_from_slice(USTAR_VERSION);
header
}
}
impl fmt::Debug for UstarHeader {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("UstarHeader")
.field("name", &String::from_utf8_lossy(truncate_null(&self.name)))
.field("mode", &String::from_utf8_lossy(truncate_null(&self.mode)))
.field("typeflag", &self.typeflag[0])
.field("magic", &self.magic)
.field(
"uname",
&String::from_utf8_lossy(truncate_null(&self.uname)),
)
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SparseEntry {
pub offset: u64,
pub length: u64,
}
#[derive(Clone, Copy, Default, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C)]
pub struct GnuSparseHeader {
pub offset: [u8; 12],
pub numbytes: [u8; 12],
}
impl GnuSparseHeader {
#[must_use]
pub fn is_empty(&self) -> bool {
self.offset[0] == 0 || self.numbytes[0] == 0
}
pub fn to_sparse_entry(&self) -> Result<SparseEntry> {
Ok(SparseEntry {
offset: parse_numeric(&self.offset)?,
length: parse_numeric(&self.numbytes)?,
})
}
pub fn set(&mut self, entry: &SparseEntry) {
encode_numeric(&mut self.offset, entry.offset)
.expect("u64 always fits in 12-byte numeric field");
encode_numeric(&mut self.numbytes, entry.length)
.expect("u64 always fits in 12-byte numeric field");
}
pub fn offset(&self) -> Result<u64> {
parse_numeric(&self.offset)
}
pub fn set_offset(&mut self, offset: u64) {
encode_numeric(&mut self.offset, offset).expect("u64 always fits in 12-byte numeric field");
}
pub fn length(&self) -> Result<u64> {
parse_numeric(&self.numbytes)
}
pub fn set_length(&mut self, length: u64) {
encode_numeric(&mut self.numbytes, length)
.expect("u64 always fits in 12-byte numeric field");
}
}
impl fmt::Debug for GnuSparseHeader {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("GnuSparseHeader")
.field("offset", &parse_octal(&self.offset).ok())
.field("numbytes", &parse_octal(&self.numbytes).ok())
.finish()
}
}
#[derive(Clone, Copy, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C)]
pub struct GnuHeader {
pub name: [u8; 100],
pub mode: [u8; 8],
pub uid: [u8; 8],
pub gid: [u8; 8],
pub size: [u8; 12],
pub mtime: [u8; 12],
pub cksum: [u8; 8],
pub typeflag: [u8; 1],
pub linkname: [u8; 100],
pub magic: [u8; 6],
pub version: [u8; 2],
pub uname: [u8; 32],
pub gname: [u8; 32],
pub dev_major: [u8; 8],
pub dev_minor: [u8; 8],
pub atime: [u8; 12],
pub ctime: [u8; 12],
pub offset: [u8; 12],
pub longnames: [u8; 4],
pub unused: [u8; 1],
pub sparse: [GnuSparseHeader; 4],
pub isextended: [u8; 1],
pub realsize: [u8; 12],
pub pad: [u8; 17],
}
impl Default for GnuHeader {
fn default() -> Self {
let mut header = Self::new_zeroed();
header.magic.copy_from_slice(GNU_MAGIC);
header.version.copy_from_slice(GNU_VERSION);
header
}
}
impl GnuHeader {
pub fn atime(&self) -> Result<u64> {
parse_numeric(&self.atime)
}
pub fn set_atime(&mut self, atime: u64) {
encode_numeric(&mut self.atime, atime).expect("u64 always fits in 12-byte numeric field");
}
pub fn ctime(&self) -> Result<u64> {
parse_numeric(&self.ctime)
}
pub fn set_ctime(&mut self, ctime: u64) {
encode_numeric(&mut self.ctime, ctime).expect("u64 always fits in 12-byte numeric field");
}
pub fn real_size(&self) -> Result<u64> {
parse_numeric(&self.realsize)
}
pub fn set_real_size(&mut self, size: u64) {
encode_numeric(&mut self.realsize, size).expect("u64 always fits in 12-byte numeric field");
}
#[must_use]
pub fn is_extended(&self) -> bool {
self.isextended[0] == 1
}
pub fn set_is_extended(&mut self, extended: bool) {
self.isextended[0] = if extended { 1 } else { 0 };
}
}
impl fmt::Debug for GnuHeader {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("GnuHeader")
.field("name", &String::from_utf8_lossy(truncate_null(&self.name)))
.field("mode", &String::from_utf8_lossy(truncate_null(&self.mode)))
.field("typeflag", &self.typeflag[0])
.field("magic", &self.magic)
.field("isextended", &self.isextended[0])
.finish_non_exhaustive()
}
}
#[derive(Clone, Copy, Default, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(C)]
pub struct GnuExtSparseHeader {
pub sparse: [GnuSparseHeader; 21],
pub isextended: [u8; 1],
pub pad: [u8; 7],
}
impl GnuExtSparseHeader {
#[must_use]
pub fn is_extended(&self) -> bool {
self.isextended[0] == 1
}
pub fn set_is_extended(&mut self, extended: bool) {
self.isextended[0] = if extended { 1 } else { 0 };
}
}
impl fmt::Debug for GnuExtSparseHeader {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("GnuExtSparseHeader")
.field("isextended", &self.isextended[0])
.finish_non_exhaustive()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum EntryType {
Regular,
Link,
Symlink,
Char,
Block,
Directory,
Fifo,
Continuous,
GnuLongName,
GnuLongLink,
GnuSparse,
XHeader,
XGlobalHeader,
Other(u8),
}
impl EntryType {
#[inline]
#[must_use]
pub fn new(byte: u8) -> Self {
Self::from_byte(byte)
}
#[must_use]
pub fn from_byte(byte: u8) -> Self {
match byte {
b'0' | b'\0' => EntryType::Regular,
b'1' => EntryType::Link,
b'2' => EntryType::Symlink,
b'3' => EntryType::Char,
b'4' => EntryType::Block,
b'5' => EntryType::Directory,
b'6' => EntryType::Fifo,
b'7' => EntryType::Continuous,
b'L' => EntryType::GnuLongName,
b'K' => EntryType::GnuLongLink,
b'S' => EntryType::GnuSparse,
b'x' => EntryType::XHeader,
b'g' => EntryType::XGlobalHeader,
other => EntryType::Other(other),
}
}
#[must_use]
pub fn file() -> Self {
Self::Regular
}
#[must_use]
pub fn hard_link() -> Self {
Self::Link
}
#[must_use]
pub fn symlink() -> Self {
Self::Symlink
}
#[must_use]
pub fn character_special() -> Self {
Self::Char
}
#[must_use]
pub fn block_special() -> Self {
Self::Block
}
#[must_use]
pub fn dir() -> Self {
Self::Directory
}
#[must_use]
pub fn fifo() -> Self {
Self::Fifo
}
#[must_use]
pub fn contiguous() -> Self {
Self::Continuous
}
#[inline]
#[must_use]
pub fn as_byte(self) -> u8 {
self.to_byte()
}
#[must_use]
pub fn to_byte(self) -> u8 {
match self {
EntryType::Regular => b'0',
EntryType::Link => b'1',
EntryType::Symlink => b'2',
EntryType::Char => b'3',
EntryType::Block => b'4',
EntryType::Directory => b'5',
EntryType::Fifo => b'6',
EntryType::Continuous => b'7',
EntryType::GnuLongName => b'L',
EntryType::GnuLongLink => b'K',
EntryType::GnuSparse => b'S',
EntryType::XHeader => b'x',
EntryType::XGlobalHeader => b'g',
EntryType::Other(b) => b,
}
}
#[must_use]
pub fn is_file(self) -> bool {
matches!(self, EntryType::Regular | EntryType::Continuous)
}
#[must_use]
pub fn is_dir(self) -> bool {
self == EntryType::Directory
}
#[must_use]
pub fn is_symlink(self) -> bool {
self == EntryType::Symlink
}
#[must_use]
pub fn is_hard_link(self) -> bool {
self == EntryType::Link
}
#[must_use]
pub fn is_character_special(self) -> bool {
self == EntryType::Char
}
#[must_use]
pub fn is_block_special(self) -> bool {
self == EntryType::Block
}
#[must_use]
pub fn is_fifo(self) -> bool {
self == EntryType::Fifo
}
#[must_use]
pub fn is_contiguous(self) -> bool {
self == EntryType::Continuous
}
#[must_use]
pub fn is_gnu_longname(self) -> bool {
self == EntryType::GnuLongName
}
#[must_use]
pub fn is_gnu_longlink(self) -> bool {
self == EntryType::GnuLongLink
}
#[must_use]
pub fn is_gnu_sparse(self) -> bool {
self == EntryType::GnuSparse
}
#[must_use]
pub fn is_pax_global_extensions(self) -> bool {
self == EntryType::XGlobalHeader
}
#[must_use]
pub fn is_pax_local_extensions(self) -> bool {
self == EntryType::XHeader
}
}
impl From<u8> for EntryType {
fn from(byte: u8) -> Self {
Self::from_byte(byte)
}
}
impl From<EntryType> for u8 {
fn from(entry_type: EntryType) -> Self {
entry_type.to_byte()
}
}
#[derive(Clone, Copy, FromBytes, IntoBytes, Immutable, KnownLayout)]
#[repr(transparent)]
pub struct Header {
bytes: [u8; HEADER_SIZE],
}
impl Header {
#[must_use]
pub fn new_ustar() -> Self {
let mut header = Self {
bytes: [0u8; HEADER_SIZE],
};
let ustar = header.as_ustar_mut();
ustar.magic.copy_from_slice(USTAR_MAGIC);
ustar.version.copy_from_slice(USTAR_VERSION);
header
}
#[must_use]
pub fn new_gnu() -> Self {
let mut header = Self {
bytes: [0u8; HEADER_SIZE],
};
let gnu = header.as_gnu_mut();
gnu.magic.copy_from_slice(GNU_MAGIC);
gnu.version.copy_from_slice(GNU_VERSION);
header
}
#[must_use]
pub fn new_old() -> Self {
Self {
bytes: [0u8; HEADER_SIZE],
}
}
#[must_use]
pub fn as_bytes(&self) -> &[u8; HEADER_SIZE] {
&self.bytes
}
pub fn as_mut_bytes(&mut self) -> &mut [u8; HEADER_SIZE] {
&mut self.bytes
}
#[must_use]
pub fn from_bytes(bytes: &[u8; HEADER_SIZE]) -> &Header {
Header::ref_from_bytes(bytes).expect("HEADER_SIZE is correct")
}
#[must_use]
pub fn as_old(&self) -> &OldHeader {
OldHeader::ref_from_bytes(&self.bytes).expect("size is correct")
}
#[must_use]
pub fn as_ustar(&self) -> &UstarHeader {
UstarHeader::ref_from_bytes(&self.bytes).expect("size is correct")
}
#[must_use]
pub fn as_gnu(&self) -> &GnuHeader {
GnuHeader::ref_from_bytes(&self.bytes).expect("size is correct")
}
#[must_use]
pub fn try_as_ustar(&self) -> Option<&UstarHeader> {
if self.is_ustar() {
Some(self.as_ustar())
} else {
None
}
}
#[must_use]
pub fn try_as_gnu(&self) -> Option<&GnuHeader> {
if self.is_gnu() {
Some(self.as_gnu())
} else {
None
}
}
#[must_use]
pub fn as_old_mut(&mut self) -> &mut OldHeader {
OldHeader::mut_from_bytes(&mut self.bytes).expect("size is correct")
}
#[must_use]
pub fn as_ustar_mut(&mut self) -> &mut UstarHeader {
UstarHeader::mut_from_bytes(&mut self.bytes).expect("size is correct")
}
#[must_use]
pub fn as_gnu_mut(&mut self) -> &mut GnuHeader {
GnuHeader::mut_from_bytes(&mut self.bytes).expect("size is correct")
}
#[must_use]
pub fn try_as_ustar_mut(&mut self) -> Option<&mut UstarHeader> {
if self.is_ustar() {
Some(self.as_ustar_mut())
} else {
None
}
}
#[must_use]
pub fn try_as_gnu_mut(&mut self) -> Option<&mut GnuHeader> {
if self.is_gnu() {
Some(self.as_gnu_mut())
} else {
None
}
}
#[must_use]
pub fn is_ustar(&self) -> bool {
let h = self.as_ustar();
h.magic == *USTAR_MAGIC && h.version == *USTAR_VERSION
}
#[must_use]
pub fn is_gnu(&self) -> bool {
let h = self.as_gnu();
h.magic == *GNU_MAGIC && h.version == *GNU_VERSION
}
#[must_use]
pub fn entry_type(&self) -> EntryType {
EntryType::from_byte(self.as_ustar().typeflag[0])
}
pub fn entry_size(&self) -> Result<u64> {
parse_numeric(&self.as_ustar().size)
}
pub fn mode(&self) -> Result<u32> {
parse_numeric(&self.as_ustar().mode).map(|v| v as u32)
}
pub fn uid(&self) -> Result<u64> {
parse_numeric(&self.as_ustar().uid)
}
pub fn gid(&self) -> Result<u64> {
parse_numeric(&self.as_ustar().gid)
}
pub fn mtime(&self) -> Result<u64> {
parse_numeric(&self.as_ustar().mtime)
}
#[must_use]
pub fn path_bytes(&self) -> &[u8] {
truncate_null(&self.as_ustar().name)
}
#[must_use]
pub fn link_name_bytes(&self) -> &[u8] {
truncate_null(&self.as_ustar().linkname)
}
pub fn device_major(&self) -> Result<Option<u32>> {
if !self.is_ustar() && !self.is_gnu() {
return Ok(None);
}
parse_octal(&self.as_ustar().dev_major).map(|v| Some(v as u32))
}
pub fn device_minor(&self) -> Result<Option<u32>> {
if !self.is_ustar() && !self.is_gnu() {
return Ok(None);
}
parse_octal(&self.as_ustar().dev_minor).map(|v| Some(v as u32))
}
#[must_use]
pub fn username(&self) -> Option<&[u8]> {
if !self.is_ustar() && !self.is_gnu() {
return None;
}
Some(truncate_null(&self.as_ustar().uname))
}
#[must_use]
pub fn groupname(&self) -> Option<&[u8]> {
if !self.is_ustar() && !self.is_gnu() {
return None;
}
Some(truncate_null(&self.as_ustar().gname))
}
#[must_use]
pub fn prefix(&self) -> Option<&[u8]> {
if !self.is_ustar() {
return None;
}
Some(truncate_null(&self.as_ustar().prefix))
}
pub fn verify_checksum(&self) -> Result<()> {
let expected = parse_octal(&self.as_ustar().cksum)?;
let computed = self.compute_checksum();
if expected == computed {
Ok(())
} else {
Err(HeaderError::ChecksumMismatch { expected, computed })
}
}
#[must_use]
pub fn compute_checksum(&self) -> u64 {
let mut sum: u64 = 0;
for (i, &byte) in self.bytes.iter().enumerate() {
if (148..156).contains(&i) {
sum += u64::from(b' ');
} else {
sum += u64::from(byte);
}
}
sum
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.bytes.iter().all(|&b| b == 0)
}
fn set_numeric_field<const N: usize>(
&mut self,
field: impl FnOnce(&mut UstarHeader) -> &mut [u8; N],
value: u64,
) -> Result<()> {
let is_gnu = self.is_gnu();
let dst = field(self.as_ustar_mut());
if is_gnu {
encode_numeric(dst, value)
} else {
encode_octal(dst, value)
}
}
pub fn set_size(&mut self, size: u64) -> Result<()> {
self.set_numeric_field(|h| &mut h.size, size)
}
pub fn set_size_small(&mut self, size: u32) {
encode_octal(&mut self.as_ustar_mut().size, u64::from(size))
.expect("u32 always fits in 12-byte octal field");
}
pub fn set_mode(&mut self, mode: u32) -> Result<()> {
encode_octal(&mut self.as_ustar_mut().mode, u64::from(mode))
}
pub fn set_mode_small(&mut self, mode: u16) {
encode_octal(&mut self.as_ustar_mut().mode, u64::from(mode))
.expect("u16 always fits in 8-byte octal field");
}
pub fn set_uid(&mut self, uid: u64) -> Result<()> {
self.set_numeric_field(|h| &mut h.uid, uid)
}
pub fn set_gid(&mut self, gid: u64) -> Result<()> {
self.set_numeric_field(|h| &mut h.gid, gid)
}
pub fn set_mtime(&mut self, mtime: u64) -> Result<()> {
self.set_numeric_field(|h| &mut h.mtime, mtime)
}
pub fn set_mtime_small(&mut self, mtime: u32) {
encode_octal(&mut self.as_ustar_mut().mtime, u64::from(mtime))
.expect("u32 always fits in 12-byte octal field");
}
pub fn set_entry_type(&mut self, ty: EntryType) {
self.as_ustar_mut().typeflag[0] = ty.to_byte();
}
pub fn set_checksum(&mut self) {
self.as_ustar_mut().cksum.fill(b' ');
let checksum: u64 = self.bytes.iter().map(|&b| u64::from(b)).sum();
encode_octal(&mut self.as_ustar_mut().cksum, checksum)
.expect("checksum always fits in 8-byte octal field");
}
pub fn set_path(&mut self, path: &[u8]) -> Result<()> {
if path.len() > self.as_ustar().name.len() {
return Err(HeaderError::FieldOverflow {
field_len: self.as_ustar().name.len(),
detail: format!("{}-byte path", path.len()),
});
}
let name = &mut self.as_ustar_mut().name;
name.fill(0);
name[..path.len()].copy_from_slice(path);
Ok(())
}
pub fn set_link_name(&mut self, link: &[u8]) -> Result<()> {
if link.len() > self.as_ustar().linkname.len() {
return Err(HeaderError::FieldOverflow {
field_len: self.as_ustar().linkname.len(),
detail: format!("{}-byte link name", link.len()),
});
}
let linkname = &mut self.as_ustar_mut().linkname;
linkname.fill(0);
linkname[..link.len()].copy_from_slice(link);
Ok(())
}
pub fn set_username(&mut self, name: &[u8]) -> Result<()> {
if name.len() > self.as_ustar().uname.len() {
return Err(HeaderError::FieldOverflow {
field_len: self.as_ustar().uname.len(),
detail: format!("{}-byte username", name.len()),
});
}
let uname = &mut self.as_ustar_mut().uname;
uname.fill(0);
uname[..name.len()].copy_from_slice(name);
Ok(())
}
pub fn set_groupname(&mut self, name: &[u8]) -> Result<()> {
if name.len() > self.as_ustar().gname.len() {
return Err(HeaderError::FieldOverflow {
field_len: self.as_ustar().gname.len(),
detail: format!("{}-byte group name", name.len()),
});
}
let gname = &mut self.as_ustar_mut().gname;
gname.fill(0);
gname[..name.len()].copy_from_slice(name);
Ok(())
}
pub fn set_device(&mut self, major: u32, minor: u32) -> Result<()> {
let fields = self.as_ustar_mut();
encode_octal(&mut fields.dev_major, u64::from(major))?;
encode_octal(&mut fields.dev_minor, u64::from(minor))
}
pub fn set_device_small(&mut self, major: u16, minor: u16) {
let fields = self.as_ustar_mut();
encode_octal(&mut fields.dev_major, u64::from(major))
.expect("u16 always fits in 8-byte octal field");
encode_octal(&mut fields.dev_minor, u64::from(minor))
.expect("u16 always fits in 8-byte octal field");
}
}
impl Default for Header {
fn default() -> Self {
Self::new_ustar()
}
}
impl fmt::Debug for Header {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Header")
.field("path", &String::from_utf8_lossy(self.path_bytes()))
.field("entry_type", &self.entry_type())
.field("size", &self.entry_size().ok())
.field("mode", &self.mode().ok().map(|m| format!("{m:04o}")))
.field("is_ustar", &self.is_ustar())
.field("is_gnu", &self.is_gnu())
.finish()
}
}
pub(crate) struct OctU64 {
buf: [u8; 22],
start: u8,
}
impl OctU64 {
pub(crate) fn new(mut value: u64) -> Self {
let mut buf = [0u8; 22];
if value == 0 {
buf[21] = b'0';
return Self { buf, start: 21 };
}
let mut pos = 22u8;
while value > 0 {
pos -= 1;
buf[pos as usize] = b'0' + (value & 7) as u8;
value >>= 3;
}
Self { buf, start: pos }
}
pub(crate) fn as_bytes(&self) -> &[u8] {
&self.buf[self.start as usize..]
}
}
fn is_tar_whitespace(b: u8) -> bool {
b.is_ascii_whitespace() || b == 0x0b
}
pub(crate) fn parse_octal(bytes: &[u8]) -> Result<u64> {
let truncated = match bytes.iter().position(|&b| b == 0) {
Some(i) => &bytes[..i],
None => bytes,
};
let trimmed = truncated
.iter()
.position(|&b| !is_tar_whitespace(b))
.map(|start| {
let rest = &truncated[start..];
let end = rest
.iter()
.rposition(|&b| !is_tar_whitespace(b))
.map_or(0, |p| p + 1);
&rest[..end]
})
.unwrap_or(&[]);
if trimmed.is_empty() {
return Ok(0);
}
let s = core::str::from_utf8(trimmed).map_err(|_| HeaderError::InvalidOctal(bytes.to_vec()))?;
u64::from_str_radix(s, 8).map_err(|_| HeaderError::InvalidOctal(bytes.to_vec()))
}
pub(crate) fn encode_numeric<const N: usize>(field: &mut [u8; N], value: u64) -> Result<()> {
const { assert!(N > 0, "encode_numeric requires N > 0") };
let use_binary = if N == 8 {
value >= 2097152 } else {
value >= 8589934592 };
if use_binary {
let data_bits = N * 8 - 1;
if data_bits < 64 && value >= (1u64 << data_bits) {
return Err(HeaderError::FieldOverflow {
field_len: N,
detail: format!("numeric value {value}"),
});
}
field.fill(0);
let value_bytes = value.to_be_bytes();
if N >= 8 {
field[N - 8..].copy_from_slice(&value_bytes);
} else {
field.copy_from_slice(&value_bytes[8 - N..]);
}
field[0] |= 0x80;
} else {
encode_octal(field, value)?;
}
Ok(())
}
pub(crate) fn encode_octal<const N: usize>(field: &mut [u8; N], value: u64) -> Result<()> {
const { assert!(N > 0, "encode_octal requires N > 0") };
let oct = OctU64::new(value);
let digits = oct.as_bytes();
if digits.len() > N - 1 {
return Err(HeaderError::FieldOverflow {
field_len: N,
detail: format!("octal value {value:#o}"),
});
}
field.fill(0);
let (digit_slots, _nul) = field.split_at_mut(N - 1);
let pad = digit_slots.len() - digits.len();
digit_slots[..pad].fill(b'0');
digit_slots[pad..].copy_from_slice(digits);
Ok(())
}
pub(crate) fn parse_numeric(bytes: &[u8]) -> Result<u64> {
if bytes.is_empty() {
return Ok(0);
}
if bytes[0] & 0x80 != 0 {
let mut value: u64 = 0;
for (i, &byte) in bytes.iter().enumerate() {
let b = if i == 0 { byte & 0x7f } else { byte };
value = value
.checked_shl(8)
.and_then(|v| v.checked_add(u64::from(b)))
.ok_or_else(|| HeaderError::InvalidOctal(bytes.to_vec()))?;
}
Ok(value)
} else {
parse_octal(bytes)
}
}
#[must_use]
pub(crate) fn truncate_null(bytes: &[u8]) -> &[u8] {
match bytes.iter().position(|&b| b == 0) {
Some(pos) => &bytes[..pos],
None => bytes,
}
}
pub const PAX_PATH: &str = "path";
pub const PAX_LINKPATH: &str = "linkpath";
pub const PAX_SIZE: &str = "size";
pub const PAX_UID: &str = "uid";
pub const PAX_GID: &str = "gid";
pub const PAX_UNAME: &str = "uname";
pub const PAX_GNAME: &str = "gname";
pub const PAX_MTIME: &str = "mtime";
pub const PAX_ATIME: &str = "atime";
pub const PAX_CTIME: &str = "ctime";
pub const PAX_SCHILY_XATTR: &str = "SCHILY.xattr.";
pub const PAX_GNU_SPARSE: &str = "GNU.sparse.";
pub const PAX_GNU_SPARSE_NUMBLOCKS: &str = "GNU.sparse.numblocks";
pub const PAX_GNU_SPARSE_OFFSET: &str = "GNU.sparse.offset";
pub const PAX_GNU_SPARSE_NUMBYTES: &str = "GNU.sparse.numbytes";
pub const PAX_GNU_SPARSE_MAP: &str = "GNU.sparse.map";
pub const PAX_GNU_SPARSE_NAME: &str = "GNU.sparse.name";
pub const PAX_GNU_SPARSE_MAJOR: &str = "GNU.sparse.major";
pub const PAX_GNU_SPARSE_MINOR: &str = "GNU.sparse.minor";
pub const PAX_GNU_SPARSE_SIZE: &str = "GNU.sparse.size";
pub const PAX_GNU_SPARSE_REALSIZE: &str = "GNU.sparse.realsize";
#[derive(Debug, Error)]
pub enum PaxError {
#[error("malformed PAX extension record")]
Malformed,
#[error("PAX key is not valid UTF-8: {0}")]
InvalidKey(#[from] core::str::Utf8Error),
}
#[cfg(feature = "std")]
impl From<PaxError> for std::io::Error {
fn from(e: PaxError) -> Self {
std::io::Error::other(e.to_string())
}
}
#[derive(Debug, Clone)]
pub struct PaxExtension<'a> {
key: &'a [u8],
value: &'a [u8],
}
impl<'a> PaxExtension<'a> {
pub fn key(&self) -> core::result::Result<&'a str, core::str::Utf8Error> {
core::str::from_utf8(self.key)
}
#[must_use]
pub fn key_bytes(&self) -> &'a [u8] {
self.key
}
pub fn value(&self) -> core::result::Result<&'a str, core::str::Utf8Error> {
core::str::from_utf8(self.value)
}
#[must_use]
pub fn value_bytes(&self) -> &'a [u8] {
self.value
}
}
#[derive(Debug)]
pub struct PaxExtensions<'a> {
data: &'a [u8],
}
impl<'a> PaxExtensions<'a> {
#[must_use]
pub fn new(data: &'a [u8]) -> Self {
Self { data }
}
#[must_use]
pub fn get(&self, key: &str) -> Option<&'a str> {
for ext in PaxExtensions::new(self.data).flatten() {
if ext.key().ok() == Some(key) {
return ext.value().ok();
}
}
None
}
#[must_use]
pub fn get_u64(&self, key: &str) -> Option<u64> {
self.get(key).and_then(|v| v.parse().ok())
}
}
impl<'a> Iterator for PaxExtensions<'a> {
type Item = core::result::Result<PaxExtension<'a>, PaxError>;
fn next(&mut self) -> Option<Self::Item> {
if self.data.is_empty() {
return None;
}
let (len_bytes, _) = self
.data
.split_at(self.data.iter().position(|&b| b == b' ')?);
let len: usize = core::str::from_utf8(len_bytes).ok()?.parse().ok()?;
let record = match self.data.get(..len) {
Some(r) => r,
None => return Some(Err(PaxError::Malformed)),
};
if record.last() != Some(&b'\n') {
return Some(Err(PaxError::Malformed));
}
let kv = match record.get(len_bytes.len() + 1..record.len() - 1) {
Some(kv) => kv,
None => return Some(Err(PaxError::Malformed)),
};
let Some(eq_pos) = kv.iter().position(|&b| b == b'=') else {
return Some(Err(PaxError::Malformed));
};
let (key, value) = (&kv[..eq_pos], &kv[eq_pos + 1..]);
self.data = &self.data[len..];
Some(Ok(PaxExtension { key, value }))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_header_size() {
assert_eq!(size_of::<OldHeader>(), HEADER_SIZE);
assert_eq!(size_of::<UstarHeader>(), HEADER_SIZE);
assert_eq!(size_of::<GnuHeader>(), HEADER_SIZE);
assert_eq!(size_of::<GnuExtSparseHeader>(), HEADER_SIZE);
assert_eq!(size_of::<Header>(), HEADER_SIZE);
}
#[test]
fn test_sparse_header_size() {
assert_eq!(size_of::<GnuSparseHeader>(), 24);
assert_eq!(21 * 24 + 1 + 7, HEADER_SIZE);
}
#[test]
fn test_new_ustar() {
let header = Header::new_ustar();
assert!(header.is_ustar());
assert!(!header.is_gnu());
}
#[test]
fn test_new_gnu() {
let header = Header::new_gnu();
assert!(header.is_gnu());
assert!(!header.is_ustar());
}
#[test]
fn test_parse_octal() {
let cases: &[(&[u8], u64)] = &[
(b"0000644\0", 0o644),
(b"0000755\0", 0o755),
(b" 123 ", 0o123),
(b"0", 0),
(b"", 0),
(b" \0\0\0", 0),
(b" ", 0),
(b"\0\0\0\0\0\0", 0),
(b" 7\0", 7),
(b"0000755", 0o755),
(b"7", 7),
(b"00000001", 1),
(b"77777777777\0", 0o77777777777),
(b"7777777\0", 0o7777777),
];
for (input, expected) in cases {
assert_eq!(
parse_octal(input).unwrap(),
*expected,
"parse_octal({input:?})"
);
}
for bad in [&b"abc"[..], b"128"] {
assert!(parse_octal(bad).is_err(), "should reject {bad:?}");
}
}
#[test]
fn test_truncate_null() {
let cases: &[(&[u8], &[u8])] = &[
(b"hello\0world", b"hello"),
(b"no null", b"no null"),
(b"\0start", b""),
(b"", b""),
];
for (input, expected) in cases {
assert_eq!(truncate_null(input), *expected, "truncate_null({input:?})");
}
}
#[test]
fn test_entry_type_roundtrip() {
let types = [
(b'0', EntryType::Regular),
(b'\0', EntryType::Regular), (b'1', EntryType::Link),
(b'2', EntryType::Symlink),
(b'3', EntryType::Char),
(b'4', EntryType::Block),
(b'5', EntryType::Directory),
(b'6', EntryType::Fifo),
(b'7', EntryType::Continuous),
(b'L', EntryType::GnuLongName),
(b'K', EntryType::GnuLongLink),
(b'S', EntryType::GnuSparse),
(b'x', EntryType::XHeader),
(b'g', EntryType::XGlobalHeader),
];
for (byte, expected) in types {
let parsed = EntryType::from_byte(byte);
assert_eq!(parsed, expected, "from_byte({byte:#x})");
if byte != b'\0' {
assert_eq!(parsed.to_byte(), byte);
}
}
}
#[test]
fn test_entry_type_predicates() {
let cases: &[(EntryType, bool, bool, bool, bool)] = &[
(EntryType::Regular, true, false, false, false),
(EntryType::Continuous, true, false, false, false),
(EntryType::Directory, false, true, false, false),
(EntryType::Symlink, false, false, true, false),
(EntryType::Link, false, false, false, true),
(EntryType::Char, false, false, false, false),
];
for &(ty, file, dir, sym, hard) in cases {
assert_eq!(ty.is_file(), file, "{ty:?}.is_file()");
assert_eq!(ty.is_dir(), dir, "{ty:?}.is_dir()");
assert_eq!(ty.is_symlink(), sym, "{ty:?}.is_symlink()");
assert_eq!(ty.is_hard_link(), hard, "{ty:?}.is_hard_link()");
}
}
#[test]
fn test_checksum_empty_header() {
let header = Header::new_ustar();
let checksum = header.compute_checksum();
assert!(checksum > 0);
}
#[test]
fn test_is_empty() {
let mut header = Header::new_ustar();
assert!(!header.is_empty());
header.as_mut_bytes().fill(0);
assert!(header.is_empty());
}
#[test]
fn test_as_format_views() {
let header = Header::new_ustar();
let _old = header.as_old();
let _ustar = header.as_ustar();
let _gnu = header.as_gnu();
}
#[test]
fn test_ustar_default_magic() {
let ustar = UstarHeader::default();
assert_eq!(&ustar.magic, USTAR_MAGIC);
assert_eq!(&ustar.version, USTAR_VERSION);
}
#[test]
fn test_gnu_default_magic() {
let gnu = GnuHeader::default();
assert_eq!(&gnu.magic, GNU_MAGIC);
assert_eq!(&gnu.version, GNU_VERSION);
}
#[test]
fn test_path_bytes() {
let mut header = Header::new_ustar();
header.as_mut_bytes()[0..5].copy_from_slice(b"hello");
assert_eq!(header.path_bytes(), b"hello");
}
#[test]
fn test_link_name_bytes() {
let mut header = Header::new_ustar();
header.as_mut_bytes()[157..163].copy_from_slice(b"target");
assert_eq!(header.link_name_bytes(), b"target");
}
#[test]
fn test_username_groupname() {
let header = Header::new_ustar();
assert!(header.username().is_some());
assert!(header.groupname().is_some());
let mut old_header = Header::new_ustar();
old_header.as_mut_bytes()[257..265].fill(0);
assert!(old_header.username().is_none());
assert!(old_header.groupname().is_none());
}
#[test]
fn test_prefix() {
let header = Header::new_ustar();
assert!(header.prefix().is_some());
let gnu_header = Header::new_gnu();
assert!(gnu_header.prefix().is_none());
}
#[test]
fn test_device_numbers() {
let header = Header::new_ustar();
assert!(header.device_major().unwrap().is_some());
assert!(header.device_minor().unwrap().is_some());
let mut old_header = Header::new_ustar();
old_header.as_mut_bytes()[257..265].fill(0);
assert!(old_header.device_major().unwrap().is_none());
assert!(old_header.device_minor().unwrap().is_none());
}
#[test]
fn test_debug_impls() {
let header = Header::new_ustar();
let _ = format!("{header:?}");
let _ = format!("{:?}", header.as_old());
let _ = format!("{:?}", header.as_ustar());
let _ = format!("{:?}", header.as_gnu());
let _ = format!("{:?}", GnuExtSparseHeader::default());
let _ = format!("{:?}", GnuSparseHeader::default());
}
#[test]
fn test_parse_numeric() {
let octal_cases: &[(&[u8], u64)] = &[
(b"0000644\0", 0o644),
(b"0000755\0", 0o755),
(b" 123 ", 0o123),
(b"", 0),
];
for (input, expected) in octal_cases {
assert_eq!(
parse_numeric(input).unwrap(),
*expected,
"parse_numeric({input:?})"
);
}
let base256_cases: &[(&[u8], u64)] = &[
(&[0x80, 0x00, 0x00, 0x01], 1),
(&[0x80, 0x00, 0x01, 0x00], 256),
(&[0x80, 0xFF], 255),
(
&[
0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
],
1 << 40, ),
];
for (input, expected) in base256_cases {
assert_eq!(
parse_numeric(input).unwrap(),
*expected,
"parse_numeric({input:?})"
);
}
}
#[test]
fn test_parse_numeric_base256_in_header() {
let mut header = Header::new_ustar();
let size_field = &mut header.as_mut_bytes()[124..136];
size_field.fill(0);
size_field[0] = 0x80; size_field[8] = 0x12;
size_field[9] = 0x34;
size_field[10] = 0x56;
size_field[11] = 0x78;
assert_eq!(header.entry_size().unwrap(), 0x12345678);
}
#[test]
fn test_parse_numeric_base256_uid_gid() {
let mut header = Header::new_ustar();
let uid_field = &mut header.as_mut_bytes()[108..116];
uid_field.fill(0);
uid_field[0] = 0x80; uid_field[7] = 0x42; assert_eq!(header.uid().unwrap(), 66);
let gid_field = &mut header.as_mut_bytes()[116..124];
gid_field.fill(0);
gid_field[0] = 0x80; gid_field[6] = 0x01;
gid_field[7] = 0x00; assert_eq!(header.gid().unwrap(), 256);
}
#[test]
fn test_from_bytes() {
let mut data = [0u8; 512];
data[257..263].copy_from_slice(USTAR_MAGIC);
data[263..265].copy_from_slice(USTAR_VERSION);
data[0..4].copy_from_slice(b"test");
let header = Header::from_bytes(&data);
assert!(header.is_ustar());
assert_eq!(header.path_bytes(), b"test");
}
#[test]
fn test_from_bytes_gnu() {
let mut data = [0u8; 512];
data[257..263].copy_from_slice(GNU_MAGIC);
data[263..265].copy_from_slice(GNU_VERSION);
let header = Header::from_bytes(&data);
assert!(header.is_gnu());
assert!(!header.is_ustar());
}
#[test]
fn test_pax_simple() {
let data = b"20 path=foo/bar.txt\n";
let mut iter = PaxExtensions::new(data);
let ext = iter.next().unwrap().unwrap();
assert_eq!(ext.key().unwrap(), "path");
assert_eq!(ext.value().unwrap(), "foo/bar.txt");
assert!(iter.next().is_none());
}
#[test]
fn test_pax_multiple() {
let data = b"20 path=foo/bar.txt\n12 uid=1000\n12 gid=1000\n";
let exts: Vec<_> = PaxExtensions::new(data).collect();
assert_eq!(exts.len(), 3);
assert_eq!(exts[0].as_ref().unwrap().key().unwrap(), "path");
assert_eq!(exts[0].as_ref().unwrap().value().unwrap(), "foo/bar.txt");
assert_eq!(exts[1].as_ref().unwrap().key().unwrap(), "uid");
assert_eq!(exts[1].as_ref().unwrap().value().unwrap(), "1000");
assert_eq!(exts[2].as_ref().unwrap().key().unwrap(), "gid");
assert_eq!(exts[2].as_ref().unwrap().value().unwrap(), "1000");
}
#[test]
fn test_pax_get() {
let data = b"20 path=foo/bar.txt\n12 uid=1000\n16 size=1234567\n";
let pax = PaxExtensions::new(data);
let str_cases: &[(&str, Option<&str>)] = &[
("path", Some("foo/bar.txt")),
("uid", Some("1000")),
("missing", None),
];
for (key, expected) in str_cases {
assert_eq!(pax.get(key), *expected, "get({key:?})");
}
let u64_cases: &[(&str, Option<u64>)] = &[
("uid", Some(1000)),
("size", Some(1234567)),
("missing", None),
];
for (key, expected) in u64_cases {
assert_eq!(pax.get_u64(key), *expected, "get_u64({key:?})");
}
}
#[test]
fn test_pax_empty() {
let data = b"";
let mut iter = PaxExtensions::new(data);
assert!(iter.next().is_none());
}
#[test]
fn test_pax_binary_value() {
let data = b"24 SCHILY.xattr.foo=\x00\x01\x02\n";
let mut iter = PaxExtensions::new(data);
let ext = iter.next().unwrap().unwrap();
assert_eq!(ext.key().unwrap(), "SCHILY.xattr.foo");
assert_eq!(ext.value_bytes(), b"\x00\x01\x02");
}
#[test]
fn test_pax_long_path() {
let long_path = "a".repeat(200);
let record = format!("210 path={}\n", long_path);
let data = record.as_bytes();
let pax = PaxExtensions::new(data);
assert_eq!(pax.get("path"), Some(long_path.as_str()));
}
#[test]
fn test_pax_unicode_path() {
let data = "35 path=日本語/ファイル.txt\n".as_bytes();
let pax = PaxExtensions::new(data);
assert_eq!(pax.get("path"), Some("日本語/ファイル.txt"));
}
#[test]
fn test_pax_mtime_fractional() {
let data = b"22 mtime=1234567890.5\n";
let pax = PaxExtensions::new(data);
assert_eq!(pax.get("mtime"), Some("1234567890.5"));
assert_eq!(pax.get_u64("mtime"), None);
}
#[test]
fn test_pax_schily_xattr() {
let data = b"30 SCHILY.xattr.user.test=val\n";
let mut iter = PaxExtensions::new(data);
let ext = iter.next().unwrap().unwrap();
let key = ext.key().unwrap();
assert_eq!(key.strip_prefix(PAX_SCHILY_XATTR), Some("user.test"));
}
#[test]
fn test_pax_malformed() {
let cases: &[&[u8]] = &[
b"15 pathfoobar\n", b"100 path=foo\n", ];
for bad in cases {
let result = PaxExtensions::new(bad).next().unwrap();
assert!(result.is_err(), "should reject {bad:?}");
}
}
#[test]
fn test_path_exactly_100_bytes() {
let mut header = Header::new_ustar();
let path = "a".repeat(100);
header.as_mut_bytes()[0..100].copy_from_slice(path.as_bytes());
assert_eq!(header.path_bytes().len(), 100);
assert_eq!(header.path_bytes(), path.as_bytes());
}
#[test]
fn test_link_name_exactly_100_bytes() {
let mut header = Header::new_ustar();
let target = "t".repeat(100);
header.as_mut_bytes()[157..257].copy_from_slice(target.as_bytes());
assert_eq!(header.link_name_bytes().len(), 100);
assert_eq!(header.link_name_bytes(), target.as_bytes());
}
#[test]
fn test_prefix_exactly_155_bytes() {
let mut header = Header::new_ustar();
let prefix = "p".repeat(155);
header.as_mut_bytes()[345..500].copy_from_slice(prefix.as_bytes());
assert_eq!(header.prefix().unwrap().len(), 155);
assert_eq!(header.prefix().unwrap(), prefix.as_bytes());
}
#[test]
fn test_sparse_header_parsing() {
let header = Header::new_gnu();
let gnu = header.as_gnu();
for sparse in &gnu.sparse {
assert_eq!(parse_octal(&sparse.offset).unwrap(), 0);
assert_eq!(parse_octal(&sparse.numbytes).unwrap(), 0);
}
}
#[test]
fn test_gnu_atime_ctime() {
let mut header = Header::new_gnu();
let gnu = header.as_gnu();
assert_eq!(parse_octal(&gnu.atime).unwrap(), 0);
assert_eq!(parse_octal(&gnu.ctime).unwrap(), 0);
header.as_mut_bytes()[345..356].copy_from_slice(b"12345670123");
let gnu = header.as_gnu();
assert_eq!(parse_octal(&gnu.atime).unwrap(), 0o12345670123);
}
#[test]
fn test_ext_sparse_header() {
let ext = GnuExtSparseHeader::default();
assert_eq!(ext.isextended[0], 0);
assert_eq!(ext.sparse.len(), 21);
assert_eq!(size_of::<GnuExtSparseHeader>(), HEADER_SIZE);
}
#[test]
fn test_base256_max_values() {
let mut bytes = [0u8; 8];
bytes[0] = 0x80; bytes[4] = 0xFF;
bytes[5] = 0xFF;
bytes[6] = 0xFF;
bytes[7] = 0xFF;
assert_eq!(parse_numeric(&bytes).unwrap(), 0xFFFFFFFF);
}
#[test]
fn test_encode_numeric_roundtrip() {
fn check<const N: usize>(value: u64, expect_b256: bool) {
let mut field = [0u8; N];
encode_numeric(&mut field, value).unwrap();
assert_eq!(
field[0] & 0x80 != 0,
expect_b256,
"base256 flag for {value} in {N}-byte field"
);
assert_eq!(
parse_numeric(&field).unwrap(),
value,
"roundtrip {value} in {N}-byte field"
);
}
check::<12>(0, false);
check::<12>(0o644, false);
check::<12>(0o77777777777, false);
check::<12>(8_589_934_592, true);
check::<12>(0x1234_5678_90AB_CDEF, true);
check::<8>(0, false);
check::<8>(2_097_151, false); check::<8>(2_097_152, true);
}
#[test]
fn test_header_format_detection() {
let cases: &[(Header, bool, bool)] = &[
(Header::new_ustar(), true, false),
(Header::new_gnu(), false, true),
(Header::new_old(), false, false),
];
for (header, ustar, gnu) in cases {
assert_eq!(header.is_ustar(), *ustar, "{header:?}");
assert_eq!(header.is_gnu(), *gnu, "{header:?}");
assert_eq!(header.try_as_ustar().is_some(), *ustar);
assert_eq!(header.try_as_gnu().is_some(), *gnu);
}
}
#[test]
fn test_header_mutable_views() {
let mut header = Header::new_ustar();
let _old = header.as_old_mut();
let _ustar = header.as_ustar_mut();
let _gnu = header.as_gnu_mut();
let mut ustar_header = Header::new_ustar();
assert!(ustar_header.try_as_ustar_mut().is_some());
assert!(ustar_header.try_as_gnu_mut().is_none());
}
#[test]
fn test_header_setters() {
let mut header = Header::new_ustar();
type NumericCase = (
fn(&mut Header, u64) -> Result<()>,
fn(&Header) -> Result<u64>,
u64,
);
let numeric_cases: &[NumericCase] = &[
(|h, v| h.set_size(v), |h| h.entry_size(), 1024),
(|h, v| h.set_uid(v), |h| h.uid(), 1000),
(|h, v| h.set_gid(v), |h| h.gid(), 1000),
(|h, v| h.set_mtime(v), |h| h.mtime(), 1234567890),
];
for (set, get, value) in numeric_cases {
set(&mut header, *value).unwrap();
assert_eq!(get(&header).unwrap(), *value, "roundtrip {value}");
}
header.set_mode(0o755).unwrap();
assert_eq!(header.mode().unwrap(), 0o755);
header.set_entry_type(EntryType::Directory);
assert_eq!(header.entry_type(), EntryType::Directory);
header.set_path(b"test.txt").unwrap();
assert_eq!(header.path_bytes(), b"test.txt");
header.set_link_name(b"target").unwrap();
assert_eq!(header.link_name_bytes(), b"target");
header.set_checksum();
header.verify_checksum().unwrap();
}
#[test]
fn test_format_aware_encoding() {
let large_uid: u64 = 0xFFFF_FFFF; let large_size: u64 = 10_000_000_000;
let mut gnu = Header::new_gnu();
gnu.set_uid(large_uid).unwrap();
assert_eq!(gnu.uid().unwrap(), large_uid);
gnu.set_size(large_size).unwrap();
assert_eq!(gnu.entry_size().unwrap(), large_size);
let mut ustar = Header::new_ustar();
assert!(ustar.set_uid(large_uid).is_err());
assert!(ustar.set_size(large_size).is_err());
ustar.set_uid(1000).unwrap();
ustar.set_size(1024).unwrap();
}
#[test]
fn test_gnu_header_atime_ctime_setters() {
let mut header = Header::new_gnu();
let gnu = header.as_gnu_mut();
gnu.set_atime(1234567890);
assert_eq!(gnu.atime().unwrap(), 1234567890);
gnu.set_ctime(1234567891);
assert_eq!(gnu.ctime().unwrap(), 1234567891);
}
#[test]
fn test_gnu_header_real_size() {
let mut header = Header::new_gnu();
let gnu = header.as_gnu_mut();
gnu.set_real_size(1_000_000);
assert_eq!(gnu.real_size().unwrap(), 1_000_000);
gnu.set_real_size(10_000_000_000);
assert_eq!(gnu.real_size().unwrap(), 10_000_000_000);
}
#[test]
fn test_gnu_header_is_extended() {
let mut header = Header::new_gnu();
let gnu = header.as_gnu_mut();
assert!(!gnu.is_extended());
gnu.set_is_extended(true);
assert!(gnu.is_extended());
gnu.set_is_extended(false);
assert!(!gnu.is_extended());
}
mod proptest_tests {
use super::*;
use proptest::prelude::*;
use std::io::Cursor;
#[derive(Debug, Clone, Copy)]
enum TarFormat {
Ustar,
Gnu,
}
fn tar_format_strategy() -> impl Strategy<Value = TarFormat> {
prop_oneof![Just(TarFormat::Ustar), Just(TarFormat::Gnu)]
}
impl TarFormat {
fn header_builder(self) -> crate::builder::HeaderBuilder {
match self {
TarFormat::Ustar => crate::builder::HeaderBuilder::new_ustar(),
TarFormat::Gnu => crate::builder::HeaderBuilder::new_gnu(),
}
}
fn tar_rs_header(self) -> tar::Header {
match self {
TarFormat::Ustar => tar::Header::new_ustar(),
TarFormat::Gnu => tar::Header::new_gnu(),
}
}
fn our_header(self) -> Header {
match self {
TarFormat::Ustar => Header::new_ustar(),
TarFormat::Gnu => Header::new_gnu(),
}
}
}
fn tar_rs_bytes(header: &tar::Header) -> [u8; 512] {
*header.as_bytes()
}
fn header_hex(bytes: &[u8; 512]) -> String {
let fields: &[(&str, std::ops::Range<usize>)] = &[
("name", 0..100),
("mode", 100..108),
("uid", 108..116),
("gid", 116..124),
("size", 124..136),
("mtime", 136..148),
("checksum", 148..156),
("typeflag", 156..157),
("linkname", 157..257),
("magic", 257..263),
("version", 263..265),
("uname", 265..297),
("gname", 297..329),
("devmajor", 329..337),
("devminor", 337..345),
("prefix", 345..500),
("padding", 500..512),
];
let mut out = String::new();
for (name, range) in fields {
let slice = &bytes[range.clone()];
if slice.iter().all(|&b| b == 0) {
continue;
}
use std::fmt::Write;
write!(out, "{name:>10}: ").unwrap();
for &b in slice {
if b.is_ascii_graphic() || b == b' ' {
out.push(b as char);
} else {
write!(out, "\\x{b:02x}").unwrap();
}
}
out.push('\n');
}
out
}
fn assert_headers_eq(ours: &[u8; 512], theirs: &[u8; 512]) {
if ours != theirs {
similar_asserts::assert_eq!(header_hex(ours), header_hex(theirs));
}
}
fn path_strategy() -> impl Strategy<Value = String> {
proptest::string::string_regex(
"[a-zA-Z0-9_][a-zA-Z0-9_.+-]*(/[a-zA-Z0-9_][a-zA-Z0-9_.+-]*)*",
)
.expect("valid regex")
.prop_filter("reasonable length", |s| !s.is_empty() && s.len() < 100)
}
fn link_target_strategy() -> impl Strategy<Value = String> {
proptest::string::string_regex(
"[a-zA-Z0-9_][a-zA-Z0-9_+-]*(/[a-zA-Z0-9_][a-zA-Z0-9_+-]*)*",
)
.expect("valid regex")
.prop_filter("reasonable length", |s| !s.is_empty() && s.len() < 100)
}
fn name_strategy() -> impl Strategy<Value = String> {
proptest::string::string_regex("[a-zA-Z_][a-zA-Z0-9_]{0,30}").expect("valid regex")
}
fn mode_strategy() -> impl Strategy<Value = u32> {
prop_oneof![
Just(0o644), Just(0o755), Just(0o600), Just(0o777), Just(0o400), (0u32..0o7777), ]
}
fn id_strategy() -> impl Strategy<Value = u64> {
prop_oneof![
Just(0u64),
Just(1000u64),
Just(65534u64), (0u64..0o7777777), ]
}
fn mtime_strategy() -> impl Strategy<Value = u64> {
prop_oneof![
Just(0u64),
Just(1234567890u64),
(0u64..0o77777777777u64), ]
}
fn size_strategy() -> impl Strategy<Value = u64> {
prop_oneof![
Just(0u64),
Just(1u64),
Just(512u64),
Just(4096u64),
(0u64..1024 * 1024), ]
}
#[derive(Debug, Clone)]
struct FileParams {
path: String,
mode: u32,
uid: u64,
gid: u64,
mtime: u64,
size: u64,
username: String,
groupname: String,
}
fn file_params_strategy() -> impl Strategy<Value = FileParams> {
(
path_strategy(),
mode_strategy(),
id_strategy(),
id_strategy(),
mtime_strategy(),
size_strategy(),
name_strategy(),
name_strategy(),
)
.prop_map(
|(path, mode, uid, gid, mtime, size, username, groupname)| FileParams {
path,
mode,
uid,
gid,
mtime,
size,
username,
groupname,
},
)
}
#[derive(Debug, Clone)]
struct SymlinkParams {
path: String,
target: String,
uid: u64,
gid: u64,
mtime: u64,
}
fn symlink_params_strategy() -> impl Strategy<Value = SymlinkParams> {
(
path_strategy(),
link_target_strategy(),
id_strategy(),
id_strategy(),
mtime_strategy(),
)
.prop_map(|(path, target, uid, gid, mtime)| SymlinkParams {
path,
target,
uid,
gid,
mtime,
})
}
#[derive(Debug, Clone)]
struct DirParams {
path: String,
mode: u32,
uid: u64,
gid: u64,
mtime: u64,
}
fn dir_params_strategy() -> impl Strategy<Value = DirParams> {
(
path_strategy(),
mode_strategy(),
id_strategy(),
id_strategy(),
mtime_strategy(),
)
.prop_map(|(path, mode, uid, gid, mtime)| DirParams {
path,
mode,
uid,
gid,
mtime,
})
}
fn create_file_tar(params: &FileParams, fmt: TarFormat) -> Vec<u8> {
let mut builder = tar::Builder::new(Vec::new());
let mut header = fmt.tar_rs_header();
header.set_path(¶ms.path).unwrap();
header.set_mode(params.mode);
header.set_uid(params.uid);
header.set_gid(params.gid);
header.set_mtime(params.mtime);
header.set_size(params.size);
header.set_entry_type(tar::EntryType::Regular);
header.set_username(¶ms.username).unwrap();
header.set_groupname(¶ms.groupname).unwrap();
header.set_cksum();
let content = vec![0u8; params.size as usize];
builder
.append_data(&mut header, ¶ms.path, content.as_slice())
.unwrap();
builder.into_inner().unwrap()
}
fn create_symlink_tar(params: &SymlinkParams, fmt: TarFormat) -> Vec<u8> {
let mut builder = tar::Builder::new(Vec::new());
let mut header = fmt.tar_rs_header();
header.set_path(¶ms.path).unwrap();
header.set_mode(0o777);
header.set_uid(params.uid);
header.set_gid(params.gid);
header.set_mtime(params.mtime);
header.set_size(0);
header.set_entry_type(tar::EntryType::Symlink);
header.set_link_name(¶ms.target).unwrap();
header.set_cksum();
builder
.append_data(&mut header, ¶ms.path, std::io::empty())
.unwrap();
builder.into_inner().unwrap()
}
fn create_dir_tar(params: &DirParams, fmt: TarFormat) -> Vec<u8> {
let mut builder = tar::Builder::new(Vec::new());
let mut header = fmt.tar_rs_header();
let path = if params.path.ends_with('/') {
params.path.clone()
} else {
format!("{}/", params.path)
};
header.set_path(&path).unwrap();
header.set_mode(params.mode);
header.set_uid(params.uid);
header.set_gid(params.gid);
header.set_mtime(params.mtime);
header.set_size(0);
header.set_entry_type(tar::EntryType::Directory);
header.set_cksum();
builder
.append_data(&mut header, &path, std::io::empty())
.unwrap();
builder.into_inner().unwrap()
}
fn extract_header_bytes(tar_data: &[u8]) -> [u8; 512] {
tar_data[..512].try_into().unwrap()
}
fn compare_headers(
our_header: &Header,
tar_header: &tar::Header,
) -> std::result::Result<(), TestCaseError> {
prop_assert_eq!(
our_header.entry_type().to_byte(),
tar_header.entry_type().as_byte(),
"entry type mismatch"
);
prop_assert_eq!(
our_header.entry_size().unwrap(),
tar_header.size().unwrap(),
"size mismatch"
);
prop_assert_eq!(
our_header.mode().unwrap(),
tar_header.mode().unwrap(),
"mode mismatch"
);
prop_assert_eq!(
our_header.uid().unwrap(),
tar_header.uid().unwrap(),
"uid mismatch"
);
prop_assert_eq!(
our_header.gid().unwrap(),
tar_header.gid().unwrap(),
"gid mismatch"
);
prop_assert_eq!(
our_header.mtime().unwrap(),
tar_header.mtime().unwrap(),
"mtime mismatch"
);
let tar_path = tar_header.path_bytes();
prop_assert_eq!(our_header.path_bytes(), tar_path.as_ref(), "path mismatch");
let our_link = our_header.link_name_bytes();
if let Some(tar_link) = tar_header.link_name_bytes() {
prop_assert_eq!(our_link, tar_link.as_ref(), "link_name mismatch");
} else {
prop_assert!(our_link.is_empty(), "expected empty link name");
}
if let Some(our_username) = our_header.username() {
if let Some(tar_username) = tar_header.username_bytes() {
prop_assert_eq!(our_username, tar_username, "username mismatch");
}
}
if let Some(our_groupname) = our_header.groupname() {
if let Some(tar_groupname) = tar_header.groupname_bytes() {
prop_assert_eq!(our_groupname, tar_groupname, "groupname mismatch");
}
}
our_header.verify_checksum().unwrap();
Ok(())
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(256))]
#[test]
fn test_file_header_crosscheck(
params in file_params_strategy(),
fmt in tar_format_strategy(),
) {
let tar_data = create_file_tar(¶ms, fmt);
let header_bytes = extract_header_bytes(&tar_data);
let our_header = Header::from_bytes(&header_bytes);
let tar_header = tar::Header::from_byte_slice(&header_bytes);
compare_headers(our_header, tar_header)?;
prop_assert!(our_header.entry_type().is_file());
prop_assert_eq!(our_header.entry_size().unwrap(), params.size);
if matches!(fmt, TarFormat::Gnu) {
prop_assert!(our_header.is_gnu());
prop_assert!(!our_header.is_ustar());
}
}
#[test]
fn test_symlink_header_crosscheck(
params in symlink_params_strategy(),
fmt in tar_format_strategy(),
) {
let tar_data = create_symlink_tar(¶ms, fmt);
let header_bytes = extract_header_bytes(&tar_data);
let our_header = Header::from_bytes(&header_bytes);
let tar_header = tar::Header::from_byte_slice(&header_bytes);
compare_headers(our_header, tar_header)?;
prop_assert!(our_header.entry_type().is_symlink());
prop_assert_eq!(our_header.link_name_bytes(), params.target.as_bytes());
if matches!(fmt, TarFormat::Gnu) {
prop_assert!(our_header.is_gnu());
}
}
#[test]
fn test_dir_header_crosscheck(
params in dir_params_strategy(),
fmt in tar_format_strategy(),
) {
let tar_data = create_dir_tar(¶ms, fmt);
let header_bytes = extract_header_bytes(&tar_data);
let our_header = Header::from_bytes(&header_bytes);
let tar_header = tar::Header::from_byte_slice(&header_bytes);
compare_headers(our_header, tar_header)?;
prop_assert!(our_header.entry_type().is_dir());
if matches!(fmt, TarFormat::Gnu) {
prop_assert!(our_header.is_gnu());
}
}
}
mod archive_tests {
use super::*;
proptest! {
#![proptest_config(ProptestConfig::with_cases(64))]
#[test]
fn test_multi_entry_archive(
files in prop::collection::vec(file_params_strategy(), 1..8),
dirs in prop::collection::vec(dir_params_strategy(), 0..4),
) {
let mut builder = tar::Builder::new(Vec::new());
for params in &dirs {
let mut header = tar::Header::new_ustar();
let path = if params.path.ends_with('/') {
params.path.clone()
} else {
format!("{}/", params.path)
};
header.set_path(&path).unwrap();
header.set_mode(params.mode);
header.set_uid(params.uid);
header.set_gid(params.gid);
header.set_mtime(params.mtime);
header.set_size(0);
header.set_entry_type(tar::EntryType::Directory);
header.set_cksum();
builder.append_data(&mut header, &path, std::io::empty()).unwrap();
}
for params in &files {
let mut header = tar::Header::new_ustar();
header.set_path(¶ms.path).unwrap();
header.set_mode(params.mode);
header.set_uid(params.uid);
header.set_gid(params.gid);
header.set_mtime(params.mtime);
header.set_size(params.size);
header.set_entry_type(tar::EntryType::Regular);
header.set_username(¶ms.username).unwrap();
header.set_groupname(¶ms.groupname).unwrap();
header.set_cksum();
let content = vec![0u8; params.size as usize];
builder.append_data(&mut header, ¶ms.path, content.as_slice()).unwrap();
}
let tar_data = builder.into_inner().unwrap();
let mut archive = tar::Archive::new(Cursor::new(&tar_data));
let entries = archive.entries().unwrap();
for entry_result in entries {
let entry = entry_result.unwrap();
let tar_header = entry.header();
let our_header = Header::from_bytes(tar_header.as_bytes());
compare_headers(our_header, tar_header)?;
}
}
}
}
mod format_detection_tests {
use super::*;
proptest! {
#![proptest_config(ProptestConfig::with_cases(128))]
#[test]
fn test_ustar_format_detected(params in file_params_strategy()) {
let tar_data = create_file_tar(¶ms, TarFormat::Ustar);
let header_bytes = extract_header_bytes(&tar_data);
let our_header = Header::from_bytes(&header_bytes);
prop_assert!(our_header.is_ustar(), "should be UStar");
prop_assert!(!our_header.is_gnu(), "should not be GNU");
prop_assert_eq!(&header_bytes[257..263], USTAR_MAGIC);
prop_assert_eq!(&header_bytes[263..265], USTAR_VERSION);
}
#[test]
fn test_gnu_format_detected(params in file_params_strategy()) {
let tar_data = create_file_tar(¶ms, TarFormat::Gnu);
let header_bytes = extract_header_bytes(&tar_data);
let our_header = Header::from_bytes(&header_bytes);
prop_assert!(our_header.is_gnu(), "should be GNU");
prop_assert!(!our_header.is_ustar(), "should not be UStar");
prop_assert_eq!(&header_bytes[257..263], GNU_MAGIC);
prop_assert_eq!(&header_bytes[263..265], GNU_VERSION);
}
}
#[test]
fn test_old_format_detection() {
let mut header_bytes = [0u8; 512];
header_bytes[0..4].copy_from_slice(b"test");
header_bytes[100..107].copy_from_slice(b"0000644");
header_bytes[124..135].copy_from_slice(b"00000000000");
header_bytes[156] = b'0';
let mut checksum: u64 = 0;
for (i, &byte) in header_bytes.iter().enumerate() {
if (148..156).contains(&i) {
checksum += u64::from(b' ');
} else {
checksum += u64::from(byte);
}
}
let checksum_str = format!("{checksum:06o}\0 ");
header_bytes[148..156].copy_from_slice(checksum_str.as_bytes());
let our_header = Header::from_bytes(&header_bytes);
assert!(!our_header.is_ustar());
assert!(!our_header.is_gnu());
assert_eq!(our_header.path_bytes(), b"test");
assert_eq!(our_header.entry_type(), EntryType::Regular);
}
}
mod checksum_tests {
use super::*;
proptest! {
#![proptest_config(ProptestConfig::with_cases(256))]
#[test]
fn test_checksum_always_valid(
params in file_params_strategy(),
fmt in tar_format_strategy(),
) {
let tar_data = create_file_tar(¶ms, fmt);
let header_bytes = extract_header_bytes(&tar_data);
let our_header = Header::from_bytes(&header_bytes);
our_header.verify_checksum().unwrap();
}
#[test]
fn test_checksum_recompute(
params in file_params_strategy(),
fmt in tar_format_strategy(),
) {
let tar_data = create_file_tar(¶ms, fmt);
let header_bytes = extract_header_bytes(&tar_data);
let our_header = Header::from_bytes(&header_bytes);
let computed = our_header.compute_checksum();
let stored = parse_octal(&header_bytes[148..156]).unwrap();
prop_assert_eq!(computed, stored);
}
}
}
mod entry_type_tests {
use super::*;
#[test]
fn test_all_entry_types_map_correctly() {
let mappings: &[(u8, EntryType, tar::EntryType)] = &[
(b'0', EntryType::Regular, tar::EntryType::Regular),
(b'\0', EntryType::Regular, tar::EntryType::Regular),
(b'1', EntryType::Link, tar::EntryType::Link),
(b'2', EntryType::Symlink, tar::EntryType::Symlink),
(b'3', EntryType::Char, tar::EntryType::Char),
(b'4', EntryType::Block, tar::EntryType::Block),
(b'5', EntryType::Directory, tar::EntryType::Directory),
(b'6', EntryType::Fifo, tar::EntryType::Fifo),
(b'7', EntryType::Continuous, tar::EntryType::Continuous),
(b'L', EntryType::GnuLongName, tar::EntryType::GNULongName),
(b'K', EntryType::GnuLongLink, tar::EntryType::GNULongLink),
(b'S', EntryType::GnuSparse, tar::EntryType::GNUSparse),
(b'x', EntryType::XHeader, tar::EntryType::XHeader),
(
b'g',
EntryType::XGlobalHeader,
tar::EntryType::XGlobalHeader,
),
];
for &(byte, expected_ours, expected_tar) in mappings {
let ours = EntryType::from_byte(byte);
let tar_type = tar::EntryType::new(byte);
assert_eq!(ours, expected_ours, "our mapping for byte {byte}");
assert_eq!(tar_type, expected_tar, "tar mapping for byte {byte}");
}
}
proptest! {
#[test]
fn test_entry_type_roundtrip(byte: u8) {
let our_type = EntryType::from_byte(byte);
let tar_type = tar::EntryType::new(byte);
let our_byte = our_type.to_byte();
let tar_byte = tar_type.as_byte();
if byte == b'\0' {
prop_assert_eq!(our_byte, b'0');
} else {
prop_assert_eq!(our_byte, tar_byte);
}
}
}
}
mod codec_tests {
use super::*;
proptest! {
#![proptest_config(ProptestConfig::with_cases(10_000))]
#[test]
fn test_encode_octal_8_roundtrip(value in 0u64..=0o7777777) {
let mut field = [0u8; 8];
encode_octal(&mut field, value).unwrap();
prop_assert_eq!(parse_octal(&field).unwrap(), value);
}
#[test]
fn test_encode_octal_12_roundtrip(value in 0u64..=0o77777777777) {
let mut field = [0u8; 12];
encode_octal(&mut field, value).unwrap();
prop_assert_eq!(parse_octal(&field).unwrap(), value);
}
#[test]
fn test_encode_numeric_8_roundtrip(value in 0u64..=(i64::MAX as u64)) {
let mut field = [0u8; 8];
encode_numeric(&mut field, value).unwrap();
prop_assert_eq!(parse_numeric(&field).unwrap(), value);
}
#[test]
fn test_encode_numeric_8_rejects_huge(value in (i64::MAX as u64 + 1)..=u64::MAX) {
let mut field = [0u8; 8];
prop_assert!(encode_numeric(&mut field, value).is_err());
}
#[test]
fn test_encode_numeric_12_roundtrip(value: u64) {
let mut field = [0u8; 12];
encode_numeric(&mut field, value).unwrap();
prop_assert_eq!(parse_numeric(&field).unwrap(), value);
}
#[test]
fn test_encode_octal_8_rejects_overflow(value in 0o10000000u64..=u64::MAX) {
let mut field = [0u8; 8];
prop_assert!(encode_octal(&mut field, value).is_err());
}
#[test]
fn test_encode_octal_12_rejects_overflow(value in 0o100000000000u64..=u64::MAX) {
let mut field = [0u8; 12];
prop_assert!(encode_octal(&mut field, value).is_err());
}
#[test]
fn test_parse_octal_8_no_panic(bytes in proptest::array::uniform8(0u8..)) {
let _ = parse_octal(&bytes);
}
#[test]
fn test_parse_octal_12_no_panic(bytes in proptest::array::uniform12(0u8..)) {
let _ = parse_octal(&bytes);
}
#[test]
fn test_parse_numeric_8_no_panic(bytes in proptest::array::uniform8(0u8..)) {
let _ = parse_numeric(&bytes);
}
#[test]
fn test_parse_numeric_12_no_panic(bytes in proptest::array::uniform12(0u8..)) {
let _ = parse_numeric(&bytes);
}
}
}
mod builder_equivalence_tests {
use super::*;
fn build_file_tar_core(params: &FileParams, fmt: TarFormat) -> Header {
let mut b = fmt.header_builder();
b.path(params.path.as_bytes())
.unwrap()
.mode(params.mode)
.unwrap()
.uid(params.uid)
.unwrap()
.gid(params.gid)
.unwrap()
.size(params.size)
.unwrap()
.mtime(params.mtime)
.unwrap()
.entry_type(EntryType::Regular)
.username(params.username.as_bytes())
.unwrap()
.groupname(params.groupname.as_bytes())
.unwrap();
b.finish()
}
fn build_file_tar_rs(params: &FileParams, fmt: TarFormat) -> [u8; 512] {
let mut h = fmt.tar_rs_header();
h.set_path(¶ms.path).unwrap();
h.set_mode(params.mode);
h.set_uid(params.uid);
h.set_gid(params.gid);
h.set_size(params.size);
h.set_mtime(params.mtime);
h.set_entry_type(tar::EntryType::Regular);
h.set_username(¶ms.username).unwrap();
h.set_groupname(¶ms.groupname).unwrap();
h.set_cksum();
tar_rs_bytes(&h)
}
fn build_symlink_tar_core(params: &SymlinkParams, fmt: TarFormat) -> Header {
let mut b = fmt.header_builder();
b.path(params.path.as_bytes())
.unwrap()
.mode(0o777)
.unwrap()
.uid(params.uid)
.unwrap()
.gid(params.gid)
.unwrap()
.size(0)
.unwrap()
.mtime(params.mtime)
.unwrap()
.entry_type(EntryType::Symlink)
.link_name(params.target.as_bytes())
.unwrap();
b.finish()
}
fn build_symlink_tar_rs(params: &SymlinkParams, fmt: TarFormat) -> [u8; 512] {
let mut h = fmt.tar_rs_header();
h.set_path(¶ms.path).unwrap();
h.set_mode(0o777);
h.set_uid(params.uid);
h.set_gid(params.gid);
h.set_size(0);
h.set_mtime(params.mtime);
h.set_entry_type(tar::EntryType::Symlink);
h.set_link_name(¶ms.target).unwrap();
h.set_cksum();
tar_rs_bytes(&h)
}
fn build_dir_tar_core(params: &DirParams, fmt: TarFormat) -> Header {
let mut b = fmt.header_builder();
let path = if params.path.ends_with('/') {
params.path.clone()
} else {
format!("{}/", params.path)
};
b.path(path.as_bytes())
.unwrap()
.mode(params.mode)
.unwrap()
.uid(params.uid)
.unwrap()
.gid(params.gid)
.unwrap()
.size(0)
.unwrap()
.mtime(params.mtime)
.unwrap()
.entry_type(EntryType::Directory);
b.finish()
}
fn build_dir_tar_rs(params: &DirParams, fmt: TarFormat) -> [u8; 512] {
let mut h = fmt.tar_rs_header();
let path = if params.path.ends_with('/') {
params.path.clone()
} else {
format!("{}/", params.path)
};
h.set_path(&path).unwrap();
h.set_mode(params.mode);
h.set_uid(params.uid);
h.set_gid(params.gid);
h.set_size(0);
h.set_mtime(params.mtime);
h.set_entry_type(tar::EntryType::Directory);
h.set_cksum();
tar_rs_bytes(&h)
}
fn build_file_header_setters(params: &FileParams, fmt: TarFormat) -> [u8; 512] {
let mut h = fmt.our_header();
h.set_path(params.path.as_bytes()).unwrap();
h.set_mode(params.mode).unwrap();
h.set_uid(params.uid).unwrap();
h.set_gid(params.gid).unwrap();
h.set_size(params.size).unwrap();
h.set_mtime(params.mtime).unwrap();
h.set_entry_type(EntryType::Regular);
h.set_username(params.username.as_bytes()).unwrap();
h.set_groupname(params.groupname.as_bytes()).unwrap();
h.set_checksum();
*h.as_bytes()
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(256))]
#[test]
fn test_file_builder_equivalence(
params in file_params_strategy(),
fmt in tar_format_strategy(),
) {
assert_headers_eq(
build_file_tar_core(¶ms, fmt).as_bytes(),
&build_file_tar_rs(¶ms, fmt),
);
}
#[test]
fn test_symlink_builder_equivalence(
params in symlink_params_strategy(),
fmt in tar_format_strategy(),
) {
assert_headers_eq(
build_symlink_tar_core(¶ms, fmt).as_bytes(),
&build_symlink_tar_rs(¶ms, fmt),
);
}
#[test]
fn test_dir_builder_equivalence(
params in dir_params_strategy(),
fmt in tar_format_strategy(),
) {
assert_headers_eq(
build_dir_tar_core(¶ms, fmt).as_bytes(),
&build_dir_tar_rs(¶ms, fmt),
);
}
#[test]
fn test_header_setters_equivalence(
params in file_params_strategy(),
fmt in tar_format_strategy(),
) {
assert_headers_eq(
&build_file_header_setters(¶ms, fmt),
&build_file_tar_rs(¶ms, fmt),
);
}
}
mod base256_equivalence {
use super::*;
fn large_id_strategy() -> impl Strategy<Value = u64> {
prop_oneof![
Just(2097152u64), Just(u32::MAX as u64), (2097152u64..u32::MAX as u64), ]
}
fn default_headers() -> (Header, tar::Header) {
let mut ours = Header::new_gnu();
ours.set_path(b"test.txt").unwrap();
ours.set_mode(0o644).unwrap();
ours.set_uid(1000).unwrap();
ours.set_gid(1000).unwrap();
ours.set_size(0).unwrap();
ours.set_mtime(0).unwrap();
ours.set_entry_type(EntryType::Regular);
let mut theirs = tar::Header::new_gnu();
theirs.set_path("test.txt").unwrap();
theirs.set_mode(0o644);
theirs.set_uid(1000);
theirs.set_gid(1000);
theirs.set_size(0);
theirs.set_mtime(0);
theirs.set_entry_type(tar::EntryType::Regular);
(ours, theirs)
}
#[test]
fn test_large_uid_encoding() {
let (mut ours, mut theirs) = default_headers();
ours.set_uid(2_500_000).unwrap();
ours.set_checksum();
theirs.set_uid(2_500_000);
theirs.set_cksum();
assert_eq!(&ours.as_bytes()[108..116], &theirs.as_bytes()[108..116]);
assert_eq!(ours.uid().unwrap(), 2_500_000);
}
#[test]
fn test_large_gid_encoding() {
let (mut ours, mut theirs) = default_headers();
ours.set_gid(3_000_000).unwrap();
ours.set_checksum();
theirs.set_gid(3_000_000);
theirs.set_cksum();
assert_eq!(&ours.as_bytes()[116..124], &theirs.as_bytes()[116..124]);
assert_eq!(ours.gid().unwrap(), 3_000_000);
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(64))]
#[test]
fn test_large_uid_proptest(uid in large_id_strategy()) {
let (mut ours, mut theirs) = default_headers();
ours.set_uid(uid).unwrap();
ours.set_checksum();
theirs.set_uid(uid);
theirs.set_cksum();
prop_assert_eq!(
&ours.as_bytes()[108..116],
&theirs.as_bytes()[108..116],
);
}
#[test]
fn test_large_gid_proptest(gid in large_id_strategy()) {
let (mut ours, mut theirs) = default_headers();
ours.set_gid(gid).unwrap();
ours.set_checksum();
theirs.set_gid(gid);
theirs.set_cksum();
prop_assert_eq!(
&ours.as_bytes()[116..124],
&theirs.as_bytes()[116..124],
);
}
}
}
mod small_setter_tests {
use super::*;
fn default_header_pair() -> (Header, tar::Header) {
let mut ours = Header::new_gnu();
ours.set_path(b"t.txt").unwrap();
ours.set_mode_small(0o644);
ours.set_uid(0).unwrap();
ours.set_gid(0).unwrap();
ours.set_size_small(0);
ours.set_mtime_small(0);
ours.set_entry_type(EntryType::Regular);
let mut theirs = tar::Header::new_gnu();
theirs.set_path("t.txt").unwrap();
theirs.set_mode(0o644);
theirs.set_uid(0);
theirs.set_gid(0);
theirs.set_size(0);
theirs.set_mtime(0);
theirs.set_entry_type(tar::EntryType::Regular);
(ours, theirs)
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(256))]
#[test]
fn test_set_mode_small_roundtrip(mode: u16) {
let (mut ours, mut theirs) = default_header_pair();
ours.set_mode_small(mode);
ours.set_checksum();
theirs.set_mode(u32::from(mode));
theirs.set_cksum();
prop_assert_eq!(ours.mode().unwrap(), u32::from(mode));
prop_assert_eq!(
&ours.as_bytes()[100..108],
&theirs.as_bytes()[100..108],
);
}
#[test]
fn test_set_size_small_roundtrip(size: u32) {
let (mut ours, mut theirs) = default_header_pair();
ours.set_size_small(size);
ours.set_checksum();
theirs.set_size(u64::from(size));
theirs.set_cksum();
prop_assert_eq!(ours.entry_size().unwrap(), u64::from(size));
prop_assert_eq!(
&ours.as_bytes()[124..136],
&theirs.as_bytes()[124..136],
);
}
#[test]
fn test_set_mtime_small_roundtrip(mtime: u32) {
let (mut ours, mut theirs) = default_header_pair();
ours.set_mtime_small(mtime);
ours.set_checksum();
theirs.set_mtime(u64::from(mtime));
theirs.set_cksum();
prop_assert_eq!(ours.mtime().unwrap(), u64::from(mtime));
prop_assert_eq!(
&ours.as_bytes()[136..148],
&theirs.as_bytes()[136..148],
);
}
#[test]
fn test_set_device_small_roundtrip(major: u16, minor: u16) {
let mut header = Header::new_ustar();
header.set_device_small(major, minor);
prop_assert_eq!(
header.device_major().unwrap().unwrap(),
u32::from(major),
);
prop_assert_eq!(
header.device_minor().unwrap().unwrap(),
u32::from(minor),
);
}
}
}
mod gnu_extensions_equivalence {
use super::*;
use crate::builder::EntryBuilder;
fn long_path_strategy() -> impl Strategy<Value = String> {
(3..15usize)
.prop_flat_map(|segments| {
proptest::collection::vec(
proptest::string::string_regex("[a-z]{5,20}").expect("valid regex"),
segments,
)
})
.prop_map(|parts| parts.join("/"))
.prop_filter("must exceed 100 bytes", |s| s.len() > 100 && s.len() < 300)
}
fn long_link_strategy() -> impl Strategy<Value = String> {
long_path_strategy()
}
#[derive(Debug, Clone)]
struct LongPathFileParams {
path: String,
mode: u32,
uid: u64,
gid: u64,
mtime: u64,
}
fn long_path_file_params_strategy() -> impl Strategy<Value = LongPathFileParams> {
(
long_path_strategy(),
mode_strategy(),
id_strategy(),
id_strategy(),
mtime_strategy(),
)
.prop_map(|(path, mode, uid, gid, mtime)| {
LongPathFileParams {
path,
mode,
uid,
gid,
mtime,
}
})
}
#[derive(Debug, Clone)]
struct LongLinkParams {
path: String,
target: String,
uid: u64,
gid: u64,
mtime: u64,
}
fn long_link_params_strategy() -> impl Strategy<Value = LongLinkParams> {
(
path_strategy(), long_link_strategy(), id_strategy(),
id_strategy(),
mtime_strategy(),
)
.prop_map(|(path, target, uid, gid, mtime)| {
LongLinkParams {
path,
target,
uid,
gid,
mtime,
}
})
}
fn extract_all_headers(tar_data: &[u8]) -> Vec<Header> {
let mut archive = tar::Archive::new(std::io::Cursor::new(tar_data));
archive
.entries()
.expect("tar entries")
.raw(true)
.map(|e| {
let e = e.expect("tar entry");
*Header::from_bytes(e.header().as_bytes())
})
.collect()
}
fn build_long_path_with_tar_rs(params: &LongPathFileParams) -> Vec<u8> {
let mut builder = tar::Builder::new(Vec::new());
let mut header = tar::Header::new_gnu();
header.set_mode(params.mode);
header.set_uid(params.uid);
header.set_gid(params.gid);
header.set_size(0);
header.set_mtime(params.mtime);
header.set_entry_type(tar::EntryType::Regular);
builder
.append_data(&mut header, ¶ms.path, std::io::empty())
.unwrap();
builder.into_inner().unwrap()
}
fn build_long_path_with_tar_core(params: &LongPathFileParams) -> Vec<Header> {
let mut builder = EntryBuilder::new_gnu();
builder
.path(params.path.as_bytes())
.mode(params.mode)
.unwrap()
.uid(params.uid)
.unwrap()
.gid(params.gid)
.unwrap()
.size(0)
.unwrap()
.mtime(params.mtime)
.unwrap()
.entry_type(EntryType::Regular);
builder.finish()
}
fn build_long_link_with_tar_rs(params: &LongLinkParams) -> Vec<u8> {
let mut builder = tar::Builder::new(Vec::new());
let mut header = tar::Header::new_gnu();
header.set_mode(0o777);
header.set_uid(params.uid);
header.set_gid(params.gid);
header.set_size(0);
header.set_mtime(params.mtime);
header.set_entry_type(tar::EntryType::Symlink);
builder
.append_link(&mut header, ¶ms.path, ¶ms.target)
.unwrap();
builder.into_inner().unwrap()
}
fn build_long_link_with_tar_core(params: &LongLinkParams) -> Vec<Header> {
let mut builder = EntryBuilder::new_gnu();
builder
.path(params.path.as_bytes())
.link_name(params.target.as_bytes())
.mode(0o777)
.unwrap()
.uid(params.uid)
.unwrap()
.gid(params.gid)
.unwrap()
.size(0)
.unwrap()
.mtime(params.mtime)
.unwrap()
.entry_type(EntryType::Symlink);
builder.finish()
}
fn compare_extension_headers(our_blocks: &[Header], tar_headers: &[Header]) {
assert!(our_blocks.len() >= 2, "expected extension + main headers");
assert!(tar_headers.len() >= 2, "expected extension + main headers");
let our_ext = &our_blocks[0];
let tar_ext = &tar_headers[0];
assert_eq!(our_ext.entry_type(), tar_ext.entry_type(), "extension type");
assert_eq!(our_ext.path_bytes(), tar_ext.path_bytes(), "extension path");
assert_eq!(
our_ext.entry_size().unwrap(),
tar_ext.entry_size().unwrap(),
"extension size"
);
let our_main = our_blocks.last().unwrap();
let tar_main = tar_headers.last().unwrap();
assert_eq!(our_main.entry_type(), tar_main.entry_type(), "main type");
assert_eq!(
our_main.mode().unwrap(),
tar_main.mode().unwrap(),
"main mode"
);
assert_eq!(our_main.uid().unwrap(), tar_main.uid().unwrap(), "main uid");
assert_eq!(our_main.gid().unwrap(), tar_main.gid().unwrap(), "main gid");
assert_eq!(
our_main.mtime().unwrap(),
tar_main.mtime().unwrap(),
"main mtime"
);
}
#[test]
fn test_gnu_longname_basic() {
let params = LongPathFileParams {
path: "a/".repeat(60) + "file.txt",
mode: 0o644,
uid: 1000,
gid: 1000,
mtime: 1234567890,
};
compare_extension_headers(
&build_long_path_with_tar_core(¶ms),
&extract_all_headers(&build_long_path_with_tar_rs(¶ms)),
);
}
#[test]
fn test_gnu_longlink_basic() {
let params = LongLinkParams {
path: "mylink".to_string(),
target: "/very/long/target/".repeat(10),
uid: 1000,
gid: 1000,
mtime: 1234567890,
};
compare_extension_headers(
&build_long_link_with_tar_core(¶ms),
&extract_all_headers(&build_long_link_with_tar_rs(¶ms)),
);
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(32))]
#[test]
fn test_gnu_longname_equivalence(params in long_path_file_params_strategy()) {
compare_extension_headers(
&build_long_path_with_tar_core(¶ms),
&extract_all_headers(&build_long_path_with_tar_rs(¶ms)),
);
}
#[test]
fn test_gnu_longlink_equivalence(params in long_link_params_strategy()) {
compare_extension_headers(
&build_long_link_with_tar_core(¶ms),
&extract_all_headers(&build_long_link_with_tar_rs(¶ms)),
);
}
}
}
mod pax_extensions_equivalence {
use super::*;
use crate::builder::{EntryBuilder, PaxBuilder};
#[derive(Debug, Clone)]
struct PaxFileParams {
path: String,
mode: u32,
uid: u64,
gid: u64,
mtime: u64,
xattr_key: String,
xattr_value: String,
}
fn pax_file_params_strategy() -> impl Strategy<Value = PaxFileParams> {
(
path_strategy(),
mode_strategy(),
id_strategy(),
id_strategy(),
mtime_strategy(),
proptest::string::string_regex("SCHILY\\.xattr\\.[a-z]{1,20}")
.expect("valid regex"),
proptest::string::string_regex("[a-zA-Z0-9]{1,30}").expect("valid regex"),
)
.prop_map(
|(path, mode, uid, gid, mtime, xattr_key, xattr_value)| PaxFileParams {
path,
mode,
uid,
gid,
mtime,
xattr_key,
xattr_value,
},
)
}
fn build_pax_with_tar_rs(params: &PaxFileParams) -> Vec<u8> {
let mut builder = tar::Builder::new(Vec::new());
let mut pax_data = Vec::new();
let record =
format_pax_record(¶ms.xattr_key, params.xattr_value.as_bytes());
pax_data.extend_from_slice(record.as_bytes());
let mut pax_header = tar::Header::new_ustar();
let pax_name = format!("PaxHeaders.0/{}", params.path);
pax_header.set_path(&pax_name).unwrap();
pax_header.set_size(pax_data.len() as u64);
pax_header.set_entry_type(tar::EntryType::XHeader);
pax_header.set_mode(0o644);
pax_header.set_uid(0);
pax_header.set_gid(0);
pax_header.set_mtime(0);
pax_header.set_cksum();
builder
.append_data(&mut pax_header, &pax_name, pax_data.as_slice())
.unwrap();
let mut header = tar::Header::new_ustar();
header.set_path(¶ms.path).unwrap();
header.set_mode(params.mode);
header.set_uid(params.uid);
header.set_gid(params.gid);
header.set_size(0);
header.set_mtime(params.mtime);
header.set_entry_type(tar::EntryType::Regular);
header.set_cksum();
builder
.append_data(&mut header, ¶ms.path, std::io::empty())
.unwrap();
builder.into_inner().unwrap()
}
fn format_pax_record(key: &str, value: &[u8]) -> String {
let rest_len = 3 + key.len() + value.len();
let mut len_len = 1;
let mut max_len = 10;
while rest_len + len_len >= max_len {
len_len += 1;
max_len *= 10;
}
let len = rest_len + len_len;
format!("{} {}={}\n", len, key, String::from_utf8_lossy(value))
}
fn build_pax_with_tar_core(params: &PaxFileParams) -> Vec<Header> {
let mut builder = EntryBuilder::new_ustar();
builder
.path(params.path.as_bytes())
.mode(params.mode)
.unwrap()
.uid(params.uid)
.unwrap()
.gid(params.gid)
.unwrap()
.size(0)
.unwrap()
.mtime(params.mtime)
.unwrap()
.entry_type(EntryType::Regular)
.add_pax(¶ms.xattr_key, params.xattr_value.as_bytes());
builder.finish()
}
#[test]
fn test_pax_xattr_basic() {
let params = PaxFileParams {
path: "testfile".to_string(),
mode: 0o644,
uid: 1000,
gid: 1000,
mtime: 1234567890,
xattr_key: "SCHILY.xattr.user.test".to_string(),
xattr_value: "value1".to_string(),
};
let _tar_data = build_pax_with_tar_rs(¶ms);
let our_headers = build_pax_with_tar_core(¶ms);
assert!(our_headers.len() >= 2, "should have PAX extension");
let our_ext = &our_headers[0];
assert_eq!(our_ext.entry_type(), EntryType::XHeader);
let our_main = our_headers.last().unwrap();
assert_eq!(our_main.entry_type(), EntryType::Regular);
}
#[test]
fn test_pax_builder_record_format() {
let mut pax = PaxBuilder::new();
pax.add("SCHILY.xattr.user.test", b"hello");
let data = pax.finish();
let exts = PaxExtensions::new(&data);
let value = exts.get("SCHILY.xattr.user.test");
assert_eq!(value, Some("hello"));
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(32))]
#[test]
fn test_pax_record_roundtrip(
key in "[a-zA-Z][a-zA-Z0-9.]{1,30}",
value in "[a-zA-Z0-9]{1,50}",
) {
let mut pax = PaxBuilder::new();
pax.add(&key, value.as_bytes());
let data = pax.finish();
let exts = PaxExtensions::new(&data);
let parsed = exts.get(&key);
prop_assert_eq!(parsed, Some(value.as_str()));
}
#[test]
fn test_pax_file_equivalence(params in pax_file_params_strategy()) {
let _tar_data = build_pax_with_tar_rs(¶ms);
let our_headers = build_pax_with_tar_core(¶ms);
prop_assert!(our_headers.len() >= 2, "should have PAX extension");
let our_ext = &our_headers[0];
prop_assert_eq!(our_ext.entry_type(), EntryType::XHeader);
let our_main = our_headers.last().unwrap();
prop_assert_eq!(our_main.entry_type(), EntryType::Regular);
prop_assert_eq!(our_main.mode().unwrap(), params.mode);
prop_assert_eq!(our_main.uid().unwrap(), params.uid);
prop_assert_eq!(our_main.gid().unwrap(), params.gid);
prop_assert_eq!(our_main.mtime().unwrap(), params.mtime);
}
}
}
}
}
}
#[cfg(kani)]
mod kani_proofs {
use super::*;
#[kani::proof]
#[kani::unwind(18)]
fn check_truncate_null_panic_freedom() {
let bytes: [u8; 16] = kani::any();
let len: usize = kani::any();
kani::assume(len <= bytes.len());
let result = truncate_null(&bytes[..len]);
kani::assert(result.len() <= len, "result within bounds");
}
#[kani::proof]
fn check_entry_type_roundtrip() {
let byte: u8 = kani::any();
let entry_type = EntryType::from_byte(byte);
let back = entry_type.to_byte();
if byte == b'\0' {
kani::assert(back == b'0', "null byte canonicalizes to '0'");
} else {
kani::assert(back == byte, "non-null bytes roundtrip exactly");
}
}
#[kani::proof]
fn check_entry_type_predicates_dont_panic() {
let byte: u8 = kani::any();
let ty = EntryType::from_byte(byte);
let _ = ty.is_file();
let _ = ty.is_dir();
let _ = ty.is_symlink();
let _ = ty.is_hard_link();
let _ = ty.is_character_special();
let _ = ty.is_block_special();
let _ = ty.is_fifo();
let _ = ty.is_contiguous();
let _ = ty.is_gnu_longname();
let _ = ty.is_gnu_longlink();
let _ = ty.is_gnu_sparse();
let _ = ty.is_pax_global_extensions();
let _ = ty.is_pax_local_extensions();
}
}