sit-rs 0.3.0

Rust-native extraction for StuffIt Expander archive files
Documentation
use std::fmt;
use std::io;

use binrw::{BinRead, BinReaderExt};
use fourcc::FourCC;
use macintosh_utils::Fork;
use v5::EntryFlags;

/// Structures used in archive version from the early days until 4.5
pub mod v1;
/// Structures used in StuffIt 5.0 and onwards
pub mod v5;

#[derive(BinRead, Debug)]
#[br(big)]
pub enum ArchiveHeader {
    V1(v1::ArchiveHeader),
    V5(v5::ArchiveHeader),
}

impl ArchiveHeader {
    pub fn entry_count(&self) -> usize {
        match self {
            ArchiveHeader::V1(header) => header.entry_count as usize,
            ArchiveHeader::V5(header) => header.entry_count as usize,
        }
    }

    pub fn version(&self) -> Version {
        match self {
            ArchiveHeader::V1(header) => header.version,
            ArchiveHeader::V5(header) => header.version,
        }
    }

    pub fn checksum_valid(&self) -> bool {
        match self {
            ArchiveHeader::V1(_) => true,
            ArchiveHeader::V5(header) => header.checksum_valid,
        }
    }
}

#[derive(Debug, PartialEq, PartialOrd, Ord, Eq, Hash, Copy, Clone)]
pub enum Algorithm {
    None,
    RLE,
    LZW,
    Huffman,
    LZAH,
    HuffmanFixed,
    LZMW,
    LzHuffman,
    Installer,
    Arsenic,

    Unknown(u8),
}

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

    fn read_options<R: io::Read + io::Seek>(
        reader: &mut R,
        _endian: binrw::Endian,
        _args: Self::Args<'_>,
    ) -> binrw::BinResult<Self> {
        Ok(match reader.read_be::<u8>()? & !(128) {
            0u8 => Self::None,
            1u8 => Self::RLE,
            2u8 => Self::LZW,
            3u8 => Self::Huffman,
            5u8 => Self::LZAH,
            6u8 => Self::HuffmanFixed,
            8u8 => Self::LZMW,
            13u8 => Self::LzHuffman,
            14u8 => Self::Installer,
            15u8 => Self::Arsenic,
            val => Self::Unknown(val),
        })
    }
}
impl fmt::Display for Algorithm {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Algorithm::RLE => "RLE".fmt(f),
            Algorithm::None => "None".fmt(f),
            Algorithm::LZW => "LZW".fmt(f),
            Algorithm::Huffman => "Huff".fmt(f),
            Algorithm::LZAH => "LZAH".fmt(f),
            Algorithm::HuffmanFixed => "HuFi".fmt(f),
            Algorithm::LZMW => "LZMW".fmt(f),
            Algorithm::LzHuffman => "LzHu".fmt(f),
            Algorithm::Installer => "Inst".fmt(f),
            Algorithm::Arsenic => "Ars".fmt(f),
            Algorithm::Unknown(m) => format!("Unknown {m}").fmt(f),
        }
    }
}

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

#[derive(Debug, Clone)]
pub enum File {
    V1(v1::File),
    V5(v5::File),
}

impl File {
    pub fn name(&self) -> &str {
        match self {
            File::V1(file) => file.file_name.as_str(),
            File::V5(file) => file.file_name.as_str(),
        }
    }

    pub fn creator(&self) -> FourCC {
        match self {
            File::V1(file) => file.creator_code,
            File::V5(file) => file.creator_code,
        }
    }

    pub fn file_code(&self) -> FourCC {
        match self {
            File::V1(file) => file.file_code,
            File::V5(file) => file.file_code,
        }
    }

    pub fn comment(&self) -> &str {
        match self {
            File::V1(_) => "",
            File::V5(file) => file.comment.as_str(),
        }
    }

    pub fn uses_encryption(&self) -> bool {
        match self {
            File::V1(file) => file.uses_encryption(),
            File::V5(file) => file.uses_encryption(),
        }
    }

    #[inline]
    pub fn has(&self, fork: Fork) -> bool {
        match self {
            File::V1(file) => file.compressed_size(fork) != 0,
            File::V5(file) => file.compressed_size(fork) != 0,
        }
    }

    #[inline]
    pub fn uncompressed_size(&self, fork: Fork) -> usize {
        match self {
            File::V1(file) => file.uncompressed_size(fork),
            File::V5(file) => file.uncompressed_size(fork),
        }
    }

    #[inline]
    pub fn compressed_size(&self, fork: Fork) -> usize {
        match self {
            File::V1(file) => file.compressed_size(fork),
            File::V5(file) => file.compressed_size(fork),
        }
    }

    #[inline]
    pub fn compression_method(&self, fork: Fork) -> Algorithm {
        match self {
            File::V1(file) => file.compression_method(fork),
            File::V5(file) => file.compression_method(fork),
        }
    }

    #[inline]
    pub fn encrypted(&self, fork: Fork) -> bool {
        match self {
            File::V1(file) => file.encrypted(fork),
            File::V5(file) => file.encrypted(fork),
        }
    }

    #[inline]
    pub fn checksum(&self, fork: Fork) -> u16 {
        match self {
            File::V1(file) => file.checksum(fork),
            File::V5(file) => file.checksum(fork),
        }
    }

    #[inline]
    pub fn offset(&self, fork: Fork) -> u64 {
        match self {
            File::V1(file) => file.offset(fork),
            File::V5(file) => file.offset(fork),
        }
    }

    pub fn created_at(&self) -> chrono::DateTime<chrono::Utc> {
        // TODO: use same name for both properties
        match self {
            File::V1(file) => file.created_at,
            File::V5(file) => file.creation_date,
        }
    }

    pub fn modified_at(&self) -> chrono::DateTime<chrono::Utc> {
        // TODO: use same name for both properties
        match self {
            File::V1(file) => file.modified_at,
            File::V5(file) => file.modification_date,
        }
    }

    pub fn index(&self) -> usize {
        match self {
            File::V1(file) => file.index,
            File::V5(file) => file.index,
        }
    }
}

#[derive(Debug, Clone)]
pub enum Directory {
    V1(v1::Directory),
    V5(v5::Directory),
}

impl Directory {
    #[inline]
    pub fn name(&self) -> &str {
        match self {
            Directory::V1(dir) => &dir.file_name,
            Directory::V5(dir) => dir.file_name(),
        }
    }

    #[inline]
    pub fn encrypted(&self, _: Fork) -> bool {
        match self {
            Directory::V1(_) => false,
            Directory::V5(dir) => dir.flags.contains(EntryFlags::ENCRYPTED),
        }
    }

    #[inline]
    pub fn algorithm(&self, fork: Fork) -> Algorithm {
        match self {
            Directory::V1(dir) => dir.algorithm(fork),
            Directory::V5(dir) => dir.algorithm(fork),
        }
    }

    #[inline]
    pub fn uncompressed_size(&self, fork: Fork) -> usize {
        match self {
            Directory::V1(dir) => dir.uncompressed_size(fork),
            Directory::V5(dir) => dir.uncompressed_size(fork),
        }
    }

    #[inline]
    pub fn compressed_size(&self, fork: Fork) -> usize {
        match self {
            Directory::V1(dir) => dir.compressed_size(fork),
            Directory::V5(dir) => dir.compressed_size(fork),
        }
    }

    #[inline]
    pub fn offset(&self, fork: Fork) -> u64 {
        match self {
            Directory::V1(dir) => dir.offset(fork),
            Directory::V5(dir) => dir.offset(fork),
        }
    }

    #[inline]
    pub fn checksum(&self, fork: Fork) -> u16 {
        match self {
            Directory::V1(dir) => dir.checksum(fork),
            Directory::V5(dir) => dir.checksum(fork),
        }
    }

    #[inline]
    pub fn has(&self, fork: Fork) -> bool {
        match self {
            Directory::V1(dir) => dir.compressed_size(fork) != 0,
            Directory::V5(dir) => dir.compressed_size(fork) != 0,
        }
    }

    pub fn comment(&self) -> &str {
        match self {
            Directory::V1(_) => "", // pre-5 versions don't support comments
            Directory::V5(dir) => dir.comment(),
        }
    }

    pub fn uses_encryption(&self) -> bool {
        match self {
            Directory::V1(dir) => dir.uses_encryption(),
            Directory::V5(dir) => dir.uses_encryption(),
        }
    }
}

/// Version of an archive file
#[derive(BinRead, Debug, PartialEq, PartialOrd, Copy, Clone)]
#[br(big)]
pub enum Version {
    /// Version used by StuffIt 1.5.x and earlier
    #[br(magic(1u8))]
    Early,
    /// Version used by StuffIt 1.6 to 4.5
    #[br(magic(2u8))]
    Later,
    /// Version used by StuffIt 5.x
    #[br(magic(5u8))]
    Five,
    /// Unknown versions
    Unknown(u8),
}

impl Version {
    pub fn short_str(&self) -> &'static str {
        match self {
            Version::Early => "1",
            Version::Later => "2",
            Version::Five => "5",
            Version::Unknown(_) => "x",
        }
    }
}

impl fmt::Display for Version {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Version::Early => f.write_str("1.5.x and earlier"),
            Version::Later => f.write_str("1.6 to 4.5"),
            Version::Five => f.write_str("5.x"),
            Version::Unknown(v) => f.write_fmt(format_args!("Unknown {v}")),
        }
    }
}