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 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 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 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 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 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}