#[derive(Debug, Clone)]
pub struct FileEntry {
pub name: String,
pub path: String,
pub entry_type: EntryType,
pub size: u64,
pub location: u64,
pub modified: Option<String>,
pub modified_unix: 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 special_type: Option<String>,
pub mode: Option<u32>,
pub uid: Option<u32>,
pub gid: Option<u32>,
pub resource_fork_size: Option<u64>,
pub aux_type: Option<u16>,
pub prodos_file_type: Option<u8>,
pub link_target_cnid: Option<u64>,
pub amiga_protection: Option<u32>,
pub amiga_comment: Option<String>,
pub amiga_date: Option<(i32, i32, i32)>,
pub dos_attributes: Option<u16>,
pub mac_dates: Option<(u32, u32, u32)>,
pub owner_label: Option<String>,
pub native_attrs: Option<String>,
pub qnx2_attrs: Option<Qnx2Attrs>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Qnx2Attrs {
pub attr: u8,
pub group_perms: u8,
pub other_perms: u8,
pub member: u8,
pub group: u8,
pub date: [u16; 2],
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum EntryType {
File,
Directory,
Symlink,
Special,
}
impl FileEntry {
pub fn root() -> Self {
Self {
name: "/".into(),
path: "/".into(),
entry_type: EntryType::Directory,
size: 0,
location: 0,
modified: None,
modified_unix: None,
type_code: None,
creator_code: None,
finder_flags: None,
prodos_file_type: None,
symlink_target: None,
special_type: None,
mode: None,
uid: None,
gid: None,
resource_fork_size: None,
aux_type: None,
link_target_cnid: None,
amiga_protection: None,
amiga_comment: None,
amiga_date: None,
dos_attributes: None,
mac_dates: None,
owner_label: None,
native_attrs: None,
qnx2_attrs: None,
}
}
pub fn new_directory(name: String, path: String, location: u64) -> Self {
Self {
name,
path,
entry_type: EntryType::Directory,
size: 0,
location,
modified: None,
modified_unix: None,
type_code: None,
creator_code: None,
finder_flags: None,
prodos_file_type: None,
symlink_target: None,
special_type: None,
mode: None,
uid: None,
gid: None,
resource_fork_size: None,
aux_type: None,
link_target_cnid: None,
amiga_protection: None,
amiga_comment: None,
amiga_date: None,
dos_attributes: None,
mac_dates: None,
owner_label: None,
native_attrs: None,
qnx2_attrs: None,
}
}
pub fn new_file(name: String, path: String, size: u64, location: u64) -> Self {
Self {
name,
path,
entry_type: EntryType::File,
size,
location,
modified: None,
modified_unix: None,
type_code: None,
creator_code: None,
finder_flags: None,
prodos_file_type: None,
symlink_target: None,
special_type: None,
mode: None,
uid: None,
gid: None,
resource_fork_size: None,
aux_type: None,
link_target_cnid: None,
amiga_protection: None,
amiga_comment: None,
amiga_date: None,
dos_attributes: None,
mac_dates: None,
owner_label: None,
native_attrs: None,
qnx2_attrs: None,
}
}
pub fn new_symlink(
name: String,
path: String,
size: u64,
location: u64,
target: String,
) -> Self {
Self {
name,
path,
entry_type: EntryType::Symlink,
size,
location,
modified: None,
modified_unix: None,
type_code: None,
creator_code: None,
finder_flags: None,
prodos_file_type: None,
symlink_target: Some(target),
special_type: None,
mode: None,
uid: None,
gid: None,
resource_fork_size: None,
aux_type: None,
link_target_cnid: None,
amiga_protection: None,
amiga_comment: None,
amiga_date: None,
dos_attributes: None,
mac_dates: None,
owner_label: None,
native_attrs: None,
qnx2_attrs: None,
}
}
pub fn new_special(
name: String,
path: String,
location: u64,
special_type_str: String,
) -> Self {
Self {
name,
path,
entry_type: EntryType::Special,
size: 0,
location,
modified: None,
modified_unix: None,
type_code: None,
creator_code: None,
finder_flags: None,
prodos_file_type: None,
symlink_target: None,
special_type: Some(special_type_str),
mode: None,
uid: None,
gid: None,
resource_fork_size: None,
aux_type: None,
link_target_cnid: None,
amiga_protection: None,
amiga_comment: None,
amiga_date: None,
dos_attributes: None,
mac_dates: None,
owner_label: None,
native_attrs: None,
qnx2_attrs: None,
}
}
pub fn archive_name(&self) -> Option<&str> {
let trimmed = self.name.trim_matches('/');
if trimmed.is_empty() || trimmed == "." || trimmed == ".." {
None
} else {
Some(trimmed)
}
}
pub fn is_directory(&self) -> bool {
self.entry_type == EntryType::Directory
}
pub fn is_file(&self) -> bool {
self.entry_type == EntryType::File
}
pub fn is_symlink(&self) -> bool {
self.entry_type == EntryType::Symlink
}
pub fn is_special(&self) -> bool {
self.entry_type == EntryType::Special
}
pub fn mode_string(&self) -> Option<String> {
self.mode.map(crate::display::unix_mode_string)
}
pub fn size_string(&self) -> String {
if self.is_directory() || self.is_special() {
return String::new();
}
crate::display::format_size(self.size)
}
pub fn creator_code_display(&self) -> Option<String> {
self.creator_code
.as_ref()
.map(crate::display::decode_ostype)
}
}
pub fn sort_for_display(entries: &mut [FileEntry]) {
entries.sort_by_cached_key(|e| (!e.is_directory(), e.name.to_lowercase()));
}
fn parent_dir(path: &str) -> &str {
match path.trim_end_matches('/').rsplit_once('/') {
Some((dir, _)) => dir,
None => "",
}
}
pub fn common_parent(entries: &[FileEntry]) -> String {
let mut it = entries.iter();
let Some(first) = it.next() else {
return String::new();
};
let mut base: Vec<&str> = parent_dir(&first.path)
.split('/')
.filter(|c| !c.is_empty())
.collect();
for e in it {
let these: Vec<&str> = parent_dir(&e.path)
.split('/')
.filter(|c| !c.is_empty())
.collect();
let keep = base
.iter()
.zip(these.iter())
.take_while(|(a, b)| a == b)
.count();
base.truncate(keep);
if base.is_empty() {
break;
}
}
if base.is_empty() {
String::new()
} else {
format!("/{}", base.join("/"))
}
}
pub fn relative_dir(entry: &FileEntry, base: &str) -> String {
let dir = parent_dir(&entry.path);
let rel = dir.strip_prefix(base).unwrap_or(dir);
rel.trim_matches('/').to_string()
}
pub fn select_child<'a>(
children: &'a [FileEntry],
fold_case: bool,
name: &str,
) -> Option<&'a FileEntry> {
children.iter().find(|c| c.name == name).or_else(|| {
fold_case
.then(|| children.iter().find(|c| c.name.eq_ignore_ascii_case(name)))
.flatten()
})
}
#[cfg(test)]
mod sort_tests {
use super::*;
fn e(name: &str, dir: bool) -> FileEntry {
let mut x = FileEntry::root();
x.name = name.to_string();
x.path = format!("/{name}");
x.entry_type = if dir {
EntryType::Directory
} else {
EntryType::File
};
x
}
#[test]
fn directories_come_first_then_case_insensitive_name() {
let mut v = vec![
e("zebra.txt", false),
e("Apple", true),
e("banana.TXT", false),
e("apple.txt", false),
e("Zoo", true),
];
sort_for_display(&mut v);
let got: Vec<&str> = v.iter().map(|x| x.name.as_str()).collect();
assert_eq!(
got,
["Apple", "Zoo", "apple.txt", "banana.TXT", "zebra.txt"],
"folders first, then files, each case-insensitively by name"
);
}
#[test]
fn uppercase_does_not_sort_ahead_of_lowercase() {
let mut v = vec![e("beta", false), e("Alpha", false), e("gamma", false)];
sort_for_display(&mut v);
let got: Vec<&str> = v.iter().map(|x| x.name.as_str()).collect();
assert_eq!(got, ["Alpha", "beta", "gamma"]);
}
}