Skip to main content

sit/structs/
v1.rs

1use std::{cmp::min, io};
2
3use binrw::{BinRead, BinReaderExt, binread};
4use fourcc::{FourCC, fourcc};
5use macintosh_utils::{FinderFlags, Fork, decode_string};
6
7use super::{Algorithm, Version};
8
9trait SitIdentifier {
10    fn is_v1(&self) -> bool;
11}
12
13impl SitIdentifier for FourCC {
14    fn is_v1(&self) -> bool {
15        matches!(
16            self,
17            fourcc!("SIT!")
18                | fourcc!("SITD")
19                | fourcc!("SIT2")
20                | fourcc!("SIT5")
21                // StuffIt Installer Maker 3.1
22                | fourcc!("STi3")
23                // StuffIt Installer Maker 6.5
24                | fourcc!("ST65")
25        )
26    }
27}
28
29#[binread]
30#[derive(Debug)]
31#[br(big)]
32pub struct ArchiveHeader {
33    /// File identifier, this should be 'SIT!', 'SITD', 'SIT2' or 'SIT5'.
34    ///
35    /// If it says 'Stuf' you are probably dealing with a version 5 archive instead
36    #[br(assert(file_code.is_v1()))]
37    pub file_code: FourCC,
38    /// Number of entries in the root of the archive
39    pub entry_count: u16,
40    /// total size of the archive file in bytes (including header and everything)
41    pub archive_len: u32,
42    /// Must be 'rLau' – this is in reference to Raymond Lau who created the format
43    #[br(assert(creator_code==fourcc!("rLau")), temp)]
44    creator_code: FourCC,
45    /// File format (sub-)version
46    pub version: Version,
47    /// Reserved for future use (in v1)
48    /// - if `version==1` this is set to 00 00 00 00 00 00 00
49    /// - if `version==2` this is set to XX NN NN NN NN XX XX
50    ///    where NN NN NN NN is the offset to the first root entry from the start of the file in bytes
51    pub reserved: [u8; 7],
52}
53
54impl ArchiveHeader {
55    pub fn first_entry_offset(&self) -> u64 {
56        match self.version {
57            Version::Early => 0x16,
58            Version::Later => {
59                (self.reserved[1] as u64) << 24
60                    | (self.reserved[2] as u64) << 16
61                    | (self.reserved[3] as u64) << 8
62                    | (self.reserved[4] as u64)
63            }
64            _ => 0x16,
65        }
66    }
67}
68
69#[binread]
70#[derive(Debug, Clone)]
71#[br(big, import { offset: u64 })]
72pub struct File {
73    #[br(restore_position, map(|v:u8| v & 128 != 0))]
74    // Data fork is encrypted
75    pub rsrc_encrypted: bool,
76    // let data_encryption = buffer[0] & 128 != 0;
77    // Method used to compress resource fork
78    pub rsrc_compression: Algorithm,
79    // Resource fork is encrypted
80    #[br(restore_position, map(|v:u8| v & 128 != 0))]
81    // Data fork is encrypted
82    pub data_encrypted: bool,
83    // Method used to compress data fork
84    pub data_compression: Algorithm,
85    #[br(temp)]
86    name_len: u8,
87    #[br(map(|r: [u8; 63]| decode_string(r[0..min(name_len as usize,63)].to_vec())))]
88    /// Name of the compressed file
89    pub file_name: String,
90    /// Mac OS file type
91    pub file_code: FourCC,
92    /// Mac OS file creator
93    pub creator_code: FourCC,
94    /// Attributes for file in Finder
95    pub flags: FinderFlags,
96    /// Creation date in classic Mac OS format (seconds since 1904)
97    #[br(map(macintosh_utils::date))]
98    pub created_at: chrono::DateTime<chrono::Utc>,
99    /// Modification date in classic Mac OS format (seconds since 1904)
100    #[br(map(macintosh_utils::date))]
101    pub modified_at: chrono::DateTime<chrono::Utc>,
102    /// Size of resource fork after decompression
103    pub rsrc_uncompressed_size: u32,
104    /// Size of data fork after decompression
105    pub data_uncompressed_size: u32,
106    /// Size of compressed resource fork data
107    pub rsrc_compressed_size: u32,
108    /// Size of compressed data fork data
109    pub data_compressed_size: u32,
110    /// CRC-16 checksum for decompressed resource data
111    ///
112    /// The CRC-16 configuration is `CRC-16-IBM` also known as `CRC-16-ARC` where the CRC of ASCII "123456789" is 0xbb3d.
113    pub rsrc_crc: u16,
114    /// CRC-16 checksum for decompressed data fork data
115    ///
116    /// The CRC-16 configuration is `CRC-16-IBM` also known as `CRC-16-ARC` where the CRC of ASCII "123456789" is 0xbb3d.
117    pub data_crc: u16,
118    /// Reserved for future use
119    pub reserved: [u8; 6],
120    /// CRC-16 checksum of the file header without these last two bytes
121    ///
122    /// The CRC-16 configuration is `CRC-16-IBM` also known as `CRC-16-ARC` where the CRC of ASCII "123456789" is 0xbb3d.
123    pub header_crc: u16,
124
125    #[br(calc(offset + Entry::HEADER_SIZE))]
126    pub payload_offset: u64,
127
128    #[br(ignore)]
129    /// Index of the file entry determine by counting previous file entries in depth-first search
130    /// order
131    pub index: usize,
132}
133
134impl File {
135    #[inline]
136    pub fn uncompressed_size(&self, fork: Fork) -> usize {
137        match fork {
138            Fork::Data => self.data_uncompressed_size as usize,
139            Fork::Resource => self.rsrc_uncompressed_size as usize,
140        }
141    }
142
143    #[inline]
144    pub fn compressed_size(&self, fork: Fork) -> usize {
145        match fork {
146            Fork::Data => self.data_compressed_size as usize,
147            Fork::Resource => self.rsrc_compressed_size as usize,
148        }
149    }
150
151    #[inline]
152    pub fn compression_method(&self, fork: Fork) -> Algorithm {
153        match fork {
154            Fork::Data => self.data_compression,
155            Fork::Resource => self.rsrc_compression,
156        }
157    }
158
159    #[inline]
160    pub fn checksum(&self, fork: Fork) -> u16 {
161        match fork {
162            Fork::Data => self.data_crc,
163            Fork::Resource => self.rsrc_crc,
164        }
165    }
166
167    #[inline]
168    pub fn encrypted(&self, fork: Fork) -> bool {
169        match fork {
170            Fork::Data => self.data_encrypted,
171            Fork::Resource => self.rsrc_encrypted,
172        }
173    }
174
175    #[inline]
176    pub fn offset(&self, fork: Fork) -> u64 {
177        match fork {
178            Fork::Resource => self.payload_offset,
179            Fork::Data => self.payload_offset + self.compressed_size(Fork::Resource) as u64,
180        }
181    }
182
183    pub fn uses_encryption(&self) -> bool {
184        self.encrypted(Fork::Data) || self.encrypted(Fork::Resource)
185    }
186}
187
188#[allow(unused)]
189#[binread]
190#[derive(Debug, Clone)]
191#[br(big)]
192pub struct Directory {
193    #[br(temp)]
194    flags1: u8,
195    #[br(calc(flags1 & 16 != 0))]
196    contains_encrypted_entries: bool,
197    pub flags2: u8,
198    #[br(temp)]
199    name_len: u8,
200    #[br(map(|r: [u8; 63]| decode_string(r[0..min(name_len as usize, 63)].to_vec())))]
201    /// Name of the compressed file
202    pub file_name: String,
203    /// position of  Mac OS file type and creator code in file header, unused for directories
204    garbage: [u8; 8],
205    /// Attributes for file in Finder
206    pub flags: FinderFlags,
207    /// Creation date in classic Mac OS format (seconds since 1904)
208    #[br(map(macintosh_utils::date))]
209    pub created_at: chrono::DateTime<chrono::Utc>,
210    /// Modification date in classic Mac OS format (seconds since 1904)
211    #[br(map(macintosh_utils::date))]
212    pub modified_at: chrono::DateTime<chrono::Utc>,
213}
214
215impl Directory {
216    #[inline]
217    pub fn uncompressed_size(&self, _: Fork) -> usize {
218        0
219    }
220
221    #[inline]
222    pub fn algorithm(&self, _: Fork) -> Algorithm {
223        Algorithm::None
224    }
225
226    #[inline]
227    pub fn compressed_size(&self, _: Fork) -> usize {
228        0
229    }
230
231    #[inline]
232    pub fn checksum(&self, _: Fork) -> u16 {
233        0
234    }
235
236    #[inline]
237    pub fn offset(&self, _: Fork) -> u64 {
238        0
239    }
240
241    pub fn uses_encryption(&self) -> bool {
242        self.contains_encrypted_entries
243    }
244}
245
246/// Represents an entry in the sit archive
247pub enum Entry {
248    File(File),
249    Directory(Directory),
250    DirectoryEnd,
251}
252
253impl Entry {
254    /// Fixed size of and archive entry header on disk
255    pub const HEADER_SIZE: u64 = 112;
256}
257
258impl BinRead for Entry {
259    type Args<'a> = ();
260
261    fn read_options<R: io::Read + io::Seek>(
262        reader: &mut R,
263        _endian: binrw::Endian,
264        _args: Self::Args<'_>,
265    ) -> binrw::BinResult<Self> {
266        let offset = reader.stream_position()?;
267        let mut buffer = vec![0u8; Entry::HEADER_SIZE as usize];
268        reader.read_exact(&mut buffer)?;
269
270        if (buffer[0] & 33) == 33 {
271            Ok(Entry::DirectoryEnd)
272        } else if (buffer[0] & 32) == 32 {
273            let mut cursor = io::Cursor::new(buffer);
274            Ok(Entry::Directory(cursor.read_be()?))
275        } else {
276            let mut cursor = io::Cursor::new(buffer);
277            Ok(Entry::File(
278                cursor.read_be_args(FileBinReadArgs { offset })?,
279            ))
280        }
281    }
282}
283
284impl From<File> for Entry {
285    fn from(val: File) -> Self {
286        Entry::File(val)
287    }
288}
289
290impl From<Directory> for Entry {
291    fn from(val: Directory) -> Self {
292        Entry::Directory(val)
293    }
294}