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)]
93pub 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 #[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 pub checksum_valid: bool,
118
119 #[br(if(flags.contains(ArchiveFlags::PADDING)))]
120 pub reserved: Option<[u8; 14]>, #[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})]
156pub struct Directory {
158 #[br(temp, assert(entry_id==fourcc::fourcc!(0xa5a5a5a5u32)))]
159 pub entry_id: FourCC,
160 pub version: u8,
161 pub unknown1: u8,
163 pub header_len: u16,
164 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 pub previous_entry_offset: u32,
174 pub next_entry_offset: u32,
176 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 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)]
340pub 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 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 pub(crate) payload_offset: u64,
409
410 #[br(ignore)]
411 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}