diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 5b0223703..2fb8dc54e 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/Cargo.lock b/Cargo.lock index 07913d096..079120422 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" @@ -1337,6 +1316,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" @@ -1747,15 +1767,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" @@ -1839,6 +1850,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" @@ -2808,6 +2828,7 @@ version = "0.1.0" dependencies = [ "chrono", "clap", + "jiff", "libc", "parse_datetime", "uucore", @@ -3079,6 +3100,7 @@ dependencies = [ "clap", "glob", "hostname", + "jiff", "lscolors", "number_prefix", "selinux", @@ -3675,7 +3697,6 @@ dependencies = [ "blake2b_simd", "blake3", "chrono", - "chrono-tz", "clap", "crc32fast", "data-encoding", @@ -3687,7 +3708,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 4a61b5f03..1b58faf6b 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" @@ -300,9 +299,13 @@ 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 = [ + "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/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"] diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 4495a0a5a..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" @@ -644,6 +623,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" @@ -831,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" @@ -851,50 +862,27 @@ 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" 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" @@ -928,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" @@ -944,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]] @@ -954,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" @@ -1133,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" @@ -1316,6 +1283,7 @@ version = "0.1.0" dependencies = [ "chrono", "clap", + "jiff", "libc", "parse_datetime", "uucore", @@ -1385,7 +1353,7 @@ dependencies = [ "itertools", "memchr", "nix", - "rand 0.9.1", + "rand", "rayon", "self_cell", "tempfile", @@ -1442,8 +1410,6 @@ dependencies = [ "bigdecimal", "blake2b_simd", "blake3", - "chrono", - "chrono-tz", "clap", "crc32fast", "data-encoding", @@ -1454,7 +1420,6 @@ dependencies = [ "fluent-bundle", "glob", "hex", - "iana-time-zone", "itertools", "libc", "md-5", @@ -1481,7 +1446,7 @@ name = "uucore-fuzz" version = "0.0.0" dependencies = [ "libfuzzer-sys", - "rand 0.9.1", + "rand", "uu_cksum", "uu_cut", "uu_date", @@ -1514,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 087d4befc..af1e87fb4 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,9 +19,14 @@ workspace = true path = "src/date.rs" [dependencies] -chrono = { workspace = true } clap = { workspace = true } -uucore = { workspace = true, features = ["custom-tz-fmt", "parser"] } +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 = ["parser"] } parse_datetime = { workspace = true } [target.'cfg(unix)'.dependencies] 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) }; diff --git a/src/uu/ls/Cargo.toml b/src/uu/ls/Cargo.toml index ff00175e7..ffb90a940 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 } @@ -30,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/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 0297a569c..b2c98689d 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)] @@ -16,24 +16,24 @@ 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::format::{Item, StrftimeItems}; -use chrono::{DateTime, Local, TimeDelta}; use clap::{ Arg, ArgAction, Command, 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; @@ -59,7 +59,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,64 +273,37 @@ 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: Timestamp, state: &mut ListState) -> bool { + // According to GNU a Gregorian year has 365.2425 * 24 * 60 * 60 == 31556952 seconds on the average. + time > state.recent_time_threshold } -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 +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, }; - TimeStyler { - default, - recent, - recent_time_threshold, - } - } - - /// Format a DateTime, using `recent` format if available, and the DateTime - /// is recent enough. - 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()) - } - .to_string() + tm.format_with_config(&config, fmt, &mut out) } } @@ -2093,8 +2065,7 @@ struct ListState<'a> { uid_cache: HashMap, #[cfg(unix)] gid_cache: HashMap, - - time_styler: TimeStyler, + recent_time_threshold: Timestamp, } #[allow(clippy::cognitive_complexity)] @@ -2111,7 +2082,7 @@ 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), + recent_time_threshold: Timestamp::now() - Duration::new(31_556_952 / 2, 0), }; for loc in locs { @@ -2907,7 +2878,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( @@ -3106,15 +3077,27 @@ 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, 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) => state.time_styler.format(time), - 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(()) + } } } 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")] diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 09cf7ac79..c31498347 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,11 +566,153 @@ fn test_date_from_stdin() { ); } +const JAN2: &str = "2024-01-02 12:00:00 +0000"; +const JUL2: &str = "2024-07-02 12:00:00 +0000"; + #[test] -fn test_date_empty_tz() { +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); + } + + // 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 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] +fn test_date_tz_with_utc_flag() { new_ucmd!() - .env("TZ", "") + .env("TZ", "Europe/Berlin") + .arg("-u") .arg("+%Z") .succeeds() .stdout_only("UTC\n"); } + +#[test] +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] +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} P[DS]T\n$").unwrap()); +} + +#[test] +fn test_date_utc_time() { + // Test that -u flag shows correct UTC time + // 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!() + .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"); +}