diff --git a/Cargo.lock b/Cargo.lock index 6a46111d88e..5c6c9696d9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3164,7 +3164,6 @@ dependencies = [ name = "uu_du" version = "0.1.0" dependencies = [ - "chrono", "clap", "glob", "thiserror 2.0.12", @@ -3381,7 +3380,6 @@ dependencies = [ "clap", "glob", "hostname", - "jiff", "lscolors", "selinux", "terminal_size", @@ -3995,6 +3993,7 @@ dependencies = [ "icu_locale", "icu_provider", "itertools 0.14.0", + "jiff", "libc", "md-5", "memchr", diff --git a/src/uu/du/Cargo.toml b/src/uu/du/Cargo.toml index 5b0d3f5e8ea..b9285e5a827 100644 --- a/src/uu/du/Cargo.toml +++ b/src/uu/du/Cargo.toml @@ -18,11 +18,10 @@ workspace = true path = "src/du.rs" [dependencies] -chrono = { workspace = true } # For the --exclude & --exclude-from options glob = { workspace = true } clap = { workspace = true } -uucore = { workspace = true, features = ["format", "parser"] } +uucore = { workspace = true, features = ["format", "parser", "time"] } thiserror = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies] diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index 4e840c47b54..01b115482b7 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -3,7 +3,6 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use chrono::{DateTime, Local}; use clap::{Arg, ArgAction, ArgMatches, Command, builder::PossibleValue}; use glob::Pattern; use std::collections::{HashMap, HashSet}; @@ -11,7 +10,7 @@ use std::env; #[cfg(not(windows))] use std::fs::Metadata; use std::fs::{self, DirEntry, File}; -use std::io::{BufRead, BufReader}; +use std::io::{BufRead, BufReader, stdout}; #[cfg(not(windows))] use std::os::unix::fs::MetadataExt; #[cfg(windows)] @@ -576,13 +575,13 @@ impl StatPrinter { } fn print_stat(&self, stat: &Stat, size: u64) -> UResult<()> { + print!("{}\t", self.convert_size(size)); + if let Some(time) = self.time { let secs = get_time_secs(time, stat)?; - let tm = DateTime::::from(UNIX_EPOCH + Duration::from_secs(secs)); - let time_str = tm.format(&self.time_format).to_string(); - print!("{}\t{time_str}\t", self.convert_size(size)); - } else { - print!("{}\t", self.convert_size(size)); + let time = UNIX_EPOCH + Duration::from_secs(secs); + uucore::time::format_system_time(&mut stdout(), time, &self.time_format, true)?; + print!("\t"); } print_verbatim(&stat.path).unwrap(); diff --git a/src/uu/ls/Cargo.toml b/src/uu/ls/Cargo.toml index fde5c698c46..c117c78748b 100644 --- a/src/uu/ls/Cargo.toml +++ b/src/uu/ls/Cargo.toml @@ -24,11 +24,6 @@ ansi-width = { workspace = true } clap = { workspace = true, features = ["env"] } glob = { workspace = true } hostname = { workspace = true } -jiff = { workspace = true, features = [ - "tzdb-bundle-platform", - "tzdb-zoneinfo", - "tzdb-concatenated", -] } lscolors = { workspace = true } selinux = { workspace = true, optional = true } terminal_size = { workspace = true } @@ -41,6 +36,7 @@ uucore = { workspace = true, features = [ "fsxattr", "parser", "quoting-style", + "time", "version-cmp", ] } uutils_term_grid = { workspace = true } diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index c59c6d185b0..00ebbb0eff4 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -31,9 +31,6 @@ use clap::{ builder::{NonEmptyStringValueParser, PossibleValue, ValueParser}, }; use glob::{MatchOptions, Pattern}; -use jiff::fmt::StdIoWrite; -use jiff::fmt::strtime::BrokenDownTime; -use jiff::{Timestamp, Zoned}; use lscolors::LsColors; use term_grid::{DEFAULT_SEPARATOR_SIZE, Direction, Filling, Grid, GridOptions, SPACES_IN_TAB}; use thiserror::Error; @@ -258,56 +255,30 @@ enum Time { Birth, } -#[derive(Debug)] -enum TimeStyle { - FullIso, - LongIso, - Iso, - Locale, - Format(String), -} - -/// Whether the given date is considered recent (i.e., in the last 6 months). -fn is_recent(time: Timestamp, state: &mut ListState) -> bool { - time > state.recent_time_threshold -} - -impl TimeStyle { - /// Format the given time according to this time format style. - fn format( - &self, - date: Zoned, - out: &mut Vec, - state: &mut ListState, - ) -> Result<(), jiff::Error> { - let recent = is_recent(date.timestamp(), state); - let tm = BrokenDownTime::from(&date); - let mut out = StdIoWrite(out); - let config = jiff::fmt::strtime::Config::new().lenient(true); - - let fmt = match (self, recent) { - (Self::FullIso, _) => "%Y-%m-%d %H:%M:%S.%f %z", - (Self::LongIso, _) => "%Y-%m-%d %H:%M", - (Self::Iso, true) => "%m-%d %H:%M", - (Self::Iso, false) => "%Y-%m-%d ", - // TODO: Using correct locale string is not implemented. - (Self::Locale, true) => "%b %e %H:%M", - (Self::Locale, false) => "%b %e %Y", - (Self::Format(fmt), _) => fmt, - }; +fn parse_time_style(options: &clap::ArgMatches) -> Result<(String, Option), LsError> { + const TIME_STYLES: [(&str, (&str, Option<&str>)); 4] = [ + ("full-iso", ("%Y-%m-%d %H:%M:%S.%f %z", None)), + ("long-iso", ("%Y-%m-%d %H:%M", None)), + ("iso", ("%m-%d %H:%M", Some("%Y-%m-%d "))), + // TODO: Using correct locale string is not implemented. + ("locale", ("%b %e %H:%M", Some("%b %e %Y"))), + ]; + // A map from a time-style parameter to a length-2 tuple of formats: + // the first one is used for recent dates, the second one for older ones (optional). + let time_styles = HashMap::from(TIME_STYLES); + let possible_time_styles = TIME_STYLES + .iter() + .map(|(x, _)| *x) + .chain(iter::once( + "+FORMAT (e.g., +%H:%M) for a 'date'-style format", + )) + .map(|s| s.to_string()); - tm.format_with_config(&config, fmt, &mut out) + // Convert time_styles references to owned String/option. + fn ok((recent, older): (&str, Option<&str>)) -> Result<(String, Option), LsError> { + Ok((recent.to_string(), older.map(String::from))) } -} -fn parse_time_style(options: &clap::ArgMatches) -> Result { - let possible_time_styles = vec![ - "full-iso".to_string(), - "long-iso".to_string(), - "iso".to_string(), - "locale".to_string(), - "+FORMAT (e.g., +%H:%M) for a 'date'-style format".to_string(), - ]; if let Some(field) = options.get_one::(options::TIME_STYLE) { //If both FULL_TIME and TIME_STYLE are present //The one added last is dominant @@ -315,26 +286,23 @@ fn parse_time_style(options: &clap::ArgMatches) -> Result { && options.indices_of(options::FULL_TIME).unwrap().next_back() > options.indices_of(options::TIME_STYLE).unwrap().next_back() { - Ok(TimeStyle::FullIso) + ok(time_styles["full-iso"]) } else { - match field.as_str() { - "full-iso" => Ok(TimeStyle::FullIso), - "long-iso" => Ok(TimeStyle::LongIso), - "iso" => Ok(TimeStyle::Iso), - "locale" => Ok(TimeStyle::Locale), - _ => match field.chars().next().unwrap() { - '+' => Ok(TimeStyle::Format(String::from(&field[1..]))), + match time_styles.get(field.as_str()) { + Some(formats) => ok(*formats), + None => match field.chars().next().unwrap() { + '+' => Ok((field[1..].to_string(), None)), _ => Err(LsError::TimeStyleParseError( String::from(field), - possible_time_styles, + possible_time_styles.collect(), )), }, } } } else if options.get_flag(options::FULL_TIME) { - Ok(TimeStyle::FullIso) + ok(time_styles["full-iso"]) } else { - Ok(TimeStyle::Locale) + ok(time_styles["locale"]) } } @@ -377,7 +345,8 @@ pub struct Config { // Dir and vdir needs access to this field pub quoting_style: QuotingStyle, indicator_style: IndicatorStyle, - time_style: TimeStyle, + 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, selinux_supported: bool, group_directories_first: bool, @@ -925,10 +894,10 @@ impl Config { 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_style = if format == Format::Long || dired { + let (time_format_recent, time_format_older) = if format == Format::Long || dired { parse_time_style(options)? } else { - TimeStyle::Iso + Default::default() }; let mut ignore_patterns: Vec = Vec::new(); @@ -1114,7 +1083,8 @@ impl Config { width, quoting_style, indicator_style, - time_style, + time_format_recent, + time_format_older, context, selinux_supported: { #[cfg(feature = "selinux")] @@ -2002,7 +1972,7 @@ struct ListState<'a> { uid_cache: HashMap, #[cfg(unix)] gid_cache: HashMap, - recent_time_threshold: Timestamp, + recent_time_threshold: SystemTime, } #[allow(clippy::cognitive_complexity)] @@ -2020,7 +1990,7 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> { #[cfg(unix)] gid_cache: HashMap::new(), // According to GNU a Gregorian year has 365.2425 * 24 * 60 * 60 == 31556952 seconds on the average. - recent_time_threshold: Timestamp::now() - Duration::new(31_556_952 / 2, 0), + recent_time_threshold: SystemTime::now() - Duration::new(31_556_952 / 2, 0), }; for loc in locs { @@ -2993,7 +2963,7 @@ fn display_group(_metadata: &Metadata, _config: &Config, _state: &mut ListState) "somegroup" } -// The implementations for get_time are separated because some options, such +// The implementations for get_system_time are separated because some options, such // as ctime will not be available #[cfg(unix)] fn get_system_time(md: &Metadata, config: &Config) -> Option { @@ -3015,28 +2985,25 @@ fn get_system_time(md: &Metadata, config: &Config) -> Option { } } -fn get_time(md: &Metadata, config: &Config) -> Option { - let time = get_system_time(md, config)?; - time.try_into().ok() -} - fn display_date( metadata: &Metadata, config: &Config, state: &mut ListState, out: &mut Vec, ) -> UResult<()> { - match get_time(metadata, config) { - // TODO: Some fancier error conversion might be nice. - Some(time) => config - .time_style - .format(time, out, state) - .map_err(|x| USimpleError::new(1, x.to_string())), - None => { - out.extend(b"???"); - Ok(()) - } - } + let Some(time) = get_system_time(metadata, config) 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 time <= state.recent_time_threshold => time_format_older, + _ => &config.time_format_recent, + }; + + uucore::time::format_system_time(out, time, fmt, false) } #[allow(dead_code)] diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index 04175299f68..381a3041eb2 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -1,4 +1,4 @@ -# spell-checker:ignore (features) bigdecimal zerocopy extendedbigdecimal +# spell-checker:ignore (features) bigdecimal zerocopy extendedbigdecimal tzdb zoneinfo [package] name = "uucore" @@ -29,6 +29,11 @@ dunce = { version = "1.0.4", optional = true } wild = "2.2.1" glob = { workspace = true, optional = true } itertools = { workspace = true, optional = true } +jiff = { workspace = true, optional = true, features = [ + "tzdb-bundle-platform", + "tzdb-zoneinfo", + "tzdb-concatenated", +] } time = { workspace = true, optional = true, features = [ "formatting", "local-offset", @@ -150,4 +155,5 @@ utmpx = ["time", "time/macros", "libc", "dns-lookup"] version-cmp = [] wide = [] tty = [] +time = ["jiff"] uptime = ["chrono", "libc", "windows-sys", "utmpx", "utmp-classic"] diff --git a/src/uucore/src/lib/features.rs b/src/uucore/src/lib/features.rs index 3a622cd6857..d06fbed545a 100644 --- a/src/uucore/src/lib/features.rs +++ b/src/uucore/src/lib/features.rs @@ -40,6 +40,8 @@ pub mod ranges; pub mod ringbuffer; #[cfg(feature = "sum")] pub mod sum; +#[cfg(feature = "time")] +pub mod time; #[cfg(feature = "update-control")] pub mod update_control; #[cfg(feature = "uptime")] diff --git a/src/uucore/src/lib/features/time.rs b/src/uucore/src/lib/features/time.rs new file mode 100644 index 00000000000..8fef2d63842 --- /dev/null +++ b/src/uucore/src/lib/features/time.rs @@ -0,0 +1,97 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +// spell-checker:ignore (ToDO) strtime + +//! Set of functions related to time handling + +use jiff::Zoned; +use jiff::fmt::StdIoWrite; +use jiff::fmt::strtime::{BrokenDownTime, Config}; +use std::io::Write; +use std::time::{SystemTime, UNIX_EPOCH}; + +use crate::error::{UResult, USimpleError}; +use crate::show_error; + +/// Format the given date according to this time format style. +fn format_zoned(out: &mut W, zoned: Zoned, fmt: &str) -> UResult<()> { + let tm = BrokenDownTime::from(&zoned); + let mut out = StdIoWrite(out); + let config = Config::new().lenient(true); + tm.format_with_config(&config, fmt, &mut out) + .map_err(|x| USimpleError::new(1, x.to_string())) +} + +/// Format a `SystemTime` according to given fmt, and append to vector out. +pub fn format_system_time( + out: &mut W, + time: SystemTime, + fmt: &str, + show_error: bool, +) -> UResult<()> { + let zoned: Result = time.try_into(); + match zoned { + Ok(zoned) => format_zoned(out, zoned, fmt), + Err(_) => { + // Assume that if we cannot build a Zoned element, the timestamp is + // out of reasonable range, just print it then. + // TODO: The range allowed by jiff is different from what GNU accepts, + // but it still far enough in the future/past to be unlikely to matter: + // jiff: Year between -9999 to 9999 (UTC) [-377705023201..=253402207200] + // GNU: Year fits in signed 32 bits (timezone dependent) + let ts: i64 = if time > UNIX_EPOCH { + time.duration_since(UNIX_EPOCH).unwrap().as_secs() as i64 + } else { + -(UNIX_EPOCH.duration_since(time).unwrap().as_secs() as i64) + }; + let str = ts.to_string(); + if show_error { + show_error!("time '{str}' is out of range"); + } + out.write_all(str.as_bytes())?; + Ok(()) + } + } +} + +#[cfg(test)] +mod tests { + use crate::time::format_system_time; + use std::time::{Duration, UNIX_EPOCH}; + + // Test epoch SystemTime get printed correctly at UTC0, with 2 simple formats. + #[test] + fn test_simple_system_time() { + unsafe { std::env::set_var("TZ", "UTC0") }; + + let time = UNIX_EPOCH; + let mut out = Vec::new(); + format_system_time(&mut out, time, "%Y-%m-%d %H:%M", false).expect("Formatting error."); + assert_eq!(String::from_utf8(out).unwrap(), "1970-01-01 00:00"); + + let mut out = Vec::new(); + format_system_time(&mut out, time, "%Y-%m-%d %H:%M:%S.%N %z", false) + .expect("Formatting error."); + assert_eq!( + String::from_utf8(out).unwrap(), + "1970-01-01 00:00:00.000000000 +0000" + ); + } + + // Test that very large (positive or negative) lead to just the timestamp being printed. + #[test] + fn test_large_system_time() { + let time = UNIX_EPOCH + Duration::from_secs(67_768_036_191_763_200); + let mut out = Vec::new(); + format_system_time(&mut out, time, "%Y-%m-%d %H:%M", false).expect("Formatting error."); + assert_eq!(String::from_utf8(out).unwrap(), "67768036191763200"); + + let time = UNIX_EPOCH - Duration::from_secs(67_768_040_922_076_800); + let mut out = Vec::new(); + format_system_time(&mut out, time, "%Y-%m-%d %H:%M", false).expect("Formatting error."); + assert_eq!(String::from_utf8(out).unwrap(), "-67768040922076800"); + } +} diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index cde28863946..d3cfaccde8d 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -65,6 +65,8 @@ pub use crate::features::ranges; pub use crate::features::ringbuffer; #[cfg(feature = "sum")] pub use crate::features::sum; +#[cfg(feature = "time")] +pub use crate::features::time; #[cfg(feature = "update-control")] pub use crate::features::update_control; #[cfg(feature = "uptime")]