use crate::iso9660::Iso9660DateTime;
pub const MAC_EPOCH_UNIX_OFFSET: i64 = 2_082_844_800;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FileTimestamps {
Hfs {
created: u32,
modified: u32,
backup: u32,
},
HfsPlus {
created: u32,
content_modified: u32,
attribute_modified: u32,
accessed: u32,
backup: u32,
},
Iso9660 {
recorded: Iso9660DateTime,
created: Option<Iso9660DateTime>,
modified: Option<Iso9660DateTime>,
accessed: Option<Iso9660DateTime>,
},
Unix { atime: i64, mtime: i64, ctime: i64 },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PosixMetadata {
pub mode: u32,
pub uid: u32,
pub gid: u32,
}
impl PosixMetadata {
pub fn permission_bits(&self) -> u32 {
self.mode & 0o7777
}
pub fn is_symlink(&self) -> bool {
self.mode & 0o170000 == 0o120000
}
}
#[derive(Debug, Clone)]
pub struct FileEntry {
pub name: String,
pub path: String,
pub entry_type: EntryType,
pub size: u64,
pub location: u64,
pub children: Option<Vec<FileEntry>>,
pub resource_fork_size: Option<u64>,
pub type_code: Option<[u8; 4]>,
pub creator_code: Option<[u8; 4]>,
pub finder_flags: Option<u16>,
pub symlink_target: Option<String>,
pub timestamps: Option<FileTimestamps>,
pub posix: Option<PosixMetadata>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EntryType {
File,
Directory,
}
pub fn format_mac_code(code: [u8; 4]) -> String {
if code.iter().all(|&b| (0x20..=0x7E).contains(&b)) {
String::from_utf8(code.to_vec()).unwrap_or_default()
} else {
format!(
"0x{:02X}{:02X}{:02X}{:02X}",
code[0], code[1], code[2], code[3]
)
}
}
impl FileEntry {
pub fn new_file(name: String, path: String, size: u64, location: u64) -> Self {
Self {
name,
path,
entry_type: EntryType::File,
size,
location,
children: None,
resource_fork_size: None,
type_code: None,
creator_code: None,
finder_flags: None,
symlink_target: None,
timestamps: None,
posix: None,
}
}
#[allow(clippy::too_many_arguments)] pub fn new_hfs_file(
name: String,
path: String,
size: u64,
location: u64,
resource_fork_size: u64,
type_code: [u8; 4],
creator_code: [u8; 4],
finder_flags: u16,
) -> Self {
Self {
name,
path,
entry_type: EntryType::File,
size,
location,
children: None,
resource_fork_size: Some(resource_fork_size),
type_code: Some(type_code),
creator_code: Some(creator_code),
finder_flags: Some(finder_flags),
symlink_target: None,
timestamps: None,
posix: None,
}
}
pub fn new_directory(name: String, path: String, location: u64) -> Self {
Self {
name,
path,
entry_type: EntryType::Directory,
size: 0,
location,
children: None,
resource_fork_size: None,
type_code: None,
creator_code: None,
finder_flags: None,
symlink_target: None,
timestamps: None,
posix: None,
}
}
pub fn root(location: u64) -> Self {
Self {
name: String::new(),
path: "/".to_string(),
entry_type: EntryType::Directory,
size: 0,
location,
children: None,
resource_fork_size: None,
type_code: None,
creator_code: None,
finder_flags: None,
symlink_target: None,
timestamps: None,
posix: None,
}
}
pub fn type_code_string(&self) -> Option<String> {
self.type_code.map(format_mac_code)
}
pub fn creator_code_string(&self) -> Option<String> {
self.creator_code.map(format_mac_code)
}
pub fn is_directory(&self) -> bool {
self.entry_type == EntryType::Directory
}
pub fn is_file(&self) -> bool {
self.entry_type == EntryType::File
}
pub fn total_size(&self) -> u64 {
self.size + self.resource_fork_size.unwrap_or(0)
}
pub fn size_string(&self) -> String {
if self.is_directory() {
return String::new();
}
match self.size {
s if s < 1_024 => format!("{} B", s),
s if s < 1_024 * 1_024 => format!("{:.1} KB", s as f64 / 1_024.0),
s if s < 1_024 * 1_024 * 1_024 => format!("{:.1} MB", s as f64 / (1_024.0 * 1_024.0)),
s => format!("{:.2} GB", s as f64 / (1_024.0 * 1_024.0 * 1_024.0)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_file_fields() {
let e = FileEntry::new_file("readme.txt".into(), "/readme.txt".into(), 1234, 42);
assert_eq!(e.name, "readme.txt");
assert_eq!(e.path, "/readme.txt");
assert_eq!(e.size, 1234);
assert_eq!(e.location, 42);
assert!(e.is_file());
assert!(!e.is_directory());
assert!(e.children.is_none());
}
#[test]
fn new_directory_fields() {
let e = FileEntry::new_directory("System".into(), "/System".into(), 17);
assert_eq!(e.name, "System");
assert_eq!(e.size, 0);
assert!(e.is_directory());
assert!(!e.is_file());
}
#[test]
fn root_entry() {
let e = FileEntry::root(16);
assert_eq!(e.path, "/");
assert!(e.name.is_empty());
assert!(e.is_directory());
assert_eq!(e.location, 16);
}
#[test]
fn size_string_bytes() {
let e = FileEntry::new_file("f".into(), "/f".into(), 512, 0);
assert_eq!(e.size_string(), "512 B");
}
#[test]
fn size_string_kb() {
let e = FileEntry::new_file("f".into(), "/f".into(), 2048, 0);
assert_eq!(e.size_string(), "2.0 KB");
}
#[test]
fn size_string_mb() {
let e = FileEntry::new_file("f".into(), "/f".into(), 1_572_864, 0);
assert_eq!(e.size_string(), "1.5 MB");
}
#[test]
fn size_string_gb() {
let e = FileEntry::new_file("f".into(), "/f".into(), 2_147_483_648, 0);
assert_eq!(e.size_string(), "2.00 GB");
}
#[test]
fn size_string_empty_for_directory() {
let e = FileEntry::new_directory("dir".into(), "/dir".into(), 0);
assert_eq!(e.size_string(), "");
}
#[test]
fn new_file_has_no_hfs_metadata() {
let e = FileEntry::new_file("a".into(), "/a".into(), 1, 1);
assert!(e.resource_fork_size.is_none());
assert!(e.type_code.is_none());
assert!(e.creator_code.is_none());
assert!(e.finder_flags.is_none());
assert!(e.type_code_string().is_none());
}
#[test]
fn new_hfs_file_populates_metadata() {
let e = FileEntry::new_hfs_file(
"note".into(),
"/note".into(),
100,
77,
50,
*b"TEXT",
*b"ttxt",
0x4000, );
assert_eq!(e.size, 100);
assert_eq!(e.resource_fork_size, Some(50));
assert_eq!(e.type_code, Some(*b"TEXT"));
assert_eq!(e.creator_code, Some(*b"ttxt"));
assert_eq!(e.finder_flags, Some(0x4000));
assert_eq!(e.type_code_string().as_deref(), Some("TEXT"));
assert_eq!(e.creator_code_string().as_deref(), Some("ttxt"));
assert_eq!(e.total_size(), 150);
}
#[test]
fn high_bit_type_code_round_trips_raw() {
let e = FileEntry::new_hfs_file(
"x".into(),
"/x".into(),
1,
1,
0,
*b"APPL",
[0x50, 0x6F, 0xC4, 0x50],
0,
);
assert_eq!(e.creator_code, Some([0x50, 0x6F, 0xC4, 0x50]));
assert_eq!(e.creator_code_string().as_deref(), Some("0x506FC450"));
}
#[test]
fn total_size_without_resource_fork() {
let e = FileEntry::new_file("f".into(), "/f".into(), 42, 1);
assert_eq!(e.total_size(), 42);
}
#[test]
fn format_mac_code_printable_ascii() {
assert_eq!(format_mac_code(*b"TEXT"), "TEXT");
assert_eq!(format_mac_code(*b"ttxt"), "ttxt");
}
#[test]
fn format_mac_code_non_printable_falls_back_to_hex() {
assert_eq!(format_mac_code([0, 0, 0, 0]), "0x00000000");
assert_eq!(format_mac_code([0xDE, 0xAD, 0xBE, 0xEF]), "0xDEADBEEF");
}
}