Skip to main content

macbinary/
lib.rs

1#![feature(seek_stream_len)]
2
3use std::{cmp::min, fmt, fs, io, path::Path};
4
5use binrw::{BinReaderExt, binread};
6use bitflags::bitflags;
7use fourcc_rs::FourCC;
8use macintosh_utils::{
9    Fork, Point,
10    chrono::{DateTime, Utc},
11    decode_string,
12};
13
14mod reader;
15pub use reader::Reader;
16
17#[derive(Debug, Eq, PartialEq)]
18pub enum Version {
19    None,
20    MacBinaryI,
21    MacBinaryII,
22    MacBinaryIII,
23}
24
25impl fmt::Display for Version {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        match self {
28            Version::None => write!(f, "None"),
29            Version::MacBinaryI => write!(f, "MacBinary I"),
30            Version::MacBinaryII => write!(f, "MacBinary II"),
31            Version::MacBinaryIII => write!(f, "Mac Binary III"),
32        }
33    }
34}
35
36#[derive(Debug, Copy, Clone, Default)]
37pub struct Config {
38    strat: ResourceForkDetectionStrategy,
39}
40
41#[derive(Debug, Copy, Clone, Default)]
42pub enum ResourceForkDetectionStrategy {
43    #[default]
44    All,
45    None,
46    HiddenDirectory,
47    NamedFork,
48    Suffix,
49}
50
51#[derive(Debug)]
52pub struct MacBinary<R> {
53    inner: R,
54    config: Config,
55    header: Option<Header>,
56}
57
58impl<R> MacBinary<R> {
59    pub fn into_inner(self) -> R {
60        self.inner
61    }
62
63    pub fn header(&self) -> Option<&Header> {
64        self.header.as_ref()
65    }
66
67    pub fn version(&self) -> Version {
68        let Some(header) = self.header.as_ref() else {
69            return Version::None;
70        };
71
72        if header.downloader_min_version == 0x81 {
73            return Version::MacBinaryII;
74        }
75
76        if header.downloader_min_version == 0x82 {
77            return Version::MacBinaryIII;
78        }
79
80        Version::MacBinaryI
81    }
82
83    pub fn creator(&self) -> FourCC {
84        self.header.as_ref().map(|h| h.creator).unwrap_or_default()
85    }
86
87    pub fn type_code(&self) -> FourCC {
88        self.header
89            .as_ref()
90            .map(|h| h.type_code)
91            .unwrap_or_default()
92    }
93}
94
95impl<R: io::Read + io::Seek> MacBinary<R> {
96    pub fn try_new(value: R) -> Result<Self, binrw::Error> {
97        Self::try_new_with_config(value, Config::default())
98    }
99
100    pub fn try_new_with_config(mut value: R, config: Config) -> Result<Self, binrw::Error> {
101        let initial_position = value.stream_position()?;
102        Ok(match value.read_be() {
103            Ok(header) => MacBinary {
104                config,
105                inner: value,
106                header: Some(header),
107            },
108            Err(_) => {
109                let _ = value.seek(std::io::SeekFrom::Start(initial_position))?;
110                MacBinary {
111                    config,
112                    inner: value,
113                    header: None,
114                }
115            }
116        })
117    }
118
119    pub fn open_fork(&mut self, fork: Fork) -> Result<Reader<&mut R>, io::Error> {
120        match fork {
121            Fork::Resource => {
122                if let Some(header) = self.header.as_ref() {
123                    let len = header.resource_fork_len as u64;
124                    let position = header.resource_fork_location();
125                    Ok(Reader::try_new(&mut self.inner, position, position + len)?)
126                } else {
127                    match self.config.strat {
128                        ResourceForkDetectionStrategy::All => todo!(),
129                        ResourceForkDetectionStrategy::None => {
130                            Ok(Reader::try_new(&mut self.inner, 0, 0)?)
131                        }
132                        ResourceForkDetectionStrategy::HiddenDirectory => todo!(),
133                        ResourceForkDetectionStrategy::NamedFork => todo!(),
134                        ResourceForkDetectionStrategy::Suffix => todo!(),
135                    }
136                }
137            }
138
139            Fork::Data => {
140                if let Some(header) = self.header.as_ref() {
141                    let len = header.data_fork_len as u64;
142                    let position = header.data_fork_location();
143                    Ok(Reader::try_new(&mut self.inner, position, position + len)?)
144                } else {
145                    let len = self.inner.stream_len()?;
146                    Ok(Reader::try_new(&mut self.inner, 0, len)?)
147                }
148            }
149        }
150    }
151
152    pub fn data_fork_len(&mut self) -> Result<u64, io::Error> {
153        match self.version() {
154            Version::None => self.inner.stream_len(),
155            _ => Ok(self.header.as_ref().unwrap().data_fork_len as u64),
156        }
157    }
158
159    pub fn resource_fork_len(&mut self) -> Result<u64, io::Error> {
160        match self.version() {
161            // TODO: apply resource fork detection strategy
162            Version::None => Ok(0),
163            _ => Ok(self.header.as_ref().unwrap().resource_fork_len as u64),
164        }
165    }
166
167    pub fn data_fork(&mut self) -> Result<Reader<&mut R>, io::Error> {
168        self.open_fork(Fork::Data)
169    }
170
171    pub fn resource_fork(&mut self) -> Result<Reader<&mut R>, io::Error> {
172        self.open_fork(Fork::Resource)
173    }
174
175    pub fn into_fork(self, fork: Fork) -> Result<Reader<R>, io::Error> {
176        let Self {
177            header,
178            mut inner,
179            config: _,
180        } = self;
181
182        match fork {
183            Fork::Resource => {
184                if let Some(header) = header {
185                    let len = header.resource_fork_len as u64;
186                    let position = header.resource_fork_location();
187
188                    Ok(Reader::try_new(inner, position, position + len)?)
189                } else {
190                    // TODO: respect config
191                    Ok(Reader::try_new(inner, 0, 0)?)
192                }
193            }
194            Fork::Data => {
195                if let Some(header) = header.as_ref() {
196                    let len = header.data_fork_len as u64;
197                    let position = header.data_fork_location();
198
199                    Ok(Reader::try_new(inner, position, position + len)?)
200                } else {
201                    let len = inner.stream_len()?;
202                    Ok(Reader::try_new(inner, 0, len)?)
203                }
204            }
205        }
206    }
207
208    pub fn comment(&mut self) -> Result<String, io::Error> {
209        if let Some(header) = self.header.as_ref()
210            && header.comment_len != 0
211        {
212            let position = self.inner.stream_position()?;
213            self.inner
214                .seek(io::SeekFrom::Start(header.file_comment_location()))?;
215            let mut data = vec![0u8; header.comment_len as usize];
216            self.inner.read_exact(&mut data)?;
217
218            let comment = macintosh_utils::decode_string(data);
219            self.inner.seek(io::SeekFrom::Start(position))?;
220            return Ok(comment);
221        }
222
223        // TODO: Consider looking for .finfo directory to achieve SheepShaver compatibility
224        Ok(String::new())
225    }
226
227    pub fn into_data_fork(self) -> Result<Reader<R>, io::Error> {
228        self.into_fork(Fork::Data)
229    }
230
231    pub fn into_resource_fork(self) -> Result<Reader<R>, io::Error> {
232        self.into_fork(Fork::Resource)
233    }
234}
235
236impl MacBinary<fs::File> {
237    pub fn open(path: impl AsRef<Path>) -> Result<Self, binrw::Error> {
238        MacBinary::try_new(fs::File::open(path)?)
239    }
240}
241
242bitflags! {
243    #[derive(Debug, Clone)]
244    pub struct Flags: u8 {
245        const LOCKED = 1<<0;
246    }
247}
248
249#[binread]
250#[derive(Debug)]
251#[br(big)]
252pub struct Header {
253    pub version: u8,
254    #[br(temp,assert(name_len > 0 && name_len < 63))]
255    name_len: u8,
256    #[br(map(|r: [u8; 63]| decode_string(r[0..min(name_len as usize, 63)].to_vec())))]
257    pub name: String,
258    pub type_code: FourCC,
259    pub creator: FourCC,
260    pub finder_flags_upper: u8,
261    #[br(temp, assert(zero==0))]
262    zero: u8,
263    pub position: Point,
264    pub window_id: u16,
265    #[br(map(Flags::from_bits_retain))]
266    pub flags: Flags,
267    #[br(temp, assert(zero_again==0))]
268    zero_again: u8,
269    pub data_fork_len: u32,
270    pub resource_fork_len: u32,
271    #[br(map(macintosh_utils::date))]
272    pub created_at: DateTime<Utc>,
273    #[br(map(macintosh_utils::date))]
274    pub modified_at: DateTime<Utc>,
275
276    pub comment_len: u16,
277    pub finder_flags_lower: u8,
278    //#[br(temp, assert(magic == fourcc!("mBIN")))]
279    pub magic: FourCC,
280    pub file_name_script: u8,
281    pub extended_finder_flags: u8,
282    #[br(temp)]
283    reserved_2: [u8; 8],
284    pub unpacked_total_len: u32,
285    pub extended_header_len: u16,
286    pub uploader_version: u8,
287    pub downloader_min_version: u8,
288    /// xmodem crc 16
289    pub checksum: u16,
290
291    #[br(temp)]
292    reserved_3: u16,
293}
294
295impl Header {
296    pub const FIXED_SIZE: usize = 128;
297
298    fn extended_header_location(&self) -> u64 {
299        Header::FIXED_SIZE as u64
300    }
301
302    fn data_fork_location(&self) -> u64 {
303        self.extended_header_location() + align_128(self.extended_header_len as u64)
304    }
305
306    fn resource_fork_location(&self) -> u64 {
307        self.data_fork_location() + align_128(self.data_fork_len as u64)
308    }
309
310    fn file_comment_location(&self) -> u64 {
311        self.resource_fork_location() + align_128(self.resource_fork_len as u64)
312    }
313}
314
315fn align_128(input: u64) -> u64 {
316    if (0x80 - 1) & input != 0 {
317        (input + 0x80) & !(0x80 - 1)
318    } else {
319        input
320    }
321}
322
323#[cfg(test)]
324mod tests {
325    use std::{
326        fs::{File, exists},
327        io::Read,
328        path::PathBuf,
329    };
330
331    use crate::{MacBinary, align_128};
332    use fourcc_rs::fourcc;
333
334    #[test]
335    fn read_macbinary_ii_header() {
336        let file = open_fixture("FRED.CPT");
337        let header = file.header().unwrap();
338        assert_eq!(header.name, "Freddie 1.0.cpt");
339        assert_eq!(header.resource_fork_len, 0);
340        assert_eq!(header.data_fork_len, 303472);
341        assert_eq!(header.magic, fourcc!("\0\0\0\0"));
342        assert_eq!(header.uploader_version, 0x81);
343        assert_eq!(header.downloader_min_version, 0x81);
344    }
345
346    #[test]
347    fn read_data_fork() {
348        let mut file = open_fixture("jpeg2gif.cpt");
349        let header = file.header().unwrap();
350        let mut buffer = vec![0u8; header.data_fork_len as usize];
351        let mut data_fork = file.data_fork().unwrap();
352        assert!(data_fork.read_exact(&mut buffer).is_ok());
353    }
354
355    fn open_fixture_raw(name: &'static str) -> File {
356        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
357            .join("test/")
358            .join(name);
359
360        if !exists(&path).unwrap() {
361            panic!("Test fixture {name} does not exist!");
362        }
363
364        std::fs::File::open(path).unwrap()
365    }
366
367    fn open_fixture(name: &'static str) -> MacBinary<File> {
368        let file = open_fixture_raw(name);
369        MacBinary::try_new(file).unwrap()
370    }
371
372    #[test]
373    fn align_int() {
374        assert_eq!(align_128(0), 0);
375        assert_eq!(align_128(1), 128);
376        assert_eq!(align_128(127), 128);
377        assert_eq!(align_128(128), 128);
378        assert_eq!(align_128(129), 256);
379    }
380}