pub const BLOCK_SIZE: u64 = 512;
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct TarEntryHeader {
pub name: String,
pub size: i64,
}
impl TarEntryHeader {
#[must_use]
pub fn parse(block: &[u8]) -> Option<Self> {
let block = block.get(..BLOCK_SIZE as usize)?;
if block.iter().all(|&byte| byte == 0) {
return None;
}
let name_field = &block[0..100];
let name_length = name_field.iter().position(|&byte| byte == 0).unwrap_or(100);
let name = String::from_utf8_lossy(&name_field[..name_length]).into_owned();
let mut size: i32 = 0;
for &byte in &block[124..136] {
match byte {
b'0'..=b'7' => {
size = size.wrapping_mul(8).wrapping_add(i32::from(byte - b'0'));
}
_ => break,
}
}
Some(Self {
name,
size: i64::from(size),
})
}
#[must_use]
pub fn occupied_bytes(&self) -> u64 {
BLOCK_SIZE + (self.size.max(0) as u64).div_ceil(BLOCK_SIZE) * BLOCK_SIZE
}
}
#[cfg(test)]
mod tests {
use super::{BLOCK_SIZE, TarEntryHeader};
fn header_block(name: &str, size_field: &[u8]) -> Vec<u8> {
let mut block = vec![0u8; BLOCK_SIZE as usize];
block[..name.len()].copy_from_slice(name.as_bytes());
block[124..124 + size_field.len()].copy_from_slice(size_field);
block
}
#[test]
fn parses_name_and_octal_size() {
let block = header_block("data00000a.tar.idx", b"00000001750\0");
let header = TarEntryHeader::parse(&block).expect("not a terminator block");
assert_eq!(header.name, "data00000a.tar.idx");
assert_eq!(header.size, 0o1750);
}
#[test]
fn zero_block_yields_none() {
assert_eq!(TarEntryHeader::parse(&vec![0u8; BLOCK_SIZE as usize]), None);
}
#[test]
fn short_blocks_yield_none_instead_of_panicking() {
assert_eq!(TarEntryHeader::parse(&[]), None);
assert_eq!(TarEntryHeader::parse(&[0x41u8; 100]), None);
assert_eq!(
TarEntryHeader::parse(&vec![0u8; BLOCK_SIZE as usize - 1]),
None
);
}
#[test]
fn size_parsing_stops_at_first_non_octal_byte() {
let block = header_block("entry", b"0017x9\0");
let header = TarEntryHeader::parse(&block).expect("header");
assert_eq!(header.size, 0o17);
let spaces = header_block("entry", b" 123\0");
assert_eq!(TarEntryHeader::parse(&spaces).expect("header").size, 0);
}
#[test]
fn oversized_octal_sizes_wrap_at_32_bits() {
let wrapped = header_block("entry", b"77777777777\0");
assert_eq!(TarEntryHeader::parse(&wrapped).expect("header").size, -1);
let wrapped_to_zero = header_block("entry", b"40000000000\0");
assert_eq!(
TarEntryHeader::parse(&wrapped_to_zero)
.expect("header")
.size,
0
);
}
#[test]
fn name_decodes_lossily() {
let mut block = vec![0u8; BLOCK_SIZE as usize];
block[0] = 0xFF;
block[1] = b'x';
let header = TarEntryHeader::parse(&block).expect("header");
assert_eq!(header.name, "\u{FFFD}x");
}
#[test]
fn occupied_bytes_includes_header_and_padding() {
let header = TarEntryHeader {
name: "x".to_owned(),
size: 1,
};
assert_eq!(header.occupied_bytes(), 1024);
let aligned = TarEntryHeader {
name: "x".to_owned(),
size: 512,
};
assert_eq!(aligned.occupied_bytes(), 1024);
let empty = TarEntryHeader {
name: "x".to_owned(),
size: 0,
};
assert_eq!(empty.occupied_bytes(), 512);
let wrapped = TarEntryHeader {
name: "x".to_owned(),
size: -1,
};
assert_eq!(wrapped.occupied_bytes(), 512);
}
}