Skip to main content

cpt/
lib.rs

1#![feature(seek_stream_len)]
2#![feature(get_mut_unchecked)]
3#![doc=include_str!("../README.md")]
4
5use std::{
6    fs,
7    io::{self, Read as _, Seek as _},
8    path::Path,
9    rc::Rc,
10};
11
12use binrw::BinReaderExt;
13pub use macintosh_utils::{chrono, fourcc, Fork, FourCC};
14
15mod entry_iterator;
16mod entry_reader;
17pub mod error;
18mod lzh;
19mod rle;
20
21/// On-disk structures of CPT files
22pub mod structs;
23
24use crate::{
25    entry_iterator::EntryIterator, entry_reader::StreamDescription, error::Feature,
26    error::VerificationError,
27};
28use entry_reader::EntryReader;
29pub use error::Error;
30use structs::ArchiveHeader;
31pub use structs::{Entry, Flags};
32const CRC32: crc::Crc<u32> = crc::Crc::<u32>::new(&crc::CRC_32_JAMCRC);
33
34/// A structure representing a CPT archive
35///
36/// Archives are created by [`opening`](`Archive::open) a file on disk or by wrapping a seekable
37/// reader using the [`try_from`](`Archive::try_from`) function.
38pub struct Archive<R> {
39    inner: Rc<R>,
40    entries_start: u64,
41}
42
43impl<R> Archive<R> {
44    /// Macintosh type code used by CPT files (`PACT`)
45    pub const TYPE_CODE: FourCC = fourcc!("PACT");
46    /// Macintosh creator code used by CPT files (`CPCT`)
47    pub const CREATOR_CODE: FourCC = fourcc!("CPCT");
48
49    /// Returns the underlying reader, note that it's read position will have changed from when you
50    /// passed it in
51    pub fn into_inner(self) -> R {
52        self.ensure_not_iterating();
53        // SAFETY: Since no one is iterating, and any open entry would borrow the archive mutably
54        // this is safe
55        let Archive { inner, .. } = self;
56        unsafe { Rc::try_unwrap(inner).unwrap_unchecked() }
57    }
58
59    /// Panics at runtime if the archive is being iterated
60    ///
61    /// This is used to assert our invariant that there can not be two or more iterators over the archive
62    /// at the same time, as this would move the seek position of the underlying
63    /// reader unpredictably.
64    fn ensure_not_iterating(&self) {
65        if Rc::strong_count(&self.inner) != 1 || Rc::weak_count(&self.inner) != 0 {
66            panic!("Can not modify archive while an iterator is runnning")
67        }
68    }
69}
70
71#[cfg(feature = "macbinary")]
72impl<A: io::Read + io::Seek> Archive<macbinary::Reader<A>> {
73    /// Creates an [`Archive`] from the given reader
74    pub fn try_from(inner: A) -> Result<Self, Error> {
75        use macintosh_utils::FourCC;
76
77        use crate::structs::CatalogHeader;
78
79        let wrapper = macbinary::MacBinary::try_new(inner)?;
80        let file_code = wrapper.type_code();
81        let creator = wrapper.creator_code();
82        if file_code != FourCC::default() && file_code != Archive::<()>::TYPE_CODE
83            || creator != FourCC::default() && creator != Archive::<()>::CREATOR_CODE
84        {
85            return Err(Error::InvalidFile);
86        }
87
88        let mut inner = wrapper.into_data_fork()?;
89        let header: ArchiveHeader = inner.read_be()?;
90        let header_count_offset: i64 =
91            header.header_offset as i64 - ArchiveHeader::PACKED_SIZE as i64;
92        inner.seek(std::io::SeekFrom::Current(header_count_offset))?;
93        let _ = inner.read_be::<CatalogHeader>()?;
94
95        let position = inner.stream_position()?;
96
97        Ok(Self {
98            inner: Rc::new(inner),
99            entries_start: position,
100        })
101    }
102}
103
104impl<R: io::Read + io::Seek> Archive<R> {
105    #[cfg(not(feature = "macbinary"))]
106    pub fn try_from(mut inner: R) -> Result<Self, Error> {
107        let header: ArchiveHeader = inner.read_be()?;
108        let header_count_offset: i64 =
109            header.header_offset as i64 - ArchiveHeader::PACKED_SIZE as i64;
110        inner.seek(std::io::SeekFrom::Current(header_count_offset))?;
111        let header_cont = inner.read_be()?;
112
113        let position = inner.stream_position()?;
114
115        Ok(Self {
116            inner: Rc::new(inner),
117            archive_header: header,
118            catalog: header_cont,
119            entries_start: position,
120        })
121    }
122
123    /// Creates an iterator over all archive entries.
124    ///
125    /// This method panics if another [`EntryIterator`] is currently alive for this archive to
126    /// avoid over sharing the underlying reader's seek position.
127    pub fn iter(&self) -> Result<EntryIterator<R>, Error> {
128        self.ensure_not_iterating();
129
130        EntryIterator::try_at(self.entries_start, self.inner.clone())
131    }
132
133    /// Verifies the given entry, returning a verification error if anything goes wrong
134    pub fn verify_entry(&mut self, entry: &Entry) -> Result<(), VerificationError> {
135        let Some(file) = entry.as_file() else {
136            return Ok(());
137        };
138
139        let mut checksum = CRC32.digest();
140
141        let mut resource = vec![0u8; file.rsrc_uncompressed_size as usize];
142        let mut reader = self.open_entry(entry, Fork::Resource)?;
143        reader.read_exact(&mut resource)?;
144        checksum.update(&resource);
145
146        let mut data = vec![0u8; file.data_uncompressed_size as usize];
147        let mut reader = self.open_entry(entry, Fork::Data)?;
148        reader.read_exact(&mut data)?;
149        checksum.update(&data);
150
151        if checksum.finalize() != file.crc32 {
152            Err(VerificationError::InvalidChecksum)
153        } else {
154            Ok(())
155        }
156    }
157
158    /// Opens the specified fork of an entry for reading
159    pub fn open_entry(&mut self, entry: &Entry, fork: Fork) -> Result<EntryReader<'_, R>, Error> {
160        if entry.as_directory().is_some() {
161            return EntryReader::try_new(&mut self.inner, StreamDescription::default());
162        }
163
164        let file = entry.as_file().unwrap();
165        if file.flags.contains(Flags::ENCRYPTED) {
166            return Err(Error::UnsupportedFeature(Feature::Encryption));
167        }
168
169        EntryReader::try_new(&mut self.inner, entry.spec(fork))
170    }
171}
172
173#[cfg(not(feature = "macbinary"))]
174impl Archive<fs::File> {
175    /// Opens an [`Archive`] from the file system
176    pub fn open(path: impl AsRef<Path>) -> Result<Self, Error> {
177        Self::try_from(fs::File::open(path)?)
178    }
179}
180
181#[cfg(feature = "macbinary")]
182impl Archive<macbinary::Reader<fs::File>> {
183    /// Opens an [`Archive`] from the file system while transparently undoing MacBinar encoding of
184    /// the archive (not its entries).
185    pub fn open(path: impl AsRef<Path>) -> Result<Self, Error> {
186        Archive::try_from(fs::File::open(path)?)
187    }
188}
189
190/// Detect if the given reader is a CPT archive
191///
192///  ```
193/// let mut file = std::fs::File::open("sample-files/FRED.CPT").unwrap();
194/// assert!(cpt::probe(file).is_ok());
195///
196/// let mut file = std::fs::File::open("README.md").unwrap();
197/// assert!(cpt::probe(file).is_err());
198/// ```
199pub fn probe<R: io::Read + io::Seek>(reader: R) -> Result<(FourCC, FourCC), Error> {
200    let _ = Archive::try_from(reader)?;
201    // TODO: we should probably verfiy the header crc here to avoid false positives
202    Ok((Archive::<()>::CREATOR_CODE, Archive::<()>::TYPE_CODE))
203}
204
205/// Verify the structure and checksums of the given reader
206///
207/// ```rust
208/// let mut file = std::fs::File::open("sample-files/FRED.CPT").unwrap();
209/// assert!(cpt::verify(file).is_ok());
210/// ```
211pub fn verify<R: io::Read + io::Seek>(reader: R) -> Result<(), VerificationError> {
212    let mut archive = Archive::try_from(reader)?;
213    if archive
214        .iter()
215        .unwrap()
216        .filter_map(|a| a.ok())
217        .filter(|a| a.is_file())
218        .all(|entry| archive.verify_entry(&entry).is_ok())
219    {
220        Ok(())
221    } else {
222        Err(VerificationError::InvalidChecksum)
223    }
224}
225
226#[cfg(test)]
227mod test {
228    use std::{
229        fs::{exists, File},
230        path::PathBuf,
231    };
232
233    use super::Archive;
234
235    #[test]
236    fn open_sample_file() {
237        let archive = open_fixture("Deep Thoughts Quotes.cpt");
238        assert_eq!(archive.iter().unwrap().count(), 91);
239
240        let archive = open_fixture("Misc Quotes.cpt");
241        assert_eq!(archive.iter().unwrap().count(), 80);
242    }
243
244    #[test]
245    fn verify_checksums() {
246        verify_archive("Deep Thoughts Quotes.cpt");
247        verify_archive("Compact Pro Package (English)");
248        verify_archive("zipit.sea");
249        verify_archive("Misc Quotes.cpt");
250        verify_archive("zipit.sea");
251    }
252
253    #[test]
254    fn open_file_backed_via_path() {
255        assert!(Archive::open("sample-files/Deep Thoughts Quotes.cpt").is_ok());
256    }
257
258    #[test]
259    fn can_open_macbinaries() {
260        verify_archive("sample-files/freddie1.cpt");
261        verify_archive("sample-files/sgnews49.cpt");
262        verify_archive("sample-files/sgnews02.cpt");
263        verify_archive("sample-files/giffer.cpt");
264        verify_archive("sample-files/h2chr308.cpt");
265        verify_archive("sample-files/h2dla250.cpt");
266        verify_archive("sample-files/h2lgm252.cpt");
267        verify_archive("sample-files/jpeg2gif.cpt");
268    }
269
270    fn open_fixture_raw(name: &'static str) -> File {
271        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
272            .join("sample-files/")
273            .join(name);
274
275        if !exists(&path).unwrap() {
276            panic!("Test fixture {name} does not exist!");
277        }
278
279        std::fs::File::open(path).unwrap()
280    }
281
282    fn open_fixture(name: &'static str) -> Archive<macbinary::Reader<File>> {
283        let file = open_fixture_raw(name);
284        Archive::try_from(file).unwrap()
285    }
286
287    fn verify_archive(name: &'static str) {
288        let mut archive = open_fixture("zipit.sea");
289        archive
290            .iter()
291            .unwrap()
292            .filter_map(|a| a.ok())
293            .filter(|a| a.is_file())
294            .for_each(|entry| {
295                archive.verify_entry(&entry).unwrap_or_else(|_| {
296                    panic!("Entry {} in {} should be valid", entry.name(), name)
297                })
298            });
299    }
300}