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
21pub 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
34pub struct Archive<R> {
39 inner: Rc<R>,
40 entries_start: u64,
41}
42
43impl<R> Archive<R> {
44 pub const TYPE_CODE: FourCC = fourcc!("PACT");
46 pub const CREATOR_CODE: FourCC = fourcc!("CPCT");
48
49 pub fn into_inner(self) -> R {
52 self.ensure_not_iterating();
53 let Archive { inner, .. } = self;
56 unsafe { Rc::try_unwrap(inner).unwrap_unchecked() }
57 }
58
59 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 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 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 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 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 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 pub fn open(path: impl AsRef<Path>) -> Result<Self, Error> {
186 Archive::try_from(fs::File::open(path)?)
187 }
188}
189
190pub fn probe<R: io::Read + io::Seek>(reader: R) -> Result<(FourCC, FourCC), Error> {
200 let _ = Archive::try_from(reader)?;
201 Ok((Archive::<()>::CREATOR_CODE, Archive::<()>::TYPE_CODE))
203}
204
205pub 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}