Skip to content

Commit 6625d0b

Browse files
yuankunzhangcakebaker
authored andcommitted
migrate date & touch utilities from chrono to jiff
1 parent 4c68e1e commit 6625d0b

7 files changed

Lines changed: 67 additions & 130 deletions

File tree

Cargo.lock

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ num-prime = "0.4.4"
352352
num-traits = "0.2.19"
353353
number_prefix = "0.4"
354354
onig = { version = "~6.5.1", default-features = false }
355-
parse_datetime = "0.11.0"
355+
parse_datetime = "0.13.0"
356356
phf = "0.13.1"
357357
phf_codegen = "0.13.1"
358358
platform-info = "2.0.3"

fuzz/Cargo.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/uu/date/src/date.rs

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -447,22 +447,13 @@ fn make_format_string(settings: &Settings) -> &str {
447447
}
448448

449449
/// Parse a `String` into a `DateTime`.
450-
/// If it fails, return a tuple of the `String` along with its `ParseError`.
451-
// TODO: Convert `parse_datetime` to jiff and remove wrapper from chrono to jiff structures.
450+
/// If it fails, return a tuple of the `String` along with its `ParseDateTimeError`.
452451
fn parse_date<S: AsRef<str> + Clone>(
453452
s: S,
454453
) -> Result<Zoned, (String, parse_datetime::ParseDateTimeError)> {
455-
match parse_datetime::parse_datetime(s.as_ref()) {
456-
Ok(date) => {
457-
let timestamp =
458-
Timestamp::new(date.timestamp(), date.timestamp_subsec_nanos() as i32).unwrap();
459-
Ok(Zoned::new(
460-
timestamp,
461-
TimeZone::try_system().unwrap_or(TimeZone::UTC),
462-
))
463-
}
464-
Err(e) => Err((s.as_ref().into(), e)),
465-
}
454+
parse_datetime::parse_datetime(s.as_ref())
455+
.map(|d| d.with_time_zone(TimeZone::try_system().unwrap_or(TimeZone::UTC)))
456+
.map_err(|e| (s.as_ref().into(), e))
466457
}
467458

468459
#[cfg(not(any(unix, windows)))]

src/uu/touch/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ path = "src/touch.rs"
2121
[dependencies]
2222
filetime = { workspace = true }
2323
clap = { workspace = true }
24-
chrono = { workspace = true }
24+
jiff = { workspace = true }
2525
parse_datetime = { workspace = true }
2626
thiserror = { workspace = true }
2727
uucore = { workspace = true, features = ["libc", "parser"] }

src/uu/touch/src/error.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ pub enum TouchError {
1616
#[error("{}", translate!("touch-error-unable-to-parse-date", "date" => .0.clone()))]
1717
InvalidDateFormat(String),
1818

19-
/// The source time couldn't be converted to a [`chrono::DateTime`]
19+
/// The source time couldn't be converted to a [`jiff::civil::DateTime`]
2020
#[error("{}", translate!("touch-error-invalid-filetime", "time" => .0))]
2121
InvalidFiletime(FileTime),
2222

src/uu/touch/src/touch.rs

Lines changed: 53 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,14 @@
44
// file that was distributed with this source code.
55

66
// spell-checker:ignore (ToDO) filetime datetime lpszfilepath mktime DATETIME datelike timelike
7-
// spell-checker:ignore (FORMATS) MMDDhhmm YYYYMMDDHHMM YYMMDDHHMM YYYYMMDDHHMMS
7+
// spell-checker:ignore (FORMATS) mmddhhmm YYYYMMDDHHMM
88

99
pub mod error;
1010

11-
use chrono::{
12-
DateTime, Datelike, Duration, Local, LocalResult, NaiveDate, NaiveDateTime, NaiveTime,
13-
TimeZone, Timelike,
14-
};
1511
use clap::builder::{PossibleValue, ValueParser};
1612
use clap::{Arg, ArgAction, ArgGroup, ArgMatches, Command};
1713
use filetime::{FileTime, set_file_times, set_symlink_file_times};
14+
use jiff::{Timestamp, ToSpan, Zoned, civil::DateTime, tz::TimeZone};
1815
use std::borrow::Cow;
1916
use std::ffi::OsString;
2017
use std::fs::{self, File};
@@ -28,6 +25,8 @@ use uucore::{format_usage, show};
2825

2926
use crate::error::TouchError;
3027

28+
const NANO: i128 = 1_000_000_000;
29+
3130
/// Options contains all the possible behaviors and flags for touch.
3231
///
3332
/// All options are public so that the options can be programmatically
@@ -103,35 +102,32 @@ pub mod options {
103102

104103
static ARG_FILES: &str = "files";
105104

106-
mod format {
107-
pub(crate) const POSIX_LOCALE: &str = "%a %b %e %H:%M:%S %Y";
108-
pub(crate) const ISO_8601: &str = "%Y-%m-%d";
109-
// "%Y%m%d%H%M.%S" 15 chars
110-
pub(crate) const YYYYMMDDHHMM_DOT_SS: &str = "%Y%m%d%H%M.%S";
111-
// "%Y-%m-%d %H:%M:%S.%SS" 12 chars
112-
pub(crate) const YYYYMMDDHHMMSS: &str = "%Y-%m-%d %H:%M:%S.%f";
113-
// "%Y-%m-%d %H:%M:%S" 12 chars
114-
pub(crate) const YYYYMMDDHHMMS: &str = "%Y-%m-%d %H:%M:%S";
115-
// "%Y-%m-%d %H:%M" 12 chars
116-
// Used for example in tests/touch/no-rights.sh
117-
pub(crate) const YYYY_MM_DD_HH_MM: &str = "%Y-%m-%d %H:%M";
118-
// "%Y%m%d%H%M" 12 chars
119-
pub(crate) const YYYYMMDDHHMM: &str = "%Y%m%d%H%M";
120-
// "%Y-%m-%d %H:%M +offset"
121-
// Used for example in tests/touch/relative.sh
122-
pub(crate) const YYYYMMDDHHMM_OFFSET: &str = "%Y-%m-%d %H:%M %z";
123-
}
124-
125-
/// Convert a [`DateTime`] with a TZ offset into a [`FileTime`]
105+
/// Convert a [`Zoned`] into a [`FileTime`]
106+
///
107+
/// The [`Zoned`] is converted into a unix timestamp from which the [`FileTime`]
108+
/// is constructed.
126109
///
127-
/// The [`DateTime`] is converted into a unix timestamp from which the [`FileTime`] is
128-
/// constructed.
129-
fn datetime_to_filetime<T: TimeZone>(dt: &DateTime<T>) -> FileTime {
130-
FileTime::from_unix_time(dt.timestamp(), dt.timestamp_subsec_nanos())
110+
/// This function panics if the timestamp cannot be represented as seconds or
111+
/// nanoseconds within the valid ranges.
112+
fn datetime_to_filetime(dt: &Zoned) -> FileTime {
113+
let ns = dt.timestamp().as_nanosecond();
114+
FileTime::from_unix_time(
115+
i64::try_from(ns.div_euclid(NANO)).expect("seconds out of i64 range"),
116+
u32::try_from(ns.rem_euclid(NANO)).expect("nanoseconds out of u32 range"),
117+
)
131118
}
132119

133-
fn filetime_to_datetime(ft: &FileTime) -> Option<DateTime<Local>> {
134-
Some(DateTime::from_timestamp(ft.unix_seconds(), ft.nanoseconds())?.into())
120+
fn filetime_to_datetime(ft: &FileTime) -> Option<Zoned> {
121+
let s = i128::from(ft.seconds());
122+
let ns = i128::from(ft.nanoseconds());
123+
124+
// Validate that nanoseconds are in valid range (0-999,999,999)
125+
if ns >= NANO {
126+
return None;
127+
}
128+
129+
let ts = Timestamp::from_nanosecond(s.checked_mul(NANO)?.checked_add(ns)?).ok()?;
130+
Some(ts.to_zoned(TimeZone::system()))
135131
}
136132

137133
/// Whether all characters in the string are digits.
@@ -376,7 +372,7 @@ pub fn touch(files: &[InputFile], opts: &Options) -> Result<(), TouchError> {
376372
(atime, mtime)
377373
}
378374
Source::Now => {
379-
let now = datetime_to_filetime(&Local::now());
375+
let now = datetime_to_filetime(&Zoned::now());
380376
(now, now)
381377
}
382378
&Source::Timestamp(ts) => (ts, ts),
@@ -588,55 +584,7 @@ fn stat(path: &Path, follow: bool) -> std::io::Result<(FileTime, FileTime)> {
588584
))
589585
}
590586

591-
fn parse_date(ref_time: DateTime<Local>, s: &str) -> Result<FileTime, TouchError> {
592-
// This isn't actually compatible with GNU touch, but there doesn't seem to
593-
// be any simple specification for what format this parameter allows and I'm
594-
// not about to implement GNU parse_datetime.
595-
// http://git.savannah.gnu.org/gitweb/?p=gnulib.git;a=blob_plain;f=lib/parse-datetime.y
596-
597-
// TODO: match on char count?
598-
599-
// "The preferred date and time representation for the current locale."
600-
// "(In the POSIX locale this is equivalent to %a %b %e %H:%M:%S %Y.)"
601-
// time 0.1.43 parsed this as 'a b e T Y'
602-
// which is equivalent to the POSIX locale: %a %b %e %H:%M:%S %Y
603-
// Tue Dec 3 ...
604-
// ("%c", POSIX_LOCALE_FORMAT),
605-
//
606-
if let Ok(parsed) = NaiveDateTime::parse_from_str(s, format::POSIX_LOCALE) {
607-
return Ok(datetime_to_filetime(&parsed.and_utc()));
608-
}
609-
610-
// Also support other formats found in the GNU tests like
611-
// in tests/misc/stat-nanoseconds.sh
612-
// or tests/touch/no-rights.sh
613-
for fmt in [
614-
format::YYYYMMDDHHMMS,
615-
format::YYYYMMDDHHMMSS,
616-
format::YYYY_MM_DD_HH_MM,
617-
format::YYYYMMDDHHMM_OFFSET,
618-
] {
619-
if let Ok(parsed) = NaiveDateTime::parse_from_str(s, fmt) {
620-
return Ok(datetime_to_filetime(&parsed.and_utc()));
621-
}
622-
}
623-
624-
// "Equivalent to %Y-%m-%d (the ISO 8601 date format). (C99)"
625-
// ("%F", ISO_8601_FORMAT),
626-
if let Ok(parsed_date) = NaiveDate::parse_from_str(s, format::ISO_8601) {
627-
let parsed = Local
628-
.from_local_datetime(&parsed_date.and_time(NaiveTime::MIN))
629-
.unwrap();
630-
return Ok(datetime_to_filetime(&parsed));
631-
}
632-
633-
// "@%s" is "The number of seconds since the Epoch, 1970-01-01 00:00:00 +0000 (UTC). (TZ) (Calculated from mktime(tm).)"
634-
if s.bytes().next() == Some(b'@') {
635-
if let Ok(ts) = &s[1..].parse::<i64>() {
636-
return Ok(FileTime::from_unix_time(*ts, 0));
637-
}
638-
}
639-
587+
fn parse_date(ref_time: Zoned, s: &str) -> Result<FileTime, TouchError> {
640588
if let Ok(dt) = parse_datetime::parse_datetime_at_date(ref_time, s) {
641589
return Ok(datetime_to_filetime(&dt));
642590
}
@@ -672,9 +620,12 @@ fn prepend_century(s: &str) -> UResult<String> {
672620
/// then cc is 20 for years in the range 0 … 68, and 19 for years in 69 … 99.
673621
/// in order to be compatible with GNU `touch`.
674622
fn parse_timestamp(s: &str) -> UResult<FileTime> {
675-
use format::*;
623+
// "%Y%m%d%H%M.%S" 15 chars
624+
const YYYYMMDDHHMM_DOT_SS: &str = "%Y%m%d%H%M.%S";
625+
// "%Y%m%d%H%M" 12 chars
626+
const YYYYMMDDHHMM: &str = "%Y%m%d%H%M";
676627

677-
let current_year = || Local::now().year();
628+
let current_year = || Zoned::now().year();
678629

679630
let (format, ts) = match s.chars().count() {
680631
15 => (YYYYMMDDHHMM_DOT_SS, s.to_owned()),
@@ -692,38 +643,33 @@ fn parse_timestamp(s: &str) -> UResult<FileTime> {
692643
}
693644
};
694645

695-
let local = NaiveDateTime::parse_from_str(&ts, format).map_err(|_| {
646+
let dt = DateTime::strptime(format, &ts).map_err(|_| {
696647
USimpleError::new(
697648
1,
698649
translate!("touch-error-invalid-date-ts-format", "date" => ts.quote()),
699650
)
700651
})?;
701-
let LocalResult::Single(mut local) = Local.from_local_datetime(&local) else {
702-
return Err(USimpleError::new(
703-
1,
704-
translate!("touch-error-invalid-date-ts-format", "date" => ts.quote()),
705-
));
706-
};
707652

708-
// Chrono caps seconds at 59, but 60 is valid. It might be a leap second
709-
// or wrap to the next minute. But that doesn't really matter, because we
710-
// only care about the timestamp anyway.
653+
// Convert the datetime into a `Zoned` object in the system time zone. If
654+
// the datetime in the system time zone is ambiguous (e.g., during the "fall
655+
// back" or "jump forward" of daylight saving time), the conversion is
656+
// rejected and an error is returned.
657+
let mut local = TimeZone::system()
658+
.to_ambiguous_zoned(dt)
659+
.unambiguous()
660+
.map_err(|_| {
661+
USimpleError::new(
662+
1,
663+
translate!("touch-error-invalid-date-ts-format", "date" => ts.quote()),
664+
)
665+
})?;
666+
667+
// Jiff caps seconds at 59, but 60 is valid. It might be a leap second or
668+
// wrap to the next minute. But that doesn't really matter, because we only
669+
// care about the timestamp anyway.
711670
// Tested in gnu/tests/touch/60-seconds
712671
if local.second() == 59 && ts.ends_with(".60") {
713-
local += Duration::try_seconds(1).unwrap();
714-
}
715-
716-
// Due to daylight saving time switch, local time can jump from 1:59 AM to
717-
// 3:00 AM, in which case any time between 2:00 AM and 2:59 AM is not
718-
// valid. If we are within this jump, chrono takes the offset from before
719-
// the jump. If we then jump forward an hour, we get the new corrected
720-
// offset. Jumping back will then now correctly take the jump into account.
721-
let local2 = local + Duration::try_hours(1).unwrap() - Duration::try_hours(1).unwrap();
722-
if local.hour() != local2.hour() {
723-
return Err(USimpleError::new(
724-
1,
725-
translate!("touch-error-invalid-date-format", "date" => s.quote()),
726-
));
672+
local += 1.second();
727673
}
728674

729675
Ok(datetime_to_filetime(&local))

0 commit comments

Comments
 (0)