From 395cbb13571cd70e6f11a59e8179cfde07de3c8b Mon Sep 17 00:00:00 2001 From: Nicolas Boichat Date: Fri, 25 Jul 2025 12:08:39 +0800 Subject: [PATCH 1/7] ls: Handle timestamps far in the future/past by just printing them Fold get_time into display_date to make that easier, the split was a remnant of previous refactoring. The allowed timestamp range is different from what GNU coreutils allows, but should not really matter for practical purposes, we could fix later if needed. It is difficult to add a test as ext4 typically doesn't support creating files with such large timestamps. --- src/uu/ls/src/ls.rs | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index c59c6d185..96bf01bb0 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -2993,7 +2993,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,25 +3015,37 @@ 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 + let Some(time) = get_system_time(metadata, config) else { + out.extend(b"???"); + return Ok(()); + }; + + let zoned: Result = time.try_into(); + match zoned { + Ok(time) => config .time_style .format(time, out, state) .map_err(|x| USimpleError::new(1, x.to_string())), - None => { - out.extend(b"???"); + Err(_) => { + // Assume that if we cannot build a Zoned element, the time 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 = if time > UNIX_EPOCH { + time.duration_since(UNIX_EPOCH).unwrap().as_secs() + } else { + out.extend(b"-"); // Add negative sign + UNIX_EPOCH.duration_since(time).unwrap().as_secs() + }; + out.extend(ts.to_string().as_bytes()); Ok(()) } } From 4907396c1082efeff5143c5b91db5f48f86b2771 Mon Sep 17 00:00:00 2001 From: Nicolas Boichat Date: Fri, 25 Jul 2025 19:19:41 +0800 Subject: [PATCH 2/7] ls: Refactor date/time printing TimeStyle was more useful when we were using chrono instead of jiff. Also, refactor some of the functions to isolate more of them from the details of `ls` implementation, so that we can move them to uucore. --- src/uu/ls/src/ls.rs | 123 +++++++++++++++++++------------------------- 1 file changed, 54 insertions(+), 69 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 96bf01bb0..022cb01be 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -31,9 +31,9 @@ use clap::{ builder::{NonEmptyStringValueParser, PossibleValue, ValueParser}, }; use glob::{MatchOptions, Pattern}; +use jiff::Zoned; 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 +258,38 @@ enum Time { Birth, } -#[derive(Debug)] -enum TimeStyle { - FullIso, - LongIso, - Iso, - Locale, - Format(String), +/// Format the given time according to this time format style. +fn format_time(date: Zoned, out: &mut Vec, fmt: &str) -> Result<(), jiff::Error> { + let tm = BrokenDownTime::from(&date); + let mut out = StdIoWrite(out); + let config = jiff::fmt::strtime::Config::new().lenient(true); + tm.format_with_config(&config, fmt, &mut out) } -/// 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, - }; - - tm.format_with_config(&config, fmt, &mut out) - } -} - -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(), +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()); + + // 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))) + } + 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 +297,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 +356,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 +905,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 +1094,8 @@ impl Config { width, quoting_style, indicator_style, - time_style, + time_format_recent, + time_format_older, context, selinux_supported: { #[cfg(feature = "selinux")] @@ -2002,7 +1983,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 +2001,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 { @@ -3026,12 +3007,16 @@ fn display_date( 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, + }; + let zoned: Result = time.try_into(); match zoned { - Ok(time) => config - .time_style - .format(time, out, state) - .map_err(|x| USimpleError::new(1, x.to_string())), + Ok(zoned) => format_time(zoned, out, fmt).map_err(|x| USimpleError::new(1, x.to_string())), Err(_) => { // Assume that if we cannot build a Zoned element, the time is // out of reasonable range, just print it then. From c9b8635279c54f6f8b3d987e30178fc1611049b8 Mon Sep 17 00:00:00 2001 From: Nicolas Boichat Date: Fri, 25 Jul 2025 16:08:39 +0800 Subject: [PATCH 3/7] uucore: Add time feature We'll move some common time handling code there, to be shared between du and ls as a start. Starting with format_system_time from from `ls`. --- Cargo.lock | 2 +- src/uu/ls/Cargo.toml | 6 +--- src/uu/ls/src/ls.rs | 33 ++----------------- src/uucore/Cargo.toml | 8 ++++- src/uucore/src/lib/features.rs | 2 ++ src/uucore/src/lib/features/time.rs | 49 +++++++++++++++++++++++++++++ src/uucore/src/lib/lib.rs | 2 ++ 7 files changed, 64 insertions(+), 38 deletions(-) create mode 100644 src/uucore/src/lib/features/time.rs diff --git a/Cargo.lock b/Cargo.lock index 6a46111d8..db53d3dbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3381,7 +3381,6 @@ dependencies = [ "clap", "glob", "hostname", - "jiff", "lscolors", "selinux", "terminal_size", @@ -3995,6 +3994,7 @@ dependencies = [ "icu_locale", "icu_provider", "itertools 0.14.0", + "jiff", "libc", "md-5", "memchr", diff --git a/src/uu/ls/Cargo.toml b/src/uu/ls/Cargo.toml index fde5c698c..c117c7874 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 022cb01be..015bfd46a 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::Zoned; -use jiff::fmt::StdIoWrite; -use jiff::fmt::strtime::BrokenDownTime; use lscolors::LsColors; use term_grid::{DEFAULT_SEPARATOR_SIZE, Direction, Filling, Grid, GridOptions, SPACES_IN_TAB}; use thiserror::Error; @@ -258,14 +255,6 @@ enum Time { Birth, } -/// Format the given time according to this time format style. -fn format_time(date: Zoned, out: &mut Vec, fmt: &str) -> Result<(), jiff::Error> { - let tm = BrokenDownTime::from(&date); - let mut out = StdIoWrite(out); - let config = jiff::fmt::strtime::Config::new().lenient(true); - tm.format_with_config(&config, fmt, &mut out) -} - 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)), @@ -3014,26 +3003,8 @@ fn display_date( _ => &config.time_format_recent, }; - let zoned: Result = time.try_into(); - match zoned { - Ok(zoned) => format_time(zoned, out, fmt).map_err(|x| USimpleError::new(1, x.to_string())), - Err(_) => { - // Assume that if we cannot build a Zoned element, the time 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 = if time > UNIX_EPOCH { - time.duration_since(UNIX_EPOCH).unwrap().as_secs() - } else { - out.extend(b"-"); // Add negative sign - UNIX_EPOCH.duration_since(time).unwrap().as_secs() - }; - out.extend(ts.to_string().as_bytes()); - Ok(()) - } - } + uucore::time::format_system_time(out, time, fmt) + .map_err(|x| USimpleError::new(1, x.to_string())) } #[allow(dead_code)] diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index 04175299f..381a3041e 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 3a622cd68..d06fbed54 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 000000000..b10daa16f --- /dev/null +++ b/src/uucore/src/lib/features/time.rs @@ -0,0 +1,49 @@ +// 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::time::{SystemTime, UNIX_EPOCH}; + +/// Format the given date according to this time format style. +fn format_zoned(out: &mut Vec, zoned: Zoned, fmt: &str) -> Result<(), jiff::Error> { + let tm = BrokenDownTime::from(&zoned); + let mut out = StdIoWrite(out); + let config = Config::new().lenient(true); + tm.format_with_config(&config, fmt, &mut out) +} + +/// Format a `SystemTime` according to given fmt, and append to vector out. +pub fn format_system_time( + out: &mut Vec, + time: SystemTime, + fmt: &str, +) -> Result<(), jiff::Error> { + 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 = if time > UNIX_EPOCH { + time.duration_since(UNIX_EPOCH).unwrap().as_secs() + } else { + out.extend(b"-"); // Add negative sign + UNIX_EPOCH.duration_since(time).unwrap().as_secs() + }; + out.extend(ts.to_string().as_bytes()); + Ok(()) + } + } +} diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index cde288639..d3cfaccde 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")] From 3b4c472c025d99d18d63320f3eb20aad8e615e36 Mon Sep 17 00:00:00 2001 From: Nicolas Boichat Date: Fri, 25 Jul 2025 21:27:06 +0800 Subject: [PATCH 4/7] uucore: time: add tests --- src/uucore/src/lib/features/time.rs | 38 +++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/uucore/src/lib/features/time.rs b/src/uucore/src/lib/features/time.rs index b10daa16f..28b4b8fc5 100644 --- a/src/uucore/src/lib/features/time.rs +++ b/src/uucore/src/lib/features/time.rs @@ -47,3 +47,41 @@ pub fn format_system_time( } } } + +#[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").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").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").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").expect("Formatting error."); + assert_eq!(String::from_utf8(out).unwrap(), "-67768040922076800"); + } +} From 72d172d2b45981e11cb9dd22c887ca1e6c78421c Mon Sep 17 00:00:00 2001 From: Nicolas Boichat Date: Fri, 25 Jul 2025 23:23:34 +0800 Subject: [PATCH 5/7] uucore: time: Take Write trait as parameter, return UResult Gives us more flexiblity to print directly to stdout, and removes the need to similar error handling across utilities. --- src/uu/ls/src/ls.rs | 1 - src/uucore/src/lib/features/time.rs | 16 ++++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 015bfd46a..6f236f2e7 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -3004,7 +3004,6 @@ fn display_date( }; uucore::time::format_system_time(out, time, fmt) - .map_err(|x| USimpleError::new(1, x.to_string())) } #[allow(dead_code)] diff --git a/src/uucore/src/lib/features/time.rs b/src/uucore/src/lib/features/time.rs index 28b4b8fc5..830eee8a7 100644 --- a/src/uucore/src/lib/features/time.rs +++ b/src/uucore/src/lib/features/time.rs @@ -10,22 +10,22 @@ 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}; + /// Format the given date according to this time format style. -fn format_zoned(out: &mut Vec, zoned: Zoned, fmt: &str) -> Result<(), jiff::Error> { +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 Vec, - time: SystemTime, - fmt: &str, -) -> Result<(), jiff::Error> { +pub fn format_system_time(out: &mut W, time: SystemTime, fmt: &str) -> UResult<()> { let zoned: Result = time.try_into(); match zoned { Ok(zoned) => format_zoned(out, zoned, fmt), @@ -39,10 +39,10 @@ pub fn format_system_time( let ts = if time > UNIX_EPOCH { time.duration_since(UNIX_EPOCH).unwrap().as_secs() } else { - out.extend(b"-"); // Add negative sign + out.write_all(b"-")?; // Add negative sign UNIX_EPOCH.duration_since(time).unwrap().as_secs() }; - out.extend(ts.to_string().as_bytes()); + out.write_all(ts.to_string().as_bytes())?; Ok(()) } } From 64565ba0d670160c8d2291e3a4cff93d44b8561c Mon Sep 17 00:00:00 2001 From: Nicolas Boichat Date: Fri, 25 Jul 2025 23:02:29 +0800 Subject: [PATCH 6/7] du: convert to use uucore time formatting function Indirectly switches to jiff, which conveniently doesn't crash on large timestamps. Also correctly prints timestamp for time far in the future or past. Removes chrono dependency. --- Cargo.lock | 1 - src/uu/du/Cargo.toml | 3 +-- src/uu/du/src/du.rs | 13 ++++++------- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index db53d3dbd..5c6c9696d 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", diff --git a/src/uu/du/Cargo.toml b/src/uu/du/Cargo.toml index 5b0d3f5e8..b9285e5a8 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 4e840c47b..2a8f4ee8f 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)?; + print!("\t"); } print_verbatim(&stat.path).unwrap(); From d1c07806a8d391b13a933135e5600ec93d669137 Mon Sep 17 00:00:00 2001 From: Nicolas Boichat Date: Sat, 26 Jul 2025 13:12:09 +0800 Subject: [PATCH 7/7] uucore: time: Add show_error parameter to format_system_time du prints a warning when the time is out of bounds. --- src/uu/du/src/du.rs | 2 +- src/uu/ls/src/ls.rs | 2 +- src/uucore/src/lib/features/time.rs | 30 +++++++++++++++++++---------- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index 2a8f4ee8f..01b115482 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -580,7 +580,7 @@ impl StatPrinter { if let Some(time) = self.time { let secs = get_time_secs(time, stat)?; let time = UNIX_EPOCH + Duration::from_secs(secs); - uucore::time::format_system_time(&mut stdout(), time, &self.time_format)?; + uucore::time::format_system_time(&mut stdout(), time, &self.time_format, true)?; print!("\t"); } diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 6f236f2e7..00ebbb0ef 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -3003,7 +3003,7 @@ fn display_date( _ => &config.time_format_recent, }; - uucore::time::format_system_time(out, time, fmt) + uucore::time::format_system_time(out, time, fmt, false) } #[allow(dead_code)] diff --git a/src/uucore/src/lib/features/time.rs b/src/uucore/src/lib/features/time.rs index 830eee8a7..8fef2d638 100644 --- a/src/uucore/src/lib/features/time.rs +++ b/src/uucore/src/lib/features/time.rs @@ -14,6 +14,7 @@ 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<()> { @@ -25,7 +26,12 @@ fn format_zoned(out: &mut W, zoned: Zoned, fmt: &str) -> UResult<()> { } /// Format a `SystemTime` according to given fmt, and append to vector out. -pub fn format_system_time(out: &mut W, time: SystemTime, fmt: &str) -> UResult<()> { +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), @@ -36,13 +42,16 @@ pub fn format_system_time(out: &mut W, time: SystemTime, fmt: &str) -> // 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 = if time > UNIX_EPOCH { - time.duration_since(UNIX_EPOCH).unwrap().as_secs() + let ts: i64 = if time > UNIX_EPOCH { + time.duration_since(UNIX_EPOCH).unwrap().as_secs() as i64 } else { - out.write_all(b"-")?; // Add negative sign - UNIX_EPOCH.duration_since(time).unwrap().as_secs() + -(UNIX_EPOCH.duration_since(time).unwrap().as_secs() as i64) }; - out.write_all(ts.to_string().as_bytes())?; + let str = ts.to_string(); + if show_error { + show_error!("time '{str}' is out of range"); + } + out.write_all(str.as_bytes())?; Ok(()) } } @@ -60,11 +69,12 @@ mod tests { let time = UNIX_EPOCH; let mut out = Vec::new(); - format_system_time(&mut out, time, "%Y-%m-%d %H:%M").expect("Formatting error."); + 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").expect("Formatting error."); + 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" @@ -76,12 +86,12 @@ mod tests { 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").expect("Formatting error."); + 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").expect("Formatting error."); + format_system_time(&mut out, time, "%Y-%m-%d %H:%M", false).expect("Formatting error."); assert_eq!(String::from_utf8(out).unwrap(), "-67768040922076800"); } }