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_fork(&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_fork(&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 #[allow(unused)]
193 fn header_corruption() {
194 let mut fixture = open_fixture_raw("StuffIt 1.10 Moby Dick.sit");
195 let mut buffer = vec![0u8; fixture.stream_len().unwrap() as usize];
196 fixture.read_exact(&mut buffer).unwrap();
197
198 buffer[0x58] = b'_';
200
201 let cursor = io::Cursor::new(buffer);
202 let mut reader = Archive::try_from(cursor).unwrap();
203 let result = reader.verify();
204
205 assert!(matches!(
206 result,
207 Err(Error::ChecksumMismatch(
208 error::ChecksumLocation::EntryHeader
209 ))
210 ));
211 }
212
213 #[test]
214 fn reading_empty_archive() {
215 let mut archive = open_fixture("StuffIt 1.10 empty.sit");
216 assert_ok!(archive.verify());
217 }
218
219 #[test]
220 fn stuffit_1_5_1() {
221 let mut archive = open_fixture("StuffIt 1.5.1.sit");
222 assert_ok!(archive.verify());
223 }
224
225 mod stuffit_1_10 {
226 use super::*;
227
228 #[test]
229 fn item_extraction() {
230 let mut archive = open_fixture("StuffIt 1.10 Moby Dick.sit");
231 let entry = archive
232 .iter()
233 .find(|e| e.is_file() && e.name() == "00b Title.txt")
234 .unwrap();
235
236 let mut data = vec![0u8; entry.uncompressed_size(Fork::Data)];
237 let mut stream = archive.open_fork(&entry, Fork::Data).unwrap();
238 let bytes_read = stream.read(&mut data).unwrap();
239 assert_eq!(bytes_read, entry.uncompressed_size(Fork::Data));
240 assert_eq!(data.len(), 47);
241
242 let string = decode_string(data);
243 assert!(string.contains("MOBY-DICK"));
244 assert!(string.contains("Herman Melville"));
245 }
246
247 #[test]
248 fn streaming_verification() {
249 use crate as sit;
250
251 let mut archive_file = open_fixture_raw("StuffIt 1.10 Moby Dick.sit");
252 let mut archive_data = vec![0u8; archive_file.stream_len().unwrap() as usize];
253 archive_file.read_exact(&mut archive_data).unwrap();
254 let reader = io::Cursor::new(archive_data);
255 let mut archive = sit::Archive::try_from(reader).unwrap();
256
257 let entry = archive
258 .iter()
259 .find(|e| e.is_file() && e.name() == "00b Title.txt")
260 .unwrap();
261 let offset_in_archive = entry.offset(Fork::Data);
262
263 let mut data = vec![0u8; entry.uncompressed_size(Fork::Data)];
264
265 let mut stream = archive.open_fork(&entry, Fork::Data).unwrap().verifying();
267 assert_ok!(stream.read_exact(&mut data));
268
269 let mut archive_data = archive.into_inner().into_inner();
271 archive_data[offset_in_archive as usize + 12] = 0xAB;
272
273 let reader = io::Cursor::new(archive_data);
274 let mut archive = sit::Archive::try_from(reader).unwrap();
275
276 let mut stream = archive.open_fork(&entry, Fork::Data).unwrap().verifying();
277
278 assert_err!(stream.read_exact(&mut data));
280 }
281
282 #[test]
283 fn full_verification() {
284 let mut fixture = open_fixture("StuffIt 1.10 Moby Dick.sit");
285 assert_ok!(fixture.verify());
286 }
287
288 #[test]
289 fn edge_cases() {
290 let mut fixture = open_fixture("StuffIt 1.10 edge cases.sit");
291 assert_ok!(fixture.verify());
292 }
293
294 #[test]
295 fn stream_validation() {
296 let mut archive = open_fixture("StuffIt 1.10 Moby Dick.sit");
297 let entry = archive
298 .iter()
299 .find(|e| e.is_file() && e.name() == "00b Title.txt")
300 .unwrap();
301
302 assert_ok!(
303 archive
304 .open_fork(&entry, Fork::Data)
305 .unwrap()
306 .verifying()
307 .slurp()
308 );
309
310 assert_ok!(
311 archive
312 .open_fork(&entry, Fork::Resource)
313 .unwrap()
314 .verifying()
315 .slurp()
316 );
317 }
318 }
319
320 mod stuffit_deluxe_4_5 {
321 use super::*;
322
323 #[test]
324 fn full_verification() {
325 let mut fixture = open_fixture("StuffIt DLX 4.5.sit");
326 assert_ok!(fixture.verify());
327 }
328
329 #[test]
330 fn offset_after_archive_header() {
331 let mut archive = open_fixture("StuffIt DLX 4.5 Offset.sit");
332 assert_ok!(archive.verify());
333 }
334
335 #[test]
336 fn encrypted_entries() {
337 let mut archive = open_fixture("StuffIt DLX 4.5 Encrypted.sit");
338 assert!(matches!(
339 archive.verify(),
340 Err(Error::UnsupportedFeature(UnsupportedFeature::Encryption))
341 ));
342 }
343
344 #[test]
345 fn self_extracting() {
346 let mut archive = open_fixture("StuffIt DLX 4.5 Self-Extracting.sea");
347 assert_ok!(archive.verify());
348 }
349
350 #[test]
351 fn entry_count() {
352 let mut archive = open_fixture("StuffIt DLX 4.5.sit");
353 let files = archive.iter().filter(|e| matches!(e, Entry::File(_)));
354
355 assert_eq!(files.count(), 144);
356
357 archive.reset().unwrap();
358 let directories = archive.iter().filter(|e| matches!(e, Entry::Directory(_)));
359 assert_eq!(directories.count(), 6);
360 }
361 }
362
363 mod stuffit_deluxe_5_5 {
364 use super::*;
365
366 #[test]
367 fn entry_count() {
368 let archive = open_fixture("StuffIt DLX 5.5 Moby Dick.sit");
369 let entry_count = archive.iter().count();
370 let directory_count = archive.iter().filter(|f| f.is_directory()).count();
371 let file_count = archive.iter().filter(|f| f.is_file()).count();
372
373 assert_eq!(directory_count, 4);
374 assert_eq!(file_count, 140);
375 assert_eq!(
376 entry_count,
377 file_count + directory_count * 2,
378 "Should have see one directory-end marker per directory"
379 );
380 }
381
382 #[test]
383 fn folder_comment() {
384 let archive = open_fixture("StuffIt DLX 5.5 Folder Comment.sit");
385 let folder = archive.iter().find(|e| e.is_directory()).unwrap();
386 assert_eq!(folder.name(), "Folder with comments");
387 assert_eq!(folder.comment(), "A folder with a comment!");
388
389 let archive = open_fixture("StuffIt DLX 5.5 Folder Comment.sit");
390 let file = archive.iter().find(|e| e.is_file()).unwrap();
391 let Entry::File(file) = file else { panic!() };
392
393 assert_eq!(file.file_code(), fourcc!("TEXT"));
394 assert_eq!(file.creator(), fourcc!("ttxt"));
395 }
396
397 #[test]
398 fn file_comment() {
399 let archive = open_fixture("StuffIt DLX 5.5 File Comment.sit");
400 let file = archive.iter().find(|e| e.is_file()).unwrap();
401 assert_eq!(file.name(), "File with comments.txt");
402 assert_eq!(file.comment(), "Look! This is a file comment!");
403 let Entry::File(file) = file else { panic!() };
404 assert_eq!(file.file_code(), fourcc!("TEXT"));
405 assert_eq!(file.creator(), fourcc!("ttxt"));
406 }
407
408 #[test]
409 fn encrypted_entries() {
410 let mut archive = open_fixture("StuffIt DLX 5.5.sit");
411 assert!(matches!(
412 archive.verify(),
413 Err(Error::UnsupportedFeature(UnsupportedFeature::Encryption))
414 ));
415 }
416 }
417
418 #[test]
419 fn stuffit_131_comment() {
420 let mut archive = open_fixture("StuffIt 1.31 Comment.sit");
421 assert_ok!(archive.verify());
422 }
423
424 #[test]
425 fn stuffit_131() {
426 let mut archive = open_fixture("StuffIt 1.31.sit");
427 assert_ok!(archive.verify());
428 }
429
430 #[test]
431 fn stuffit_201_comment() {
432 let mut archive = open_fixture("StuffIt 2.0.1 Comment.sit");
433 assert_ok!(archive.verify());
434 }
435
436 #[test]
437 fn stuffit_201_encryption_methods() {
438 let mut archive = open_fixture("StuffIt 2.0.1 Encryption Methods.sit");
439 assert!(matches!(
440 archive.verify(),
441 Err(Error::UnsupportedFeature(UnsupportedFeature::Encryption))
442 ));
443 }
444
445 #[test]
446 fn stuffit_201_compression_methods() {
447 let mut archive = open_fixture("StuffIt 2.0.1 Compression Methods.sit");
448 assert_ok!(archive.verify());
449 }
450
451 #[test]
452 fn stuffit_201_fixed_huffman() {
453 let mut archive = open_fixture("StuffIt 2.0.1 Fixed Huffman.sit");
454 assert_ok!(archive.verify());
455 }
456
457 #[test]
458 fn stuffit_201_signature() {
459 let mut archive = open_fixture("StuffIt 2.0.1 Signature.sit");
460 assert_ok!(archive.verify());
461 }
462
463 #[test]
464 fn stuffit_201() {
465 let mut archive = open_fixture("StuffIt 2.0.1.sit");
466 assert_ok!(archive.verify());
467 }
468
469 #[test]
470 fn stuffit_201_best_guess() {
471 let mut archive = open_fixture("StuffIt 2.0.1 Best Guess.sit");
472 assert_ok!(archive.verify());
473 }
474
475 #[test]
476 fn stuffit_201_better_compression() {
477 let mut archive = open_fixture("StuffIt 2.0.1 Better Compression.sit");
478 assert_ok!(archive.verify());
479 }
480
481 #[test]
482 fn stuffit_201_fast() {
483 let mut archive = open_fixture("StuffIt 2.0.1 Fast.sit");
484 assert_ok!(archive.verify());
485 }
486
487 #[test]
488 fn stuffit_201_faster() {
489 let mut archive = open_fixture("StuffIt 2.0.1 Faster.sit");
490 assert_ok!(archive.verify());
491 }
492
493 #[test]
494 fn stuffit_201_optimal() {
495 let mut archive = open_fixture("StuffIt 2.0.1 Optimal.sit");
496 assert_ok!(archive.verify());
497 }
498
499 #[test]
500 fn stuffit_351() {
501 let mut archive = open_fixture("StuffIt 3.5.1.sit");
502 assert_ok!(archive.verify());
503 }
504
505 #[test]
506 fn stuffit_40() {
507 let mut archive = open_fixture("StuffIt 4.0.sit");
508 assert_ok!(archive.verify());
509 }
510
511 #[test]
512 fn stuffit_45() {
513 let mut archive = open_fixture("StuffIt 4.5.sit");
514 assert_ok!(archive.verify());
515 }
516
517 #[test]
518 fn stuffit_55_comment() {
519 let mut archive = open_fixture("StuffIt 5.5 Comment.sit");
520 assert_ok!(archive.verify());
521 }
522
523 #[test]
524 fn stuffit_55() {
525 let mut archive = open_fixture("StuffIt 5.5.sit");
526 assert_ok!(archive.verify());
527 }
528
529 #[test]
530 fn stuffit_60_receipt() {
531 let mut archive = open_fixture("StuffIt 6.0 Receipt.sit");
532 assert_ok!(archive.verify());
533 }
534
535 #[test]
536 fn stuffit_60() {
537 let mut archive = open_fixture("StuffIt 6.0.sit");
538 assert_ok!(archive.verify());
539 }
540
541 #[test]
542 fn stuffit_703() {
543 let mut archive = open_fixture("StuffIt 7.0.3.sit");
544 assert_ok!(archive.verify());
545 }
546
547 #[test]
548 fn stuffit_703_without_finder_desktop_files() {
549 let mut archive = open_fixture("StuffIt 7.0.3 wihout Finder.sit");
550 assert_ok!(archive.verify());
551 }
552
553 fn open_fixture_raw(name: &'static str) -> File {
554 let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
555 .join("test/")
556 .join(name);
557
558 if !exists(&path).unwrap() {
559 panic!("Test fixture {name} does not exist!");
560 }
561
562 std::fs::File::open(path).unwrap()
563 }
564
565 fn open_fixture(name: &'static str) -> Archive<File> {
566 let file = open_fixture_raw(name);
567 Archive::try_from(file).unwrap()
568 }
569}