use crate::{Error, Result};
pub(crate) const BLOCK_SIZE: usize = 64 * 1024;
pub(crate) const ENTRIES_PER_OFFSET_RECORD: usize = 16;
pub(crate) const OFFSET_RECORD_SIZE: usize = 8 + 2 * ENTRIES_PER_OFFSET_RECORD;
pub(crate) const FILE_ENTRY_SIZE: usize = 16;
pub(crate) const FOOTER_SIZE: usize = 6 * 16 + 32 + 8 + 4 + 4;
pub(crate) const MAGIC: u32 = 0x169f_52d6;
pub(crate) const VERSION_1: u32 = 0x61bf_3a01;
pub(crate) const ROOT_NAME_OFFSET: u32 = 0x7fff_ffff;
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct Section {
pub offset: u64,
pub size: u64,
}
impl Section {
pub fn end(self) -> Option<u64> {
self.offset.checked_add(self.size)
}
pub fn is_within(self, file_size: u64) -> bool {
self.end().is_some_and(|end| end <= file_size)
}
}
#[derive(Clone, Debug)]
pub(crate) struct Footer {
pub sections: [Section; 6],
pub integrity_hash: [u8; 32],
pub total_size: u64,
pub version: u32,
pub magic: u32,
}
impl Footer {
pub fn decode(bytes: &[u8; FOOTER_SIZE]) -> Self {
let mut cursor = 0;
let mut sections = [Section::default(); 6];
for section in &mut sections {
section.offset = read_u64(bytes, &mut cursor);
section.size = read_u64(bytes, &mut cursor);
}
let mut integrity_hash = [0; 32];
integrity_hash.copy_from_slice(&bytes[cursor..cursor + 32]);
cursor += 32;
let total_size = read_u64(bytes, &mut cursor);
let version = read_u32(bytes, &mut cursor);
let magic = read_u32(bytes, &mut cursor);
Self {
sections,
integrity_hash,
total_size,
version,
magic,
}
}
pub fn encode(&self, zero_hash: bool) -> [u8; FOOTER_SIZE] {
let mut bytes = [0; FOOTER_SIZE];
let mut cursor = 0;
for section in self.sections {
write_u64(&mut bytes, &mut cursor, section.offset);
write_u64(&mut bytes, &mut cursor, section.size);
}
if !zero_hash {
bytes[cursor..cursor + 32].copy_from_slice(&self.integrity_hash);
}
cursor += 32;
write_u64(&mut bytes, &mut cursor, self.total_size);
write_u32(&mut bytes, &mut cursor, self.version);
write_u32(&mut bytes, &mut cursor, self.magic);
bytes
}
}
#[derive(Clone, Debug)]
pub(crate) struct OffsetRecord {
pub base_offset: u64,
pub sizes: [u16; ENTRIES_PER_OFFSET_RECORD],
}
impl OffsetRecord {
pub fn decode(bytes: &[u8]) -> Self {
let mut cursor = 0;
let base_offset = read_u64(bytes, &mut cursor);
let mut sizes = [0; ENTRIES_PER_OFFSET_RECORD];
for size in &mut sizes {
*size = read_u16(bytes, &mut cursor);
}
Self { base_offset, sizes }
}
pub fn encode(&self) -> [u8; OFFSET_RECORD_SIZE] {
let mut bytes = [0; OFFSET_RECORD_SIZE];
let mut cursor = 0;
write_u64(&mut bytes, &mut cursor, self.base_offset);
for size in self.sizes {
write_u16(&mut bytes, &mut cursor, size);
}
bytes
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum EntryData {
File { offset: u64, size: u64 },
Directory { start: u32, count: u32 },
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct FileEntry {
pub name_offset: u32,
pub data: EntryData,
}
impl FileEntry {
pub fn decode(bytes: &[u8]) -> Self {
let mut cursor = 0;
let name_and_type = read_u32(bytes, &mut cursor);
let first = read_u32(bytes, &mut cursor);
let second = read_u32(bytes, &mut cursor);
let third = read_u32(bytes, &mut cursor);
let name_offset = name_and_type & ROOT_NAME_OFFSET;
let data = if name_and_type & 0x8000_0000 != 0 {
let offset = u64::from(first) | (u64::from(third & 0xffff) << 32);
let size = u64::from(second) | (u64::from(third & 0xffff_0000) << 16);
EntryData::File { offset, size }
} else {
EntryData::Directory {
start: first,
count: second,
}
};
Self { name_offset, data }
}
pub fn encode(self) -> Result<[u8; FILE_ENTRY_SIZE]> {
let mut bytes = [0; FILE_ENTRY_SIZE];
let mut cursor = 0;
let is_file = matches!(self.data, EntryData::File { .. });
write_u32(
&mut bytes,
&mut cursor,
self.name_offset | if is_file { 0x8000_0000 } else { 0 },
);
match self.data {
EntryData::File { offset, size } => {
if offset >= (1_u64 << 48) || size >= (1_u64 << 48) {
return Err(Error::ArchiveTooLarge);
}
write_u32(&mut bytes, &mut cursor, offset as u32);
write_u32(&mut bytes, &mut cursor, size as u32);
let high = ((offset >> 32) as u32 & 0xffff) | ((size >> 16) as u32 & 0xffff_0000);
write_u32(&mut bytes, &mut cursor, high);
}
EntryData::Directory { start, count } => {
write_u32(&mut bytes, &mut cursor, start);
write_u32(&mut bytes, &mut cursor, count);
write_u32(&mut bytes, &mut cursor, 0);
}
}
Ok(bytes)
}
}
fn read_u16(bytes: &[u8], cursor: &mut usize) -> u16 {
let value = u16::from_be_bytes(bytes[*cursor..*cursor + 2].try_into().unwrap());
*cursor += 2;
value
}
fn read_u32(bytes: &[u8], cursor: &mut usize) -> u32 {
let value = u32::from_be_bytes(bytes[*cursor..*cursor + 4].try_into().unwrap());
*cursor += 4;
value
}
fn read_u64(bytes: &[u8], cursor: &mut usize) -> u64 {
let value = u64::from_be_bytes(bytes[*cursor..*cursor + 8].try_into().unwrap());
*cursor += 8;
value
}
fn write_u16(bytes: &mut [u8], cursor: &mut usize, value: u16) {
bytes[*cursor..*cursor + 2].copy_from_slice(&value.to_be_bytes());
*cursor += 2;
}
fn write_u32(bytes: &mut [u8], cursor: &mut usize, value: u32) {
bytes[*cursor..*cursor + 4].copy_from_slice(&value.to_be_bytes());
*cursor += 4;
}
fn write_u64(bytes: &mut [u8], cursor: &mut usize, value: u64) {
bytes[*cursor..*cursor + 8].copy_from_slice(&value.to_be_bytes());
*cursor += 8;
}
pub(crate) fn path_components(path: &[u8]) -> impl Iterator<Item = &[u8]> {
path.split(|byte| matches!(byte, b'/' | b'\\'))
.filter(|component| !component.is_empty())
}
pub(crate) fn eq_name(left: &[u8], right: &[u8]) -> bool {
left.eq_ignore_ascii_case(right)
}
pub(crate) fn cmp_name(left: &[u8], right: &[u8]) -> std::cmp::Ordering {
left.iter()
.map(u8::to_ascii_lowercase)
.cmp(right.iter().map(u8::to_ascii_lowercase))
}