Skip to main content

sit/
lib.rs

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
14/// Implementation of the various decompression methods that are used in StuffIt archives
15pub mod algos;
16/// Data structures that make up the structure of archives
17pub 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
54/// Extract data from the first file entry denoted by `file_name`
55pub 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
75/// Extract data from nth file entry in the archive
76pub 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    // TODO: Enable test when header verification has been implemented
192    #[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        // corrupt file header in an insignificant way (change file code)
199        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            //Read data from the unmodified (valid) archive
266            let mut stream = archive.open_fork(&entry, Fork::Data).unwrap().verifying();
267            assert_ok!(stream.read_exact(&mut data));
268
269            // Now let's corrupt some data in the archive
270            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            // The read operation returns an error when the end of the stream has been reached
279            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}