From 301e33cfe301efcd83308d3f0ee7ebb2473bc20b Mon Sep 17 00:00:00 2001 From: Nicolas Boichat Date: Mon, 28 Apr 2025 09:50:51 +0800 Subject: [PATCH 01/12] date: switch from chrono to jiff Also adds cargo dependency. --- Cargo.lock | 51 +++++++++++++++++++ Cargo.toml | 5 ++ fuzz/Cargo.lock | 57 +++++++++++++++++++++ src/uu/date/Cargo.toml | 9 +++- src/uu/date/src/date.rs | 108 ++++++++++++++++++++++------------------ 5 files changed, 180 insertions(+), 50 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index de221b82a..dc7cf0927 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1347,6 +1347,47 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jiff" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27e77966151130221b079bcec80f1f34a9e414fa489d99152a201c07fd2182bc" +dependencies = [ + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", + "windows-sys 0.59.0", +] + +[[package]] +name = "jiff-static" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97265751f8a9a4228476f2fc17874a9e7e70e96b893368e42619880fe143b48a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + [[package]] name = "js-sys" version = "0.3.77" @@ -1849,6 +1890,15 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -2818,6 +2868,7 @@ version = "0.1.0" dependencies = [ "chrono", "clap", + "jiff", "libc", "parse_datetime", "uucore", diff --git a/Cargo.toml b/Cargo.toml index 656c7f1ad..44dda8c7d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -304,6 +304,11 @@ hostname = "0.4" iana-time-zone = "0.1.57" indicatif = "0.17.8" itertools = "0.14.0" +jiff = { version = "0.2.10", default-features = false, features = [ + "std", + "alloc", + "tz-system", +] } libc = "0.2.172" linux-raw-sys = "0.9" lscolors = { version = "0.20.0", default-features = false, features = [ diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 4495a0a5a..f7d500c6a 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -644,6 +644,47 @@ dependencies = [ "either", ] +[[package]] +name = "jiff" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27e77966151130221b079bcec80f1f34a9e414fa489d99152a201c07fd2182bc" +dependencies = [ + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", + "windows-sys", +] + +[[package]] +name = "jiff-static" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97265751f8a9a4228476f2fc17874a9e7e70e96b893368e42619880fe143b48a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + [[package]] name = "jobserver" version = "0.1.33" @@ -895,6 +936,21 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "portable-atomic" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1316,6 +1372,7 @@ version = "0.1.0" dependencies = [ "chrono", "clap", + "jiff", "libc", "parse_datetime", "uucore", diff --git a/src/uu/date/Cargo.toml b/src/uu/date/Cargo.toml index 087d4befc..f498befc7 100644 --- a/src/uu/date/Cargo.toml +++ b/src/uu/date/Cargo.toml @@ -1,4 +1,4 @@ -# spell-checker:ignore datetime +# spell-checker:ignore datetime tzdb zoneinfo [package] name = "uu_date" description = "date ~ (uutils) display or set the current time" @@ -19,8 +19,13 @@ workspace = true path = "src/date.rs" [dependencies] -chrono = { workspace = true } clap = { workspace = true } +chrono = { workspace = true } # TODO: Eventually we'll want to remove this +jiff = { workspace = true, features = [ + "tzdb-bundle-platform", + "tzdb-zoneinfo", + "tzdb-concatenated", +] } uucore = { workspace = true, features = ["custom-tz-fmt", "parser"] } parse_datetime = { workspace = true } diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index f4c9313cb..aff353fee 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -3,19 +3,17 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (chrono) Datelike Timelike ; (format) DATEFILE MMDDhhmm ; (vars) datetime datetimes +// spell-checker:ignore strtime ; (format) DATEFILE MMDDhhmm ; (vars) datetime datetimes -use chrono::format::{Item, StrftimeItems}; -use chrono::{DateTime, FixedOffset, Local, Offset, TimeDelta, Utc}; -#[cfg(windows)] -use chrono::{Datelike, Timelike}; use clap::{Arg, ArgAction, Command}; +use jiff::fmt::strtime; +use jiff::tz::TimeZone; +use jiff::{SignedDuration, Timestamp, Zoned}; #[cfg(all(unix, not(target_os = "macos"), not(target_os = "redox")))] use libc::{CLOCK_REALTIME, clock_settime, timespec}; use std::fs::File; use std::io::{BufRead, BufReader}; use std::path::PathBuf; -use uucore::custom_tz_fmt::custom_time_format; use uucore::display::Quotable; use uucore::error::FromIo; use uucore::error::{UResult, USimpleError}; @@ -75,7 +73,7 @@ struct Settings { utc: bool, format: Format, date_source: DateSource, - set_to: Option>, + set_to: Option, } /// Various ways of displaying the date @@ -93,7 +91,7 @@ enum DateSource { Custom(String), File(PathBuf), Stdin, - Human(TimeDelta), + Human(SignedDuration), } enum Iso8601Format { @@ -167,9 +165,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { }; let date_source = if let Some(date) = matches.get_one::(OPT_DATE) { - let ref_time = Local::now(); - if let Ok(new_time) = parse_datetime::parse_datetime_at_date(ref_time, date.as_str()) { - let duration = new_time.signed_duration_since(ref_time); + if let Ok(duration) = parse_offset(date.as_str()) { DateSource::Human(duration) } else { DateSource::Custom(date.into()) @@ -203,39 +199,37 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { if let Some(date) = settings.set_to { // All set time functions expect UTC datetimes. - let date: DateTime = if settings.utc { - date.with_timezone(&Utc) + let date = if settings.utc { + date.with_time_zone(TimeZone::UTC) } else { - date.into() + date }; return set_system_datetime(date); } else { // Get the current time, either in the local time zone or UTC. - let now: DateTime = if settings.utc { - let now = Utc::now(); - now.with_timezone(&now.offset().fix()) + let now = if settings.utc { + Timestamp::now().to_zoned(TimeZone::UTC) } else { - let now = Local::now(); - now.with_timezone(now.offset()) + Zoned::now() }; // Iterate over all dates - whether it's a single date or a file. let dates: Box> = match settings.date_source { DateSource::Custom(ref input) => { - let date = parse_date(input.clone()); + let date = parse_date(input); let iter = std::iter::once(date); Box::new(iter) } DateSource::Human(relative_time) => { // Double check the result is overflow or not of the current_time + relative_time // it may cause a panic of chrono::datetime::DateTime add - match now.checked_add_signed(relative_time) { - Some(date) => { + match now.checked_add(relative_time) { + Ok(date) => { let iter = std::iter::once(Ok(date)); Box::new(iter) } - None => { + Err(_) => { return Err(USimpleError::new( 1, format!("invalid date {relative_time}"), @@ -272,23 +266,16 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // Format all the dates for date in dates { match date { - Ok(date) => { - let format_string = custom_time_format(format_string); - // Hack to work around panic in chrono, - // TODO - remove when a fix for https://github.com/chronotope/chrono/issues/623 is released - let format_items = StrftimeItems::new(format_string.as_str()); - if format_items.clone().any(|i| i == Item::Error) { + // TODO: Switch to lenient formatting. + Ok(date) => match strtime::format(format_string, &date) { + Ok(s) => println!("{s}"), + Err(e) => { return Err(USimpleError::new( 1, - format!("invalid format {}", format_string.replace("%f", "%N")), + format!("invalid format {} ({e})", format_string), )); } - let formatted = date - .format_with_items(format_items) - .to_string() - .replace("%f", "%N"); - println!("{formatted}"); - } + }, Err((input, _err)) => show!(USimpleError::new( 1, format!("invalid date {}", input.quote()) @@ -388,13 +375,13 @@ fn make_format_string(settings: &Settings) -> &str { Iso8601Format::Hours => "%FT%H%:z", Iso8601Format::Minutes => "%FT%H:%M%:z", Iso8601Format::Seconds => "%FT%T%:z", - Iso8601Format::Ns => "%FT%T,%f%:z", + Iso8601Format::Ns => "%FT%T,%N%:z", }, Format::Rfc5322 => "%a, %d %h %Y %T %z", Format::Rfc3339(ref fmt) => match *fmt { Rfc3339Format::Date => "%F", Rfc3339Format::Seconds => "%F %T%:z", - Rfc3339Format::Ns => "%F %T.%f%:z", + Rfc3339Format::Ns => "%F %T.%N%:z", }, Format::Custom(ref fmt) => fmt, Format::Default => "%a %b %e %X %Z %Y", @@ -403,19 +390,43 @@ fn make_format_string(settings: &Settings) -> &str { /// Parse a `String` into a `DateTime`. /// If it fails, return a tuple of the `String` along with its `ParseError`. +// TODO: Convert `parse_datetime` to jiff and remove wrapper from chrono to jiff structures. fn parse_date + Clone>( s: S, -) -> Result, (String, parse_datetime::ParseDateTimeError)> { - parse_datetime::parse_datetime(s.as_ref()).map_err(|e| (s.as_ref().into(), e)) +) -> Result { + match parse_datetime::parse_datetime(s.as_ref()) { + Ok(date) => { + let timestamp = + Timestamp::new(date.timestamp(), date.timestamp_subsec_nanos() as i32).unwrap(); + Ok(Zoned::new(timestamp, TimeZone::UTC)) + } + Err(e) => Err((s.as_ref().into(), e)), + } +} + +// TODO: Convert `parse_datetime` to jiff and remove wrapper from chrono to jiff structures. +// Also, consider whether parse_datetime::parse_datetime_at_date can be renamed to something +// like parse_datetime::parse_offset, instead of doing some addition/subtraction. +fn parse_offset(date: &str) -> Result { + let ref_time = chrono::Local::now(); + if let Ok(new_time) = parse_datetime::parse_datetime_at_date(ref_time, date) { + let duration = new_time.signed_duration_since(ref_time); + Ok(SignedDuration::new( + duration.num_seconds(), + duration.subsec_nanos(), + )) + } else { + Err(()) + } } #[cfg(not(any(unix, windows)))] -fn set_system_datetime(_date: DateTime) -> UResult<()> { +fn set_system_datetime(_date: Zoned) -> UResult<()> { unimplemented!("setting date not implemented (unsupported target)"); } #[cfg(target_os = "macos")] -fn set_system_datetime(_date: DateTime) -> UResult<()> { +fn set_system_datetime(_date: Zoned) -> UResult<()> { Err(USimpleError::new( 1, "setting the date is not supported by macOS".to_string(), @@ -423,7 +434,7 @@ fn set_system_datetime(_date: DateTime) -> UResult<()> { } #[cfg(target_os = "redox")] -fn set_system_datetime(_date: DateTime) -> UResult<()> { +fn set_system_datetime(_date: Zoned) -> UResult<()> { Err(USimpleError::new( 1, "setting the date is not supported by Redox".to_string(), @@ -436,10 +447,11 @@ fn set_system_datetime(_date: DateTime) -> UResult<()> { /// `` /// `` /// `` -fn set_system_datetime(date: DateTime) -> UResult<()> { +fn set_system_datetime(date: Zoned) -> UResult<()> { + let ts = date.timestamp(); let timespec = timespec { - tv_sec: date.timestamp() as _, - tv_nsec: date.timestamp_subsec_nanos() as _, + tv_sec: ts.as_second() as _, + tv_nsec: ts.subsec_nanosecond() as _, }; let result = unsafe { clock_settime(CLOCK_REALTIME, ×pec) }; @@ -456,7 +468,7 @@ fn set_system_datetime(date: DateTime) -> UResult<()> { /// See here for more: /// https://docs.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-setsystemtime /// https://docs.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-systemtime -fn set_system_datetime(date: DateTime) -> UResult<()> { +fn set_system_datetime(date: Zoned) -> UResult<()> { let system_time = SYSTEMTIME { wYear: date.year() as u16, wMonth: date.month() as u16, @@ -467,7 +479,7 @@ fn set_system_datetime(date: DateTime) -> UResult<()> { wMinute: date.minute() as u16, wSecond: date.second() as u16, // TODO: be careful of leap seconds - valid range is [0, 999] - how to handle? - wMilliseconds: ((date.nanosecond() / 1_000_000) % 1000) as u16, + wMilliseconds: ((date.subsec_nanosecond() / 1_000_000) % 1000) as u16, }; let result = unsafe { SetSystemTime(&system_time) }; From 07c9205d229d023189e1087e442724fb7ee2a386 Mon Sep 17 00:00:00 2001 From: Nicolas Boichat Date: Mon, 28 Apr 2025 20:37:12 +0800 Subject: [PATCH 02/12] Revert "ls: Optimize time formatting" This reverts commit fc6b896c271eed5654418acc267eb21377c55690. This also reverts the change from new to new_lenient, we'll recover that later as part of the jiff conversion. --- src/uu/ls/src/ls.rs | 86 +++++++++++++-------------------------------- 1 file changed, 25 insertions(+), 61 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 0297a569c..5cc6983b2 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -27,7 +27,6 @@ use std::{ use std::{collections::HashSet, io::IsTerminal}; use ansi_width::ansi_width; -use chrono::format::{Item, StrftimeItems}; use chrono::{DateTime, Local, TimeDelta}; use clap::{ Arg, ArgAction, Command, @@ -274,64 +273,32 @@ enum TimeStyle { Format(String), } -/// A struct/impl used to format a file DateTime, precomputing the format for performance reasons. -struct TimeStyler { - // default format, always specified. - default: Vec>, - // format for a recent time, only specified it is is different from the default - recent: Option>>, - // If `recent` is set, cache the threshold time when we switch from recent to default format. - recent_time_threshold: Option>, +/// Whether the given date is considered recent (i.e., in the last 6 months). +fn is_recent(time: DateTime) -> bool { + // According to GNU a Gregorian year has 365.2425 * 24 * 60 * 60 == 31556952 seconds on the average. + time + TimeDelta::try_seconds(31_556_952 / 2).unwrap() > Local::now() } -impl TimeStyler { - /// Create a TimeStyler based on a TimeStyle specification. - fn new(style: &TimeStyle) -> TimeStyler { - let default: Vec> = match style { - TimeStyle::FullIso => StrftimeItems::new("%Y-%m-%d %H:%M:%S.%f %z").parse(), - TimeStyle::LongIso => StrftimeItems::new("%Y-%m-%d %H:%M").parse(), - TimeStyle::Iso => StrftimeItems::new("%Y-%m-%d ").parse(), - // In this version of chrono translating can be done - // The function is chrono::datetime::DateTime::format_localized - // However it's currently still hard to get the current pure-rust-locale - // So it's not yet implemented - TimeStyle::Locale => StrftimeItems::new("%b %e %Y").parse(), - TimeStyle::Format(fmt) => { - StrftimeItems::new_lenient(custom_tz_fmt::custom_time_format(fmt).as_str()) - .parse_to_owned() - } - } - .unwrap(); - let recent = match style { - TimeStyle::Iso => Some(StrftimeItems::new("%m-%d %H:%M")), - // See comment above about locale - TimeStyle::Locale => Some(StrftimeItems::new("%b %e %H:%M")), - _ => None, - } - .map(|x| x.collect()); - let recent_time_threshold = if recent.is_some() { - // According to GNU a Gregorian year has 365.2425 * 24 * 60 * 60 == 31556952 seconds on the average. - Some(Local::now() - TimeDelta::try_seconds(31_556_952 / 2).unwrap()) - } else { - None - }; - - TimeStyler { - default, - recent, - recent_time_threshold, - } - } - - /// Format a DateTime, using `recent` format if available, and the DateTime - /// is recent enough. +impl TimeStyle { + /// Format the given time according to this time format style. fn format(&self, time: DateTime) -> String { - if self.recent.is_none() || time <= self.recent_time_threshold.unwrap() { - time.format_with_items(self.default.iter()) - } else { - time.format_with_items(self.recent.as_ref().unwrap().iter()) + let recent = is_recent(time); + match (self, recent) { + (Self::FullIso, _) => time.format("%Y-%m-%d %H:%M:%S.%f %z").to_string(), + (Self::LongIso, _) => time.format("%Y-%m-%d %H:%M").to_string(), + (Self::Iso, true) => time.format("%m-%d %H:%M").to_string(), + (Self::Iso, false) => time.format("%Y-%m-%d ").to_string(), + // spell-checker:ignore (word) datetime + //In this version of chrono translating can be done + //The function is chrono::datetime::DateTime::format_localized + //However it's currently still hard to get the current pure-rust-locale + //So it's not yet implemented + (Self::Locale, true) => time.format("%b %e %H:%M").to_string(), + (Self::Locale, false) => time.format("%b %e %Y").to_string(), + (Self::Format(fmt), _) => time + .format(custom_tz_fmt::custom_time_format(fmt).as_str()) + .to_string(), } - .to_string() } } @@ -2093,8 +2060,6 @@ struct ListState<'a> { uid_cache: HashMap, #[cfg(unix)] gid_cache: HashMap, - - time_styler: TimeStyler, } #[allow(clippy::cognitive_complexity)] @@ -2111,7 +2076,6 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> { uid_cache: HashMap::new(), #[cfg(unix)] gid_cache: HashMap::new(), - time_styler: TimeStyler::new(&config.time_style), }; for loc in locs { @@ -2907,7 +2871,7 @@ fn display_item_long( }; output_display.extend(b" "); - output_display.extend(display_date(md, config, state).as_bytes()); + output_display.extend(display_date(md, config).as_bytes()); output_display.extend(b" "); let item_name = display_item_name( @@ -3111,9 +3075,9 @@ fn get_time(md: &Metadata, config: &Config) -> Option> { Some(time.into()) } -fn display_date(metadata: &Metadata, config: &Config, state: &mut ListState) -> String { +fn display_date(metadata: &Metadata, config: &Config) -> String { match get_time(metadata, config) { - Some(time) => state.time_styler.format(time), + Some(time) => config.time_style.format(time), None => "???".into(), } } From fc947eca339b74007ecadf1e72a27a61e4a51d39 Mon Sep 17 00:00:00 2001 From: Nicolas Boichat Date: Mon, 28 Apr 2025 21:09:52 +0800 Subject: [PATCH 03/12] ls: convert to jiff --- Cargo.lock | 1 + src/uu/ls/Cargo.toml | 7 +++++++ src/uu/ls/src/ls.rs | 34 +++++++++++++++------------------- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dc7cf0927..427a8166c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3141,6 +3141,7 @@ dependencies = [ "clap", "glob", "hostname", + "jiff", "lscolors", "number_prefix", "selinux", diff --git a/src/uu/ls/Cargo.toml b/src/uu/ls/Cargo.toml index ff00175e7..83a2f4fa5 100644 --- a/src/uu/ls/Cargo.toml +++ b/src/uu/ls/Cargo.toml @@ -1,3 +1,5 @@ +# spell-checker:ignore tzdb zoneinfo + [package] name = "uu_ls" description = "ls ~ (uutils) display directory contents" @@ -23,6 +25,11 @@ chrono = { 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 } number_prefix = { workspace = true } selinux = { workspace = true, optional = true } diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 5cc6983b2..3214b4910 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -16,23 +16,22 @@ use std::{ fs::{self, DirEntry, FileType, Metadata, ReadDir}, io::{BufWriter, ErrorKind, Stdout, Write, stdout}, path::{Path, PathBuf}, - time::{SystemTime, UNIX_EPOCH}, + time::{Duration, SystemTime, UNIX_EPOCH}, }; #[cfg(unix)] use std::{ collections::HashMap, os::unix::fs::{FileTypeExt, MetadataExt}, - time::Duration, }; use std::{collections::HashSet, io::IsTerminal}; use ansi_width::ansi_width; -use chrono::{DateTime, Local, TimeDelta}; use clap::{ Arg, ArgAction, Command, builder::{NonEmptyStringValueParser, PossibleValue, ValueParser}, }; use glob::{MatchOptions, Pattern}; +use jiff::{Timestamp, Zoned}; use lscolors::LsColors; use term_grid::{DEFAULT_SEPARATOR_SIZE, Direction, Filling, Grid, GridOptions, SPACES_IN_TAB}; use thiserror::Error; @@ -58,7 +57,6 @@ use uucore::libc::{dev_t, major, minor}; use uucore::line_ending::LineEnding; use uucore::quoting_style::{self, QuotingStyle, escape_name}; use uucore::{ - custom_tz_fmt, display::Quotable, error::{UError, UResult, set_exit_code}, format_usage, @@ -274,30 +272,28 @@ enum TimeStyle { } /// Whether the given date is considered recent (i.e., in the last 6 months). -fn is_recent(time: DateTime) -> bool { +fn is_recent(time: Timestamp) -> bool { // According to GNU a Gregorian year has 365.2425 * 24 * 60 * 60 == 31556952 seconds on the average. - time + TimeDelta::try_seconds(31_556_952 / 2).unwrap() > Local::now() + time + Duration::new(31_556_952 / 2, 0) > Timestamp::now() } impl TimeStyle { /// Format the given time according to this time format style. - fn format(&self, time: DateTime) -> String { - let recent = is_recent(time); + fn format(&self, date: Zoned) -> String { + let recent = is_recent(date.timestamp()); match (self, recent) { - (Self::FullIso, _) => time.format("%Y-%m-%d %H:%M:%S.%f %z").to_string(), - (Self::LongIso, _) => time.format("%Y-%m-%d %H:%M").to_string(), - (Self::Iso, true) => time.format("%m-%d %H:%M").to_string(), - (Self::Iso, false) => time.format("%Y-%m-%d ").to_string(), + (Self::FullIso, _) => date.strftime("%Y-%m-%d %H:%M:%S.%f %z").to_string(), + (Self::LongIso, _) => date.strftime("%Y-%m-%d %H:%M").to_string(), + (Self::Iso, true) => date.strftime("%m-%d %H:%M").to_string(), + (Self::Iso, false) => date.strftime("%Y-%m-%d ").to_string(), // spell-checker:ignore (word) datetime //In this version of chrono translating can be done //The function is chrono::datetime::DateTime::format_localized //However it's currently still hard to get the current pure-rust-locale //So it's not yet implemented - (Self::Locale, true) => time.format("%b %e %H:%M").to_string(), - (Self::Locale, false) => time.format("%b %e %Y").to_string(), - (Self::Format(fmt), _) => time - .format(custom_tz_fmt::custom_time_format(fmt).as_str()) - .to_string(), + (Self::Locale, true) => date.strftime("%b %e %H:%M").to_string(), + (Self::Locale, false) => date.strftime("%b %e %Y").to_string(), + (Self::Format(fmt), _) => date.strftime(&fmt).to_string(), } } } @@ -3070,9 +3066,9 @@ fn get_system_time(md: &Metadata, config: &Config) -> Option { } } -fn get_time(md: &Metadata, config: &Config) -> Option> { +fn get_time(md: &Metadata, config: &Config) -> Option { let time = get_system_time(md, config)?; - Some(time.into()) + time.try_into().ok() } fn display_date(metadata: &Metadata, config: &Config) -> String { From c599363242341a247edaa09f6af43ec65eaf5ff7 Mon Sep 17 00:00:00 2001 From: Nicolas Boichat Date: Wed, 30 Apr 2025 00:29:23 +0800 Subject: [PATCH 04/12] ls: cache recent time threshold in jiff implementation --- src/uu/ls/src/ls.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 3214b4910..e092ea85f 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -272,15 +272,15 @@ enum TimeStyle { } /// Whether the given date is considered recent (i.e., in the last 6 months). -fn is_recent(time: Timestamp) -> bool { +fn is_recent(time: Timestamp, state: &mut ListState) -> bool { // According to GNU a Gregorian year has 365.2425 * 24 * 60 * 60 == 31556952 seconds on the average. - time + Duration::new(31_556_952 / 2, 0) > Timestamp::now() + time > state.recent_time_threshold } impl TimeStyle { /// Format the given time according to this time format style. - fn format(&self, date: Zoned) -> String { - let recent = is_recent(date.timestamp()); + fn format(&self, date: Zoned, state: &mut ListState) -> String { + let recent = is_recent(date.timestamp(), state); match (self, recent) { (Self::FullIso, _) => date.strftime("%Y-%m-%d %H:%M:%S.%f %z").to_string(), (Self::LongIso, _) => date.strftime("%Y-%m-%d %H:%M").to_string(), @@ -2056,6 +2056,7 @@ struct ListState<'a> { uid_cache: HashMap, #[cfg(unix)] gid_cache: HashMap, + recent_time_threshold: Timestamp, } #[allow(clippy::cognitive_complexity)] @@ -2072,6 +2073,7 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> { uid_cache: HashMap::new(), #[cfg(unix)] gid_cache: HashMap::new(), + recent_time_threshold: Timestamp::now() - Duration::new(31_556_952 / 2, 0), }; for loc in locs { @@ -2867,7 +2869,7 @@ fn display_item_long( }; output_display.extend(b" "); - output_display.extend(display_date(md, config).as_bytes()); + output_display.extend(display_date(md, config, state).as_bytes()); output_display.extend(b" "); let item_name = display_item_name( @@ -3071,9 +3073,9 @@ fn get_time(md: &Metadata, config: &Config) -> Option { time.try_into().ok() } -fn display_date(metadata: &Metadata, config: &Config) -> String { +fn display_date(metadata: &Metadata, config: &Config, state: &mut ListState) -> String { match get_time(metadata, config) { - Some(time) => config.time_style.format(time), + Some(time) => config.time_style.format(time, state), None => "???".into(), } } From 10fb220c72739591015d872e761c0ae449693dc3 Mon Sep 17 00:00:00 2001 From: Nicolas Boichat Date: Wed, 30 Apr 2025 00:56:22 +0800 Subject: [PATCH 05/12] ls: Avoid additional String creation/copy in display_date From code provided in #7852 by @BurntSushi. Depending on the benchmarks, there is _still_ a small performance difference (~4%) vs main, but it's seen mostly on small trees getting printed repeatedly, which is probably not a terribly interesting use case. --- src/uu/ls/src/ls.rs | 48 +++++++++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index e092ea85f..e4a5e00f8 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (ToDO) somegroup nlink tabsize dired subdired dtype colorterm stringly nohash +// spell-checker:ignore (ToDO) somegroup nlink tabsize dired subdired dtype colorterm stringly nohash strtime use std::iter; #[cfg(windows)] @@ -31,6 +31,8 @@ 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}; @@ -279,21 +281,29 @@ fn is_recent(time: Timestamp, state: &mut ListState) -> bool { impl TimeStyle { /// Format the given time according to this time format style. - fn format(&self, date: Zoned, state: &mut ListState) -> String { + 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 out = StdIoWrite(out); + match (self, recent) { - (Self::FullIso, _) => date.strftime("%Y-%m-%d %H:%M:%S.%f %z").to_string(), - (Self::LongIso, _) => date.strftime("%Y-%m-%d %H:%M").to_string(), - (Self::Iso, true) => date.strftime("%m-%d %H:%M").to_string(), - (Self::Iso, false) => date.strftime("%Y-%m-%d ").to_string(), + (Self::FullIso, _) => tm.format("%Y-%m-%d %H:%M:%S.%f %z", out), + (Self::LongIso, _) => tm.format("%Y-%m-%d %H:%M", out), + (Self::Iso, true) => tm.format("%m-%d %H:%M", out), + (Self::Iso, false) => tm.format("%Y-%m-%d ", out), // spell-checker:ignore (word) datetime //In this version of chrono translating can be done //The function is chrono::datetime::DateTime::format_localized //However it's currently still hard to get the current pure-rust-locale //So it's not yet implemented - (Self::Locale, true) => date.strftime("%b %e %H:%M").to_string(), - (Self::Locale, false) => date.strftime("%b %e %Y").to_string(), - (Self::Format(fmt), _) => date.strftime(&fmt).to_string(), + (Self::Locale, true) => tm.format("%b %e %H:%M", out), + (Self::Locale, false) => tm.format("%b %e %Y", out), + (Self::Format(fmt), _) => tm.format(&fmt, out), } } } @@ -2869,7 +2879,7 @@ fn display_item_long( }; output_display.extend(b" "); - output_display.extend(display_date(md, config, state).as_bytes()); + display_date(md, config, state, &mut output_display)?; output_display.extend(b" "); let item_name = display_item_name( @@ -3073,10 +3083,22 @@ fn get_time(md: &Metadata, config: &Config) -> Option { time.try_into().ok() } -fn display_date(metadata: &Metadata, config: &Config, state: &mut ListState) -> String { +fn display_date( + metadata: &Metadata, + config: &Config, + state: &mut ListState, + out: &mut Vec, +) -> UResult<()> { match get_time(metadata, config) { - Some(time) => config.time_style.format(time, state), - None => "???".into(), + // 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(()) + } } } From 6031de5a2915ee3287823167d2d928f1dd51aa0e Mon Sep 17 00:00:00 2001 From: Nicolas Boichat Date: Thu, 1 May 2025 10:58:38 +0800 Subject: [PATCH 06/12] ls: switch to lenient formating configuration --- src/uu/ls/src/ls.rs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index e4a5e00f8..df55e10f1 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -289,21 +289,24 @@ impl TimeStyle { ) -> Result<(), jiff::Error> { let recent = is_recent(date.timestamp(), state); let tm = BrokenDownTime::from(&date); - let out = StdIoWrite(out); + let mut out = StdIoWrite(out); + let config = jiff::fmt::strtime::Config::new().lenient(true); match (self, recent) { - (Self::FullIso, _) => tm.format("%Y-%m-%d %H:%M:%S.%f %z", out), - (Self::LongIso, _) => tm.format("%Y-%m-%d %H:%M", out), - (Self::Iso, true) => tm.format("%m-%d %H:%M", out), - (Self::Iso, false) => tm.format("%Y-%m-%d ", out), + (Self::FullIso, _) => { + tm.format_with_config(&config, "%Y-%m-%d %H:%M:%S.%f %z", &mut out) + } + (Self::LongIso, _) => tm.format_with_config(&config, "%Y-%m-%d %H:%M", &mut out), + (Self::Iso, true) => tm.format_with_config(&config, "%m-%d %H:%M", &mut out), + (Self::Iso, false) => tm.format_with_config(&config, "%Y-%m-%d ", &mut out), // spell-checker:ignore (word) datetime //In this version of chrono translating can be done //The function is chrono::datetime::DateTime::format_localized //However it's currently still hard to get the current pure-rust-locale //So it's not yet implemented - (Self::Locale, true) => tm.format("%b %e %H:%M", out), - (Self::Locale, false) => tm.format("%b %e %Y", out), - (Self::Format(fmt), _) => tm.format(&fmt, out), + (Self::Locale, true) => tm.format_with_config(&config, "%b %e %H:%M", &mut out), + (Self::Locale, false) => tm.format_with_config(&config, "%b %e %Y", &mut out), + (Self::Format(fmt), _) => tm.format_with_config(&config, fmt, &mut out), } } } From d1525e2d2ea6af1707d0552c560469389331ed40 Mon Sep 17 00:00:00 2001 From: Jadi Date: Thu, 27 Mar 2025 17:01:43 -0700 Subject: [PATCH 07/12] date: Add more TZ tests [drinkcat: separated test changes] --- tests/by-util/test_date.rs | 110 +++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 09cf7ac79..e5dda70bb 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -572,3 +572,113 @@ fn test_date_empty_tz() { .succeeds() .stdout_only("UTC\n"); } + +#[test] +fn test_date_tz_utc() { + new_ucmd!() + .env("TZ", "UTC0") + .arg("+%Z") + .succeeds() + .stdout_only("UTC\n"); +} + +#[test] +fn test_date_tz_berlin() { + new_ucmd!() + .env("TZ", "Europe/Berlin") + .arg("+%Z") + .succeeds() + .stdout_matches(&Regex::new(r"^(CET|CEST)\n$").unwrap()); +} + +#[test] +fn test_date_tz_vancouver() { + new_ucmd!() + .env("TZ", "America/Vancouver") + .arg("+%Z") + .succeeds() + .stdout_matches(&Regex::new(r"^(PDT|PST)\n$").unwrap()); +} + +#[test] +fn test_date_tz_invalid() { + new_ucmd!() + .env("TZ", "Invalid/Timezone") + .arg("+%Z") + .succeeds() + .stdout_only("UTC\n"); +} + +#[test] +fn test_date_tz_with_format() { + new_ucmd!() + .env("TZ", "Europe/Berlin") + .arg("+%Y-%m-%d %H:%M:%S %Z") + .succeeds() + .stdout_matches( + &Regex::new(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} (CET|CEST)\n$").unwrap(), + ); +} + +#[test] +fn test_date_tz_with_utc_flag() { + new_ucmd!() + .env("TZ", "Europe/Berlin") + .arg("-u") + .arg("+%Z") + .succeeds() + .stdout_only("UTC\n"); +} + +#[test] +fn test_date_tz_with_date_string() { + new_ucmd!() + .env("TZ", "Asia/Tokyo") + .arg("-d") + .arg("2024-01-01 12:00:00") + .arg("+%Y-%m-%d %H:%M:%S %Z") + .succeeds() + .stdout_only("2024-01-01 12:00:00 JST\n"); +} + +#[test] +fn test_date_tz_with_relative_time() { + new_ucmd!() + .env("TZ", "America/Vancouver") + .arg("-d") + .arg("1 hour ago") + .arg("+%Y-%m-%d %H:%M:%S %Z") + .succeeds() + .stdout_matches(&Regex::new(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} PDT\n$").unwrap()); +} + +#[test] +fn test_date_utc_time() { + // Test that -u flag shows correct UTC time + new_ucmd!().arg("-u").arg("+%H:%M").succeeds(); + + // Test that -u flag shows UTC timezone + new_ucmd!() + .arg("-u") + .arg("+%Z") + .succeeds() + .stdout_only("UTC\n"); + + // Test that -u flag with specific timestamp shows correct UTC time + new_ucmd!() + .arg("-u") + .arg("-d") + .arg("@0") + .succeeds() + .stdout_only("Thu Jan 1 00:00:00 UTC 1970\n"); +} + +#[test] +fn test_date_empty_tz_time() { + new_ucmd!() + .env("TZ", "") + .arg("-d") + .arg("@0") + .succeeds() + .stdout_only("Thu Jan 1 00:00:00 UTC 1970\n"); +} From dadda0dd6a4c27de8f18b4eb3f350db85f40d04b Mon Sep 17 00:00:00 2001 From: Nicolas Boichat Date: Tue, 6 May 2025 11:27:03 +0800 Subject: [PATCH 08/12] test_date: Extend coverage to a lot more timezones Also test %z/%Z formats. --- tests/by-util/test_date.rs | 120 +++++++++++++++++++------------------ 1 file changed, 61 insertions(+), 59 deletions(-) diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index e5dda70bb..713717f6a 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -2,6 +2,8 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +// +// spell-checker: ignore: AEDT AEST EEST NZDT NZST use chrono::{DateTime, Datelike, Duration, NaiveTime, Utc}; // spell-checker:disable-line use regex::Regex; @@ -564,60 +566,43 @@ fn test_date_from_stdin() { ); } -#[test] -fn test_date_empty_tz() { - new_ucmd!() - .env("TZ", "") - .arg("+%Z") - .succeeds() - .stdout_only("UTC\n"); -} +const JAN2: &str = "2024-01-02 12:00:00 +0000"; +const JUL2: &str = "2024-07-02 12:00:00 +0000"; #[test] -fn test_date_tz_utc() { - new_ucmd!() - .env("TZ", "UTC0") - .arg("+%Z") - .succeeds() - .stdout_only("UTC\n"); -} +fn test_date_tz() { + fn test_tz(tz: &str, date: &str, output: &str) { + println!("Test with TZ={tz}, date=\"{date}\"."); + new_ucmd!() + .env("TZ", tz) + .arg("-d") + .arg(date) + .arg("+%Y-%m-%d %H:%M:%S %Z") + .succeeds() + .stdout_only(output); + } -#[test] -fn test_date_tz_berlin() { - new_ucmd!() - .env("TZ", "Europe/Berlin") - .arg("+%Z") - .succeeds() - .stdout_matches(&Regex::new(r"^(CET|CEST)\n$").unwrap()); -} + // Empty TZ, UTC0, invalid timezone. + test_tz("", JAN2, "2024-01-02 12:00:00 UTC\n"); + test_tz("UTC0", JAN2, "2024-01-02 12:00:00 UTC\n"); + // TODO: We do not handle invalid timezones the same way as GNU coreutils + //test_tz("Invalid/Timezone", JAN2, "2024-01-02 12:00:00 Invalid\n"); -#[test] -fn test_date_tz_vancouver() { - new_ucmd!() - .env("TZ", "America/Vancouver") - .arg("+%Z") - .succeeds() - .stdout_matches(&Regex::new(r"^(PDT|PST)\n$").unwrap()); -} - -#[test] -fn test_date_tz_invalid() { - new_ucmd!() - .env("TZ", "Invalid/Timezone") - .arg("+%Z") - .succeeds() - .stdout_only("UTC\n"); -} - -#[test] -fn test_date_tz_with_format() { - new_ucmd!() - .env("TZ", "Europe/Berlin") - .arg("+%Y-%m-%d %H:%M:%S %Z") - .succeeds() - .stdout_matches( - &Regex::new(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} (CET|CEST)\n$").unwrap(), - ); + // Test various locations, some of them use daylight saving, some don't. + test_tz("America/Vancouver", JAN2, "2024-01-02 04:00:00 PST\n"); + test_tz("America/Vancouver", JUL2, "2024-07-02 05:00:00 PDT\n"); + test_tz("Europe/Berlin", JAN2, "2024-01-02 13:00:00 CET\n"); + test_tz("Europe/Berlin", JUL2, "2024-07-02 14:00:00 CEST\n"); + test_tz("Africa/Cairo", JAN2, "2024-01-02 14:00:00 EET\n"); + // Egypt restored daylight saving in 2023, so if the database is outdated, this will fail. + //test_tz("Africa/Cairo", JUL2, "2024-07-02 15:00:00 EEST\n"); + test_tz("Asia/Tokyo", JAN2, "2024-01-02 21:00:00 JST\n"); + test_tz("Asia/Tokyo", JUL2, "2024-07-02 21:00:00 JST\n"); + test_tz("Australia/Sydney", JAN2, "2024-01-02 23:00:00 AEDT\n"); + test_tz("Australia/Sydney", JUL2, "2024-07-02 22:00:00 AEST\n"); // Shifts the other way. + test_tz("Pacific/Tahiti", JAN2, "2024-01-02 02:00:00 -10\n"); // No abbreviation. + test_tz("Antarctica/South_Pole", JAN2, "2024-01-03 01:00:00 NZDT\n"); + test_tz("Antarctica/South_Pole", JUL2, "2024-07-03 00:00:00 NZST\n"); } #[test] @@ -631,14 +616,31 @@ fn test_date_tz_with_utc_flag() { } #[test] -fn test_date_tz_with_date_string() { - new_ucmd!() - .env("TZ", "Asia/Tokyo") - .arg("-d") - .arg("2024-01-01 12:00:00") - .arg("+%Y-%m-%d %H:%M:%S %Z") - .succeeds() - .stdout_only("2024-01-01 12:00:00 JST\n"); +fn test_date_tz_various_formats() { + fn test_tz(tz: &str, date: &str, output: &str) { + println!("Test with TZ={tz}, date=\"{date}\"."); + new_ucmd!() + .env("TZ", tz) + .arg("-d") + .arg(date) + .arg("+%z %:z %::z %:::z %Z") + .succeeds() + .stdout_only(output); + } + + test_tz( + "America/Vancouver", + JAN2, + "-0800 -08:00 -08:00:00 -08 PST\n", + ); + // Half-hour timezone + test_tz("Asia/Calcutta", JAN2, "+0530 +05:30 +05:30:00 +05:30 IST\n"); + test_tz("Europe/Berlin", JAN2, "+0100 +01:00 +01:00:00 +01 CET\n"); + test_tz( + "Australia/Sydney", + JAN2, + "+1100 +11:00 +11:00:00 +11 AEDT\n", + ); } #[test] @@ -649,7 +651,7 @@ fn test_date_tz_with_relative_time() { .arg("1 hour ago") .arg("+%Y-%m-%d %H:%M:%S %Z") .succeeds() - .stdout_matches(&Regex::new(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} PDT\n$").unwrap()); + .stdout_matches(&Regex::new(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} P[DS]T\n$").unwrap()); } #[test] From eb5fc4c4cb63307d9ce11a33e58e02304051621a Mon Sep 17 00:00:00 2001 From: Nicolas Boichat Date: Sun, 27 Apr 2025 14:26:00 +0800 Subject: [PATCH 09/12] Cross.toml: Install tzdata in container Linux tests require that now, as we now assume /usr/share/zoneinfo is present. --- .github/workflows/CICD.yml | 4 ---- Cross.toml | 7 +++++++ 2 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 Cross.toml diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index c9dcb2e35..7fa43165f 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -653,10 +653,6 @@ jobs: ;; esac outputs CARGO_TEST_OPTIONS - # ** pass needed environment into `cross` container (iff `cross` not already configured via "Cross.toml") - if [ "${CARGO_CMD}" = 'cross' ] && [ ! -e "Cross.toml" ] ; then - printf "[build.env]\npassthrough = [\"CI\", \"RUST_BACKTRACE\", \"CARGO_TERM_COLOR\"]\n" > Cross.toml - fi # * executable for `strip`? STRIP="strip" case ${{ matrix.job.target }} in diff --git a/Cross.toml b/Cross.toml new file mode 100644 index 000000000..52f5bad21 --- /dev/null +++ b/Cross.toml @@ -0,0 +1,7 @@ +# spell-checker:ignore (misc) dpkg noninteractive tzdata +[build] +pre-build = [ + "apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install tzdata", +] +[build.env] +passthrough = ["CI", "RUST_BACKTRACE", "CARGO_TERM_COLOR"] From 986bdf545da4ceaae0ae18339b11096a7059c704 Mon Sep 17 00:00:00 2001 From: Nicolas Boichat Date: Sun, 25 May 2025 16:06:55 +0200 Subject: [PATCH 10/12] uucore: Remove custom_tz_fmt, cleanup dependencies Nobody needs it anymore. --- Cargo.lock | 32 ------ Cargo.toml | 2 - fuzz/Cargo.lock | 102 +------------------ src/uu/date/Cargo.toml | 2 +- src/uu/ls/Cargo.toml | 1 - src/uucore/Cargo.toml | 3 - src/uucore/src/lib/features.rs | 2 - src/uucore/src/lib/features/custom_tz_fmt.rs | 60 ----------- src/uucore/src/lib/lib.rs | 2 - 9 files changed, 6 insertions(+), 200 deletions(-) delete mode 100644 src/uucore/src/lib/features/custom_tz_fmt.rs diff --git a/Cargo.lock b/Cargo.lock index 427a8166c..ab9d8d50b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -314,27 +314,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "chrono-tz" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efdce149c370f133a071ca8ef6ea340b7b88748ab0810097a9e2976eaa34b4f3" -dependencies = [ - "chrono", - "chrono-tz-build", - "phf", -] - -[[package]] -name = "chrono-tz-build" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94fea34d77a245229e7746bd2beb786cd2a896f306ff491fb8cecb3074b10a7" -dependencies = [ - "parse-zoneinfo", - "phf_codegen", -] - [[package]] name = "clang-sys" version = "1.8.1" @@ -1798,15 +1777,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "parse-zoneinfo" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" -dependencies = [ - "regex", -] - [[package]] name = "parse_datetime" version = "0.9.0" @@ -3738,7 +3708,6 @@ dependencies = [ "blake2b_simd", "blake3", "chrono", - "chrono-tz", "clap", "crc32fast", "data-encoding", @@ -3750,7 +3719,6 @@ dependencies = [ "fluent-bundle", "glob", "hex", - "iana-time-zone", "itertools 0.14.0", "libc", "md-5", diff --git a/Cargo.toml b/Cargo.toml index 44dda8c7d..046f1c6ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -282,7 +282,6 @@ chrono = { version = "0.4.41", default-features = false, features = [ "alloc", "clock", ] } -chrono-tz = "0.10.0" clap = { version = "4.5", features = ["wrap_help", "cargo"] } clap_complete = "4.4" clap_mangen = "0.2" @@ -301,7 +300,6 @@ gcd = "2.3" glob = "0.3.1" half = "2.4.1" hostname = "0.4" -iana-time-zone = "0.1.57" indicatif = "0.17.8" itertools = "0.14.0" jiff = { version = "0.2.10", default-features = false, features = [ diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index f7d500c6a..cd9aae5a4 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -219,27 +219,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "chrono-tz" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efdce149c370f133a071ca8ef6ea340b7b88748ab0810097a9e2976eaa34b4f3" -dependencies = [ - "chrono", - "chrono-tz-build", - "phf", -] - -[[package]] -name = "chrono-tz-build" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f10f8c9340e31fc120ff885fcdb54a0b48e474bbd77cab557f0c30a3e569402" -dependencies = [ - "parse-zoneinfo", - "phf_codegen", -] - [[package]] name = "clap" version = "4.5.38" @@ -872,15 +851,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "parse-zoneinfo" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" -dependencies = [ - "regex", -] - [[package]] name = "parse_datetime" version = "0.9.0" @@ -892,44 +862,6 @@ dependencies = [ "regex", ] -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_shared", -] - -[[package]] -name = "phf_codegen" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" -dependencies = [ - "phf_generator", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared", - "rand 0.8.5", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher", -] - [[package]] name = "pkg-config" version = "0.3.32" @@ -984,15 +916,6 @@ version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "rand_core 0.6.4", -] - [[package]] name = "rand" version = "0.9.1" @@ -1000,7 +923,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" dependencies = [ "rand_chacha", - "rand_core 0.9.3", + "rand_core", ] [[package]] @@ -1010,15 +933,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core", ] -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" - [[package]] name = "rand_core" version = "0.9.3" @@ -1189,12 +1106,6 @@ version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" -[[package]] -name = "siphasher" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" - [[package]] name = "sm3" version = "0.4.2" @@ -1442,7 +1353,7 @@ dependencies = [ "itertools", "memchr", "nix", - "rand 0.9.1", + "rand", "rayon", "self_cell", "tempfile", @@ -1499,8 +1410,6 @@ dependencies = [ "bigdecimal", "blake2b_simd", "blake3", - "chrono", - "chrono-tz", "clap", "crc32fast", "data-encoding", @@ -1511,7 +1420,6 @@ dependencies = [ "fluent-bundle", "glob", "hex", - "iana-time-zone", "itertools", "libc", "md-5", @@ -1538,7 +1446,7 @@ name = "uucore-fuzz" version = "0.0.0" dependencies = [ "libfuzzer-sys", - "rand 0.9.1", + "rand", "uu_cksum", "uu_cut", "uu_date", @@ -1571,7 +1479,7 @@ version = "0.1.0" dependencies = [ "console", "libc", - "rand 0.9.1", + "rand", "similar", "tempfile", "uucore", diff --git a/src/uu/date/Cargo.toml b/src/uu/date/Cargo.toml index f498befc7..af1e87fb4 100644 --- a/src/uu/date/Cargo.toml +++ b/src/uu/date/Cargo.toml @@ -26,7 +26,7 @@ jiff = { workspace = true, features = [ "tzdb-zoneinfo", "tzdb-concatenated", ] } -uucore = { workspace = true, features = ["custom-tz-fmt", "parser"] } +uucore = { workspace = true, features = ["parser"] } parse_datetime = { workspace = true } [target.'cfg(unix)'.dependencies] diff --git a/src/uu/ls/Cargo.toml b/src/uu/ls/Cargo.toml index 83a2f4fa5..ffb90a940 100644 --- a/src/uu/ls/Cargo.toml +++ b/src/uu/ls/Cargo.toml @@ -37,7 +37,6 @@ terminal_size = { workspace = true } thiserror = { workspace = true } uucore = { workspace = true, features = [ "colors", - "custom-tz-fmt", "entries", "format", "fs", diff --git a/src/uucore/Cargo.toml b/src/uucore/Cargo.toml index be2db18e5..7395df343 100644 --- a/src/uucore/Cargo.toml +++ b/src/uucore/Cargo.toml @@ -21,7 +21,6 @@ path = "src/lib/lib.rs" [dependencies] chrono = { workspace = true, optional = true } -chrono-tz = { workspace = true, optional = true } clap = { workspace = true } uucore_procs = { workspace = true } number_prefix = { workspace = true } @@ -29,7 +28,6 @@ dns-lookup = { workspace = true, optional = true } dunce = { version = "1.0.4", optional = true } wild = "2.2.1" glob = { workspace = true, optional = true } -iana-time-zone = { workspace = true, optional = true } itertools = { workspace = true, optional = true } time = { workspace = true, optional = true, features = [ "formatting", @@ -138,6 +136,5 @@ utf8 = [] utmpx = ["time", "time/macros", "libc", "dns-lookup"] version-cmp = [] wide = [] -custom-tz-fmt = ["chrono", "chrono-tz", "iana-time-zone"] tty = [] 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 257043e00..44db53071 100644 --- a/src/uucore/src/lib/features.rs +++ b/src/uucore/src/lib/features.rs @@ -14,8 +14,6 @@ pub mod buf_copy; pub mod checksum; #[cfg(feature = "colors")] pub mod colors; -#[cfg(feature = "custom-tz-fmt")] -pub mod custom_tz_fmt; #[cfg(feature = "encoding")] pub mod encoding; #[cfg(feature = "extendedbigdecimal")] diff --git a/src/uucore/src/lib/features/custom_tz_fmt.rs b/src/uucore/src/lib/features/custom_tz_fmt.rs deleted file mode 100644 index 0d2b6aebe..000000000 --- a/src/uucore/src/lib/features/custom_tz_fmt.rs +++ /dev/null @@ -1,60 +0,0 @@ -// This file is part of the uutils coreutils package. -// -// For the full copyright and license information, please view the LICENSE -// file that was distributed with this source code. - -use chrono::{TimeZone, Utc}; -use chrono_tz::{OffsetName, Tz}; -use iana_time_zone::get_timezone; - -/// Get the alphabetic abbreviation of the current timezone. -/// -/// For example, "UTC" or "CET" or "PDT" -fn timezone_abbreviation() -> String { - let tz = match std::env::var("TZ") { - // TODO Support other time zones... - Ok(s) if s == "UTC0" || s.is_empty() => Tz::Etc__UTC, - _ => match get_timezone() { - Ok(tz_str) => tz_str.parse().unwrap(), - Err(_) => Tz::Etc__UTC, - }, - }; - - let offset = tz.offset_from_utc_date(&Utc::now().date_naive()); - offset.abbreviation().unwrap_or("UTC").to_string() -} - -/// Adapt the given string to be accepted by the chrono library crate. -/// -/// # Arguments -/// -/// fmt: the format of the string -/// -/// # Return -/// -/// A string that can be used as parameter of the chrono functions that use formats -pub fn custom_time_format(fmt: &str) -> String { - // TODO - Revisit when chrono 0.5 is released. https://github.com/chronotope/chrono/issues/970 - // chrono crashes on %#z, but it's the same as %z anyway. - // GNU `date` uses `%N` for nano seconds, however the `chrono` crate uses `%f`. - fmt.replace("%#z", "%z") - .replace("%N", "%f") - .replace("%Z", timezone_abbreviation().as_ref()) -} - -#[cfg(test)] -mod tests { - use super::{custom_time_format, timezone_abbreviation}; - - #[test] - fn test_custom_time_format() { - assert_eq!(custom_time_format("%Y-%m-%d %H-%M-%S"), "%Y-%m-%d %H-%M-%S"); - assert_eq!(custom_time_format("%d-%m-%Y %H-%M-%S"), "%d-%m-%Y %H-%M-%S"); - assert_eq!(custom_time_format("%Y-%m-%d %H-%M-%S"), "%Y-%m-%d %H-%M-%S"); - assert_eq!( - custom_time_format("%Y-%m-%d %H-%M-%S.%N"), - "%Y-%m-%d %H-%M-%S.%f" - ); - assert_eq!(custom_time_format("%Z"), timezone_abbreviation()); - } -} diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index b1a9363f7..8dc49cadd 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -41,8 +41,6 @@ pub use crate::features::buf_copy; pub use crate::features::checksum; #[cfg(feature = "colors")] pub use crate::features::colors; -#[cfg(feature = "custom-tz-fmt")] -pub use crate::features::custom_tz_fmt; #[cfg(feature = "encoding")] pub use crate::features::encoding; #[cfg(feature = "extendedbigdecimal")] From 5d75e28b879e837aacd288b7b91b1902e60e9895 Mon Sep 17 00:00:00 2001 From: Nicolas Boichat Date: Sun, 1 Jun 2025 19:36:58 +0200 Subject: [PATCH 11/12] ls: Simplify TimeStyle::format Also, the comment does not fully apply anymore, so we can leave it more open-ended to figure out how to support locale. --- src/uu/ls/src/ls.rs | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index df55e10f1..b2c98689d 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -292,22 +292,18 @@ impl TimeStyle { let mut out = StdIoWrite(out); let config = jiff::fmt::strtime::Config::new().lenient(true); - match (self, recent) { - (Self::FullIso, _) => { - tm.format_with_config(&config, "%Y-%m-%d %H:%M:%S.%f %z", &mut out) - } - (Self::LongIso, _) => tm.format_with_config(&config, "%Y-%m-%d %H:%M", &mut out), - (Self::Iso, true) => tm.format_with_config(&config, "%m-%d %H:%M", &mut out), - (Self::Iso, false) => tm.format_with_config(&config, "%Y-%m-%d ", &mut out), - // spell-checker:ignore (word) datetime - //In this version of chrono translating can be done - //The function is chrono::datetime::DateTime::format_localized - //However it's currently still hard to get the current pure-rust-locale - //So it's not yet implemented - (Self::Locale, true) => tm.format_with_config(&config, "%b %e %H:%M", &mut out), - (Self::Locale, false) => tm.format_with_config(&config, "%b %e %Y", &mut out), - (Self::Format(fmt), _) => tm.format_with_config(&config, fmt, &mut out), - } + 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) } } From 66d1e8a8720627082147eb13da98ad833629bb1c Mon Sep 17 00:00:00 2001 From: Nicolas Boichat Date: Sun, 1 Jun 2025 20:00:42 +0200 Subject: [PATCH 12/12] test_date: Expand on test_date_utc_time Using the current time requires a bit of care, but it's nice to have a test that doesn't use a fixed date as input. --- tests/by-util/test_date.rs | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 713717f6a..c31498347 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -657,7 +657,39 @@ fn test_date_tz_with_relative_time() { #[test] fn test_date_utc_time() { // Test that -u flag shows correct UTC time - new_ucmd!().arg("-u").arg("+%H:%M").succeeds(); + // We get 2 UTC times just in case we're really unlucky and this runs around + // an hour change. + let utc_hour_1: i32 = new_ucmd!() + .env("TZ", "Asia/Taipei") + .arg("-u") + .arg("+%-H") + .succeeds() + .stdout_str() + .trim_end() + .parse() + .unwrap(); + let tpe_hour: i32 = new_ucmd!() + .env("TZ", "Asia/Taipei") + .arg("+%-H") + .succeeds() + .stdout_str() + .trim_end() + .parse() + .unwrap(); + let utc_hour_2: i32 = new_ucmd!() + .env("TZ", "Asia/Taipei") + .arg("-u") + .arg("+%-H") + .succeeds() + .stdout_str() + .trim_end() + .parse() + .unwrap(); + // Taipei is always 8 hours ahead of UTC (no daylight savings) + assert!( + (tpe_hour - utc_hour_1 + 24) % 24 == 8 || (tpe_hour - utc_hour_2 + 24) % 24 == 8, + "TPE: {tpe_hour} UTC: {utc_hour_1}/{utc_hour_2}" + ); // Test that -u flag shows UTC timezone new_ucmd!()