#![cfg_attr(feature = "no_std", no_std)]
#![deny(missing_docs)]
use core::fmt::{self, Display, Formatter};
use crc::{Crc, CRC_16_XMODEM};
#[cfg(feature = "no_std")]
use heapless::String;
use crate::binary::read::{ReadBinary, ReadBinaryDep, ReadCtxt, ReadFrom, ReadScope};
use crate::binary::{NumFrom, U32Be};
use crate::macroman::FromMacRoman;
pub(crate) mod binary;
pub(crate) mod error;
mod macroman;
pub mod resource;
#[cfg(test)]
mod test;
#[cfg(target_family = "wasm")]
mod wasm;
const MBIN_SIG: u32 = u32::from_be_bytes(*b"mBIN");
pub use crate::error::ParseError;
pub use crate::resource::ResourceFork;
#[derive(Copy, Clone, Eq, PartialEq)]
pub struct FourCC(pub u32);
pub struct MacBinary<'a> {
version: Version,
header: Header<'a>,
data_fork: &'a [u8],
rsrc_fork: &'a [u8],
}
#[allow(unused)]
struct Header<'a> {
filename: &'a [u8],
secondary_header_len: u16,
data_fork_len: u32,
rsrc_fork_len: u32,
file_type: FourCC,
file_creator: FourCC,
finder_flags: u8,
vpos: u16,
hpos: u16,
window_or_folder_id: u16,
protected: bool,
created: u32,
modified: u32,
comment_len: u16,
finder_flags2: u8,
signature: FourCC,
script: u8,
extended_finder_flags: u8,
version: u8,
min_version: u8,
crc: u16,
}
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Copy, Clone)]
pub enum Version {
I = 1,
II = 2,
III = 3,
}
pub fn detect(data: &[u8]) -> Option<Version> {
(data.len() >= 128 && data[0] == 0).then_some(())?;
if ReadScope::new(&data[102..][..4]).read::<FourCC>() == Ok(FourCC(MBIN_SIG)) {
return Some(Version::III);
}
if data[74] != 0 || data[82] != 0 {
return None;
}
let crc = u16::from_be_bytes(data[124..][..2].try_into().unwrap());
if crc == calc_crc(&data[..124]) {
return Some(Version::II);
}
let data_fork_len = u32::from_be_bytes(data[83..][..4].try_into().unwrap());
let rsrc_fork_len = u32::from_be_bytes(data[87..][..4].try_into().unwrap());
let macbinary1 = data[101..=125].iter().all(|byte| *byte == 0)
&& (1..=63).contains(&data[1])
&& data_fork_len <= 0x007F_FFFF
&& rsrc_fork_len <= 0x007F_FFFF;
if macbinary1 {
Some(Version::I)
} else {
None
}
}
pub fn parse(data: &[u8]) -> Result<MacBinary<'_>, ParseError> {
let Some(version) = detect(data) else {
return Err(ParseError::BadVersion) };
ReadScope::new(data).read_dep::<MacBinary<'_>>(version)
}
impl ReadBinary for Header<'_> {
type HostType<'a> = Header<'a>;
fn read<'a>(ctxt: &mut ReadCtxt<'a>) -> Result<Self::HostType<'a>, ParseError> {
let _ = ctxt.read_u8()?;
let filename_len = ctxt.read_u8()?;
ctxt.check((1..=31).contains(&filename_len))?; let filename_data = ctxt.read_slice(63)?;
let file_type = ctxt.read::<FourCC>()?;
let file_creator = ctxt.read::<FourCC>()?;
let finder_flags = ctxt.read_u8()?;
let _ = ctxt.read_u8()?;
let vpos = ctxt.read_u16be()?;
let hpos = ctxt.read_u16be()?;
let window_or_folder_id = ctxt.read_u16be()?;
let protected = ctxt.read_u8()?;
let _ = ctxt.read_u8()?;
let data_fork_len = ctxt.read_u32be()?;
let rsrc_fork_len = ctxt.read_u32be()?;
let created = ctxt.read_u32be()?;
let modified = ctxt.read_u32be()?;
let comment_len = ctxt.read_u16be()?;
let finder_flags2 = ctxt.read_u8()?;
let signature = ctxt.read::<FourCC>()?;
let script = ctxt.read_u8()?;
let extended_finder_flags = ctxt.read_u8()?;
let _ = ctxt.read_slice(8)?;
let _ = ctxt.read_u32be()?;
let secondary_header_len = ctxt.read_u16be()?;
let version = ctxt.read_u8()?;
let min_version = ctxt.read_u8()?;
let crc = ctxt.read_u16be()?;
let _ = ctxt.read_u16be()?;
Ok(Header {
filename: &filename_data[..usize::from(filename_len)],
file_type,
file_creator,
finder_flags,
vpos,
hpos,
window_or_folder_id,
protected: protected != 0,
data_fork_len,
rsrc_fork_len,
created,
modified,
comment_len,
finder_flags2,
signature,
script,
extended_finder_flags,
secondary_header_len,
version,
min_version,
crc,
})
}
}
impl ReadBinaryDep for MacBinary<'_> {
type Args<'a> = Version;
type HostType<'a> = MacBinary<'a>;
fn read_dep<'a>(
ctxt: &mut ReadCtxt<'a>,
version: Version,
) -> Result<Self::HostType<'a>, ParseError> {
let crc_data = ctxt.scope().data().get(..124).ok_or(ParseError::BadEof)?;
let header = ctxt.read::<Header<'_>>()?;
let crc = calc_crc(crc_data);
if version >= Version::II && crc != header.crc {
return Err(ParseError::CrcMismatch);
}
let _ = ctxt.read_slice(usize::from(next_u16_multiple_of_128(
header.secondary_header_len,
)?))?;
let data_fork = ctxt.read_slice(usize::num_from(header.data_fork_len))?;
let padding = next_u32_multiple_of_128(header.data_fork_len)? - header.data_fork_len;
let _ = ctxt.read_slice(usize::num_from(padding))?;
let rsrc_fork = ctxt.read_slice(usize::num_from(header.rsrc_fork_len))?;
Ok(MacBinary {
version,
header,
data_fork,
rsrc_fork,
})
}
}
impl MacBinary<'_> {
pub fn version(&self) -> Version {
self.version
}
#[cfg(not(feature = "no_std"))]
pub fn filename(&self) -> String {
String::from_macroman(self.header.filename)
}
#[cfg(feature = "no_std")]
pub fn filename<const N: usize>(&self) -> Option<String<N>> {
String::try_from_macroman(self.header.filename)
}
pub fn filename_bytes(&self) -> &[u8] {
self.header.filename
}
pub fn file_creator(&self) -> FourCC {
self.header.file_creator
}
pub fn file_type(&self) -> FourCC {
self.header.file_type
}
pub fn created(&self) -> u32 {
mactime(self.header.created)
}
pub fn modified(&self) -> u32 {
mactime(self.header.modified)
}
pub fn data_fork(&self) -> &[u8] {
self.data_fork
}
pub fn resource_fork_raw(&self) -> &[u8] {
self.rsrc_fork
}
pub fn resource_fork(&self) -> Result<Option<ResourceFork<'_>>, ParseError> {
if self.rsrc_fork.is_empty() {
return Ok(None);
}
ResourceFork::new(self.rsrc_fork).map(Some)
}
}
impl ReadFrom for FourCC {
type ReadType = U32Be;
fn from(value: u32) -> Self {
FourCC(value)
}
}
impl Display for FourCC {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let tag = self.0;
let bytes = tag.to_be_bytes();
if bytes.iter().all(|c| c.is_ascii() && !c.is_ascii_control()) {
let s = core::str::from_utf8(&bytes).unwrap(); s.fmt(f)
} else {
write!(f, "0x{:08x}", tag)
}
}
}
impl fmt::Debug for FourCC {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "'{}'", self)
}
}
fn next_u16_multiple_of_128(value: u16) -> Result<u16, ParseError> {
let rem = value % 128;
if rem == 0 {
Ok(value)
} else {
value.checked_add(128 - rem).ok_or(ParseError::Overflow)
}
}
fn next_u32_multiple_of_128(value: u32) -> Result<u32, ParseError> {
let rem = value % 128;
if rem == 0 {
Ok(value)
} else {
value.checked_add(128 - rem).ok_or(ParseError::Overflow)
}
}
fn mactime(timestamp: u32) -> u32 {
const OFFSET: u32 = 66 * 365 * 86400 + (17 * 86400);
timestamp.wrapping_sub(OFFSET)
}
fn calc_crc(data: &[u8]) -> u16 {
let crc: Crc<u16> = Crc::<u16>::new(&CRC_16_XMODEM);
crc.checksum(data)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test::read_fixture;
#[test]
fn test_next_multiple() {
assert_eq!(next_u16_multiple_of_128(0), Ok(0));
assert_eq!(next_u16_multiple_of_128(3), Ok(128));
assert_eq!(next_u16_multiple_of_128(128), Ok(128));
assert_eq!(next_u16_multiple_of_128(129), Ok(256));
assert_eq!(next_u32_multiple_of_128(0), Ok(0));
assert_eq!(next_u32_multiple_of_128(3), Ok(128));
assert_eq!(next_u32_multiple_of_128(128), Ok(128));
assert_eq!(next_u32_multiple_of_128(129), Ok(256));
}
#[test]
fn test_next_multiple_overflow() {
assert_eq!(
next_u16_multiple_of_128(u16::MAX - 3),
Err(ParseError::Overflow)
);
assert_eq!(
next_u32_multiple_of_128(u32::MAX - 3),
Err(ParseError::Overflow)
);
}
fn check_text_file(file: &MacBinary, version: Version) {
assert_eq!(file.version(), version);
assert_eq!(file.filename(), "Text File");
assert_eq!(file.file_type(), FourCC(u32::from_be_bytes(*b"TEXT")));
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");
assert_eq!(file.resource_fork_raw().len(), 1454);
}
#[test]
fn test_macbinary_1() {
let data = read_fixture("tests/Text File I.Bin");
let file = parse(&data).unwrap();
check_text_file(&file, Version::I);
}
#[test]
fn test_macbinary_2() {
let data = read_fixture("tests/Text File II.bin");
let file = parse(&data).unwrap();
check_text_file(&file, Version::II);
}
#[test]
fn test_macbinary_3() {
let data = read_fixture("tests/Text File.bin");
let file = parse(&data).unwrap();
check_text_file(&file, Version::III);
}
#[test]
fn test_no_resource_fork() {
let data = read_fixture("tests/No resource fork.txt.bin");
let file = parse(&data).unwrap();
assert_eq!(file.version(), Version::III);
assert!(file.resource_fork().unwrap().is_none());
}
#[test]
fn test_dates() {
let data = read_fixture("tests/Date Test.bin");
let file = parse(&data).unwrap();
assert_eq!(file.version(), Version::III);
assert_eq!(file.filename(), "Date Test");
assert_eq!(file.file_type(), FourCC(u32::from_be_bytes(*b"TEXT")));
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");
assert_eq!(file.created(), 1679824852);
assert_eq!(file.modified(), 1679824852);
}
}