diff --git a/src/uu/ls/src/config.rs b/src/uu/ls/src/config.rs new file mode 100644 index 000000000..035b83fd7 --- /dev/null +++ b/src/uu/ls/src/config.rs @@ -0,0 +1,1085 @@ +// 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 new file mode 100644 index 000000000..e080b1ad1 --- /dev/null +++ b/src/uu/ls/src/display.rs @@ -0,0 +1,1314 @@ +// 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 507689eee..913bc04ab 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -13,168 +13,51 @@ use std::borrow::Cow; use std::cell::RefCell; #[cfg(unix)] use std::os::unix::fs::{FileTypeExt, MetadataExt}; -#[cfg(windows)] -use std::os::windows::fs::MetadataExt; use std::{ - cell::{LazyCell, OnceCell}, + cell::OnceCell, cmp::Reverse, ffi::{OsStr, OsString}, - fmt::Write as _, fs::{self, DirEntry, FileType, Metadata, ReadDir}, - io::{BufWriter, ErrorKind, IsTerminal, Stdout, Write, stdout}, - iter, - num::IntErrorKind, + io::{BufWriter, ErrorKind, Stdout, Write, stdout}, ops::RangeInclusive, path::{Path, PathBuf}, time::{Duration, SystemTime, UNIX_EPOCH}, }; -use ansi_width::ansi_width; use clap::{ Arg, ArgAction, Command, builder::{NonEmptyStringValueParser, PossibleValue, ValueParser}, }; -use glob::{MatchOptions, Pattern}; -use lscolors::{Colorable, LsColors}; -use term_grid::{DEFAULT_SEPARATOR_SIZE, Direction, Filling, Grid, GridOptions, SPACES_IN_TAB}; +use lscolors::Colorable; 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, - fs::display_permissions, - fsext::{MetadataTimeField, metadata_get_time}, - line_ending::LineEnding, + fsext::metadata_get_time, os_str_as_bytes_lossy, - parser::parse_glob, - parser::parse_size::parse_size_non_zero_u64, parser::shortcut_value_parser::ShortcutValueParser, - quoting_style::{QuotingStyle, locale_aware_escape_dir_name, locale_aware_escape_name}, - show, show_error, show_warning, - time::{FormatSystemTimeFallback, format, format_system_time}, - translate, + show, translate, version_cmp::version_cmp, }; -mod dired; -use dired::{DiredOutput, is_dired_arg_present}; mod colors; -use crate::options::QUOTING_STYLE; -use colors::{LsColorsParseError, StyleManager, color_name, validate_ls_colors_env}; +mod config; +mod dired; +mod display; -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 use config::{Config, options}; +pub use display::Format; - 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; +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}; #[derive(Error, Debug)] enum LsError { @@ -230,1026 +113,6 @@ 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)?; @@ -2097,98 +960,6 @@ impl Colorable for PathData { } } -/// 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); - } - 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}"); - } - } -} - type DirData = (PathBuf, bool); // A struct to encapsulate state that is passed around from `list` functions. @@ -2407,51 +1178,6 @@ fn sort_entries(entries: &mut [PathData], config: &Config) { } } -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 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 depth_first_list( (dir_path, needs_blank_line): DirData, mut read_dir: ReadDir, @@ -2598,61 +1324,6 @@ fn get_metadata_with_deref_opt(p_buf: &Path, dereference: bool) -> std::io::Resu } } -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. -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$}") -} - fn return_total( items: &[PathData], config: &Config, @@ -2675,181 +1346,6 @@ fn return_total( )) } -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 -} - -#[allow(clippy::cognitive_complexity)] -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(()) -} - #[allow(unused_variables)] fn get_block_size(md: &Metadata, config: &Config) -> u64 { /* GNU ls will display sizes in terms of block size @@ -2857,6 +1353,8 @@ fn get_block_size(md: &Metadata, config: &Config) -> u64 { */ #[cfg(unix)] { + use uucore::format::human::SizeFormat; + let raw_blocks = if md.file_type().is_char_device() || md.file_type().is_block_device() { 0u64 } else { @@ -2874,468 +1372,6 @@ fn get_block_size(md: &Metadata, config: &Config) -> u64 { } } -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 calculate_line_len(output_len: usize, item_len: usize, line_ending: LineEnding) -> usize { - output_len + item_len + line_ending.to_string().len() -} - -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); -} - -/// 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(()) -} - -#[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) -} - -#[allow(dead_code)] -enum SizeOrDeviceId { - Size(String), - Device(String, String), -} - -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)) -} - -fn display_size(size: u64, config: &Config) -> String { - human_readable(size, config.size_format) -} - #[cfg(unix)] fn file_is_executable(md: &Metadata) -> bool { // Mode always returns u32, but the flags might not be, based on the platform @@ -3347,264 +1383,6 @@ fn file_is_executable(md: &Metadata) -> bool { return md.mode() & ((S_IXUSR | S_IXGRP | S_IXOTH) as u32) != 0; } -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 - } -} - -/// 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, - } -} - -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 -} - -#[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(unix)] -fn display_symlink_count(metadata: &Metadata) -> String { - metadata.nlink().to_string() -} - -#[cfg(unix)] -fn display_inode(metadata: &Metadata) -> String { - get_inode(metadata) -} - /// This returns the `SELinux` security context as UTF8 `String`. /// In the long term this should be changed to [`OsStr`], see discussions at #2621/#2656 fn get_security_context<'a>( @@ -3631,6 +1409,8 @@ 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 @@ -3683,125 +1463,3 @@ 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)) -}