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 pub fn into_inner(self) -> R {
47 self.ensure_not_iterating();
48 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 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}