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
14pub 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#[derive(Debug, Copy, Clone, Default)]
56pub enum ResourceForkDetectionStrategy {
57 #[default]
58 All,
60 None,
62 HiddenDirectory,
64 NamedFork,
66 Suffix,
68}
69
70#[derive(Debug)]
72pub struct MacBinary<R> {
73 inner: R,
74 _config: Config,
75 header: Option<Header>,
76}
77
78impl<R> MacBinary<R> {
79 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 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 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 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 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 pub fn open(path: impl AsRef<Path>) -> Result<Self, Error> {
271 MacBinary::try_new(fs::File::open(path)?)
272 }
273}
274
275pub fn probe_file(p: impl AsRef<Path>) -> Result<Version, Error> {
277 Ok(MacBinary::open(p)?.version())
278}
279
280pub fn probe(r: impl io::Read + io::Seek) -> Result<Version, Error> {
282 Ok(MacBinary::try_new(r)?.version())
283}
284
285pub 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}