compact-pro 0.1.0

Read compressed files created by Compact Pro
Documentation
use binrw::{binread, BinRead, BinReaderExt};
use bitflags::bitflags;
use macintosh_utils::{chrono, decode_string, FinderFlags, Fork, FourCC};

use crate::entry_reader::{CompressionMethod, EntrySpecification};

#[derive(BinRead, Debug)]
#[br(big)]
pub struct ArchiveHeader {
    /// File identifier, 0x01
    pub magic: u8,
    /// Volume number (meaning not entirely clear, is 0x01 for single-volume archives)
    pub volume: u8,
    /// Cross-volume magic number (meaning not entirely clear)
    pub cross_volume_magic: u16,
    /// Offset to file headers from beginning of file
    pub header_offset: u32,
}

impl ArchiveHeader {
    /// Total size of archive header on the wire
    pub const PACKED_SIZE: usize = 8;
}

#[binread]
#[derive(Debug)]
#[br(big)]
pub struct CatalogHeader {
    /// CRC-32 of the header
    pub header_checksum: u32,
    /// Total number of files and directories
    pub entry_count: u16,
    /// Archive comment
    #[br(map(macintosh_utils::string))]
    pub comment: String,
}

#[derive(Debug, Clone)]
pub enum Entry {
    File(File),
    Directory(Directory),
}

impl Entry {
    pub fn name(&self) -> &str {
        match self {
            Entry::File(file_header) => &file_header.name,
            Entry::Directory(dir_header) => &dir_header.name,
        }
    }

    pub fn is_file(&self) -> bool {
        matches!(self, Entry::File(_))
    }

    pub fn is_directory(&self) -> bool {
        matches!(self, Entry::Directory(_))
    }

    pub fn as_file(&self) -> Option<&File> {
        match self {
            Entry::File(file) => Some(file),
            Entry::Directory(_) => None,
        }
    }

    pub fn as_directory(&self) -> Option<&Directory> {
        match self {
            Entry::File(_) => None,
            Entry::Directory(directory) => Some(directory),
        }
    }

    pub(crate) fn spec(&self, fork: Fork) -> EntrySpecification {
        match self {
            Entry::File(file) => file.spec(fork),
            Entry::Directory(_) => EntrySpecification::default(),
        }
    }
}

impl BinRead for Entry {
    type Args<'a> = ();

    fn read_options<R: std::io::Read + std::io::Seek>(
        reader: &mut R,
        _: binrw::Endian,
        _: Self::Args<'_>,
    ) -> binrw::BinResult<Self> {
        let name_len_and_type: u8 = reader.read_be()?;
        let is_directory = (name_len_and_type & 0x80) != 0;
        let name_len = name_len_and_type & !0x80;

        if is_directory {
            Ok(Entry::Directory(reader.read_be_args((name_len,))?))
        } else {
            Ok(Entry::File(reader.read_be_args((name_len,))?))
        }
    }
}

#[derive(BinRead, Debug, Clone)]
#[br(import(name_len: u8), big)]
pub struct File {
    #[br(count(name_len), map(decode_string))]
    pub name: String,
    /// Volume number (meaning not entirely clear)
    pub volume: u8,
    /// Offset to file data from beginning of file
    pub offset: u32,
    /// Mac OS file type
    pub file_code: FourCC,
    /// Mac OS file creator
    pub creator_code: FourCC,
    /// Creation date in classic Mac OS format (seconds since 1904)
    #[br(map(macintosh_utils::date))]
    pub created_at: chrono::DateTime<chrono::Utc>,
    /// Modification date in classic Mac OS format (seconds since 1904)
    #[br(map(macintosh_utils::date))]
    pub modified_at: chrono::DateTime<chrono::Utc>,
    /// Mac OS Finder flags
    pub finder_flags: FinderFlags,
    /// Uncompressed file data CRC. This is calculated for the concatenation of the resource and data forks.
    pub crc32: u32,
    /// File flags
    #[br(map(|v: u16| Flags::from_bits_retain(v)))]
    pub flags: Flags,
    /// Resource fork uncompressed length
    pub rsrc_uncompressed_size: u32,
    /// Data fork uncompressed length
    pub data_uncompressed_size: u32,
    /// Resource fork compressed length
    pub rsrc_compressed_size: u32,
    /// Data fork compressed length
    pub data_compressed_size: u32,
}

impl File {
    pub(crate) fn spec(&self, fork: Fork) -> EntrySpecification {
        let method = if fork.is_data() {
            if self.flags.contains(Flags::DATA_LZH_COMPRESSED) {
                CompressionMethod::Lzh
            } else {
                CompressionMethod::Rle
            }
        } else if self.flags.contains(Flags::RSRC_LZH_COMPRESSED) {
            CompressionMethod::Lzh
        } else {
            CompressionMethod::Rle
        };

        match fork {
            Fork::Data => EntrySpecification {
                method,
                uncompressed_len: self.data_uncompressed_size as usize,
                compressed_len: self.data_compressed_size as usize,
                offset: self.offset as u64 + self.rsrc_compressed_size as u64,
            },
            Fork::Resource => EntrySpecification {
                method,
                uncompressed_len: self.rsrc_uncompressed_size as usize,
                compressed_len: self.rsrc_compressed_size as usize,
                offset: self.offset as u64,
            },
        }
    }
}

bitflags! {
    #[derive(Debug, Clone)]
    pub struct Flags: u16 {
        const ENCRYPTED = 1<<0;
        const RSRC_LZH_COMPRESSED = 1<<1;
        const DATA_LZH_COMPRESSED = 1<<2;
    }
}

#[derive(BinRead, Debug, Clone)]
#[br(import(name_len: u8), big)]
pub struct Directory {
    #[br(count(name_len), map(decode_string))]
    pub name: String,
    pub entries: u16,
}