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(&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(&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        // corrupt file header in an insignificant way (change file name)
197        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            //Read data from the unmodified (valid) archive
262            let mut stream = archive.open(&entry, Fork::Data).unwrap().verifying();
263            assert_ok!(stream.read_exact(&mut data));
264
265            // Now let's corrupt some data in the archive
266            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            // The read operation returns an error when the end of the stream has been reached
275            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}