1use std::io::Read;
36
37use anyhow::{bail, Context, Result};
38
39use super::stuffit::{crc16_arc, ForkCodec, ForkInfo, StuffItArchive, StuffItEntry};
40
41const RESOURCE_MARK: &str = "XtraStuf.mac";
44
45const EB_ID_MAC3: u16 = 0x334D;
47const EB_ID_JLEE: u16 = 0x07C8;
49const EB_M3_FL_UNCMPR: u16 = 0x04;
51const MAC3_FIXED_LEN: usize = 14;
53
54fn le16(b: &[u8], off: usize) -> Option<u16> {
57 b.get(off..off + 2)
58 .map(|s| u16::from_le_bytes([s[0], s[1]]))
59}
60
61fn le32(b: &[u8], off: usize) -> Option<u32> {
62 b.get(off..off + 4)
63 .map(|s| u32::from_le_bytes([s[0], s[1], s[2], s[3]]))
64}
65
66struct CentralEntry {
68 name: String,
69 method: u16,
70 comp_size: u32,
71 uncomp_size: u32,
72 local_offset: u32,
73 extra: Vec<u8>,
75 is_dir: bool,
76}
77
78fn find_eocd(bytes: &[u8]) -> Option<(usize, usize)> {
80 const EOCD_SIG: &[u8; 4] = b"PK\x05\x06";
81 if bytes.len() < 22 {
82 return None;
83 }
84 let scan_start = bytes.len().saturating_sub(22 + 0xFFFF);
86 let mut i = bytes.len() - 22;
87 loop {
88 if &bytes[i..i + 4] == EOCD_SIG {
89 let count = le16(bytes, i + 10)? as usize;
90 let cd_offset = le32(bytes, i + 16)? as usize;
91 if cd_offset <= bytes.len() {
92 return Some((cd_offset, count));
93 }
94 }
95 if i == scan_start {
96 return None;
97 }
98 i -= 1;
99 }
100}
101
102fn read_central_directory(bytes: &[u8]) -> Result<Vec<CentralEntry>> {
105 let (cd_offset, count) = find_eocd(bytes).context("not a ZIP archive (no EOCD record)")?;
106 const CEN_SIG: &[u8; 4] = b"PK\x01\x02";
107 let mut entries = Vec::with_capacity(count.min(4096));
108 let mut p = cd_offset;
109 while p + 46 <= bytes.len() && &bytes[p..p + 4] == CEN_SIG {
110 let method = le16(bytes, p + 10).context("central header truncated")?;
111 let comp_size = le32(bytes, p + 20).context("central header truncated")?;
112 let uncomp_size = le32(bytes, p + 24).context("central header truncated")?;
113 let name_len = le16(bytes, p + 28).context("central header truncated")? as usize;
114 let extra_len = le16(bytes, p + 30).context("central header truncated")? as usize;
115 let comment_len = le16(bytes, p + 32).context("central header truncated")? as usize;
116 let local_offset = le32(bytes, p + 42).context("central header truncated")?;
117 let name_start = p + 46;
118 let extra_start = name_start + name_len;
119 let comment_start = extra_start + extra_len;
120 let next = comment_start + comment_len;
121 if next > bytes.len() {
122 bail!("central directory entry extends past end of file");
123 }
124 let name_raw = &bytes[name_start..extra_start];
125 let name = decode_zip_name(name_raw);
126 let extra = bytes[extra_start..comment_start].to_vec();
127 let is_dir = name.ends_with('/');
128 entries.push(CentralEntry {
129 name,
130 method,
131 comp_size,
132 uncomp_size,
133 local_offset,
134 extra,
135 is_dir,
136 });
137 p = next;
138 }
139 Ok(entries)
140}
141
142fn decode_zip_name(raw: &[u8]) -> String {
146 match std::str::from_utf8(raw) {
147 Ok(s) => s.to_string(),
148 Err(_) => raw.iter().map(|&b| b as char).collect(),
149 }
150}
151
152struct Mac3 {
154 info_flags: u16,
155 type_code: [u8; 4],
156 creator_code: [u8; 4],
157 blob: Vec<u8>,
159}
160
161fn find_mac3(extra: &[u8]) -> Option<Mac3> {
163 let mut i = 0usize;
164 while i + 4 <= extra.len() {
165 let id = u16::from_le_bytes([extra[i], extra[i + 1]]);
166 let sz = u16::from_le_bytes([extra[i + 2], extra[i + 3]]) as usize;
167 let data_start = i + 4;
168 let data_end = data_start.checked_add(sz)?;
169 if data_end > extra.len() {
170 break;
171 }
172 if id == EB_ID_MAC3 && sz >= MAC3_FIXED_LEN {
173 let data = &extra[data_start..data_end];
174 let info_flags = u16::from_le_bytes([data[4], data[5]]);
175 let mut type_code = [0u8; 4];
176 type_code.copy_from_slice(&data[6..10]);
177 let mut creator_code = [0u8; 4];
178 creator_code.copy_from_slice(&data[10..14]);
179 return Some(Mac3 {
180 info_flags,
181 type_code,
182 creator_code,
183 blob: data[MAC3_FIXED_LEN..].to_vec(),
184 });
185 }
186 i = data_end;
187 }
188 None
189}
190
191fn find_jlee(extra: &[u8]) -> Option<([u8; 4], [u8; 4], u16)> {
195 let mut i = 0usize;
196 while i + 4 <= extra.len() {
197 let id = u16::from_le_bytes([extra[i], extra[i + 1]]);
198 let sz = u16::from_le_bytes([extra[i + 2], extra[i + 3]]) as usize;
199 let data_start = i + 4;
200 let data_end = data_start.checked_add(sz)?;
201 if data_end > extra.len() {
202 break;
203 }
204 if id == EB_ID_JLEE && sz >= 14 && &extra[data_start..data_start + 4] == b"JLEE" {
205 let data = &extra[data_start..data_end];
206 let mut type_code = [0u8; 4];
207 type_code.copy_from_slice(&data[4..8]);
208 let mut creator_code = [0u8; 4];
209 creator_code.copy_from_slice(&data[8..12]);
210 let flags = u16::from_be_bytes([data[12], data[13]]);
211 return Some((type_code, creator_code, flags));
212 }
213 i = data_end;
214 }
215 None
216}
217
218fn decode_mac3_blob(blob: &[u8], info_flags: u16) -> Option<Vec<u8>> {
222 if info_flags & EB_M3_FL_UNCMPR != 0 {
223 return Some(blob.to_vec());
224 }
225 if blob.len() < 6 {
226 return None;
227 }
228 let method = u16::from_le_bytes([blob[0], blob[1]]);
229 let stream = &blob[6..]; match method {
231 0 => Some(stream.to_vec()),
232 8 => inflate(stream).ok(),
233 _ => None,
234 }
235}
236
237fn inflate(stream: &[u8]) -> Result<Vec<u8>> {
239 let mut out = Vec::new();
240 flate2::read::DeflateDecoder::new(stream)
241 .read_to_end(&mut out)
242 .context("inflating deflate stream")?;
243 Ok(out)
244}
245
246struct Mac3Meta {
248 finder_flags: u16,
249 create_date: u32,
250 mod_date: u32,
251}
252
253fn mac3_meta(blob: &[u8]) -> Mac3Meta {
256 Mac3Meta {
257 finder_flags: le16(blob, 0).unwrap_or(0),
258 create_date: le32(blob, 26).unwrap_or(0),
259 mod_date: le32(blob, 30).unwrap_or(0),
260 }
261}
262
263pub fn is_maczip(bytes: &[u8]) -> bool {
267 if !(bytes.starts_with(b"PK\x03\x04") || bytes.starts_with(b"PK\x05\x06")) {
269 return false;
270 }
271 match read_central_directory(bytes) {
272 Ok(entries) => entries
273 .iter()
274 .any(|e| find_mac3(&e.extra).is_some() || find_jlee(&e.extra).is_some()),
275 Err(_) => false,
276 }
277}
278
279struct Merged {
281 components: Vec<String>,
282 name: String,
283 is_dir: bool,
284 type_code: [u8; 4],
285 creator_code: [u8; 4],
286 finder_flags: u16,
287 create_date: u32,
288 mod_date: u32,
289 data: Option<Vec<u8>>,
290 rsrc: Option<Vec<u8>>,
291 meta_from_data: bool,
294}
295
296fn read_entry_payload<'a>(bytes: &'a [u8], e: &CentralEntry) -> Result<(Vec<u8>, &'a [u8])> {
301 let lo = e.local_offset as usize;
302 if lo + 30 > bytes.len() || &bytes[lo..lo + 4] != b"PK\x03\x04" {
303 bail!("bad local header for {}", e.name);
304 }
305 let name_len = le16(bytes, lo + 26).context("local header truncated")? as usize;
306 let extra_len = le16(bytes, lo + 28).context("local header truncated")? as usize;
307 let extra_start = lo + 30 + name_len;
308 let data_start = extra_start + extra_len;
309 let data_end = data_start
310 .checked_add(e.comp_size as usize)
311 .context("fork length overflow")?;
312 if data_end > bytes.len() || extra_start > bytes.len() {
313 bail!("fork data extends past end of file for {}", e.name);
314 }
315 let local_extra = &bytes[extra_start..data_start];
316 let raw = &bytes[data_start..data_end];
317 let data = match e.method {
318 0 => raw.to_vec(),
319 8 => inflate(raw).with_context(|| format!("inflating {}", e.name))?,
320 m => bail!("unsupported ZIP compression method {m} for {}", e.name),
321 };
322 if data.len() as u32 != e.uncomp_size {
323 bail!(
324 "size mismatch for {} ({} != {})",
325 e.name,
326 data.len(),
327 e.uncomp_size
328 );
329 }
330 Ok((data, local_extra))
331}
332
333pub fn parse(bytes: &[u8]) -> Result<(Vec<u8>, StuffItArchive)> {
338 let entries = read_central_directory(bytes)?;
339
340 let mut merged: Vec<Merged> = Vec::new();
341 let mut index: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
342
343 for e in &entries {
344 let comps: Vec<String> = e
345 .name
346 .split('/')
347 .filter(|c| !c.is_empty())
348 .map(|c| c.to_string())
349 .collect();
350 if comps.is_empty() {
351 continue;
352 }
353 let is_rsrc = comps.iter().any(|c| c == RESOURCE_MARK);
354 let logical: Vec<String> = comps.into_iter().filter(|c| c != RESOURCE_MARK).collect();
355 if logical.is_empty() {
356 continue; }
358 let key = logical.join("/");
359
360 if e.is_dir {
361 if is_rsrc {
364 continue;
365 }
366 index.entry(key.clone()).or_insert_with(|| {
367 merged.push(Merged {
368 name: logical.last().cloned().unwrap_or_default(),
369 components: logical.clone(),
370 is_dir: true,
371 type_code: [0; 4],
372 creator_code: [0; 4],
373 finder_flags: 0,
374 create_date: 0,
375 mod_date: 0,
376 data: None,
377 rsrc: None,
378 meta_from_data: false,
379 });
380 merged.len() - 1
381 });
382 continue;
383 }
384
385 let (payload, local_extra) = match read_entry_payload(bytes, e) {
386 Ok(v) => v,
387 Err(_) => continue, };
389 let mac3 = find_mac3(local_extra);
390 let jlee = mac3.is_none().then(|| find_jlee(local_extra)).flatten();
391
392 let idx = *index.entry(key.clone()).or_insert_with(|| {
393 merged.push(Merged {
394 name: logical.last().cloned().unwrap_or_default(),
395 components: logical.clone(),
396 is_dir: false,
397 type_code: [0; 4],
398 creator_code: [0; 4],
399 finder_flags: 0,
400 create_date: 0,
401 mod_date: 0,
402 data: None,
403 rsrc: None,
404 meta_from_data: false,
405 });
406 merged.len() - 1
407 });
408 let m = &mut merged[idx];
409
410 if !m.meta_from_data {
412 if let Some(ref info) = mac3 {
413 m.type_code = info.type_code;
414 m.creator_code = info.creator_code;
415 if let Some(blob) = decode_mac3_blob(&info.blob, info.info_flags) {
416 let meta = mac3_meta(&blob);
417 m.finder_flags = meta.finder_flags;
418 m.create_date = meta.create_date;
419 m.mod_date = meta.mod_date;
420 }
421 if !is_rsrc {
422 m.meta_from_data = true;
423 }
424 } else if let Some((tc, cc, flags)) = jlee {
425 m.type_code = tc;
426 m.creator_code = cc;
427 m.finder_flags = flags;
428 if !is_rsrc {
429 m.meta_from_data = true;
430 }
431 }
432 }
433
434 if is_rsrc {
435 m.rsrc = Some(payload);
436 } else {
437 m.data = Some(payload);
438 }
439 }
440
441 let mut buf: Vec<u8> = Vec::new();
444 let mut out_entries: Vec<StuffItEntry> = Vec::with_capacity(merged.len());
445 for m in merged {
446 if m.is_dir {
447 out_entries.push(StuffItEntry {
448 path: m.components,
449 name: m.name,
450 is_dir: true,
451 type_code: [0; 4],
452 creator_code: [0; 4],
453 finder_flags: 0,
454 create_date: 0,
455 mod_date: 0,
456 data: None,
457 rsrc: None,
458 });
459 continue;
460 }
461 let data = m.data.unwrap_or_default();
462 let rsrc = m.rsrc.unwrap_or_default();
463 let data_off = buf.len() as u64;
464 buf.extend_from_slice(&data);
465 let rsrc_off = buf.len() as u64;
466 buf.extend_from_slice(&rsrc);
467 out_entries.push(StuffItEntry {
468 path: m.components,
469 name: m.name,
470 is_dir: false,
471 type_code: m.type_code,
472 creator_code: m.creator_code,
473 finder_flags: m.finder_flags,
474 create_date: m.create_date,
475 mod_date: m.mod_date,
476 data: Some(store_fork(&data, data_off, crc16_arc(&data))),
477 rsrc: (!rsrc.is_empty()).then(|| store_fork(&rsrc, rsrc_off, crc16_arc(&rsrc))),
478 });
479 }
480
481 if out_entries.iter().all(|e| e.is_dir) {
482 bail!("ZIP carries no MacZip file entries");
483 }
484
485 Ok((
486 buf,
487 StuffItArchive {
488 entries: out_entries,
489 truncated: None,
490 damage: Vec::new(),
491 },
492 ))
493}
494
495fn store_fork(data: &[u8], offset: u64, crc: u16) -> ForkInfo {
497 ForkInfo {
498 method: 0,
499 codec: ForkCodec::StuffIt,
500 encrypted: false,
501 uncompressed_len: data.len() as u32,
502 compressed_len: data.len() as u32,
503 crc,
504 crc32: 0,
505 offset,
506 }
507}
508
509#[cfg(test)]
510mod tests {
511 use super::*;
512 use crate::stuffit::decompress_fork;
513
514 struct ZipBuilder {
517 local: Vec<u8>,
518 central: Vec<u8>,
519 count: u16,
520 }
521 impl ZipBuilder {
522 fn new() -> Self {
523 Self {
524 local: Vec::new(),
525 central: Vec::new(),
526 count: 0,
527 }
528 }
529 fn add(&mut self, name: &str, data: &[u8], extra_local: &[u8], extra_central: &[u8]) {
532 let crc = crc32(data);
533 let off = self.local.len() as u32;
534 self.local.extend_from_slice(b"PK\x03\x04");
536 self.local.extend_from_slice(&[20, 0]); self.local.extend_from_slice(&[0, 0]); self.local.extend_from_slice(&[0, 0]); self.local.extend_from_slice(&[0, 0, 0, 0]); self.local.extend_from_slice(&crc.to_le_bytes());
541 self.local
542 .extend_from_slice(&(data.len() as u32).to_le_bytes()); self.local
544 .extend_from_slice(&(data.len() as u32).to_le_bytes()); self.local
546 .extend_from_slice(&(name.len() as u16).to_le_bytes());
547 self.local
548 .extend_from_slice(&(extra_local.len() as u16).to_le_bytes());
549 self.local.extend_from_slice(name.as_bytes());
550 self.local.extend_from_slice(extra_local);
551 self.local.extend_from_slice(data);
552 self.central.extend_from_slice(b"PK\x01\x02");
554 self.central.extend_from_slice(&[20, 0, 20, 0]); self.central.extend_from_slice(&[0, 0]); self.central.extend_from_slice(&[0, 0]); self.central.extend_from_slice(&[0, 0, 0, 0]); self.central.extend_from_slice(&crc.to_le_bytes());
559 self.central
560 .extend_from_slice(&(data.len() as u32).to_le_bytes());
561 self.central
562 .extend_from_slice(&(data.len() as u32).to_le_bytes());
563 self.central
564 .extend_from_slice(&(name.len() as u16).to_le_bytes());
565 self.central
566 .extend_from_slice(&(extra_central.len() as u16).to_le_bytes());
567 self.central.extend_from_slice(&[0, 0]); self.central.extend_from_slice(&[0, 0]); self.central.extend_from_slice(&[0, 0]); self.central.extend_from_slice(&[0, 0, 0, 0]); self.central.extend_from_slice(&off.to_le_bytes());
572 self.central.extend_from_slice(name.as_bytes());
573 self.central.extend_from_slice(extra_central);
574 self.count += 1;
575 }
576 fn finish(self) -> Vec<u8> {
577 let mut out = self.local;
578 let cd_off = out.len() as u32;
579 out.extend_from_slice(&self.central);
580 let cd_size = out.len() as u32 - cd_off;
581 out.extend_from_slice(b"PK\x05\x06");
582 out.extend_from_slice(&[0, 0, 0, 0]); out.extend_from_slice(&self.count.to_le_bytes());
584 out.extend_from_slice(&self.count.to_le_bytes());
585 out.extend_from_slice(&cd_size.to_le_bytes());
586 out.extend_from_slice(&cd_off.to_le_bytes());
587 out.extend_from_slice(&[0, 0]); out
589 }
590 }
591
592 fn crc32(data: &[u8]) -> u32 {
593 let mut crc = 0xFFFF_FFFFu32;
595 for &b in data {
596 crc ^= b as u32;
597 for _ in 0..8 {
598 crc = if crc & 1 != 0 {
599 (crc >> 1) ^ 0xEDB8_8320
600 } else {
601 crc >> 1
602 };
603 }
604 }
605 !crc
606 }
607
608 fn mac3_field(type_code: &[u8; 4], creator: &[u8; 4], blob: &[u8], local: bool) -> Vec<u8> {
611 let mut data = Vec::new();
612 let attrsize = if local { blob.len() as u32 } else { 0 };
613 data.extend_from_slice(&attrsize.to_le_bytes()); data.extend_from_slice(&EB_M3_FL_UNCMPR.to_le_bytes()); data.extend_from_slice(type_code);
616 data.extend_from_slice(creator);
617 if local {
618 data.extend_from_slice(blob);
619 }
620 let mut out = Vec::new();
621 out.extend_from_slice(&EB_ID_MAC3.to_le_bytes());
622 out.extend_from_slice(&(data.len() as u16).to_le_bytes());
623 out.extend_from_slice(&data);
624 out
625 }
626
627 fn mac3_blob(fd_flags: u16, create: u32, modify: u32) -> Vec<u8> {
630 let mut b = vec![0u8; 40];
631 b[0..2].copy_from_slice(&fd_flags.to_le_bytes());
632 b[26..30].copy_from_slice(&create.to_le_bytes());
633 b[30..34].copy_from_slice(&modify.to_le_bytes());
634 b
635 }
636
637 #[test]
638 fn pairs_forks_and_reads_finder_metadata() {
639 let blob = mac3_blob(0x2000, 0x1111_1111, 0x2222_2222);
640 let mut zb = ZipBuilder::new();
641 zb.add(
643 "Apps/Game",
644 b"DATA-FORK",
645 &mac3_field(b"APPL", b"Po.P", &blob, true),
646 &mac3_field(b"APPL", b"Po.P", &[], false),
647 );
648 zb.add(
650 "Apps/XtraStuf.mac/Game",
651 b"RSRC-FORK",
652 &mac3_field(b"APPL", b"Po.P", &[], true),
653 &mac3_field(b"APPL", b"Po.P", &[], false),
654 );
655 let zip = zb.finish();
656
657 assert!(is_maczip(&zip), "should detect MacZip via Mac3 field");
658 let (buf, archive) = parse(&zip).expect("parse MacZip");
659 let files: Vec<_> = archive.entries.iter().filter(|e| !e.is_dir).collect();
661 assert_eq!(files.len(), 1, "data + rsrc forks must merge to one entry");
662 let f = files[0];
663 assert_eq!(f.name, "Game");
664 assert_eq!(f.path, vec!["Apps".to_string(), "Game".to_string()]);
665 assert_eq!(&f.type_code, b"APPL");
666 assert_eq!(&f.creator_code, b"Po.P");
667 assert_eq!(f.finder_flags, 0x2000, "hasBundle from Mac3 blob");
668 assert_eq!(f.create_date, 0x1111_1111);
669 assert_eq!(f.mod_date, 0x2222_2222);
670 let data = decompress_fork(&buf, f.data.as_ref().unwrap()).unwrap();
671 assert_eq!(data, b"DATA-FORK");
672 let rsrc = decompress_fork(&buf, f.rsrc.as_ref().unwrap()).unwrap();
673 assert_eq!(rsrc, b"RSRC-FORK");
674 }
675
676 #[test]
677 fn plain_disk_image_zip_is_not_maczip() {
678 let mut zb = ZipBuilder::new();
681 zb.add("backup.img", b"\x00\x01\x02 not a mac file", &[], &[]);
682 let zip = zb.finish();
683 assert!(!is_maczip(&zip), "plain disk-image zip must not be MacZip");
684 }
685
686 #[test]
687 fn deflated_mac3_blob_round_trips() {
688 use flate2::{write::DeflateEncoder, Compression};
690 use std::io::Write;
691 let raw_blob = mac3_blob(0x0400, 7, 9); let mut enc = DeflateEncoder::new(Vec::new(), Compression::default());
693 enc.write_all(&raw_blob).unwrap();
694 let deflated = enc.finish().unwrap();
695 let mut packed = Vec::new();
696 packed.extend_from_slice(&8u16.to_le_bytes()); packed.extend_from_slice(&0u32.to_le_bytes()); packed.extend_from_slice(&deflated);
699
700 let decoded = decode_mac3_blob(&packed, 0).expect("inflate memcompress blob");
701 let meta = mac3_meta(&decoded);
702 assert_eq!(meta.finder_flags, 0x0400);
703 assert_eq!(meta.create_date, 7);
704 assert_eq!(meta.mod_date, 9);
705 }
706}