Skip to main content

cpt/
lib.rs

1#![feature(seek_stream_len)]
2#![feature(get_mut_unchecked)]
3use std::{
4    fs,
5    io::{self, Read as _, Seek as _},
6    path::Path,
7    rc::Rc,
8};
9
10use binrw::BinReaderExt;
11use fourcc_rs::fourcc;
12
13mod entry_iterator;
14mod entry_reader;
15mod error;
16mod lzh;
17mod rle;
18pub mod structs;
19
20use entry_reader::EntryReader;
21pub use error::Error;
22use macintosh_utils::FourCC;
23pub use macintosh_utils::{chrono, Fork};
24use structs::ArchiveHeader;
25pub use structs::{Entry, Flags};
26
27use crate::{
28    entry_iterator::EntryIterator,
29    entry_reader::EntrySpecification,
30    error::{Feature, VerificationError},
31};
32
33const CRC32: crc::Crc<u32> = crc::Crc::<u32>::new(&crc::CRC_32_JAMCRC);
34
35pub struct Archive<R> {
36    inner: Rc<R>,
37    entries_start: u64,
38}
39
40impl<R> Archive<R> {
41    pub const TYPE_CODE: FourCC = fourcc!("PACT");
42    pub const CREATOR_CODE: FourCC = fourcc!("CPCT");
43
44    /// Returns the underlying reader, not that it's read position will have changed from when you
45    /// passed it in
46    pub fn into_inner(self) -> R {
47        self.ensure_not_iterating();
48        // SAFETY: Since no one is iterating, and any open entry would borrow the archive mutably
49        // this is safe
50        let Archive { inner, .. } = self;
51        unsafe { Rc::try_unwrap(inner).unwrap_unchecked() }
52    }
53
54    fn ensure_not_iterating(&self) {
55        if Rc::strong_count(&self.inner) != 1 || Rc::weak_count(&self.inner) != 0 {
56            panic!("Can not modify archive while an iterator is runnning")
57        }
58    }
59}
60
61#[cfg(feature = "macbinary")]
62impl<A: io::Read + io::Seek> Archive<macbinary::Reader<A>> {
63    pub fn try_from(inner: A) -> Result<Self, Error> {
64        use macintosh_utils::FourCC;
65
66        use crate::structs::CatalogHeader;
67
68        let wrapper = macbinary::MacBinary::try_new(inner)?;
69        let file_code = wrapper.type_code();
70        let creator = wrapper.creator();
71        if file_code != FourCC::default() && file_code != Archive::<()>::TYPE_CODE
72            || creator != FourCC::default() && creator != Archive::<()>::CREATOR_CODE
73        {
74            return Err(Error::InvalidFile);
75        }
76
77        let mut inner = wrapper.into_data_fork()?;
78        let header: ArchiveHeader = inner.read_be()?;
79        let header_count_offset: i64 =
80            header.header_offset as i64 - ArchiveHeader::PACKED_SIZE as i64;
81        inner.seek(std::io::SeekFrom::Current(header_count_offset))?;
82        let _ = inner.read_be::<CatalogHeader>()?;
83
84        let position = inner.stream_position()?;
85
86        Ok(Self {
87            inner: Rc::new(inner),
88            entries_start: position,
89        })
90    }
91}
92
93impl<R: io::Read + io::Seek> Archive<R> {
94    #[cfg(not(feature = "macbinary"))]
95    pub fn try_from(mut inner: R) -> Result<Self, Error> {
96        let header: ArchiveHeader = inner.read_be()?;
97        let header_count_offset: i64 =
98            header.header_offset as i64 - ArchiveHeader::PACKED_SIZE as i64;
99        inner.seek(std::io::SeekFrom::Current(header_count_offset))?;
100        let header_cont = inner.read_be()?;
101
102        let position = inner.stream_position()?;
103
104        Ok(Self {
105            inner: Rc::new(inner),
106            archive_header: header,
107            catalog: header_cont,
108            entries_start: position,
109        })
110    }
111
112    pub fn iter(&self) -> Result<EntryIterator<R>, Error> {
113        self.ensure_not_iterating();
114
115        EntryIterator::try_at(self.entries_start, self.inner.clone())
116    }
117
118    pub fn verify_entry(&mut self, entry: &Entry) -> Result<(), VerificationError> {
119        let Some(file) = entry.as_file() else {
120            return Ok(());
121        };
122
123        let mut checksum = CRC32.digest();
124
125        let mut resource = vec![0u8; file.rsrc_uncompressed_size as usize];
126        let mut reader = self.open_entry(entry, Fork::Resource)?;
127        reader.read_exact(&mut resource)?;
128        checksum.update(&resource);
129
130        let mut data = vec![0u8; file.data_uncompressed_size as usize];
131        let mut reader = self.open_entry(entry, Fork::Data)?;
132        reader.read_exact(&mut data)?;
133        checksum.update(&data);
134
135        if checksum.finalize() != file.crc32 {
136            Err(VerificationError::InvalidChecksum)
137        } else {
138            Ok(())
139        }
140    }
141
142    pub fn open_entry(&mut self, entry: &Entry, fork: Fork) -> Result<EntryReader<'_, R>, Error> {
143        if entry.as_directory().is_some() {
144            return EntryReader::try_new(&mut self.inner, EntrySpecification::default());
145        }
146
147        let file = entry.as_file().unwrap();
148        if file.flags.contains(Flags::ENCRYPTED) {
149            return Err(Error::UnsupportedFeature(Feature::Encryption));
150        }
151
152        EntryReader::try_new(&mut self.inner, entry.spec(fork))
153    }
154}
155
156#[cfg(not(feature = "macbinary"))]
157impl Archive<fs::File> {
158    pub fn open(path: impl AsRef<Path>) -> Result<Self, Error> {
159        Self::try_from(fs::File::open(path)?)
160    }
161}
162#[cfg(feature = "macbinary")]
163impl Archive<macbinary::Reader<fs::File>> {
164    pub fn open(path: impl AsRef<Path>) -> Result<Self, Error> {
165        Archive::try_from(fs::File::open(path)?)
166    }
167}
168
169pub fn probe<R: io::Read + io::Seek>(reader: R) -> Result<(FourCC, FourCC), Error> {
170    let archive = Archive::try_from(reader)?;
171
172    // TODO: we should probably verfiy the header crc here to avoid false positives
173
174    Ok((Archive::<()>::CREATOR_CODE, Archive::<()>::TYPE_CODE))
175}
176
177#[cfg(test)]
178mod test {
179    use std::{
180        fs::{exists, File},
181        path::PathBuf,
182    };
183
184    use super::Archive;
185
186    #[test]
187    fn open_sample_file() {
188        let archive = open_fixture("Deep Thoughts Quotes.cpt");
189        assert_eq!(archive.iter().unwrap().count(), 91);
190
191        let archive = open_fixture("Misc Quotes.cpt");
192        assert_eq!(archive.iter().unwrap().count(), 80);
193    }
194
195    #[test]
196    fn verify_checksums() {
197        verify_archive("Deep Thoughts Quotes.cpt");
198        verify_archive("Compact Pro Package (English)");
199        verify_archive("zipit.sea");
200        verify_archive("Misc Quotes.cpt");
201        verify_archive("zipit.sea");
202    }
203
204    #[test]
205    fn open_file_backed_via_path() {
206        assert!(Archive::open("sample-files/Deep Thoughts Quotes.cpt").is_ok());
207    }
208
209    #[test]
210    fn can_open_macbinaries() {
211        verify_archive("sample-files/freddie1.cpt");
212        verify_archive("sample-files/sgnews49.cpt");
213        verify_archive("sample-files/sgnews02.cpt");
214        verify_archive("sample-files/giffer.cpt");
215        verify_archive("sample-files/h2chr308.cpt");
216        verify_archive("sample-files/h2dla250.cpt");
217        verify_archive("sample-files/h2lgm252.cpt");
218        verify_archive("sample-files/jpeg2gif.cpt");
219    }
220
221    fn open_fixture_raw(name: &'static str) -> File {
222        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
223            .join("sample-files/")
224            .join(name);
225
226        if !exists(&path).unwrap() {
227            panic!("Test fixture {name} does not exist!");
228        }
229
230        std::fs::File::open(path).unwrap()
231    }
232
233    fn open_fixture(name: &'static str) -> Archive<macbinary::Reader<File>> {
234        let file = open_fixture_raw(name);
235        Archive::try_from(file).unwrap()
236    }
237
238    fn verify_archive(name: &'static str) {
239        let mut archive = open_fixture("zipit.sea");
240        archive
241            .iter()
242            .unwrap()
243            .filter_map(|a| a.ok())
244            .filter(|a| a.is_file())
245            .for_each(|entry| {
246                archive.verify_entry(&entry).unwrap_or_else(|_| {
247                    panic!("Entry {} in {} should be valid", entry.name(), name)
248                })
249            });
250    }
251}