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], 0x00051607);
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> {
260 #[cfg(target_os = "macos")]
262 {
263 let rsrc_path = host_path.join("..namedfork/rsrc");
264 if let Ok(data) = std::fs::read(&rsrc_path) {
265 if !data.is_empty() {
266 let (type_code, creator_code) = read_finder_info_xattr(host_path);
267 return Some(ImportedResourceFork {
268 data,
269 data_fork: None,
270 type_code,
271 creator_code,
272 name: None,
273 finder_flags: None,
274 });
275 }
276 }
277 }
278
279 let file_name = host_path.file_name()?.to_str()?;
280 let parent = host_path.parent()?;
281
282 let ad_path = parent.join(format!("._{file_name}"));
284 if ad_path.is_file() {
285 if let Ok(ad_data) = std::fs::read(&ad_path) {
286 if let Some(parsed) = parse_appledouble(&ad_data) {
287 return Some(parsed);
288 }
289 }
290 }
291
292 if is_regular_file(host_path) {
303 if let Some(head) = read_header_128(host_path) {
304 if macbinary_header_ok(&head) {
305 if let Ok(mb_data) = std::fs::read(host_path) {
306 if let Some(parsed) = parse_macbinary(&mb_data) {
307 return Some(parsed);
308 }
309 }
310 }
311 }
312 }
313
314 if is_regular_file(host_path) {
319 if let Some(head) = read_header_128(host_path) {
320 if looks_like_binhex(&head) {
321 if let Ok(hqx) = std::fs::read(host_path) {
322 if let Some(parsed) = parse_binhex_fork(&hqx) {
323 return Some(parsed);
324 }
325 }
326 }
327 }
328 }
329
330 let rsrc_path = parent.join(format!("{file_name}.rsrc"));
332 if rsrc_path.is_file() {
333 if let Ok(data) = std::fs::read(&rsrc_path) {
334 if !data.is_empty() {
335 return Some(ImportedResourceFork {
336 data,
337 data_fork: None,
338 type_code: None,
339 creator_code: None,
340 name: None,
341 finder_flags: None,
342 });
343 }
344 }
345 }
346
347 None
348}
349
350fn looks_like_binhex(head: &[u8]) -> bool {
354 head.windows(BINHEX_PREFIX.len())
355 .any(|w| w == BINHEX_PREFIX)
356}
357
358const BINHEX_PREFIX: &[u8] = b"(This file must be converted with BinHex";
360
361pub fn parse_binhex_fork(data: &[u8]) -> Option<ImportedResourceFork> {
369 let bh = crate::binhex::parse_binhex(data).ok()?;
370 let type_code = (bh.type_code != [0; 4]).then_some(bh.type_code);
371 let creator_code = (bh.creator_code != [0; 4]).then_some(bh.creator_code);
372 if bh.resource_fork.is_empty() && type_code.is_none() && creator_code.is_none() {
373 return None;
374 }
375 Some(ImportedResourceFork {
376 data: bh.resource_fork,
377 data_fork: Some(bh.data_fork),
378 type_code,
379 creator_code,
380 name: (!bh.name.is_empty()).then_some(bh.name),
381 finder_flags: Some(bh.flags),
382 })
383}
384
385pub fn is_resource_fork_sidecar(host_path: &std::path::Path) -> bool {
388 let file_name = match host_path.file_name().and_then(|n| n.to_str()) {
389 Some(n) => n,
390 None => return false,
391 };
392 let parent = match host_path.parent() {
393 Some(p) => p,
394 None => return false,
395 };
396
397 if let Some(primary) = file_name.strip_prefix("._") {
399 if parent.join(primary).is_file() {
400 return true;
401 }
402 }
403
404 if let Some(stem) = file_name.strip_suffix(".rsrc") {
406 if !stem.is_empty() && parent.join(stem).is_file() {
407 return true;
408 }
409 }
410
411 false
412}
413
414pub fn parse_appledouble(data: &[u8]) -> Option<ImportedResourceFork> {
425 if data.len() < 26 {
426 return None;
427 }
428 if BigEndian::read_u32(&data[0..4]) != 0x00051607 {
430 return None;
431 }
432
433 let num_entries = BigEndian::read_u16(&data[24..26]) as usize;
434 let mut rsrc_data: Option<Vec<u8>> = None;
435 let mut finder_flags: Option<u16> = None;
436 let mut type_code: Option<[u8; 4]> = None;
437 let mut creator_code: Option<[u8; 4]> = None;
438
439 for i in 0..num_entries {
440 let desc_off = 26 + i * 12;
441 if desc_off + 12 > data.len() {
442 break;
443 }
444 let entry_id = BigEndian::read_u32(&data[desc_off..desc_off + 4]);
445 let offset = BigEndian::read_u32(&data[desc_off + 4..desc_off + 8]) as usize;
446 let length = BigEndian::read_u32(&data[desc_off + 8..desc_off + 12]) as usize;
447
448 if offset + length > data.len() {
449 continue;
450 }
451
452 match entry_id {
453 2 => {
454 rsrc_data = Some(data[offset..offset + length].to_vec());
456 }
457 9
458 if length >= 8 => {
460 let mut tc = [0u8; 4];
461 let mut cc = [0u8; 4];
462 tc.copy_from_slice(&data[offset..offset + 4]);
463 cc.copy_from_slice(&data[offset + 4..offset + 8]);
464 if tc != [0; 4] {
465 type_code = Some(tc);
466 }
467 if cc != [0; 4] {
468 creator_code = Some(cc);
469 }
470 if length >= 10 {
471 finder_flags =
472 Some(BigEndian::read_u16(&data[offset + 8..offset + 10]));
473 }
474 }
475 _ => {}
476 }
477 }
478
479 let rsrc = rsrc_data.unwrap_or_default();
483 if rsrc.is_empty() && type_code.is_none() && creator_code.is_none() {
484 return None;
485 }
486
487 Some(ImportedResourceFork {
488 data: rsrc,
489 data_fork: None,
490 type_code,
491 creator_code,
492 name: None,
493 finder_flags,
494 })
495}
496
497fn is_regular_file(path: &std::path::Path) -> bool {
502 std::fs::metadata(path)
503 .map(|m| m.file_type().is_file())
504 .unwrap_or(false)
505}
506
507fn read_header_128(path: &std::path::Path) -> Option<[u8; 128]> {
510 use std::io::Read as _;
511 let mut head = [0u8; 128];
512 let mut f = std::fs::File::open(path).ok()?;
513 f.read_exact(&mut head).ok()?;
514 Some(head)
515}
516
517fn macbinary_header_ok(head: &[u8; 128]) -> bool {
527 if head[0] != 0 {
528 return false;
529 }
530 let name_len = head[1] as usize;
531 if name_len == 0 || name_len > 63 {
532 return false;
533 }
534 if head[122] != 129 && head[122] != 130 {
535 return false;
536 }
537 BigEndian::read_u16(&head[124..126]) == macbinary_crc16(&head[0..124])
538}
539
540pub fn parse_macbinary(data: &[u8]) -> Option<ImportedResourceFork> {
554 if data.len() < 128 {
555 return None;
556 }
557 if data[0] != 0 {
559 return None;
560 }
561 let name_len = data[1] as usize;
563 if name_len == 0 || name_len > 63 {
564 return None;
565 }
566 let version = data[122];
568 if version != 129 && version != 130 {
569 return None;
570 }
571 let stored_crc = BigEndian::read_u16(&data[124..126]);
573 let computed_crc = macbinary_crc16(&data[0..124]);
574 if stored_crc != computed_crc {
575 return None;
576 }
577
578 let type_code: [u8; 4] = data[65..69].try_into().unwrap();
579 let creator_code: [u8; 4] = data[69..73].try_into().unwrap();
580
581 let data_len = BigEndian::read_u32(&data[83..87]) as usize;
582 let rsrc_len = BigEndian::read_u32(&data[87..91]) as usize;
583
584 let data_start = 128;
585 let data_padded = pad_to_128(data_len);
586 let rsrc_start = data_start + data_padded;
587
588 if rsrc_start + rsrc_len > data.len() || data_start + data_len > data.len() {
591 return None;
592 }
593
594 Some(ImportedResourceFork {
595 data: data[rsrc_start..rsrc_start + rsrc_len].to_vec(),
596 data_fork: Some(data[data_start..data_start + data_len].to_vec()),
597 type_code: if type_code != [0; 4] {
598 Some(type_code)
599 } else {
600 None
601 },
602 creator_code: if creator_code != [0; 4] {
603 Some(creator_code)
604 } else {
605 None
606 },
607 name: {
608 let n = crate::hfs::decode_mac_filename(&data[2..2 + name_len]);
609 (!n.is_empty()).then_some(n)
610 },
611 finder_flags: Some(((data[73] as u16) << 8) | data[101] as u16),
613 })
614}
615
616#[cfg(target_os = "macos")]
620fn read_finder_info_xattr(path: &std::path::Path) -> (Option<[u8; 4]>, Option<[u8; 4]>) {
621 use std::ffi::CString;
622 use std::os::unix::ffi::OsStrExt;
623
624 let c_path = match CString::new(path.as_os_str().as_bytes()) {
625 Ok(p) => p,
626 Err(_) => return (None, None),
627 };
628 let c_name = match CString::new("com.apple.FinderInfo") {
629 Ok(n) => n,
630 Err(_) => return (None, None),
631 };
632
633 let mut buf = [0u8; 32];
634 let n = unsafe {
636 libc::getxattr(
637 c_path.as_ptr(),
638 c_name.as_ptr(),
639 buf.as_mut_ptr() as *mut libc::c_void,
640 buf.len(),
641 0,
642 0,
643 )
644 };
645 if n < 8 {
646 return (None, None);
647 }
648
649 let mut tc = [0u8; 4];
650 let mut cc = [0u8; 4];
651 tc.copy_from_slice(&buf[0..4]);
652 cc.copy_from_slice(&buf[4..8]);
653
654 let tc = if tc != [0; 4] { Some(tc) } else { None };
655 let cc = if cc != [0; 4] { Some(cc) } else { None };
656 (tc, cc)
657}
658
659pub fn mac_name_bytes(name: &str) -> Vec<u8> {
662 let mut out = Vec::with_capacity(name.len());
663 for ch in name.chars() {
664 let mut one = [0u8; 4];
665 let bytes =
666 crate::hfs::utf8_to_mac_roman(ch.encode_utf8(&mut one)).unwrap_or_else(|_| vec![b'?']);
667 if out.len() + bytes.len() > 63 {
668 break;
669 }
670 out.extend_from_slice(&bytes);
671 }
672 out
673}
674
675#[cfg(test)]
676mod tests {
677 use super::*;
678
679 #[test]
683 fn container_parsers_carry_the_finder_flags() {
684 let mut mb = build_macbinary("Flag", b"APPL", b"ttxt", MacFileDates::default(), b"x", b"");
685 mb[73] = 0x01; mb[101] = 0x40; let crc = macbinary_crc16(&mb[0..124]);
688 BigEndian::write_u16(&mut mb[124..126], crc);
689 assert_eq!(parse_macbinary(&mb).unwrap().finder_flags, Some(0x0140));
690
691 let mut ad = build_appledouble(b"APPL", b"ttxt", MacFileDates::default(), b"RSRC");
692 assert_eq!(BigEndian::read_u32(&ad[26..30]), 9);
694 let off = BigEndian::read_u32(&ad[30..34]) as usize;
695 BigEndian::write_u16(&mut ad[off + 8..off + 10], 0x4000);
696 assert_eq!(parse_appledouble(&ad).unwrap().finder_flags, Some(0x4000));
697 }
698
699 #[test]
700 fn macbinary_names_are_mac_roman_and_cut_on_a_character() {
701 let mb = build_macbinary(
702 "Caf\u{e9}",
703 b"TEXT",
704 b"ttxt",
705 MacFileDates::default(),
706 b"x",
707 b"",
708 );
709 assert_eq!(mb[1], 4);
710 assert_eq!(&mb[2..6], &[b'C', b'a', b'f', 0x8E]);
711 let long = "\u{e9}".repeat(70);
712 let bytes = mac_name_bytes(&long);
713 assert_eq!(bytes.len(), 63);
714 assert!(bytes.iter().all(|&b| b == 0x8E));
715 assert_eq!(mac_name_bytes("\u{65e5}"), b"?");
716 }
717
718 #[test]
719 fn test_sanitize_filename() {
720 assert_eq!(sanitize_filename("hello:world"), "hello_world");
721 assert_eq!(sanitize_filename("file/name"), "file_name");
722 assert_eq!(sanitize_filename("normal.txt"), "normal.txt");
723 assert_eq!(sanitize_filename("a<b>c"), "a_b_c");
724 assert_eq!(sanitize_filename(".."), "_");
725 assert_eq!(sanitize_filename(""), "_");
726 }
727
728 #[test]
729 fn test_build_appledouble_header() {
730 let ad = build_appledouble(b"TEXT", b"ttxt", MacFileDates::default(), &[0xDE, 0xAD]);
731 assert_eq!(BigEndian::read_u32(&ad[0..4]), 0x00051607);
733 assert_eq!(BigEndian::read_u32(&ad[4..8]), 0x00020000);
735 assert_eq!(BigEndian::read_u16(&ad[24..26]), 2);
737 let fi_offset = BigEndian::read_u32(&ad[30..34]) as usize;
739 assert_eq!(&ad[fi_offset..fi_offset + 4], b"TEXT");
740 assert_eq!(&ad[fi_offset + 4..fi_offset + 8], b"ttxt");
741 let rsrc_offset = BigEndian::read_u32(&ad[42..46]) as usize;
743 let rsrc_len = BigEndian::read_u32(&ad[46..50]) as usize;
744 assert_eq!(rsrc_len, 2);
745 assert_eq!(&ad[rsrc_offset..rsrc_offset + 2], &[0xDE, 0xAD]);
746 }
747
748 #[test]
749 fn test_build_macbinary() {
750 let mb = build_macbinary(
751 "test.txt",
752 b"TEXT",
753 b"ttxt",
754 MacFileDates::default(),
755 b"hello",
756 &[1, 2, 3],
757 );
758 assert_eq!(mb[1], 8);
760 assert_eq!(&mb[2..10], b"test.txt");
761 assert_eq!(&mb[65..69], b"TEXT");
763 assert_eq!(&mb[69..73], b"ttxt");
764 assert_eq!(BigEndian::read_u32(&mb[83..87]), 5);
766 assert_eq!(BigEndian::read_u32(&mb[87..91]), 3);
768 assert_eq!(mb[122], 130);
770 assert_eq!(&mb[128..133], b"hello");
772 assert_eq!(&mb[256..259], &[1, 2, 3]);
774 }
775
776 #[test]
777 fn test_macbinary_crc() {
778 let zeros = [0u8; 124];
780 assert_eq!(macbinary_crc16(&zeros), 0);
781 }
782
783 #[test]
784 fn test_pad_to_128() {
785 assert_eq!(pad_to_128(0), 0);
786 assert_eq!(pad_to_128(1), 128);
787 assert_eq!(pad_to_128(128), 128);
788 assert_eq!(pad_to_128(129), 256);
789 }
790
791 #[test]
792 fn test_resource_fork_mode_labels() {
793 assert_eq!(
794 ResourceForkMode::AppleDouble.label(),
795 "AppleDouble (._file)"
796 );
797 assert_eq!(ResourceForkMode::DataForkOnly.label(), "Data fork only");
798 }
799
800 #[test]
801 fn test_parse_appledouble_roundtrip() {
802 let rsrc = vec![0xCA, 0xFE, 0xBA, 0xBE];
803 let ad = build_appledouble(b"TEXT", b"ttxt", MacFileDates::default(), &rsrc);
804 let parsed = parse_appledouble(&ad).expect("should parse");
805 assert_eq!(parsed.data, rsrc);
806 assert_eq!(parsed.type_code, Some(*b"TEXT"));
807 assert_eq!(parsed.creator_code, Some(*b"ttxt"));
808 assert!(parsed.data_fork.is_none());
809 }
810
811 #[test]
812 fn test_parse_appledouble_bad_magic() {
813 assert!(parse_appledouble(&[0; 30]).is_none());
814 }
815
816 #[test]
817 fn test_parse_macbinary_roundtrip() {
818 let data_fork = b"hello world";
819 let rsrc = vec![1, 2, 3, 4, 5];
820 let mb = build_macbinary(
821 "test.txt",
822 b"TEXT",
823 b"ttxt",
824 MacFileDates::default(),
825 data_fork,
826 &rsrc,
827 );
828 let parsed = parse_macbinary(&mb).expect("should parse");
829 assert_eq!(parsed.data, rsrc);
830 assert_eq!(parsed.data_fork.as_deref(), Some(data_fork.as_slice()));
831 assert_eq!(parsed.type_code, Some(*b"TEXT"));
832 assert_eq!(parsed.creator_code, Some(*b"ttxt"));
833 }
834
835 #[test]
836 fn test_parse_macbinary_bad_header() {
837 assert!(parse_macbinary(&[0; 50]).is_none());
839 let mut bad = vec![0; 256];
841 bad[0] = 1;
842 assert!(parse_macbinary(&bad).is_none());
843 }
844
845 #[test]
846 fn test_parse_appledouble_finfo_only() {
847 let ad = build_appledouble(b"MOSS", b"sigl", MacFileDates::default(), &[]);
850 let parsed = parse_appledouble(&ad).expect("FInfo-only sidecar should parse");
851 assert!(parsed.data.is_empty());
852 assert_eq!(parsed.type_code, Some(*b"MOSS"));
853 assert_eq!(parsed.creator_code, Some(*b"sigl"));
854 }
855
856 #[test]
857 fn test_parse_appledouble_empty_finder_info_rejected() {
858 let ad = build_appledouble(&[0; 4], &[0; 4], MacFileDates::default(), &[]);
860 assert!(parse_appledouble(&ad).is_none());
861 }
862
863 #[test]
866 fn test_parse_macbinary_no_rsrc_still_unwraps() {
867 let mb = build_macbinary(
868 "test.txt",
869 b"TEXT",
870 b"ttxt",
871 MacFileDates::default(),
872 b"data",
873 &[],
874 );
875 let parsed = parse_macbinary(&mb).expect("a MacBinary with an empty resource fork");
876 assert!(parsed.data.is_empty());
877 assert_eq!(parsed.data_fork.as_deref(), Some(b"data".as_slice()));
878 assert_eq!(parsed.type_code, Some(*b"TEXT"));
879 assert_eq!(parsed.name.as_deref(), Some("test.txt"));
880 }
881
882 #[test]
883 fn test_build_macbinary_dates() {
884 let dates = MacFileDates {
885 created: 0x1234_5678,
886 modified: 0x2345_6789,
887 };
888 let mb = build_macbinary("f", b"TEXT", b"ttxt", dates, b"x", &[9]);
889 assert_eq!(BigEndian::read_u32(&mb[91..95]), 0x1234_5678);
891 assert_eq!(BigEndian::read_u32(&mb[95..99]), 0x2345_6789);
892 assert!(parse_macbinary(&mb).is_some());
894 }
895
896 #[test]
897 fn test_build_appledouble_dates() {
898 let created = (MAC_EPOCH_UNIX_OFFSET + APPLEDOUBLE_EPOCH_UNIX + 100) as u32;
900 let dates = MacFileDates {
901 created,
902 modified: 0,
903 };
904 let ad = build_appledouble(b"TEXT", b"ttxt", dates, &[0xAA]);
905 assert_eq!(BigEndian::read_u16(&ad[24..26]), 3);
907 assert_eq!(BigEndian::read_u32(&ad[38..42]), 8);
909 let dates_off = BigEndian::read_u32(&ad[42..46]) as usize;
910 assert_eq!(BigEndian::read_i32(&ad[dates_off..dates_off + 4]), 100);
911 assert_eq!(
913 BigEndian::read_i32(&ad[dates_off + 4..dates_off + 8]),
914 i32::MIN
915 );
916 let parsed = parse_appledouble(&ad).expect("parse");
918 assert_eq!(parsed.type_code, Some(*b"TEXT"));
919 assert_eq!(parsed.data, vec![0xAA]);
920 }
921
922 #[test]
923 fn test_build_appledouble_no_dates_layout_unchanged() {
924 let ad = build_appledouble(b"TEXT", b"ttxt", MacFileDates::default(), &[0xAA]);
927 assert_eq!(BigEndian::read_u16(&ad[24..26]), 2);
928 assert_eq!(BigEndian::read_u32(&ad[38..42]), 2);
930 }
931
932 #[test]
936 fn macbinary_header_gate_agrees_with_the_parser() {
937 let mb = build_macbinary(
938 "note.txt",
939 b"TEXT",
940 b"ttxt",
941 MacFileDates::default(),
942 b"data",
943 &[0xAA, 0xBB],
944 );
945 let head: [u8; 128] = mb[..128].try_into().unwrap();
946 assert!(macbinary_header_ok(&head));
947 assert!(parse_macbinary(&mb).is_some());
948
949 let mut ddr = [0u8; 128];
952 ddr[0] = b'E';
953 ddr[1] = b'R';
954 assert!(!macbinary_header_ok(&ddr));
955 assert!(parse_macbinary(&ddr).is_none());
956 }
957
958 #[test]
967 #[cfg(any(target_os = "linux", target_os = "macos"))]
968 fn non_regular_paths_are_not_read_whole() {
969 use std::ffi::CString;
970 use std::os::unix::ffi::OsStrExt;
971
972 let dir = tempfile::tempdir().unwrap();
973 let fifo = dir.path().join("pipe");
974 let c_path = CString::new(fifo.as_os_str().as_bytes()).unwrap();
975 assert_eq!(unsafe { libc::mkfifo(c_path.as_ptr(), 0o644) }, 0);
976
977 assert!(!is_regular_file(&fifo));
978 assert!(detect_resource_fork(&fifo).is_none());
979 }
980}