1#![doc = include_str!("../README.md")]
2#![feature(seek_stream_len)]
3#![feature(unsafe_cell_access)]
4#![feature(get_mut_unchecked)]
5
6use std::fs;
7use std::io;
8use std::io::Read;
9use std::path;
10
11use fourcc::{FourCC, fourcc};
12pub use macintosh_utils::Fork;
13
14pub mod algos;
16pub mod structs;
18
19mod archive;
20mod entry;
21pub mod error;
22pub(crate) mod verify;
23
24pub use archive::Archive;
25pub use archive::EntryIterator;
26pub use archive::EntryReader;
27pub use archive::ReadableEntry;
28pub use entry::Entry;
29pub use error::Error;
30pub use verify::{VerifyingEntryReader, VerifyingIterator};
31
32use crate::error::ExtractionError;
33
34pub fn verify<R: io::Read + io::Seek>(_reader: R) -> Result<(), Error> {
35 todo!()
36}
37
38pub fn verify_path<P: AsRef<path::Path>>(path: P) -> Result<(), Error> {
39 let file = fs::File::open(path)?;
40 verify(file)
41}
42
43pub fn probe<R: io::Read + io::Seek>(reader: R) -> Result<(FourCC, FourCC), Error> {
44 let archive = Archive::try_from(reader)?;
45
46 match archive.header() {
47 structs::ArchiveHeader::V1(archive_header) => {
48 Ok((fourcc!("rLau"), archive_header.file_code))
49 }
50 structs::ArchiveHeader::V5(_) => Ok((fourcc!("rLau"), fourcc!("SIT!"))),
51 }
52}
53
54pub fn extract_file<R: io::Read + io::Seek>(
56 reader: R,
57 file_name: &str,
58 fork: Fork,
59) -> Result<Vec<u8>, ExtractionError> {
60 let mut archive = Archive::try_from(reader)?;
61 let Some(entry) = archive
62 .iter()
63 .find(|e| e.is_file() && e.name() == file_name)
64 else {
65 return Err(ExtractionError::ItemNotFound);
66 };
67
68 let mut data = vec![0u8; entry.uncompressed_size(fork)];
69 let mut reader = archive.open(&entry, fork)?;
70 reader.read_exact(&mut data)?;
71
72 Ok(data)
73}
74
75pub fn extract_file_by_index<R: io::Read + io::Seek>(
77 reader: R,
78 index: usize,
79 fork: Fork,
80) -> Result<Vec<u8>, ExtractionError> {
81 let mut archive = Archive::try_from(reader)?;
82 let Some(entry) = archive
83 .iter()
84 .find(|e| matches!(e, Entry::File(f) if f.index() == index))
85 else {
86 return Err(ExtractionError::ItemNotFound);
87 };
88
89 let mut data = vec![0u8; entry.uncompressed_size(fork)];
90 let mut reader = archive.open(&entry, fork)?;
91 reader.read_exact(&mut data)?;
92
93 Ok(data)
94}
95
96#[cfg(test)]
97mod test {
98 use fourcc::fourcc;
99 use macintosh_utils::decode_string;
100
101 use crate::{archive::ReadableEntry, error::UnsupportedFeature};
102
103 use super::*;
104 use std::{
105 fs::{File, exists},
106 io::{self, Seek as _},
107 panic,
108 path::PathBuf,
109 };
110
111 macro_rules! assert_ok {
112 ($expression:expr) => {
113 match $expression {
114 Ok(_) => (),
115 Err(e) => {
116 panic!(
117 "Expected {} not to return an error, but got {:?} instead",
118 stringify!($expression),
119 e
120 );
121 }
122 }
123 };
124 }
125
126 macro_rules! assert_err {
127 ($expression:expr) => {
128 match $expression {
129 Ok(val) => panic!(
130 "Expected {} return an error, but got Ok({:?}) instead",
131 stringify!($expression),
132 val
133 ),
134 Err(_) => {
135 assert!(true);
136 }
137 }
138 };
139 }
140
141 #[test]
142 #[should_panic]
143 fn exclusive_archive_access_enforcement_with_multiple_iterators() {
144 let archive = open_fixture("StuffIt 1.10 Moby Dick.sit");
145
146 let _iterator = archive.iter();
147 let _iterator = archive.iter();
148 }
149
150 #[test]
151 #[should_panic]
152 fn exclusive_archive_access_enforcement_with_resetting() {
153 let mut archive = open_fixture("StuffIt 1.10 Moby Dick.sit");
154
155 let _iterator = archive.iter();
156 let _ = archive.reset();
157 }
158
159 #[test]
160 fn simple_file_extraction() {
161 let reader = open_fixture_raw("StuffIt 1.10 Moby Dick.sit");
162 let data = extract_file(reader, "00b Title.txt", Fork::Data).unwrap();
163 let contents = String::from_utf8_lossy(&data);
164
165 assert!(contents.contains("MOBY-DICK"));
166 }
167
168 #[test]
169 fn missing_file_extraction() {
170 let reader = open_fixture_raw("StuffIt 1.10 Moby Dick.sit");
171 let result = extract_file(reader, "i don't exist", Fork::Data);
172 assert!(matches!(result, Err(ExtractionError::ItemNotFound)));
173 }
174
175 #[test]
176 fn simple_file_extraction_by_index() {
177 let reader = open_fixture_raw("StuffIt 1.10 Moby Dick.sit");
178 let data = extract_file_by_index(reader, 1, Fork::Data).unwrap();
179 let contents = String::from_utf8_lossy(&data);
180
181 assert!(contents.contains("MOBY-DICK"));
182 }
183
184 #[test]
185 fn missing_file_extraction_by_index() {
186 let reader = open_fixture_raw("StuffIt 1.10 Moby Dick.sit");
187 let result = extract_file_by_index(reader, 823, Fork::Data);
188 assert!(matches!(result, Err(ExtractionError::ItemNotFound)));
189 }
190
191 fn header_corruption() {
192 let mut fixture = open_fixture_raw("eastermorning.sit");
193 let mut buffer = vec![0u8; fixture.stream_len().unwrap() as usize];
194 fixture.read_exact(&mut buffer).unwrap();
195
196 buffer[0x8A] = b'B';
198
199 let cursor = io::Cursor::new(buffer);
200 let mut reader = Archive::try_from(cursor).unwrap();
201 assert!(matches!(
202 reader.verify(),
203 Err(Error::ChecksumMismatch(
204 error::ChecksumLocation::EntryHeader
205 ))
206 ));
207 }
208
209 #[test]
210 fn reading_empty_archive() {
211 let mut archive = open_fixture("StuffIt 1.10 empty.sit");
212 assert_ok!(archive.verify());
213 }
214
215 #[test]
216 fn stuffit_1_5_1() {
217 let mut archive = open_fixture("StuffIt 1.5.1.sit");
218 assert_ok!(archive.verify());
219 }
220
221 mod stuffit_1_10 {
222 use super::*;
223
224 #[test]
225 fn item_extraction() {
226 let mut archive = open_fixture("StuffIt 1.10 Moby Dick.sit");
227 let entry = archive
228 .iter()
229 .find(|e| e.is_file() && e.name() == "00b Title.txt")
230 .unwrap();
231
232 let mut data = vec![0u8; entry.uncompressed_size(Fork::Data)];
233 let mut stream = archive.open(&entry, Fork::Data).unwrap();
234 let bytes_read = stream.read(&mut data).unwrap();
235 assert_eq!(bytes_read, entry.uncompressed_size(Fork::Data));
236 assert_eq!(data.len(), 47);
237
238 let string = decode_string(data);
239 assert!(string.contains("MOBY-DICK"));
240 assert!(string.contains("Herman Melville"));
241 }
242
243 #[test]
244 fn streaming_verification() {
245 use crate as sit;
246
247 let mut archive_file = open_fixture_raw("StuffIt 1.10 Moby Dick.sit");
248 let mut archive_data = vec![0u8; archive_file.stream_len().unwrap() as usize];
249 archive_file.read_exact(&mut archive_data).unwrap();
250 let reader = io::Cursor::new(archive_data);
251 let mut archive = sit::Archive::try_from(reader).unwrap();
252
253 let entry = archive
254 .iter()
255 .find(|e| e.is_file() && e.name() == "00b Title.txt")
256 .unwrap();
257 let offset_in_archive = entry.offset(Fork::Data);
258
259 let mut data = vec![0u8; entry.uncompressed_size(Fork::Data)];
260
261 let mut stream = archive.open(&entry, Fork::Data).unwrap().verifying();
263 assert_ok!(stream.read_exact(&mut data));
264
265 let mut archive_data = archive.into_inner().into_inner();
267 archive_data[offset_in_archive as usize + 12] = 0xAB;
268
269 let reader = io::Cursor::new(archive_data);
270 let mut archive = sit::Archive::try_from(reader).unwrap();
271
272 let mut stream = archive.open(&entry, Fork::Data).unwrap().verifying();
273
274 assert_err!(stream.read_exact(&mut data));
276 }
277
278 #[test]
279 fn full_verification() {
280 let mut fixture = open_fixture("StuffIt 1.10 Moby Dick.sit");
281 assert_ok!(fixture.verify());
282 }
283
284 #[test]
285 fn edge_cases() {
286 let mut fixture = open_fixture("StuffIt 1.10 edge cases.sit");
287 assert_ok!(fixture.verify());
288 }
289
290 #[test]
291 fn stream_validation() {
292 let mut archive = open_fixture("StuffIt 1.10 Moby Dick.sit");
293 let entry = archive
294 .iter()
295 .find(|e| e.is_file() && e.name() == "00b Title.txt")
296 .unwrap();
297
298 assert_ok!(
299 archive
300 .open(&entry, Fork::Data)
301 .unwrap()
302 .verifying()
303 .slurp()
304 );
305
306 assert_ok!(
307 archive
308 .open(&entry, Fork::Resource)
309 .unwrap()
310 .verifying()
311 .slurp()
312 );
313 }
314 }
315
316 mod stuffit_deluxe_4_5 {
317 use super::*;
318
319 #[test]
320 fn full_verification() {
321 let mut fixture = open_fixture("StuffIt DLX 4.5.sit");
322 assert_ok!(fixture.verify());
323 }
324
325 #[test]
326 fn offset_after_archive_header() {
327 let mut archive = open_fixture("StuffIt DLX 4.5 Offset.sit");
328 assert_ok!(archive.verify());
329 }
330
331 #[test]
332 fn encrypted_entries() {
333 let mut archive = open_fixture("StuffIt DLX 4.5 Encrypted.sit");
334 assert!(matches!(
335 archive.verify(),
336 Err(Error::UnsupportedFeature(UnsupportedFeature::Encryption))
337 ));
338 }
339
340 #[test]
341 fn self_extracting() {
342 let mut archive = open_fixture("StuffIt DLX 4.5 Self-Extracting.sea");
343 assert_ok!(archive.verify());
344 }
345
346 #[test]
347 fn entry_count() {
348 let mut archive = open_fixture("StuffIt DLX 4.5.sit");
349 let files = archive.iter().filter(|e| matches!(e, Entry::File(_)));
350
351 assert_eq!(files.count(), 144);
352
353 archive.reset().unwrap();
354 let directories = archive.iter().filter(|e| matches!(e, Entry::Directory(_)));
355 assert_eq!(directories.count(), 6);
356 }
357 }
358
359 mod stuffit_deluxe_5_5 {
360 use super::*;
361
362 #[test]
363 fn entry_count() {
364 let archive = open_fixture("StuffIt DLX 5.5 Moby Dick.sit");
365 let entry_count = archive.iter().count();
366 let directory_count = archive.iter().filter(|f| f.is_directory()).count();
367 let file_count = archive.iter().filter(|f| f.is_file()).count();
368
369 assert_eq!(directory_count, 4);
370 assert_eq!(file_count, 140);
371 assert_eq!(
372 entry_count,
373 file_count + directory_count * 2,
374 "Should have see one directory-end marker per directory"
375 );
376 }
377
378 #[test]
379 fn folder_comment() {
380 let archive = open_fixture("StuffIt DLX 5.5 Folder Comment.sit");
381 let folder = archive.iter().find(|e| e.is_directory()).unwrap();
382 assert_eq!(folder.name(), "Folder with comments");
383 assert_eq!(folder.comment(), "A folder with a comment!");
384
385 let archive = open_fixture("StuffIt DLX 5.5 Folder Comment.sit");
386 let file = archive.iter().find(|e| e.is_file()).unwrap();
387 let Entry::File(file) = file else { panic!() };
388
389 assert_eq!(file.file_code(), fourcc!("TEXT"));
390 assert_eq!(file.creator(), fourcc!("ttxt"));
391 }
392
393 #[test]
394 fn file_comment() {
395 let archive = open_fixture("StuffIt DLX 5.5 File Comment.sit");
396 let file = archive.iter().find(|e| e.is_file()).unwrap();
397 assert_eq!(file.name(), "File with comments.txt");
398 assert_eq!(file.comment(), "Look! This is a file comment!");
399 let Entry::File(file) = file else { panic!() };
400 assert_eq!(file.file_code(), fourcc!("TEXT"));
401 assert_eq!(file.creator(), fourcc!("ttxt"));
402 }
403
404 #[test]
405 fn encrypted_entries() {
406 let mut archive = open_fixture("StuffIt DLX 5.5.sit");
407 assert!(matches!(
408 archive.verify(),
409 Err(Error::UnsupportedFeature(UnsupportedFeature::Encryption))
410 ));
411 }
412 }
413
414 #[test]
415 fn stuffit_131_comment() {
416 let mut archive = open_fixture("StuffIt 1.31 Comment.sit");
417 assert_ok!(archive.verify());
418 }
419
420 #[test]
421 fn stuffit_131() {
422 let mut archive = open_fixture("StuffIt 1.31.sit");
423 assert_ok!(archive.verify());
424 }
425
426 #[test]
427 fn stuffit_201_comment() {
428 let mut archive = open_fixture("StuffIt 2.0.1 Comment.sit");
429 assert_ok!(archive.verify());
430 }
431
432 #[test]
433 fn stuffit_201_encryption_methods() {
434 let mut archive = open_fixture("StuffIt 2.0.1 Encryption Methods.sit");
435 assert!(matches!(
436 archive.verify(),
437 Err(Error::UnsupportedFeature(UnsupportedFeature::Encryption))
438 ));
439 }
440
441 #[test]
442 fn stuffit_201_compression_methods() {
443 let mut archive = open_fixture("StuffIt 2.0.1 Compression Methods.sit");
444 assert_ok!(archive.verify());
445 }
446
447 #[test]
448 fn stuffit_201_fixed_huffman() {
449 let mut archive = open_fixture("StuffIt 2.0.1 Fixed Huffman.sit");
450 assert_ok!(archive.verify());
451 }
452
453 #[test]
454 fn stuffit_201_signature() {
455 let mut archive = open_fixture("StuffIt 2.0.1 Signature.sit");
456 assert_ok!(archive.verify());
457 }
458
459 #[test]
460 fn stuffit_201() {
461 let mut archive = open_fixture("StuffIt 2.0.1.sit");
462 assert_ok!(archive.verify());
463 }
464
465 #[test]
466 fn stuffit_201_best_guess() {
467 let mut archive = open_fixture("StuffIt 2.0.1 Best Guess.sit");
468 assert_ok!(archive.verify());
469 }
470
471 #[test]
472 fn stuffit_201_better_compression() {
473 let mut archive = open_fixture("StuffIt 2.0.1 Better Compression.sit");
474 assert_ok!(archive.verify());
475 }
476
477 #[test]
478 fn stuffit_201_fast() {
479 let mut archive = open_fixture("StuffIt 2.0.1 Fast.sit");
480 assert_ok!(archive.verify());
481 }
482
483 #[test]
484 fn stuffit_201_faster() {
485 let mut archive = open_fixture("StuffIt 2.0.1 Faster.sit");
486 assert_ok!(archive.verify());
487 }
488
489 #[test]
490 fn stuffit_201_optimal() {
491 let mut archive = open_fixture("StuffIt 2.0.1 Optimal.sit");
492 assert_ok!(archive.verify());
493 }
494
495 #[test]
496 fn stuffit_351() {
497 let mut archive = open_fixture("StuffIt 3.5.1.sit");
498 assert_ok!(archive.verify());
499 }
500
501 #[test]
502 fn stuffit_40() {
503 let mut archive = open_fixture("StuffIt 4.0.sit");
504 assert_ok!(archive.verify());
505 }
506
507 #[test]
508 fn stuffit_45() {
509 let mut archive = open_fixture("StuffIt 4.5.sit");
510 assert_ok!(archive.verify());
511 }
512
513 #[test]
514 fn stuffit_55_comment() {
515 let mut archive = open_fixture("StuffIt 5.5 Comment.sit");
516 assert_ok!(archive.verify());
517 }
518
519 #[test]
520 fn stuffit_55() {
521 let mut archive = open_fixture("StuffIt 5.5.sit");
522 assert_ok!(archive.verify());
523 }
524
525 #[test]
526 fn stuffit_60_receipt() {
527 let mut archive = open_fixture("StuffIt 6.0 Receipt.sit");
528 assert_ok!(archive.verify());
529 }
530
531 #[test]
532 fn stuffit_60() {
533 let mut archive = open_fixture("StuffIt 6.0.sit");
534 assert_ok!(archive.verify());
535 }
536
537 #[test]
538 fn stuffit_703() {
539 let mut archive = open_fixture("StuffIt 7.0.3.sit");
540 assert_ok!(archive.verify());
541 }
542
543 #[test]
544 fn stuffit_703_without_finder_desktop_files() {
545 let mut archive = open_fixture("StuffIt 7.0.3 wihout Finder.sit");
546 assert_ok!(archive.verify());
547 }
548
549 fn open_fixture_raw(name: &'static str) -> File {
550 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
551 .join("test/")
552 .join(name);
553
554 if !exists(&path).unwrap() {
555 panic!("Test fixture {name} does not exist!");
556 }
557
558 std::fs::File::open(path).unwrap()
559 }
560
561 fn open_fixture(name: &'static str) -> Archive<File> {
562 let file = open_fixture_raw(name);
563 Archive::try_from(file).unwrap()
564 }
565}