Skip to main content

sit/
archive.rs

1use std::{
2    fmt::Debug,
3    fs,
4    io::{self},
5    path::{self},
6    rc::Rc,
7};
8
9use binrw::BinReaderExt;
10use macintosh_utils::Fork;
11
12use crate::{
13    Entry, Error, VerifyingEntryReader,
14    structs::{
15        Algorithm, ArchiveHeader, File, Version, v1,
16        v5::{self, EntryBinReadArgs},
17    },
18    verify::VerifyingIterator,
19};
20
21pub struct EntryReader<'a, T: io::Read + io::Seek>(crate::algos::EntryReader<'a, T>, u16);
22
23impl<'a, T: io::Read + io::Seek> EntryReader<'a, T> {
24    #[inline]
25    pub(crate) fn try_from(
26        reader: &'a mut Rc<T>,
27        algo: Algorithm,
28        uncompressed_size: u64,
29        compressed_size: usize,
30        offset: u64,
31        checksum: u16,
32    ) -> Result<Self, Error> {
33        Ok(Self(
34            crate::algos::EntryReader::try_from(
35                reader,
36                algo,
37                uncompressed_size,
38                compressed_size,
39                offset,
40            )?,
41            checksum,
42        ))
43    }
44
45    pub fn verifying(self) -> VerifyingEntryReader<'a, T> {
46        self.0.verifying(self.1)
47    }
48
49    pub fn verify(self) -> Result<(), Error> {
50        // Arsenic compression uses 32-bit checksums interleaved with the compressed blocks
51        // so it get's special treatment
52        let is_arsenic = matches!(self.0, crate::algos::EntryReader::Arsenic { .. });
53
54        VerifyingEntryReader::new(self.0, self.1, is_arsenic).slurp()
55    }
56}
57
58impl<'a, T: io::Read + io::Seek> io::Read for EntryReader<'a, T> {
59    #[inline]
60    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
61        self.0.read(buf)
62    }
63}
64
65impl<'a, T: io::Read + io::Seek> io::Seek for EntryReader<'a, T> {
66    #[inline]
67    fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
68        self.0.seek(pos)
69    }
70
71    #[inline]
72    fn stream_len(&mut self) -> io::Result<u64> {
73        self.0.stream_len()
74    }
75
76    #[inline]
77    fn stream_position(&mut self) -> io::Result<u64> {
78        self.0.stream_position()
79    }
80}
81
82#[derive(Debug)]
83pub struct Archive<R> {
84    header_location: u64,
85    header: ArchiveHeader,
86    inner: Rc<R>,
87}
88
89impl Archive<fs::File> {
90    pub fn open_path<P: AsRef<path::Path>>(p: P) -> Result<Self, Error> {
91        Archive::try_from(fs::File::open(p.as_ref())?)
92    }
93}
94
95impl<R: io::Seek + io::Read> Archive<R> {
96    pub fn try_from(mut inner: R) -> Result<Self, Error> {
97        let header_location = inner.stream_position()?;
98
99        match inner.read_be() {
100            Ok(header) => Ok(Self {
101                inner: Rc::new(inner),
102                header_location,
103                header,
104            }),
105            Err(binrw::Error::AssertFail { .. }) => Err(Error::InvalidFile(
106                crate::error::InvalidFileReason::InvalidHeader,
107            )),
108            Err(e) => Err(e.into()),
109        }
110    }
111
112    pub fn version(&self) -> Version {
113        self.header.version()
114    }
115
116    pub fn header(&self) -> &ArchiveHeader {
117        &self.header
118    }
119
120    /// Verify the archive's integrity by validating all checksums
121    pub fn verify(&mut self) -> Result<(), Error> {
122        if !self.header.checksum_valid() {
123            return Err(Error::ChecksumMismatch(
124                crate::error::ChecksumLocation::ArchiveHeader,
125            ));
126        }
127
128        self.iter().try_for_each(|e| {
129            if let Entry::Directory(crate::structs::Directory::V5(dir)) = &e
130                && dir.marks_end()
131            {
132                return Ok(());
133            }
134
135            if !e.is_file() {
136                return Ok(());
137            }
138
139            if e.has(Fork::Resource) {
140                self.open(&e, Fork::Resource)?.verifying().slurp()?;
141            }
142
143            if e.has(Fork::Data) {
144                self.open(&e, Fork::Data)?.verifying().slurp()?;
145            }
146
147            Ok(())
148        })
149    }
150
151    /// Open an archive entry for reading
152    pub fn open<'a, E: ReadableEntry>(
153        &'a mut self,
154        entry: &E,
155        fork: Fork,
156    ) -> Result<EntryReader<'a, R>, Error> {
157        let algo = entry.algorithm(fork);
158        let compressed_size = entry.compressed_size(fork);
159        let uncompressed_size = entry.uncompressed_size(fork);
160        let offset = entry.offset(fork);
161        let checksum = entry.checksum(fork);
162
163        if entry.encrypted(fork) {
164            return Err(Error::UnsupportedFeature(
165                crate::error::UnsupportedFeature::Encryption,
166            ));
167        }
168
169        EntryReader::try_from(
170            &mut self.inner,
171            algo,
172            uncompressed_size as u64,
173            compressed_size,
174            offset,
175            checksum,
176        )
177    }
178
179    /// Iterate thorugh all archive entries
180    pub fn iter(&self) -> EntryIterator<R> {
181        self.ensure_not_iterating();
182
183        // SAFETY: Archives can only be iterated once, and opening an entry locks the archive until
184        // the handle is released. That ensures we don't modify the underlying reader concurrently
185        let catalog_offset = match &self.header {
186            ArchiveHeader::V1(hdr) => self.header_location + hdr.first_entry_offset(),
187            ArchiveHeader::V5(hdr) => self.header_location + hdr.first_entry_offset(),
188        };
189
190        EntryIterator::new(
191            self.inner.clone(),
192            self.header.entry_count(),
193            catalog_offset,
194            matches!(self.header, ArchiveHeader::V1(_)),
195        )
196    }
197
198    /// Returns the underlying reader
199    pub fn into_inner(self) -> R {
200        self.ensure_not_iterating();
201
202        // SAFETY: Since no one is iterating, and any open entry would borrow the archive mutably
203        // this is safe
204        let Archive { inner, .. } = self;
205        unsafe { Rc::try_unwrap(inner).unwrap_unchecked() }
206    }
207
208    pub fn reset(&mut self) -> Result<(), Error> {
209        self.ensure_not_iterating();
210
211        // SAFETY: Since no one is iterating, and any open entry would borrow the archive mutably,
212        // this is safe
213        unsafe {
214            Rc::get_mut_unchecked(&mut self.inner)
215                .seek(io::SeekFrom::Start(self.header_location))?;
216        }
217
218        Ok(())
219    }
220
221    fn ensure_not_iterating(&self) {
222        if Rc::strong_count(&self.inner) != 1 || Rc::weak_count(&self.inner) != 0 {
223            panic!("Can not modify archive while an iterator is runnning")
224        }
225    }
226}
227
228/// Trait implemented by archive entries to create a decompression stream
229pub trait ReadableEntry {
230    /// Algorithm used to compress the entry's data or resource fork
231    fn algorithm(&self, fork: Fork) -> Algorithm;
232    /// Amount of bytes occupied by the specified fork
233    fn compressed_size(&self, fork: Fork) -> usize;
234    /// Size of the fork in bytes after decompression
235    fn uncompressed_size(&self, fork: Fork) -> usize;
236    fn encrypted(&self, fork: Fork) -> bool;
237    /// Offset from start of the archive header to beginning of the compressed data
238    fn offset(&self, fork: Fork) -> u64;
239    /// Checksum of the uncompressed data
240    fn checksum(&self, fork: Fork) -> u16;
241}
242
243impl ReadableEntry for Entry {
244    fn encrypted(&self, fork: Fork) -> bool {
245        match self {
246            Entry::File(e) => e.encrypted(fork),
247            Entry::Directory(_) => false,
248            Entry::DirectoryEnd(_) => false,
249        }
250    }
251
252    fn algorithm(&self, fork: Fork) -> Algorithm {
253        match self {
254            Entry::File(e) => e.algorithm(fork),
255            Entry::Directory(e) => e.algorithm(fork),
256            Entry::DirectoryEnd(_) => Algorithm::None,
257        }
258    }
259
260    fn compressed_size(&self, fork: Fork) -> usize {
261        match self {
262            Entry::File(e) => e.compressed_size(fork),
263            Entry::Directory(e) => e.compressed_size(fork),
264            Entry::DirectoryEnd(_) => 0,
265        }
266    }
267
268    fn uncompressed_size(&self, fork: Fork) -> usize {
269        match self {
270            Entry::File(e) => e.uncompressed_size(fork),
271            Entry::Directory(e) => e.uncompressed_size(fork),
272            Entry::DirectoryEnd(_) => 0,
273        }
274    }
275
276    fn offset(&self, fork: Fork) -> u64 {
277        match self {
278            Entry::File(e) => e.offset(fork),
279            Entry::Directory(e) => e.offset(fork),
280            Entry::DirectoryEnd(off) => *off,
281        }
282    }
283
284    fn checksum(&self, fork: Fork) -> u16 {
285        match self {
286            Entry::File(file) => file.checksum(fork),
287            Entry::Directory(_) => 0,
288            Entry::DirectoryEnd(_) => 0,
289        }
290    }
291}
292
293impl ReadableEntry for File {
294    #[inline]
295    fn algorithm(&self, fork: Fork) -> Algorithm {
296        File::compression_method(self, fork)
297    }
298
299    #[inline]
300    fn compressed_size(&self, fork: Fork) -> usize {
301        File::compressed_size(self, fork)
302    }
303
304    #[inline]
305    fn uncompressed_size(&self, fork: Fork) -> usize {
306        File::uncompressed_size(self, fork)
307    }
308
309    #[inline]
310    fn encrypted(&self, fork: Fork) -> bool {
311        File::encrypted(self, fork)
312    }
313
314    #[inline]
315    fn offset(&self, fork: Fork) -> u64 {
316        File::offset(self, fork)
317    }
318
319    #[inline]
320    fn checksum(&self, fork: Fork) -> u16 {
321        File::checksum(self, fork)
322    }
323}
324
325impl ReadableEntry for v5::File {
326    #[inline]
327    fn algorithm(&self, fork: Fork) -> Algorithm {
328        v5::File::compression_method(self, fork)
329    }
330
331    #[inline]
332    fn compressed_size(&self, fork: Fork) -> usize {
333        v5::File::compressed_size(self, fork)
334    }
335
336    #[inline]
337    fn uncompressed_size(&self, fork: Fork) -> usize {
338        v5::File::uncompressed_size(self, fork)
339    }
340
341    #[inline]
342    fn encrypted(&self, fork: Fork) -> bool {
343        v5::File::encrypted(self, fork)
344    }
345
346    #[inline]
347    fn offset(&self, fork: Fork) -> u64 {
348        v5::File::offset(self, fork)
349    }
350
351    #[inline]
352    fn checksum(&self, fork: Fork) -> u16 {
353        v5::File::checksum(self, fork)
354    }
355}
356
357impl ReadableEntry for v1::File {
358    #[inline]
359    fn algorithm(&self, fork: Fork) -> Algorithm {
360        v1::File::compression_method(self, fork)
361    }
362
363    #[inline]
364    fn compressed_size(&self, fork: Fork) -> usize {
365        v1::File::compressed_size(self, fork)
366    }
367
368    #[inline]
369    fn uncompressed_size(&self, fork: Fork) -> usize {
370        v1::File::uncompressed_size(self, fork)
371    }
372
373    #[inline]
374    fn encrypted(&self, fork: Fork) -> bool {
375        v1::File::encrypted(self, fork)
376    }
377
378    #[inline]
379    fn offset(&self, fork: Fork) -> u64 {
380        v1::File::offset(self, fork)
381    }
382
383    #[inline]
384    fn checksum(&self, fork: Fork) -> u16 {
385        v1::File::checksum(self, fork)
386    }
387}
388
389pub struct EntryIterator<R: io::Read + io::Seek> {
390    next_offset: u64,
391    next_file_index: usize,
392    stack: Vec<u32>,
393    v1: bool,
394
395    pub(crate) reader: Rc<R>,
396}
397
398impl<R: io::Read + io::Seek> EntryIterator<R> {
399    fn new(reader: Rc<R>, entry_count: usize, offset: u64, v1: bool) -> Self {
400        Self {
401            next_offset: offset,
402            next_file_index: 0,
403            stack: vec![entry_count as u32],
404            reader,
405            v1,
406        }
407    }
408
409    pub fn verifying(self) -> VerifyingIterator<R> {
410        let Self {
411            next_offset,
412            stack,
413            reader,
414            v1,
415            next_file_index,
416        } = self;
417
418        VerifyingIterator {
419            next_offset,
420            stack,
421            reader,
422            v1,
423            next_file_index,
424        }
425    }
426}
427
428impl<R: io::Read + io::Seek> Iterator for EntryIterator<R> {
429    type Item = Entry;
430
431    fn next(&mut self) -> Option<Self::Item> {
432        let reader = unsafe { Rc::get_mut_unchecked(&mut self.reader) };
433
434        match self.stack.last_mut() {
435            None => return None,
436            Some(0) => {
437                self.stack.pop();
438
439                // Don't send directory end marker for the root catalog
440                return if self.stack.is_empty() {
441                    None
442                } else {
443                    Some(Entry::DirectoryEnd(self.next_offset))
444                };
445            }
446            Some(d) => *d -= 1,
447        }
448
449        let Ok(entry_offset) = reader.seek(io::SeekFrom::Start(self.next_offset)) else {
450            log::warn!("Failed seeking to next archive entry");
451            return None;
452        };
453
454        if self.v1 {
455            let Ok(entry) = reader.read_be::<v1::Entry>() else {
456                return None;
457            };
458
459            let Ok(payload_offset) = reader.stream_position() else {
460                return None;
461            };
462
463            match entry {
464                v1::Entry::Directory(dir) => {
465                    self.next_offset = payload_offset;
466                    self.stack.push(u32::MAX);
467
468                    Some(Entry::Directory(dir.into()))
469                }
470                v1::Entry::DirectoryEnd => {
471                    self.next_offset = payload_offset;
472                    self.stack.pop();
473
474                    Some(Entry::DirectoryEnd(payload_offset))
475                }
476                v1::Entry::File(mut file) => {
477                    self.next_offset = payload_offset
478                        + file.data_compressed_size as u64
479                        + file.rsrc_compressed_size as u64;
480                    log::debug!(
481                        "Entry: {}, {:?} {} 0x{:04x}",
482                        file.file_name,
483                        file.data_compression,
484                        file.data_compressed_size,
485                        file.checksum(Fork::Data)
486                    );
487
488                    file.index = self.next_file_index;
489                    self.next_file_index += 1;
490
491                    Some(Entry::File(file.into()))
492                }
493            }
494        } else {
495            let Ok(entry) = reader.read_be_args::<v5::Entry>(
496                EntryBinReadArgs::builder().offset(entry_offset).finalize(),
497            ) else {
498                return None;
499            };
500
501            let Ok(payload_offset) = reader.stream_position() else {
502                return None;
503            };
504
505            match entry {
506                v5::Entry::Directory(dir) => {
507                    if dir.marks_end() {
508                        self.next_offset = payload_offset;
509                        return self.next();
510                    }
511
512                    self.stack.push(dir.child_count);
513                    self.next_offset = dir.first_child_offset;
514
515                    Some(Entry::Directory(dir.into()))
516                }
517                v5::Entry::File(mut file) => {
518                    self.next_offset = file.next_entry_offset as u64;
519                    file.payload_offset = payload_offset;
520
521                    file.index = self.next_file_index;
522                    self.next_file_index += 1;
523
524                    Some(Entry::File(file.into()))
525                }
526            }
527        }
528    }
529}