diff --git a/Cargo.lock b/Cargo.lock index 083d19187..48fba57c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3608,6 +3608,7 @@ dependencies = [ "clap", "codspeed-divan-compat", "fluent", + "fnv", "glob", "hostname", "lscolors", diff --git a/src/uu/ls/Cargo.toml b/src/uu/ls/Cargo.toml index b130d0a9e..5fab67614 100644 --- a/src/uu/ls/Cargo.toml +++ b/src/uu/ls/Cargo.toml @@ -24,6 +24,7 @@ ansi-width = { workspace = true } clap = { workspace = true, features = ["env"] } glob = { workspace = true } hostname = { workspace = true } +fnv = { workspace = true } lscolors = { workspace = true } selinux = { workspace = true, optional = true } terminal_size = { workspace = true } diff --git a/src/uu/ls/src/colors.rs b/src/uu/ls/src/colors.rs index 4affbbf8c..a7f58d0fd 100644 --- a/src/uu/ls/src/colors.rs +++ b/src/uu/ls/src/colors.rs @@ -3,11 +3,9 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. use super::PathData; -use super::get_metadata_with_deref_opt; -use lscolors::{Indicator, LsColors, Style}; +use lscolors::{Colorable, Indicator, LsColors, Style}; use std::ffi::OsString; -use std::fs::{DirEntry, Metadata}; -use std::io::{BufWriter, Stdout}; +use std::fs::Metadata; /// We need this struct to be able to store the previous style. /// This because we need to check the previous value in case we don't need @@ -135,25 +133,22 @@ impl<'a> StyleManager<'a> { self.apply_style(style, name, wrap) } - pub(crate) fn apply_style_based_on_dir_entry( + pub(crate) fn apply_style_based_on_colorable( &mut self, - dir_entry: &DirEntry, + path: &T, name: OsString, wrap: bool, ) -> OsString { - let style = self.colors.style_for(dir_entry); + let style = self.colors.style_for(path); self.apply_style(style, name, wrap) } } /// Colors the provided name based on the style determined for the given path -/// This function is quite long because it tries to leverage [`DirEntry`] to avoid -/// unnecessary calls to stat and manages the symlink errors pub(crate) fn color_name( name: OsString, path: &PathData, style_manager: &mut StyleManager, - out: &mut BufWriter, target_symlink: Option<&PathData>, wrap: bool, ) -> OsString { @@ -179,23 +174,21 @@ pub(crate) fn color_name( if !path.must_dereference { // If we need to dereference (follow) a symlink, we will need to get the metadata - if let Some(de) = &path.de { - // There is a DirEntry, we don't need to get the metadata for the color - return style_manager.apply_style_based_on_dir_entry(de, name, wrap); - } + // There is a DirEntry, we don't need to get the metadata for the color + return style_manager.apply_style_based_on_colorable(path, name, wrap); } if let Some(target) = target_symlink { // use the optional target_symlink - // Use fn get_metadata_with_deref_opt instead of get_metadata() here because ls + // Use fn symlink_metadata directly instead of get_metadata() here because ls // should not exit with an err, if we are unable to obtain the target_metadata - let md_res = get_metadata_with_deref_opt(&target.p_buf, path.must_dereference); - let md = md_res.or_else(|_| path.p_buf.symlink_metadata()); - style_manager.apply_style_based_on_metadata(path, md.ok().as_ref(), name, wrap) + style_manager.apply_style_based_on_colorable(target, name, wrap) } else { - let md_option = path.get_metadata(out); - let symlink_metadata = path.p_buf.symlink_metadata().ok(); - let md = md_option.or(symlink_metadata.as_ref()); - style_manager.apply_style_based_on_metadata(path, md, name, wrap) + let md_option: Option = path + .metadata() + .cloned() + .or_else(|| path.p_buf.symlink_metadata().ok()); + + style_manager.apply_style_based_on_metadata(path, md_option.as_ref(), name, wrap) } } diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index ba7f26f1a..e677e3b16 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -6,7 +6,10 @@ // spell-checker:ignore (ToDO) somegroup nlink tabsize dired subdired dtype colorterm stringly nohash strtime #[cfg(unix)] -use std::collections::HashMap; +use fnv::FnvHashMap as HashMap; +use fnv::FnvHashSet as HashSet; +use std::borrow::Cow; +use std::cell::RefCell; #[cfg(unix)] use std::os::unix::fs::{FileTypeExt, MetadataExt}; #[cfg(windows)] @@ -14,7 +17,6 @@ use std::os::windows::fs::MetadataExt; use std::{ cell::{LazyCell, OnceCell}, cmp::Reverse, - collections::HashSet, ffi::{OsStr, OsString}, fmt::Write as FmtWrite, fs::{self, DirEntry, FileType, Metadata, ReadDir}, @@ -32,7 +34,7 @@ use clap::{ builder::{NonEmptyStringValueParser, PossibleValue, ValueParser}, }; use glob::{MatchOptions, Pattern}; -use lscolors::LsColors; +use lscolors::{Colorable, LsColors}; use term_grid::{DEFAULT_SEPARATOR_SIZE, Direction, Filling, Grid, GridOptions, SPACES_IN_TAB}; use thiserror::Error; @@ -1772,22 +1774,22 @@ struct PathData { // Result got from symlink_metadata() or metadata() based on config md: OnceCell>, ft: OnceCell>, - // can be used to avoid reading the metadata. Can be also called d_type: + // can be used to avoid reading the filetype. Can be also called d_type: // https://www.gnu.org/software/libc/manual/html_node/Directory-Entries.html - de: Option, + de: RefCell>>, + security_context: OnceCell>, // Name of the file - will be empty for . or .. display_name: OsString, // PathBuf that all above data corresponds to p_buf: PathBuf, must_dereference: bool, - security_context: String, command_line: bool, } impl PathData { fn new( p_buf: PathBuf, - dir_entry: Option>, + dir_entry: Option, file_name: Option, config: &Config, command_line: bool, @@ -1797,13 +1799,14 @@ impl PathData { let display_name = if let Some(name) = file_name { name } else if command_line { - p_buf.clone().into() + p_buf.as_os_str().to_os_string() } else { - p_buf - .file_name() - .unwrap_or_else(|| p_buf.iter().next_back().unwrap()) - .to_owned() + dir_entry + .as_ref() + .map(|inner| inner.file_name()) + .unwrap_or_default() }; + let must_dereference = match &config.dereference { Dereference::All => true, Dereference::Args => command_line, @@ -1821,78 +1824,67 @@ impl PathData { Dereference::None => false, }; - let de: Option = match dir_entry { - Some(de) => de.ok(), - None => None, - }; - // Why prefer to check the DirEntry file_type()? B/c the call is // nearly free compared to a metadata() call on a Path - fn get_file_type( - de: &DirEntry, - p_buf: &Path, - must_dereference: bool, - ) -> OnceCell> { + let ft: OnceCell> = OnceCell::new(); + let md: OnceCell> = OnceCell::new(); + let security_context: OnceCell> = OnceCell::new(); + + let de: RefCell>> = if let Some(de) = dir_entry { if must_dereference { if let Ok(md_pb) = p_buf.metadata() { - return OnceCell::from(Some(md_pb.file_type())); + md.get_or_init(|| Some(md_pb.clone())); + ft.get_or_init(|| Some(md_pb.file_type())); } } + if let Ok(ft_de) = de.file_type() { - OnceCell::from(Some(ft_de)) - } else if let Ok(md_pb) = p_buf.symlink_metadata() { - OnceCell::from(Some(md_pb.file_type())) - } else { - OnceCell::new() + ft.get_or_init(|| Some(ft_de)); } - } - let ft = match de { - Some(ref de) => get_file_type(de, &p_buf, must_dereference), - None => OnceCell::new(), + + RefCell::new(Some(de.into())) + } else { + RefCell::new(None) }; - let security_context = get_security_context(config, &p_buf, must_dereference); - Self { - md: OnceCell::new(), + md, ft, de, + security_context, display_name, p_buf, must_dereference, - security_context, command_line, } } - fn get_metadata(&self, out: &mut BufWriter) -> Option<&Metadata> { + fn metadata(&self) -> Option<&Metadata> { self.md .get_or_init(|| { - // check if we can use DirEntry metadata - // it will avoid a call to stat() if !self.must_dereference { - if let Some(dir_entry) = &self.de { + if let Some(dir_entry) = RefCell::take(&self.de).as_deref() { return dir_entry.metadata().ok(); } } - // if not, check if we can use Path metadata - match get_metadata_with_deref_opt(self.p_buf.as_path(), self.must_dereference) { + match get_metadata_with_deref_opt(self.path(), self.must_dereference) { Err(err) => { // FIXME: A bit tricky to propagate the result here - out.flush().unwrap(); + let mut out: std::io::StdoutLock<'static> = stdout().lock(); + let _ = out.flush(); let errno = err.raw_os_error().unwrap_or(1i32); // a bad fd will throw an error when dereferenced, // but GNU will not throw an error until a bad fd "dir" // is entered, here we match that GNU behavior, by handing // back the non-dereferenced metadata upon an EBADF if self.must_dereference && errno == 9i32 { - if let Some(dir_entry) = &self.de { - return dir_entry.metadata().ok(); + if let Ok(file) = self.path().read_link() { + return file.symlink_metadata().ok(); } } show!(LsError::IOErrorContext( - self.p_buf.clone(), + self.path().to_path_buf(), err, self.command_line )); @@ -1904,11 +1896,50 @@ impl PathData { .as_ref() } - fn file_type(&self, out: &mut BufWriter) -> Option<&FileType> { + fn file_type(&self) -> Option<&FileType> { self.ft - .get_or_init(|| self.get_metadata(out).map(|md| md.file_type())) + .get_or_init(|| self.metadata().map(|md| md.file_type())) .as_ref() } + + fn is_dangling_link(&self) -> bool { + // deref enabled, self is real dir entry, self has metadata associated with link, but not with target + self.must_dereference && self.file_type().is_none() && self.metadata().is_none() + } + + #[cfg(unix)] + fn is_executable_file(&self) -> bool { + self.file_type().is_some_and(|f| f.is_file()) + && self.metadata().is_some_and(file_is_executable) + } + + fn security_context(&self, config: &Config) -> &str { + self.security_context + .get_or_init(|| get_security_context(&self.p_buf, self.must_dereference, config).into()) + } + + fn path(&self) -> &Path { + &self.p_buf + } + + fn display_name(&self) -> &OsStr { + &self.display_name + } +} + +impl Colorable for PathData { + fn file_name(&self) -> OsString { + self.display_name().to_os_string() + } + fn file_type(&self) -> Option { + self.file_type().copied() + } + fn metadata(&self) -> Option { + self.metadata().cloned() + } + fn path(&self) -> PathBuf { + self.path().to_path_buf() + } } /// Show the directory name in the case where several arguments are given to ls @@ -1928,7 +1959,7 @@ fn show_dir_name( config: &Config, ) -> std::io::Result<()> { let escaped_name = - locale_aware_escape_dir_name(path_data.p_buf.as_os_str(), config.quoting_style); + locale_aware_escape_dir_name(path_data.path().as_os_str(), config.quoting_style); let name = if config.hyperlink && !config.dired { create_hyperlink(&escaped_name, path_data) @@ -1967,9 +1998,9 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> { out: BufWriter::new(stdout()), style_manager: config.color.as_ref().map(StyleManager::new), #[cfg(unix)] - uid_cache: HashMap::new(), + uid_cache: HashMap::default(), #[cfg(unix)] - gid_cache: HashMap::new(), + gid_cache: HashMap::default(), // Time range for which to use the "recent" format. Anything from 0.5 year in the past to now // (files with modification time in the future use "old" format). // According to GNU a Gregorian year has 365.2425 * 24 * 60 * 60 == 31556952 seconds on the average. @@ -1986,11 +2017,11 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> { // Proper GNU handling is don't show if dereferenced symlink DNE // but only for the base dir, for a child dir show, and print ?s // in long format - if path_data.get_metadata(&mut state.out).is_none() { + if path_data.metadata().is_none() { continue; } - let show_dir_contents = match path_data.file_type(&mut state.out) { + let show_dir_contents = match path_data.file_type() { Some(ft) => !config.directory && ft.is_dir(), None => { set_exit_code(1); @@ -2005,8 +2036,8 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> { } } - sort_entries(&mut files, config, &mut state.out); - sort_entries(&mut dirs, config, &mut state.out); + sort_entries(&mut files, config); + sort_entries(&mut dirs, config); if let Some(style_manager) = state.style_manager.as_mut() { // ls will try to write a reset before anything is written if normal @@ -2022,12 +2053,12 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> { for (pos, path_data) in dirs.iter().enumerate() { // Do read_dir call here to match GNU semantics by printing // read_dir errors before directory headings, names and totals - let read_dir = match fs::read_dir(&path_data.p_buf) { + let read_dir = match fs::read_dir(path_data.path()) { Err(err) => { // flush stdout buffer before the error to preserve formatting and order state.out.flush()?; show!(LsError::IOErrorContext( - path_data.p_buf.clone(), + path_data.path().to_path_buf(), err, path_data.command_line )); @@ -2046,7 +2077,7 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> { writeln!(state.out)?; if config.dired { // First directory displayed - let dir_len = path_data.display_name.len(); + let dir_len = path_data.display_name().len(); // add the //SUBDIRED// coordinates dired::calculate_subdired(&mut dired, dir_len); // Add the padding for the dir name @@ -2058,9 +2089,9 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> { writeln!(state.out)?; } } - let mut listed_ancestors = HashSet::new(); + let mut listed_ancestors = HashSet::default(); listed_ancestors.insert(FileInformation::from_path( - &path_data.p_buf, + path_data.path(), path_data.must_dereference, )?); enter_directory( @@ -2078,38 +2109,38 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> { Ok(()) } -fn sort_entries(entries: &mut [PathData], config: &Config, out: &mut BufWriter) { +fn sort_entries(entries: &mut [PathData], config: &Config) { match config.sort { Sort::Time => entries.sort_by_key(|k| { Reverse( - k.get_metadata(out) + k.metadata() .and_then(|md| metadata_get_time(md, config.time)) .unwrap_or(UNIX_EPOCH), ) }), Sort::Size => { - entries.sort_by_key(|k| Reverse(k.get_metadata(out).map_or(0, |md| md.len()))); + entries.sort_by_key(|k| Reverse(k.metadata().map_or(0, |md| md.len()))); } // The default sort in GNU ls is case insensitive - Sort::Name => entries.sort_by(|a, b| a.display_name.cmp(&b.display_name)), + Sort::Name => entries.sort_by(|a, b| a.display_name().cmp(b.display_name())), Sort::Version => entries.sort_by(|a, b| { version_cmp( - os_str_as_bytes_lossy(a.p_buf.as_os_str()).as_ref(), - os_str_as_bytes_lossy(b.p_buf.as_os_str()).as_ref(), + os_str_as_bytes_lossy(a.path().as_os_str()).as_ref(), + os_str_as_bytes_lossy(b.path().as_os_str()).as_ref(), ) - .then(a.p_buf.to_string_lossy().cmp(&b.p_buf.to_string_lossy())) + .then(a.path().to_string_lossy().cmp(&b.path().to_string_lossy())) }), Sort::Extension => entries.sort_by(|a, b| { - a.p_buf + a.path() .extension() - .cmp(&b.p_buf.extension()) - .then(a.p_buf.file_stem().cmp(&b.p_buf.file_stem())) + .cmp(&b.path().extension()) + .then(a.path().file_stem().cmp(&b.path().file_stem())) }), Sort::Width => entries.sort_by(|a, b| { - a.display_name + a.display_name() .len() - .cmp(&b.display_name.len()) - .then(a.display_name.cmp(&b.display_name)) + .cmp(&b.display_name().len()) + .then(a.display_name().cmp(b.display_name())) }), Sort::None => {} } @@ -2120,19 +2151,23 @@ fn sort_entries(entries: &mut [PathData], config: &Config, out: &mut BufWriter { + !match ft { + None => { // If it metadata cannot be determined, treat as a file. get_metadata_with_deref_opt(p.p_buf.as_path(), true) .map_or_else(|_| false, |m| m.is_dir()) } - Some(Some(m)) => m.is_dir(), + Some(ft) => ft.is_dir(), } }); } @@ -2167,16 +2202,19 @@ fn should_display(entry: &DirEntry, config: &Config) -> bool { require_literal_separator: false, case_sensitive: true, }; + let file_name = entry.file_name(); // If the decoding fails, still match best we can // FIXME: use OsStrings or Paths once we have a glob crate that supports it: // https://github.com/rust-lang/glob/issues/23 // https://github.com/rust-lang/glob/issues/78 // https://github.com/BurntSushi/ripgrep/issues/1250 + let file_name = match file_name.to_str() { - Some(s) => s.to_string(), - None => file_name.to_string_lossy().into_owned(), + Some(s) => Cow::Borrowed(s), + None => file_name.to_string_lossy(), }; + !config .ignore_patterns .iter() @@ -2196,14 +2234,14 @@ fn enter_directory( let mut entries: Vec = if config.files == Files::All { vec![ PathData::new( - path_data.p_buf.clone(), + path_data.path().to_path_buf(), None, Some(".".into()), config, false, ), PathData::new( - path_data.p_buf.join(".."), + path_data.path().join(".."), None, Some("..".into()), config, @@ -2227,12 +2265,12 @@ fn enter_directory( if should_display(&dir_entry, config) { let entry_path_data = - PathData::new(dir_entry.path(), Some(Ok(dir_entry)), None, config, false); + PathData::new(dir_entry.path(), Some(dir_entry), None, config, false); entries.push(entry_path_data); } } - sort_entries(&mut entries, config, &mut state.out); + sort_entries(&mut entries, config); // Print total after any error display if config.format == Format::Long || config.alloc_size { @@ -2249,23 +2287,20 @@ fn enter_directory( for e in entries .iter() .skip(if config.files == Files::All { 2 } else { 0 }) - .filter(|p| { - p.ft.get() - .is_some_and(|o_ft| o_ft.is_some_and(|ft| ft.is_dir())) - }) + .filter(|p| p.file_type().is_some_and(|ft| ft.is_dir())) { - match fs::read_dir(&e.p_buf) { + match fs::read_dir(e.path()) { Err(err) => { state.out.flush()?; show!(LsError::IOErrorContext( - e.p_buf.clone(), + e.path().to_path_buf(), err, e.command_line )); } Ok(rd) => { if listed_ancestors - .insert(FileInformation::from_path(&e.p_buf, e.must_dereference)?) + .insert(FileInformation::from_path(e.path(), e.must_dereference)?) { // when listing several directories in recursive mode, we show // "dirname:" at the beginning of the file list @@ -2276,7 +2311,7 @@ fn enter_directory( // 2 = \n + \n dired.padding = 2; dired::indent(&mut state.out)?; - let dir_name_size = e.p_buf.to_string_lossy().len(); + let dir_name_size = e.path().to_string_lossy().len(); dired::calculate_subdired(dired, dir_name_size); // inject dir name dired::add_dir_name(dired, dir_name_size); @@ -2286,10 +2321,10 @@ fn enter_directory( writeln!(state.out)?; enter_directory(e, rd, config, state, listed_ancestors, dired)?; listed_ancestors - .remove(&FileInformation::from_path(&e.p_buf, e.must_dereference)?); + .remove(&FileInformation::from_path(e.path(), e.must_dereference)?); } else { state.out.flush()?; - show!(LsError::AlreadyListedError(e.p_buf.clone())); + show!(LsError::AlreadyListedError(e.path().to_path_buf())); } } } @@ -2313,7 +2348,7 @@ fn display_dir_entry_size( state: &mut ListState, ) -> (usize, usize, usize, usize, usize, usize) { // TODO: Cache/memorize the display_* results so we don't have to recalculate them. - if let Some(md) = entry.get_metadata(&mut state.out) { + if let Some(md) = entry.metadata() { let (size_len, major_len, minor_len) = match display_len_or_rdev(md, config) { SizeOrDeviceId::Device(major, minor) => { (major.len() + minor.len() + 2usize, major.len(), minor.len()) @@ -2370,7 +2405,7 @@ fn return_total( let mut total_size = 0; for item in items { total_size += item - .get_metadata(out) + .metadata() .as_ref() .map_or(0, |md| get_block_size(md, config)); } @@ -2388,13 +2423,12 @@ fn display_additional_leading_info( item: &PathData, padding: &PaddingCollection, config: &Config, - out: &mut BufWriter, ) -> UResult { let mut result = String::new(); #[cfg(unix)] { if config.inode { - let i = if let Some(md) = item.get_metadata(out) { + let i = if let Some(md) = item.metadata() { get_inode(md) } else { "?".to_owned() @@ -2404,7 +2438,7 @@ fn display_additional_leading_info( } if config.alloc_size { - let s = if let Some(md) = item.get_metadata(out) { + let s = if let Some(md) = item.metadata() { display_size(get_block_size(md, config), config) } else { "?".to_owned() @@ -2431,7 +2465,7 @@ fn display_items( // option, print the security context to the left of the size column. let quoted = items.iter().any(|item| { - let name = locale_aware_escape_name(&item.display_name, config.quoting_style); + let name = locale_aware_escape_name(item.display_name(), config.quoting_style); os_str_starts_with(&name, b"'") }); @@ -2445,12 +2479,7 @@ fn display_items( let should_display_leading_info = config.alloc_size; if should_display_leading_info { - let more_info = display_additional_leading_info( - item, - &padding_collection, - config, - &mut state.out, - )?; + let more_info = display_additional_leading_info(item, &padding_collection, config)?; write!(state.out, "{more_info}")?; } @@ -2461,7 +2490,7 @@ fn display_items( let mut longest_context_len = 1; let prefix_context = if config.context { for item in items { - let context_len = item.security_context.len(); + let context_len = item.security_context(config).len(); longest_context_len = context_len.max(longest_context_len); } Some(longest_context_len) @@ -2477,8 +2506,18 @@ fn display_items( } let mut names_vec = Vec::new(); + + #[cfg(unix)] + let should_display_leading_info = config.inode || config.alloc_size; + #[cfg(not(unix))] + let should_display_leading_info = config.alloc_size; + for i in items { - let more_info = display_additional_leading_info(i, &padding, config, &mut state.out)?; + let more_info = if should_display_leading_info { + Some(display_additional_leading_info(i, &padding, config)?) + } else { + None + }; // it's okay to set current column to zero which is used to decide // whether text will wrap or not, because when format is grid or // column ls will try to place the item name in a new line if it @@ -2702,14 +2741,14 @@ fn display_item_long( if config.dired { output_display.extend(b" "); } - if let Some(md) = item.get_metadata(&mut state.out) { + if let Some(md) = item.metadata() { #[cfg(any(not(unix), target_os = "android", target_os = "macos"))] // TODO: See how Mac should work here let is_acl_set = false; #[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] - let is_acl_set = has_acl(item.display_name.as_os_str()); + let is_acl_set = has_acl(item.display_name()); output_display.extend(display_permissions(md, true).as_bytes()); - if item.security_context.len() > 1 { + if item.security_context(config).len() > 1 { // GNU `ls` uses a "." character to indicate a file with a security context, // but not other alternate access method. output_display.extend(b"."); @@ -2731,7 +2770,7 @@ fn display_item_long( if config.context { output_display.extend(b" "); - output_display.extend_pad_right(&item.security_context, padding.context); + output_display.extend_pad_right(item.security_context(config), padding.context); } // Author is only different from owner on GNU/Hurd, so we reuse @@ -2778,7 +2817,7 @@ fn display_item_long( item, config, None, - String::new(), + None, state, LazyCell::new(Box::new(|| { ansi_width(&String::from_utf8_lossy(&output_display)) @@ -2806,7 +2845,7 @@ fn display_item_long( } else { #[cfg(unix)] let leading_char = { - if let Some(Some(ft)) = item.ft.get() { + if let Some(ft) = item.file_type() { if ft.is_char_device() { "c" } else if ft.is_block_device() { @@ -2818,13 +2857,15 @@ fn display_item_long( } else { "-" } + } else if item.is_dangling_link() { + "l" } else { "-" } }; #[cfg(not(unix))] let leading_char = { - if let Some(Some(ft)) = item.ft.get() { + if let Some(ft) = item.file_type() { if ft.is_symlink() { "l" } else if ft.is_dir() { @@ -2832,6 +2873,8 @@ fn display_item_long( } else { "-" } + } else if item.is_dangling_link() { + "l" } else { "-" } @@ -2839,7 +2882,7 @@ fn display_item_long( output_display.extend(leading_char.as_bytes()); output_display.extend(b"?????????"); - if item.security_context.len() > 1 { + if item.security_context(config).len() > 1 { // GNU `ls` uses a "." character to indicate a file with a security context, // but not other alternate access method. output_display.extend(b"."); @@ -2859,7 +2902,7 @@ fn display_item_long( if config.context { output_display.extend(b" "); - output_display.extend_pad_right(&item.security_context, padding.context); + output_display.extend_pad_right(item.security_context(config), padding.context); } // Author is only different from owner on GNU/Hurd, so we reuse @@ -2873,7 +2916,7 @@ fn display_item_long( item, config, None, - String::new(), + None, state, LazyCell::new(Box::new(|| { ansi_width(&String::from_utf8_lossy(&output_display)) @@ -3017,8 +3060,8 @@ fn file_is_executable(md: &Metadata) -> bool { return md.mode() & ((S_IXUSR | S_IXGRP | S_IXOTH) as u32) != 0; } -fn classify_file(path: &PathData, out: &mut BufWriter) -> Option { - let file_type = path.file_type(out)?; +fn classify_file(path: &PathData) -> Option { + let file_type = path.file_type()?; if file_type.is_dir() { Some('/') @@ -3031,11 +3074,9 @@ fn classify_file(path: &PathData, out: &mut BufWriter) -> Option { Some('=') } else if file_type.is_fifo() { Some('|') - } else if file_type.is_file() // Safe unwrapping if the file was removed between listing and display // See https://github.com/uutils/coreutils/issues/5371 - && path.get_metadata(out).is_some_and(file_is_executable) - { + } else if path.is_executable_file() { Some('*') } else { None @@ -3065,12 +3106,12 @@ fn display_item_name( path: &PathData, config: &Config, prefix_context: Option, - more_info: String, + more_info: Option, state: &mut ListState, current_column: LazyCell usize + '_>>, ) -> OsString { // This is our return value. We start by `&path.display_name` and modify it along the way. - let mut name = locale_aware_escape_name(&path.display_name, config.quoting_style); + let mut name = locale_aware_escape_name(path.display_name(), config.quoting_style); let is_wrap = |namelen: usize| config.width != 0 && *current_column + namelen > config.width.into(); @@ -3081,24 +3122,19 @@ fn display_item_name( if let Some(style_manager) = &mut state.style_manager { let len = name.len(); - name = color_name( - name, - path, - style_manager, - &mut state.out, - None, - is_wrap(len), - ); + name = color_name(name, path, style_manager, None, is_wrap(len)); } - if config.format != Format::Long && !more_info.is_empty() { - let old_name = name; - name = more_info.into(); - name.push(&old_name); + if config.format != Format::Long { + if let Some(info) = more_info { + let old_name = name; + name = info.into(); + name.push(&old_name); + } } if config.indicator_style != IndicatorStyle::None { - let sym = classify_file(path, &mut state.out); + let sym = classify_file(path); let char_opt = match config.indicator_style { IndicatorStyle::Classify => sym, @@ -3125,12 +3161,11 @@ fn display_item_name( } if config.format == Format::Long - && path.file_type(&mut state.out).is_some() - && path.file_type(&mut state.out).unwrap().is_symlink() + && path.file_type().is_some_and(|ft| ft.is_symlink()) && !path.must_dereference { - match path.p_buf.read_link() { - Ok(target) => { + match path.path().read_link() { + Ok(target_path) => { name.push(" -> "); // We might as well color the symlink output after the arrow. @@ -3139,9 +3174,9 @@ fn display_item_name( if let Some(style_manager) = &mut state.style_manager { // We get the absolute path to be able to construct PathData with valid Metadata. // This is because relative symlinks will fail to get_metadata. - let mut absolute_target = target.clone(); - if target.is_relative() { - if let Some(parent) = path.p_buf.parent() { + let mut absolute_target = target_path.clone(); + if target_path.is_relative() { + if let Some(parent) = path.path().parent() { absolute_target = parent.join(absolute_target); } } @@ -3152,20 +3187,13 @@ fn display_item_name( // Because we use an absolute path, we can assume this is guaranteed to exist. // Otherwise, we use path.md(), which will guarantee we color to the same // color of non-existent symlinks according to style_for_path_with_metadata. - if path.get_metadata(&mut state.out).is_none() - && get_metadata_with_deref_opt( - target_data.p_buf.as_path(), - target_data.must_dereference, - ) - .is_err() - { - name.push(path.p_buf.read_link().unwrap()); + if path.metadata().is_none() && target_data.metadata().is_none() { + name.push(target_path); } else { name.push(color_name( - locale_aware_escape_name(target.as_os_str(), config.quoting_style), + locale_aware_escape_name(target_path.as_os_str(), config.quoting_style), path, style_manager, - &mut state.out, Some(&target_data), is_wrap(name.len()), )); @@ -3174,13 +3202,17 @@ fn display_item_name( // If no coloring is required, we just use target as is. // Apply the right quoting name.push(locale_aware_escape_name( - target.as_os_str(), + target_path.as_os_str(), config.quoting_style, )); } } Err(err) => { - show!(LsError::IOErrorContext(path.p_buf.clone(), err, false)); + show!(LsError::IOErrorContext( + path.path().to_path_buf(), + err, + false + )); } } } @@ -3190,10 +3222,11 @@ fn display_item_name( if config.context { if let Some(pad_count) = prefix_context { let security_context = if matches!(config.format, Format::Commas) { - path.security_context.clone() + path.security_context(config).to_string() } else { - pad_left(&path.security_context, pad_count) + pad_left(path.security_context(config), pad_count) }; + let old_name = name; name = format!("{security_context} ").into(); name.push(old_name); @@ -3207,7 +3240,7 @@ fn create_hyperlink(name: &OsStr, path: &PathData) -> OsString { let hostname = hostname::get().unwrap_or_else(|_| OsString::from("")); let hostname = hostname.to_string_lossy(); - let absolute_path = fs::canonicalize(&path.p_buf).unwrap_or_default(); + let absolute_path = fs::canonicalize(path.path()).unwrap_or_default(); let absolute_path = absolute_path.to_string_lossy(); #[cfg(not(target_os = "windows"))] @@ -3253,57 +3286,60 @@ fn display_inode(metadata: &Metadata) -> String { /// This returns the `SELinux` security context as UTF8 `String`. /// In the long term this should be changed to [`OsStr`], see discussions at #2621/#2656 -fn get_security_context(config: &Config, p_buf: &Path, must_dereference: bool) -> String { - let substitute_string = "?".to_string(); +fn get_security_context<'a>( + path: &'a Path, + must_dereference: bool, + config: &'a Config, +) -> Cow<'a, str> { + static SUBSTITUTE_STRING: &str = "?"; + // If we must dereference, ensure that the symlink is actually valid even if the system // does not support SELinux. // Conforms to the GNU coreutils where a dangling symlink results in exit code 1. if must_dereference { - match get_metadata_with_deref_opt(p_buf, must_dereference) { - Err(err) => { - // The Path couldn't be dereferenced, so return early and set exit code 1 - // to indicate a minor error - // Only show error when context display is requested to avoid duplicate messages - if config.context { - show!(LsError::IOErrorContext(p_buf.to_path_buf(), err, false)); - } - return substitute_string; + if let Err(err) = get_metadata_with_deref_opt(path, must_dereference) { + // The Path couldn't be dereferenced, so return early and set exit code 1 + // to indicate a minor error + // Only show error when context display is requested to avoid duplicate messages + if config.context { + show!(LsError::IOErrorContext(path.to_path_buf(), err, false)); } - Ok(_md) => (), + return Cow::Borrowed(SUBSTITUTE_STRING); } } + if config.selinux_supported { #[cfg(feature = "selinux")] { - match selinux::SecurityContext::of_path(p_buf, must_dereference.to_owned(), false) { + match selinux::SecurityContext::of_path(path, must_dereference, false) { Err(_r) => { // TODO: show the actual reason why it failed - show_warning!("failed to get security context of: {}", p_buf.quote()); - substitute_string + show_warning!("failed to get security context of: {}", path.quote()); + return Cow::Borrowed(SUBSTITUTE_STRING); } - Ok(None) => substitute_string, + Ok(None) => return Cow::Borrowed(SUBSTITUTE_STRING), Ok(Some(context)) => { let context = context.as_bytes(); let context = context.strip_suffix(&[0]).unwrap_or(context); - String::from_utf8(context.to_vec()).unwrap_or_else(|e| { + + let res: String = String::from_utf8(context.to_vec()).unwrap_or_else(|e| { show_warning!( "getting security context of: {}: {}", - p_buf.quote(), + path.quote(), e.to_string() ); - String::from_utf8_lossy(context).into_owned() - }) + + String::from_utf8_lossy(context).to_string() + }); + + return Cow::Owned(res); } } } - #[cfg(not(feature = "selinux"))] - { - substitute_string - } - } else { - substitute_string } + + Cow::Borrowed(SUBSTITUTE_STRING) } #[cfg(unix)] @@ -3327,7 +3363,7 @@ fn calculate_padding_collection( for item in items { #[cfg(unix)] if config.inode { - let inode_len = if let Some(md) = item.get_metadata(&mut state.out) { + let inode_len = if let Some(md) = item.metadata() { display_inode(md).len() } else { continue; @@ -3336,14 +3372,14 @@ fn calculate_padding_collection( } if config.alloc_size { - if let Some(md) = item.get_metadata(&mut state.out) { + if let Some(md) = item.metadata() { let block_size_len = display_size(get_block_size(md, config), config).len(); padding_collections.block_size = block_size_len.max(padding_collections.block_size); } } if config.format == Format::Long { - let context_len = item.security_context.len(); + let context_len = item.security_context(config).len(); let (link_count_len, uname_len, group_len, size_len, major_len, minor_len) = display_dir_entry_size(item, config, state); padding_collections.link_count = link_count_len.max(padding_collections.link_count); @@ -3386,13 +3422,13 @@ fn calculate_padding_collection( for item in items { if config.alloc_size { - if let Some(md) = item.get_metadata(&mut state.out) { + if let Some(md) = item.metadata() { let block_size_len = display_size(get_block_size(md, config), config).len(); padding_collections.block_size = block_size_len.max(padding_collections.block_size); } } - let context_len = item.security_context.len(); + let context_len = item.security_context(config).len(); let (link_count_len, uname_len, group_len, size_len, _major_len, _minor_len) = display_dir_entry_size(item, config, state); padding_collections.link_count = link_count_len.max(padding_collections.link_count);