use byteorder::{BigEndian, ByteOrder};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ResourceForkMode {
Native,
AppleDouble,
MacBinary,
BinHex,
SeparateRsrc,
DataForkOnly,
}
impl ResourceForkMode {
pub fn label(&self) -> &'static str {
match self {
ResourceForkMode::Native => "Native resource fork",
ResourceForkMode::AppleDouble => "AppleDouble (._file)",
ResourceForkMode::MacBinary => "MacBinary III (.bin)",
ResourceForkMode::BinHex => "BinHex 4.0 (.hqx)",
ResourceForkMode::SeparateRsrc => "Separate (.rsrc)",
ResourceForkMode::DataForkOnly => "Data fork only",
}
}
pub const ALL: [ResourceForkMode; 6] = [
ResourceForkMode::Native,
ResourceForkMode::AppleDouble,
ResourceForkMode::MacBinary,
ResourceForkMode::BinHex,
ResourceForkMode::SeparateRsrc,
ResourceForkMode::DataForkOnly,
];
}
#[derive(Debug, Clone, Copy, Default)]
pub struct MacFileDates {
pub created: u32,
pub modified: u32,
}
const MAC_EPOCH_UNIX_OFFSET: i64 = 2_082_844_800;
const APPLEDOUBLE_EPOCH_UNIX: i64 = 946_684_800;
const APPLEDOUBLE_DATE_UNKNOWN: i32 = i32::MIN;
fn mac1904_to_appledouble(mac_secs: u32) -> i32 {
if mac_secs == 0 {
return APPLEDOUBLE_DATE_UNKNOWN;
}
let secs_2000 = mac_secs as i64 - MAC_EPOCH_UNIX_OFFSET - APPLEDOUBLE_EPOCH_UNIX;
secs_2000.clamp(i32::MIN as i64 + 1, i32::MAX as i64) as i32
}
pub fn build_appledouble(
type_code: &[u8; 4],
creator_code: &[u8; 4],
dates: MacFileDates,
rsrc_data: &[u8],
) -> Vec<u8> {
let has_dates = dates.created != 0 || dates.modified != 0;
let num_entries: u16 = if has_dates { 3 } else { 2 };
let header_len = 26 + num_entries as usize * 12;
let finder_offset = header_len as u32;
let finder_len: u32 = 32;
let dates_offset = finder_offset + finder_len; let dates_len: u32 = 16;
let rsrc_offset = if has_dates {
dates_offset + dates_len
} else {
finder_offset + finder_len
};
let rsrc_len: u32 = rsrc_data.len() as u32;
let total = rsrc_offset as usize + rsrc_data.len();
let mut buf = vec![0u8; total];
BigEndian::write_u32(&mut buf[0..4], 0x00051607);
BigEndian::write_u32(&mut buf[4..8], 0x00020000);
BigEndian::write_u16(&mut buf[24..26], num_entries);
let mut d = 26;
let mut write_desc = |buf: &mut [u8], id: u32, off: u32, len: u32| {
BigEndian::write_u32(&mut buf[d..d + 4], id);
BigEndian::write_u32(&mut buf[d + 4..d + 8], off);
BigEndian::write_u32(&mut buf[d + 8..d + 12], len);
d += 12;
};
write_desc(&mut buf, 9, finder_offset, finder_len); if has_dates {
write_desc(&mut buf, 8, dates_offset, dates_len); }
write_desc(&mut buf, 2, rsrc_offset, rsrc_len);
let fi_start = finder_offset as usize;
buf[fi_start..fi_start + 4].copy_from_slice(type_code);
buf[fi_start + 4..fi_start + 8].copy_from_slice(creator_code);
if has_dates {
let di = dates_offset as usize;
BigEndian::write_i32(&mut buf[di..di + 4], mac1904_to_appledouble(dates.created));
BigEndian::write_i32(
&mut buf[di + 4..di + 8],
mac1904_to_appledouble(dates.modified),
);
BigEndian::write_i32(&mut buf[di + 8..di + 12], APPLEDOUBLE_DATE_UNKNOWN);
BigEndian::write_i32(&mut buf[di + 12..di + 16], APPLEDOUBLE_DATE_UNKNOWN);
}
buf[rsrc_offset as usize..].copy_from_slice(rsrc_data);
buf
}
pub fn build_macbinary(
filename: &str,
type_code: &[u8; 4],
creator_code: &[u8; 4],
dates: MacFileDates,
data_fork: &[u8],
rsrc_data: &[u8],
) -> Vec<u8> {
let data_padded = pad_to_128(data_fork.len());
let rsrc_padded = pad_to_128(rsrc_data.len());
let total = 128 + data_padded + rsrc_padded;
let mut buf = vec![0u8; total];
let name_bytes = mac_name_bytes(filename);
let name_len = name_bytes.len();
buf[1] = name_len as u8;
buf[2..2 + name_len].copy_from_slice(&name_bytes);
buf[65..69].copy_from_slice(type_code);
buf[69..73].copy_from_slice(creator_code);
BigEndian::write_u32(&mut buf[83..87], data_fork.len() as u32);
BigEndian::write_u32(&mut buf[87..91], rsrc_data.len() as u32);
BigEndian::write_u32(&mut buf[91..95], dates.created);
BigEndian::write_u32(&mut buf[95..99], dates.modified);
buf[122] = 130;
buf[123] = 129;
let crc = macbinary_crc16(&buf[0..124]);
BigEndian::write_u16(&mut buf[124..126], crc);
buf[128..128 + data_fork.len()].copy_from_slice(data_fork);
let rsrc_start = 128 + data_padded;
buf[rsrc_start..rsrc_start + rsrc_data.len()].copy_from_slice(rsrc_data);
buf
}
pub fn sanitize_filename(name: &str) -> String {
let check = disk_core::name_legalize::validate_host_name;
disk_core::name_legalize::legalize_name(&check, name).unwrap_or_else(|_| "_".to_string())
}
fn pad_to_128(len: usize) -> usize {
(len + 127) & !127
}
pub fn macbinary_crc16(data: &[u8]) -> u16 {
let mut crc: u16 = 0;
for &byte in data {
crc ^= (byte as u16) << 8;
for _ in 0..8 {
if crc & 0x8000 != 0 {
crc = (crc << 1) ^ 0x1021;
} else {
crc <<= 1;
}
}
}
crc
}
pub use disk_core::filesystem::ImportedResourceFork;
pub fn detect_resource_fork(host_path: &std::path::Path) -> Option<ImportedResourceFork> {
#[cfg(target_os = "macos")]
{
let rsrc_path = host_path.join("..namedfork/rsrc");
if let Ok(data) = std::fs::read(&rsrc_path) {
if !data.is_empty() {
let (type_code, creator_code) = read_finder_info_xattr(host_path);
return Some(ImportedResourceFork {
data,
data_fork: None,
type_code,
creator_code,
name: None,
finder_flags: None,
});
}
}
}
let file_name = host_path.file_name()?.to_str()?;
let parent = host_path.parent()?;
let ad_path = parent.join(format!("._{file_name}"));
if ad_path.is_file() {
if let Ok(ad_data) = std::fs::read(&ad_path) {
if let Some(parsed) = parse_appledouble(&ad_data) {
return Some(parsed);
}
}
}
if is_regular_file(host_path) {
if let Some(head) = read_header_128(host_path) {
if macbinary_header_ok(&head) {
if let Ok(mb_data) = std::fs::read(host_path) {
if let Some(parsed) = parse_macbinary(&mb_data) {
return Some(parsed);
}
}
}
}
}
if is_regular_file(host_path) {
if let Some(head) = read_header_128(host_path) {
if looks_like_binhex(&head) {
if let Ok(hqx) = std::fs::read(host_path) {
if let Some(parsed) = parse_binhex_fork(&hqx) {
return Some(parsed);
}
}
}
}
}
let rsrc_path = parent.join(format!("{file_name}.rsrc"));
if rsrc_path.is_file() {
if let Ok(data) = std::fs::read(&rsrc_path) {
if !data.is_empty() {
return Some(ImportedResourceFork {
data,
data_fork: None,
type_code: None,
creator_code: None,
name: None,
finder_flags: None,
});
}
}
}
None
}
fn looks_like_binhex(head: &[u8]) -> bool {
head.windows(BINHEX_PREFIX.len())
.any(|w| w == BINHEX_PREFIX)
}
const BINHEX_PREFIX: &[u8] = b"(This file must be converted with BinHex";
pub fn parse_binhex_fork(data: &[u8]) -> Option<ImportedResourceFork> {
let bh = crate::binhex::parse_binhex(data).ok()?;
let type_code = (bh.type_code != [0; 4]).then_some(bh.type_code);
let creator_code = (bh.creator_code != [0; 4]).then_some(bh.creator_code);
if bh.resource_fork.is_empty() && type_code.is_none() && creator_code.is_none() {
return None;
}
Some(ImportedResourceFork {
data: bh.resource_fork,
data_fork: Some(bh.data_fork),
type_code,
creator_code,
name: (!bh.name.is_empty()).then_some(bh.name),
finder_flags: Some(bh.flags),
})
}
pub fn is_resource_fork_sidecar(host_path: &std::path::Path) -> bool {
let file_name = match host_path.file_name().and_then(|n| n.to_str()) {
Some(n) => n,
None => return false,
};
let parent = match host_path.parent() {
Some(p) => p,
None => return false,
};
if let Some(primary) = file_name.strip_prefix("._") {
if parent.join(primary).is_file() {
return true;
}
}
if let Some(stem) = file_name.strip_suffix(".rsrc") {
if !stem.is_empty() && parent.join(stem).is_file() {
return true;
}
}
false
}
pub fn parse_appledouble(data: &[u8]) -> Option<ImportedResourceFork> {
if data.len() < 26 {
return None;
}
if BigEndian::read_u32(&data[0..4]) != 0x00051607 {
return None;
}
let num_entries = BigEndian::read_u16(&data[24..26]) as usize;
let mut rsrc_data: Option<Vec<u8>> = None;
let mut finder_flags: Option<u16> = None;
let mut type_code: Option<[u8; 4]> = None;
let mut creator_code: Option<[u8; 4]> = None;
for i in 0..num_entries {
let desc_off = 26 + i * 12;
if desc_off + 12 > data.len() {
break;
}
let entry_id = BigEndian::read_u32(&data[desc_off..desc_off + 4]);
let offset = BigEndian::read_u32(&data[desc_off + 4..desc_off + 8]) as usize;
let length = BigEndian::read_u32(&data[desc_off + 8..desc_off + 12]) as usize;
if offset + length > data.len() {
continue;
}
match entry_id {
2 => {
rsrc_data = Some(data[offset..offset + length].to_vec());
}
9
if length >= 8 => {
let mut tc = [0u8; 4];
let mut cc = [0u8; 4];
tc.copy_from_slice(&data[offset..offset + 4]);
cc.copy_from_slice(&data[offset + 4..offset + 8]);
if tc != [0; 4] {
type_code = Some(tc);
}
if cc != [0; 4] {
creator_code = Some(cc);
}
if length >= 10 {
finder_flags =
Some(BigEndian::read_u16(&data[offset + 8..offset + 10]));
}
}
_ => {}
}
}
let rsrc = rsrc_data.unwrap_or_default();
if rsrc.is_empty() && type_code.is_none() && creator_code.is_none() {
return None;
}
Some(ImportedResourceFork {
data: rsrc,
data_fork: None,
type_code,
creator_code,
name: None,
finder_flags,
})
}
fn is_regular_file(path: &std::path::Path) -> bool {
std::fs::metadata(path)
.map(|m| m.file_type().is_file())
.unwrap_or(false)
}
fn read_header_128(path: &std::path::Path) -> Option<[u8; 128]> {
use std::io::Read as _;
let mut head = [0u8; 128];
let mut f = std::fs::File::open(path).ok()?;
f.read_exact(&mut head).ok()?;
Some(head)
}
fn macbinary_header_ok(head: &[u8; 128]) -> bool {
if head[0] != 0 {
return false;
}
let name_len = head[1] as usize;
if name_len == 0 || name_len > 63 {
return false;
}
if head[122] != 129 && head[122] != 130 {
return false;
}
BigEndian::read_u16(&head[124..126]) == macbinary_crc16(&head[0..124])
}
pub fn parse_macbinary(data: &[u8]) -> Option<ImportedResourceFork> {
if data.len() < 128 {
return None;
}
if data[0] != 0 {
return None;
}
let name_len = data[1] as usize;
if name_len == 0 || name_len > 63 {
return None;
}
let version = data[122];
if version != 129 && version != 130 {
return None;
}
let stored_crc = BigEndian::read_u16(&data[124..126]);
let computed_crc = macbinary_crc16(&data[0..124]);
if stored_crc != computed_crc {
return None;
}
let type_code: [u8; 4] = data[65..69].try_into().unwrap();
let creator_code: [u8; 4] = data[69..73].try_into().unwrap();
let data_len = BigEndian::read_u32(&data[83..87]) as usize;
let rsrc_len = BigEndian::read_u32(&data[87..91]) as usize;
let data_start = 128;
let data_padded = pad_to_128(data_len);
let rsrc_start = data_start + data_padded;
if rsrc_start + rsrc_len > data.len() || data_start + data_len > data.len() {
return None;
}
Some(ImportedResourceFork {
data: data[rsrc_start..rsrc_start + rsrc_len].to_vec(),
data_fork: Some(data[data_start..data_start + data_len].to_vec()),
type_code: if type_code != [0; 4] {
Some(type_code)
} else {
None
},
creator_code: if creator_code != [0; 4] {
Some(creator_code)
} else {
None
},
name: {
let n = crate::hfs::decode_mac_filename(&data[2..2 + name_len]);
(!n.is_empty()).then_some(n)
},
finder_flags: Some(((data[73] as u16) << 8) | data[101] as u16),
})
}
#[cfg(target_os = "macos")]
fn read_finder_info_xattr(path: &std::path::Path) -> (Option<[u8; 4]>, Option<[u8; 4]>) {
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
let c_path = match CString::new(path.as_os_str().as_bytes()) {
Ok(p) => p,
Err(_) => return (None, None),
};
let c_name = match CString::new("com.apple.FinderInfo") {
Ok(n) => n,
Err(_) => return (None, None),
};
let mut buf = [0u8; 32];
let n = unsafe {
libc::getxattr(
c_path.as_ptr(),
c_name.as_ptr(),
buf.as_mut_ptr() as *mut libc::c_void,
buf.len(),
0,
0,
)
};
if n < 8 {
return (None, None);
}
let mut tc = [0u8; 4];
let mut cc = [0u8; 4];
tc.copy_from_slice(&buf[0..4]);
cc.copy_from_slice(&buf[4..8]);
let tc = if tc != [0; 4] { Some(tc) } else { None };
let cc = if cc != [0; 4] { Some(cc) } else { None };
(tc, cc)
}
pub fn mac_name_bytes(name: &str) -> Vec<u8> {
let mut out = Vec::with_capacity(name.len());
for ch in name.chars() {
let mut one = [0u8; 4];
let bytes =
crate::hfs::utf8_to_mac_roman(ch.encode_utf8(&mut one)).unwrap_or_else(|_| vec![b'?']);
if out.len() + bytes.len() > 63 {
break;
}
out.extend_from_slice(&bytes);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn container_parsers_carry_the_finder_flags() {
let mut mb = build_macbinary("Flag", b"APPL", b"ttxt", MacFileDates::default(), b"x", b"");
mb[73] = 0x01; mb[101] = 0x40; let crc = macbinary_crc16(&mb[0..124]);
BigEndian::write_u16(&mut mb[124..126], crc);
assert_eq!(parse_macbinary(&mb).unwrap().finder_flags, Some(0x0140));
let mut ad = build_appledouble(b"APPL", b"ttxt", MacFileDates::default(), b"RSRC");
assert_eq!(BigEndian::read_u32(&ad[26..30]), 9);
let off = BigEndian::read_u32(&ad[30..34]) as usize;
BigEndian::write_u16(&mut ad[off + 8..off + 10], 0x4000);
assert_eq!(parse_appledouble(&ad).unwrap().finder_flags, Some(0x4000));
}
#[test]
fn macbinary_names_are_mac_roman_and_cut_on_a_character() {
let mb = build_macbinary(
"Caf\u{e9}",
b"TEXT",
b"ttxt",
MacFileDates::default(),
b"x",
b"",
);
assert_eq!(mb[1], 4);
assert_eq!(&mb[2..6], &[b'C', b'a', b'f', 0x8E]);
let long = "\u{e9}".repeat(70);
let bytes = mac_name_bytes(&long);
assert_eq!(bytes.len(), 63);
assert!(bytes.iter().all(|&b| b == 0x8E));
assert_eq!(mac_name_bytes("\u{65e5}"), b"?");
}
#[test]
fn test_sanitize_filename() {
assert_eq!(sanitize_filename("hello:world"), "hello_world");
assert_eq!(sanitize_filename("file/name"), "file_name");
assert_eq!(sanitize_filename("normal.txt"), "normal.txt");
assert_eq!(sanitize_filename("a<b>c"), "a_b_c");
assert_eq!(sanitize_filename(".."), "_");
assert_eq!(sanitize_filename(""), "_");
}
#[test]
fn test_build_appledouble_header() {
let ad = build_appledouble(b"TEXT", b"ttxt", MacFileDates::default(), &[0xDE, 0xAD]);
assert_eq!(BigEndian::read_u32(&ad[0..4]), 0x00051607);
assert_eq!(BigEndian::read_u32(&ad[4..8]), 0x00020000);
assert_eq!(BigEndian::read_u16(&ad[24..26]), 2);
let fi_offset = BigEndian::read_u32(&ad[30..34]) as usize;
assert_eq!(&ad[fi_offset..fi_offset + 4], b"TEXT");
assert_eq!(&ad[fi_offset + 4..fi_offset + 8], b"ttxt");
let rsrc_offset = BigEndian::read_u32(&ad[42..46]) as usize;
let rsrc_len = BigEndian::read_u32(&ad[46..50]) as usize;
assert_eq!(rsrc_len, 2);
assert_eq!(&ad[rsrc_offset..rsrc_offset + 2], &[0xDE, 0xAD]);
}
#[test]
fn test_build_macbinary() {
let mb = build_macbinary(
"test.txt",
b"TEXT",
b"ttxt",
MacFileDates::default(),
b"hello",
&[1, 2, 3],
);
assert_eq!(mb[1], 8);
assert_eq!(&mb[2..10], b"test.txt");
assert_eq!(&mb[65..69], b"TEXT");
assert_eq!(&mb[69..73], b"ttxt");
assert_eq!(BigEndian::read_u32(&mb[83..87]), 5);
assert_eq!(BigEndian::read_u32(&mb[87..91]), 3);
assert_eq!(mb[122], 130);
assert_eq!(&mb[128..133], b"hello");
assert_eq!(&mb[256..259], &[1, 2, 3]);
}
#[test]
fn test_macbinary_crc() {
let zeros = [0u8; 124];
assert_eq!(macbinary_crc16(&zeros), 0);
}
#[test]
fn test_pad_to_128() {
assert_eq!(pad_to_128(0), 0);
assert_eq!(pad_to_128(1), 128);
assert_eq!(pad_to_128(128), 128);
assert_eq!(pad_to_128(129), 256);
}
#[test]
fn test_resource_fork_mode_labels() {
assert_eq!(
ResourceForkMode::AppleDouble.label(),
"AppleDouble (._file)"
);
assert_eq!(ResourceForkMode::DataForkOnly.label(), "Data fork only");
}
#[test]
fn test_parse_appledouble_roundtrip() {
let rsrc = vec![0xCA, 0xFE, 0xBA, 0xBE];
let ad = build_appledouble(b"TEXT", b"ttxt", MacFileDates::default(), &rsrc);
let parsed = parse_appledouble(&ad).expect("should parse");
assert_eq!(parsed.data, rsrc);
assert_eq!(parsed.type_code, Some(*b"TEXT"));
assert_eq!(parsed.creator_code, Some(*b"ttxt"));
assert!(parsed.data_fork.is_none());
}
#[test]
fn test_parse_appledouble_bad_magic() {
assert!(parse_appledouble(&[0; 30]).is_none());
}
#[test]
fn test_parse_macbinary_roundtrip() {
let data_fork = b"hello world";
let rsrc = vec![1, 2, 3, 4, 5];
let mb = build_macbinary(
"test.txt",
b"TEXT",
b"ttxt",
MacFileDates::default(),
data_fork,
&rsrc,
);
let parsed = parse_macbinary(&mb).expect("should parse");
assert_eq!(parsed.data, rsrc);
assert_eq!(parsed.data_fork.as_deref(), Some(data_fork.as_slice()));
assert_eq!(parsed.type_code, Some(*b"TEXT"));
assert_eq!(parsed.creator_code, Some(*b"ttxt"));
}
#[test]
fn test_parse_macbinary_bad_header() {
assert!(parse_macbinary(&[0; 50]).is_none());
let mut bad = vec![0; 256];
bad[0] = 1;
assert!(parse_macbinary(&bad).is_none());
}
#[test]
fn test_parse_appledouble_finfo_only() {
let ad = build_appledouble(b"MOSS", b"sigl", MacFileDates::default(), &[]);
let parsed = parse_appledouble(&ad).expect("FInfo-only sidecar should parse");
assert!(parsed.data.is_empty());
assert_eq!(parsed.type_code, Some(*b"MOSS"));
assert_eq!(parsed.creator_code, Some(*b"sigl"));
}
#[test]
fn test_parse_appledouble_empty_finder_info_rejected() {
let ad = build_appledouble(&[0; 4], &[0; 4], MacFileDates::default(), &[]);
assert!(parse_appledouble(&ad).is_none());
}
#[test]
fn test_parse_macbinary_no_rsrc_still_unwraps() {
let mb = build_macbinary(
"test.txt",
b"TEXT",
b"ttxt",
MacFileDates::default(),
b"data",
&[],
);
let parsed = parse_macbinary(&mb).expect("a MacBinary with an empty resource fork");
assert!(parsed.data.is_empty());
assert_eq!(parsed.data_fork.as_deref(), Some(b"data".as_slice()));
assert_eq!(parsed.type_code, Some(*b"TEXT"));
assert_eq!(parsed.name.as_deref(), Some("test.txt"));
}
#[test]
fn test_build_macbinary_dates() {
let dates = MacFileDates {
created: 0x1234_5678,
modified: 0x2345_6789,
};
let mb = build_macbinary("f", b"TEXT", b"ttxt", dates, b"x", &[9]);
assert_eq!(BigEndian::read_u32(&mb[91..95]), 0x1234_5678);
assert_eq!(BigEndian::read_u32(&mb[95..99]), 0x2345_6789);
assert!(parse_macbinary(&mb).is_some());
}
#[test]
fn test_build_appledouble_dates() {
let created = (MAC_EPOCH_UNIX_OFFSET + APPLEDOUBLE_EPOCH_UNIX + 100) as u32;
let dates = MacFileDates {
created,
modified: 0,
};
let ad = build_appledouble(b"TEXT", b"ttxt", dates, &[0xAA]);
assert_eq!(BigEndian::read_u16(&ad[24..26]), 3);
assert_eq!(BigEndian::read_u32(&ad[38..42]), 8);
let dates_off = BigEndian::read_u32(&ad[42..46]) as usize;
assert_eq!(BigEndian::read_i32(&ad[dates_off..dates_off + 4]), 100);
assert_eq!(
BigEndian::read_i32(&ad[dates_off + 4..dates_off + 8]),
i32::MIN
);
let parsed = parse_appledouble(&ad).expect("parse");
assert_eq!(parsed.type_code, Some(*b"TEXT"));
assert_eq!(parsed.data, vec![0xAA]);
}
#[test]
fn test_build_appledouble_no_dates_layout_unchanged() {
let ad = build_appledouble(b"TEXT", b"ttxt", MacFileDates::default(), &[0xAA]);
assert_eq!(BigEndian::read_u16(&ad[24..26]), 2);
assert_eq!(BigEndian::read_u32(&ad[38..42]), 2);
}
#[test]
fn macbinary_header_gate_agrees_with_the_parser() {
let mb = build_macbinary(
"note.txt",
b"TEXT",
b"ttxt",
MacFileDates::default(),
b"data",
&[0xAA, 0xBB],
);
let head: [u8; 128] = mb[..128].try_into().unwrap();
assert!(macbinary_header_ok(&head));
assert!(parse_macbinary(&mb).is_some());
let mut ddr = [0u8; 128];
ddr[0] = b'E';
ddr[1] = b'R';
assert!(!macbinary_header_ok(&ddr));
assert!(parse_macbinary(&ddr).is_none());
}
#[test]
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn non_regular_paths_are_not_read_whole() {
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
let dir = tempfile::tempdir().unwrap();
let fifo = dir.path().join("pipe");
let c_path = CString::new(fifo.as_os_str().as_bytes()).unwrap();
assert_eq!(unsafe { libc::mkfifo(c_path.as_ptr(), 0o644) }, 0);
assert!(!is_regular_file(&fifo));
assert!(detect_resource_fork(&fifo).is_none());
}
}