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 }, } #[derive(Debug)] pub struct DirEntry { inner: DirEntryInner, metadata: OnceCell>, style: OnceCell>, } 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) -> 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 { 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 { 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 { 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 { self.file_type() } fn metadata(&self) -> Option { 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"))); } }