1use byteorder::{BigEndian, ByteOrder};
7
8#[derive(Debug, Clone, Copy, PartialEq)]
10pub enum ResourceForkMode {
11 Native,
14 AppleDouble,
16 MacBinary,
18 BinHex,
20 SeparateRsrc,
22 DataForkOnly,
24}
25
26impl ResourceForkMode {
27 pub fn label(&self) -> &'static str {
28 match self {
29 ResourceForkMode::Native => "Native resource fork",
30 ResourceForkMode::AppleDouble => "AppleDouble (._file)",
31 ResourceForkMode::MacBinary => "MacBinary III (.bin)",
32 ResourceForkMode::BinHex => "BinHex 4.0 (.hqx)",
33 ResourceForkMode::SeparateRsrc => "Separate (.rsrc)",
34 ResourceForkMode::DataForkOnly => "Data fork only",
35 }
36 }
37
38 pub const ALL: [ResourceForkMode; 6] = [
39 ResourceForkMode::Native,
40 ResourceForkMode::AppleDouble,
41 ResourceForkMode::MacBinary,
42 ResourceForkMode::BinHex,
43 ResourceForkMode::SeparateRsrc,
44 ResourceForkMode::DataForkOnly,
45 ];
46}
47
48#[derive(Debug, Clone, Copy, Default)]
54pub struct MacFileDates {
55 pub created: u32,
57 pub modified: u32,
59}
60
61const MAC_EPOCH_UNIX_OFFSET: i64 = 2_082_844_800;
63const APPLEDOUBLE_EPOCH_UNIX: i64 = 946_684_800;
65const APPLEDOUBLE_DATE_UNKNOWN: i32 = i32::MIN;
67
68fn mac1904_to_appledouble(mac_secs: u32) -> i32 {
72 if mac_secs == 0 {
73 return APPLEDOUBLE_DATE_UNKNOWN;
74 }
75 let secs_2000 = mac_secs as i64 - MAC_EPOCH_UNIX_OFFSET - APPLEDOUBLE_EPOCH_UNIX;
76 secs_2000.clamp(i32::MIN as i64 + 1, i32::MAX as i64) as i32
78}
79
80pub fn build_appledouble(
86 type_code: &[u8; 4],
87 creator_code: &[u8; 4],
88 dates: MacFileDates,
89 rsrc_data: &[u8],
90) -> Vec<u8> {
91 let has_dates = dates.created != 0 || dates.modified != 0;
96 let num_entries: u16 = if has_dates { 3 } else { 2 };
97 let header_len = 26 + num_entries as usize * 12; let finder_offset = header_len as u32;
100 let finder_len: u32 = 32;
101 let dates_offset = finder_offset + finder_len; let dates_len: u32 = 16;
103 let rsrc_offset = if has_dates {
104 dates_offset + dates_len
105 } else {
106 finder_offset + finder_len
107 };
108 let rsrc_len: u32 = rsrc_data.len() as u32;
109
110 let total = rsrc_offset as usize + rsrc_data.len();
111 let mut buf = vec![0u8; total];
112
113 BigEndian::write_u32(&mut buf[0..4], APPLEDOUBLE_MAGIC);
115 BigEndian::write_u32(&mut buf[4..8], 0x00020000);
117 BigEndian::write_u16(&mut buf[24..26], num_entries);
120
121 let mut d = 26;
123 let mut write_desc = |buf: &mut [u8], id: u32, off: u32, len: u32| {
124 BigEndian::write_u32(&mut buf[d..d + 4], id);
125 BigEndian::write_u32(&mut buf[d + 4..d + 8], off);
126 BigEndian::write_u32(&mut buf[d + 8..d + 12], len);
127 d += 12;
128 };
129 write_desc(&mut buf, 9, finder_offset, finder_len); if has_dates {
131 write_desc(&mut buf, 8, dates_offset, dates_len); }
133 write_desc(&mut buf, 2, rsrc_offset, rsrc_len); let fi_start = finder_offset as usize;
137 buf[fi_start..fi_start + 4].copy_from_slice(type_code);
138 buf[fi_start + 4..fi_start + 8].copy_from_slice(creator_code);
139 if has_dates {
145 let di = dates_offset as usize;
146 BigEndian::write_i32(&mut buf[di..di + 4], mac1904_to_appledouble(dates.created));
147 BigEndian::write_i32(
148 &mut buf[di + 4..di + 8],
149 mac1904_to_appledouble(dates.modified),
150 );
151 BigEndian::write_i32(&mut buf[di + 8..di + 12], APPLEDOUBLE_DATE_UNKNOWN);
152 BigEndian::write_i32(&mut buf[di + 12..di + 16], APPLEDOUBLE_DATE_UNKNOWN);
153 }
154
155 buf[rsrc_offset as usize..].copy_from_slice(rsrc_data);
157
158 buf
159}
160
161pub fn build_macbinary(
166 filename: &str,
167 type_code: &[u8; 4],
168 creator_code: &[u8; 4],
169 dates: MacFileDates,
170 data_fork: &[u8],
171 rsrc_data: &[u8],
172) -> Vec<u8> {
173 let data_padded = pad_to_128(data_fork.len());
174 let rsrc_padded = pad_to_128(rsrc_data.len());
175 let total = 128 + data_padded + rsrc_padded;
176 let mut buf = vec![0u8; total];
177
178 let name_bytes = mac_name_bytes(filename);
181 let name_len = name_bytes.len();
182 buf[1] = name_len as u8;
183 buf[2..2 + name_len].copy_from_slice(&name_bytes);
184
185 buf[65..69].copy_from_slice(type_code);
187 buf[69..73].copy_from_slice(creator_code);
188
189 BigEndian::write_u32(&mut buf[83..87], data_fork.len() as u32);
191 BigEndian::write_u32(&mut buf[87..91], rsrc_data.len() as u32);
193
194 BigEndian::write_u32(&mut buf[91..95], dates.created);
199 BigEndian::write_u32(&mut buf[95..99], dates.modified);
200
201 buf[122] = 130;
203 buf[123] = 129;
205
206 let crc = macbinary_crc16(&buf[0..124]);
208 BigEndian::write_u16(&mut buf[124..126], crc);
209
210 buf[128..128 + data_fork.len()].copy_from_slice(data_fork);
212 let rsrc_start = 128 + data_padded;
214 buf[rsrc_start..rsrc_start + rsrc_data.len()].copy_from_slice(rsrc_data);
215
216 buf
217}
218
219pub fn sanitize_filename(name: &str) -> String {
221 let check = disk_core::name_legalize::validate_host_name;
222 disk_core::name_legalize::legalize_name(&check, name).unwrap_or_else(|_| "_".to_string())
223}
224
225fn pad_to_128(len: usize) -> usize {
227 (len + 127) & !127
228}
229
230pub fn macbinary_crc16(data: &[u8]) -> u16 {
234 let mut crc: u16 = 0;
235 for &byte in data {
236 crc ^= (byte as u16) << 8;
237 for _ in 0..8 {
238 if crc & 0x8000 != 0 {
239 crc = (crc << 1) ^ 0x1021;
240 } else {
241 crc <<= 1;
242 }
243 }
244 }
245 crc
246}
247
248pub use disk_core::filesystem::ImportedResourceFork;
249
250pub fn detect_resource_fork(host_path: &std::path::Path) -> Option<ImportedResourceFork> {
262 #[cfg(target_os = "macos")]
264 {
265 let rsrc_path = host_path.join("..namedfork/rsrc");
266 if let Ok(data) = std::fs::read(&rsrc_path) {
267 if !data.is_empty() {
268 let (type_code, creator_code) = read_finder_info_xattr(host_path);
269 return Some(ImportedResourceFork {
270 data,
271 data_fork: None,
272 type_code,
273 creator_code,
274 name: None,
275 finder_flags: None,
276 });
277 }
278 }
279 }
280
281 let file_name = host_path.file_name()?.to_str()?;
282 let parent = host_path.parent()?;
283
284 let ad_path = parent.join(format!("._{file_name}"));
286 if ad_path.is_file() {
287 if let Ok(ad_data) = std::fs::read(&ad_path) {
288 if let Some(parsed) = parse_appledouble(&ad_data) {
289 return Some(parsed);
290 }
291 }
292 }
293
294 if is_regular_file(host_path) {
305 if let Some(head) = read_header_128(host_path) {
306 if macbinary_header_ok(&head) {
307 if let Ok(mb_data) = std::fs::read(host_path) {
308 if let Some(parsed) = parse_macbinary(&mb_data) {
309 return Some(parsed);
310 }
311 }
312 }
313 }
314 }
315
316 if is_regular_file(host_path) {
318 if let Some(magic) = read_magic(host_path) {
319 if magic == APPLESINGLE_MAGIC {
320 if let Ok(bytes) = std::fs::read(host_path) {
321 if let Some(parsed) = parse_applesingle(&bytes) {
322 return Some(parsed);
323 }
324 }
325 }
326 }
327 }
328
329 if is_regular_file(host_path) {
334 if let Some(head) = read_header_128(host_path) {
335 if looks_like_binhex(&head) {
336 if let Ok(hqx) = std::fs::read(host_path) {
337 if let Some(parsed) = parse_binhex_fork(&hqx) {
338 return Some(parsed);
339 }
340 }
341 }
342 }
343 }
344
345 let rsrc_path = parent.join(format!("{file_name}.rsrc"));
347 if rsrc_path.is_file() {
348 if let Ok(data) = std::fs::read(&rsrc_path) {
349 if !data.is_empty() {
350 return Some(ImportedResourceFork {
351 data,
352 data_fork: None,
353 type_code: None,
354 creator_code: None,
355 name: None,
356 finder_flags: None,
357 });
358 }
359 }
360 }
361
362 None
363}
364
365fn looks_like_binhex(head: &[u8]) -> bool {
369 head.windows(BINHEX_PREFIX.len())
370 .any(|w| w == BINHEX_PREFIX)
371}
372
373const BINHEX_PREFIX: &[u8] = b"(This file must be converted with BinHex";
375
376pub fn parse_binhex_fork(data: &[u8]) -> Option<ImportedResourceFork> {
384 let bh = crate::binhex::parse_binhex(data).ok()?;
385 let type_code = (bh.type_code != [0; 4]).then_some(bh.type_code);
386 let creator_code = (bh.creator_code != [0; 4]).then_some(bh.creator_code);
387 if bh.resource_fork.is_empty() && type_code.is_none() && creator_code.is_none() {
388 return None;
389 }
390 Some(ImportedResourceFork {
391 data: bh.resource_fork,
392 data_fork: Some(bh.data_fork),
393 type_code,
394 creator_code,
395 name: (!bh.name.is_empty()).then_some(bh.name),
396 finder_flags: Some(bh.flags),
397 })
398}
399
400pub fn is_resource_fork_sidecar(host_path: &std::path::Path) -> bool {
403 let file_name = match host_path.file_name().and_then(|n| n.to_str()) {
404 Some(n) => n,
405 None => return false,
406 };
407 let parent = match host_path.parent() {
408 Some(p) => p,
409 None => return false,
410 };
411
412 if let Some(primary) = file_name.strip_prefix("._") {
414 if parent.join(primary).is_file() {
415 return true;
416 }
417 }
418
419 if let Some(stem) = file_name.strip_suffix(".rsrc") {
421 if !stem.is_empty() && parent.join(stem).is_file() {
422 return true;
423 }
424 }
425
426 false
427}
428
429const APPLEDOUBLE_MAGIC: u32 = 0x00051607;
431const APPLESINGLE_MAGIC: u32 = 0x00051600;
433
434#[derive(Default)]
436struct AppleEntries {
437 data_fork: Option<Vec<u8>>,
438 rsrc: Option<Vec<u8>>,
439 name: Option<String>,
440 type_code: Option<[u8; 4]>,
441 creator_code: Option<[u8; 4]>,
442 finder_flags: Option<u16>,
443}
444
445fn parse_apple_entries(data: &[u8], magic: u32) -> Option<AppleEntries> {
451 if data.len() < 26 || BigEndian::read_u32(&data[0..4]) != magic {
452 return None;
453 }
454 let num_entries = BigEndian::read_u16(&data[24..26]) as usize;
455 let mut out = AppleEntries::default();
456 for i in 0..num_entries {
457 let desc_off = 26 + i * 12;
458 if desc_off + 12 > data.len() {
459 break;
460 }
461 let entry_id = BigEndian::read_u32(&data[desc_off..desc_off + 4]);
462 let offset = BigEndian::read_u32(&data[desc_off + 4..desc_off + 8]) as usize;
463 let length = BigEndian::read_u32(&data[desc_off + 8..desc_off + 12]) as usize;
464 let Some(body) = offset
465 .checked_add(length)
466 .filter(|end| *end <= data.len())
467 .map(|end| &data[offset..end])
468 else {
469 continue;
470 };
471 match entry_id {
472 1 => out.data_fork = Some(body.to_vec()),
473 2 => out.rsrc = Some(body.to_vec()),
474 3 if !body.is_empty() => out.name = Some(crate::hfs::decode_mac_filename(body)),
475 9 if length >= 8 => {
476 let tc: [u8; 4] = body[0..4].try_into().ok()?;
477 let cc: [u8; 4] = body[4..8].try_into().ok()?;
478 out.type_code = (tc != [0; 4]).then_some(tc);
479 out.creator_code = (cc != [0; 4]).then_some(cc);
480 if length >= 10 {
481 out.finder_flags = Some(BigEndian::read_u16(&body[8..10]));
482 }
483 }
484 _ => {}
485 }
486 }
487 Some(out)
488}
489
490pub fn parse_appledouble(data: &[u8]) -> Option<ImportedResourceFork> {
495 let e = parse_apple_entries(data, APPLEDOUBLE_MAGIC)?;
496 let rsrc = e.rsrc.unwrap_or_default();
497 if rsrc.is_empty() && e.type_code.is_none() && e.creator_code.is_none() {
498 return None;
499 }
500 Some(ImportedResourceFork {
501 data: rsrc,
502 data_fork: None,
503 type_code: e.type_code,
504 creator_code: e.creator_code,
505 name: None,
506 finder_flags: e.finder_flags,
507 })
508}
509
510pub fn parse_applesingle(data: &[u8]) -> Option<ImportedResourceFork> {
515 let e = parse_apple_entries(data, APPLESINGLE_MAGIC)?;
516 if e.data_fork.is_none() && e.rsrc.is_none() && e.type_code.is_none() {
517 return None;
518 }
519 Some(ImportedResourceFork {
520 data: e.rsrc.unwrap_or_default(),
521 data_fork: Some(e.data_fork.unwrap_or_default()),
522 type_code: e.type_code,
523 creator_code: e.creator_code,
524 name: e.name,
525 finder_flags: e.finder_flags,
526 })
527}
528
529fn is_regular_file(path: &std::path::Path) -> bool {
534 std::fs::metadata(path)
535 .map(|m| m.file_type().is_file())
536 .unwrap_or(false)
537}
538
539fn read_header_128(path: &std::path::Path) -> Option<[u8; 128]> {
542 use std::io::Read as _;
543 let mut head = [0u8; 128];
544 let mut f = std::fs::File::open(path).ok()?;
545 f.read_exact(&mut head).ok()?;
546 Some(head)
547}
548
549fn read_magic(path: &std::path::Path) -> Option<u32> {
551 use std::io::Read as _;
552 let mut head = [0u8; 4];
553 std::fs::File::open(path).ok()?.read_exact(&mut head).ok()?;
554 Some(u32::from_be_bytes(head))
555}
556
557fn macbinary_header_ok(head: &[u8; 128]) -> bool {
567 if head[0] != 0 {
568 return false;
569 }
570 let name_len = head[1] as usize;
571 if name_len == 0 || name_len > 63 {
572 return false;
573 }
574 if head[122] != 129 && head[122] != 130 {
575 return false;
576 }
577 BigEndian::read_u16(&head[124..126]) == macbinary_crc16(&head[0..124])
578}
579
580pub fn parse_macbinary(data: &[u8]) -> Option<ImportedResourceFork> {
594 if data.len() < 128 {
595 return None;
596 }
597 if data[0] != 0 {
599 return None;
600 }
601 let name_len = data[1] as usize;
603 if name_len == 0 || name_len > 63 {
604 return None;
605 }
606 let version = data[122];
608 if version != 129 && version != 130 {
609 return None;
610 }
611 let stored_crc = BigEndian::read_u16(&data[124..126]);
613 let computed_crc = macbinary_crc16(&data[0..124]);
614 if stored_crc != computed_crc {
615 return None;
616 }
617
618 let type_code: [u8; 4] = data[65..69].try_into().unwrap();
619 let creator_code: [u8; 4] = data[69..73].try_into().unwrap();
620
621 let data_len = BigEndian::read_u32(&data[83..87]) as usize;
622 let rsrc_len = BigEndian::read_u32(&data[87..91]) as usize;
623
624 let data_start = 128;
625 let data_padded = pad_to_128(data_len);
626 let rsrc_start = data_start + data_padded;
627
628 if rsrc_start + rsrc_len > data.len() || data_start + data_len > data.len() {
631 return None;
632 }
633
634 Some(ImportedResourceFork {
635 data: data[rsrc_start..rsrc_start + rsrc_len].to_vec(),
636 data_fork: Some(data[data_start..data_start + data_len].to_vec()),
637 type_code: if type_code != [0; 4] {
638 Some(type_code)
639 } else {
640 None
641 },
642 creator_code: if creator_code != [0; 4] {
643 Some(creator_code)
644 } else {
645 None
646 },
647 name: {
648 let n = crate::hfs::decode_mac_filename(&data[2..2 + name_len]);
649 (!n.is_empty()).then_some(n)
650 },
651 finder_flags: Some(((data[73] as u16) << 8) | data[101] as u16),
653 })
654}
655
656#[cfg(target_os = "macos")]
660fn read_finder_info_xattr(path: &std::path::Path) -> (Option<[u8; 4]>, Option<[u8; 4]>) {
661 use std::ffi::CString;
662 use std::os::unix::ffi::OsStrExt;
663
664 let c_path = match CString::new(path.as_os_str().as_bytes()) {
665 Ok(p) => p,
666 Err(_) => return (None, None),
667 };
668 let c_name = match CString::new("com.apple.FinderInfo") {
669 Ok(n) => n,
670 Err(_) => return (None, None),
671 };
672
673 let mut buf = [0u8; 32];
674 let n = unsafe {
676 libc::getxattr(
677 c_path.as_ptr(),
678 c_name.as_ptr(),
679 buf.as_mut_ptr() as *mut libc::c_void,
680 buf.len(),
681 0,
682 0,
683 )
684 };
685 if n < 8 {
686 return (None, None);
687 }
688
689 let mut tc = [0u8; 4];
690 let mut cc = [0u8; 4];
691 tc.copy_from_slice(&buf[0..4]);
692 cc.copy_from_slice(&buf[4..8]);
693
694 let tc = if tc != [0; 4] { Some(tc) } else { None };
695 let cc = if cc != [0; 4] { Some(cc) } else { None };
696 (tc, cc)
697}
698
699pub fn mac_name_bytes(name: &str) -> Vec<u8> {
702 let mut out = Vec::with_capacity(name.len());
703 for ch in name.chars() {
704 let mut one = [0u8; 4];
705 let bytes =
706 crate::hfs::utf8_to_mac_roman(ch.encode_utf8(&mut one)).unwrap_or_else(|_| vec![b'?']);
707 if out.len() + bytes.len() > 63 {
708 break;
709 }
710 out.extend_from_slice(&bytes);
711 }
712 out
713}
714
715#[cfg(test)]
716mod tests {
717 use super::*;
718
719 #[test]
723 fn container_parsers_carry_the_finder_flags() {
724 let mut mb = build_macbinary("Flag", b"APPL", b"ttxt", MacFileDates::default(), b"x", b"");
725 mb[73] = 0x01; mb[101] = 0x40; let crc = macbinary_crc16(&mb[0..124]);
728 BigEndian::write_u16(&mut mb[124..126], crc);
729 assert_eq!(parse_macbinary(&mb).unwrap().finder_flags, Some(0x0140));
730
731 let mut ad = build_appledouble(b"APPL", b"ttxt", MacFileDates::default(), b"RSRC");
732 assert_eq!(BigEndian::read_u32(&ad[26..30]), 9);
734 let off = BigEndian::read_u32(&ad[30..34]) as usize;
735 BigEndian::write_u16(&mut ad[off + 8..off + 10], 0x4000);
736 assert_eq!(parse_appledouble(&ad).unwrap().finder_flags, Some(0x4000));
737 }
738
739 #[test]
740 fn macbinary_names_are_mac_roman_and_cut_on_a_character() {
741 let mb = build_macbinary(
742 "Caf\u{e9}",
743 b"TEXT",
744 b"ttxt",
745 MacFileDates::default(),
746 b"x",
747 b"",
748 );
749 assert_eq!(mb[1], 4);
750 assert_eq!(&mb[2..6], &[b'C', b'a', b'f', 0x8E]);
751 let long = "\u{e9}".repeat(70);
752 let bytes = mac_name_bytes(&long);
753 assert_eq!(bytes.len(), 63);
754 assert!(bytes.iter().all(|&b| b == 0x8E));
755 assert_eq!(mac_name_bytes("\u{65e5}"), b"?");
756 }
757
758 #[test]
759 fn test_sanitize_filename() {
760 assert_eq!(sanitize_filename("hello:world"), "hello_world");
761 assert_eq!(sanitize_filename("file/name"), "file_name");
762 assert_eq!(sanitize_filename("normal.txt"), "normal.txt");
763 assert_eq!(sanitize_filename("a<b>c"), "a_b_c");
764 assert_eq!(sanitize_filename(".."), "_");
765 assert_eq!(sanitize_filename(""), "_");
766 }
767
768 #[test]
769 fn test_build_appledouble_header() {
770 let ad = build_appledouble(b"TEXT", b"ttxt", MacFileDates::default(), &[0xDE, 0xAD]);
771 assert_eq!(BigEndian::read_u32(&ad[0..4]), 0x00051607);
773 assert_eq!(BigEndian::read_u32(&ad[4..8]), 0x00020000);
775 assert_eq!(BigEndian::read_u16(&ad[24..26]), 2);
777 let fi_offset = BigEndian::read_u32(&ad[30..34]) as usize;
779 assert_eq!(&ad[fi_offset..fi_offset + 4], b"TEXT");
780 assert_eq!(&ad[fi_offset + 4..fi_offset + 8], b"ttxt");
781 let rsrc_offset = BigEndian::read_u32(&ad[42..46]) as usize;
783 let rsrc_len = BigEndian::read_u32(&ad[46..50]) as usize;
784 assert_eq!(rsrc_len, 2);
785 assert_eq!(&ad[rsrc_offset..rsrc_offset + 2], &[0xDE, 0xAD]);
786 }
787
788 #[test]
789 fn test_build_macbinary() {
790 let mb = build_macbinary(
791 "test.txt",
792 b"TEXT",
793 b"ttxt",
794 MacFileDates::default(),
795 b"hello",
796 &[1, 2, 3],
797 );
798 assert_eq!(mb[1], 8);
800 assert_eq!(&mb[2..10], b"test.txt");
801 assert_eq!(&mb[65..69], b"TEXT");
803 assert_eq!(&mb[69..73], b"ttxt");
804 assert_eq!(BigEndian::read_u32(&mb[83..87]), 5);
806 assert_eq!(BigEndian::read_u32(&mb[87..91]), 3);
808 assert_eq!(mb[122], 130);
810 assert_eq!(&mb[128..133], b"hello");
812 assert_eq!(&mb[256..259], &[1, 2, 3]);
814 }
815
816 #[test]
817 fn test_macbinary_crc() {
818 let zeros = [0u8; 124];
820 assert_eq!(macbinary_crc16(&zeros), 0);
821 }
822
823 #[test]
824 fn test_pad_to_128() {
825 assert_eq!(pad_to_128(0), 0);
826 assert_eq!(pad_to_128(1), 128);
827 assert_eq!(pad_to_128(128), 128);
828 assert_eq!(pad_to_128(129), 256);
829 }
830
831 #[test]
832 fn test_resource_fork_mode_labels() {
833 assert_eq!(
834 ResourceForkMode::AppleDouble.label(),
835 "AppleDouble (._file)"
836 );
837 assert_eq!(ResourceForkMode::DataForkOnly.label(), "Data fork only");
838 }
839
840 #[test]
841 fn test_parse_appledouble_roundtrip() {
842 let rsrc = vec![0xCA, 0xFE, 0xBA, 0xBE];
843 let ad = build_appledouble(b"TEXT", b"ttxt", MacFileDates::default(), &rsrc);
844 let parsed = parse_appledouble(&ad).expect("should parse");
845 assert_eq!(parsed.data, rsrc);
846 assert_eq!(parsed.type_code, Some(*b"TEXT"));
847 assert_eq!(parsed.creator_code, Some(*b"ttxt"));
848 assert!(parsed.data_fork.is_none());
849 }
850
851 #[test]
852 fn test_parse_appledouble_bad_magic() {
853 assert!(parse_appledouble(&[0; 30]).is_none());
854 }
855
856 #[test]
857 fn test_parse_macbinary_roundtrip() {
858 let data_fork = b"hello world";
859 let rsrc = vec![1, 2, 3, 4, 5];
860 let mb = build_macbinary(
861 "test.txt",
862 b"TEXT",
863 b"ttxt",
864 MacFileDates::default(),
865 data_fork,
866 &rsrc,
867 );
868 let parsed = parse_macbinary(&mb).expect("should parse");
869 assert_eq!(parsed.data, rsrc);
870 assert_eq!(parsed.data_fork.as_deref(), Some(data_fork.as_slice()));
871 assert_eq!(parsed.type_code, Some(*b"TEXT"));
872 assert_eq!(parsed.creator_code, Some(*b"ttxt"));
873 }
874
875 #[test]
876 fn test_parse_macbinary_bad_header() {
877 assert!(parse_macbinary(&[0; 50]).is_none());
879 let mut bad = vec![0; 256];
881 bad[0] = 1;
882 assert!(parse_macbinary(&bad).is_none());
883 }
884
885 #[test]
886 fn test_parse_appledouble_finfo_only() {
887 let ad = build_appledouble(b"MOSS", b"sigl", MacFileDates::default(), &[]);
890 let parsed = parse_appledouble(&ad).expect("FInfo-only sidecar should parse");
891 assert!(parsed.data.is_empty());
892 assert_eq!(parsed.type_code, Some(*b"MOSS"));
893 assert_eq!(parsed.creator_code, Some(*b"sigl"));
894 }
895
896 #[test]
897 fn test_parse_appledouble_empty_finder_info_rejected() {
898 let ad = build_appledouble(&[0; 4], &[0; 4], MacFileDates::default(), &[]);
900 assert!(parse_appledouble(&ad).is_none());
901 }
902
903 #[test]
906 fn test_parse_macbinary_no_rsrc_still_unwraps() {
907 let mb = build_macbinary(
908 "test.txt",
909 b"TEXT",
910 b"ttxt",
911 MacFileDates::default(),
912 b"data",
913 &[],
914 );
915 let parsed = parse_macbinary(&mb).expect("a MacBinary with an empty resource fork");
916 assert!(parsed.data.is_empty());
917 assert_eq!(parsed.data_fork.as_deref(), Some(b"data".as_slice()));
918 assert_eq!(parsed.type_code, Some(*b"TEXT"));
919 assert_eq!(parsed.name.as_deref(), Some("test.txt"));
920 }
921
922 #[test]
923 fn test_build_macbinary_dates() {
924 let dates = MacFileDates {
925 created: 0x1234_5678,
926 modified: 0x2345_6789,
927 };
928 let mb = build_macbinary("f", b"TEXT", b"ttxt", dates, b"x", &[9]);
929 assert_eq!(BigEndian::read_u32(&mb[91..95]), 0x1234_5678);
931 assert_eq!(BigEndian::read_u32(&mb[95..99]), 0x2345_6789);
932 assert!(parse_macbinary(&mb).is_some());
934 }
935
936 #[test]
937 fn test_build_appledouble_dates() {
938 let created = (MAC_EPOCH_UNIX_OFFSET + APPLEDOUBLE_EPOCH_UNIX + 100) as u32;
940 let dates = MacFileDates {
941 created,
942 modified: 0,
943 };
944 let ad = build_appledouble(b"TEXT", b"ttxt", dates, &[0xAA]);
945 assert_eq!(BigEndian::read_u16(&ad[24..26]), 3);
947 assert_eq!(BigEndian::read_u32(&ad[38..42]), 8);
949 let dates_off = BigEndian::read_u32(&ad[42..46]) as usize;
950 assert_eq!(BigEndian::read_i32(&ad[dates_off..dates_off + 4]), 100);
951 assert_eq!(
953 BigEndian::read_i32(&ad[dates_off + 4..dates_off + 8]),
954 i32::MIN
955 );
956 let parsed = parse_appledouble(&ad).expect("parse");
958 assert_eq!(parsed.type_code, Some(*b"TEXT"));
959 assert_eq!(parsed.data, vec![0xAA]);
960 }
961
962 #[test]
963 fn test_build_appledouble_no_dates_layout_unchanged() {
964 let ad = build_appledouble(b"TEXT", b"ttxt", MacFileDates::default(), &[0xAA]);
967 assert_eq!(BigEndian::read_u16(&ad[24..26]), 2);
968 assert_eq!(BigEndian::read_u32(&ad[38..42]), 2);
970 }
971
972 #[test]
976 fn macbinary_header_gate_agrees_with_the_parser() {
977 let mb = build_macbinary(
978 "note.txt",
979 b"TEXT",
980 b"ttxt",
981 MacFileDates::default(),
982 b"data",
983 &[0xAA, 0xBB],
984 );
985 let head: [u8; 128] = mb[..128].try_into().unwrap();
986 assert!(macbinary_header_ok(&head));
987 assert!(parse_macbinary(&mb).is_some());
988
989 let mut ddr = [0u8; 128];
992 ddr[0] = b'E';
993 ddr[1] = b'R';
994 assert!(!macbinary_header_ok(&ddr));
995 assert!(parse_macbinary(&ddr).is_none());
996 }
997
998 #[test]
1007 #[cfg(any(target_os = "linux", target_os = "macos"))]
1008 fn non_regular_paths_are_not_read_whole() {
1009 use std::ffi::CString;
1010 use std::os::unix::ffi::OsStrExt;
1011
1012 let dir = tempfile::tempdir().unwrap();
1013 let fifo = dir.path().join("pipe");
1014 let c_path = CString::new(fifo.as_os_str().as_bytes()).unwrap();
1015 assert_eq!(unsafe { libc::mkfifo(c_path.as_ptr(), 0o644) }, 0);
1016
1017 assert!(!is_regular_file(&fifo));
1018 assert!(detect_resource_fork(&fifo).is_none());
1019 }
1020
1021 fn applesingle(data: &[u8], rsrc: &[u8], name: &[u8]) -> Vec<u8> {
1023 let mut finfo = [0u8; 16];
1024 finfo[0..4].copy_from_slice(b"TEXT");
1025 finfo[4..8].copy_from_slice(b"ttxt");
1026 finfo[8..10].copy_from_slice(&0x0100u16.to_be_bytes());
1027 let entries: [(u32, &[u8]); 4] = [(1, data), (2, rsrc), (3, name), (9, &finfo)];
1028 let mut out = Vec::new();
1029 out.extend_from_slice(&APPLESINGLE_MAGIC.to_be_bytes());
1030 out.extend_from_slice(&0x0002_0000u32.to_be_bytes());
1031 out.extend_from_slice(&[0u8; 16]);
1032 out.extend_from_slice(&(entries.len() as u16).to_be_bytes());
1033 let mut offset = 26 + 12 * entries.len();
1034 for (id, body) in entries {
1035 out.extend_from_slice(&id.to_be_bytes());
1036 out.extend_from_slice(&(offset as u32).to_be_bytes());
1037 out.extend_from_slice(&(body.len() as u32).to_be_bytes());
1038 offset += body.len();
1039 }
1040 for (_, body) in entries {
1041 out.extend_from_slice(body);
1042 }
1043 out
1044 }
1045
1046 #[test]
1047 fn applesingle_unwraps_to_both_forks_and_its_mac_name() {
1048 let bytes = applesingle(b"hello", b"RSRC", b"Read Me");
1049 let f = parse_applesingle(&bytes).expect("parses");
1050 assert_eq!(f.data_fork.as_deref(), Some(&b"hello"[..]));
1051 assert_eq!(f.data, b"RSRC");
1052 assert_eq!(f.name.as_deref(), Some("Read Me"));
1053 assert_eq!(
1054 (f.type_code, f.creator_code),
1055 (Some(*b"TEXT"), Some(*b"ttxt"))
1056 );
1057 assert_eq!(f.finder_flags, Some(0x0100));
1058 assert!(parse_appledouble(&bytes).is_none());
1060 let ad = build_appledouble(b"TEXT", b"ttxt", MacFileDates::default(), b"R");
1061 assert!(parse_applesingle(&ad).is_none());
1062 }
1063
1064 #[test]
1065 fn a_host_applesingle_file_is_detected_as_its_own_wrapper() {
1066 let dir = tempfile::tempdir().unwrap();
1067 let path = dir.path().join("readme.as");
1068 std::fs::write(&path, applesingle(b"hello", b"", b"Read Me")).unwrap();
1069 let f = detect_resource_fork(&path).expect("detected");
1070 assert_eq!(f.data_fork.as_deref(), Some(&b"hello"[..]));
1071 assert_eq!(f.name.as_deref(), Some("Read Me"));
1072 assert!(f.data.is_empty());
1073 }
1074}