#![feature(seek_stream_len)]
use std::{cmp::min, fmt, fs, io, path::Path};
use binrw::{BinReaderExt, binread};
use bitflags::bitflags;
use fourcc_rs::FourCC;
use macintosh_utils::{
Fork, Point,
chrono::{DateTime, Utc},
decode_string,
};
mod reader;
pub use reader::Reader;
#[derive(Debug, Eq, PartialEq)]
pub enum Version {
None,
MacBinaryI,
MacBinaryII,
MacBinaryIII,
}
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Version::None => write!(f, "None"),
Version::MacBinaryI => write!(f, "MacBinary I"),
Version::MacBinaryII => write!(f, "MacBinary II"),
Version::MacBinaryIII => write!(f, "Mac Binary III"),
}
}
}
#[derive(Debug, Copy, Clone, Default)]
pub struct Config {
strat: ResourceForkDetectionStrategy,
}
#[derive(Debug, Copy, Clone, Default)]
pub enum ResourceForkDetectionStrategy {
#[default]
All,
None,
HiddenDirectory,
NamedFork,
Suffix,
}
#[derive(Debug)]
pub struct MacBinary<R> {
inner: R,
config: Config,
header: Option<Header>,
}
impl<R> MacBinary<R> {
pub fn into_inner(self) -> R {
self.inner
}
pub fn header(&self) -> Option<&Header> {
self.header.as_ref()
}
pub fn version(&self) -> Version {
let Some(header) = self.header.as_ref() else {
return Version::None;
};
if header.downloader_min_version == 0x81 {
return Version::MacBinaryII;
}
if header.downloader_min_version == 0x82 {
return Version::MacBinaryIII;
}
Version::MacBinaryI
}
pub fn creator(&self) -> FourCC {
self.header.as_ref().map(|h| h.creator).unwrap_or_default()
}
pub fn type_code(&self) -> FourCC {
self.header
.as_ref()
.map(|h| h.type_code)
.unwrap_or_default()
}
}
impl<R: io::Read + io::Seek> MacBinary<R> {
pub fn try_new(value: R) -> Result<Self, binrw::Error> {
Self::try_new_with_config(value, Config::default())
}
pub fn try_new_with_config(mut value: R, config: Config) -> Result<Self, binrw::Error> {
let initial_position = value.stream_position()?;
Ok(match value.read_be() {
Ok(header) => MacBinary {
config,
inner: value,
header: Some(header),
},
Err(_) => {
let _ = value.seek(std::io::SeekFrom::Start(initial_position))?;
MacBinary {
config,
inner: value,
header: None,
}
}
})
}
pub fn open_fork(&mut self, fork: Fork) -> Result<Reader<&mut R>, io::Error> {
match fork {
Fork::Resource => {
if let Some(header) = self.header.as_ref() {
let len = header.resource_fork_len as u64;
let position = header.resource_fork_location();
Ok(Reader::try_new(&mut self.inner, position, position + len)?)
} else {
match self.config.strat {
ResourceForkDetectionStrategy::All => todo!(),
ResourceForkDetectionStrategy::None => {
Ok(Reader::try_new(&mut self.inner, 0, 0)?)
}
ResourceForkDetectionStrategy::HiddenDirectory => todo!(),
ResourceForkDetectionStrategy::NamedFork => todo!(),
ResourceForkDetectionStrategy::Suffix => todo!(),
}
}
}
Fork::Data => {
if let Some(header) = self.header.as_ref() {
let len = header.data_fork_len as u64;
let position = header.data_fork_location();
Ok(Reader::try_new(&mut self.inner, position, position + len)?)
} else {
let len = self.inner.stream_len()?;
Ok(Reader::try_new(&mut self.inner, 0, len)?)
}
}
}
}
pub fn data_fork_len(&mut self) -> Result<u64, io::Error> {
match self.version() {
Version::None => self.inner.stream_len(),
_ => Ok(self.header.as_ref().unwrap().data_fork_len as u64),
}
}
pub fn resource_fork_len(&mut self) -> Result<u64, io::Error> {
match self.version() {
Version::None => Ok(0),
_ => Ok(self.header.as_ref().unwrap().resource_fork_len as u64),
}
}
pub fn data_fork(&mut self) -> Result<Reader<&mut R>, io::Error> {
self.open_fork(Fork::Data)
}
pub fn resource_fork(&mut self) -> Result<Reader<&mut R>, io::Error> {
self.open_fork(Fork::Resource)
}
pub fn into_fork(self, fork: Fork) -> Result<Reader<R>, io::Error> {
let Self {
header,
mut inner,
config: _,
} = self;
match fork {
Fork::Resource => {
if let Some(header) = header {
let len = header.resource_fork_len as u64;
let position = header.resource_fork_location();
Ok(Reader::try_new(inner, position, position + len)?)
} else {
Ok(Reader::try_new(inner, 0, 0)?)
}
}
Fork::Data => {
if let Some(header) = header.as_ref() {
let len = header.data_fork_len as u64;
let position = header.data_fork_location();
Ok(Reader::try_new(inner, position, position + len)?)
} else {
let len = inner.stream_len()?;
Ok(Reader::try_new(inner, 0, len)?)
}
}
}
}
pub fn comment(&mut self) -> Result<String, io::Error> {
if let Some(header) = self.header.as_ref()
&& header.comment_len != 0
{
let position = self.inner.stream_position()?;
self.inner
.seek(io::SeekFrom::Start(header.file_comment_location()))?;
let mut data = vec![0u8; header.comment_len as usize];
self.inner.read_exact(&mut data)?;
let comment = macintosh_utils::decode_string(data);
self.inner.seek(io::SeekFrom::Start(position))?;
return Ok(comment);
}
Ok(String::new())
}
pub fn into_data_fork(self) -> Result<Reader<R>, io::Error> {
self.into_fork(Fork::Data)
}
pub fn into_resource_fork(self) -> Result<Reader<R>, io::Error> {
self.into_fork(Fork::Resource)
}
}
impl MacBinary<fs::File> {
pub fn open(path: impl AsRef<Path>) -> Result<Self, binrw::Error> {
MacBinary::try_new(fs::File::open(path)?)
}
}
bitflags! {
#[derive(Debug, Clone)]
pub struct Flags: u8 {
const LOCKED = 1<<0;
}
}
#[binread]
#[derive(Debug)]
#[br(big)]
pub struct Header {
pub version: u8,
#[br(temp,assert(name_len > 0 && name_len < 63))]
name_len: u8,
#[br(map(|r: [u8; 63]| decode_string(r[0..min(name_len as usize, 63)].to_vec())))]
pub name: String,
pub type_code: FourCC,
pub creator: FourCC,
pub finder_flags_upper: u8,
#[br(temp, assert(zero==0))]
zero: u8,
pub position: Point,
pub window_id: u16,
#[br(map(Flags::from_bits_retain))]
pub flags: Flags,
#[br(temp, assert(zero_again==0))]
zero_again: u8,
pub data_fork_len: u32,
pub resource_fork_len: u32,
#[br(map(macintosh_utils::date))]
pub created_at: DateTime<Utc>,
#[br(map(macintosh_utils::date))]
pub modified_at: DateTime<Utc>,
pub comment_len: u16,
pub finder_flags_lower: u8,
pub magic: FourCC,
pub file_name_script: u8,
pub extended_finder_flags: u8,
#[br(temp)]
reserved_2: [u8; 8],
pub unpacked_total_len: u32,
pub extended_header_len: u16,
pub uploader_version: u8,
pub downloader_min_version: u8,
pub checksum: u16,
#[br(temp)]
reserved_3: u16,
}
impl Header {
pub const FIXED_SIZE: usize = 128;
fn extended_header_location(&self) -> u64 {
Header::FIXED_SIZE as u64
}
fn data_fork_location(&self) -> u64 {
self.extended_header_location() + align_128(self.extended_header_len as u64)
}
fn resource_fork_location(&self) -> u64 {
self.data_fork_location() + align_128(self.data_fork_len as u64)
}
fn file_comment_location(&self) -> u64 {
self.resource_fork_location() + align_128(self.resource_fork_len as u64)
}
}
fn align_128(input: u64) -> u64 {
if (0x80 - 1) & input != 0 {
(input + 0x80) & !(0x80 - 1)
} else {
input
}
}
#[cfg(test)]
mod tests {
use std::{
fs::{File, exists},
io::Read,
path::PathBuf,
};
use crate::{MacBinary, align_128};
use fourcc_rs::fourcc;
#[test]
fn read_macbinary_ii_header() {
let file = open_fixture("FRED.CPT");
let header = file.header().unwrap();
assert_eq!(header.name, "Freddie 1.0.cpt");
assert_eq!(header.resource_fork_len, 0);
assert_eq!(header.data_fork_len, 303472);
assert_eq!(header.magic, fourcc!("\0\0\0\0"));
assert_eq!(header.uploader_version, 0x81);
assert_eq!(header.downloader_min_version, 0x81);
}
#[test]
fn read_data_fork() {
let mut file = open_fixture("jpeg2gif.cpt");
let header = file.header().unwrap();
let mut buffer = vec![0u8; header.data_fork_len as usize];
let mut data_fork = file.data_fork().unwrap();
assert!(data_fork.read_exact(&mut buffer).is_ok());
}
fn open_fixture_raw(name: &'static str) -> File {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("test/")
.join(name);
if !exists(&path).unwrap() {
panic!("Test fixture {name} does not exist!");
}
std::fs::File::open(path).unwrap()
}
fn open_fixture(name: &'static str) -> MacBinary<File> {
let file = open_fixture_raw(name);
MacBinary::try_new(file).unwrap()
}
#[test]
fn align_int() {
assert_eq!(align_128(0), 0);
assert_eq!(align_128(1), 128);
assert_eq!(align_128(127), 128);
assert_eq!(align_128(128), 128);
assert_eq!(align_128(129), 256);
}
}