use std::io;
use binrw::{BinReaderExt, binread};
use bitflags::bitflags;
use fourcc::FourCC;
use macintosh_utils::{FinderFlags, Fork, decode_string};
use super::{Algorithm, Version};
bitflags! {
#[derive(Debug, Default)]
pub struct ArchiveFlags: u8 {
const RECEIPT = 1<<3;
const PADDING = 1<<4;
const COMMENT = 1<<5;
const FLAG_40 = 1<<6;
const ENCRYPTED = 1<<7;
}
}
macro_rules! derive_binread_bitflags {
($name: ident) => {
impl binrw::BinRead for $name {
type Args<'a> = ();
fn read_options<R: io::Read + io::Seek>(
reader: &mut R,
_endian: binrw::Endian,
_args: Self::Args<'_>,
) -> binrw::BinResult<Self> {
let flags = reader.read_be()?;
if let Some(flags) = Self::from_bits(flags) {
return Ok(flags);
}
log::warn!(
"{} has unknown bits (0x{:02x}) set, something has probably gone wrong",
stringify!($name),
!Self::all().bits() & flags
);
Ok(Self::from_bits(Self::all().bits() & flags).unwrap())
}
}
};
}
derive_binread_bitflags!(ArchiveFlags);
bitflags! {
#[derive(Debug, Default, Clone, Copy)]
pub struct EntryFlags: u8 {
const UNKNOWN1 = 1<<2;
const COMMENT = 1<<3;
const RESOURCE_FORK=1<<4;
const DIRECTORY=1<<6;
const ENCRYPTED=1<<5;
const LOCKED=1<<7;
}
}
derive_binread_bitflags!(EntryFlags);
bitflags! {
#[derive(Debug, Default, Clone, Copy)]
pub struct AdditionalFlags: u8 {
const RESOURCE_DATA=0x01;
const UNKNOWN=0x0C;
}
}
derive_binread_bitflags!(AdditionalFlags);
#[binread]
#[derive(Debug)]
#[br(big)]
pub struct Unknown {
pub unknown1: u32,
pub unknown2: u16,
pub unknown3: u16,
pub unknown4: u16,
pub unknown5: u16,
pub unknown6: u8,
pub unknown7: u8,
pub unknown8: u16,
pub unknown9: u16,
pub unknown10: u16,
}
#[binread]
#[derive(Debug)]
#[br(big)]
pub struct ArchiveHeader {
#[br(temp, assert(magic1 == *b"StuffIt (c)1997-"))]
magic1: [u8; 16],
pub copyright_year: FourCC,
#[br(temp, assert(magic2 == *b" Aladdin Systems, Inc., http://www.aladdinsys.com/StuffIt/"))]
magic2: [u8; 58],
#[br(temp)]
_newlinenewline: u16,
pub unknown: [u8; 2],
pub version: Version,
pub flags: ArchiveFlags,
pub total_archive_size: u32,
pub catalog_offset: u32,
pub entry_count: u16,
#[br(assert(catalog_offset == catalog_offset_rep), temp)]
catalog_offset_rep: u32,
pub archive_header_crc: u16,
#[br(calc(true))]
pub checksum_valid: bool,
#[br(if(flags.contains(ArchiveFlags::PADDING)))]
pub reserved: Option<[u8; 14]>,
#[br(temp, if(flags.contains(ArchiveFlags::COMMENT), 0))]
comment_length: u16,
#[br(temp, if(flags.contains(ArchiveFlags::COMMENT), 0))]
padding_length: u16,
#[br(temp, if(flags.contains(ArchiveFlags::ENCRYPTED), 5), assert(hash_length == 5))]
hash_length: u8,
#[br(if(flags.contains(ArchiveFlags::ENCRYPTED)))]
pub hash: Option<[u8; 5]>,
#[br(temp, if(flags.contains(ArchiveFlags::FLAG_40), 0))]
unknown_count: u16,
#[br(if(flags.contains(ArchiveFlags::FLAG_40)), count(unknown_count))]
pub unknown_items: Vec<Unknown>,
#[br(count(comment_length))]
pub comment: Vec<u8>,
#[br(count(padding_length))]
pub padding: Vec<u8>,
}
impl ArchiveHeader {
pub fn first_entry_offset(&self) -> u64 {
self.catalog_offset as u64
}
}
#[binread]
#[derive(Debug, Clone)]
#[br(big, import {offset: u64})]
pub struct Directory {
#[br(temp, assert(entry_id==fourcc::fourcc!(0xa5a5a5a5u32)))]
pub entry_id: FourCC,
pub version: u8,
pub unknown1: u8,
pub header_len: u16,
pub unknown2: u8,
#[br(assert(flags.contains(EntryFlags::DIRECTORY)))]
pub flags: EntryFlags,
#[br(map(macintosh_utils::date))]
pub creation_date: chrono::DateTime<chrono::Utc>,
#[br(map(macintosh_utils::date))]
pub modification_date: chrono::DateTime<chrono::Utc>,
pub previous_entry_offset: u32,
pub next_entry_offset: u32,
pub directory_entry_offset: u32,
pub file_name_len: u16,
pub header_crc: u16,
pub data_uncompressed_len: u32,
pub data_compressed_len: u32,
pub data_crc: u16,
pub unknown3: u16,
#[br(map(|v:u16| v as u32 + 1))]
pub child_count: u32,
#[br(if(data_uncompressed_len != 0xFFFFFFFF), args { version, file_name_len, flags})]
pub real_dir: Option<RealDirectory>,
#[br(calc(offset + header_len as u64 + 36))]
pub first_child_offset: u64,
}
impl Directory {
#[inline]
pub fn finder_flags(&self) -> FinderFlags {
if let Some(dir) = &self.real_dir {
dir.finder_flags
} else {
FinderFlags::default()
}
}
#[inline]
pub fn comment(&self) -> &str {
if let Some(dir) = &self.real_dir {
dir.comment.as_str()
} else {
""
}
}
#[inline]
pub fn file_name(&self) -> &str {
if let Some(dir) = &self.real_dir {
dir.file_name.as_str()
} else {
""
}
}
#[inline]
pub fn file_code(&self) -> FourCC {
if let Some(dir) = &self.real_dir {
dir.file_code
} else {
FourCC::new(0)
}
}
#[inline]
pub fn creator_code(&self) -> FourCC {
if let Some(dir) = &self.real_dir {
dir.creator_code
} else {
FourCC::new(0)
}
}
#[inline]
pub(crate) fn marks_end(&self) -> bool {
self.real_dir.is_none()
}
#[inline]
pub fn checksum(&self, fork: Fork) -> u16 {
if !self.marks_end() {
match fork {
Fork::Data => self.data_crc,
Fork::Resource => 0,
}
} else {
0
}
}
#[inline]
pub fn compressed_size(&self, fork: Fork) -> usize {
if !self.marks_end() {
match fork {
Fork::Data => 0,
Fork::Resource => 0,
}
} else {
0
}
}
#[inline]
pub fn uncompressed_size(&self, fork: Fork) -> usize {
if !self.marks_end() {
match fork {
Fork::Data => 0,
Fork::Resource => 0,
}
} else {
0
}
}
#[inline]
pub fn algorithm(&self, fork: Fork) -> Algorithm {
if !self.marks_end() {
match fork {
Fork::Data => Algorithm::None,
Fork::Resource => Algorithm::None,
}
} else {
Algorithm::None
}
}
#[inline]
pub fn offset(&self, fork: Fork) -> u64 {
match fork {
Fork::Data => self.first_child_offset + self.compressed_size(Fork::Resource) as u64,
Fork::Resource => self.first_child_offset,
}
}
pub fn uses_encryption(&self) -> bool {
self.flags.contains(EntryFlags::ENCRYPTED)
}
}
#[binread]
#[derive(Debug, Clone)]
#[br(big, import { file_name_len: u16, version: u8, flags: EntryFlags })]
pub struct RealDirectory {
#[br(count(file_name_len), map(decode_string))]
pub file_name: String,
#[br(if(flags.contains(EntryFlags::COMMENT), 0))]
pub comment_len: u16,
#[br(if(flags.contains(EntryFlags::COMMENT)))]
pub unknown3: Option<u16>,
#[br(count(comment_len), map(decode_string))]
pub comment: String,
pub unknown4: u16,
pub unknown5: u16,
pub file_code: FourCC,
pub creator_code: FourCC,
pub finder_flags: FinderFlags,
pub unknown7a: i16,
pub unknown7b: i16,
pub unknown7c: u8,
pub unknown7d: u8,
pub unknown7e: i16,
pub unknown7f: i16,
pub unknown7: [u8; 8],
#[br(if(version == 1))]
pub unknown6a: Option<u16>,
#[br(if(version == 1))]
pub unknown6b: Option<u16>,
}
#[binread]
#[derive(Debug, Clone)]
#[br(big)]
pub struct File {
#[br(temp, assert(entry_id==fourcc::fourcc!(0xa5a5a5a5u32)))]
entry_id: FourCC,
pub version: u8,
pub unknown1: u8,
pub header_len: u16,
pub unknown2: u8,
#[br(assert(!flags.contains(EntryFlags::DIRECTORY)))]
pub flags: EntryFlags,
#[br(map(macintosh_utils::date))]
pub creation_date: chrono::DateTime<chrono::Utc>,
#[br(map(macintosh_utils::date))]
pub modification_date: chrono::DateTime<chrono::Utc>,
pub previous_entry_offset: u32,
pub next_entry_offset: u32,
pub directory_entry_offset: u32,
pub file_name_len: u16,
pub header_crc: u16,
pub data_uncompressed_size: u32,
pub data_compressed_size: u32,
pub data_crc: u16,
pub unknown: u16,
pub data_compression: Algorithm,
#[br(temp)]
data_pass_len: u8,
#[br(count(if !flags.contains(EntryFlags::ENCRYPTED) { 0 } else {data_pass_len}))]
pub datakey: Vec<u8>,
#[br(count(file_name_len), map(decode_string))]
pub file_name: String,
#[br(if(flags.contains(EntryFlags::COMMENT), 0))]
pub comment_len: u16,
#[br(if(flags.contains(EntryFlags::COMMENT), 0))]
pub unknown9: u16,
#[br(count(comment_len), map(decode_string))]
pub comment: String,
pub unknown10: u8,
pub additional_flags: AdditionalFlags,
pub unknown_checksum: u16,
pub file_code: FourCC,
pub creator_code: FourCC,
pub finder_flags: FinderFlags,
#[br(temp, count(if version == 1 { 22 } else { 18 }))]
_garbage: Vec<u8>,
#[br(temp, calc(additional_flags.contains(AdditionalFlags::RESOURCE_DATA)))]
rsrc: bool,
#[br(if(rsrc, 0))]
pub rsrc_uncompressed_size: u32,
#[br(if(rsrc, 0))]
pub rsrc_compressed_size: u32,
#[br(if(rsrc, 0))]
pub rsrc_crc: u16,
#[br(if(rsrc, 0))]
pub rsrc_unknown: u16,
#[br(if(rsrc, Algorithm::None))]
pub rsrc_compression: Algorithm,
#[br(temp, if(rsrc, 0))]
res_key_len: u8,
#[br(count(if !flags.contains(EntryFlags::ENCRYPTED) { 0 } else {res_key_len}))]
pub res_key: Vec<u8>,
#[br(calc(0))]
pub(crate) payload_offset: u64,
#[br(ignore)]
pub index: usize,
}
impl File {
#[inline]
pub fn offset(&self, fork: Fork) -> u64 {
match fork {
Fork::Resource => self.payload_offset,
Fork::Data => self.payload_offset + self.compressed_size(Fork::Resource) as u64,
}
}
#[inline]
pub fn uncompressed_size(&self, fork: Fork) -> usize {
match fork {
Fork::Resource => self.rsrc_uncompressed_size as usize,
Fork::Data => self.data_uncompressed_size as usize,
}
}
#[inline]
pub fn compressed_size(&self, fork: Fork) -> usize {
match fork {
Fork::Resource => self.rsrc_compressed_size as usize,
Fork::Data => self.data_compressed_size as usize,
}
}
#[inline]
pub fn compression_method(&self, fork: Fork) -> Algorithm {
match fork {
Fork::Resource => self.rsrc_compression,
Fork::Data => self.data_compression,
}
}
#[inline]
pub fn checksum(&self, fork: Fork) -> u16 {
match fork {
Fork::Resource => self.rsrc_crc,
Fork::Data => self.data_crc,
}
}
#[inline]
pub fn encrypted(&self, fork: Fork) -> bool {
match fork {
Fork::Resource => self.flags.contains(EntryFlags::ENCRYPTED),
Fork::Data => self.flags.contains(EntryFlags::ENCRYPTED),
}
}
pub fn uses_encryption(&self) -> bool {
self.flags.contains(EntryFlags::ENCRYPTED)
}
}
#[binread]
#[derive(Debug)]
#[br(big, import {offset: u64})]
pub enum Entry {
Directory(#[br(args { offset })] Directory),
File(File),
}
impl Entry {
#[inline]
pub fn next_entry_offset(&self) -> u64 {
match self {
Entry::Directory(entry) => entry.next_entry_offset as u64,
Entry::File(entry) => entry.next_entry_offset as u64,
}
}
#[inline]
pub fn name(&self) -> &str {
match self {
Entry::Directory(directory_entry) => directory_entry.file_name(),
Entry::File(file_entry) => file_entry.file_name.as_str(),
}
}
}