Files
sausage/userland/fd/src/dir_entry.rs
T
surma b9e3e3677d userland: search and exec tools, spawn library (T14)
rg (ripgrep 15.2), grep (ours, on ripgrep's regex crates), find and xargs
(ours), sed (uutils 0.3 wrapper), diff and cmp (uutils diffutils 0.6 with -r),
fd 10.5, jaq 2.3, env and timeout. userland/spawn is a small Command-like
client of the process interface. search-test.nu: 42 cases match native tools.
2026-11-06 17:21:51 +00:00

192 lines
5.3 KiB
Rust

use std::cell::OnceCell;
use std::ffi::OsString;
use std::fs::{FileType, Metadata};
use std::path::{Path, PathBuf};
use lscolors::{Colorable, LsColors, Style};
use crate::config::Config;
use crate::filesystem::strip_current_dir;
#[derive(Debug)]
enum DirEntryInner {
Normal(ignore::DirEntry),
// Broken symlinks reach us as walk errors rather than entries, so we carry
// over the depth the walker recorded on the error.
BrokenSymlink { path: PathBuf, depth: Option<usize> },
}
#[derive(Debug)]
pub struct DirEntry {
inner: DirEntryInner,
metadata: OnceCell<Option<Metadata>>,
style: OnceCell<Option<Style>>,
}
impl DirEntry {
#[inline]
pub fn normal(e: ignore::DirEntry) -> Self {
Self {
inner: DirEntryInner::Normal(e),
metadata: OnceCell::new(),
style: OnceCell::new(),
}
}
pub fn broken_symlink(path: PathBuf, depth: Option<usize>) -> Self {
Self {
inner: DirEntryInner::BrokenSymlink { path, depth },
metadata: OnceCell::new(),
style: OnceCell::new(),
}
}
pub fn path(&self) -> &Path {
match &self.inner {
DirEntryInner::Normal(e) => e.path(),
DirEntryInner::BrokenSymlink { path, .. } => path.as_path(),
}
}
pub fn into_path(self) -> PathBuf {
match self.inner {
DirEntryInner::Normal(e) => e.into_path(),
DirEntryInner::BrokenSymlink { path, .. } => path,
}
}
/// Returns the path as it should be presented to the user.
/// When stripping `./` would leave the path starting with `-`, keep the original
/// (with `./`) so downstream tools don't interpret it as an option.
pub fn stripped_path(&self, config: &Config) -> &Path {
let path = self.path();
if config.strip_cwd_prefix {
let stripped = strip_current_dir(path);
if starts_with_dash(stripped) {
path
} else {
stripped
}
} else {
path
}
}
/// Returns the path as it should be presented to the user.
pub fn into_stripped_path(self, config: &Config) -> PathBuf {
if config.strip_cwd_prefix {
self.stripped_path(config).to_path_buf()
} else {
self.into_path()
}
}
pub fn file_type(&self) -> Option<FileType> {
match &self.inner {
DirEntryInner::Normal(e) => e.file_type(),
DirEntryInner::BrokenSymlink { .. } => self.metadata().map(|m| m.file_type()),
}
}
pub fn metadata(&self) -> Option<&Metadata> {
self.metadata
.get_or_init(|| match &self.inner {
DirEntryInner::Normal(e) => e.metadata().ok(),
DirEntryInner::BrokenSymlink { path, .. } => path.symlink_metadata().ok(),
})
.as_ref()
}
pub fn depth(&self) -> Option<usize> {
match &self.inner {
DirEntryInner::Normal(e) => Some(e.depth()),
DirEntryInner::BrokenSymlink { depth, .. } => *depth,
}
}
pub fn style(&self, ls_colors: &LsColors) -> Option<&Style> {
self.style
.get_or_init(|| ls_colors.style_for(self).cloned())
.as_ref()
}
}
fn starts_with_dash(path: &Path) -> bool {
path.as_os_str().as_encoded_bytes().first() == Some(&b'-')
}
impl PartialEq for DirEntry {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.path() == other.path()
}
}
impl Eq for DirEntry {}
impl PartialOrd for DirEntry {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for DirEntry {
#[inline]
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.path().cmp(other.path())
}
}
impl Colorable for DirEntry {
fn path(&self) -> PathBuf {
self.path().to_owned()
}
fn file_name(&self) -> OsString {
let name = match &self.inner {
DirEntryInner::Normal(e) => e.file_name(),
DirEntryInner::BrokenSymlink { path, .. } => {
// Path::file_name() only works if the last component is Normal,
// but we want it for all component types, so we open code it.
// Copied from LsColors::style_for_path_with_metadata().
path.components()
.next_back()
.map(|c| c.as_os_str())
.unwrap_or_else(|| path.as_os_str())
}
};
name.to_owned()
}
fn file_type(&self) -> Option<FileType> {
self.file_type()
}
fn metadata(&self) -> Option<Metadata> {
self.metadata().cloned()
}
}
#[cfg(test)]
mod tests {
use super::starts_with_dash;
use std::path::Path;
#[test]
fn dash_prefixed_paths_detected() {
assert!(starts_with_dash(Path::new("-rf")));
assert!(starts_with_dash(Path::new("--delete")));
assert!(starts_with_dash(Path::new("-")));
}
#[test]
fn safe_paths_not_flagged() {
assert!(!starts_with_dash(Path::new("foo")));
assert!(!starts_with_dash(Path::new("./foo")));
assert!(!starts_with_dash(Path::new("sub/-rf")));
assert!(!starts_with_dash(Path::new("")));
assert!(!starts_with_dash(Path::new(" -rf")));
}
}