use std::io::Cursor;
use dsk_apple::dc42;
use fs_mac::binhex;
use super::compactpro::is_compactpro;
use super::extract::is_stuffitx;
use super::stuffit::{find_sea_archive, is_stuffit};
use super::stuffit5::is_stuffit5;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MacArchiveKind {
BinHexSingleFile,
BinHexOverSit,
BinHexOverSea,
Sit,
Sit5,
Sea,
CompactPro,
BinHexOverCompactPro,
Mar,
MacBinary,
MacZip,
}
impl MacArchiveKind {
pub fn label(self) -> &'static str {
match self {
MacArchiveKind::BinHexSingleFile => "BinHex 4.0",
MacArchiveKind::BinHexOverSit => "StuffIt-over-BinHex",
MacArchiveKind::BinHexOverSea => "SEA-over-BinHex",
MacArchiveKind::Sit => "StuffIt",
MacArchiveKind::Sit5 => "StuffIt 5",
MacArchiveKind::Sea => "SEA",
MacArchiveKind::CompactPro => "Compact Pro",
MacArchiveKind::BinHexOverCompactPro => "Compact-Pro-over-BinHex",
MacArchiveKind::Mar => "MAR",
MacArchiveKind::MacBinary => "MacBinary",
MacArchiveKind::MacZip => "MacZip",
}
}
pub fn is_binhex_wrapped(self) -> bool {
matches!(
self,
MacArchiveKind::BinHexSingleFile
| MacArchiveKind::BinHexOverSit
| MacArchiveKind::BinHexOverSea
| MacArchiveKind::BinHexOverCompactPro
)
}
pub fn is_multi_file(self) -> bool {
!matches!(
self,
MacArchiveKind::BinHexSingleFile | MacArchiveKind::MacBinary
)
}
}
pub fn detect_mac_archive(bytes: &[u8]) -> Option<MacArchiveKind> {
if let Ok(bh) = binhex::parse_binhex(bytes) {
let inner = bh.data_fork.as_slice();
if is_stuffit(inner) || is_stuffit5(inner) {
return Some(MacArchiveKind::BinHexOverSit);
}
if is_compactpro(inner) {
return Some(MacArchiveKind::BinHexOverCompactPro);
}
if find_sea_archive(inner).is_some() {
return Some(MacArchiveKind::BinHexOverSea);
}
return Some(MacArchiveKind::BinHexSingleFile);
}
if is_stuffit(bytes) {
return Some(MacArchiveKind::Sit);
}
if is_stuffit5(bytes) {
return Some(MacArchiveKind::Sit5);
}
if is_stuffitx(bytes) {
return None;
}
if is_compactpro(bytes) {
return Some(MacArchiveKind::CompactPro);
}
if super::mar::is_mar(bytes) {
return Some(MacArchiveKind::Mar);
}
if super::macbinary::is_macbinary(bytes).is_some() {
return Some(MacArchiveKind::MacBinary);
}
if super::maczip::is_maczip(bytes) {
return Some(MacArchiveKind::MacZip);
}
if find_sea_archive(bytes).is_some() {
return Some(MacArchiveKind::Sea);
}
None
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MountableImageKind {
DiskCopy42,
RawHfs,
RawHfsPlus,
}
impl MountableImageKind {
pub fn label(self) -> &'static str {
match self {
MountableImageKind::DiskCopy42 => "DiskCopy 4.2",
MountableImageKind::RawHfs => "raw HFS",
MountableImageKind::RawHfsPlus => "raw HFS+",
}
}
}
pub fn detect_mountable_image(bytes: &[u8]) -> Option<MountableImageKind> {
if dc42::detect_dc42(&mut Cursor::new(bytes), bytes.len() as u64).is_some() {
return Some(MountableImageKind::DiskCopy42);
}
if bytes.len() >= 1026 {
let sig = u16::from_be_bytes([bytes[1024], bytes[1025]]);
return match sig {
0x4244 => Some(MountableImageKind::RawHfs),
0x482B | 0x4858 => Some(MountableImageKind::RawHfsPlus),
_ => None,
};
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn synth_sit_header() -> Vec<u8> {
let mut out = vec![0u8; 14];
out[0..4].copy_from_slice(b"SIT!");
out[10..14].copy_from_slice(b"rLau");
out
}
fn synth_sit5_header() -> Vec<u8> {
b"StuffIt (c)1997-2002 by Aladdin Systems, Inc.".to_vec()
}
fn synth_sea_blob() -> Vec<u8> {
let mut out = vec![0u8; 256];
out[100..104].copy_from_slice(b"SIT!");
out[110..114].copy_from_slice(b"rLau");
out
}
fn wrap_binhex(name: &str, data_fork: Vec<u8>) -> Vec<u8> {
let bh = fs_mac::binhex::BinHexFile {
name: name.to_string(),
type_code: [0; 4],
creator_code: [0; 4],
flags: 0,
data_fork,
resource_fork: Vec::new(),
};
fs_mac::binhex::build_binhex(&bh).into_bytes()
}
#[test]
fn detects_mac_archive_kinds() {
let cases: Vec<(Vec<u8>, MacArchiveKind)> = vec![
(synth_sit_header(), MacArchiveKind::Sit),
(synth_sit5_header(), MacArchiveKind::Sit5),
(synth_sea_blob(), MacArchiveKind::Sea),
(
wrap_binhex("Document.txt", b"hello world".to_vec()),
MacArchiveKind::BinHexSingleFile,
),
(
wrap_binhex("Bundle.sit", synth_sit_header()),
MacArchiveKind::BinHexOverSit,
),
(
wrap_binhex("Bundle.sit", synth_sit5_header()),
MacArchiveKind::BinHexOverSit,
),
(
wrap_binhex("Installer.sea", synth_sea_blob()),
MacArchiveKind::BinHexOverSea,
),
];
for (input, expected) in cases {
assert_eq!(detect_mac_archive(&input), Some(expected));
}
}
#[test]
fn returns_none_for_random_bytes() {
let junk = (0..200u8).collect::<Vec<u8>>();
assert_eq!(detect_mac_archive(&junk), None);
}
#[test]
fn returns_none_for_empty() {
assert_eq!(detect_mac_archive(&[]), None);
}
#[test]
fn returns_none_for_stuffitx() {
let mut sx = b"StuffIt!".to_vec();
sx.extend_from_slice(&[0u8; 32]);
assert_eq!(detect_mac_archive(&sx), None);
}
#[test]
fn label_is_human_readable() {
assert!(MacArchiveKind::Sit.label().is_ascii());
assert!(MacArchiveKind::BinHexOverSit.label().is_ascii());
assert!(MacArchiveKind::Sea.label().is_ascii());
}
#[test]
fn is_binhex_wrapped_covers_the_three_hqx_kinds() {
for k in [
MacArchiveKind::BinHexSingleFile,
MacArchiveKind::BinHexOverSit,
MacArchiveKind::BinHexOverSea,
] {
assert!(k.is_binhex_wrapped(), "{k:?}");
}
for k in [
MacArchiveKind::Sit,
MacArchiveKind::Sit5,
MacArchiveKind::Sea,
] {
assert!(!k.is_binhex_wrapped(), "{k:?}");
}
}
#[test]
fn mountable_image_detects_raw_hfs_at_byte_1024() {
let mut buf = vec![0u8; 2048];
buf[1024] = 0x42;
buf[1025] = 0x44;
assert_eq!(
detect_mountable_image(&buf),
Some(MountableImageKind::RawHfs)
);
}
#[test]
fn mountable_image_detects_hfs_plus_signatures() {
for sig in [0x482Bu16, 0x4858u16] {
let mut buf = vec![0u8; 2048];
buf[1024..1026].copy_from_slice(&sig.to_be_bytes());
assert_eq!(
detect_mountable_image(&buf),
Some(MountableImageKind::RawHfsPlus),
"sig 0x{sig:04X}",
);
}
}
#[test]
fn mountable_image_returns_none_for_random_short_buffer() {
assert_eq!(detect_mountable_image(&[0u8; 64]), None);
assert_eq!(detect_mountable_image(&vec![0u8; 4096]), None);
}
#[test]
fn mountable_image_label_is_ascii() {
for kind in [
MountableImageKind::DiskCopy42,
MountableImageKind::RawHfs,
MountableImageKind::RawHfsPlus,
] {
assert!(kind.label().is_ascii(), "{kind:?}");
}
}
#[test]
fn is_multi_file_excludes_binhex_single_file() {
assert!(!MacArchiveKind::BinHexSingleFile.is_multi_file());
for k in [
MacArchiveKind::BinHexOverSit,
MacArchiveKind::BinHexOverSea,
MacArchiveKind::Sit,
MacArchiveKind::Sit5,
MacArchiveKind::Sea,
MacArchiveKind::Mar,
] {
assert!(k.is_multi_file(), "{k:?}");
}
}
#[test]
fn detects_macbinary_and_not_raw_disk_image() {
use byteorder::{BigEndian, ByteOrder};
let mut hdr = [0u8; 128];
hdr[1] = 1;
hdr[2] = b'x';
hdr[65..69].copy_from_slice(b"TEXT");
hdr[69..73].copy_from_slice(b"ttxt");
BigEndian::write_u32(&mut hdr[83..87], 4); hdr[122] = 129;
hdr[123] = 129;
let crc = fs_mac::resource_fork::macbinary_crc16(&hdr[0..124]);
BigEndian::write_u16(&mut hdr[124..126], crc);
let mut mb = hdr.to_vec();
mb.extend_from_slice(b"DATA");
while mb.len() % 128 != 0 {
mb.push(0);
}
assert_eq!(detect_mac_archive(&mb), Some(MacArchiveKind::MacBinary));
assert!(!MacArchiveKind::MacBinary.is_multi_file());
let mut hfs = vec![0u8; 2048];
hfs[0] = 0x4C;
hfs[1] = 0x4B;
hfs[1024] = 0x42;
hfs[1025] = 0x44;
assert_eq!(detect_mac_archive(&hfs), None);
}
#[test]
fn detects_mar() {
let mar = crate::mar::build_archive(
"x",
&[crate::stuffit::StuffItInputNode::File(
crate::stuffit::StuffItInput {
name: "f".into(),
type_code: *b"TEXT",
creator_code: *b"ttxt",
finder_flags: 0,
create_date: 0,
mod_date: 0,
data_fork: b"hi".to_vec(),
resource_fork: Vec::new(),
},
)],
)
.unwrap();
assert_eq!(detect_mac_archive(&mar), Some(MacArchiveKind::Mar));
assert!(!MacArchiveKind::Mar.is_binhex_wrapped());
assert_eq!(MacArchiveKind::Mar.label(), "MAR");
}
}