1#![cfg_attr(feature = "no_std", no_std)]
2#![deny(missing_docs)]
3
4use core::fmt::{self, Display, Formatter};
21
22use crc::{Crc, CRC_16_XMODEM};
23#[cfg(feature = "no_std")]
24use heapless::String;
25
26use crate::binary::read::{ReadBinary, ReadBinaryDep, ReadCtxt, ReadFrom, ReadScope};
27use crate::binary::{NumFrom, U32Be};
28use crate::macroman::FromMacRoman;
29
30pub(crate) mod binary;
31pub(crate) mod error;
32mod macroman;
33pub mod resource;
34#[cfg(test)]
35mod test;
36#[cfg(target_family = "wasm")]
37mod wasm;
38
39const MBIN_SIG: u32 = u32::from_be_bytes(*b"mBIN");
40
41pub use crate::error::ParseError;
42pub use crate::resource::ResourceFork;
43
44#[derive(Copy, Clone, Eq, PartialEq)]
49pub struct FourCC(pub u32);
50
51pub struct MacBinary<'a> {
53 version: Version,
54 header: Header<'a>,
55 data_fork: &'a [u8],
56 rsrc_fork: &'a [u8],
57}
58
59#[allow(unused)]
61struct Header<'a> {
62 filename: &'a [u8],
63 secondary_header_len: u16,
64 data_fork_len: u32,
65 rsrc_fork_len: u32,
66 file_type: FourCC,
67 file_creator: FourCC,
68 finder_flags: u8,
69 vpos: u16,
70 hpos: u16,
71 window_or_folder_id: u16,
72 protected: bool,
73 created: u32,
74 modified: u32,
75 comment_len: u16,
76 finder_flags2: u8,
77 signature: FourCC,
78 script: u8,
92 extended_finder_flags: u8,
93 version: u8,
94 min_version: u8,
95 crc: u16,
96}
97
98#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Copy, Clone)]
100pub enum Version {
101 I = 1,
103 II = 2,
105 III = 3,
107}
108
109pub fn detect(data: &[u8]) -> Option<Version> {
111 (data.len() >= 128 && data[0] == 0).then_some(())?;
113
114 if ReadScope::new(&data[102..][..4]).read::<FourCC>() == Ok(FourCC(MBIN_SIG)) {
116 return Some(Version::III);
117 }
118
119 if data[74] != 0 || data[82] != 0 {
121 return None;
122 }
123
124 let crc = u16::from_be_bytes(data[124..][..2].try_into().unwrap());
125 if crc == calc_crc(&data[..124]) {
126 return Some(Version::II);
127 }
128
129 let data_fork_len = u32::from_be_bytes(data[83..][..4].try_into().unwrap());
135 let rsrc_fork_len = u32::from_be_bytes(data[87..][..4].try_into().unwrap());
136 let macbinary1 = data[101..=125].iter().all(|byte| *byte == 0)
137 && (1..=63).contains(&data[1])
138 && data_fork_len <= 0x007F_FFFF
139 && rsrc_fork_len <= 0x007F_FFFF;
140
141 if macbinary1 {
142 Some(Version::I)
143 } else {
144 None
145 }
146}
147
148pub fn parse(data: &[u8]) -> Result<MacBinary<'_>, ParseError> {
150 let Some(version) = detect(data) else {
151 return Err(ParseError::BadVersion) };
153 ReadScope::new(data).read_dep::<MacBinary<'_>>(version)
154}
155
156impl ReadBinary for Header<'_> {
157 type HostType<'a> = Header<'a>;
158
159 fn read<'a>(ctxt: &mut ReadCtxt<'a>) -> Result<Self::HostType<'a>, ParseError> {
160 let _ = ctxt.read_u8()?;
162 let filename_len = ctxt.read_u8()?;
164 ctxt.check((1..=31).contains(&filename_len))?; let filename_data = ctxt.read_slice(63)?;
167 let file_type = ctxt.read::<FourCC>()?;
169 let file_creator = ctxt.read::<FourCC>()?;
171 let finder_flags = ctxt.read_u8()?;
173 let _ = ctxt.read_u8()?;
175 let vpos = ctxt.read_u16be()?;
177 let hpos = ctxt.read_u16be()?;
179 let window_or_folder_id = ctxt.read_u16be()?;
181 let protected = ctxt.read_u8()?;
183 let _ = ctxt.read_u8()?;
185 let data_fork_len = ctxt.read_u32be()?;
187 let rsrc_fork_len = ctxt.read_u32be()?;
189 let created = ctxt.read_u32be()?;
191 let modified = ctxt.read_u32be()?;
193 let comment_len = ctxt.read_u16be()?;
195 let finder_flags2 = ctxt.read_u8()?;
197 let signature = ctxt.read::<FourCC>()?;
199 let script = ctxt.read_u8()?;
201 let extended_finder_flags = ctxt.read_u8()?;
203 let _ = ctxt.read_slice(8)?;
205 let _ = ctxt.read_u32be()?;
207 let secondary_header_len = ctxt.read_u16be()?;
209 let version = ctxt.read_u8()?;
211 let min_version = ctxt.read_u8()?;
214 let crc = ctxt.read_u16be()?;
216 let _ = ctxt.read_u16be()?;
218
219 Ok(Header {
220 filename: &filename_data[..usize::from(filename_len)],
221 file_type,
222 file_creator,
223 finder_flags,
224 vpos,
225 hpos,
226 window_or_folder_id,
227 protected: protected != 0,
228 data_fork_len,
229 rsrc_fork_len,
230 created,
231 modified,
232 comment_len,
233 finder_flags2,
234 signature,
235 script,
236 extended_finder_flags,
237 secondary_header_len,
238 version,
239 min_version,
240 crc,
241 })
242 }
243}
244
245impl ReadBinaryDep for MacBinary<'_> {
246 type Args<'a> = Version;
247 type HostType<'a> = MacBinary<'a>;
248
249 fn read_dep<'a>(
250 ctxt: &mut ReadCtxt<'a>,
251 version: Version,
252 ) -> Result<Self::HostType<'a>, ParseError> {
253 let crc_data = ctxt.scope().data().get(..124).ok_or(ParseError::BadEof)?;
254
255 let header = ctxt.read::<Header<'_>>()?;
262
263 let crc = calc_crc(crc_data);
265 if version >= Version::II && crc != header.crc {
266 return Err(ParseError::CrcMismatch);
267 }
268
269 let _ = ctxt.read_slice(usize::from(next_u16_multiple_of_128(
271 header.secondary_header_len,
272 )?))?;
273
274 let data_fork = ctxt.read_slice(usize::num_from(header.data_fork_len))?;
276
277 let padding = next_u32_multiple_of_128(header.data_fork_len)? - header.data_fork_len;
279 let _ = ctxt.read_slice(usize::num_from(padding))?;
280
281 let rsrc_fork = ctxt.read_slice(usize::num_from(header.rsrc_fork_len))?;
283
284 Ok(MacBinary {
285 version,
286 header,
287 data_fork,
288 rsrc_fork,
289 })
290 }
291}
292
293impl MacBinary<'_> {
294 pub fn version(&self) -> Version {
296 self.version
297 }
298
299 #[cfg(not(feature = "no_std"))]
301 pub fn filename(&self) -> String {
302 String::from_macroman(self.header.filename)
313 }
314
315 #[cfg(feature = "no_std")]
322 pub fn filename<const N: usize>(&self) -> Option<String<N>> {
323 String::try_from_macroman(self.header.filename)
325 }
326
327 pub fn filename_bytes(&self) -> &[u8] {
329 self.header.filename
330 }
331
332 pub fn file_creator(&self) -> FourCC {
334 self.header.file_creator
335 }
336
337 pub fn file_type(&self) -> FourCC {
339 self.header.file_type
340 }
341
342 pub fn created(&self) -> u32 {
344 mactime(self.header.created)
345 }
346
347 pub fn modified(&self) -> u32 {
349 mactime(self.header.modified)
350 }
351
352 pub fn data_fork(&self) -> &[u8] {
354 self.data_fork
355 }
356
357 pub fn resource_fork_raw(&self) -> &[u8] {
359 self.rsrc_fork
360 }
361
362 pub fn resource_fork(&self) -> Result<Option<ResourceFork<'_>>, ParseError> {
367 if self.rsrc_fork.is_empty() {
368 return Ok(None);
369 }
370
371 ResourceFork::new(self.rsrc_fork).map(Some)
372 }
373}
374
375impl ReadFrom for FourCC {
376 type ReadType = U32Be;
377
378 fn from(value: u32) -> Self {
379 FourCC(value)
380 }
381}
382
383impl Display for FourCC {
384 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
385 let tag = self.0;
386 let bytes = tag.to_be_bytes();
387 if bytes.iter().all(|c| c.is_ascii() && !c.is_ascii_control()) {
388 let s = core::str::from_utf8(&bytes).unwrap(); s.fmt(f)
390 } else {
391 write!(f, "0x{:08x}", tag)
392 }
393 }
394}
395
396impl fmt::Debug for FourCC {
397 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
398 write!(f, "'{}'", self)
399 }
400}
401
402fn next_u16_multiple_of_128(value: u16) -> Result<u16, ParseError> {
403 let rem = value % 128;
404 if rem == 0 {
405 Ok(value)
406 } else {
407 value.checked_add(128 - rem).ok_or(ParseError::Overflow)
408 }
409}
410
411fn next_u32_multiple_of_128(value: u32) -> Result<u32, ParseError> {
412 let rem = value % 128;
413 if rem == 0 {
414 Ok(value)
415 } else {
416 value.checked_add(128 - rem).ok_or(ParseError::Overflow)
417 }
418}
419
420fn mactime(timestamp: u32) -> u32 {
424 const OFFSET: u32 = 66 * 365 * 86400 + (17 * 86400);
426 timestamp.wrapping_sub(OFFSET)
427}
428
429fn calc_crc(data: &[u8]) -> u16 {
430 let crc: Crc<u16> = Crc::<u16>::new(&CRC_16_XMODEM);
431 crc.checksum(data)
432}
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437 use crate::test::read_fixture;
438
439 #[test]
440 fn test_next_multiple() {
441 assert_eq!(next_u16_multiple_of_128(0), Ok(0));
442 assert_eq!(next_u16_multiple_of_128(3), Ok(128));
443 assert_eq!(next_u16_multiple_of_128(128), Ok(128));
444 assert_eq!(next_u16_multiple_of_128(129), Ok(256));
445
446 assert_eq!(next_u32_multiple_of_128(0), Ok(0));
447 assert_eq!(next_u32_multiple_of_128(3), Ok(128));
448 assert_eq!(next_u32_multiple_of_128(128), Ok(128));
449 assert_eq!(next_u32_multiple_of_128(129), Ok(256));
450 }
451
452 #[test]
453 fn test_next_multiple_overflow() {
454 assert_eq!(
455 next_u16_multiple_of_128(u16::MAX - 3),
456 Err(ParseError::Overflow)
457 );
458 assert_eq!(
459 next_u32_multiple_of_128(u32::MAX - 3),
460 Err(ParseError::Overflow)
461 );
462 }
463
464 fn check_text_file(file: &MacBinary, version: Version) {
465 assert_eq!(file.version(), version);
466 assert_eq!(file.filename(), "Text File");
467 assert_eq!(file.file_type(), FourCC(u32::from_be_bytes(*b"TEXT")));
468 assert_eq!(file.file_creator(), FourCC(u32::from_be_bytes(*b"R*ch"))); assert_eq!(file.data_fork(), b"This is a test file.\r");
470 assert_eq!(file.resource_fork_raw().len(), 1454);
471 }
472
473 #[test]
474 fn test_macbinary_1() {
475 let data = read_fixture("tests/Text File I.Bin");
476 let file = parse(&data).unwrap();
477
478 check_text_file(&file, Version::I);
479 }
480
481 #[test]
482 fn test_macbinary_2() {
483 let data = read_fixture("tests/Text File II.bin");
484 let file = parse(&data).unwrap();
485
486 check_text_file(&file, Version::II);
487 }
488
489 #[test]
490 fn test_macbinary_3() {
491 let data = read_fixture("tests/Text File.bin");
492 let file = parse(&data).unwrap();
493
494 check_text_file(&file, Version::III);
495 }
496
497 #[test]
498 fn test_no_resource_fork() {
499 let data = read_fixture("tests/No resource fork.txt.bin");
500 let file = parse(&data).unwrap();
501
502 assert_eq!(file.version(), Version::III);
503 assert!(file.resource_fork().unwrap().is_none());
504 }
505
506 #[test]
507 fn test_dates() {
508 let data = read_fixture("tests/Date Test.bin");
509 let file = parse(&data).unwrap();
510
511 assert_eq!(file.version(), Version::III);
512 assert_eq!(file.filename(), "Date Test");
513 assert_eq!(file.file_type(), FourCC(u32::from_be_bytes(*b"TEXT")));
514 assert_eq!(file.file_creator(), FourCC(u32::from_be_bytes(*b"MPS "))); assert_eq!(file.data_fork(), b"Sunday, 26 March 2023 10:00:52 AM\r");
516 assert_eq!(file.created(), 1679824852);
517 assert_eq!(file.modified(), 1679824852);
518 }
519}