#[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<String>,
pub creator_code: Option<String>,
pub symlink_target: Option<String>,
}
#[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,
symlink_target: None,
}
}
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],
) -> Self {
Self {
name,
path,
entry_type: EntryType::File,
size,
location,
children: None,
resource_fork_size: Some(resource_fork_size),
type_code: Some(format_mac_code(type_code)),
creator_code: Some(format_mac_code(creator_code)),
symlink_target: 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,
symlink_target: 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,
symlink_target: None,
}
}
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());
}
#[test]
fn new_hfs_file_populates_metadata() {
let e = FileEntry::new_hfs_file(
"note".into(),
"/note".into(),
100,
77,
50,
*b"TEXT",
*b"ttxt",
);
assert_eq!(e.size, 100);
assert_eq!(e.resource_fork_size, Some(50));
assert_eq!(e.type_code.as_deref(), Some("TEXT"));
assert_eq!(e.creator_code.as_deref(), Some("ttxt"));
assert_eq!(e.total_size(), 150);
}
#[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");
}
}