From 2ac91d569ebcca4a9c59fdbc77f15d5645613efe Mon Sep 17 00:00:00 2001 From: electricboogie <32370782+kimono-koans@users.noreply.github.com> Date: Thu, 19 Mar 2026 17:47:59 -0500 Subject: [PATCH 01/14] Remove potentially redundant statx calls --- src/uu/ls/src/ls.rs | 30 +++++++----------------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 507689eee41..5cfe3b0c889 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -3481,29 +3481,13 @@ fn display_item_name( false, ); - // Check if the target actually needs coloring - let md_option: Option = target_data - .metadata() - .cloned() - .or_else(|| target_data.p_buf.symlink_metadata().ok()); - let style = style_manager.colors.style_for_path_with_metadata( - &target_data.p_buf, - md_option.as_ref(), - ); - - if style.is_some() { - // Only apply coloring if there's actually a style - name.push(color_name( - escaped_target, - &target_data, - style_manager, - None, - is_wrap(name.len()), - )); - } else { - // For regular files with no coloring, just use plain text - name.push(escaped_target); - } + name.push(color_name( + escaped_target, + &target_data, + style_manager, + None, + is_wrap(name.len()), + )) } Err(_) => { name.push( From 882e346528b4300ac88957d0dc7513ab5408e1b8 Mon Sep 17 00:00:00 2001 From: electricboogie <32370782+kimono-koans@users.noreply.github.com> Date: Thu, 19 Mar 2026 18:17:30 -0500 Subject: [PATCH 02/14] Remove further redundancy --- src/uu/ls/src/ls.rs | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 5cfe3b0c889..757b8274838 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -1985,21 +1985,14 @@ impl PathData { 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() { - 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() { - ft.get_or_init(|| Some(ft_de)); - } - - RefCell::new(Some(de.into())) - } else { + let de: RefCell>> = if must_dereference { RefCell::new(None) + } else { + if let Some(de) = dir_entry { + RefCell::new(Some(de.into())) + } else { + RefCell::new(None) + } }; Self { @@ -3481,13 +3474,17 @@ fn display_item_name( false, ); - name.push(color_name( - escaped_target, - &target_data, - style_manager, - None, - is_wrap(name.len()), - )) + if config.color.is_some() { + name.push(color_name( + escaped_target, + &target_data, + style_manager, + None, + is_wrap(name.len()), + )) + } else { + name.push(escaped_target); + } } Err(_) => { name.push( From a29e1d78b6489696b78681dec05fe8d35be9ad36 Mon Sep 17 00:00:00 2001 From: electricboogie <32370782+kimono-koans@users.noreply.github.com> Date: Thu, 19 Mar 2026 18:21:23 -0500 Subject: [PATCH 03/14] Cleanup lints --- src/uu/ls/src/ls.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 757b8274838..7222d056e32 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -3481,7 +3481,7 @@ fn display_item_name( style_manager, None, is_wrap(name.len()), - )) + )); } else { name.push(escaped_target); } From d13148c9592bf741686d7b5f6e26c9d3bbe60a5a Mon Sep 17 00:00:00 2001 From: electricboogie <32370782+kimono-koans@users.noreply.github.com> Date: Thu, 19 Mar 2026 21:11:41 -0500 Subject: [PATCH 04/14] No need to hold onto dir entries --- src/uu/ls/src/ls.rs | 48 ++++++--------------------------------------- 1 file changed, 6 insertions(+), 42 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 7222d056e32..a2f8e7e354d 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -10,7 +10,6 @@ use rustc_hash::FxHashMap; use rustc_hash::FxHashSet; use std::borrow::Cow; -use std::cell::RefCell; #[cfg(unix)] use std::os::unix::fs::{FileTypeExt, MetadataExt}; #[cfg(windows)] @@ -1929,9 +1928,6 @@ struct PathData { // Result got from symlink_metadata() or metadata() based on config md: OnceCell>, ft: OnceCell>, - // 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: RefCell>>, security_context: OnceCell>, // Name of the file - will be empty for . or .. display_name: OsString, @@ -1944,7 +1940,6 @@ struct PathData { impl PathData { fn new( p_buf: PathBuf, - dir_entry: Option, file_name: Option, config: &Config, command_line: bool, @@ -1953,13 +1948,8 @@ impl PathData { // For '..', the filename is None let display_name = if let Some(name) = file_name { name - } else if command_line { - p_buf.as_os_str().to_os_string() } else { - dir_entry - .as_ref() - .map(DirEntry::file_name) - .unwrap_or_default() + p_buf.as_os_str().to_os_string() }; let must_dereference = match &config.dereference { @@ -1985,20 +1975,9 @@ impl PathData { let md: OnceCell> = OnceCell::new(); let security_context: OnceCell> = OnceCell::new(); - let de: RefCell>> = if must_dereference { - RefCell::new(None) - } else { - if let Some(de) = dir_entry { - RefCell::new(Some(de.into())) - } else { - RefCell::new(None) - } - }; - Self { md, ft, - de, security_context, display_name, p_buf, @@ -2010,12 +1989,6 @@ impl PathData { fn metadata(&self) -> Option<&Metadata> { self.md .get_or_init(|| { - if !self.must_dereference { - if let Some(dir_entry) = RefCell::take(&self.de).as_deref() { - return dir_entry.metadata().ok(); - } - } - match get_metadata_with_deref_opt(self.path(), self.must_dereference) { Err(err) => { // FIXME: A bit tricky to propagate the result here @@ -2228,7 +2201,7 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> { }; for loc in locs { - let path_data = PathData::new(PathBuf::from(loc), None, None, config, true); + let path_data = PathData::new(PathBuf::from(loc), None, config, true); // Getting metadata here is no big deal as it's just the CWD // and we really just want to know if the strings exist as files/dirs @@ -2447,13 +2420,13 @@ fn should_display(entry: &DirEntry, config: &Config) -> bool { fn depth_first_list( (dir_path, needs_blank_line): DirData, - mut read_dir: ReadDir, + read_dir: ReadDir, config: &Config, state: &mut ListState, dired: &mut DiredOutput, is_top_level: bool, ) -> UResult<()> { - let path_data = PathData::new(dir_path, None, None, config, false); + let path_data = PathData::new(dir_path, None, config, false); // Print dir heading - name... 'total' comes after error display if state.initial_locs_len > 1 || config.recursive { @@ -2496,14 +2469,12 @@ fn depth_first_list( let v = vec![ PathData::new( path_data.path().to_path_buf(), - None, Some(".".into()), config, false, ), PathData::new( path_data.path().join(".."), - None, Some("..".into()), config, false, @@ -2515,17 +2486,11 @@ fn depth_first_list( }; // Convert those entries to the PathData struct - for raw_entry in read_dir.by_ref() { + for raw_entry in read_dir { match raw_entry { Ok(dir_entry) => { if should_display(&dir_entry, config) { - buf.push(PathData::new( - dir_entry.path(), - Some(dir_entry), - None, - config, - false, - )); + buf.push(PathData::new(dir_entry.path(), None, config, false)); } } Err(err) => { @@ -3468,7 +3433,6 @@ fn display_item_name( Ok(resolved_target) => { let target_data = PathData::new( resolved_target, - None, target_path.file_name().map(OsStr::to_os_string), config, false, From 6c7890ddd27bbc5f2d841885d6490d1cd5ae2649 Mon Sep 17 00:00:00 2001 From: electricboogie <32370782+kimono-koans@users.noreply.github.com> Date: Thu, 19 Mar 2026 21:13:54 -0500 Subject: [PATCH 05/14] Cleanup --- src/uu/ls/src/ls.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index a2f8e7e354d..0bbcfafc138 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -1948,8 +1948,10 @@ impl PathData { // For '..', the filename is None let display_name = if let Some(name) = file_name { name - } else { + } else if command_line { p_buf.as_os_str().to_os_string() + } else { + p_buf.file_name().unwrap_or_default().to_os_string() }; let must_dereference = match &config.dereference { From 0d0d2eda88427a8b1fdd58180cd8f65f9461960e Mon Sep 17 00:00:00 2001 From: electricboogie <32370782+kimono-koans@users.noreply.github.com> Date: Thu, 19 Mar 2026 21:28:17 -0500 Subject: [PATCH 06/14] Remove all dir entry requests --- src/uu/ls/src/ls.rs | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 0bbcfafc138..fcb9b0933f8 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -19,7 +19,7 @@ use std::{ cmp::Reverse, ffi::{OsStr, OsString}, fmt::Write as _, - fs::{self, DirEntry, FileType, Metadata, ReadDir}, + fs::{self, FileType, Metadata, ReadDir}, io::{BufWriter, ErrorKind, IsTerminal, Stdout, Write, stdout}, iter, num::IntErrorKind, @@ -1971,8 +1971,6 @@ impl PathData { Dereference::None => false, }; - // Why prefer to check the DirEntry file_type()? B/c the call is - // nearly free compared to a metadata() call on a Path let ft: OnceCell> = OnceCell::new(); let md: OnceCell> = OnceCell::new(); let security_context: OnceCell> = OnceCell::new(); @@ -2375,22 +2373,22 @@ fn sort_entries(entries: &mut [PathData], config: &Config) { } } -fn is_hidden(file_path: &DirEntry) -> bool { +fn is_hidden(path_data: &PathData) -> bool { #[cfg(windows)] { - let metadata = file_path.metadata().unwrap(); + let metadata = path_data.metadata().unwrap(); let attr = metadata.file_attributes(); (attr & 0x2) > 0 } #[cfg(not(windows))] { - file_path.file_name().as_encoded_bytes().starts_with(b".") + path_data.file_name().as_encoded_bytes().starts_with(b".") } } -fn should_display(entry: &DirEntry, config: &Config) -> bool { +fn should_display(path_data: &PathData, config: &Config) -> bool { // check if hidden - if config.files == Files::Normal && is_hidden(entry) { + if config.files == Files::Normal && is_hidden(path_data) { return false; } @@ -2402,22 +2400,19 @@ fn should_display(entry: &DirEntry, config: &Config) -> bool { case_sensitive: true, }; - let file_name = entry.file_name(); + let file_name = path_data.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) => Cow::Borrowed(s), - None => file_name.to_string_lossy(), - }; + let file_name_as_cow = file_name.to_string_lossy(); !config .ignore_patterns .iter() - .any(|p| p.matches_with(&file_name, options)) + .any(|p| p.matches_with(&file_name_as_cow, options)) } fn depth_first_list( @@ -2491,8 +2486,9 @@ fn depth_first_list( for raw_entry in read_dir { match raw_entry { Ok(dir_entry) => { - if should_display(&dir_entry, config) { - buf.push(PathData::new(dir_entry.path(), None, config, false)); + let path_data = PathData::new(dir_entry.path(), None, config, false); + if should_display(&path_data, config) { + buf.push(path_data); } } Err(err) => { From da8b68d5f316582929bb46237c503048d5d220a1 Mon Sep 17 00:00:00 2001 From: electricboogie <32370782+kimono-koans@users.noreply.github.com> Date: Thu, 19 Mar 2026 21:34:53 -0500 Subject: [PATCH 07/14] Shouldn't be borrowing a readdir by ref --- src/uu/ls/src/ls.rs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index fcb9b0933f8..c8a40147d65 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -2245,7 +2245,7 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> { let needs_blank_line = pos != 0 || !files.is_empty(); // 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.path()) { + let mut 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()?; @@ -2267,7 +2267,7 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> { // List each of the arguments to ls first. depth_first_list( (path_data.path().to_path_buf(), needs_blank_line), - read_dir, + &mut read_dir, config, &mut state, &mut dired, @@ -2276,7 +2276,7 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> { // Only runs if it must list recursively. while let Some(dir_data) = state.stack.pop() { - let read_dir = match fs::read_dir(&dir_data.0) { + let mut read_dir = match fs::read_dir(&dir_data.0) { Err(err) => { // flush stdout buffer before the error to preserve formatting and order state.out.flush()?; @@ -2290,7 +2290,14 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> { Ok(rd) => rd, }; - depth_first_list(dir_data, read_dir, config, &mut state, &mut dired, false)?; + depth_first_list( + dir_data, + &mut read_dir, + config, + &mut state, + &mut dired, + false, + )?; // Heuristic to ensure stack does not keep its capacity forever if there is // combinatorial explosion; we decrease it logarithmically here. @@ -2417,7 +2424,7 @@ fn should_display(path_data: &PathData, config: &Config) -> bool { fn depth_first_list( (dir_path, needs_blank_line): DirData, - read_dir: ReadDir, + read_dir: &mut ReadDir, config: &Config, state: &mut ListState, dired: &mut DiredOutput, @@ -2483,7 +2490,7 @@ fn depth_first_list( }; // Convert those entries to the PathData struct - for raw_entry in read_dir { + for raw_entry in read_dir.into_iter() { match raw_entry { Ok(dir_entry) => { let path_data = PathData::new(dir_entry.path(), None, config, false); From b955aa99693dd98d6b6fe68b3c603e89ba197f3b Mon Sep 17 00:00:00 2001 From: electricboogie <32370782+kimono-koans@users.noreply.github.com> Date: Thu, 19 Mar 2026 21:58:31 -0500 Subject: [PATCH 08/14] Use dentries only for file types --- src/uu/ls/src/ls.rs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index c8a40147d65..31f68401fb4 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -9,11 +9,11 @@ #[cfg(unix)] use rustc_hash::FxHashMap; use rustc_hash::FxHashSet; -use std::borrow::Cow; #[cfg(unix)] use std::os::unix::fs::{FileTypeExt, MetadataExt}; #[cfg(windows)] use std::os::windows::fs::MetadataExt; +use std::{borrow::Cow, fs::DirEntry}; use std::{ cell::{LazyCell, OnceCell}, cmp::Reverse, @@ -1940,13 +1940,14 @@ struct PathData { impl PathData { fn new( p_buf: PathBuf, - file_name: Option, + opt_dir_entry: Option, + opt_file_name: Option, config: &Config, command_line: bool, ) -> Self { // We cannot use `Path::ends_with` or `Path::Components`, because they remove occurrences of '.' // For '..', the filename is None - let display_name = if let Some(name) = file_name { + let display_name = if let Some(name) = opt_file_name { name } else if command_line { p_buf.as_os_str().to_os_string() @@ -1975,6 +1976,10 @@ impl PathData { let md: OnceCell> = OnceCell::new(); let security_context: OnceCell> = OnceCell::new(); + if !must_dereference { + ft.get_or_init(|| opt_dir_entry.and_then(|de| de.file_type().ok())); + } + Self { md, ft, @@ -2201,7 +2206,7 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> { }; for loc in locs { - let path_data = PathData::new(PathBuf::from(loc), None, config, true); + let path_data = PathData::new(PathBuf::from(loc), None, None, config, true); // Getting metadata here is no big deal as it's just the CWD // and we really just want to know if the strings exist as files/dirs @@ -2430,7 +2435,7 @@ fn depth_first_list( dired: &mut DiredOutput, is_top_level: bool, ) -> UResult<()> { - let path_data = PathData::new(dir_path, None, config, false); + let path_data = PathData::new(dir_path, None, None, config, false); // Print dir heading - name... 'total' comes after error display if state.initial_locs_len > 1 || config.recursive { @@ -2473,12 +2478,14 @@ fn depth_first_list( let v = vec![ PathData::new( path_data.path().to_path_buf(), + None, Some(".".into()), config, false, ), PathData::new( path_data.path().join(".."), + None, Some("..".into()), config, false, @@ -2493,7 +2500,8 @@ fn depth_first_list( for raw_entry in read_dir.into_iter() { match raw_entry { Ok(dir_entry) => { - let path_data = PathData::new(dir_entry.path(), None, config, false); + let path_data = + PathData::new(dir_entry.path(), Some(dir_entry), None, config, false); if should_display(&path_data, config) { buf.push(path_data); } @@ -3438,6 +3446,7 @@ fn display_item_name( Ok(resolved_target) => { let target_data = PathData::new( resolved_target, + None, target_path.file_name().map(OsStr::to_os_string), config, false, From 21e5354d7cf94297e080cbef4858cdf551cd682f Mon Sep 17 00:00:00 2001 From: electricboogie <32370782+kimono-koans@users.noreply.github.com> Date: Thu, 19 Mar 2026 22:02:08 -0500 Subject: [PATCH 09/14] Cleanup --- src/uu/ls/src/ls.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 31f68401fb4..e4631cda52d 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -2497,7 +2497,7 @@ fn depth_first_list( }; // Convert those entries to the PathData struct - for raw_entry in read_dir.into_iter() { + for raw_entry in read_dir { match raw_entry { Ok(dir_entry) => { let path_data = From e9c02636f5fd7a9160e53266092684122a4c79db Mon Sep 17 00:00:00 2001 From: electricboogie <32370782+kimono-koans@users.noreply.github.com> Date: Thu, 19 Mar 2026 22:11:28 -0500 Subject: [PATCH 10/14] Cleanup --- src/uu/ls/src/ls.rs | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index e4631cda52d..afd705446c8 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -2497,7 +2497,7 @@ fn depth_first_list( }; // Convert those entries to the PathData struct - for raw_entry in read_dir { + for raw_entry in read_dir.into_iter() { match raw_entry { Ok(dir_entry) => { let path_data = @@ -3443,7 +3443,7 @@ fn display_item_name( } match fs::canonicalize(&absolute_target) { - Ok(resolved_target) => { + Ok(resolved_target) if config.color.is_some() => { let target_data = PathData::new( resolved_target, None, @@ -3452,19 +3452,15 @@ fn display_item_name( false, ); - if config.color.is_some() { - name.push(color_name( - escaped_target, - &target_data, - style_manager, - None, - is_wrap(name.len()), - )); - } else { - name.push(escaped_target); - } + name.push(color_name( + escaped_target, + &target_data, + style_manager, + None, + is_wrap(name.len()), + )); } - Err(_) => { + _ => { name.push( style_manager.apply_missing_target_style( escaped_target, From 0e3e29dbd9ceb7b5a267e3f88c9874cac3639b23 Mon Sep 17 00:00:00 2001 From: electricboogie <32370782+kimono-koans@users.noreply.github.com> Date: Thu, 19 Mar 2026 22:15:19 -0500 Subject: [PATCH 11/14] Cleanup --- src/uu/ls/src/ls.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index afd705446c8..884a5173bf3 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -1976,10 +1976,6 @@ impl PathData { let md: OnceCell> = OnceCell::new(); let security_context: OnceCell> = OnceCell::new(); - if !must_dereference { - ft.get_or_init(|| opt_dir_entry.and_then(|de| de.file_type().ok())); - } - Self { md, ft, From 77950b53cd8c6e71cf1763699dec8f7380d75fef Mon Sep 17 00:00:00 2001 From: electricboogie <32370782+kimono-koans@users.noreply.github.com> Date: Thu, 19 Mar 2026 22:18:18 -0500 Subject: [PATCH 12/14] Fix bug on initial dir entry --- src/uu/ls/src/ls.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 884a5173bf3..46fb12408d8 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -1976,6 +1976,10 @@ impl PathData { let md: OnceCell> = OnceCell::new(); let security_context: OnceCell> = OnceCell::new(); + if !must_dereference { + opt_dir_entry.map(|de| ft.get_or_init(|| de.file_type().ok())); + } + Self { md, ft, From 64eb894c657c0336fe94c6943efcaa7ec0b84022 Mon Sep 17 00:00:00 2001 From: electricboogie <32370782+kimono-koans@users.noreply.github.com> Date: Thu, 19 Mar 2026 22:22:00 -0500 Subject: [PATCH 13/14] Fix lints --- src/uu/ls/src/ls.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 46fb12408d8..748c62de8e8 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -2497,7 +2497,7 @@ fn depth_first_list( }; // Convert those entries to the PathData struct - for raw_entry in read_dir.into_iter() { + for raw_entry in read_dir { match raw_entry { Ok(dir_entry) => { let path_data = From b5260691e67c3147459e970b1e563aaf2ff63e3e Mon Sep 17 00:00:00 2001 From: electricboogie <32370782+kimono-koans@users.noreply.github.com> Date: Sat, 21 Mar 2026 12:31:30 -0500 Subject: [PATCH 14/14] Revert "Merge branch 'main' into Redundant-statx-syscalls" This reverts commit 20da1fc507b7332d2dc486354dd72eac390e986c, reversing changes made to 1cd24679ada07c50d04d2fbb05bbd7278c17f142. --- .github/workflows/l10n.yml | 24 +- .github/workflows/make.yml | 34 - Cargo.lock | 12 - Cargo.toml | 2 - GNUmakefile | 41 +- src/uu/b2sum/src/b2sum.rs | 10 +- src/uu/cksum/benches/cksum_bench.rs | 2 +- src/uu/cksum/src/cksum.rs | 8 +- src/uu/ls/src/config.rs | 1090 ----------- src/uu/ls/src/display.rs | 1319 ------------- src/uu/ls/src/ls.rs | 1697 +++++++++++++++-- src/uucore/src/lib/features/checksum/mod.rs | 56 +- .../src/lib/features/checksum/validate.rs | 39 +- src/uucore/src/lib/features/sum.rs | 37 +- tests/by-util/test_cksum.rs | 110 -- util/run-gnu-tests-smack-ci.sh | 13 +- 16 files changed, 1628 insertions(+), 2866 deletions(-) delete mode 100644 src/uu/ls/src/config.rs delete mode 100644 src/uu/ls/src/display.rs diff --git a/.github/workflows/l10n.yml b/.github/workflows/l10n.yml index 30b2cd5b17e..57c4d51ef21 100644 --- a/.github/workflows/l10n.yml +++ b/.github/workflows/l10n.yml @@ -62,6 +62,11 @@ jobs: brew install coreutils ;; esac + - name: Build with platform features + shell: bash + run: | + ## Build with platform-specific features to enable l10n functionality + cargo build --features ${{ matrix.job.features }} - name: Test l10n functionality shell: bash run: | @@ -144,6 +149,10 @@ jobs: sudo apt-get -y update ; sudo apt-get -y install libselinux1-dev sudo locale-gen --keep-existing fr_FR.UTF-8 locale -a | grep -i fr || exit 1 + - name: Build coreutils with clap localization support + shell: bash + run: | + cargo build --features feat_os_unix --bin coreutils - name: Test English clap error localization shell: bash run: | @@ -312,6 +321,11 @@ jobs: ## Generate French locale for testing sudo locale-gen --keep-existing fr_FR.UTF-8 locale -a | grep -i fr || echo "French locale not found, continuing anyway" + - name: Build coreutils with l10n support + shell: bash + run: | + ## Build coreutils with Unix features and l10n support + cargo build --features feat_os_unix --bin coreutils - name: Test French localization shell: bash run: | @@ -692,7 +706,7 @@ jobs: mkdir -p "$CARGO_INSTALL_DIR" # Install using cargo with l10n features - cargo install --path . --features "ls,cat,touch" --root "$CARGO_INSTALL_DIR" --locked + cargo install --path . --features ${{ matrix.job.features }} --root "$CARGO_INSTALL_DIR" --locked # Verify installation echo "Testing cargo-installed binaries..." @@ -1365,3 +1379,11 @@ jobs: echo "::warning::More locales than expected ($total_match_count entries)" echo "This might be expected for utility + uucore locales" fi + + l10n_locale_embedding_regression_test: + name: L10n/Locale Embedding Regression Test + runs-on: ubuntu-latest + needs: [l10n_locale_embedding_cat, l10n_locale_embedding_ls, l10n_locale_embedding_multicall, l10n_locale_embedding_cargo_install] + steps: + - name: All locale embedding tests passed + run: echo "✓ All locale embedding tests passed successfully" diff --git a/.github/workflows/make.yml b/.github/workflows/make.yml index fb1ed4bd9c6..7fc96461216 100644 --- a/.github/workflows/make.yml +++ b/.github/workflows/make.yml @@ -266,40 +266,6 @@ jobs: # 2. the makefile doesn't try to install libstdbuf even though stdbuf is skipped DESTDIR=/tmp/ make SKIP_UTILS="stdbuf" install - # keep this job minimal to avoid have many duplicated build with CICD - build_makefile-other: - name: Build/Makefile - runs-on: ${{ matrix.job.os }} - env: - CARGO_INCREMENTAL: 0 - strategy: - fail-fast: false - matrix: - job: - - { os: windows-latest , features: feat_os_windows } - steps: - - uses: actions/checkout@v6 - with: - persist-credentials: false - - uses: Swatinem/rust-cache@v2 - - name: Run sccache-cache - id: sccache-setup - uses: mozilla-actions/sccache-action@v0.0.9 - continue-on-error: true - - name: Export sccache - if: steps.sccache-setup.outcome == 'success' - run: | - echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENV - echo "SCCACHE_GHA_ENABLED=true" >> $GITHUB_ENV - - name: "`make build`" - shell: bash - run: | - set -x - # Check that we exclude unix programs to avoid build failure - make PREFIX=/tmp/usr MULTICALL=y COMPLETIONS=n MANPAGES=n LOCALES=n \ - SKIP_UTILS="arch b2sum base32 base64 basename basenc cat cksum comm cp csplit cut date dd df dir dircolors dirname du echo env expand expr factor false fmt fold head hostname join link ln ls md5sum mkdir mktemp more mv nl nproc numfmt od paste pr printenv printf ptx pwd readlink realpath rm rmdir seq sha1sum sha224sum sha256sum sha384sum sha512sum shred shuf sleep sort split sum sync tac tail tee test touch tr truncate tsort uname unexpand uniq unlink vdir wc whoami yes" - target/debug/coreutils.exe true - test_busybox: name: Tests/BusyBox test suite runs-on: ${{ matrix.job.os }} diff --git a/Cargo.lock b/Cargo.lock index e3919220ddb..6237a36609f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -559,7 +559,6 @@ dependencies = [ "regex", "rlimit", "rstest", - "rstest_reuse", "rustc-hash", "selinux", "sha1", @@ -2559,17 +2558,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "rstest_reuse" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3a8fb4672e840a587a66fc577a5491375df51ddb88f2a2c2a792598c326fe14" -dependencies = [ - "quote", - "rand 0.8.5", - "syn", -] - [[package]] name = "rust-ini" version = "0.21.3" diff --git a/Cargo.toml b/Cargo.toml index 9ef80cd072c..92013b0af57 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -432,7 +432,6 @@ rayon = "1.10" regex = "1.10.4" rlimit = "0.11.0" rstest = "0.26.0" -rstest_reuse = "0.7.0" rustc-hash = "2.1.1" rust-ini = "0.21.0" same-file = "1.0.6" @@ -641,7 +640,6 @@ uucore = { workspace = true, features = [ walkdir.workspace = true hex-literal = "1.0.0" rstest.workspace = true -rstest_reuse.workspace = true [target.'cfg(unix)'.dev-dependencies] nix = { workspace = true, features = [ diff --git a/GNUmakefile b/GNUmakefile index a99fefa46ec..ae266b6c277 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -62,31 +62,44 @@ TOYBOX_ROOT := $(BASEDIR)/tmp TOYBOX_VER := 0.8.12 TOYBOX_SRC := $(TOYBOX_ROOT)/toybox-$(TOYBOX_VER) -# Detect the target system -# See https://doc.rust-lang.org/beta/rustc/platform-support.html -# todo: support building wasm -OS := $(or $(CARGO_BUILD_TARGET),$(shell rustc --print host-tuple)) +#------------------------------------------------------------------------ +# Detect the host system. +# On Windows uname -s might return MINGW_NT-* or CYGWIN_NT-*. +# Otherwise let it default to the kernel name returned by uname -s +# (Linux, Darwin, FreeBSD, …). +#------------------------------------------------------------------------ +OS ?= $(shell uname -s) # Windows does not allow symlink by default. # Allow to override LN for AppArmor. -ifneq (,$(findstring windows,$(OS))) +ifneq (,$(findstring _NT,$(OS))) LN ?= ln -f endif LN ?= ln -sf +# Possible programs +PROGS := \ + $(shell sed -n '/feat_Tier1 = \[/,/\]/p' Cargo.toml | sed '1d;2d' |tr -d '],"\n')\ + $(shell sed -n '/feat_common_core = \[/,/\]/p' Cargo.toml | sed '1d' |tr -d '],"\n') + +UNIX_PROGS := \ + $(shell sed -n '/feat_require_unix_core = \[/,/\]/p' Cargo.toml | sed '1d' |tr -d '],"\n') \ + hostid \ + pinky \ + stdbuf \ + uptime \ + users \ + who + SELINUX_PROGS := \ chcon \ runcon $(info Detected OS = $(OS)) -ifeq (,$(findstring windows,$(OS))) - FEATURE_EXTRACT_UTILS := feat_os_unix -else - FEATURE_EXTRACT_UTILS := feat_Tier1 +ifeq (,$(findstring MINGW,$(OS))) + PROGS += $(UNIX_PROGS) endif -PROGS := $(shell cargo tree --depth 1 --features $(FEATURE_EXTRACT_UTILS) --format "{p}" --prefix none | sed -E -n 's/^uu_([^ ]+).*/\1/p') - ifeq ($(SELINUX_ENABLED),1) PROGS += $(SELINUX_PROGS) endif @@ -101,7 +114,7 @@ endif # Programs with usable tests TESTS := \ - $(sort $(filter $(UTILS),$(PROGS) $(SELINUX_PROGS))) + $(sort $(filter $(UTILS),$(PROGS) $(UNIX_PROGS) $(SELINUX_PROGS))) TEST_NO_FAIL_FAST := TEST_SPEC_FEATURE := @@ -275,7 +288,7 @@ install: build install-manpages install-completions install-locales mkdir -p $(INSTALLDIR_BIN) ifneq (,$(and $(findstring stdbuf,$(UTILS)),$(findstring feat_external_libstdbuf,$(CARGOFLAGS)))) mkdir -p $(DESTDIR)$(LIBSTDBUF_DIR) -ifneq (,$(findstring cygwin,$(OS))) +ifneq (,$(findstring CYGWIN,$(OS))) $(INSTALL) -m 755 $(BUILDDIR)/deps/stdbuf.dll $(DESTDIR)$(LIBSTDBUF_DIR)/libstdbuf.dll else $(INSTALL) -m 755 $(BUILDDIR)/deps/libstdbuf.* $(DESTDIR)$(LIBSTDBUF_DIR)/ @@ -295,7 +308,7 @@ else endif uninstall: -ifeq (,$(findstring windows,$(OS))) +ifeq (,$(findstring MINGW,$(OS))) rm -f $(DESTDIR)$(LIBSTDBUF_DIR)/libstdbuf.* -rm -d $(DESTDIR)$(LIBSTDBUF_DIR) 2>/dev/null || true endif diff --git a/src/uu/b2sum/src/b2sum.rs b/src/uu/b2sum/src/b2sum.rs index ddd5fe3e9e6..6df276f2393 100644 --- a/src/uu/b2sum/src/b2sum.rs +++ b/src/uu/b2sum/src/b2sum.rs @@ -9,14 +9,18 @@ use clap::Command; use uu_checksum_common::{standalone_checksum_app_with_length, standalone_with_length_main}; -use uucore::checksum::{AlgoKind, calculate_blake_length_str}; +use uucore::checksum::{AlgoKind, calculate_blake2b_length_str}; use uucore::error::UResult; use uucore::translate; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let calculate_blake2b_length = |s: &str| calculate_blake_length_str(AlgoKind::Blake2b, s); - standalone_with_length_main(AlgoKind::Blake2b, uu_app(), args, calculate_blake2b_length) + standalone_with_length_main( + AlgoKind::Blake2b, + uu_app(), + args, + calculate_blake2b_length_str, + ) } #[inline] diff --git a/src/uu/cksum/benches/cksum_bench.rs b/src/uu/cksum/benches/cksum_bench.rs index 81f70bbb3d1..5c2d3d9c614 100644 --- a/src/uu/cksum/benches/cksum_bench.rs +++ b/src/uu/cksum/benches/cksum_bench.rs @@ -104,7 +104,7 @@ bench_algorithm!(cksum_sha224, "sha224"); bench_algorithm!(cksum_sha256, "sha256"); bench_algorithm!(cksum_sha384, "sha384"); bench_algorithm!(cksum_sha512, "sha512"); -bench_algorithm!(cksum_blake3, "blake3"); +// broken. benchmarking error messages issues/10002 bench_algorithm!(cksum_blake3, "blake3"); bench_shake_algorithm!(cksum_shake128, "shake128", Shake128); bench_shake_algorithm!(cksum_shake256, "shake256", Shake256); diff --git a/src/uu/cksum/src/cksum.rs b/src/uu/cksum/src/cksum.rs index 19898def4f9..e484c8323ad 100644 --- a/src/uu/cksum/src/cksum.rs +++ b/src/uu/cksum/src/cksum.rs @@ -12,7 +12,7 @@ use uu_checksum_common::{ChecksumCommand, checksum_main, default_checksum_app, o use uucore::checksum::compute::OutputFormat; use uucore::checksum::{ - AlgoKind, ChecksumError, calculate_blake_length_str, sanitize_sha2_sha3_length_str, + AlgoKind, ChecksumError, calculate_blake2b_length_str, sanitize_sha2_sha3_length_str, }; use uucore::error::UResult; use uucore::hardware::{HasHardwareFeatures as _, SimdPolicy}; @@ -67,10 +67,8 @@ fn maybe_sanitize_length( Err(_) => Err(ChecksumError::InvalidLength(len.into()).into()), }, - // For BLAKE, if a length is provided, validate it. - (Some(algo @ (AlgoKind::Blake2b | AlgoKind::Blake3)), Some(len)) => { - calculate_blake_length_str(algo, len) - } + // For BLAKE2b, if a length is provided, validate it. + (Some(AlgoKind::Blake2b), Some(len)) => calculate_blake2b_length_str(len), // For any other provided algorithm, check if length is 0. // Otherwise, this is an error. diff --git a/src/uu/ls/src/config.rs b/src/uu/ls/src/config.rs deleted file mode 100644 index 12d14238f4d..00000000000 --- a/src/uu/ls/src/config.rs +++ /dev/null @@ -1,1090 +0,0 @@ -// This file is part of the uutils coreutils package. -// -// For the full copyright and license information, please view the LICENSE -// file that was distributed with this source code. - -// spell-checker:ignore (ToDO) somegroup nlink tabsize dired subdired dtype colorterm stringly -// spell-checker:ignore nohash strtime clocale - -use std::{ - ffi::{OsStr, OsString}, - io::{IsTerminal, stdout}, - num::IntErrorKind, -}; - -use glob::Pattern; -use lscolors::LsColors; -use term_grid::SPACES_IN_TAB; - -use uucore::{ - display::Quotable, error::UResult, format::human::SizeFormat, fsext::MetadataTimeField, - line_ending::LineEnding, parser::parse_glob, parser::parse_size::parse_size_non_zero_u64, - quoting_style::QuotingStyle, show_error, show_warning, time::format, translate, -}; - -use crate::{ - LsError, - colors::{LsColorsParseError, validate_ls_colors_env}, - dired::is_dired_arg_present, - display::{Format, IndicatorStyle, LocaleQuoting, LongFormat}, - options::QUOTING_STYLE, -}; - -pub mod options { - pub mod format { - pub static ONE_LINE: &str = "1"; - pub static LONG: &str = "long"; - pub static COLUMNS: &str = "C"; - pub static ACROSS: &str = "x"; - pub static TAB_SIZE: &str = "tabsize"; - pub static COMMAS: &str = "m"; - pub static LONG_NO_OWNER: &str = "g"; - pub static LONG_NO_GROUP: &str = "o"; - pub static LONG_NUMERIC_UID_GID: &str = "numeric-uid-gid"; - } - - pub mod files { - pub static ALL: &str = "all"; - pub static ALMOST_ALL: &str = "almost-all"; - pub static UNSORTED_ALL: &str = "f"; - } - - pub mod sort { - pub static SIZE: &str = "S"; - pub static TIME: &str = "t"; - pub static NONE: &str = "U"; - pub static VERSION: &str = "v"; - pub static EXTENSION: &str = "X"; - } - - pub mod time { - pub static ACCESS: &str = "u"; - pub static CHANGE: &str = "c"; - } - - pub mod size { - pub static ALLOCATION_SIZE: &str = "size"; - pub static BLOCK_SIZE: &str = "block-size"; - pub static HUMAN_READABLE: &str = "human-readable"; - pub static SI: &str = "si"; - pub static KIBIBYTES: &str = "kibibytes"; - } - - pub mod quoting { - pub static ESCAPE: &str = "escape"; - pub static LITERAL: &str = "literal"; - pub static C: &str = "quote-name"; - } - - pub mod indicator_style { - pub static SLASH: &str = "p"; - pub static FILE_TYPE: &str = "file-type"; - pub static CLASSIFY: &str = "classify"; - } - - pub mod dereference { - pub static ALL: &str = "dereference"; - pub static ARGS: &str = "dereference-command-line"; - pub static DIR_ARGS: &str = "dereference-command-line-symlink-to-dir"; - } - - pub static HELP: &str = "help"; - pub static QUOTING_STYLE: &str = "quoting-style"; - pub static HIDE_CONTROL_CHARS: &str = "hide-control-chars"; - pub static SHOW_CONTROL_CHARS: &str = "show-control-chars"; - pub static WIDTH: &str = "width"; - pub static AUTHOR: &str = "author"; - pub static NO_GROUP: &str = "no-group"; - pub static FORMAT: &str = "format"; - pub static SORT: &str = "sort"; - pub static TIME: &str = "time"; - pub static IGNORE_BACKUPS: &str = "ignore-backups"; - pub static DIRECTORY: &str = "directory"; - pub static INODE: &str = "inode"; - pub static REVERSE: &str = "reverse"; - pub static RECURSIVE: &str = "recursive"; - pub static COLOR: &str = "color"; - pub static PATHS: &str = "paths"; - pub static INDICATOR_STYLE: &str = "indicator-style"; - pub static TIME_STYLE: &str = "time-style"; - pub static FULL_TIME: &str = "full-time"; - pub static HIDE: &str = "hide"; - pub static IGNORE: &str = "ignore"; - pub static CONTEXT: &str = "context"; - pub static GROUP_DIRECTORIES_FIRST: &str = "group-directories-first"; - pub static ZERO: &str = "zero"; - pub static DIRED: &str = "dired"; - pub static HYPERLINK: &str = "hyperlink"; -} - -const DEFAULT_TERM_WIDTH: u16 = 80; -const POSIXLY_CORRECT_BLOCK_SIZE: u64 = 512; -const DEFAULT_BLOCK_SIZE: u64 = 1024; -const DEFAULT_FILE_SIZE_BLOCK_SIZE: u64 = 1; - -pub(crate) enum Dereference { - None, - DirArgs, - Args, - All, -} - -#[derive(PartialEq, Eq)] -pub(crate) enum Sort { - None, - Name, - Size, - Time, - Version, - Extension, - Width, -} - -#[derive(PartialEq, Eq)] -pub(crate) enum Files { - All, - AlmostAll, - Normal, -} - -pub struct Config { - // Dir and vdir needs access to this field - pub format: Format, - pub(crate) files: Files, - pub(crate) sort: Sort, - pub(crate) recursive: bool, - pub(crate) reverse: bool, - pub(crate) dereference: Dereference, - pub(crate) ignore_patterns: Vec, - pub(crate) size_format: SizeFormat, - pub(crate) directory: bool, - pub(crate) time: MetadataTimeField, - #[cfg(unix)] - pub(crate) inode: bool, - pub(crate) color: Option, - pub(crate) long: LongFormat, - pub(crate) alloc_size: bool, - pub(crate) file_size_block_size: u64, - #[allow(dead_code)] - pub(crate) block_size: u64, // is never read on Windows - pub(crate) width: u16, - // Dir and vdir needs access to this field - pub quoting_style: QuotingStyle, - pub(crate) locale_quoting: Option, - pub(crate) indicator_style: IndicatorStyle, - pub(crate) time_format_recent: String, // Time format for recent dates - pub(crate) time_format_older: Option, // Time format for older dates (optional, if not present, time_format_recent is used) - pub(crate) context: bool, - #[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))] - pub(crate) selinux_supported: bool, - #[cfg(all(feature = "smack", target_os = "linux"))] - pub(crate) smack_supported: bool, - pub(crate) group_directories_first: bool, - pub(crate) line_ending: LineEnding, - pub(crate) dired: bool, - pub(crate) hyperlink: bool, - pub(crate) tab_size: usize, -} - -/// Extracts the format to display the information based on the options provided. -/// -/// # Returns -/// -/// A tuple containing the Format variant and an Option containing a &'static str -/// which corresponds to the option used to define the format. -fn extract_format(options: &clap::ArgMatches) -> (Format, Option<&'static str>) { - if let Some(format_) = options.get_one::(options::FORMAT) { - ( - match format_.as_str() { - "long" | "verbose" => Format::Long, - "single-column" => Format::OneLine, - "columns" | "vertical" => Format::Columns, - "across" | "horizontal" => Format::Across, - "commas" => Format::Commas, - // below should never happen as clap already restricts the values. - _ => unreachable!("Invalid field for --format"), - }, - Some(options::FORMAT), - ) - } else if options.get_flag(options::format::LONG) { - (Format::Long, Some(options::format::LONG)) - } else if options.get_flag(options::format::ACROSS) { - (Format::Across, Some(options::format::ACROSS)) - } else if options.get_flag(options::format::COMMAS) { - (Format::Commas, Some(options::format::COMMAS)) - } else if options.get_flag(options::format::COLUMNS) { - (Format::Columns, Some(options::format::COLUMNS)) - } else if stdout().is_terminal() { - (Format::Columns, None) - } else { - (Format::OneLine, None) - } -} - -/// Extracts the type of files to display -/// -/// # Returns -/// -/// A Files variant representing the type of files to display. -fn extract_files(options: &clap::ArgMatches) -> Files { - let get_last_index = |flag: &str| -> usize { - if options.value_source(flag) == Some(clap::parser::ValueSource::CommandLine) { - options.index_of(flag).unwrap_or(0) - } else { - 0 - } - }; - - let all_index = get_last_index(options::files::ALL); - let almost_all_index = get_last_index(options::files::ALMOST_ALL); - let unsorted_all_index = get_last_index(options::files::UNSORTED_ALL); - - let max_index = all_index.max(almost_all_index).max(unsorted_all_index); - - if max_index == 0 { - Files::Normal - } else if max_index == almost_all_index { - Files::AlmostAll - } else { - // Either -a or -f wins, both show all files - Files::All - } -} - -/// Extracts the sorting method to use based on the options provided. -/// -/// # Returns -/// -/// A Sort variant representing the sorting method to use. -fn extract_sort(options: &clap::ArgMatches) -> Sort { - let get_last_index = |flag: &str| -> usize { - if options.value_source(flag) == Some(clap::parser::ValueSource::CommandLine) { - options.index_of(flag).unwrap_or(0) - } else { - 0 - } - }; - - let sort_index = options - .get_one::(options::SORT) - .and_then(|_| options.indices_of(options::SORT)) - .map_or(0, |mut indices| indices.next_back().unwrap_or(0)); - let time_index = get_last_index(options::sort::TIME); - let size_index = get_last_index(options::sort::SIZE); - let none_index = get_last_index(options::sort::NONE); - let version_index = get_last_index(options::sort::VERSION); - let extension_index = get_last_index(options::sort::EXTENSION); - let unsorted_all_index = get_last_index(options::files::UNSORTED_ALL); - - let max_sort_index = sort_index - .max(time_index) - .max(size_index) - .max(none_index) - .max(version_index) - .max(extension_index) - .max(unsorted_all_index); - - match max_sort_index { - 0 => { - // No sort flags specified, use default behavior - if !options.get_flag(options::format::LONG) - && (options.get_flag(options::time::ACCESS) - || options.get_flag(options::time::CHANGE) - || options.get_one::(options::TIME).is_some()) - { - Sort::Time - } else { - Sort::Name - } - } - idx if idx == unsorted_all_index || idx == none_index => Sort::None, - idx if idx == sort_index => { - if let Some(field) = options.get_one::(options::SORT) { - match field.as_str() { - "none" => Sort::None, - "name" => Sort::Name, - "time" => Sort::Time, - "size" => Sort::Size, - "version" => Sort::Version, - "extension" => Sort::Extension, - "width" => Sort::Width, - _ => unreachable!("Invalid field for --sort"), - } - } else { - Sort::Name - } - } - idx if idx == time_index => Sort::Time, - idx if idx == size_index => Sort::Size, - idx if idx == version_index => Sort::Version, - idx if idx == extension_index => Sort::Extension, - _ => Sort::Name, - } -} - -/// Extracts the time to use based on the options provided. -/// -/// # Returns -/// -/// A `MetadataTimeField` variant representing the time to use. -fn extract_time(options: &clap::ArgMatches) -> MetadataTimeField { - if let Some(field) = options.get_one::(options::TIME) { - field.as_str().into() - } else if options.get_flag(options::time::ACCESS) { - MetadataTimeField::Access - } else if options.get_flag(options::time::CHANGE) { - MetadataTimeField::Change - } else { - MetadataTimeField::Modification - } -} - -/// Some env variables can be passed -/// For now, we are only verifying if empty or not and known for `TERM` -fn is_color_compatible_term() -> bool { - let is_term_set = std::env::var("TERM").is_ok(); - let is_colorterm_set = std::env::var("COLORTERM").is_ok(); - - let term = std::env::var("TERM").unwrap_or_default(); - let colorterm = std::env::var("COLORTERM").unwrap_or_default(); - - // Search function in the TERM struct to manage the wildcards - let term_matches = |term: &str| -> bool { - uucore::colors::TERMS.iter().any(|&pattern| { - term == pattern - || (pattern.ends_with('*') && term.starts_with(&pattern[..pattern.len() - 1])) - }) - }; - - if is_term_set && term.is_empty() && is_colorterm_set && colorterm.is_empty() { - return false; - } - - if !term.is_empty() && !term_matches(&term) { - return false; - } - true -} - -/// Extracts the color option to use based on the options provided. -/// -/// # Returns -/// -/// A boolean representing whether or not to use color. -fn extract_color(options: &clap::ArgMatches) -> bool { - if !is_color_compatible_term() { - return false; - } - - let get_last_index = |flag: &str| -> usize { - if options.value_source(flag) == Some(clap::parser::ValueSource::CommandLine) { - options.index_of(flag).unwrap_or(0) - } else { - 0 - } - }; - - let color_index = options - .get_one::(options::COLOR) - .and_then(|_| options.indices_of(options::COLOR)) - .map_or(0, |mut indices| indices.next_back().unwrap_or(0)); - let unsorted_all_index = get_last_index(options::files::UNSORTED_ALL); - - let color_enabled = match options.get_one::(options::COLOR) { - None => options.contains_id(options::COLOR), - Some(val) => match val.as_str() { - "" | "always" | "yes" | "force" => true, - "auto" | "tty" | "if-tty" => stdout().is_terminal(), - /* "never" | "no" | "none" | */ _ => false, - }, - }; - - // If --color was explicitly specified, always honor it regardless of -f - // Otherwise, if -f is present without explicit color, disable color - if color_index > 0 { - // Color was explicitly specified - color_enabled - } else if unsorted_all_index > 0 { - // -f present without explicit color, disable implicit color - false - } else { - color_enabled - } -} - -/// Extracts the hyperlink option to use based on the options provided. -/// -/// # Returns -/// -/// A boolean representing whether to hyperlink files. -fn extract_hyperlink(options: &clap::ArgMatches) -> bool { - let hyperlink = options - .get_one::(options::HYPERLINK) - .unwrap() - .as_str(); - - match hyperlink { - "always" | "yes" | "force" => true, - "auto" | "tty" | "if-tty" => stdout().is_terminal(), - "never" | "no" | "none" => false, - _ => unreachable!("should be handled by clap"), - } -} - -/// Match the argument given to --quoting-style or the [`QUOTING_STYLE`] env variable. -/// -/// # Arguments -/// -/// * `style`: the actual argument string -/// * `show_control` - A boolean value representing whether to show control characters. -/// -/// # Returns -/// -/// * An option with None if the style string is invalid, or a `QuotingStyle` wrapped in `Some`. -struct QuotingStyleSpec { - style: QuotingStyle, - fixed_control: bool, - locale: Option, -} - -impl QuotingStyleSpec { - fn new(style: QuotingStyle) -> Self { - Self { - style, - fixed_control: false, - locale: None, - } - } - - fn with_locale(style: QuotingStyle, locale: LocaleQuoting) -> Self { - Self { - style, - fixed_control: true, - locale: Some(locale), - } - } -} -fn match_quoting_style_name( - style: &str, - show_control: bool, -) -> Option<(QuotingStyle, Option)> { - let spec = match style { - "literal" => QuotingStyleSpec::new(QuotingStyle::Literal { - show_control: false, - }), - "shell" => QuotingStyleSpec::new(QuotingStyle::SHELL), - "shell-always" => QuotingStyleSpec::new(QuotingStyle::SHELL_QUOTE), - "shell-escape" => QuotingStyleSpec::new(QuotingStyle::SHELL_ESCAPE), - "shell-escape-always" => QuotingStyleSpec::new(QuotingStyle::SHELL_ESCAPE_QUOTE), - "c" => QuotingStyleSpec::new(QuotingStyle::C_DOUBLE), - "escape" => QuotingStyleSpec::new(QuotingStyle::C_NO_QUOTES), - "locale" => QuotingStyleSpec { - style: QuotingStyle::Literal { - show_control: false, - }, - fixed_control: true, - locale: Some(LocaleQuoting::Single), - }, - "clocale" => QuotingStyleSpec::with_locale(QuotingStyle::C_DOUBLE, LocaleQuoting::Double), - _ => return None, - }; - - let style = if spec.fixed_control { - spec.style - } else { - spec.style.show_control(show_control) - }; - - Some((style, spec.locale)) -} - -/// Extracts the quoting style to use based on the options provided. -/// If no options are given, it looks if a default quoting style is provided -/// through the [`QUOTING_STYLE`] environment variable. -/// -/// # Arguments -/// -/// * `options` - A reference to a [`clap::ArgMatches`] object containing command line arguments. -/// * `show_control` - A boolean value representing whether or not to show control characters. -/// -/// # Returns -/// -/// A [`QuotingStyle`] variant representing the quoting style to use. -fn extract_quoting_style( - options: &clap::ArgMatches, - show_control: bool, -) -> (QuotingStyle, Option) { - let opt_quoting_style = options.get_one::(QUOTING_STYLE); - - if let Some(style) = opt_quoting_style { - match match_quoting_style_name(style, show_control) { - Some(pair) => pair, - None => unreachable!("Should have been caught by Clap"), - } - } else if options.get_flag(options::quoting::LITERAL) { - (QuotingStyle::Literal { show_control }, None) - } else if options.get_flag(options::quoting::ESCAPE) { - (QuotingStyle::C_NO_QUOTES, None) - } else if options.get_flag(options::quoting::C) { - (QuotingStyle::C_DOUBLE, None) - } else if options.get_flag(options::DIRED) { - (QuotingStyle::Literal { show_control }, None) - } else { - // If set, the QUOTING_STYLE environment variable specifies a default style. - if let Ok(style) = std::env::var("QUOTING_STYLE") { - match match_quoting_style_name(style.as_str(), show_control) { - Some(pair) => return pair, - None => eprintln!( - "{}", - translate!("ls-invalid-quoting-style", "program" => std::env::args().next().unwrap_or_else(|| "ls".to_string()), "style" => style.clone()) - ), - } - } - - // By default, `ls` uses Shell escape quoting style when writing to a terminal file - // descriptor and Literal otherwise. - if stdout().is_terminal() { - (QuotingStyle::SHELL_ESCAPE.show_control(show_control), None) - } else { - (QuotingStyle::Literal { show_control }, None) - } - } -} - -/// Extracts the indicator style to use based on the options provided. -/// -/// # Returns -/// -/// An [`IndicatorStyle`] variant representing the indicator style to use. -fn extract_indicator_style(options: &clap::ArgMatches) -> IndicatorStyle { - if let Some(field) = options.get_one::(options::INDICATOR_STYLE) { - match field.as_str() { - "none" => IndicatorStyle::None, - "file-type" => IndicatorStyle::FileType, - "classify" => IndicatorStyle::Classify, - "slash" => IndicatorStyle::Slash, - &_ => IndicatorStyle::None, - } - } else if let Some(field) = options.get_one::(options::indicator_style::CLASSIFY) { - match field.as_str() { - "never" | "no" | "none" => IndicatorStyle::None, - "always" | "yes" | "force" => IndicatorStyle::Classify, - "auto" | "tty" | "if-tty" => { - if stdout().is_terminal() { - IndicatorStyle::Classify - } else { - IndicatorStyle::None - } - } - &_ => IndicatorStyle::None, - } - } else if options.get_flag(options::indicator_style::SLASH) { - IndicatorStyle::Slash - } else if options.get_flag(options::indicator_style::FILE_TYPE) { - IndicatorStyle::FileType - } else { - IndicatorStyle::None - } -} - -/// Parses the width value from either the command line arguments or the environment variables. -fn parse_width(width_match: Option<&String>) -> Result { - let parse_width_from_args = |s: &str| -> Result { - let radix = if s.starts_with('0') && s.len() > 1 { - 8 - } else { - 10 - }; - match u16::from_str_radix(s, radix) { - Ok(x) => Ok(x), - Err(e) => match e.kind() { - IntErrorKind::PosOverflow => Ok(u16::MAX), - _ => Err(LsError::InvalidLineWidth(s.into())), - }, - } - }; - - let parse_width_from_env = |columns: OsString| { - if let Some(columns) = columns.to_str().and_then(|s| s.parse().ok()) { - columns - } else { - show_error!( - "{}", - translate!("ls-invalid-columns-width", "width" => columns.quote()) - ); - DEFAULT_TERM_WIDTH - } - }; - - let calculate_term_size = || match terminal_size::terminal_size() { - Some((width, _)) => width.0, - None => DEFAULT_TERM_WIDTH, - }; - - let ret = match width_match { - Some(x) => parse_width_from_args(x)?, - None => match std::env::var_os("COLUMNS") { - Some(columns) => parse_width_from_env(columns), - None => calculate_term_size(), - }, - }; - - Ok(ret) -} - -impl Config { - #[allow(clippy::cognitive_complexity)] - pub fn from(options: &clap::ArgMatches) -> UResult { - let context = options.get_flag(options::CONTEXT); - let (mut format, opt) = extract_format(options); - let files = extract_files(options); - - // The -o, -n and -g options are tricky. They cannot override with each - // other because it's possible to combine them. For example, the option - // -og should hide both owner and group. Furthermore, they are not - // reset if -l or --format=long is used. So these should just show the - // group: -gl or "-g --format=long". Finally, they are also not reset - // when switching to a different format option in-between like this: - // -ogCl or "-og --format=vertical --format=long". - // - // -1 has a similar issue: it does nothing if the format is long. This - // actually makes it distinct from the --format=singe-column option, - // which always applies. - // - // The idea here is to not let these options override with the other - // options, but manually whether they have an index that's greater than - // the other format options. If so, we set the appropriate format. - if format != Format::Long { - let idx = opt - .and_then(|opt| options.indices_of(opt).map(|x| x.max().unwrap())) - .unwrap_or(0); - if [ - options::format::LONG_NO_OWNER, - options::format::LONG_NO_GROUP, - options::format::LONG_NUMERIC_UID_GID, - options::FULL_TIME, - ] - .iter() - .filter_map(|opt| { - if options.value_source(opt) == Some(clap::parser::ValueSource::CommandLine) { - options.indices_of(opt) - } else { - None - } - }) - .flatten() - .any(|i| i >= idx) - { - format = Format::Long; - } else if let Some(mut indices) = options.indices_of(options::format::ONE_LINE) { - if options.value_source(options::format::ONE_LINE) - == Some(clap::parser::ValueSource::CommandLine) - && indices.any(|i| i > idx) - { - format = Format::OneLine; - } - } - } - - let sort = extract_sort(options); - let time = extract_time(options); - let mut needs_color = extract_color(options); - let hyperlink = extract_hyperlink(options); - - let opt_block_size = options.get_one::(options::size::BLOCK_SIZE); - let opt_si = opt_block_size.is_some() - && options - .get_one::(options::size::BLOCK_SIZE) - .unwrap() - .eq("si") - || options.get_flag(options::size::SI); - let opt_hr = (opt_block_size.is_some() - && options - .get_one::(options::size::BLOCK_SIZE) - .unwrap() - .eq("human-readable")) - || options.get_flag(options::size::HUMAN_READABLE); - let opt_kb = options.get_flag(options::size::KIBIBYTES); - - let size_format = if opt_si { - SizeFormat::Decimal - } else if opt_hr { - SizeFormat::Binary - } else { - SizeFormat::Bytes - }; - - let env_var_blocksize = std::env::var_os("BLOCKSIZE"); - let env_var_block_size = std::env::var_os("BLOCK_SIZE"); - let env_var_ls_block_size = std::env::var_os("LS_BLOCK_SIZE"); - let env_var_posixly_correct = std::env::var_os("POSIXLY_CORRECT"); - let mut is_env_var_blocksize = false; - - let raw_block_size = if let Some(opt_block_size) = opt_block_size { - OsString::from(opt_block_size) - } else if let Some(env_var_ls_block_size) = env_var_ls_block_size { - env_var_ls_block_size - } else if let Some(env_var_block_size) = env_var_block_size { - env_var_block_size - } else if let Some(env_var_blocksize) = env_var_blocksize { - is_env_var_blocksize = true; - env_var_blocksize - } else { - OsString::from("") - }; - - let (file_size_block_size, block_size) = if !opt_si && !opt_hr && !raw_block_size.is_empty() - { - if let Ok(size) = parse_size_non_zero_u64(&raw_block_size.to_string_lossy()) { - match (is_env_var_blocksize, opt_kb) { - (true, true) => (DEFAULT_FILE_SIZE_BLOCK_SIZE, DEFAULT_BLOCK_SIZE), - (true, false) => (DEFAULT_FILE_SIZE_BLOCK_SIZE, size), - (false, true) => { - // --block-size overrides -k - if opt_block_size.is_some() { - (size, size) - } else { - (size, DEFAULT_BLOCK_SIZE) - } - } - (false, false) => (size, size), - } - } else { - // only fail if invalid block size was specified with --block-size, - // ignore invalid block size from env vars - if let Some(invalid_block_size) = opt_block_size { - return Err(Box::new(LsError::BlockSizeParseError( - invalid_block_size.clone(), - ))); - } - if is_env_var_blocksize { - (DEFAULT_FILE_SIZE_BLOCK_SIZE, DEFAULT_BLOCK_SIZE) - } else { - (DEFAULT_BLOCK_SIZE, DEFAULT_BLOCK_SIZE) - } - } - } else if env_var_posixly_correct.is_some() { - if opt_kb { - (DEFAULT_FILE_SIZE_BLOCK_SIZE, DEFAULT_BLOCK_SIZE) - } else { - (DEFAULT_FILE_SIZE_BLOCK_SIZE, POSIXLY_CORRECT_BLOCK_SIZE) - } - } else if opt_si { - (DEFAULT_FILE_SIZE_BLOCK_SIZE, 1000) - } else { - (DEFAULT_FILE_SIZE_BLOCK_SIZE, DEFAULT_BLOCK_SIZE) - }; - - let long = { - let author = options.get_flag(options::AUTHOR); - let group = !options.get_flag(options::NO_GROUP) - && !options.get_flag(options::format::LONG_NO_GROUP); - let owner = !options.get_flag(options::format::LONG_NO_OWNER); - #[cfg(unix)] - let numeric_uid_gid = options.get_flag(options::format::LONG_NUMERIC_UID_GID); - LongFormat { - author, - group, - owner, - #[cfg(unix)] - numeric_uid_gid, - } - }; - let width = parse_width(options.get_one::(options::WIDTH))?; - - #[allow(clippy::needless_bool)] - let mut show_control = if options.get_flag(options::HIDE_CONTROL_CHARS) { - false - } else if options.get_flag(options::SHOW_CONTROL_CHARS) { - true - } else { - !stdout().is_terminal() - }; - - let (mut quoting_style, mut locale_quoting) = extract_quoting_style(options, show_control); - let indicator_style = extract_indicator_style(options); - // Only parse the value to "--time-style" if it will become relevant. - let dired = options.get_flag(options::DIRED); - let (time_format_recent, time_format_older) = if format == Format::Long || dired { - parse_time_style(options)? - } else { - Default::default() - }; - - let mut ignore_patterns: Vec = Vec::new(); - - if options.get_flag(options::IGNORE_BACKUPS) { - ignore_patterns.push(Pattern::new("*~").unwrap()); - ignore_patterns.push(Pattern::new(".*~").unwrap()); - } - - for pattern in options - .get_many::(options::IGNORE) - .into_iter() - .flatten() - { - if let Ok(p) = parse_glob::from_str(pattern) { - ignore_patterns.push(p); - } else { - show_warning!( - "{}", - translate!("ls-invalid-ignore-pattern", "pattern" => pattern.quote()) - ); - } - } - - if files == Files::Normal { - for pattern in options - .get_many::(options::HIDE) - .into_iter() - .flatten() - { - if let Ok(p) = parse_glob::from_str(pattern) { - ignore_patterns.push(p); - } else { - show_warning!( - "{}", - translate!("ls-invalid-hide-pattern", "pattern" => pattern.quote()) - ); - } - } - } - - // According to ls info page, `--zero` implies the following flags: - // - `--show-control-chars` - // - `--format=single-column` - // - `--color=none` - // - `--quoting-style=literal` - // Current GNU ls implementation allows `--zero` Behavior to be - // overridden by later flags. - let zero_formats_opts = [ - options::format::ACROSS, - options::format::COLUMNS, - options::format::COMMAS, - options::format::LONG, - options::format::LONG_NO_GROUP, - options::format::LONG_NO_OWNER, - options::format::LONG_NUMERIC_UID_GID, - options::format::ONE_LINE, - options::FORMAT, - ]; - let zero_colors_opts = [options::COLOR]; - let zero_show_control_opts = [options::HIDE_CONTROL_CHARS, options::SHOW_CONTROL_CHARS]; - let zero_quoting_style_opts = [ - QUOTING_STYLE, - options::quoting::C, - options::quoting::ESCAPE, - options::quoting::LITERAL, - ]; - let get_last = |flag: &str| -> usize { - if options.value_source(flag) == Some(clap::parser::ValueSource::CommandLine) { - options.index_of(flag).unwrap_or(0) - } else { - 0 - } - }; - if get_last(options::ZERO) - > zero_formats_opts - .into_iter() - .map(get_last) - .max() - .unwrap_or(0) - { - format = if format == Format::Long { - format - } else { - Format::OneLine - }; - } - if get_last(options::ZERO) - > zero_colors_opts - .into_iter() - .map(get_last) - .max() - .unwrap_or(0) - { - needs_color = false; - } - if get_last(options::ZERO) - > zero_show_control_opts - .into_iter() - .map(get_last) - .max() - .unwrap_or(0) - { - show_control = true; - } - if get_last(options::ZERO) - > zero_quoting_style_opts - .into_iter() - .map(get_last) - .max() - .unwrap_or(0) - { - quoting_style = QuotingStyle::Literal { show_control }; - locale_quoting = None; - } - - if needs_color { - if let Err(err) = validate_ls_colors_env() { - if let LsColorsParseError::UnrecognizedPrefix(prefix) = &err { - show_warning!( - "{}", - translate!( - "ls-warning-unrecognized-ls-colors-prefix", - "prefix" => prefix.quote() - ) - ); - } - show_warning!("{}", translate!("ls-warning-unparsable-ls-colors")); - needs_color = false; - } - } - - let color = if needs_color { - Some(LsColors::from_env().unwrap_or_default()) - } else { - None - }; - - if dired || is_dired_arg_present() { - // --dired implies --format=long - // if we have --dired --hyperlink, we don't show dired but we still want to see the - // long format - format = Format::Long; - } - if dired && options.get_flag(options::ZERO) { - return Err(Box::new(LsError::DiredAndZeroAreIncompatible)); - } - - let dereference = if options.get_flag(options::dereference::ALL) { - Dereference::All - } else if options.get_flag(options::dereference::ARGS) { - Dereference::Args - } else if options.get_flag(options::dereference::DIR_ARGS) { - Dereference::DirArgs - } else if options.get_flag(options::DIRECTORY) - || indicator_style == IndicatorStyle::Classify - || format == Format::Long - { - Dereference::None - } else { - Dereference::DirArgs - }; - - let tab_size = if needs_color { - Some(0) - } else { - options - .get_one::(options::format::TAB_SIZE) - .and_then(|size| size.parse::().ok()) - .or_else(|| std::env::var("TABSIZE").ok().and_then(|s| s.parse().ok())) - } - .unwrap_or(SPACES_IN_TAB); - - Ok(Self { - format, - files, - sort, - recursive: options.get_flag(options::RECURSIVE), - reverse: options.get_flag(options::REVERSE), - dereference, - ignore_patterns, - size_format, - directory: options.get_flag(options::DIRECTORY), - time, - color, - #[cfg(unix)] - inode: options.get_flag(options::INODE), - long, - alloc_size: options.get_flag(options::size::ALLOCATION_SIZE), - file_size_block_size, - block_size, - width, - quoting_style, - locale_quoting, - indicator_style, - time_format_recent, - time_format_older, - context, - #[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))] - selinux_supported: uucore::selinux::is_selinux_enabled(), - #[cfg(all(feature = "smack", target_os = "linux"))] - smack_supported: uucore::smack::is_smack_enabled(), - group_directories_first: options.get_flag(options::GROUP_DIRECTORIES_FIRST), - line_ending: LineEnding::from_zero_flag(options.get_flag(options::ZERO)), - dired, - hyperlink, - tab_size, - }) - } -} - -fn parse_time_style(options: &clap::ArgMatches) -> Result<(String, Option), LsError> { - // TODO: Using correct locale string is not implemented. - const LOCALE_FORMAT: (&str, Option<&str>) = ("%b %e %H:%M", Some("%b %e %Y")); - - // Convert time_styles references to owned String/option. - #[expect(clippy::unnecessary_wraps, reason = "internal result helper")] - fn ok((recent, older): (&str, Option<&str>)) -> Result<(String, Option), LsError> { - Ok((recent.to_string(), older.map(String::from))) - } - - if let Some(field) = options - .get_one::(options::TIME_STYLE) - .map(ToOwned::to_owned) - .or_else(|| std::env::var("TIME_STYLE").ok()) - { - //If both FULL_TIME and TIME_STYLE are present - //The one added last is dominant - if options.get_flag(options::FULL_TIME) - && options.indices_of(options::FULL_TIME).unwrap().next_back() - > options.indices_of(options::TIME_STYLE).unwrap().next_back() - { - ok((format::FULL_ISO, None)) - } else { - let field = if let Some(field) = field.strip_prefix("posix-") { - // See GNU documentation, set format to "locale" if LC_TIME="POSIX", - // else just strip the prefix and continue (even "posix+FORMAT" is - // supported). - // TODO: This needs to be moved to uucore and handled by icu? - if std::env::var_os("LC_TIME").as_deref() == Some(OsStr::new("POSIX")) - || std::env::var_os("LC_ALL").as_deref() == Some(OsStr::new("POSIX")) - { - return ok(LOCALE_FORMAT); - } - field - } else { - &field - }; - - match field { - "full-iso" => ok((format::FULL_ISO, None)), - "long-iso" => ok((format::LONG_ISO, None)), - // ISO older format needs extra padding. - "iso" => Ok(( - "%m-%d %H:%M".to_string(), - Some(format::ISO.to_string() + " "), - )), - "locale" => ok(LOCALE_FORMAT), - _ => match field.chars().next().unwrap() { - '+' => { - // recent/older formats are (optionally) separated by a newline - let mut it = field[1..].split('\n'); - let recent = it.next().unwrap_or_default(); - let older = it.next(); - match it.next() { - None => ok((recent, older)), - Some(_) => Err(LsError::TimeStyleParseError(String::from(field))), - } - } - _ => Err(LsError::TimeStyleParseError(String::from(field))), - }, - } - } - } else if options.get_flag(options::FULL_TIME) { - ok((format::FULL_ISO, None)) - } else { - ok(LOCALE_FORMAT) - } -} diff --git a/src/uu/ls/src/display.rs b/src/uu/ls/src/display.rs deleted file mode 100644 index 4f77ebd8720..00000000000 --- a/src/uu/ls/src/display.rs +++ /dev/null @@ -1,1319 +0,0 @@ -// This file is part of the uutils coreutils package. -// -// For the full copyright and license information, please view the LICENSE -// file that was distributed with this source code. - -// spell-checker:ignore (ToDO) somegroup nlink tabsize dired subdired dtype colorterm stringly -// spell-checker:ignore nohash strtime clocale - -use std::cell::LazyCell; -#[cfg(unix)] -use std::os::unix::fs::{FileTypeExt, MetadataExt}; -#[cfg(windows)] -use std::os::windows::fs::MetadataExt; -/// Show the directory name in the case where several arguments are given to ls -use std::{borrow::Cow, iter}; -use std::{ - ffi::{OsStr, OsString}, - fmt::Write as _, - fs::{self, DirEntry, FileType, Metadata}, - io::{BufWriter, Stdout, Write}, -}; - -use ansi_width::ansi_width; -use glob::MatchOptions; -use term_grid::{DEFAULT_SEPARATOR_SIZE, Direction, Filling, Grid, GridOptions}; - -#[cfg(unix)] -use uucore::entries; -#[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] -use uucore::fsxattr::has_acl; -#[cfg(any( - target_os = "linux", - target_os = "macos", - target_os = "android", - target_os = "ios", - target_os = "freebsd", - target_os = "dragonfly", - target_os = "netbsd", - target_os = "openbsd", - target_os = "illumos", - target_os = "solaris" -))] -use uucore::libc::{dev_t, major, minor}; -use uucore::{ - error::UResult, - format::human::human_readable, - fs::display_permissions, - fsext::metadata_get_time, - line_ending::LineEnding, - os_str_as_bytes_lossy, - quoting_style::{QuotingStyle, locale_aware_escape_dir_name, locale_aware_escape_name}, - show, - time::{FormatSystemTimeFallback, format_system_time}, -}; - -use crate::colors::color_name; -use crate::config::Files; -use crate::dired::{self, DiredOutput}; -use crate::{Config, ListState, LsError, PathData, get_block_size}; - -// Fields that can be removed or added to the long format -pub(crate) struct LongFormat { - pub(crate) author: bool, - pub(crate) group: bool, - pub(crate) owner: bool, - #[cfg(unix)] - pub(crate) numeric_uid_gid: bool, -} - -pub(crate) struct PaddingCollection { - #[cfg(unix)] - pub(crate) inode: usize, - pub(crate) link_count: usize, - pub(crate) uname: usize, - pub(crate) group: usize, - pub(crate) context: usize, - pub(crate) size: usize, - #[cfg(unix)] - pub(crate) major: usize, - #[cfg(unix)] - pub(crate) minor: usize, - pub(crate) block_size: usize, -} - -pub(crate) struct DisplayItemName { - pub(crate) displayed: OsString, - pub(crate) dired_name_len: usize, -} - -#[derive(PartialEq, Eq)] -pub(crate) enum IndicatorStyle { - None, - Slash, - FileType, - Classify, -} - -#[derive(Clone, Copy, PartialEq, Eq)] -pub(crate) enum LocaleQuoting { - Single, - Double, -} - -#[derive(PartialEq, Eq, Debug)] -pub enum Format { - Columns, - Long, - OneLine, - Across, - Commas, -} - -#[allow(dead_code)] -enum SizeOrDeviceId { - Size(String), - Device(String, String), -} - -/// or the recursive flag is passed. -/// -/// ```no-exec -/// $ ls -R -/// .: <- This is printed by this function -/// dir1 file1 file2 -/// -/// dir1: <- This as well -/// file11 -/// ``` -pub fn show_dir_name( - path_data: &PathData, - out: &mut BufWriter, - config: &Config, -) -> std::io::Result<()> { - let escaped_name = escape_dir_name_with_locale(path_data.path().as_os_str(), config); - - let name = if config.hyperlink && !config.dired { - create_hyperlink(&escaped_name, path_data) - } else { - escaped_name - }; - - write_os_str(out, &name)?; - write!(out, ":") -} - -fn escape_with_locale(name: &OsStr, config: &Config, fallback: F) -> OsString -where - F: FnOnce(&OsStr, QuotingStyle) -> OsString, -{ - if let Some(locale) = config.locale_quoting { - locale_quote(name, locale) - } else { - fallback(name, config.quoting_style) - } -} - -fn escape_dir_name_with_locale(name: &OsStr, config: &Config) -> OsString { - escape_with_locale(name, config, locale_aware_escape_dir_name) -} - -fn escape_name_with_locale(name: &OsStr, config: &Config) -> OsString { - escape_with_locale(name, config, locale_aware_escape_name) -} - -fn locale_quote(name: &OsStr, style: LocaleQuoting) -> OsString { - let bytes = os_str_as_bytes_lossy(name); - let mut quoted = String::new(); - match style { - LocaleQuoting::Single => quoted.push('\''), - LocaleQuoting::Double => quoted.push('"'), - } - for &byte in bytes.as_ref() { - push_locale_byte(&mut quoted, byte, style); - } - match style { - LocaleQuoting::Single => quoted.push('\''), - LocaleQuoting::Double => quoted.push('"'), - } - OsString::from(quoted) -} - -fn push_locale_byte(buf: &mut String, byte: u8, style: LocaleQuoting) { - match (style, byte) { - (LocaleQuoting::Single, b'\'') => buf.push_str("'\\''"), - (LocaleQuoting::Double, b'"') => buf.push_str("\\\""), - (_, b'\\') => buf.push_str("\\\\"), - _ => push_basic_escape(buf, byte), - } -} - -fn push_basic_escape(buf: &mut String, byte: u8) { - match byte { - b'\x07' => buf.push_str("\\a"), - b'\x08' => buf.push_str("\\b"), - b'\t' => buf.push_str("\\t"), - b'\n' => buf.push_str("\\n"), - b'\x0b' => buf.push_str("\\v"), - b'\x0c' => buf.push_str("\\f"), - b'\r' => buf.push_str("\\r"), - b'\x1b' => buf.push_str("\\e"), - b'"' => buf.push('"'), - b'\'' => buf.push('\''), - b if (0x20..=0x7e).contains(&b) => buf.push(b as char), - _ => { - let _ = write!(buf, "\\{byte:03o}"); - } - } -} - -pub fn should_display(entry: &DirEntry, config: &Config) -> bool { - // check if hidden - if config.files == Files::Normal && is_hidden(entry) { - return false; - } - - // check if it is among ignore_patterns - let options = MatchOptions { - // setting require_literal_leading_dot to match behavior in GNU ls - require_literal_leading_dot: true, - 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) => Cow::Borrowed(s), - None => file_name.to_string_lossy(), - }; - - !config - .ignore_patterns - .iter() - .any(|p| p.matches_with(&file_name, options)) -} - -fn display_dir_entry_size( - entry: &PathData, - config: &Config, - 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.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()) - } - SizeOrDeviceId::Size(size) => (size.len(), 0usize, 0usize), - }; - ( - display_symlink_count(md).len(), - display_uname(md, config, state).len(), - display_group(md, config, state).len(), - size_len, - major_len, - minor_len, - ) - } else { - (0, 0, 0, 0, 0, 0) - } -} - -// A simple, performant, ExtendPad trait to add a string to a Vec, padding with spaces -// on the left or right, without making additional copies, or using formatting functions. -pub trait ExtendPad { - fn extend_pad_left(&mut self, string: &str, count: usize); - fn extend_pad_right(&mut self, string: &str, count: usize); -} - -impl ExtendPad for Vec { - fn extend_pad_left(&mut self, string: &str, count: usize) { - if string.len() < count { - self.extend(iter::repeat_n(b' ', count - string.len())); - } - self.extend(string.as_bytes()); - } - - fn extend_pad_right(&mut self, string: &str, count: usize) { - self.extend(string.as_bytes()); - if string.len() < count { - self.extend(iter::repeat_n(b' ', count - string.len())); - } - } -} - -// TODO: Consider converting callers to use ExtendPad instead, as it avoids -// additional copies. -fn pad_left(string: &str, count: usize) -> String { - format!("{string:>count$}") -} - -#[allow(clippy::cognitive_complexity)] -pub fn display_items( - items: &[PathData], - config: &Config, - state: &mut ListState, - dired: &mut DiredOutput, -) -> UResult<()> { - // `-Z`, `--context`: - // Display the SELinux security context or '?' if none is found. When used with the `-l` - // option, print the security context to the left of the size column. - - let quoted = items.iter().any(|item| { - let name = escape_name_with_locale(item.display_name(), config); - os_str_starts_with(&name, b"'") - }); - - if config.format == Format::Long { - let padding_collection = calculate_padding_collection(items, config, state); - - for item in items { - #[cfg(unix)] - let should_display_leading_info = config.inode || config.alloc_size; - #[cfg(not(unix))] - let should_display_leading_info = config.alloc_size; - - if should_display_leading_info { - let more_info = display_additional_leading_info(item, &padding_collection, config); - - write!(state.out, "{more_info}")?; - } - - display_item_long(item, &padding_collection, config, state, dired, quoted)?; - } - } else { - let mut longest_context_len = 1; - let prefix_context = if config.context { - for item in items { - let context_len = item.security_context(config).len(); - longest_context_len = context_len.max(longest_context_len); - } - Some(longest_context_len) - } else { - None - }; - - let padding = calculate_padding_collection(items, config, state); - - // we need to apply normal color to non filename output - if let Some(style_manager) = &mut state.style_manager { - write!(state.out, "{}", style_manager.apply_normal())?; - } - - 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 = 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 - // wraps. - let cell = display_item_name( - i, - config, - prefix_context, - more_info, - state, - LazyCell::new(Box::new(|| 0)), - ); - - names_vec.push(cell.displayed); - } - - let mut names = names_vec.into_iter(); - - match config.format { - Format::Columns => { - display_grid( - names, - config.width, - Direction::TopToBottom, - &mut state.out, - quoted, - config.tab_size, - )?; - } - Format::Across => { - display_grid( - names, - config.width, - Direction::LeftToRight, - &mut state.out, - quoted, - config.tab_size, - )?; - } - Format::Commas => { - let mut current_col = 0; - if let Some(name) = names.next() { - write_os_str(&mut state.out, &name)?; - current_col = ansi_width(&name.to_string_lossy()) as u16 + 2; - } - for name in names { - let name_width = ansi_width(&name.to_string_lossy()) as u16; - // If the width is 0 we print one single line - if config.width != 0 && current_col + name_width + 1 > config.width { - current_col = name_width + 2; - writeln!(state.out, ",")?; - } else { - current_col += name_width + 2; - write!(state.out, ", ")?; - } - write_os_str(&mut state.out, &name)?; - } - // Current col is never zero again if names have been printed. - // So we print a newline. - if current_col > 0 { - write!(state.out, "{}", config.line_ending)?; - } - } - _ => { - for name in names { - write_os_str(&mut state.out, &name)?; - write!(state.out, "{}", config.line_ending)?; - } - } - } - } - - Ok(()) -} - -fn display_grid( - names: impl Iterator, - width: u16, - direction: Direction, - out: &mut BufWriter, - quoted: bool, - tab_size: usize, -) -> UResult<()> { - if width == 0 { - // If the width is 0 we print one single line - let mut printed_something = false; - for name in names { - if printed_something { - write!(out, " ")?; - } - printed_something = true; - write_os_str(out, &name)?; - } - if printed_something { - writeln!(out)?; - } - } else { - let names: Vec<_> = if quoted { - // In case some names are quoted, GNU adds a space before each - // entry that does not start with a quote to make it prettier - // on multiline. - // - // Example: - // ``` - // $ ls - // 'a\nb' bar - // foo baz - // ^ ^ - // These spaces is added - // ``` - names - .map(|n| { - if os_str_starts_with(&n, b"'") || os_str_starts_with(&n, b"\"") { - n - } else { - let mut ret: OsString = " ".into(); - ret.push(n); - ret - } - }) - .collect() - } else { - names.collect() - }; - - // FIXME: the Grid crate only supports &str, so can't display raw bytes - let names: Vec<_> = names - .into_iter() - .map(|s| s.to_string_lossy().into_owned()) - .collect(); - - // Since tab_size=0 means no \t, use Spaces separator for optimization. - let filling = match tab_size { - 0 => Filling::Spaces(DEFAULT_SEPARATOR_SIZE), - _ => Filling::Tabs { - spaces: DEFAULT_SEPARATOR_SIZE, - tab_size, - }, - }; - - let grid = Grid::new( - names, - GridOptions { - filling, - direction, - width: width as usize, - }, - ); - write!(out, "{grid}")?; - } - Ok(()) -} - -fn display_additional_leading_info( - item: &PathData, - padding: &PaddingCollection, - config: &Config, -) -> String { - let mut result = String::new(); - #[cfg(unix)] - { - if config.inode { - let i = if let Some(md) = item.metadata() { - get_inode(md) - } else { - "?".to_owned() - }; - write!(result, "{} ", pad_left(&i, padding.inode)).unwrap(); - } - } - - if config.alloc_size { - let s = if let Some(md) = item.metadata() { - display_size(get_block_size(md, config), config) - } else { - "?".to_owned() - }; - // extra space is insert to align the sizes, as needed for all formats, except for the comma format. - if config.format == Format::Commas { - write!(result, "{s} ").unwrap(); - } else { - write!(result, "{} ", pad_left(&s, padding.block_size)).unwrap(); - } - } - - result -} - -fn calculate_line_len(output_len: usize, item_len: usize, line_ending: LineEnding) -> usize { - output_len + item_len + line_ending.to_string().len() -} - -#[cfg(unix)] -fn get_inode(metadata: &Metadata) -> String { - format!("{}", metadata.ino()) -} - -// Currently getpwuid is `linux` target only. If it's broken state.out into -// a posix-compliant attribute this can be updated... -#[cfg(unix)] -fn display_uname<'a>(metadata: &Metadata, config: &Config, state: &'a mut ListState) -> &'a String { - let uid = metadata.uid(); - - state.uid_cache.entry(uid).or_insert_with(|| { - if config.long.numeric_uid_gid { - uid.to_string() - } else { - entries::uid2usr(uid).unwrap_or_else(|_| uid.to_string()) - } - }) -} - -#[cfg(unix)] -fn display_group<'a>(metadata: &Metadata, config: &Config, state: &'a mut ListState) -> &'a String { - let gid = metadata.gid(); - state.gid_cache.entry(gid).or_insert_with(|| { - if config.long.numeric_uid_gid { - gid.to_string() - } else { - entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string()) - } - }) -} - -#[cfg(not(unix))] -fn display_uname(_metadata: &Metadata, _config: &Config, _state: &mut ListState) -> &'static str { - "somebody" -} - -#[cfg(not(unix))] -fn display_group(_metadata: &Metadata, _config: &Config, _state: &mut ListState) -> &'static str { - "somegroup" -} - -fn display_date( - metadata: &Metadata, - config: &Config, - state: &mut ListState, - out: &mut Vec, -) -> UResult<()> { - let Some(time) = metadata_get_time(metadata, config.time) else { - out.extend(b"???"); - return Ok(()); - }; - - // Use "recent" format if the given date is considered recent (i.e., in the last 6 months), - // or if no "older" format is available. - let fmt = match &config.time_format_older { - Some(time_format_older) if !state.recent_time_range.contains(&time) => time_format_older, - _ => &config.time_format_recent, - }; - - format_system_time(out, time, fmt, FormatSystemTimeFallback::Integer) -} - -fn display_len_or_rdev(metadata: &Metadata, config: &Config) -> SizeOrDeviceId { - #[cfg(any( - target_os = "linux", - target_os = "macos", - target_os = "android", - target_os = "ios", - target_os = "freebsd", - target_os = "dragonfly", - target_os = "netbsd", - target_os = "openbsd", - target_os = "illumos", - target_os = "solaris" - ))] - { - let ft = metadata.file_type(); - if ft.is_char_device() || ft.is_block_device() { - // A type cast is needed here as the `dev_t` type varies across OSes. - let dev = metadata.rdev() as dev_t; - let major = major(dev); - let minor = minor(dev); - return SizeOrDeviceId::Device(major.to_string(), minor.to_string()); - } - } - let len_adjusted = { - let d = metadata.len() / config.file_size_block_size; - let r = metadata.len() % config.file_size_block_size; - if r == 0 { d } else { d + 1 } - }; - SizeOrDeviceId::Size(display_size(len_adjusted, config)) -} - -pub fn display_size(size: u64, config: &Config) -> String { - human_readable(size, config.size_format) -} - -/// Takes a [`PathData`] struct and returns a cell with a name ready for displaying. -/// -/// This function relies on the following parameters in the provided `&Config`: -/// * `config.quoting_style` to decide how we will escape `name` using [`locale_aware_escape_name`]. -/// * `config.inode` decides whether to display inode numbers beside names using [`get_inode`]. -/// * `config.color` decides whether it's going to color `name` using [`color_name`]. -/// * `config.indicator_style` to append specific characters to `name` using [`classify_file`]. -/// * `config.format` to display symlink targets if `Format::Long`. This function is also -/// responsible for coloring symlink target names if `config.color` is specified. -/// * `config.context` to prepend security context to `name` if compiled with `feat_selinux`. -/// * `config.hyperlink` decides whether to hyperlink the item -/// -/// Note that non-unicode sequences in symlink targets are dealt with using -/// [`std::path::Path::to_string_lossy`]. -#[allow(clippy::cognitive_complexity)] -fn display_item_name( - path: &PathData, - config: &Config, - prefix_context: Option, - more_info: Option, - state: &mut ListState, - current_column: LazyCell usize + '_>>, -) -> DisplayItemName { - // This is our return value. We start by `&path.display_name` and modify it along the way. - let mut name = escape_name_with_locale(path.display_name(), config); - - let is_wrap = - |namelen: usize| config.width != 0 && *current_column + namelen > config.width.into(); - - if config.hyperlink { - name = create_hyperlink(&name, path); - } - - if let Some(style_manager) = &mut state.style_manager { - let len = name.len(); - name = color_name(name, path, style_manager, None, is_wrap(len)); - } - - 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); - - let char_opt = match config.indicator_style { - IndicatorStyle::Classify => sym, - IndicatorStyle::FileType => { - // Don't append an asterisk. - match sym { - Some('*') => None, - _ => sym, - } - } - IndicatorStyle::Slash => { - // Append only a slash. - match sym { - Some('/') => Some('/'), - _ => None, - } - } - IndicatorStyle::None => None, - }; - - if let Some(c) = char_opt { - name.push(OsStr::new(&c.to_string())); - } - } - - let dired_name_len = if config.dired { name.len() } else { 0 }; - - if config.format == Format::Long - && path.file_type().is_some_and(FileType::is_symlink) - && !path.must_dereference - { - match path.path().read_link() { - Ok(target_path) => { - name.push(" -> "); - - // We might as well color the symlink output after the arrow. - // This makes extra system calls, but provides important information that - // people run `ls -l --color` are very interested in. - if let Some(style_manager) = &mut state.style_manager { - let escaped_target = escape_name_with_locale(target_path.as_os_str(), config); - // 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_path.clone(); - if target_path.is_relative() { - if let Some(parent) = path.path().parent() { - absolute_target = parent.join(absolute_target); - } - } - - match fs::canonicalize(&absolute_target) { - Ok(resolved_target) => { - let target_data = PathData::new( - resolved_target, - None, - target_path.file_name().map(OsStr::to_os_string), - config, - false, - ); - - // Check if the target actually needs coloring - let md_option: Option = target_data - .metadata() - .cloned() - .or_else(|| target_data.p_buf.symlink_metadata().ok()); - let style = style_manager.colors.style_for_path_with_metadata( - &target_data.p_buf, - md_option.as_ref(), - ); - - if style.is_some() { - // Only apply coloring if there's actually a style - name.push(color_name( - escaped_target, - &target_data, - style_manager, - None, - is_wrap(name.len()), - )); - } else { - // For regular files with no coloring, just use plain text - name.push(escaped_target); - } - } - Err(_) => { - name.push( - style_manager.apply_missing_target_style( - escaped_target, - is_wrap(name.len()), - ), - ); - } - } - } else { - // If no coloring is required, we just use target as is. - // Apply the right quoting - name.push(escape_name_with_locale(target_path.as_os_str(), config)); - } - } - Err(err) => { - show!(LsError::IOErrorContext( - path.path().to_path_buf(), - err, - false - )); - } - } - } - - // Prepend the security context to the `name` and adjust `width` in order - // to get correct alignment from later calls to`display_grid()`. - if config.context { - if let Some(pad_count) = prefix_context { - let security_context = if matches!(config.format, Format::Commas) { - path.security_context(config).to_string() - } else { - pad_left(path.security_context(config), pad_count) - }; - - let old_name = name; - name = format!("{security_context} ").into(); - name.push(old_name); - } - } - - DisplayItemName { - displayed: name, - dired_name_len, - } -} - -/// This writes to the [`BufWriter`] `state.out` a single string of the output of `ls -l`. -/// -/// It writes the following keys, in order: -/// * `inode` ([`get_inode`], config-optional) -/// * `permissions` ([`display_permissions`]) -/// * `symlink_count` ([`display_symlink_count`]) -/// * `owner` ([`display_uname`], config-optional) -/// * `group` ([`display_group`], config-optional) -/// * `author` ([`display_uname`], config-optional) -/// * `size / rdev` ([`display_len_or_rdev`]) -/// * `system_time` ([`display_date`]) -/// * `item_name` ([`display_item_name`]) -/// -/// This function needs to display information in columns: -/// * permissions and `system_time` are already guaranteed to be pre-formatted in fixed length. -/// * `item_name` is the last column and is left-aligned. -/// * Everything else needs to be padded using [`pad_left`]. -/// -/// That's why we have the parameters: -/// ```txt -/// longest_link_count_len: usize, -/// longest_uname_len: usize, -/// longest_group_len: usize, -/// longest_context_len: usize, -/// longest_size_len: usize, -/// ``` -/// that decide the maximum possible character count of each field. -#[allow(clippy::write_literal)] -#[allow(clippy::cognitive_complexity)] -fn display_item_long( - item: &PathData, - padding: &PaddingCollection, - config: &Config, - state: &mut ListState, - dired: &mut DiredOutput, - quoted: bool, -) -> UResult<()> { - let mut output_display: Vec = Vec::with_capacity(128); - - // apply normal color to non filename outputs - if let Some(style_manager) = &mut state.style_manager { - output_display.extend(style_manager.apply_normal().as_bytes()); - } - if config.dired { - output_display.extend(b" "); - } - 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.path()); - output_display.extend(display_permissions(md, true).as_bytes()); - 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"."); - } else if is_acl_set { - output_display.extend(b"+"); - } else { - output_display.extend(b" "); - } - - output_display.extend_pad_left(&display_symlink_count(md), padding.link_count); - - if config.long.owner { - output_display.extend(b" "); - output_display.extend_pad_right(display_uname(md, config, state), padding.uname); - } - - if config.long.group { - output_display.extend(b" "); - output_display.extend_pad_right(display_group(md, config, state), padding.group); - } - - if config.context { - output_display.extend(b" "); - output_display.extend_pad_right(item.security_context(config), padding.context); - } - - // Author is only different from owner on GNU/Hurd, so we reuse - // the owner, since GNU/Hurd is not currently supported by Rust. - if config.long.author { - output_display.extend(b" "); - output_display.extend_pad_right(display_uname(md, config, state), padding.uname); - } - - match display_len_or_rdev(md, config) { - SizeOrDeviceId::Size(size) => { - output_display.extend(b" "); - output_display.extend_pad_left(&size, padding.size); - } - SizeOrDeviceId::Device(major, minor) => { - output_display.extend(b" "); - output_display.extend_pad_left( - &major, - #[cfg(not(unix))] - 0usize, - #[cfg(unix)] - padding.major.max( - padding - .size - .saturating_sub(padding.minor.saturating_add(2usize)), - ), - ); - output_display.extend(b", "); - output_display.extend_pad_left( - &minor, - #[cfg(not(unix))] - 0usize, - #[cfg(unix)] - padding.minor, - ); - } - } - - output_display.extend(b" "); - display_date(md, config, state, &mut output_display)?; - output_display.extend(b" "); - - let item_display = display_item_name( - item, - config, - None, - None, - state, - LazyCell::new(Box::new(|| { - ansi_width(&String::from_utf8_lossy(&output_display)) - })), - ); - - let needs_space = quoted && !os_str_starts_with(&item_display.displayed, b"'"); - - if config.dired { - let mut dired_name_len = item_display.dired_name_len; - if needs_space { - dired_name_len += 1; - } - let displayed_len = item_display.displayed.len() + usize::from(needs_space); - update_dired_for_item( - dired, - output_display.len(), - displayed_len, - dired_name_len, - config.line_ending, - ); - } - - let item_name = item_display.displayed; - let displayed_item = if needs_space { - let mut ret: OsString = " ".into(); - ret.push(&item_name); - ret - } else { - item_name - }; - - write_os_str(&mut output_display, &displayed_item)?; - output_display.extend(config.line_ending.to_string().as_bytes()); - } else { - #[cfg(unix)] - let leading_char = { - if let Some(ft) = item.file_type() { - if ft.is_char_device() { - "c" - } else if ft.is_block_device() { - "b" - } else if ft.is_symlink() { - "l" - } else if ft.is_dir() { - "d" - } else { - "-" - } - } else if item.is_dangling_link() { - "l" - } else { - "-" - } - }; - #[cfg(not(unix))] - let leading_char = { - if let Some(ft) = item.file_type() { - if ft.is_symlink() { - "l" - } else if ft.is_dir() { - "d" - } else { - "-" - } - } else if item.is_dangling_link() { - "l" - } else { - "-" - } - }; - - output_display.extend(leading_char.as_bytes()); - output_display.extend(b"?????????"); - 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"."); - } - output_display.extend(b" "); - output_display.extend_pad_left("?", padding.link_count); - - if config.long.owner { - output_display.extend(b" "); - output_display.extend_pad_right("?", padding.uname); - } - - if config.long.group { - output_display.extend(b" "); - output_display.extend_pad_right("?", padding.group); - } - - if config.context { - output_display.extend(b" "); - output_display.extend_pad_right(item.security_context(config), padding.context); - } - - // Author is only different from owner on GNU/Hurd, so we reuse - // the owner, since GNU/Hurd is not currently supported by Rust. - if config.long.author { - output_display.extend(b" "); - output_display.extend_pad_right("?", padding.uname); - } - - let displayed_item = display_item_name( - item, - config, - None, - None, - state, - LazyCell::new(Box::new(|| { - ansi_width(&String::from_utf8_lossy(&output_display)) - })), - ); - let date_len = 12; - - output_display.extend(b" "); - output_display.extend_pad_left("?", padding.size); - output_display.extend(b" "); - output_display.extend_pad_left("?", date_len); - output_display.extend(b" "); - - if config.dired { - update_dired_for_item( - dired, - output_display.len(), - displayed_item.displayed.len(), - displayed_item.dired_name_len, - config.line_ending, - ); - } - let displayed_item = displayed_item.displayed; - write_os_str(&mut output_display, &displayed_item)?; - output_display.extend(config.line_ending.to_string().as_bytes()); - } - state.out.write_all(&output_display)?; - - Ok(()) -} - -fn classify_file(path: &PathData) -> Option { - let file_type = path.file_type()?; - - if file_type.is_dir() { - Some('/') - } else if file_type.is_symlink() { - Some('@') - } else { - #[cfg(unix)] - { - if file_type.is_socket() { - Some('=') - } else if file_type.is_fifo() { - Some('|') - // Safe unwrapping if the file was removed between listing and display - // See https://github.com/uutils/coreutils/issues/5371 - } else if path.is_executable_file() { - Some('*') - } else { - None - } - } - #[cfg(not(unix))] - None - } -} - -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.path()).unwrap_or_default(); - - // Get bytes for URL encoding in a cross-platform way - let absolute_path_bytes = os_str_as_bytes_lossy(absolute_path.as_os_str()); - - // a set of safe ASCII bytes that don't need encoding - #[cfg(not(target_os = "windows"))] - let unencoded_bytes = b"_-.~/"; - #[cfg(target_os = "windows")] - let unencoded_bytes = b"_-.~/\\:"; - - // Encode at byte level to properly handle UTF-8 sequences and preserve invalid UTF-8 - let full_encoded_path: String = absolute_path_bytes - .iter() - .map(|&b: &u8| { - if b.is_ascii_alphanumeric() || unencoded_bytes.contains(&b) { - (b as char).to_string() - } else { - format!("%{b:02x}") - } - }) - .collect(); - - // OSC 8 hyperlink format: \x1b]8;;URL\x1b\\TEXT\x1b]8;;\x1b\\ - // \x1b = ESC, \x1b\\ = ESC backslash - let mut ret: OsString = format!("\x1b]8;;file://{hostname}{full_encoded_path}\x1b\\").into(); - ret.push(name); - ret.push("\x1b]8;;\x1b\\"); - - ret -} - -fn is_hidden(file_path: &DirEntry) -> bool { - #[cfg(windows)] - { - let metadata = file_path.metadata().unwrap(); - let attr = metadata.file_attributes(); - (attr & 0x2) > 0 - } - #[cfg(not(windows))] - { - file_path.file_name().as_encoded_bytes().starts_with(b".") - } -} - -fn update_dired_for_item( - dired: &mut DiredOutput, - output_display_len: usize, - displayed_len: usize, - dired_name_len: usize, - line_ending: LineEnding, -) { - let line_len = calculate_line_len(output_display_len, displayed_len, line_ending); - dired::calculate_and_update_positions(dired, output_display_len, dired_name_len, line_len); -} - -#[cfg(unix)] -fn display_symlink_count(metadata: &Metadata) -> String { - metadata.nlink().to_string() -} - -#[cfg(unix)] -fn display_inode(metadata: &Metadata) -> String { - get_inode(metadata) -} - -#[cfg(unix)] -fn calculate_padding_collection( - items: &[PathData], - config: &Config, - state: &mut ListState, -) -> PaddingCollection { - let mut padding_collections = PaddingCollection { - inode: 1, - link_count: 1, - uname: 1, - group: 1, - context: 1, - size: 1, - major: 1, - minor: 1, - block_size: 1, - }; - - for item in items { - #[cfg(unix)] - if config.inode { - let inode_len = if let Some(md) = item.metadata() { - display_inode(md).len() - } else { - continue; - }; - padding_collections.inode = inode_len.max(padding_collections.inode); - } - - if config.alloc_size { - 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(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); - padding_collections.uname = uname_len.max(padding_collections.uname); - padding_collections.group = group_len.max(padding_collections.group); - if config.context { - padding_collections.context = context_len.max(padding_collections.context); - } - - // correctly align columns when some files have capabilities/ACLs and others do not - { - #[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()); - if context_len > 1 || is_acl_set { - padding_collections.link_count += 1; - } - } - - if items.len() == 1usize { - padding_collections.size = 0usize; - padding_collections.major = 0usize; - padding_collections.minor = 0usize; - } else { - padding_collections.major = major_len.max(padding_collections.major); - padding_collections.minor = minor_len.max(padding_collections.minor); - padding_collections.size = size_len - .max(padding_collections.size) - .max(padding_collections.major); - } - } - } - - padding_collections -} - -#[cfg(not(unix))] -fn display_symlink_count(_metadata: &Metadata) -> String { - // Currently not sure of how to get this on Windows, so I'm punting. - // Git Bash looks like it may do the same thing. - String::from("1") -} - -#[cfg(not(unix))] -fn calculate_padding_collection( - items: &[PathData], - config: &Config, - state: &mut ListState, -) -> PaddingCollection { - let mut padding_collections = PaddingCollection { - link_count: 1, - uname: 1, - group: 1, - context: 1, - size: 1, - block_size: 1, - }; - - for item in items { - if config.alloc_size { - 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(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); - padding_collections.uname = uname_len.max(padding_collections.uname); - padding_collections.group = group_len.max(padding_collections.group); - if config.context { - padding_collections.context = context_len.max(padding_collections.context); - } - padding_collections.size = size_len.max(padding_collections.size); - } - - padding_collections -} - -fn os_str_starts_with(haystack: &OsStr, needle: &[u8]) -> bool { - os_str_as_bytes_lossy(haystack).starts_with(needle) -} - -fn write_os_str(writer: &mut W, string: &OsStr) -> std::io::Result<()> { - writer.write_all(&os_str_as_bytes_lossy(string)) -} diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index f67c912dd38..748c62de8e8 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -15,7 +15,7 @@ use std::os::unix::fs::{FileTypeExt, MetadataExt}; use std::os::windows::fs::MetadataExt; use std::{borrow::Cow, fs::DirEntry}; use std::{ - cell::OnceCell, + cell::{LazyCell, OnceCell}, cmp::Reverse, ffi::{OsStr, OsString}, fmt::Write as _, @@ -28,40 +28,152 @@ use std::{ time::{Duration, SystemTime, UNIX_EPOCH}, }; +use ansi_width::ansi_width; use clap::{ Arg, ArgAction, Command, builder::{NonEmptyStringValueParser, PossibleValue, ValueParser}, }; -use lscolors::Colorable; +use glob::{MatchOptions, Pattern}; +use lscolors::{Colorable, LsColors}; +use term_grid::{DEFAULT_SEPARATOR_SIZE, Direction, Filling, Grid, GridOptions, SPACES_IN_TAB}; use thiserror::Error; +#[cfg(unix)] +use uucore::entries; +#[cfg(all(unix, not(any(target_os = "android", target_os = "macos"))))] +use uucore::fsxattr::has_acl; #[cfg(unix)] use uucore::libc::{S_IXGRP, S_IXOTH, S_IXUSR}; +#[cfg(any( + target_os = "linux", + target_os = "macos", + target_os = "android", + target_os = "ios", + target_os = "freebsd", + target_os = "dragonfly", + target_os = "netbsd", + target_os = "openbsd", + target_os = "illumos", + target_os = "solaris" +))] +use uucore::libc::{dev_t, major, minor}; use uucore::{ display::Quotable, error::{UError, UResult, set_exit_code}, + format::human::{SizeFormat, human_readable}, format_usage, fs::FileInformation, - fsext::metadata_get_time, + fs::display_permissions, + fsext::{MetadataTimeField, metadata_get_time}, + line_ending::LineEnding, os_str_as_bytes_lossy, + parser::parse_glob, + parser::parse_size::parse_size_non_zero_u64, parser::shortcut_value_parser::ShortcutValueParser, - show, translate, + quoting_style::{QuotingStyle, locale_aware_escape_dir_name, locale_aware_escape_name}, + show, show_error, show_warning, + time::{FormatSystemTimeFallback, format, format_system_time}, + translate, version_cmp::version_cmp, }; -mod colors; -mod config; mod dired; -mod display; +use dired::{DiredOutput, is_dired_arg_present}; +mod colors; +use crate::options::QUOTING_STYLE; +use colors::{LsColorsParseError, StyleManager, color_name, validate_ls_colors_env}; + +pub mod options { + pub mod format { + pub static ONE_LINE: &str = "1"; + pub static LONG: &str = "long"; + pub static COLUMNS: &str = "C"; + pub static ACROSS: &str = "x"; + pub static TAB_SIZE: &str = "tabsize"; + pub static COMMAS: &str = "m"; + pub static LONG_NO_OWNER: &str = "g"; + pub static LONG_NO_GROUP: &str = "o"; + pub static LONG_NUMERIC_UID_GID: &str = "numeric-uid-gid"; + } + + pub mod files { + pub static ALL: &str = "all"; + pub static ALMOST_ALL: &str = "almost-all"; + pub static UNSORTED_ALL: &str = "f"; + } + + pub mod sort { + pub static SIZE: &str = "S"; + pub static TIME: &str = "t"; + pub static NONE: &str = "U"; + pub static VERSION: &str = "v"; + pub static EXTENSION: &str = "X"; + } + + pub mod time { + pub static ACCESS: &str = "u"; + pub static CHANGE: &str = "c"; + } + + pub mod size { + pub static ALLOCATION_SIZE: &str = "size"; + pub static BLOCK_SIZE: &str = "block-size"; + pub static HUMAN_READABLE: &str = "human-readable"; + pub static SI: &str = "si"; + pub static KIBIBYTES: &str = "kibibytes"; + } + + pub mod quoting { + pub static ESCAPE: &str = "escape"; + pub static LITERAL: &str = "literal"; + pub static C: &str = "quote-name"; + } -pub use config::{Config, options}; -pub use display::Format; + pub mod indicator_style { + pub static SLASH: &str = "p"; + pub static FILE_TYPE: &str = "file-type"; + pub static CLASSIFY: &str = "classify"; + } -use colors::StyleManager; -use config::options::QUOTING_STYLE; -use config::{Dereference, Files, Sort}; -use dired::DiredOutput; -use display::{display_items, display_size, should_display, show_dir_name}; + pub mod dereference { + pub static ALL: &str = "dereference"; + pub static ARGS: &str = "dereference-command-line"; + pub static DIR_ARGS: &str = "dereference-command-line-symlink-to-dir"; + } + + pub static HELP: &str = "help"; + pub static QUOTING_STYLE: &str = "quoting-style"; + pub static HIDE_CONTROL_CHARS: &str = "hide-control-chars"; + pub static SHOW_CONTROL_CHARS: &str = "show-control-chars"; + pub static WIDTH: &str = "width"; + pub static AUTHOR: &str = "author"; + pub static NO_GROUP: &str = "no-group"; + pub static FORMAT: &str = "format"; + pub static SORT: &str = "sort"; + pub static TIME: &str = "time"; + pub static IGNORE_BACKUPS: &str = "ignore-backups"; + pub static DIRECTORY: &str = "directory"; + pub static INODE: &str = "inode"; + pub static REVERSE: &str = "reverse"; + pub static RECURSIVE: &str = "recursive"; + pub static COLOR: &str = "color"; + pub static PATHS: &str = "paths"; + pub static INDICATOR_STYLE: &str = "indicator-style"; + pub static TIME_STYLE: &str = "time-style"; + pub static FULL_TIME: &str = "full-time"; + pub static HIDE: &str = "hide"; + pub static IGNORE: &str = "ignore"; + pub static CONTEXT: &str = "context"; + pub static GROUP_DIRECTORIES_FIRST: &str = "group-directories-first"; + pub static ZERO: &str = "zero"; + pub static DIRED: &str = "dired"; + pub static HYPERLINK: &str = "hyperlink"; +} + +const DEFAULT_TERM_WIDTH: u16 = 80; +const POSIXLY_CORRECT_BLOCK_SIZE: u64 = 512; +const DEFAULT_BLOCK_SIZE: u64 = 1024; +const DEFAULT_FILE_SIZE_BLOCK_SIZE: u64 = 1; #[derive(Error, Debug)] enum LsError { @@ -117,6 +229,1026 @@ impl UError for LsError { } } +#[derive(PartialEq, Eq, Debug)] +pub enum Format { + Columns, + Long, + OneLine, + Across, + Commas, +} + +#[derive(PartialEq, Eq)] +enum Sort { + None, + Name, + Size, + Time, + Version, + Extension, + Width, +} + +#[derive(PartialEq, Eq)] +enum Files { + All, + AlmostAll, + Normal, +} + +fn parse_time_style(options: &clap::ArgMatches) -> Result<(String, Option), LsError> { + // TODO: Using correct locale string is not implemented. + const LOCALE_FORMAT: (&str, Option<&str>) = ("%b %e %H:%M", Some("%b %e %Y")); + + // Convert time_styles references to owned String/option. + #[expect(clippy::unnecessary_wraps, reason = "internal result helper")] + fn ok((recent, older): (&str, Option<&str>)) -> Result<(String, Option), LsError> { + Ok((recent.to_string(), older.map(String::from))) + } + + if let Some(field) = options + .get_one::(options::TIME_STYLE) + .map(ToOwned::to_owned) + .or_else(|| std::env::var("TIME_STYLE").ok()) + { + //If both FULL_TIME and TIME_STYLE are present + //The one added last is dominant + if options.get_flag(options::FULL_TIME) + && options.indices_of(options::FULL_TIME).unwrap().next_back() + > options.indices_of(options::TIME_STYLE).unwrap().next_back() + { + ok((format::FULL_ISO, None)) + } else { + let field = if let Some(field) = field.strip_prefix("posix-") { + // See GNU documentation, set format to "locale" if LC_TIME="POSIX", + // else just strip the prefix and continue (even "posix+FORMAT" is + // supported). + // TODO: This needs to be moved to uucore and handled by icu? + if std::env::var_os("LC_TIME").as_deref() == Some(OsStr::new("POSIX")) + || std::env::var_os("LC_ALL").as_deref() == Some(OsStr::new("POSIX")) + { + return ok(LOCALE_FORMAT); + } + field + } else { + &field + }; + + match field { + "full-iso" => ok((format::FULL_ISO, None)), + "long-iso" => ok((format::LONG_ISO, None)), + // ISO older format needs extra padding. + "iso" => Ok(( + "%m-%d %H:%M".to_string(), + Some(format::ISO.to_string() + " "), + )), + "locale" => ok(LOCALE_FORMAT), + _ => match field.chars().next().unwrap() { + '+' => { + // recent/older formats are (optionally) separated by a newline + let mut it = field[1..].split('\n'); + let recent = it.next().unwrap_or_default(); + let older = it.next(); + match it.next() { + None => ok((recent, older)), + Some(_) => Err(LsError::TimeStyleParseError(String::from(field))), + } + } + _ => Err(LsError::TimeStyleParseError(String::from(field))), + }, + } + } + } else if options.get_flag(options::FULL_TIME) { + ok((format::FULL_ISO, None)) + } else { + ok(LOCALE_FORMAT) + } +} + +enum Dereference { + None, + DirArgs, + Args, + All, +} + +#[derive(PartialEq, Eq)] +enum IndicatorStyle { + None, + Slash, + FileType, + Classify, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum LocaleQuoting { + Single, + Double, +} + +pub struct Config { + // Dir and vdir needs access to this field + pub format: Format, + files: Files, + sort: Sort, + recursive: bool, + reverse: bool, + dereference: Dereference, + ignore_patterns: Vec, + size_format: SizeFormat, + directory: bool, + time: MetadataTimeField, + #[cfg(unix)] + inode: bool, + color: Option, + long: LongFormat, + alloc_size: bool, + file_size_block_size: u64, + #[allow(dead_code)] + block_size: u64, // is never read on Windows + width: u16, + // Dir and vdir needs access to this field + pub quoting_style: QuotingStyle, + locale_quoting: Option, + indicator_style: IndicatorStyle, + time_format_recent: String, // Time format for recent dates + time_format_older: Option, // Time format for older dates (optional, if not present, time_format_recent is used) + context: bool, + #[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))] + selinux_supported: bool, + #[cfg(all(feature = "smack", target_os = "linux"))] + smack_supported: bool, + group_directories_first: bool, + line_ending: LineEnding, + dired: bool, + hyperlink: bool, + tab_size: usize, +} + +// Fields that can be removed or added to the long format +struct LongFormat { + author: bool, + group: bool, + owner: bool, + #[cfg(unix)] + numeric_uid_gid: bool, +} + +struct PaddingCollection { + #[cfg(unix)] + inode: usize, + link_count: usize, + uname: usize, + group: usize, + context: usize, + size: usize, + #[cfg(unix)] + major: usize, + #[cfg(unix)] + minor: usize, + block_size: usize, +} + +struct DisplayItemName { + displayed: OsString, + dired_name_len: usize, +} + +/// Extracts the format to display the information based on the options provided. +/// +/// # Returns +/// +/// A tuple containing the Format variant and an Option containing a &'static str +/// which corresponds to the option used to define the format. +fn extract_format(options: &clap::ArgMatches) -> (Format, Option<&'static str>) { + if let Some(format_) = options.get_one::(options::FORMAT) { + ( + match format_.as_str() { + "long" | "verbose" => Format::Long, + "single-column" => Format::OneLine, + "columns" | "vertical" => Format::Columns, + "across" | "horizontal" => Format::Across, + "commas" => Format::Commas, + // below should never happen as clap already restricts the values. + _ => unreachable!("Invalid field for --format"), + }, + Some(options::FORMAT), + ) + } else if options.get_flag(options::format::LONG) { + (Format::Long, Some(options::format::LONG)) + } else if options.get_flag(options::format::ACROSS) { + (Format::Across, Some(options::format::ACROSS)) + } else if options.get_flag(options::format::COMMAS) { + (Format::Commas, Some(options::format::COMMAS)) + } else if options.get_flag(options::format::COLUMNS) { + (Format::Columns, Some(options::format::COLUMNS)) + } else if stdout().is_terminal() { + (Format::Columns, None) + } else { + (Format::OneLine, None) + } +} + +/// Extracts the type of files to display +/// +/// # Returns +/// +/// A Files variant representing the type of files to display. +fn extract_files(options: &clap::ArgMatches) -> Files { + let get_last_index = |flag: &str| -> usize { + if options.value_source(flag) == Some(clap::parser::ValueSource::CommandLine) { + options.index_of(flag).unwrap_or(0) + } else { + 0 + } + }; + + let all_index = get_last_index(options::files::ALL); + let almost_all_index = get_last_index(options::files::ALMOST_ALL); + let unsorted_all_index = get_last_index(options::files::UNSORTED_ALL); + + let max_index = all_index.max(almost_all_index).max(unsorted_all_index); + + if max_index == 0 { + Files::Normal + } else if max_index == almost_all_index { + Files::AlmostAll + } else { + // Either -a or -f wins, both show all files + Files::All + } +} + +/// Extracts the sorting method to use based on the options provided. +/// +/// # Returns +/// +/// A Sort variant representing the sorting method to use. +fn extract_sort(options: &clap::ArgMatches) -> Sort { + let get_last_index = |flag: &str| -> usize { + if options.value_source(flag) == Some(clap::parser::ValueSource::CommandLine) { + options.index_of(flag).unwrap_or(0) + } else { + 0 + } + }; + + let sort_index = options + .get_one::(options::SORT) + .and_then(|_| options.indices_of(options::SORT)) + .map_or(0, |mut indices| indices.next_back().unwrap_or(0)); + let time_index = get_last_index(options::sort::TIME); + let size_index = get_last_index(options::sort::SIZE); + let none_index = get_last_index(options::sort::NONE); + let version_index = get_last_index(options::sort::VERSION); + let extension_index = get_last_index(options::sort::EXTENSION); + let unsorted_all_index = get_last_index(options::files::UNSORTED_ALL); + + let max_sort_index = sort_index + .max(time_index) + .max(size_index) + .max(none_index) + .max(version_index) + .max(extension_index) + .max(unsorted_all_index); + + match max_sort_index { + 0 => { + // No sort flags specified, use default behavior + if !options.get_flag(options::format::LONG) + && (options.get_flag(options::time::ACCESS) + || options.get_flag(options::time::CHANGE) + || options.get_one::(options::TIME).is_some()) + { + Sort::Time + } else { + Sort::Name + } + } + idx if idx == unsorted_all_index || idx == none_index => Sort::None, + idx if idx == sort_index => { + if let Some(field) = options.get_one::(options::SORT) { + match field.as_str() { + "none" => Sort::None, + "name" => Sort::Name, + "time" => Sort::Time, + "size" => Sort::Size, + "version" => Sort::Version, + "extension" => Sort::Extension, + "width" => Sort::Width, + _ => unreachable!("Invalid field for --sort"), + } + } else { + Sort::Name + } + } + idx if idx == time_index => Sort::Time, + idx if idx == size_index => Sort::Size, + idx if idx == version_index => Sort::Version, + idx if idx == extension_index => Sort::Extension, + _ => Sort::Name, + } +} + +/// Extracts the time to use based on the options provided. +/// +/// # Returns +/// +/// A `MetadataTimeField` variant representing the time to use. +fn extract_time(options: &clap::ArgMatches) -> MetadataTimeField { + if let Some(field) = options.get_one::(options::TIME) { + field.as_str().into() + } else if options.get_flag(options::time::ACCESS) { + MetadataTimeField::Access + } else if options.get_flag(options::time::CHANGE) { + MetadataTimeField::Change + } else { + MetadataTimeField::Modification + } +} + +/// Some env variables can be passed +/// For now, we are only verifying if empty or not and known for `TERM` +fn is_color_compatible_term() -> bool { + let is_term_set = std::env::var("TERM").is_ok(); + let is_colorterm_set = std::env::var("COLORTERM").is_ok(); + + let term = std::env::var("TERM").unwrap_or_default(); + let colorterm = std::env::var("COLORTERM").unwrap_or_default(); + + // Search function in the TERM struct to manage the wildcards + let term_matches = |term: &str| -> bool { + uucore::colors::TERMS.iter().any(|&pattern| { + term == pattern + || (pattern.ends_with('*') && term.starts_with(&pattern[..pattern.len() - 1])) + }) + }; + + if is_term_set && term.is_empty() && is_colorterm_set && colorterm.is_empty() { + return false; + } + + if !term.is_empty() && !term_matches(&term) { + return false; + } + true +} + +/// Extracts the color option to use based on the options provided. +/// +/// # Returns +/// +/// A boolean representing whether or not to use color. +fn extract_color(options: &clap::ArgMatches) -> bool { + if !is_color_compatible_term() { + return false; + } + + let get_last_index = |flag: &str| -> usize { + if options.value_source(flag) == Some(clap::parser::ValueSource::CommandLine) { + options.index_of(flag).unwrap_or(0) + } else { + 0 + } + }; + + let color_index = options + .get_one::(options::COLOR) + .and_then(|_| options.indices_of(options::COLOR)) + .map_or(0, |mut indices| indices.next_back().unwrap_or(0)); + let unsorted_all_index = get_last_index(options::files::UNSORTED_ALL); + + let color_enabled = match options.get_one::(options::COLOR) { + None => options.contains_id(options::COLOR), + Some(val) => match val.as_str() { + "" | "always" | "yes" | "force" => true, + "auto" | "tty" | "if-tty" => stdout().is_terminal(), + /* "never" | "no" | "none" | */ _ => false, + }, + }; + + // If --color was explicitly specified, always honor it regardless of -f + // Otherwise, if -f is present without explicit color, disable color + if color_index > 0 { + // Color was explicitly specified + color_enabled + } else if unsorted_all_index > 0 { + // -f present without explicit color, disable implicit color + false + } else { + color_enabled + } +} + +/// Extracts the hyperlink option to use based on the options provided. +/// +/// # Returns +/// +/// A boolean representing whether to hyperlink files. +fn extract_hyperlink(options: &clap::ArgMatches) -> bool { + let hyperlink = options + .get_one::(options::HYPERLINK) + .unwrap() + .as_str(); + + match hyperlink { + "always" | "yes" | "force" => true, + "auto" | "tty" | "if-tty" => stdout().is_terminal(), + "never" | "no" | "none" => false, + _ => unreachable!("should be handled by clap"), + } +} + +/// Match the argument given to --quoting-style or the [`QUOTING_STYLE`] env variable. +/// +/// # Arguments +/// +/// * `style`: the actual argument string +/// * `show_control` - A boolean value representing whether to show control characters. +/// +/// # Returns +/// +/// * An option with None if the style string is invalid, or a `QuotingStyle` wrapped in `Some`. +struct QuotingStyleSpec { + style: QuotingStyle, + fixed_control: bool, + locale: Option, +} + +impl QuotingStyleSpec { + fn new(style: QuotingStyle) -> Self { + Self { + style, + fixed_control: false, + locale: None, + } + } + + fn with_locale(style: QuotingStyle, locale: LocaleQuoting) -> Self { + Self { + style, + fixed_control: true, + locale: Some(locale), + } + } +} + +fn match_quoting_style_name( + style: &str, + show_control: bool, +) -> Option<(QuotingStyle, Option)> { + let spec = match style { + "literal" => QuotingStyleSpec::new(QuotingStyle::Literal { + show_control: false, + }), + "shell" => QuotingStyleSpec::new(QuotingStyle::SHELL), + "shell-always" => QuotingStyleSpec::new(QuotingStyle::SHELL_QUOTE), + "shell-escape" => QuotingStyleSpec::new(QuotingStyle::SHELL_ESCAPE), + "shell-escape-always" => QuotingStyleSpec::new(QuotingStyle::SHELL_ESCAPE_QUOTE), + "c" => QuotingStyleSpec::new(QuotingStyle::C_DOUBLE), + "escape" => QuotingStyleSpec::new(QuotingStyle::C_NO_QUOTES), + "locale" => QuotingStyleSpec { + style: QuotingStyle::Literal { + show_control: false, + }, + fixed_control: true, + locale: Some(LocaleQuoting::Single), + }, + "clocale" => QuotingStyleSpec::with_locale(QuotingStyle::C_DOUBLE, LocaleQuoting::Double), + _ => return None, + }; + + let style = if spec.fixed_control { + spec.style + } else { + spec.style.show_control(show_control) + }; + + Some((style, spec.locale)) +} + +/// Extracts the quoting style to use based on the options provided. +/// If no options are given, it looks if a default quoting style is provided +/// through the [`QUOTING_STYLE`] environment variable. +/// +/// # Arguments +/// +/// * `options` - A reference to a [`clap::ArgMatches`] object containing command line arguments. +/// * `show_control` - A boolean value representing whether or not to show control characters. +/// +/// # Returns +/// +/// A [`QuotingStyle`] variant representing the quoting style to use. +fn extract_quoting_style( + options: &clap::ArgMatches, + show_control: bool, +) -> (QuotingStyle, Option) { + let opt_quoting_style = options.get_one::(QUOTING_STYLE); + + if let Some(style) = opt_quoting_style { + match match_quoting_style_name(style, show_control) { + Some(pair) => pair, + None => unreachable!("Should have been caught by Clap"), + } + } else if options.get_flag(options::quoting::LITERAL) { + (QuotingStyle::Literal { show_control }, None) + } else if options.get_flag(options::quoting::ESCAPE) { + (QuotingStyle::C_NO_QUOTES, None) + } else if options.get_flag(options::quoting::C) { + (QuotingStyle::C_DOUBLE, None) + } else if options.get_flag(options::DIRED) { + (QuotingStyle::Literal { show_control }, None) + } else { + // If set, the QUOTING_STYLE environment variable specifies a default style. + if let Ok(style) = std::env::var("QUOTING_STYLE") { + match match_quoting_style_name(style.as_str(), show_control) { + Some(pair) => return pair, + None => eprintln!( + "{}", + translate!("ls-invalid-quoting-style", "program" => std::env::args().next().unwrap_or_else(|| "ls".to_string()), "style" => style.clone()) + ), + } + } + + // By default, `ls` uses Shell escape quoting style when writing to a terminal file + // descriptor and Literal otherwise. + if stdout().is_terminal() { + (QuotingStyle::SHELL_ESCAPE.show_control(show_control), None) + } else { + (QuotingStyle::Literal { show_control }, None) + } + } +} + +/// Extracts the indicator style to use based on the options provided. +/// +/// # Returns +/// +/// An [`IndicatorStyle`] variant representing the indicator style to use. +fn extract_indicator_style(options: &clap::ArgMatches) -> IndicatorStyle { + if let Some(field) = options.get_one::(options::INDICATOR_STYLE) { + match field.as_str() { + "none" => IndicatorStyle::None, + "file-type" => IndicatorStyle::FileType, + "classify" => IndicatorStyle::Classify, + "slash" => IndicatorStyle::Slash, + &_ => IndicatorStyle::None, + } + } else if let Some(field) = options.get_one::(options::indicator_style::CLASSIFY) { + match field.as_str() { + "never" | "no" | "none" => IndicatorStyle::None, + "always" | "yes" | "force" => IndicatorStyle::Classify, + "auto" | "tty" | "if-tty" => { + if stdout().is_terminal() { + IndicatorStyle::Classify + } else { + IndicatorStyle::None + } + } + &_ => IndicatorStyle::None, + } + } else if options.get_flag(options::indicator_style::SLASH) { + IndicatorStyle::Slash + } else if options.get_flag(options::indicator_style::FILE_TYPE) { + IndicatorStyle::FileType + } else { + IndicatorStyle::None + } +} + +/// Parses the width value from either the command line arguments or the environment variables. +fn parse_width(width_match: Option<&String>) -> Result { + let parse_width_from_args = |s: &str| -> Result { + let radix = if s.starts_with('0') && s.len() > 1 { + 8 + } else { + 10 + }; + match u16::from_str_radix(s, radix) { + Ok(x) => Ok(x), + Err(e) => match e.kind() { + IntErrorKind::PosOverflow => Ok(u16::MAX), + _ => Err(LsError::InvalidLineWidth(s.into())), + }, + } + }; + + let parse_width_from_env = |columns: OsString| { + if let Some(columns) = columns.to_str().and_then(|s| s.parse().ok()) { + columns + } else { + show_error!( + "{}", + translate!("ls-invalid-columns-width", "width" => columns.quote()) + ); + DEFAULT_TERM_WIDTH + } + }; + + let calculate_term_size = || match terminal_size::terminal_size() { + Some((width, _)) => width.0, + None => DEFAULT_TERM_WIDTH, + }; + + let ret = match width_match { + Some(x) => parse_width_from_args(x)?, + None => match std::env::var_os("COLUMNS") { + Some(columns) => parse_width_from_env(columns), + None => calculate_term_size(), + }, + }; + + Ok(ret) +} + +impl Config { + #[allow(clippy::cognitive_complexity)] + pub fn from(options: &clap::ArgMatches) -> UResult { + let context = options.get_flag(options::CONTEXT); + let (mut format, opt) = extract_format(options); + let files = extract_files(options); + + // The -o, -n and -g options are tricky. They cannot override with each + // other because it's possible to combine them. For example, the option + // -og should hide both owner and group. Furthermore, they are not + // reset if -l or --format=long is used. So these should just show the + // group: -gl or "-g --format=long". Finally, they are also not reset + // when switching to a different format option in-between like this: + // -ogCl or "-og --format=vertical --format=long". + // + // -1 has a similar issue: it does nothing if the format is long. This + // actually makes it distinct from the --format=singe-column option, + // which always applies. + // + // The idea here is to not let these options override with the other + // options, but manually whether they have an index that's greater than + // the other format options. If so, we set the appropriate format. + if format != Format::Long { + let idx = opt + .and_then(|opt| options.indices_of(opt).map(|x| x.max().unwrap())) + .unwrap_or(0); + if [ + options::format::LONG_NO_OWNER, + options::format::LONG_NO_GROUP, + options::format::LONG_NUMERIC_UID_GID, + options::FULL_TIME, + ] + .iter() + .filter_map(|opt| { + if options.value_source(opt) == Some(clap::parser::ValueSource::CommandLine) { + options.indices_of(opt) + } else { + None + } + }) + .flatten() + .any(|i| i >= idx) + { + format = Format::Long; + } else if let Some(mut indices) = options.indices_of(options::format::ONE_LINE) { + if options.value_source(options::format::ONE_LINE) + == Some(clap::parser::ValueSource::CommandLine) + && indices.any(|i| i > idx) + { + format = Format::OneLine; + } + } + } + + let sort = extract_sort(options); + let time = extract_time(options); + let mut needs_color = extract_color(options); + let hyperlink = extract_hyperlink(options); + + let opt_block_size = options.get_one::(options::size::BLOCK_SIZE); + let opt_si = opt_block_size.is_some() + && options + .get_one::(options::size::BLOCK_SIZE) + .unwrap() + .eq("si") + || options.get_flag(options::size::SI); + let opt_hr = (opt_block_size.is_some() + && options + .get_one::(options::size::BLOCK_SIZE) + .unwrap() + .eq("human-readable")) + || options.get_flag(options::size::HUMAN_READABLE); + let opt_kb = options.get_flag(options::size::KIBIBYTES); + + let size_format = if opt_si { + SizeFormat::Decimal + } else if opt_hr { + SizeFormat::Binary + } else { + SizeFormat::Bytes + }; + + let env_var_blocksize = std::env::var_os("BLOCKSIZE"); + let env_var_block_size = std::env::var_os("BLOCK_SIZE"); + let env_var_ls_block_size = std::env::var_os("LS_BLOCK_SIZE"); + let env_var_posixly_correct = std::env::var_os("POSIXLY_CORRECT"); + let mut is_env_var_blocksize = false; + + let raw_block_size = if let Some(opt_block_size) = opt_block_size { + OsString::from(opt_block_size) + } else if let Some(env_var_ls_block_size) = env_var_ls_block_size { + env_var_ls_block_size + } else if let Some(env_var_block_size) = env_var_block_size { + env_var_block_size + } else if let Some(env_var_blocksize) = env_var_blocksize { + is_env_var_blocksize = true; + env_var_blocksize + } else { + OsString::from("") + }; + + let (file_size_block_size, block_size) = if !opt_si && !opt_hr && !raw_block_size.is_empty() + { + if let Ok(size) = parse_size_non_zero_u64(&raw_block_size.to_string_lossy()) { + match (is_env_var_blocksize, opt_kb) { + (true, true) => (DEFAULT_FILE_SIZE_BLOCK_SIZE, DEFAULT_BLOCK_SIZE), + (true, false) => (DEFAULT_FILE_SIZE_BLOCK_SIZE, size), + (false, true) => { + // --block-size overrides -k + if opt_block_size.is_some() { + (size, size) + } else { + (size, DEFAULT_BLOCK_SIZE) + } + } + (false, false) => (size, size), + } + } else { + // only fail if invalid block size was specified with --block-size, + // ignore invalid block size from env vars + if let Some(invalid_block_size) = opt_block_size { + return Err(Box::new(LsError::BlockSizeParseError( + invalid_block_size.clone(), + ))); + } + if is_env_var_blocksize { + (DEFAULT_FILE_SIZE_BLOCK_SIZE, DEFAULT_BLOCK_SIZE) + } else { + (DEFAULT_BLOCK_SIZE, DEFAULT_BLOCK_SIZE) + } + } + } else if env_var_posixly_correct.is_some() { + if opt_kb { + (DEFAULT_FILE_SIZE_BLOCK_SIZE, DEFAULT_BLOCK_SIZE) + } else { + (DEFAULT_FILE_SIZE_BLOCK_SIZE, POSIXLY_CORRECT_BLOCK_SIZE) + } + } else if opt_si { + (DEFAULT_FILE_SIZE_BLOCK_SIZE, 1000) + } else { + (DEFAULT_FILE_SIZE_BLOCK_SIZE, DEFAULT_BLOCK_SIZE) + }; + + let long = { + let author = options.get_flag(options::AUTHOR); + let group = !options.get_flag(options::NO_GROUP) + && !options.get_flag(options::format::LONG_NO_GROUP); + let owner = !options.get_flag(options::format::LONG_NO_OWNER); + #[cfg(unix)] + let numeric_uid_gid = options.get_flag(options::format::LONG_NUMERIC_UID_GID); + LongFormat { + author, + group, + owner, + #[cfg(unix)] + numeric_uid_gid, + } + }; + let width = parse_width(options.get_one::(options::WIDTH))?; + + #[allow(clippy::needless_bool)] + let mut show_control = if options.get_flag(options::HIDE_CONTROL_CHARS) { + false + } else if options.get_flag(options::SHOW_CONTROL_CHARS) { + true + } else { + !stdout().is_terminal() + }; + + let (mut quoting_style, mut locale_quoting) = extract_quoting_style(options, show_control); + let indicator_style = extract_indicator_style(options); + // Only parse the value to "--time-style" if it will become relevant. + let dired = options.get_flag(options::DIRED); + let (time_format_recent, time_format_older) = if format == Format::Long || dired { + parse_time_style(options)? + } else { + Default::default() + }; + + let mut ignore_patterns: Vec = Vec::new(); + + if options.get_flag(options::IGNORE_BACKUPS) { + ignore_patterns.push(Pattern::new("*~").unwrap()); + ignore_patterns.push(Pattern::new(".*~").unwrap()); + } + + for pattern in options + .get_many::(options::IGNORE) + .into_iter() + .flatten() + { + if let Ok(p) = parse_glob::from_str(pattern) { + ignore_patterns.push(p); + } else { + show_warning!( + "{}", + translate!("ls-invalid-ignore-pattern", "pattern" => pattern.quote()) + ); + } + } + + if files == Files::Normal { + for pattern in options + .get_many::(options::HIDE) + .into_iter() + .flatten() + { + if let Ok(p) = parse_glob::from_str(pattern) { + ignore_patterns.push(p); + } else { + show_warning!( + "{}", + translate!("ls-invalid-hide-pattern", "pattern" => pattern.quote()) + ); + } + } + } + + // According to ls info page, `--zero` implies the following flags: + // - `--show-control-chars` + // - `--format=single-column` + // - `--color=none` + // - `--quoting-style=literal` + // Current GNU ls implementation allows `--zero` Behavior to be + // overridden by later flags. + let zero_formats_opts = [ + options::format::ACROSS, + options::format::COLUMNS, + options::format::COMMAS, + options::format::LONG, + options::format::LONG_NO_GROUP, + options::format::LONG_NO_OWNER, + options::format::LONG_NUMERIC_UID_GID, + options::format::ONE_LINE, + options::FORMAT, + ]; + let zero_colors_opts = [options::COLOR]; + let zero_show_control_opts = [options::HIDE_CONTROL_CHARS, options::SHOW_CONTROL_CHARS]; + let zero_quoting_style_opts = [ + QUOTING_STYLE, + options::quoting::C, + options::quoting::ESCAPE, + options::quoting::LITERAL, + ]; + let get_last = |flag: &str| -> usize { + if options.value_source(flag) == Some(clap::parser::ValueSource::CommandLine) { + options.index_of(flag).unwrap_or(0) + } else { + 0 + } + }; + if get_last(options::ZERO) + > zero_formats_opts + .into_iter() + .map(get_last) + .max() + .unwrap_or(0) + { + format = if format == Format::Long { + format + } else { + Format::OneLine + }; + } + if get_last(options::ZERO) + > zero_colors_opts + .into_iter() + .map(get_last) + .max() + .unwrap_or(0) + { + needs_color = false; + } + if get_last(options::ZERO) + > zero_show_control_opts + .into_iter() + .map(get_last) + .max() + .unwrap_or(0) + { + show_control = true; + } + if get_last(options::ZERO) + > zero_quoting_style_opts + .into_iter() + .map(get_last) + .max() + .unwrap_or(0) + { + quoting_style = QuotingStyle::Literal { show_control }; + locale_quoting = None; + } + + if needs_color { + if let Err(err) = validate_ls_colors_env() { + if let LsColorsParseError::UnrecognizedPrefix(prefix) = &err { + show_warning!( + "{}", + translate!( + "ls-warning-unrecognized-ls-colors-prefix", + "prefix" => prefix.quote() + ) + ); + } + show_warning!("{}", translate!("ls-warning-unparsable-ls-colors")); + needs_color = false; + } + } + + let color = if needs_color { + Some(LsColors::from_env().unwrap_or_default()) + } else { + None + }; + + if dired || is_dired_arg_present() { + // --dired implies --format=long + // if we have --dired --hyperlink, we don't show dired but we still want to see the + // long format + format = Format::Long; + } + if dired && options.get_flag(options::ZERO) { + return Err(Box::new(LsError::DiredAndZeroAreIncompatible)); + } + + let dereference = if options.get_flag(options::dereference::ALL) { + Dereference::All + } else if options.get_flag(options::dereference::ARGS) { + Dereference::Args + } else if options.get_flag(options::dereference::DIR_ARGS) { + Dereference::DirArgs + } else if options.get_flag(options::DIRECTORY) + || indicator_style == IndicatorStyle::Classify + || format == Format::Long + { + Dereference::None + } else { + Dereference::DirArgs + }; + + let tab_size = if needs_color { + Some(0) + } else { + options + .get_one::(options::format::TAB_SIZE) + .and_then(|size| size.parse::().ok()) + .or_else(|| std::env::var("TABSIZE").ok().and_then(|s| s.parse().ok())) + } + .unwrap_or(SPACES_IN_TAB); + + Ok(Self { + format, + files, + sort, + recursive: options.get_flag(options::RECURSIVE), + reverse: options.get_flag(options::REVERSE), + dereference, + ignore_patterns, + size_format, + directory: options.get_flag(options::DIRECTORY), + time, + color, + #[cfg(unix)] + inode: options.get_flag(options::INODE), + long, + alloc_size: options.get_flag(options::size::ALLOCATION_SIZE), + file_size_block_size, + block_size, + width, + quoting_style, + locale_quoting, + indicator_style, + time_format_recent, + time_format_older, + context, + #[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))] + selinux_supported: uucore::selinux::is_selinux_enabled(), + #[cfg(all(feature = "smack", target_os = "linux"))] + smack_supported: uucore::smack::is_smack_enabled(), + group_directories_first: options.get_flag(options::GROUP_DIRECTORIES_FIRST), + line_ending: LineEnding::from_zero_flag(options.get_flag(options::ZERO)), + dired, + hyperlink, + tab_size, + }) + } +} + #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uucore::clap_localization::handle_clap_result_with_exit_code(uu_app(), args, 2)?; @@ -925,14 +2057,106 @@ 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 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 +/// or the recursive flag is passed. +/// +/// ```no-exec +/// $ ls -R +/// .: <- This is printed by this function +/// dir1 file1 file2 +/// +/// dir1: <- This as well +/// file11 +/// ``` +fn show_dir_name( + path_data: &PathData, + out: &mut BufWriter, + config: &Config, +) -> std::io::Result<()> { + let escaped_name = escape_dir_name_with_locale(path_data.path().as_os_str(), config); + + let name = if config.hyperlink && !config.dired { + create_hyperlink(&escaped_name, path_data) + } else { + escaped_name + }; + + write_os_str(out, &name)?; + write!(out, ":") +} + +fn escape_with_locale(name: &OsStr, config: &Config, fallback: F) -> OsString +where + F: FnOnce(&OsStr, QuotingStyle) -> OsString, +{ + if let Some(locale) = config.locale_quoting { + locale_quote(name, locale) + } else { + fallback(name, config.quoting_style) + } +} + +fn escape_dir_name_with_locale(name: &OsStr, config: &Config) -> OsString { + escape_with_locale(name, config, locale_aware_escape_dir_name) +} + +fn escape_name_with_locale(name: &OsStr, config: &Config) -> OsString { + escape_with_locale(name, config, locale_aware_escape_name) +} + +fn locale_quote(name: &OsStr, style: LocaleQuoting) -> OsString { + let bytes = os_str_as_bytes_lossy(name); + let mut quoted = String::new(); + match style { + LocaleQuoting::Single => quoted.push('\''), + LocaleQuoting::Double => quoted.push('"'), + } + for &byte in bytes.as_ref() { + push_locale_byte(&mut quoted, byte, style); } - fn metadata(&self) -> Option { - self.metadata().cloned() + match style { + LocaleQuoting::Single => quoted.push('\''), + LocaleQuoting::Double => quoted.push('"'), } - fn path(&self) -> PathBuf { - self.path().to_path_buf() + OsString::from(quoted) +} + +fn push_locale_byte(buf: &mut String, byte: u8, style: LocaleQuoting) { + match (style, byte) { + (LocaleQuoting::Single, b'\'') => buf.push_str("'\\''"), + (LocaleQuoting::Double, b'"') => buf.push_str("\\\""), + (_, b'\\') => buf.push_str("\\\\"), + _ => push_basic_escape(buf, byte), + } +} + +fn push_basic_escape(buf: &mut String, byte: u8) { + match byte { + b'\x07' => buf.push_str("\\a"), + b'\x08' => buf.push_str("\\b"), + b'\t' => buf.push_str("\\t"), + b'\n' => buf.push_str("\\n"), + b'\x0b' => buf.push_str("\\v"), + b'\x0c' => buf.push_str("\\f"), + b'\r' => buf.push_str("\\r"), + b'\x1b' => buf.push_str("\\e"), + b'"' => buf.push('"'), + b'\'' => buf.push('\''), + b if (0x20..=0x7e).contains(&b) => buf.push(b as char), + _ => { + let _ = write!(buf, "\\{byte:03o}"); + } } } @@ -2121,74 +3345,102 @@ fn classify_file(path: &PathData) -> Option { } } -fn depth_first_list( - (dir_path, needs_blank_line): DirData, - mut read_dir: ReadDir, +/// Takes a [`PathData`] struct and returns a cell with a name ready for displaying. +/// +/// This function relies on the following parameters in the provided `&Config`: +/// * `config.quoting_style` to decide how we will escape `name` using [`locale_aware_escape_name`]. +/// * `config.inode` decides whether to display inode numbers beside names using [`get_inode`]. +/// * `config.color` decides whether it's going to color `name` using [`color_name`]. +/// * `config.indicator_style` to append specific characters to `name` using [`classify_file`]. +/// * `config.format` to display symlink targets if `Format::Long`. This function is also +/// responsible for coloring symlink target names if `config.color` is specified. +/// * `config.context` to prepend security context to `name` if compiled with `feat_selinux`. +/// * `config.hyperlink` decides whether to hyperlink the item +/// +/// Note that non-unicode sequences in symlink targets are dealt with using +/// [`std::path::Path::to_string_lossy`]. +#[allow(clippy::cognitive_complexity)] +fn display_item_name( + path: &PathData, config: &Config, + prefix_context: Option, + more_info: Option, state: &mut ListState, - dired: &mut DiredOutput, - is_top_level: bool, -) -> UResult<()> { - let path_data = PathData::new(dir_path, None, None, config, false); + current_column: LazyCell usize + '_>>, +) -> DisplayItemName { + // This is our return value. We start by `&path.display_name` and modify it along the way. + let mut name = escape_name_with_locale(path.display_name(), config); - // Print dir heading - name... 'total' comes after error display - if state.initial_locs_len > 1 || config.recursive { - if is_top_level { - if needs_blank_line { - writeln!(state.out)?; - if config.dired { - dired.padding += 1; + let is_wrap = + |namelen: usize| config.width != 0 && *current_column + namelen > config.width.into(); + + if config.hyperlink { + name = create_hyperlink(&name, path); + } + + if let Some(style_manager) = &mut state.style_manager { + let len = name.len(); + name = color_name(name, path, style_manager, None, is_wrap(len)); + } + + 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); + + let char_opt = match config.indicator_style { + IndicatorStyle::Classify => sym, + IndicatorStyle::FileType => { + // Don't append an asterisk. + match sym { + Some('*') => None, + _ => sym, } } - if config.dired { - dired::indent(&mut state.out)?; - } - show_dir_name(&path_data, &mut state.out, config)?; - writeln!(state.out)?; - if config.dired { - let dir_len = path_data.path().as_os_str().len(); - // add the //SUBDIRED// coordinates - dired::calculate_subdired(dired, dir_len); - // Add the padding for the dir name - dired::add_dir_name(dired, dir_len); - } - } else { - writeln!(state.out)?; - if config.dired { - dired.padding += 1; - dired::indent(&mut state.out)?; - let dir_name_size = path_data.path().as_os_str().len(); - dired::calculate_subdired(dired, dir_name_size); - dired::add_dir_name(dired, dir_name_size); + IndicatorStyle::Slash => { + // Append only a slash. + match sym { + Some('/') => Some('/'), + _ => None, + } } - show_dir_name(&path_data, &mut state.out, config)?; - writeln!(state.out)?; + IndicatorStyle::None => None, + }; + + if let Some(c) = char_opt { + name.push(OsStr::new(&c.to_string())); } } - // Append entries with initial dot files and record their existence - let (ref mut buf, trim) = if config.files == Files::All { - const DOT_DIRECTORIES: usize = 2; - let v = vec![ - PathData::new( - path_data.path().to_path_buf(), - None, - Some(".".into()), - config, - false, - ), - PathData::new( - path_data.path().join(".."), - None, - Some("..".into()), - config, - false, - ), - ]; - (v, DOT_DIRECTORIES) - } else { - (Vec::new(), 0) - }; + let dired_name_len = if config.dired { name.len() } else { 0 }; + + if config.format == Format::Long + && path.file_type().is_some_and(FileType::is_symlink) + && !path.must_dereference + { + match path.path().read_link() { + Ok(target_path) => { + name.push(" -> "); + + // We might as well color the symlink output after the arrow. + // This makes extra system calls, but provides important information that + // people run `ls -l --color` are very interested in. + if let Some(style_manager) = &mut state.style_manager { + let escaped_target = escape_name_with_locale(target_path.as_os_str(), config); + // 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_path.clone(); + if target_path.is_relative() { + if let Some(parent) = path.path().parent() { + absolute_target = parent.join(absolute_target); + } + } match fs::canonicalize(&absolute_target) { Ok(resolved_target) if config.color.is_some() => { @@ -2224,125 +3476,88 @@ fn depth_first_list( } } Err(err) => { - state.out.flush()?; - show!(LsError::IOError(err)); + show!(LsError::IOErrorContext( + path.path().to_path_buf(), + err, + false + )); } } } - // Relinquish unused space since we won't need it anymore. - buf.shrink_to_fit(); - sort_entries(buf, config); + // Prepend the security context to the `name` and adjust `width` in order + // to get correct alignment from later calls to`display_grid()`. + if config.context { + if let Some(pad_count) = prefix_context { + let security_context = if matches!(config.format, Format::Commas) { + path.security_context(config).to_string() + } else { + pad_left(path.security_context(config), pad_count) + }; - if config.format == Format::Long || config.alloc_size { - let total = return_total(buf, config, &mut state.out)?; - write!(state.out, "{}", total.as_str())?; - if config.dired { - dired::add_total(dired, total.len()); + let old_name = name; + name = format!("{security_context} ").into(); + name.push(old_name); } } - display_items(buf, config, state, dired)?; + DisplayItemName { + displayed: name, + dired_name_len, + } +} - if config.recursive { - for e in buf - .iter() - .skip(trim) - .filter(|p| p.file_type().is_some_and(FileType::is_dir)) - .rev() - { - // Try to open only to report any errors in order to match GNU semantics. - if let Err(err) = fs::read_dir(e.path()) { - state.out.flush()?; - show!(LsError::IOErrorContext( - e.path().to_path_buf(), - err, - e.command_line - )); +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.path()).unwrap_or_default(); + + // Get bytes for URL encoding in a cross-platform way + let absolute_path_bytes = os_str_as_bytes_lossy(absolute_path.as_os_str()); + + // a set of safe ASCII bytes that don't need encoding + #[cfg(not(target_os = "windows"))] + let unencoded_bytes = b"_-.~/"; + #[cfg(target_os = "windows")] + let unencoded_bytes = b"_-.~/\\:"; + + // Encode at byte level to properly handle UTF-8 sequences and preserve invalid UTF-8 + let full_encoded_path: String = absolute_path_bytes + .iter() + .map(|&b: &u8| { + if b.is_ascii_alphanumeric() || unencoded_bytes.contains(&b) { + (b as char).to_string() } else { - let fi = FileInformation::from_path(e.path(), e.must_dereference)?; - if state.listed_ancestors.insert(fi) { - // Push to stack, but with a less aggressive growth curve. - let (cap, len) = (state.stack.capacity(), state.stack.len()); - if cap == len { - state.stack.reserve_exact(len / 4 + 4); - } - state.stack.push((e.path().to_path_buf(), true)); - } else { - state.out.flush()?; - show!(LsError::AlreadyListedError(e.path().to_path_buf())); - } + format!("%{b:02x}") } - } - } - Ok(()) -} + }) + .collect(); -fn get_metadata_with_deref_opt(p_buf: &Path, dereference: bool) -> std::io::Result { - if dereference { - p_buf.metadata() - } else { - p_buf.symlink_metadata() - } -} + // OSC 8 hyperlink format: \x1b]8;;URL\x1b\\TEXT\x1b]8;;\x1b\\ + // \x1b = ESC, \x1b\\ = ESC backslash + let mut ret: OsString = format!("\x1b]8;;file://{hostname}{full_encoded_path}\x1b\\").into(); + ret.push(name); + ret.push("\x1b]8;;\x1b\\"); -fn return_total( - items: &[PathData], - config: &Config, - out: &mut BufWriter, -) -> UResult { - let mut total_size = 0; - for item in items { - total_size += item - .metadata() - .as_ref() - .map_or(0, |md| get_block_size(md, config)); - } - if config.dired { - dired::indent(out)?; - } - Ok(format!( - "{}{}", - translate!("ls-total", "size" => display_size(total_size, config)), - config.line_ending - )) + ret } -#[allow(unused_variables)] -fn get_block_size(md: &Metadata, config: &Config) -> u64 { - /* GNU ls will display sizes in terms of block size - md.len() will differ from this value when the file has some holes - */ - #[cfg(unix)] - { - use uucore::format::human::SizeFormat; +#[cfg(not(unix))] +fn display_symlink_count(_metadata: &Metadata) -> String { + // Currently not sure of how to get this on Windows, so I'm punting. + // Git Bash looks like it may do the same thing. + String::from("1") +} - let raw_blocks = if md.file_type().is_char_device() || md.file_type().is_block_device() { - 0u64 - } else { - md.blocks() * 512 - }; - match config.size_format { - SizeFormat::Binary | SizeFormat::Decimal => raw_blocks, - SizeFormat::Bytes => raw_blocks / config.block_size, - } - } - #[cfg(not(unix))] - { - // no way to get block size for windows, fall-back to file size - md.len() - } +#[cfg(unix)] +fn display_symlink_count(metadata: &Metadata) -> String { + metadata.nlink().to_string() } #[cfg(unix)] -fn file_is_executable(md: &Metadata) -> bool { - // Mode always returns u32, but the flags might not be, based on the platform - // e.g. linux has u32, mac has u16. - // S_IXUSR -> user has execute permission - // S_IXGRP -> group has execute permission - // S_IXOTH -> other users have execute permission - #[allow(clippy::unnecessary_cast)] - return md.mode() & ((S_IXUSR | S_IXGRP | S_IXOTH) as u32) != 0; +fn display_inode(metadata: &Metadata) -> String { + get_inode(metadata) } /// This returns the `SELinux` security context as UTF8 `String`. @@ -2371,8 +3586,6 @@ fn get_security_context<'a>( #[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))] if config.selinux_supported { - use uucore::show_warning; - match selinux::SecurityContext::of_path(path, must_dereference, false) { Err(_r) => { // TODO: show the actual reason why it failed @@ -2425,3 +3638,125 @@ fn get_security_context<'a>( Cow::Borrowed(SUBSTITUTE_STRING) } + +#[cfg(unix)] +fn calculate_padding_collection( + items: &[PathData], + config: &Config, + state: &mut ListState, +) -> PaddingCollection { + let mut padding_collections = PaddingCollection { + inode: 1, + link_count: 1, + uname: 1, + group: 1, + context: 1, + size: 1, + major: 1, + minor: 1, + block_size: 1, + }; + + for item in items { + #[cfg(unix)] + if config.inode { + let inode_len = if let Some(md) = item.metadata() { + display_inode(md).len() + } else { + continue; + }; + padding_collections.inode = inode_len.max(padding_collections.inode); + } + + if config.alloc_size { + 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(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); + padding_collections.uname = uname_len.max(padding_collections.uname); + padding_collections.group = group_len.max(padding_collections.group); + if config.context { + padding_collections.context = context_len.max(padding_collections.context); + } + + // correctly align columns when some files have capabilities/ACLs and others do not + { + #[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()); + if context_len > 1 || is_acl_set { + padding_collections.link_count += 1; + } + } + + if items.len() == 1usize { + padding_collections.size = 0usize; + padding_collections.major = 0usize; + padding_collections.minor = 0usize; + } else { + padding_collections.major = major_len.max(padding_collections.major); + padding_collections.minor = minor_len.max(padding_collections.minor); + padding_collections.size = size_len + .max(padding_collections.size) + .max(padding_collections.major); + } + } + } + + padding_collections +} + +#[cfg(not(unix))] +fn calculate_padding_collection( + items: &[PathData], + config: &Config, + state: &mut ListState, +) -> PaddingCollection { + let mut padding_collections = PaddingCollection { + link_count: 1, + uname: 1, + group: 1, + context: 1, + size: 1, + block_size: 1, + }; + + for item in items { + if config.alloc_size { + 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(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); + padding_collections.uname = uname_len.max(padding_collections.uname); + padding_collections.group = group_len.max(padding_collections.group); + if config.context { + padding_collections.context = context_len.max(padding_collections.context); + } + padding_collections.size = size_len.max(padding_collections.size); + } + + padding_collections +} + +fn os_str_starts_with(haystack: &OsStr, needle: &[u8]) -> bool { + os_str_as_bytes_lossy(haystack).starts_with(needle) +} + +fn write_os_str(writer: &mut W, string: &OsStr) -> std::io::Result<()> { + writer.write_all(&os_str_as_bytes_lossy(string)) +} diff --git a/src/uucore/src/lib/features/checksum/mod.rs b/src/uucore/src/lib/features/checksum/mod.rs index 9433bbf3682..1f77b7e635f 100644 --- a/src/uucore/src/lib/features/checksum/mod.rs +++ b/src/uucore/src/lib/features/checksum/mod.rs @@ -115,8 +115,6 @@ impl AlgoKind { ALGORITHM_OPTIONS_SHA384 => Sha384, ALGORITHM_OPTIONS_SHA512 => Sha512, - // Extensions not in GNU as of version 9.10 - ALGORITHM_OPTIONS_BLAKE3 => Blake3, ALGORITHM_OPTIONS_SHAKE128 => Shake128, ALGORITHM_OPTIONS_SHAKE256 => Shake256, _ => return Err(ChecksumError::UnknownAlgorithm(algo.as_ref().to_string()).into()), @@ -247,11 +245,11 @@ pub enum SizedAlgoKind { Md5, Sm3, Sha1, + Blake3, Sha2(ShaLength), Sha3(ShaLength), - // Note: we store Blake*'s length as BYTES. + // Note: we store Blake2b's length as BYTES. Blake2b(Option), - Blake3(Option), // Shake* length are stored in bits. Shake128(Option), Shake256(Option), @@ -269,6 +267,7 @@ impl SizedAlgoKind { | ak::Md5 | ak::Sm3 | ak::Sha1 + | ak::Blake3 | ak::Sha224 | ak::Sha256 | ak::Sha384 @@ -283,8 +282,8 @@ impl SizedAlgoKind { (ak::Md5, _) => Ok(Self::Md5), (ak::Sm3, _) => Ok(Self::Sm3), (ak::Sha1, _) => Ok(Self::Sha1), + (ak::Blake3, _) => Ok(Self::Blake3), - (ak::Blake3, l) => Ok(Self::Blake3(l)), (ak::Shake128, l) => Ok(Self::Shake128(l)), (ak::Shake256, l) => Ok(Self::Shake256(l)), (ak::Sha2, Some(l)) => Ok(Self::Sha2(ShaLength::try_from(l)?)), @@ -294,8 +293,7 @@ impl SizedAlgoKind { } // [`calculate_blake2b_length`] expects a length in bits but we // have a length in bytes. - (algo @ ak::Blake2b, Some(l)) => Ok(Self::Blake2b(calculate_blake_length_str( - algo, + (ak::Blake2b, Some(l)) => Ok(Self::Blake2b(calculate_blake2b_length_str( &(8 * l).to_string(), )?)), (ak::Blake2b, None) => Ok(Self::Blake2b(None)), @@ -312,16 +310,11 @@ impl SizedAlgoKind { Self::Md5 => "MD5".into(), Self::Sm3 => "SM3".into(), Self::Sha1 => "SHA1".into(), + Self::Blake3 => "BLAKE3".into(), Self::Sha2(len) => format!("SHA{}", len.as_usize()), Self::Sha3(len) => format!("SHA3-{}", len.as_usize()), Self::Blake2b(Some(byte_len)) => format!("BLAKE2b-{}", byte_len * 8), Self::Blake2b(None) => "BLAKE2b".into(), - Self::Blake3(byte_len) => { - format!( - "BLAKE3-{}", - byte_len.unwrap_or(Blake3::DEFAULT_BYTE_SIZE) * 8 - ) - } Self::Shake128(opt_bit_len) => format!( "SHAKE128-{}", opt_bit_len.unwrap_or(Shake128::DEFAULT_BIT_SIZE) @@ -346,6 +339,7 @@ impl SizedAlgoKind { Self::Md5 => Box::new(Md5::default()), Self::Sm3 => Box::new(Sm3::default()), Self::Sha1 => Box::new(Sha1::default()), + Self::Blake3 => Box::new(Blake3::default()), Self::Sha2(Len224) => Box::new(Sha224::default()), Self::Sha2(Len256) => Box::new(Sha256::default()), Self::Sha2(Len384) => Box::new(Sha384::default()), @@ -357,9 +351,6 @@ impl SizedAlgoKind { Self::Blake2b(len_opt) => { Box::new(len_opt.map(Blake2b::with_output_bytes).unwrap_or_default()) } - Self::Blake3(len_opt) => { - Box::new(len_opt.map(Blake3::with_output_bytes).unwrap_or_default()) - } Self::Shake128(len_opt) => { Box::new(len_opt.map(Shake128::with_output_bits).unwrap_or_default()) } @@ -378,7 +369,7 @@ impl SizedAlgoKind { Self::Md5 => 128, Self::Sm3 => 512, Self::Sha1 => 160, - Self::Blake3(len) => len.unwrap_or(Blake3::DEFAULT_BYTE_SIZE) * 8, + Self::Blake3 => 256, Self::Sha2(len) => len.as_usize(), Self::Sha3(len) => len.as_usize(), Self::Blake2b(len) => len.unwrap_or(Blake2b::DEFAULT_BYTE_SIZE * 8), @@ -495,22 +486,20 @@ pub fn digest_reader( Ok((digest.result(), output_size)) } -/// Calculates the BYTE length of the digest. -pub fn calculate_blake_length_str(algo: AlgoKind, bit_length: &str) -> UResult> { - debug_assert!(matches!(algo, AlgoKind::Blake2b | AlgoKind::Blake3)); - +/// Calculates the length of the digest. +pub fn calculate_blake2b_length_str(bit_length: &str) -> UResult> { // Blake2b's length is parsed in an u64. match bit_length.parse::() { Ok(0) => Ok(None), // Error cases - Ok(n) if n > 512 && algo == AlgoKind::Blake2b => { + Ok(n) if n > 512 => { show_error!("{}", ChecksumError::InvalidLength(bit_length.into())); - Err(ChecksumError::LengthTooBigForBlake(algo.to_uppercase().into()).into()) + Err(ChecksumError::LengthTooBigForBlake("BLAKE2b".into()).into()) } Err(e) if *e.kind() == IntErrorKind::PosOverflow => { show_error!("{}", ChecksumError::InvalidLength(bit_length.into())); - Err(ChecksumError::LengthTooBigForBlake(algo.to_uppercase().into()).into()) + Err(ChecksumError::LengthTooBigForBlake("BLAKE2b".into()).into()) } Err(_) => Err(ChecksumError::InvalidLength(bit_length.into()).into()), @@ -643,19 +632,10 @@ mod tests { #[test] fn test_calculate_blake2b_length() { - assert_eq!( - calculate_blake_length_str(AlgoKind::Blake2b, "0").unwrap(), - None - ); - assert!(calculate_blake_length_str(AlgoKind::Blake2b, "10").is_err()); - assert!(calculate_blake_length_str(AlgoKind::Blake2b, "520").is_err()); - assert_eq!( - calculate_blake_length_str(AlgoKind::Blake2b, "512").unwrap(), - None - ); - assert_eq!( - calculate_blake_length_str(AlgoKind::Blake2b, "256").unwrap(), - Some(32) - ); + assert_eq!(calculate_blake2b_length_str("0").unwrap(), None); + assert!(calculate_blake2b_length_str("10").is_err()); + assert!(calculate_blake2b_length_str("520").is_err()); + assert_eq!(calculate_blake2b_length_str("512").unwrap(), None); + assert_eq!(calculate_blake2b_length_str("256").unwrap(), Some(32)); } } diff --git a/src/uucore/src/lib/features/checksum/validate.rs b/src/uucore/src/lib/features/checksum/validate.rs index fcfba47e8c0..17b1c7bf522 100644 --- a/src/uucore/src/lib/features/checksum/validate.rs +++ b/src/uucore/src/lib/features/checksum/validate.rs @@ -614,7 +614,6 @@ fn identify_algo_name_and_length( algo_name_input: Option, last_algo: &mut Option, ) -> Result<(AlgoKind, Option), LineCheckError> { - use AlgoKind as ak; let algo_from_line = line_info.algo_name.clone().unwrap_or_default(); let Ok(line_algo) = AlgoKind::from_cksum(algo_from_line.to_lowercase()) else { // Unknown algorithm @@ -630,20 +629,24 @@ fn identify_algo_name_and_length( match (algo_name_input, line_algo) { (l, r) if l == r => (), // Edge case for SHA2, which matches SHA(224|256|384|512) - (ak::Sha2, ak::Sha224 | ak::Sha256 | ak::Sha384 | ak::Sha512) => (), + ( + AlgoKind::Sha2, + AlgoKind::Sha224 | AlgoKind::Sha256 | AlgoKind::Sha384 | AlgoKind::Sha512, + ) => (), _ => return Err(LineCheckError::ImproperlyFormatted), } } let bytes = if let Some(bitlen) = line_info.algo_bit_len { match line_algo { - ak::Blake2b | ak::Blake3 if bitlen % 8 == 0 => Some(bitlen / 8), - ak::Sha2 | ak::Sha3 if [224, 256, 384, 512].contains(&bitlen) => Some(bitlen), - ak::Shake128 | ak::Shake256 => Some(bitlen), + AlgoKind::Blake2b if bitlen % 8 == 0 => Some(bitlen / 8), + AlgoKind::Sha2 | AlgoKind::Sha3 if [224, 256, 384, 512].contains(&bitlen) => { + Some(bitlen) + } + AlgoKind::Shake128 | AlgoKind::Shake256 => Some(bitlen), // Either - // the algo based line is provided with a bit length with an - // algorithm that does not support it (only Blake2b, Blake3, sha2, - // and sha3 do). + // the algo based line is provided with a bit length + // with an algorithm that does not support it (only Blake2B does). // // eg: MD5-128 (foo.txt) = fffffffff // ^ This is illegal @@ -651,12 +654,9 @@ fn identify_algo_name_and_length( // the given length is wrong because it's not a multiple of 8. _ => return Err(LineCheckError::ImproperlyFormatted), } - } else if line_algo == ak::Blake2b { + } else if line_algo == AlgoKind::Blake2b { // Default length with BLAKE2b, Some(64) - } else if line_algo == ak::Blake3 { - // Default length with BLAKE3, - Some(32) } else { None }; @@ -741,7 +741,7 @@ fn process_algo_based_line( // If the digest bitlen is known, we can check the format of the expected // checksum with it. let digest_char_length_hint = match (algo_kind, algo_byte_len) { - (AlgoKind::Blake2b | AlgoKind::Blake3, Some(byte_len)) => Some(byte_len), + (AlgoKind::Blake2b, Some(byte_len)) => Some(byte_len), (AlgoKind::Shake128 | AlgoKind::Shake256, Some(bit_len)) => Some(bit_len.div_ceil(8)), (AlgoKind::Shake128, None) => Some(sum::Shake128::DEFAULT_BIT_SIZE.div_ceil(8)), (AlgoKind::Shake256, None) => Some(sum::Shake256::DEFAULT_BIT_SIZE.div_ceil(8)), @@ -764,7 +764,6 @@ fn process_non_algo_based_line( cli_algo_length: Option, opts: ChecksumValidateOptions, ) -> Result<(), LineCheckError> { - use AlgoKind as ak; let mut filename_to_check = line_info.filename.as_slice(); if filename_to_check.starts_with(b"*") && line_number == 0 @@ -779,16 +778,16 @@ fn process_non_algo_based_line( // When a specific algorithm name is input, use it and use the provided // bits except when dealing with blake2b, sha2 and sha3, where we will // detect the length. - let algo_byte_len = match cli_algo_kind { - ak::Blake2b | ak::Blake3 => Some(expected_checksum.len()), - ak::Sha2 | ak::Sha3 => { + let (algo_kind, algo_byte_len) = match cli_algo_kind { + AlgoKind::Blake2b => (AlgoKind::Blake2b, Some(expected_checksum.len())), + algo @ (AlgoKind::Sha2 | AlgoKind::Sha3) => { // multiplication by 8 to get the number of bits - Some(expected_checksum.len() * 8) + (algo, Some(expected_checksum.len() * 8)) } - _ => cli_algo_length, + _ => (cli_algo_kind, cli_algo_length), }; - let algo = SizedAlgoKind::from_unsized(cli_algo_kind, algo_byte_len)?; + let algo = SizedAlgoKind::from_unsized(algo_kind, algo_byte_len)?; compute_and_check_digest_from_file(filename_to_check, &expected_checksum, algo, opts) } diff --git a/src/uucore/src/lib/features/sum.rs b/src/uucore/src/lib/features/sum.rs index af122ced1a3..279643a962b 100644 --- a/src/uucore/src/lib/features/sum.rs +++ b/src/uucore/src/lib/features/sum.rs @@ -122,48 +122,25 @@ impl Digest for Blake2b { } } -pub struct Blake3 { - digest: blake3::Hasher, - byte_size: usize, -} - -impl Blake3 { - /// Default length for the BLAKE3 digest in bytes. - pub const DEFAULT_BYTE_SIZE: usize = 32; - - pub fn with_output_bytes(output_bytes: usize) -> Self { - Self { - digest: blake3::Hasher::new(), - byte_size: output_bytes, - } - } -} - -impl Default for Blake3 { - fn default() -> Self { - Self { - digest: blake3::Hasher::default(), - byte_size: Self::DEFAULT_BYTE_SIZE, - } - } -} +#[derive(Default)] +pub struct Blake3(blake3::Hasher); impl Digest for Blake3 { fn hash_update(&mut self, input: &[u8]) { - self.digest.update(input); + self.0.update(input); } fn hash_finalize(&mut self, out: &mut [u8]) { - let mut hash_result = self.digest.finalize_xof(); - hash_result.fill(out); + let hash_result = &self.0.finalize(); + out.copy_from_slice(hash_result.as_bytes()); } fn reset(&mut self) { - *self = Self::with_output_bytes(self.output_bytes()); + *self = Self::default(); } fn output_bits(&self) -> usize { - self.byte_size * 8 + 256 } } diff --git a/tests/by-util/test_cksum.rs b/tests/by-util/test_cksum.rs index 1ab5bb45285..67f96e90762 100644 --- a/tests/by-util/test_cksum.rs +++ b/tests/by-util/test_cksum.rs @@ -5,7 +5,6 @@ // spell-checker:ignore (words) asdf algo algos asha mgmt xffname hexa GFYEQ HYQK Yqxb dont checkfile use rstest::rstest; -use rstest_reuse::{apply, template}; use uutests::at_and_ucmd; use uutests::new_ucmd; @@ -3288,112 +3287,3 @@ fn test_check_shake256_no_length() { .fails() .stderr_only("cksum: 'standard input': no properly formatted checksum lines found\n"); } - -#[template] -#[rstest] -#[case::no_length( - b"foo", - "04e0bb39f30b1a3feb89f536c93be15055482df748674b00d26e5a75777702e9", - None -)] -#[case( - b"foo", - "04e0bb39f30b1a3feb89f536c93be15055482df748674b00d26e5a75777702e9", - Some(0) -)] -#[case( - b"foo", - "04e0bb39f30b1a3feb89f536c93be15055482df748674b00d26e5a75777702e9", - Some(256) -)] -#[case( - b"foo", - "04e0bb39f30b1a3feb89f536c93be15055482df748674b00d26e5a75777702e9791074b7511b59d31c71c62f5a745689fa6c", - Some(400) -)] -#[case(b"foo", "04e0bb39f3", Some(40))] -#[case(b"foo", "04e0", Some(16))] -#[case(b"foo", "04", Some(8))] -fn test_blake3(#[case] input: &[u8], #[case] expected: &str, #[case] length: Option) {} - -#[apply(test_blake3)] -fn test_compute_blake3( - #[case] input: &[u8], - #[case] expected: &str, - #[case] length: Option, -) { - let length_args: &[String] = if let Some(len) = length { - &["-l".into(), len.to_string()] - } else { - &[] - }; - - new_ucmd!() - .arg("-a") - .arg("blake3") - .args(length_args) - .pipe_in(input) - .succeeds() - .stdout_only(format!( - "BLAKE3{} (-) = {expected}\n", - match length { - Some(0) | None => "-256".into(), - Some(i) => format!("-{i}"), - } - )); - - // with --untagged - new_ucmd!() - .arg("-a") - .arg("blake3") - .arg("--untagged") - .args(length_args) - .pipe_in(input) - .succeeds() - .stdout_only(format!("{expected} -\n")); -} - -#[apply(test_blake3)] -fn test_check_blake3_tagged( - #[case] input: &[u8], - #[case] digest: &str, - #[case] opt_len: Option, -) { - let (at, mut ucmd) = at_and_ucmd!(); - at.write_bytes("FILE", input); - - let len = match opt_len { - Some(0) => "-256".into(), - Some(i) => format!("-{i}"), - None => String::new(), - }; - - let tagged = format!("BLAKE3{len} (FILE) = {digest}",); - - ucmd.arg("-c") - .arg("-a") - .arg("blake3") - .pipe_in(tagged) - .succeeds() - .stdout_only("FILE: OK\n"); -} - -#[apply(test_blake3)] -#[allow(clippy::used_underscore_binding)] -fn test_check_blake3_untagged( - #[case] input: &[u8], - #[case] digest: &str, - #[case] _opt_len: Option, -) { - let (at, mut ucmd) = at_and_ucmd!(); - at.write_bytes("FILE", input); - - let untagged = format!("{digest} FILE"); - - ucmd.arg("-c") - .arg("-a") - .arg("blake3") - .pipe_in(untagged) - .succeeds() - .stdout_only("FILE: OK\n"); -} diff --git a/util/run-gnu-tests-smack-ci.sh b/util/run-gnu-tests-smack-ci.sh index 867a15dad4a..e37e29ca3fa 100755 --- a/util/run-gnu-tests-smack-ci.sh +++ b/util/run-gnu-tests-smack-ci.sh @@ -17,14 +17,15 @@ mkdir -p "$QEMU_DIR"/{rootfs/{bin,lib64,proc,sys,dev,tmp,etc,gnu},kernel} # Copy Ubuntu kernel (runner's kernel does not work) sudo apt-get update || : -sudo apt-get install -y --no-install-recommends linux-image-generic +sudo apt-get install -y linux-image-generic sudo install -Dvm644 "$(ls -1 /boot/vmlinuz-*-generic | head -n 1)" "$QEMU_DIR/kernel/vmlinuz" # Setup busybox -curl -L -o b.tar.gz https://dl-cdn.alpinelinux.org/alpine/latest-stable/main/x86_64/busybox-static-1.37.0-r30.apk -tar -xf b.tar.gz -install -Dvm755 bin/busybox.static "$QEMU_DIR/rootfs/bin/busybox" -(cd "$QEMU_DIR/rootfs/bin" && ./busybox --list | xargs -I{} ln -sf busybox {} 2>/dev/null) +BUSYBOX=/tmp/busybox +[ -f "$BUSYBOX" ] || curl -sL -o "$BUSYBOX" https://busybox.net/downloads/binaries/1.35.0-x86_64-linux-musl/busybox +chmod +x "$BUSYBOX" +cp "$BUSYBOX" "$QEMU_DIR/rootfs/bin/" +(cd "$QEMU_DIR/rootfs/bin" && "$BUSYBOX" --list | xargs -I{} ln -sf busybox {} 2>/dev/null) # Copy required libraries for lib in ld-linux-x86-64.so.2 libc.so.6 libm.so.6 libgcc_s.so.1 libpthread.so.0 libdl.so.2 librt.so.1; do @@ -103,7 +104,7 @@ for TEST_PATH in $QEMU_TESTS; do # Hardlink utilities for SMACK/ROOTFS tests for U in $("$REPO_DIR/target/${PROFILE}/coreutils" --list); do - ln -f "$REPO_DIR/target/${PROFILE}/coreutils" "$WORK/bin/$U" + ln -vf "$REPO_DIR/target/${PROFILE}/coreutils" "$WORK/bin/$U" done # Set test script path and user