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) };