1use anyhow::{bail, Result};
31use byteorder::{BigEndian, ByteOrder};
32
33use fs_mac::resource_fork::macbinary_crc16;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum MacBinaryVersion {
38 One,
40 Two,
42 Three,
44}
45
46impl MacBinaryVersion {
47 pub fn label(self) -> &'static str {
49 match self {
50 MacBinaryVersion::One => "MacBinary I",
51 MacBinaryVersion::Two => "MacBinary II",
52 MacBinaryVersion::Three => "MacBinary III",
53 }
54 }
55}
56
57#[derive(Debug, Clone)]
60pub struct MacBinaryFile {
61 pub filename: String,
63 pub type_code: [u8; 4],
65 pub creator_code: [u8; 4],
67 pub finder_flags: u16,
70 pub finder_location: (i16, i16),
72 pub finder_folder: i16,
74 pub create_date: u32,
76 pub modify_date: u32,
78 pub version: MacBinaryVersion,
80 pub data_fork: Vec<u8>,
82 pub resource_fork: Vec<u8>,
84}
85
86const fn pad128(n: usize) -> usize {
89 n.div_ceil(128) * 128
90}
91
92const MAX_FORK_LEN: u32 = 0x007F_FFFF;
94
95fn version_of(bytes: &[u8]) -> MacBinaryVersion {
101 match bytes[122] {
102 0x82 => MacBinaryVersion::Three,
103 0x81 => MacBinaryVersion::Two,
104 _ => MacBinaryVersion::One,
105 }
106}
107
108fn finder_flags_of(bytes: &[u8], version: MacBinaryVersion) -> u16 {
110 let hi = bytes[73] as u16;
111 let lo = match version {
112 MacBinaryVersion::Two | MacBinaryVersion::Three => bytes[101] as u16,
114 MacBinaryVersion::One => bytes[74] as u16,
116 };
117 (hi << 8) | lo
118}
119
120pub fn is_macbinary(bytes: &[u8]) -> Option<MacBinaryVersion> {
130 if bytes.len() < 128 {
131 return None;
132 }
133 if bytes[0] != 0 || bytes[82] != 0 || bytes[108..116].iter().any(|&b| b != 0) {
136 return None;
137 }
138 let name_len = bytes[1] as usize;
139 if name_len == 0 || name_len > 63 {
140 return None;
141 }
142 if bytes[2..2 + name_len]
145 .iter()
146 .any(|&b| b < 0x20 || b == b':')
147 {
148 return None;
149 }
150 let data_len = BigEndian::read_u32(&bytes[83..87]);
151 let rsrc_len = BigEndian::read_u32(&bytes[87..91]);
152 if data_len > MAX_FORK_LEN || rsrc_len > MAX_FORK_LEN || (data_len == 0 && rsrc_len == 0) {
153 return None;
154 }
155 if !fourcc_plausible(&bytes[65..69]) || !fourcc_plausible(&bytes[69..73]) {
156 return None;
157 }
158
159 if bytes[122] == 0x82 && &bytes[102..106] == b"mBIN" {
162 return Some(MacBinaryVersion::Three);
163 }
164 let stored_crc = BigEndian::read_u16(&bytes[124..126]);
167 if stored_crc != 0 && stored_crc == macbinary_crc16(&bytes[0..124]) {
168 return Some(version_of(bytes));
169 }
170 let min = 128usize + data_len as usize + rsrc_len as usize;
175 let max = 128usize + pad128(data_len as usize) + pad128(rsrc_len as usize);
176 if (min..=max).contains(&bytes.len()) {
177 Some(version_of(bytes))
178 } else {
179 None
180 }
181}
182
183fn fourcc_plausible(b: &[u8]) -> bool {
186 b == [0u8; 4] || b.iter().all(|&c| c >= 0x20)
187}
188
189pub fn parse(bytes: &[u8]) -> Result<MacBinaryFile> {
194 if bytes.len() < 128 {
195 bail!("MacBinary: file too short ({} bytes)", bytes.len());
196 }
197 if bytes[0] != 0 || bytes[82] != 0 {
199 bail!(
200 "MacBinary: header sanity bytes wrong (b0={:#x}, b82={:#x})",
201 bytes[0],
202 bytes[82]
203 );
204 }
205 let name_len = bytes[1] as usize;
206 if name_len == 0 || name_len > 63 {
207 bail!("MacBinary: bad filename length {name_len}");
208 }
209 let filename = fs_mac::hfs::decode_mac_filename(&bytes[2..2 + name_len]);
210
211 let version = version_of(bytes);
212
213 let mut type_code = [0u8; 4];
214 type_code.copy_from_slice(&bytes[65..69]);
215 let mut creator_code = [0u8; 4];
216 creator_code.copy_from_slice(&bytes[69..73]);
217
218 let finder_flags = finder_flags_of(bytes, version);
219 let finder_location = (
220 BigEndian::read_i16(&bytes[75..77]),
221 BigEndian::read_i16(&bytes[77..79]),
222 );
223 let finder_folder = BigEndian::read_i16(&bytes[79..81]);
224 let data_len = BigEndian::read_u32(&bytes[83..87]) as usize;
225 let rsrc_len = BigEndian::read_u32(&bytes[87..91]) as usize;
226 let create_date = BigEndian::read_u32(&bytes[91..95]);
227 let modify_date = BigEndian::read_u32(&bytes[95..99]);
228
229 let sec_hdr_len = BigEndian::read_u16(&bytes[120..122]) as usize;
232 let data_start = 128 + pad128(sec_hdr_len);
233 let data_end = data_start + data_len;
234 if data_end > bytes.len() {
235 bail!(
236 "MacBinary: data fork extends past file ({data_end} > {})",
237 bytes.len()
238 );
239 }
240 let data_fork = bytes[data_start..data_end].to_vec();
241
242 let rsrc_start = data_start + pad128(data_len);
243 let rsrc_end = rsrc_start + rsrc_len;
244 if rsrc_end > bytes.len() {
245 bail!(
246 "MacBinary: resource fork extends past file ({rsrc_end} > {})",
247 bytes.len()
248 );
249 }
250 let resource_fork = bytes[rsrc_start..rsrc_end].to_vec();
251
252 Ok(MacBinaryFile {
253 filename,
254 type_code,
255 creator_code,
256 finder_flags,
257 finder_location,
258 finder_folder,
259 create_date,
260 modify_date,
261 version,
262 data_fork,
263 resource_fork,
264 })
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 fn build_mb2(
273 filename: &[u8],
274 type_code: [u8; 4],
275 creator: [u8; 4],
276 finder_flags: u16,
277 data: &[u8],
278 rsrc: &[u8],
279 ) -> Vec<u8> {
280 let mut hdr = [0u8; 128];
281 hdr[1] = filename.len() as u8;
282 hdr[2..2 + filename.len()].copy_from_slice(filename);
283 hdr[65..69].copy_from_slice(&type_code);
284 hdr[69..73].copy_from_slice(&creator);
285 hdr[73] = (finder_flags >> 8) as u8;
286 hdr[101] = (finder_flags & 0xFF) as u8;
287 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);
291 BigEndian::write_u32(&mut hdr[87..91], rsrc.len() as u32);
292 BigEndian::write_u32(&mut hdr[91..95], 0xAAAA_AAAA); BigEndian::write_u32(&mut hdr[95..99], 0xBBBB_BBBB); hdr[122] = 129; hdr[123] = 129;
296 let crc = macbinary_crc16(&hdr[0..124]);
297 BigEndian::write_u16(&mut hdr[124..126], crc);
298
299 let mut out = hdr.to_vec();
300 out.extend_from_slice(data);
301 while out.len() % 128 != 0 {
302 out.push(0);
303 }
304 out.extend_from_slice(rsrc);
305 while out.len() % 128 != 0 {
306 out.push(0);
307 }
308 out
309 }
310
311 #[test]
312 fn parse_extracts_all_fields() {
313 let data = b"hello mac";
314 let rsrc = b"RSRC";
315 let mb = build_mb2(b"Greet", *b"TEXT", *b"ttxt", 0x2000, data, rsrc);
316 let f = parse(&mb).unwrap();
317 assert_eq!(f.filename, "Greet");
318 assert_eq!(&f.type_code, b"TEXT");
319 assert_eq!(&f.creator_code, b"ttxt");
320 assert_eq!(f.finder_flags, 0x2000);
321 assert_eq!(f.finder_location, (100, 200));
322 assert_eq!(f.finder_folder, -1);
323 assert_eq!(f.create_date, 0xAAAA_AAAA);
324 assert_eq!(f.modify_date, 0xBBBB_BBBB);
325 assert_eq!(f.version, MacBinaryVersion::Two);
326 assert_eq!(f.data_fork, data);
327 assert_eq!(f.resource_fork, rsrc);
328 }
329
330 #[test]
331 fn is_macbinary_accepts_crc_validated_ii() {
332 let mb = build_mb2(b"x", *b"TEXT", *b"ttxt", 0, b"data", b"rs");
333 assert_eq!(is_macbinary(&mb), Some(MacBinaryVersion::Two));
334 }
335
336 #[test]
337 fn is_macbinary_detects_iii_signature() {
338 let mut mb = build_mb2(b"x", *b"APPL", *b"Po.P", 0x2000, b"data", b"");
339 mb[122] = 0x82; mb[102..106].copy_from_slice(b"mBIN");
341 assert_eq!(is_macbinary(&mb), Some(MacBinaryVersion::Three));
343 }
344
345 #[test]
346 fn parse_accepts_empty_resource_fork() {
347 let mb = build_mb2(b"img", *b"????", *b"????", 0, b"disk-image-bytes", b"");
350 let f = parse(&mb).unwrap();
351 assert!(f.resource_fork.is_empty());
352 assert_eq!(f.data_fork, b"disk-image-bytes");
353 }
354
355 #[test]
356 fn is_macbinary_rejects_raw_disk_image() {
357 let mut buf = vec![0u8; 2048];
360 buf[0] = 0x4C; buf[1] = 0x4B;
362 buf[1024] = 0x42;
363 buf[1025] = 0x44;
364 assert_eq!(is_macbinary(&buf), None);
365 }
366
367 #[test]
368 fn is_macbinary_rejects_random_and_short() {
369 assert_eq!(is_macbinary(&[0u8; 64]), None);
370 let junk: Vec<u8> = (0..200u8).collect();
371 assert_eq!(is_macbinary(&junk), None);
372 }
373
374 #[test]
375 fn parse_rejects_bad_reserved_byte() {
376 let mut mb = build_mb2(b"x", *b"TEXT", *b"ttxt", 0, b"d", b"");
377 mb[82] = 0x55; assert!(parse(&mb).is_err());
379 }
380}