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
99pub mod error;
1010
11- use chrono:: {
12- DateTime , Datelike , Duration , Local , LocalResult , NaiveDate , NaiveDateTime , NaiveTime ,
13- TimeZone , Timelike ,
14- } ;
1511use clap:: builder:: { PossibleValue , ValueParser } ;
1612use clap:: { Arg , ArgAction , ArgGroup , ArgMatches , Command } ;
1713use filetime:: { FileTime , set_file_times, set_symlink_file_times} ;
14+ use jiff:: { Timestamp , ToSpan , Zoned , civil:: DateTime , tz:: TimeZone } ;
1815use std:: borrow:: Cow ;
1916use std:: ffi:: OsString ;
2017use std:: fs:: { self , File } ;
@@ -28,6 +25,8 @@ use uucore::{format_usage, show};
2825
2926use 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
104103static 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`.
674622fn 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