zarust 0.2.0

Rust implementation of the ZArchive format
Documentation
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))
}