use crate::nav::FileCategory;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
#[derive(Debug, Clone)]
pub struct FileEntry {
pub path: PathBuf,
pub name: String,
pub is_dir: bool,
pub is_hidden: bool,
pub size: u64,
pub modified: Option<SystemTime>,
pub is_symlink: bool,
pub is_readonly: bool,
}
impl FileEntry {
pub fn new(
path: PathBuf,
is_dir: bool,
size: u64,
modified: Option<SystemTime>,
is_symlink: bool,
is_readonly: bool,
) -> Self {
let name = path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default();
let is_hidden = name.starts_with('.');
Self {
path,
name,
is_dir,
is_hidden,
size,
modified,
is_symlink,
is_readonly,
}
}
pub fn parent_entry(parent_path: PathBuf) -> Self {
Self {
path: parent_path,
name: "..".to_string(),
is_dir: true,
is_hidden: false,
size: 0,
modified: None,
is_symlink: false,
is_readonly: false,
}
}
pub fn is_parent_entry(&self) -> bool {
self.name == ".."
}
pub fn extension(&self) -> Option<&str> {
self.path.extension().and_then(|e| e.to_str())
}
pub fn resolve_symlink(&self) -> Option<PathBuf> {
if !self.is_symlink {
return None;
}
fs::read_link(&self.path).ok()
}
#[cfg(unix)]
pub fn is_executable(&self) -> bool {
use std::os::unix::fs::PermissionsExt;
fs::metadata(&self.path)
.map(|m| m.permissions().mode() & 0o111 != 0)
.unwrap_or(false)
}
#[cfg(windows)]
pub fn is_executable(&self) -> bool {
const EXECUTABLE_EXTENSIONS: &[&str] = &["exe", "bat", "cmd", "ps1", "com", "msi"];
self.extension()
.map(|ext| EXECUTABLE_EXTENSIONS.contains(&ext.to_lowercase().as_str()))
.unwrap_or(false)
}
#[cfg(not(any(unix, windows)))]
pub fn is_executable(&self) -> bool {
false
}
pub fn is_symlink_broken(&self) -> bool {
if !self.is_symlink {
return false;
}
match fs::read_link(&self.path) {
Ok(target) => {
let target_path = resolve_relative_to(&self.path, &target);
!target_path.exists()
}
Err(_) => true, }
}
pub fn is_symlink_loop(&self) -> bool {
if !self.is_symlink {
return false;
}
match fs::canonicalize(&self.path) {
Ok(_) => false,
Err(e) => {
e.raw_os_error() == Some(40) || e.kind() == std::io::ErrorKind::NotFound
|| matches!(e.kind(), std::io::ErrorKind::Other)
}
}
}
pub fn category(&self) -> FileCategory {
if self.is_symlink {
FileCategory::Symlink
} else if self.is_dir {
FileCategory::Directory
} else if self.is_executable() {
FileCategory::Executable
} else {
self.extension()
.map(FileCategory::from_extension)
.unwrap_or(FileCategory::Unknown)
}
}
}
fn resolve_relative_to(base: &Path, target: &Path) -> PathBuf {
if target.is_absolute() {
target.to_path_buf()
} else {
base.parent()
.map(|p| p.join(target))
.unwrap_or_else(|| target.to_path_buf())
}
}
impl PartialEq for FileEntry {
fn eq(&self, other: &Self) -> bool {
self.path == other.path
}
}
impl Eq for FileEntry {}
impl std::hash::Hash for FileEntry {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.path.hash(state);
}
}