fast-fs 0.2.1

High-speed async file system traversal library with batteries-included file browser component
Documentation
// <FILE>crates/fast-fs/src/models/cls_file_entry.rs</FILE> - <DESC>FileEntry struct</DESC>
// <VERS>VERSION: 0.5.0</VERS>
// <WCTX>Parent directory entry support</WCTX>
// <CLOG>Added parent_entry() factory method for ".." entries</CLOG>

//! File entry representation
use crate::nav::FileCategory;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::SystemTime;

/// A file entry with cached metadata
#[derive(Debug, Clone)]
pub struct FileEntry {
    /// Full path to the file
    pub path: PathBuf,
    /// File name (cached for sorting, lossy UTF-8)
    pub name: String,
    /// Is this a directory?
    pub is_dir: bool,
    /// Is this a hidden file?
    pub is_hidden: bool,
    /// File size in bytes (0 for directories)
    pub size: u64,
    /// Last modified time
    pub modified: Option<SystemTime>,
    /// Is this a symbolic link? (from DirEntry::file_type)
    pub is_symlink: bool,
    /// Is this file readonly? (from metadata.permissions)
    pub is_readonly: bool,
}

impl FileEntry {
    /// Create a new file entry from a path and metadata
    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,
        }
    }

    /// Create a parent directory entry ("..")
    ///
    /// This is used to display and navigate to the parent directory
    /// in file browsers. The path should be the actual parent path.
    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,
        }
    }

    /// Check if this is a parent directory entry ("..")
    pub fn is_parent_entry(&self) -> bool {
        self.name == ".."
    }

    /// Get the file extension if any
    pub fn extension(&self) -> Option<&str> {
        self.path.extension().and_then(|e| e.to_str())
    }

    /// Resolve symlink target. Returns None if not a symlink or broken/loop.
    /// **Cost:** One read_link syscall. Cache result if called repeatedly.
    pub fn resolve_symlink(&self) -> Option<PathBuf> {
        if !self.is_symlink {
            return None;
        }
        fs::read_link(&self.path).ok()
    }

    /// Check if file is executable.
    /// **Cost:** Unix: stat() for mode bits. Windows: extension check (cheap).
    #[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)
    }

    /// Check if file is executable.
    /// **Cost:** Windows: extension check (cheap).
    #[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)
    }

    /// Fallback for other platforms
    #[cfg(not(any(unix, windows)))]
    pub fn is_executable(&self) -> bool {
        false
    }

    /// Detect broken symlink (target doesn't exist).
    /// **Cost:** One stat() on target path.
    pub fn is_symlink_broken(&self) -> bool {
        if !self.is_symlink {
            return false;
        }
        // A symlink is broken if we can read_link but the target doesn't exist
        match fs::read_link(&self.path) {
            Ok(target) => {
                let target_path = resolve_relative_to(&self.path, &target);
                !target_path.exists()
            }
            Err(_) => true, // Can't read link, consider it broken
        }
    }

    /// Detect symlink loop (circular reference).
    /// **Cost:** Path canonicalization.
    pub fn is_symlink_loop(&self) -> bool {
        if !self.is_symlink {
            return false;
        }
        // A loop is detected when canonicalize fails with a specific error
        // or when we detect circular references
        match fs::canonicalize(&self.path) {
            Ok(_) => false,
            Err(e) => {
                // On Unix, ELOOP error indicates a symlink loop
                e.raw_os_error() == Some(40) // ELOOP on Linux
                    || e.kind() == std::io::ErrorKind::NotFound
                    || matches!(e.kind(), std::io::ErrorKind::Other)
            }
        }
    }

    /// Get the file category for UI icons and grouping.
    ///
    /// Resolution order:
    /// 1. If symlink → Symlink
    /// 2. If directory → Directory
    /// 3. If executable (Unix: +x, Windows: exe/bat/etc) → Executable
    /// 4. Otherwise → based on extension
    ///
    /// **Cost:** May call is_executable() which does a stat() on Unix.
    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)
        }
    }
}

/// Resolve a relative path against a base path
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);
    }
}

// <FILE>crates/fast-fs/src/models/cls_file_entry.rs</FILE> - <DESC>FileEntry struct</DESC>
// <VERS>END OF VERSION: 0.5.0</VERS>