#![doc = include_str!("../README.md")]
#![feature(seek_stream_len)]
#![feature(unsafe_cell_access)]
#![feature(get_mut_unchecked)]
use std::fs;
use std::io;
use std::io::Read;
use std::path;
use fourcc::{FourCC, fourcc};
pub use macintosh_utils::Fork;
pub mod algos;
pub mod structs;
mod archive;
mod entry;
pub mod error;
pub(crate) mod verify;
pub use archive::Archive;
pub use archive::EntryIterator;
pub use archive::EntryReader;
pub use archive::ReadableEntry;
pub use entry::Entry;
pub use error::Error;
pub use verify::{VerifyingEntryReader, VerifyingIterator};
use crate::error::ExtractionError;
pub fn verify<R: io::Read + io::Seek>(_reader: R) -> Result<(), Error> {
todo!()
}
pub fn verify_path<P: AsRef<path::Path>>(path: P) -> Result<(), Error> {
let file = fs::File::open(path)?;
verify(file)
}
pub fn probe<R: io::Read + io::Seek>(reader: R) -> Result<(FourCC, FourCC), Error> {
let archive = Archive::try_from(reader)?;
match archive.header() {
structs::ArchiveHeader::V1(archive_header) => {
Ok((fourcc!("rLau"), archive_header.file_code))
}
structs::ArchiveHeader::V5(_) => Ok((fourcc!("rLau"), fourcc!("SIT!"))),
}
}
pub fn extract_file<R: io::Read + io::Seek>(
reader: R,
file_name: &str,
fork: Fork,
) -> Result<Vec<u8>, ExtractionError> {
let mut archive = Archive::try_from(reader)?;
let Some(entry) = archive
.iter()
.find(|e| e.is_file() && e.name() == file_name)
else {
return Err(ExtractionError::ItemNotFound);
};
let mut data = vec![0u8; entry.uncompressed_size(fork)];
let mut reader = archive.open_fork(&entry, fork)?;
reader.read_exact(&mut data)?;
Ok(data)
}
pub fn extract_file_by_index<R: io::Read + io::Seek>(
reader: R,
index: usize,
fork: Fork,
) -> Result<Vec<u8>, ExtractionError> {
let mut archive = Archive::try_from(reader)?;
let Some(entry) = archive
.iter()
.find(|e| matches!(e, Entry::File(f) if f.index() == index))
else {
return Err(ExtractionError::ItemNotFound);
};
let mut data = vec![0u8; entry.uncompressed_size(fork)];
let mut reader = archive.open_fork(&entry, fork)?;
reader.read_exact(&mut data)?;
Ok(data)
}
#[cfg(test)]
mod test {
use fourcc::fourcc;
use macintosh_utils::decode_string;
use crate::{archive::ReadableEntry, error::UnsupportedFeature};
use super::*;
use std::{
fs::{File, exists},
io::{self, Seek as _},
panic,
path::PathBuf,
};
macro_rules! assert_ok {
($expression:expr) => {
match $expression {
Ok(_) => (),
Err(e) => {
panic!(
"Expected {} not to return an error, but got {:?} instead",
stringify!($expression),
e
);
}
}
};
}
macro_rules! assert_err {
($expression:expr) => {
match $expression {
Ok(val) => panic!(
"Expected {} return an error, but got Ok({:?}) instead",
stringify!($expression),
val
),
Err(_) => {
assert!(true);
}
}
};
}
#[test]
#[should_panic]
fn exclusive_archive_access_enforcement_with_multiple_iterators() {
let archive = open_fixture("StuffIt 1.10 Moby Dick.sit");
let _iterator = archive.iter();
let _iterator = archive.iter();
}
#[test]
#[should_panic]
fn exclusive_archive_access_enforcement_with_resetting() {
let mut archive = open_fixture("StuffIt 1.10 Moby Dick.sit");
let _iterator = archive.iter();
let _ = archive.reset();
}
#[test]
fn simple_file_extraction() {
let reader = open_fixture_raw("StuffIt 1.10 Moby Dick.sit");
let data = extract_file(reader, "00b Title.txt", Fork::Data).unwrap();
let contents = String::from_utf8_lossy(&data);
assert!(contents.contains("MOBY-DICK"));
}
#[test]
fn missing_file_extraction() {
let reader = open_fixture_raw("StuffIt 1.10 Moby Dick.sit");
let result = extract_file(reader, "i don't exist", Fork::Data);
assert!(matches!(result, Err(ExtractionError::ItemNotFound)));
}
#[test]
fn simple_file_extraction_by_index() {
let reader = open_fixture_raw("StuffIt 1.10 Moby Dick.sit");
let data = extract_file_by_index(reader, 1, Fork::Data).unwrap();
let contents = String::from_utf8_lossy(&data);
assert!(contents.contains("MOBY-DICK"));
}
#[test]
fn missing_file_extraction_by_index() {
let reader = open_fixture_raw("StuffIt 1.10 Moby Dick.sit");
let result = extract_file_by_index(reader, 823, Fork::Data);
assert!(matches!(result, Err(ExtractionError::ItemNotFound)));
}
#[allow(unused)]
fn header_corruption() {
let mut fixture = open_fixture_raw("StuffIt 1.10 Moby Dick.sit");
let mut buffer = vec![0u8; fixture.stream_len().unwrap() as usize];
fixture.read_exact(&mut buffer).unwrap();
buffer[0x58] = b'_';
let cursor = io::Cursor::new(buffer);
let mut reader = Archive::try_from(cursor).unwrap();
let result = reader.verify();
assert!(matches!(
result,
Err(Error::ChecksumMismatch(
error::ChecksumLocation::EntryHeader
))
));
}
#[test]
fn reading_empty_archive() {
let mut archive = open_fixture("StuffIt 1.10 empty.sit");
assert_ok!(archive.verify());
}
#[test]
fn stuffit_1_5_1() {
let mut archive = open_fixture("StuffIt 1.5.1.sit");
assert_ok!(archive.verify());
}
mod stuffit_1_10 {
use super::*;
#[test]
fn item_extraction() {
let mut archive = open_fixture("StuffIt 1.10 Moby Dick.sit");
let entry = archive
.iter()
.find(|e| e.is_file() && e.name() == "00b Title.txt")
.unwrap();
let mut data = vec![0u8; entry.uncompressed_size(Fork::Data)];
let mut stream = archive.open_fork(&entry, Fork::Data).unwrap();
let bytes_read = stream.read(&mut data).unwrap();
assert_eq!(bytes_read, entry.uncompressed_size(Fork::Data));
assert_eq!(data.len(), 47);
let string = decode_string(data);
assert!(string.contains("MOBY-DICK"));
assert!(string.contains("Herman Melville"));
}
#[test]
fn streaming_verification() {
use crate as sit;
let mut archive_file = open_fixture_raw("StuffIt 1.10 Moby Dick.sit");
let mut archive_data = vec![0u8; archive_file.stream_len().unwrap() as usize];
archive_file.read_exact(&mut archive_data).unwrap();
let reader = io::Cursor::new(archive_data);
let mut archive = sit::Archive::try_from(reader).unwrap();
let entry = archive
.iter()
.find(|e| e.is_file() && e.name() == "00b Title.txt")
.unwrap();
let offset_in_archive = entry.offset(Fork::Data);
let mut data = vec![0u8; entry.uncompressed_size(Fork::Data)];
let mut stream = archive.open_fork(&entry, Fork::Data).unwrap().verifying();
assert_ok!(stream.read_exact(&mut data));
let mut archive_data = archive.into_inner().into_inner();
archive_data[offset_in_archive as usize + 12] = 0xAB;
let reader = io::Cursor::new(archive_data);
let mut archive = sit::Archive::try_from(reader).unwrap();
let mut stream = archive.open_fork(&entry, Fork::Data).unwrap().verifying();
assert_err!(stream.read_exact(&mut data));
}
#[test]
fn full_verification() {
let mut fixture = open_fixture("StuffIt 1.10 Moby Dick.sit");
assert_ok!(fixture.verify());
}
#[test]
fn edge_cases() {
let mut fixture = open_fixture("StuffIt 1.10 edge cases.sit");
assert_ok!(fixture.verify());
}
#[test]
fn stream_validation() {
let mut archive = open_fixture("StuffIt 1.10 Moby Dick.sit");
let entry = archive
.iter()
.find(|e| e.is_file() && e.name() == "00b Title.txt")
.unwrap();
assert_ok!(
archive
.open_fork(&entry, Fork::Data)
.unwrap()
.verifying()
.slurp()
);
assert_ok!(
archive
.open_fork(&entry, Fork::Resource)
.unwrap()
.verifying()
.slurp()
);
}
}
mod stuffit_deluxe_4_5 {
use super::*;
#[test]
fn full_verification() {
let mut fixture = open_fixture("StuffIt DLX 4.5.sit");
assert_ok!(fixture.verify());
}
#[test]
fn offset_after_archive_header() {
let mut archive = open_fixture("StuffIt DLX 4.5 Offset.sit");
assert_ok!(archive.verify());
}
#[test]
fn encrypted_entries() {
let mut archive = open_fixture("StuffIt DLX 4.5 Encrypted.sit");
assert!(matches!(
archive.verify(),
Err(Error::UnsupportedFeature(UnsupportedFeature::Encryption))
));
}
#[test]
fn self_extracting() {
let mut archive = open_fixture("StuffIt DLX 4.5 Self-Extracting.sea");
assert_ok!(archive.verify());
}
#[test]
fn entry_count() {
let mut archive = open_fixture("StuffIt DLX 4.5.sit");
let files = archive.iter().filter(|e| matches!(e, Entry::File(_)));
assert_eq!(files.count(), 144);
archive.reset().unwrap();
let directories = archive.iter().filter(|e| matches!(e, Entry::Directory(_)));
assert_eq!(directories.count(), 6);
}
}
mod stuffit_deluxe_5_5 {
use super::*;
#[test]
fn entry_count() {
let archive = open_fixture("StuffIt DLX 5.5 Moby Dick.sit");
let entry_count = archive.iter().count();
let directory_count = archive.iter().filter(|f| f.is_directory()).count();
let file_count = archive.iter().filter(|f| f.is_file()).count();
assert_eq!(directory_count, 4);
assert_eq!(file_count, 140);
assert_eq!(
entry_count,
file_count + directory_count * 2,
"Should have see one directory-end marker per directory"
);
}
#[test]
fn folder_comment() {
let archive = open_fixture("StuffIt DLX 5.5 Folder Comment.sit");
let folder = archive.iter().find(|e| e.is_directory()).unwrap();
assert_eq!(folder.name(), "Folder with comments");
assert_eq!(folder.comment(), "A folder with a comment!");
let archive = open_fixture("StuffIt DLX 5.5 Folder Comment.sit");
let file = archive.iter().find(|e| e.is_file()).unwrap();
let Entry::File(file) = file else { panic!() };
assert_eq!(file.file_code(), fourcc!("TEXT"));
assert_eq!(file.creator(), fourcc!("ttxt"));
}
#[test]
fn file_comment() {
let archive = open_fixture("StuffIt DLX 5.5 File Comment.sit");
let file = archive.iter().find(|e| e.is_file()).unwrap();
assert_eq!(file.name(), "File with comments.txt");
assert_eq!(file.comment(), "Look! This is a file comment!");
let Entry::File(file) = file else { panic!() };
assert_eq!(file.file_code(), fourcc!("TEXT"));
assert_eq!(file.creator(), fourcc!("ttxt"));
}
#[test]
fn encrypted_entries() {
let mut archive = open_fixture("StuffIt DLX 5.5.sit");
assert!(matches!(
archive.verify(),
Err(Error::UnsupportedFeature(UnsupportedFeature::Encryption))
));
}
}
#[test]
fn stuffit_131_comment() {
let mut archive = open_fixture("StuffIt 1.31 Comment.sit");
assert_ok!(archive.verify());
}
#[test]
fn stuffit_131() {
let mut archive = open_fixture("StuffIt 1.31.sit");
assert_ok!(archive.verify());
}
#[test]
fn stuffit_201_comment() {
let mut archive = open_fixture("StuffIt 2.0.1 Comment.sit");
assert_ok!(archive.verify());
}
#[test]
fn stuffit_201_encryption_methods() {
let mut archive = open_fixture("StuffIt 2.0.1 Encryption Methods.sit");
assert!(matches!(
archive.verify(),
Err(Error::UnsupportedFeature(UnsupportedFeature::Encryption))
));
}
#[test]
fn stuffit_201_compression_methods() {
let mut archive = open_fixture("StuffIt 2.0.1 Compression Methods.sit");
assert_ok!(archive.verify());
}
#[test]
fn stuffit_201_fixed_huffman() {
let mut archive = open_fixture("StuffIt 2.0.1 Fixed Huffman.sit");
assert_ok!(archive.verify());
}
#[test]
fn stuffit_201_signature() {
let mut archive = open_fixture("StuffIt 2.0.1 Signature.sit");
assert_ok!(archive.verify());
}
#[test]
fn stuffit_201() {
let mut archive = open_fixture("StuffIt 2.0.1.sit");
assert_ok!(archive.verify());
}
#[test]
fn stuffit_201_best_guess() {
let mut archive = open_fixture("StuffIt 2.0.1 Best Guess.sit");
assert_ok!(archive.verify());
}
#[test]
fn stuffit_201_better_compression() {
let mut archive = open_fixture("StuffIt 2.0.1 Better Compression.sit");
assert_ok!(archive.verify());
}
#[test]
fn stuffit_201_fast() {
let mut archive = open_fixture("StuffIt 2.0.1 Fast.sit");
assert_ok!(archive.verify());
}
#[test]
fn stuffit_201_faster() {
let mut archive = open_fixture("StuffIt 2.0.1 Faster.sit");
assert_ok!(archive.verify());
}
#[test]
fn stuffit_201_optimal() {
let mut archive = open_fixture("StuffIt 2.0.1 Optimal.sit");
assert_ok!(archive.verify());
}
#[test]
fn stuffit_351() {
let mut archive = open_fixture("StuffIt 3.5.1.sit");
assert_ok!(archive.verify());
}
#[test]
fn stuffit_40() {
let mut archive = open_fixture("StuffIt 4.0.sit");
assert_ok!(archive.verify());
}
#[test]
fn stuffit_45() {
let mut archive = open_fixture("StuffIt 4.5.sit");
assert_ok!(archive.verify());
}
#[test]
fn stuffit_55_comment() {
let mut archive = open_fixture("StuffIt 5.5 Comment.sit");
assert_ok!(archive.verify());
}
#[test]
fn stuffit_55() {
let mut archive = open_fixture("StuffIt 5.5.sit");
assert_ok!(archive.verify());
}
#[test]
fn stuffit_60_receipt() {
let mut archive = open_fixture("StuffIt 6.0 Receipt.sit");
assert_ok!(archive.verify());
}
#[test]
fn stuffit_60() {
let mut archive = open_fixture("StuffIt 6.0.sit");
assert_ok!(archive.verify());
}
#[test]
fn stuffit_703() {
let mut archive = open_fixture("StuffIt 7.0.3.sit");
assert_ok!(archive.verify());
}
#[test]
fn stuffit_703_without_finder_desktop_files() {
let mut archive = open_fixture("StuffIt 7.0.3 wihout Finder.sit");
assert_ok!(archive.verify());
}
#[test]
fn installer_maker_311_project() {
let mut archive = open_fixture("Installer Maker 3.1.1 Project.sit");
assert_ok!(archive.verify());
}
#[test]
fn installer_maker_311_installer() {
let mut archive = open_fixture("Installer Maker 3.1.1 Installer.sit");
assert_ok!(archive.verify());
}
#[test]
fn installer_maker_311_installer_with_multiple_blocks() {
let mut archive = open_fixture("Installer Maker 3.1.1 Installer Multi Block.sit");
assert_ok!(archive.verify());
}
#[test]
fn installer_maker_65_installer() {
let mut archive = open_fixture("Installer Maker 6.5 Installer.sit");
assert_ok!(archive.verify());
}
fn open_fixture_raw(name: &'static str) -> File {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("test/")
.join(name);
if !exists(&path).unwrap() {
panic!("Test fixture {name} does not exist!");
}
std::fs::File::open(path).unwrap()
}
fn open_fixture(name: &'static str) -> Archive<File> {
let file = open_fixture_raw(name);
Archive::try_from(file).unwrap()
}
}