Skip to main content

sit/structs/
v5.rs

1use std::io;
2
3use binrw::{BinReaderExt, binread};
4use bitflags::bitflags;
5use fourcc::FourCC;
6use macintosh_utils::{FinderFlags, Fork, decode_string};
7
8use super::{Algorithm, Version};
9
10bitflags! {
11    #[derive(Debug, Default)]
12    pub struct ArchiveFlags: u8 {
13        const RECEIPT = 1<<3;
14        const PADDING = 1<<4;
15        const COMMENT = 1<<5;
16        const FLAG_40 = 1<<6;
17        const ENCRYPTED = 1<<7;
18    }
19}
20
21macro_rules! derive_binread_bitflags {
22    ($name: ident) => {
23        impl binrw::BinRead for $name {
24            type Args<'a> = ();
25
26            fn read_options<R: io::Read + io::Seek>(
27                reader: &mut R,
28                _endian: binrw::Endian,
29                _args: Self::Args<'_>,
30            ) -> binrw::BinResult<Self> {
31                let flags = reader.read_be()?;
32                if let Some(flags) = Self::from_bits(flags) {
33                    return Ok(flags);
34                }
35
36                log::warn!(
37                    "{} has unknown bits (0x{:02x}) set, something has probably gone wrong",
38                    stringify!($name),
39                    !Self::all().bits() & flags
40                );
41
42                Ok(Self::from_bits(Self::all().bits() & flags).unwrap())
43            }
44        }
45    };
46}
47
48derive_binread_bitflags!(ArchiveFlags);
49
50bitflags! {
51    #[derive(Debug, Default, Clone, Copy)]
52    pub struct EntryFlags: u8 {
53        const UNKNOWN1 = 1<<2;
54        const COMMENT = 1<<3;
55        const RESOURCE_FORK=1<<4;
56        const DIRECTORY=1<<6;
57        const ENCRYPTED=1<<5;
58        const LOCKED=1<<7;
59    }
60}
61
62derive_binread_bitflags!(EntryFlags);
63
64bitflags! {
65    #[derive(Debug, Default, Clone, Copy)]
66    pub struct AdditionalFlags: u8 {
67        const RESOURCE_DATA=0x01;
68        const UNKNOWN=0x0C;
69    }
70}
71
72derive_binread_bitflags!(AdditionalFlags);
73
74#[binread]
75#[derive(Debug)]
76#[br(big)]
77pub struct Unknown {
78    pub unknown1: u32,
79    pub unknown2: u16,
80    pub unknown3: u16,
81    pub unknown4: u16,
82    pub unknown5: u16,
83    pub unknown6: u8,
84    pub unknown7: u8,
85    pub unknown8: u16,
86    pub unknown9: u16,
87    pub unknown10: u16,
88}
89
90#[binread]
91#[derive(Debug)]
92#[br(big)]
93/// Header at the start of the an archive
94pub struct ArchiveHeader {
95    #[br(temp, assert(magic1 == *b"StuffIt (c)1997-"))]
96    magic1: [u8; 16],
97    pub copyright_year: FourCC,
98    #[br(temp, assert(magic2 == *b" Aladdin Systems, Inc., http://www.aladdinsys.com/StuffIt/"))]
99    magic2: [u8; 58],
100    /// Two newline characters (seen as \r\n and \n\n")
101    #[br(temp)]
102    _newlinenewline: u16,
103
104    pub unknown: [u8; 2],
105    pub version: Version,
106    pub flags: ArchiveFlags,
107    pub total_archive_size: u32,
108    pub catalog_offset: u32,
109    pub entry_count: u16,
110    #[br(assert(catalog_offset == catalog_offset_rep), temp)]
111    catalog_offset_rep: u32,
112
113    pub archive_header_crc: u16,
114
115    #[br(calc(true))]
116    /// TODO: Actually check crc
117    pub checksum_valid: bool,
118
119    #[br(if(flags.contains(ArchiveFlags::PADDING)))]
120    pub reserved: Option<[u8; 14]>, // "\r¥¥Reserved¥¥\u{0}"
121
122    #[br(temp, if(flags.contains(ArchiveFlags::COMMENT), 0))]
123    comment_length: u16,
124
125    #[br(temp, if(flags.contains(ArchiveFlags::COMMENT), 0))]
126    padding_length: u16,
127
128    #[br(temp, if(flags.contains(ArchiveFlags::ENCRYPTED), 5), assert(hash_length == 5))]
129    hash_length: u8,
130
131    #[br(if(flags.contains(ArchiveFlags::ENCRYPTED)))]
132    pub hash: Option<[u8; 5]>,
133
134    #[br(temp, if(flags.contains(ArchiveFlags::FLAG_40), 0))]
135    unknown_count: u16,
136
137    #[br(if(flags.contains(ArchiveFlags::FLAG_40)), count(unknown_count))]
138    pub unknown_items: Vec<Unknown>,
139
140    #[br(count(comment_length))]
141    pub comment: Vec<u8>,
142
143    #[br(count(padding_length))]
144    pub padding: Vec<u8>,
145}
146
147impl ArchiveHeader {
148    pub fn first_entry_offset(&self) -> u64 {
149        self.catalog_offset as u64
150    }
151}
152
153#[binread]
154#[derive(Debug, Clone)]
155#[br(big, import {offset: u64})]
156/// A directory entry in v5 archives
157pub struct Directory {
158    #[br(temp, assert(entry_id==fourcc::fourcc!(0xa5a5a5a5u32)))]
159    pub entry_id: FourCC,
160    pub version: u8,
161    //#[br(assert(unknown1==0))]
162    pub unknown1: u8,
163    pub header_len: u16,
164    //#[br(assert(unknown1==0))]
165    pub unknown2: u8,
166    #[br(assert(flags.contains(EntryFlags::DIRECTORY)))]
167    pub flags: EntryFlags,
168    #[br(map(macintosh_utils::date))]
169    pub creation_date: chrono::DateTime<chrono::Utc>,
170    #[br(map(macintosh_utils::date))]
171    pub modification_date: chrono::DateTime<chrono::Utc>,
172    /// Offset of previous entry on the same level
173    pub previous_entry_offset: u32,
174    /// Offset of next entry on the same level
175    pub next_entry_offset: u32,
176    /// Offset to parent directory entry
177    pub directory_entry_offset: u32,
178    pub file_name_len: u16,
179    pub header_crc: u16,
180    pub data_uncompressed_len: u32,
181    pub data_compressed_len: u32,
182    pub data_crc: u16,
183    pub unknown3: u16,
184    #[br(map(|v:u16| v as u32 + 1))]
185    pub child_count: u32,
186    #[br(if(data_uncompressed_len != 0xFFFFFFFF), args {  version, file_name_len, flags})]
187    pub real_dir: Option<RealDirectory>,
188    #[br(calc(offset + header_len as u64 + 36))]
189    pub first_child_offset: u64,
190}
191
192impl Directory {
193    #[inline]
194    pub fn finder_flags(&self) -> FinderFlags {
195        if let Some(dir) = &self.real_dir {
196            dir.finder_flags
197        } else {
198            FinderFlags::default()
199        }
200    }
201
202    #[inline]
203    pub fn comment(&self) -> &str {
204        if let Some(dir) = &self.real_dir {
205            dir.comment.as_str()
206        } else {
207            ""
208        }
209    }
210
211    #[inline]
212    pub fn file_name(&self) -> &str {
213        if let Some(dir) = &self.real_dir {
214            dir.file_name.as_str()
215        } else {
216            ""
217        }
218    }
219
220    #[inline]
221    pub fn file_code(&self) -> FourCC {
222        if let Some(dir) = &self.real_dir {
223            dir.file_code
224        } else {
225            FourCC::new(0)
226        }
227    }
228
229    #[inline]
230    pub fn creator_code(&self) -> FourCC {
231        if let Some(dir) = &self.real_dir {
232            dir.creator_code
233        } else {
234            FourCC::new(0)
235        }
236    }
237
238    #[inline]
239    pub(crate) fn marks_end(&self) -> bool {
240        self.real_dir.is_none()
241    }
242
243    #[inline]
244    pub fn checksum(&self, fork: Fork) -> u16 {
245        if !self.marks_end() {
246            match fork {
247                Fork::Data => self.data_crc,
248                Fork::Resource => 0,
249            }
250        } else {
251            0
252        }
253    }
254
255    #[inline]
256    pub fn compressed_size(&self, fork: Fork) -> usize {
257        if !self.marks_end() {
258            match fork {
259                Fork::Data => 0,
260                Fork::Resource => 0,
261            }
262        } else {
263            0
264        }
265    }
266
267    #[inline]
268    pub fn uncompressed_size(&self, fork: Fork) -> usize {
269        if !self.marks_end() {
270            match fork {
271                Fork::Data => 0,
272                Fork::Resource => 0,
273            }
274        } else {
275            0
276        }
277    }
278
279    #[inline]
280    pub fn algorithm(&self, fork: Fork) -> Algorithm {
281        if !self.marks_end() {
282            match fork {
283                // TODO: figure out where the real algorithm is stored
284                Fork::Data => Algorithm::None,
285                Fork::Resource => Algorithm::None,
286            }
287        } else {
288            Algorithm::None
289        }
290    }
291
292    #[inline]
293    pub fn offset(&self, fork: Fork) -> u64 {
294        match fork {
295            Fork::Data => self.first_child_offset + self.compressed_size(Fork::Resource) as u64,
296            Fork::Resource => self.first_child_offset,
297        }
298    }
299
300    pub fn uses_encryption(&self) -> bool {
301        self.flags.contains(EntryFlags::ENCRYPTED)
302    }
303}
304
305#[binread]
306#[derive(Debug, Clone)]
307#[br(big, import {  file_name_len: u16,  version: u8, flags: EntryFlags })]
308pub struct RealDirectory {
309    #[br(count(file_name_len), map(decode_string))]
310    pub file_name: String,
311    #[br(if(flags.contains(EntryFlags::COMMENT), 0))]
312    pub comment_len: u16,
313    #[br(if(flags.contains(EntryFlags::COMMENT)))]
314    pub unknown3: Option<u16>,
315    #[br(count(comment_len), map(decode_string))]
316    pub comment: String,
317    pub unknown4: u16,
318    pub unknown5: u16,
319    pub file_code: FourCC,
320    pub creator_code: FourCC,
321    pub finder_flags: FinderFlags,
322    pub unknown7a: i16,
323    pub unknown7b: i16,
324
325    pub unknown7c: u8,
326    pub unknown7d: u8,
327
328    pub unknown7e: i16,
329    pub unknown7f: i16,
330    pub unknown7: [u8; 8],
331    #[br(if(version == 1))]
332    pub unknown6a: Option<u16>,
333    #[br(if(version == 1))]
334    pub unknown6b: Option<u16>,
335}
336
337#[binread]
338#[derive(Debug, Clone)]
339#[br(big)]
340/// A file entry in v5 archives
341pub struct File {
342    #[br(temp, assert(entry_id==fourcc::fourcc!(0xa5a5a5a5u32)))]
343    entry_id: FourCC,
344    pub version: u8,
345    pub unknown1: u8,
346    pub header_len: u16,
347    pub unknown2: u8,
348    #[br(assert(!flags.contains(EntryFlags::DIRECTORY)))]
349    pub flags: EntryFlags,
350    #[br(map(macintosh_utils::date))]
351    pub creation_date: chrono::DateTime<chrono::Utc>,
352    #[br(map(macintosh_utils::date))]
353    pub modification_date: chrono::DateTime<chrono::Utc>,
354    pub previous_entry_offset: u32,
355    pub next_entry_offset: u32,
356    pub directory_entry_offset: u32,
357    pub file_name_len: u16,
358    pub header_crc: u16,
359    pub data_uncompressed_size: u32,
360    pub data_compressed_size: u32,
361    pub data_crc: u16,
362    pub unknown: u16,
363    pub data_compression: Algorithm,
364    #[br(temp)]
365    data_pass_len: u8,
366    #[br(count(if !flags.contains(EntryFlags::ENCRYPTED) { 0 } else {data_pass_len}))]
367    pub datakey: Vec<u8>,
368    #[br(count(file_name_len), map(decode_string))]
369    pub file_name: String,
370    #[br(if(flags.contains(EntryFlags::COMMENT), 0))]
371    pub comment_len: u16,
372    #[br(if(flags.contains(EntryFlags::COMMENT), 0))]
373    pub unknown9: u16,
374    #[br(count(comment_len), map(decode_string))]
375    pub comment: String,
376    pub unknown10: u8,
377    pub additional_flags: AdditionalFlags,
378    // TODO: Seen this as the same value (0x92ae) across all entries in two directories in
379    // md5.5.sit
380    pub unknown_checksum: u16,
381    pub file_code: FourCC,
382    pub creator_code: FourCC,
383    pub finder_flags: FinderFlags,
384    #[br(temp, count(if version == 1 { 22 } else { 18 }))]
385    _garbage: Vec<u8>,
386
387    #[br(temp, calc(additional_flags.contains(AdditionalFlags::RESOURCE_DATA)))]
388    rsrc: bool,
389
390    #[br(if(rsrc, 0))]
391    pub rsrc_uncompressed_size: u32,
392    #[br(if(rsrc, 0))]
393    pub rsrc_compressed_size: u32,
394    #[br(if(rsrc, 0))]
395    pub rsrc_crc: u16,
396    #[br(if(rsrc, 0))]
397    pub rsrc_unknown: u16,
398    #[br(if(rsrc, Algorithm::None))]
399    pub rsrc_compression: Algorithm,
400    #[br(temp, if(rsrc, 0))]
401    res_key_len: u8,
402    #[br(count(if !flags.contains(EntryFlags::ENCRYPTED) { 0 } else {res_key_len}))]
403    pub res_key: Vec<u8>,
404
405    #[br(calc(0))]
406    /// Archive iterator will set this to the actual offset in the stream after the struct has been
407    /// read
408    pub(crate) payload_offset: u64,
409
410    #[br(ignore)]
411    /// Index of the file entry determine by counting previous file entries in depth-first search
412    /// order
413    pub index: usize,
414}
415
416impl File {
417    #[inline]
418    pub fn offset(&self, fork: Fork) -> u64 {
419        match fork {
420            Fork::Resource => self.payload_offset,
421            Fork::Data => self.payload_offset + self.compressed_size(Fork::Resource) as u64,
422        }
423    }
424
425    #[inline]
426    pub fn uncompressed_size(&self, fork: Fork) -> usize {
427        match fork {
428            Fork::Resource => self.rsrc_uncompressed_size as usize,
429            Fork::Data => self.data_uncompressed_size as usize,
430        }
431    }
432
433    #[inline]
434    pub fn compressed_size(&self, fork: Fork) -> usize {
435        match fork {
436            Fork::Resource => self.rsrc_compressed_size as usize,
437            Fork::Data => self.data_compressed_size as usize,
438        }
439    }
440
441    #[inline]
442    pub fn compression_method(&self, fork: Fork) -> Algorithm {
443        match fork {
444            Fork::Resource => self.rsrc_compression,
445            Fork::Data => self.data_compression,
446        }
447    }
448
449    #[inline]
450    pub fn checksum(&self, fork: Fork) -> u16 {
451        match fork {
452            Fork::Resource => self.rsrc_crc,
453            Fork::Data => self.data_crc,
454        }
455    }
456
457    #[inline]
458    pub fn encrypted(&self, fork: Fork) -> bool {
459        match fork {
460            Fork::Resource => self.flags.contains(EntryFlags::ENCRYPTED),
461            Fork::Data => self.flags.contains(EntryFlags::ENCRYPTED),
462        }
463    }
464
465    pub fn uses_encryption(&self) -> bool {
466        self.flags.contains(EntryFlags::ENCRYPTED)
467    }
468}
469
470#[binread]
471#[derive(Debug)]
472#[br(big, import {offset: u64})]
473pub enum Entry {
474    Directory(#[br(args { offset })] Directory),
475    File(File),
476}
477
478impl Entry {
479    #[inline]
480    pub fn next_entry_offset(&self) -> u64 {
481        match self {
482            Entry::Directory(entry) => entry.next_entry_offset as u64,
483            Entry::File(entry) => entry.next_entry_offset as u64,
484        }
485    }
486
487    #[inline]
488    pub fn name(&self) -> &str {
489        match self {
490            Entry::Directory(directory_entry) => directory_entry.file_name(),
491            Entry::File(file_entry) => file_entry.file_name.as_str(),
492        }
493    }
494}