Skip to main content

macbinary/
lib.rs

1#![feature(seek_stream_len)]
2#![doc=include_str!("../README.md")]
3
4use std::{fmt, fs, io, path::Path};
5
6use binrw::BinReaderExt;
7use macintosh_utils::FourCC;
8
9mod reader;
10pub use reader::Reader;
11
12use crate::structs::Header;
13
14/// On-disk structures
15pub mod structs;
16
17pub type Error = binrw::Error;
18pub type Fork = macintosh_utils::Fork;
19
20#[derive(Debug, Eq, PartialEq)]
21pub enum Version {
22    None,
23    MacBinaryI,
24    MacBinaryII,
25    MacBinaryIII,
26}
27
28impl fmt::Display for Version {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        match self {
31            Version::None => write!(f, "None"),
32            Version::MacBinaryI => write!(f, "MacBinary I"),
33            Version::MacBinaryII => write!(f, "MacBinary II"),
34            Version::MacBinaryIII => write!(f, "Mac Binary III"),
35        }
36    }
37}
38
39#[derive(Debug, Copy, Clone, Default)]
40pub struct Config {
41    strat: ResourceForkDetectionStrategy,
42}
43
44/// Strategy to detect resource fork on the disk
45///
46/// Since (most) modern file systems and operating systems don't support resource forks,
47/// the resource data is often stored in a separate file.
48///
49/// There are several conventions. _SheepShaver_ for example places the resource fork in a directory called `.rsrc` next to
50/// data fork. While `resource_dasm` choses to append `.rsrc` to the filename to store the resource
51/// fork.
52///
53/// MacOS is still capable of managing proper resource forks and uses special syntax
54/// (e.g. `path/to/file.ext/..namedfork/rsrc`) to access its contents.
55#[derive(Debug, Copy, Clone, Default)]
56pub enum ResourceForkDetectionStrategy {
57    #[default]
58    /// Try each of the subsequent strategies until a resource fork is found
59    All,
60    /// Don't try to find any resource forks
61    None,
62    /// Look for a file with the same name in a directory called `.rsrc`
63    HiddenDirectory,
64    /// On macos, open the actual resource fork using the `/..namedfork/rsrc` syntax
65    NamedFork,
66    /// Look for a file with the same name and an additional extension called `.rsrc`
67    Suffix,
68}
69
70/// Wrapper for transparently reading MacBinary encoded files
71#[derive(Debug)]
72pub struct MacBinary<R> {
73    inner: R,
74    _config: Config,
75    header: Option<Header>,
76}
77
78impl<R> MacBinary<R> {
79    /// Return the initial reader
80    pub fn into_inner(self) -> R {
81        self.inner
82    }
83
84    pub fn header(&self) -> Option<&Header> {
85        self.header.as_ref()
86    }
87
88    pub fn version(&self) -> Version {
89        let Some(header) = self.header.as_ref() else {
90            return Version::None;
91        };
92
93        if header.downloader_min_version == 0x81 {
94            return Version::MacBinaryII;
95        }
96
97        if header.downloader_min_version == 0x82 {
98            return Version::MacBinaryIII;
99        }
100
101        Version::MacBinaryI
102    }
103
104    /// Original file name
105    pub fn name(&self) -> &str {
106        self.header
107            .as_ref()
108            .map(|h| h.name.as_str())
109            .unwrap_or_default()
110    }
111
112    pub fn creator_code(&self) -> FourCC {
113        self.header
114            .as_ref()
115            .map(|h| h.creator_code)
116            .unwrap_or_default()
117    }
118
119    pub fn type_code(&self) -> FourCC {
120        self.header
121            .as_ref()
122            .map(|h| h.type_code)
123            .unwrap_or_default()
124    }
125}
126
127impl<R: io::Read + io::Seek> MacBinary<R> {
128    pub fn try_new(value: R) -> Result<Self, Error> {
129        Self::try_new_with_config(value, Config::default())
130    }
131
132    pub fn try_new_with_config(mut value: R, config: Config) -> Result<Self, Error> {
133        let initial_position = value.stream_position()?;
134        Ok(match value.read_be() {
135            Ok(header) => MacBinary {
136                _config: config,
137                inner: value,
138                header: Some(header),
139            },
140            Err(_) => {
141                let _ = value.seek(std::io::SeekFrom::Start(initial_position))?;
142                MacBinary {
143                    _config: config,
144                    inner: value,
145                    header: None,
146                }
147            }
148        })
149    }
150
151    pub fn open_fork(&mut self, fork: Fork) -> Result<Reader<&mut R>, io::Error> {
152        match fork {
153            Fork::Resource => {
154                if let Some(header) = self.header.as_ref() {
155                    let len = header.resource_fork_len as u64;
156                    let position = header.resource_fork_location();
157                    Ok(Reader::try_new(&mut self.inner, position, position + len)?)
158                } else {
159                    match self._config.strat {
160                        ResourceForkDetectionStrategy::All => todo!(),
161                        ResourceForkDetectionStrategy::None => {
162                            Ok(Reader::try_new(&mut self.inner, 0, 0)?)
163                        }
164                        ResourceForkDetectionStrategy::HiddenDirectory => todo!(),
165                        ResourceForkDetectionStrategy::NamedFork => todo!(),
166                        ResourceForkDetectionStrategy::Suffix => todo!(),
167                    }
168                }
169            }
170
171            Fork::Data => {
172                if let Some(header) = self.header.as_ref() {
173                    let len = header.data_fork_len as u64;
174                    let position = header.data_fork_location();
175                    Ok(Reader::try_new(&mut self.inner, position, position + len)?)
176                } else {
177                    let len = self.inner.stream_len()?;
178                    Ok(Reader::try_new(&mut self.inner, 0, len)?)
179                }
180            }
181        }
182    }
183
184    pub fn data_fork_len(&mut self) -> Result<u64, io::Error> {
185        match self.version() {
186            Version::None => self.inner.stream_len(),
187            _ => Ok(self.header.as_ref().unwrap().data_fork_len as u64),
188        }
189    }
190
191    pub fn resource_fork_len(&mut self) -> Result<u64, io::Error> {
192        match self.version() {
193            // TODO: apply resource fork detection strategy
194            Version::None => Ok(0),
195            _ => Ok(self.header.as_ref().unwrap().resource_fork_len as u64),
196        }
197    }
198
199    pub fn data_fork(&mut self) -> Result<Reader<&mut R>, io::Error> {
200        self.open_fork(Fork::Data)
201    }
202
203    pub fn resource_fork(&mut self) -> Result<Reader<&mut R>, io::Error> {
204        self.open_fork(Fork::Resource)
205    }
206
207    pub fn into_fork(self, fork: Fork) -> Result<Reader<R>, io::Error> {
208        let Self {
209            header,
210            mut inner,
211            _config,
212        } = self;
213
214        match fork {
215            Fork::Resource => {
216                if let Some(header) = header {
217                    let len = header.resource_fork_len as u64;
218                    let position = header.resource_fork_location();
219
220                    Ok(Reader::try_new(inner, position, position + len)?)
221                } else {
222                    // TODO: respect config
223                    Ok(Reader::try_new(inner, 0, 0)?)
224                }
225            }
226            Fork::Data => {
227                if let Some(header) = header.as_ref() {
228                    let len = header.data_fork_len as u64;
229                    let position = header.data_fork_location();
230
231                    Ok(Reader::try_new(inner, position, position + len)?)
232                } else {
233                    let len = inner.stream_len()?;
234                    Ok(Reader::try_new(inner, 0, len)?)
235                }
236            }
237        }
238    }
239
240    pub fn comment(&mut self) -> Result<String, io::Error> {
241        if let Some(header) = self.header.as_ref()
242            && header.comment_len != 0
243        {
244            let position = self.inner.stream_position()?;
245            self.inner
246                .seek(io::SeekFrom::Start(header.file_comment_location()))?;
247            let mut data = vec![0u8; header.comment_len as usize];
248            self.inner.read_exact(&mut data)?;
249
250            let comment = macintosh_utils::decode_string(data);
251            self.inner.seek(io::SeekFrom::Start(position))?;
252            return Ok(comment);
253        }
254
255        // TODO: Consider looking for .finfo directory to achieve SheepShaver compatibility
256        Ok(String::new())
257    }
258
259    pub fn into_data_fork(self) -> Result<Reader<R>, io::Error> {
260        self.into_fork(Fork::Data)
261    }
262
263    pub fn into_resource_fork(self) -> Result<Reader<R>, io::Error> {
264        self.into_fork(Fork::Resource)
265    }
266}
267
268impl MacBinary<fs::File> {
269    /// Open file at path
270    pub fn open(path: impl AsRef<Path>) -> Result<Self, Error> {
271        MacBinary::try_new(fs::File::open(path)?)
272    }
273}
274
275/// Determine the MacBinary version used by the given file
276pub fn probe_file(p: impl AsRef<Path>) -> Result<Version, Error> {
277    Ok(MacBinary::open(p)?.version())
278}
279
280/// Determine the MacBinary version used by the reader
281pub fn probe(r: impl io::Read + io::Seek) -> Result<Version, Error> {
282    Ok(MacBinary::try_new(r)?.version())
283}
284
285/// Create a [`MacBinary`] by opening the given path
286pub fn open_file(p: impl AsRef<Path>) -> Result<MacBinary<fs::File>, Error> {
287    MacBinary::open(p)
288}
289
290#[cfg(test)]
291mod tests {
292    use std::{
293        fs::{File, exists},
294        io::Read,
295        path::PathBuf,
296    };
297
298    use crate::MacBinary;
299    use macintosh_utils::fourcc;
300
301    #[test]
302    fn read_macbinary_ii_header() {
303        let file = open_fixture("FRED.CPT");
304        let header = file.header().unwrap();
305        assert_eq!(header.name, "Freddie 1.0.cpt");
306        assert_eq!(header.resource_fork_len, 0);
307        assert_eq!(header.data_fork_len, 303472);
308        assert_eq!(header.magic, fourcc!("\0\0\0\0"));
309        assert_eq!(header.uploader_version, 0x81);
310        assert_eq!(header.downloader_min_version, 0x81);
311    }
312
313    #[test]
314    fn read_data_fork() {
315        let mut file = open_fixture("jpeg2gif.cpt");
316        let header = file.header().unwrap();
317        let mut buffer = vec![0u8; header.data_fork_len as usize];
318        let mut data_fork = file.data_fork().unwrap();
319        assert!(data_fork.read_exact(&mut buffer).is_ok());
320    }
321
322    fn open_fixture_raw(name: &'static str) -> File {
323        let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
324            .join("test/")
325            .join(name);
326
327        if !exists(&path).unwrap() {
328            panic!("Test fixture {name} does not exist!");
329        }
330
331        std::fs::File::open(path).unwrap()
332    }
333
334    fn open_fixture(name: &'static str) -> MacBinary<File> {
335        let file = open_fixture_raw(name);
336        MacBinary::try_new(file).unwrap()
337    }
338}