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")));
}
}