use anyhow::{bail, Result};
use byteorder::{BigEndian, ByteOrder};
use fs_mac::resource_fork::macbinary_crc16;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MacBinaryVersion {
One,
Two,
Three,
}
impl MacBinaryVersion {
pub fn label(self) -> &'static str {
match self {
MacBinaryVersion::One => "MacBinary I",
MacBinaryVersion::Two => "MacBinary II",
MacBinaryVersion::Three => "MacBinary III",
}
}
}
#[derive(Debug, Clone)]
pub struct MacBinaryFile {
pub filename: String,
pub type_code: [u8; 4],
pub creator_code: [u8; 4],
pub finder_flags: u16,
pub finder_location: (i16, i16),
pub finder_folder: i16,
pub create_date: u32,
pub modify_date: u32,
pub version: MacBinaryVersion,
pub data_fork: Vec<u8>,
pub resource_fork: Vec<u8>,
}
const fn pad128(n: usize) -> usize {
n.div_ceil(128) * 128
}
const MAX_FORK_LEN: u32 = 0x007F_FFFF;
fn version_of(bytes: &[u8]) -> MacBinaryVersion {
match bytes[122] {
0x82 => MacBinaryVersion::Three,
0x81 => MacBinaryVersion::Two,
_ => MacBinaryVersion::One,
}
}
fn finder_flags_of(bytes: &[u8], version: MacBinaryVersion) -> u16 {
let hi = bytes[73] as u16;
let lo = match version {
MacBinaryVersion::Two | MacBinaryVersion::Three => bytes[101] as u16,
MacBinaryVersion::One => bytes[74] as u16,
};
(hi << 8) | lo
}
pub fn is_macbinary(bytes: &[u8]) -> Option<MacBinaryVersion> {
if bytes.len() < 128 {
return None;
}
if bytes[0] != 0 || bytes[82] != 0 || bytes[108..116].iter().any(|&b| b != 0) {
return None;
}
let name_len = bytes[1] as usize;
if name_len == 0 || name_len > 63 {
return None;
}
if bytes[2..2 + name_len]
.iter()
.any(|&b| b < 0x20 || b == b':')
{
return None;
}
let data_len = BigEndian::read_u32(&bytes[83..87]);
let rsrc_len = BigEndian::read_u32(&bytes[87..91]);
if data_len > MAX_FORK_LEN || rsrc_len > MAX_FORK_LEN || (data_len == 0 && rsrc_len == 0) {
return None;
}
if !fourcc_plausible(&bytes[65..69]) || !fourcc_plausible(&bytes[69..73]) {
return None;
}
if bytes[122] == 0x82 && &bytes[102..106] == b"mBIN" {
return Some(MacBinaryVersion::Three);
}
let stored_crc = BigEndian::read_u16(&bytes[124..126]);
if stored_crc != 0 && stored_crc == macbinary_crc16(&bytes[0..124]) {
return Some(version_of(bytes));
}
let min = 128usize + data_len as usize + rsrc_len as usize;
let max = 128usize + pad128(data_len as usize) + pad128(rsrc_len as usize);
if (min..=max).contains(&bytes.len()) {
Some(version_of(bytes))
} else {
None
}
}
fn fourcc_plausible(b: &[u8]) -> bool {
b == [0u8; 4] || b.iter().all(|&c| c >= 0x20)
}
pub fn parse(bytes: &[u8]) -> Result<MacBinaryFile> {
if bytes.len() < 128 {
bail!("MacBinary: file too short ({} bytes)", bytes.len());
}
if bytes[0] != 0 || bytes[82] != 0 {
bail!(
"MacBinary: header sanity bytes wrong (b0={:#x}, b82={:#x})",
bytes[0],
bytes[82]
);
}
let name_len = bytes[1] as usize;
if name_len == 0 || name_len > 63 {
bail!("MacBinary: bad filename length {name_len}");
}
let filename = fs_mac::hfs::decode_mac_filename(&bytes[2..2 + name_len]);
let version = version_of(bytes);
let mut type_code = [0u8; 4];
type_code.copy_from_slice(&bytes[65..69]);
let mut creator_code = [0u8; 4];
creator_code.copy_from_slice(&bytes[69..73]);
let finder_flags = finder_flags_of(bytes, version);
let finder_location = (
BigEndian::read_i16(&bytes[75..77]),
BigEndian::read_i16(&bytes[77..79]),
);
let finder_folder = BigEndian::read_i16(&bytes[79..81]);
let data_len = BigEndian::read_u32(&bytes[83..87]) as usize;
let rsrc_len = BigEndian::read_u32(&bytes[87..91]) as usize;
let create_date = BigEndian::read_u32(&bytes[91..95]);
let modify_date = BigEndian::read_u32(&bytes[95..99]);
let sec_hdr_len = BigEndian::read_u16(&bytes[120..122]) as usize;
let data_start = 128 + pad128(sec_hdr_len);
let data_end = data_start + data_len;
if data_end > bytes.len() {
bail!(
"MacBinary: data fork extends past file ({data_end} > {})",
bytes.len()
);
}
let data_fork = bytes[data_start..data_end].to_vec();
let rsrc_start = data_start + pad128(data_len);
let rsrc_end = rsrc_start + rsrc_len;
if rsrc_end > bytes.len() {
bail!(
"MacBinary: resource fork extends past file ({rsrc_end} > {})",
bytes.len()
);
}
let resource_fork = bytes[rsrc_start..rsrc_end].to_vec();
Ok(MacBinaryFile {
filename,
type_code,
creator_code,
finder_flags,
finder_location,
finder_folder,
create_date,
modify_date,
version,
data_fork,
resource_fork,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn build_mb2(
filename: &[u8],
type_code: [u8; 4],
creator: [u8; 4],
finder_flags: u16,
data: &[u8],
rsrc: &[u8],
) -> Vec<u8> {
let mut hdr = [0u8; 128];
hdr[1] = filename.len() as u8;
hdr[2..2 + filename.len()].copy_from_slice(filename);
hdr[65..69].copy_from_slice(&type_code);
hdr[69..73].copy_from_slice(&creator);
hdr[73] = (finder_flags >> 8) as u8;
hdr[101] = (finder_flags & 0xFF) as u8;
BigEndian::write_i16(&mut hdr[75..77], 100); BigEndian::write_i16(&mut hdr[77..79], 200); BigEndian::write_i16(&mut hdr[79..81], -1); BigEndian::write_u32(&mut hdr[83..87], data.len() as u32);
BigEndian::write_u32(&mut hdr[87..91], rsrc.len() as u32);
BigEndian::write_u32(&mut hdr[91..95], 0xAAAA_AAAA); BigEndian::write_u32(&mut hdr[95..99], 0xBBBB_BBBB); hdr[122] = 129; hdr[123] = 129;
let crc = macbinary_crc16(&hdr[0..124]);
BigEndian::write_u16(&mut hdr[124..126], crc);
let mut out = hdr.to_vec();
out.extend_from_slice(data);
while out.len() % 128 != 0 {
out.push(0);
}
out.extend_from_slice(rsrc);
while out.len() % 128 != 0 {
out.push(0);
}
out
}
#[test]
fn parse_extracts_all_fields() {
let data = b"hello mac";
let rsrc = b"RSRC";
let mb = build_mb2(b"Greet", *b"TEXT", *b"ttxt", 0x2000, data, rsrc);
let f = parse(&mb).unwrap();
assert_eq!(f.filename, "Greet");
assert_eq!(&f.type_code, b"TEXT");
assert_eq!(&f.creator_code, b"ttxt");
assert_eq!(f.finder_flags, 0x2000);
assert_eq!(f.finder_location, (100, 200));
assert_eq!(f.finder_folder, -1);
assert_eq!(f.create_date, 0xAAAA_AAAA);
assert_eq!(f.modify_date, 0xBBBB_BBBB);
assert_eq!(f.version, MacBinaryVersion::Two);
assert_eq!(f.data_fork, data);
assert_eq!(f.resource_fork, rsrc);
}
#[test]
fn is_macbinary_accepts_crc_validated_ii() {
let mb = build_mb2(b"x", *b"TEXT", *b"ttxt", 0, b"data", b"rs");
assert_eq!(is_macbinary(&mb), Some(MacBinaryVersion::Two));
}
#[test]
fn is_macbinary_detects_iii_signature() {
let mut mb = build_mb2(b"x", *b"APPL", *b"Po.P", 0x2000, b"data", b"");
mb[122] = 0x82; mb[102..106].copy_from_slice(b"mBIN");
assert_eq!(is_macbinary(&mb), Some(MacBinaryVersion::Three));
}
#[test]
fn parse_accepts_empty_resource_fork() {
let mb = build_mb2(b"img", *b"????", *b"????", 0, b"disk-image-bytes", b"");
let f = parse(&mb).unwrap();
assert!(f.resource_fork.is_empty());
assert_eq!(f.data_fork, b"disk-image-bytes");
}
#[test]
fn is_macbinary_rejects_raw_disk_image() {
let mut buf = vec![0u8; 2048];
buf[0] = 0x4C; buf[1] = 0x4B;
buf[1024] = 0x42;
buf[1025] = 0x44;
assert_eq!(is_macbinary(&buf), None);
}
#[test]
fn is_macbinary_rejects_random_and_short() {
assert_eq!(is_macbinary(&[0u8; 64]), None);
let junk: Vec<u8> = (0..200u8).collect();
assert_eq!(is_macbinary(&junk), None);
}
#[test]
fn parse_rejects_bad_reserved_byte() {
let mut mb = build_mb2(b"x", *b"TEXT", *b"ttxt", 0, b"d", b"");
mb[82] = 0x55; assert!(parse(&mb).is_err());
}
}