diff --git a/Cargo.lock b/Cargo.lock index b7f1c6c3c75..c6cd487f950 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3124,10 +3124,10 @@ dependencies = [ name = "uu_touch" version = "0.0.20" dependencies = [ + "chrono", "clap", "filetime", "parse_datetime", - "time", "uucore", "windows-sys 0.48.0", ] diff --git a/src/uu/touch/Cargo.toml b/src/uu/touch/Cargo.toml index cde016f8217..ce07520930c 100644 --- a/src/uu/touch/Cargo.toml +++ b/src/uu/touch/Cargo.toml @@ -18,13 +18,8 @@ path = "src/touch.rs" [dependencies] filetime = { workspace = true } clap = { workspace = true } +chrono = { workspace = true } parse_datetime = { workspace = true } -time = { workspace = true, features = [ - "parsing", - "formatting", - "local-offset", - "macros", -] } uucore = { workspace = true, features = ["libc"] } [target.'cfg(target_os = "windows")'.dependencies] diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index 230a6bb7046..9db01d7b8c7 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -6,16 +6,16 @@ // For the full copyright and license information, please view the LICENSE file // that was distributed with this source code. -// spell-checker:ignore (ToDO) filetime datetime MMDDhhmm lpszfilepath mktime YYYYMMDDHHMM YYMMDDHHMM DATETIME YYYYMMDDHHMMS subsecond humantime +// spell-checker:ignore (ToDO) filetime datetime lpszfilepath mktime DATETIME subsecond datelike timelike +// spell-checker:ignore (FORMATS) MMDDhhmm YYYYMMDDHHMM YYMMDDHHMM YYYYMMDDHHMMS +use chrono::{DateTime, Datelike, Duration, Local, NaiveDate, NaiveTime, TimeZone, Timelike, Utc}; use clap::builder::ValueParser; use clap::{crate_version, Arg, ArgAction, ArgGroup, Command}; -use filetime::{set_symlink_file_times, FileTime}; +use filetime::{set_file_times, set_symlink_file_times, FileTime}; use std::ffi::OsString; use std::fs::{self, File}; use std::path::{Path, PathBuf}; -use time::macros::{format_description, offset, time}; -use time::Duration; use uucore::display::Quotable; use uucore::error::{FromIo, UError, UResult, USimpleError}; use uucore::{format_usage, help_about, help_usage, show}; @@ -41,27 +41,31 @@ pub mod options { static ARG_FILES: &str = "files"; -// Convert a date/time to a date with a TZ offset -fn to_local(tm: time::PrimitiveDateTime) -> time::OffsetDateTime { - let offset = match time::OffsetDateTime::now_local() { - Ok(lo) => lo.offset(), - Err(e) => { - panic!("error: {e}"); - } - }; - tm.assume_offset(offset) +mod format { + pub(crate) const POSIX_LOCALE: &str = "%a %b %e %H:%M:%S %Y"; + pub(crate) const ISO_8601: &str = "%Y-%m-%d"; + // "%Y%m%d%H%M.%S" 15 chars + pub(crate) const YYYYMMDDHHMM_DOT_SS: &str = "%Y%m%d%H%M.%S"; + // "%Y-%m-%d %H:%M:%S.%SS" 12 chars + pub(crate) const YYYYMMDDHHMMSS: &str = "%Y-%m-%d %H:%M:%S.%f"; + // "%Y-%m-%d %H:%M:%S" 12 chars + pub(crate) const YYYYMMDDHHMMS: &str = "%Y-%m-%d %H:%M:%S"; + // "%Y-%m-%d %H:%M" 12 chars + // Used for example in tests/touch/no-rights.sh + pub(crate) const YYYY_MM_DD_HH_MM: &str = "%Y-%m-%d %H:%M"; + // "%Y%m%d%H%M" 12 chars + pub(crate) const YYYYMMDDHHMM: &str = "%Y%m%d%H%M"; + // "%Y-%m-%d %H:%M +offset" + // Used for example in tests/touch/relative.sh + pub(crate) const YYYYMMDDHHMM_OFFSET: &str = "%Y-%m-%d %H:%M %z"; } -// Convert a date/time with a TZ offset into a FileTime -fn local_dt_to_filetime(dt: time::OffsetDateTime) -> FileTime { - FileTime::from_unix_time(dt.unix_timestamp(), dt.nanosecond()) -} - -// Convert a date/time, considering that the input is in UTC time -// Used for touch -d 1970-01-01 18:43:33.023456789 for example -fn dt_to_filename(tm: time::PrimitiveDateTime) -> FileTime { - let dt = tm.assume_offset(offset!(UTC)); - local_dt_to_filetime(dt) +/// Convert a DateTime with a TZ offset into a FileTime +/// +/// The DateTime is converted into a unix timestamp from which the FileTime is +/// constructed. +fn dt_to_filetime(dt: &DateTime) -> FileTime { + FileTime::from_unix_time(dt.timestamp(), dt.timestamp_subsec_nanos()) } #[uucore::main] @@ -120,7 +124,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { { parse_timestamp(ts)? } else { - local_dt_to_filetime(time::OffsetDateTime::now_local().unwrap()) + dt_to_filetime(&Local::now()) }; (timestamp, timestamp) } @@ -212,7 +216,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } else if matches.get_flag(options::NO_DEREF) { set_symlink_file_times(path, atime, mtime) } else { - filetime::set_file_times(path, atime, mtime) + set_file_times(path, atime, mtime) } .map_err_context(|| format!("setting times of {}", path.quote()))?; } @@ -332,64 +336,6 @@ fn stat(path: &Path, follow: bool) -> UResult<(FileTime, FileTime)> { )) } -const POSIX_LOCALE_FORMAT: &[time::format_description::FormatItem] = format_description!( - "[weekday repr:short] [month repr:short] [day padding:space] \ - [hour]:[minute]:[second] [year]" -); - -const ISO_8601_FORMAT: &[time::format_description::FormatItem] = - format_description!("[year]-[month]-[day]"); - -// "%Y%m%d%H%M.%S" 15 chars -const YYYYMMDDHHMM_DOT_SS_FORMAT: &[time::format_description::FormatItem] = format_description!( - "[year repr:full][month repr:numerical padding:zero]\ - [day][hour][minute].[second]" -); - -// "%Y-%m-%d %H:%M:%S.%SS" 12 chars -const YYYYMMDDHHMMSS_FORMAT: &[time::format_description::FormatItem] = format_description!( - "[year repr:full]-[month repr:numerical padding:zero]-\ - [day] [hour]:[minute]:[second].[subsecond]" -); - -// "%Y-%m-%d %H:%M:%S" 12 chars -const YYYYMMDDHHMMS_FORMAT: &[time::format_description::FormatItem] = format_description!( - "[year repr:full]-[month repr:numerical padding:zero]-\ - [day] [hour]:[minute]:[second]" -); - -// "%Y-%m-%d %H:%M" 12 chars -// Used for example in tests/touch/no-rights.sh -const YYYY_MM_DD_HH_MM_FORMAT: &[time::format_description::FormatItem] = format_description!( - "[year repr:full]-[month repr:numerical padding:zero]-\ - [day] [hour]:[minute]" -); - -// "%Y%m%d%H%M" 12 chars -const YYYYMMDDHHMM_FORMAT: &[time::format_description::FormatItem] = format_description!( - "[year repr:full][month repr:numerical padding:zero]\ - [day][hour][minute]" -); - -// "%y%m%d%H%M.%S" 13 chars -const YYMMDDHHMM_DOT_SS_FORMAT: &[time::format_description::FormatItem] = format_description!( - "[year repr:last_two padding:none][month][day]\ - [hour][minute].[second]" -); - -// "%y%m%d%H%M" 10 chars -const YYMMDDHHMM_FORMAT: &[time::format_description::FormatItem] = format_description!( - "[year repr:last_two padding:none][month padding:zero][day padding:zero]\ - [hour repr:24 padding:zero][minute padding:zero]" -); - -// "%Y-%m-%d %H:%M +offset" -// Used for example in tests/touch/relative.sh -const YYYYMMDDHHMM_OFFSET_FORMAT: &[time::format_description::FormatItem] = format_description!( - "[year]-[month]-[day] [hour repr:24]:[minute] \ - [offset_hour sign:mandatory][offset_minute]" -); - fn parse_date(s: &str) -> UResult { // This isn't actually compatible with GNU touch, but there doesn't seem to // be any simple specification for what format this parameter allows and I'm @@ -405,66 +351,61 @@ fn parse_date(s: &str) -> UResult { // Tue Dec 3 ... // ("%c", POSIX_LOCALE_FORMAT), // - if let Ok(parsed) = time::PrimitiveDateTime::parse(s, &POSIX_LOCALE_FORMAT) { - return Ok(local_dt_to_filetime(to_local(parsed))); + if let Ok(parsed) = Local.datetime_from_str(s, format::POSIX_LOCALE) { + return Ok(dt_to_filetime(&parsed)); } // Also support other formats found in the GNU tests like // in tests/misc/stat-nanoseconds.sh // or tests/touch/no-rights.sh for fmt in [ - YYYYMMDDHHMMS_FORMAT, - YYYYMMDDHHMMSS_FORMAT, - YYYY_MM_DD_HH_MM_FORMAT, - YYYYMMDDHHMM_OFFSET_FORMAT, + format::YYYYMMDDHHMMS, + format::YYYYMMDDHHMMSS, + format::YYYY_MM_DD_HH_MM, + format::YYYYMMDDHHMM_OFFSET, ] { - if let Ok(parsed) = time::PrimitiveDateTime::parse(s, &fmt) { - return Ok(dt_to_filename(parsed)); + if let Ok(parsed) = Utc.datetime_from_str(s, fmt) { + return Ok(dt_to_filetime(&parsed)); } } // "Equivalent to %Y-%m-%d (the ISO 8601 date format). (C99)" // ("%F", ISO_8601_FORMAT), - if let Ok(parsed) = time::Date::parse(s, &ISO_8601_FORMAT) { - return Ok(local_dt_to_filetime(to_local( - time::PrimitiveDateTime::new(parsed, time!(00:00)), - ))); + if let Ok(parsed_date) = NaiveDate::parse_from_str(s, format::ISO_8601) { + let parsed = Local + .from_local_datetime(&parsed_date.and_time(NaiveTime::MIN)) + .unwrap(); + return Ok(dt_to_filetime(&parsed)); } // "@%s" is "The number of seconds since the Epoch, 1970-01-01 00:00:00 +0000 (UTC). (TZ) (Calculated from mktime(tm).)" if s.bytes().next() == Some(b'@') { if let Ok(ts) = &s[1..].parse::() { - // Don't convert to local time in this case - seconds since epoch are not time-zone dependent - return Ok(local_dt_to_filetime( - time::OffsetDateTime::from_unix_timestamp(*ts).unwrap(), - )); + return Ok(FileTime::from_unix_time(*ts, 0)); } } if let Ok(duration) = parse_datetime::from_str(s) { - let now_local = time::OffsetDateTime::now_local().unwrap(); - let diff = now_local - .checked_add(time::Duration::nanoseconds( - duration.num_nanoseconds().unwrap(), - )) - .unwrap(); - return Ok(local_dt_to_filetime(diff)); + let dt = Local::now() + duration; + return Ok(dt_to_filetime(&dt)); } Err(USimpleError::new(1, format!("Unable to parse date: {s}"))) } fn parse_timestamp(s: &str) -> UResult { - // TODO: handle error - let now = time::OffsetDateTime::now_utc(); - - let (mut format, mut ts) = match s.chars().count() { - 15 => (YYYYMMDDHHMM_DOT_SS_FORMAT, s.to_owned()), - 12 => (YYYYMMDDHHMM_FORMAT, s.to_owned()), - 13 => (YYMMDDHHMM_DOT_SS_FORMAT, s.to_owned()), - 10 => (YYMMDDHHMM_FORMAT, s.to_owned()), - 11 => (YYYYMMDDHHMM_DOT_SS_FORMAT, format!("{}{}", now.year(), s)), - 8 => (YYYYMMDDHHMM_FORMAT, format!("{}{}", now.year(), s)), + use format::*; + + let current_year = || Local::now().year(); + + let (format, ts) = match s.chars().count() { + 15 => (YYYYMMDDHHMM_DOT_SS, s.to_owned()), + 12 => (YYYYMMDDHHMM, s.to_owned()), + // If we don't add "20", we have insufficient information to parse + 13 => (YYYYMMDDHHMM_DOT_SS, format!("20{}", s)), + 10 => (YYYYMMDDHHMM, format!("20{}", s)), + 11 => (YYYYMMDDHHMM_DOT_SS, format!("{}{}", current_year(), s)), + 8 => (YYYYMMDDHHMM, format!("{}{}", current_year(), s)), _ => { return Err(USimpleError::new( 1, @@ -472,54 +413,34 @@ fn parse_timestamp(s: &str) -> UResult { )) } }; - // workaround time returning Err(TryFromParsed(InsufficientInformation)) for year w/ - // repr:last_two - // https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=1ccfac7c07c5d1c7887a11decf0e1996 - if s.chars().count() == 10 { - format = YYYYMMDDHHMM_FORMAT; - ts = "20".to_owned() + &ts; - } else if s.chars().count() == 13 { - format = YYYYMMDDHHMM_DOT_SS_FORMAT; - ts = "20".to_owned() + &ts; - } - let leap_sec = if (format == YYYYMMDDHHMM_DOT_SS_FORMAT || format == YYMMDDHHMM_DOT_SS_FORMAT) - && ts.ends_with(".60") - { - // Work around to disable leap seconds - // Used in gnu/tests/touch/60-seconds - ts = ts.replace(".60", ".59"); - true - } else { - false - }; - - let tm = time::PrimitiveDateTime::parse(&ts, &format) + let mut local = chrono::Local + .datetime_from_str(&ts, format) .map_err(|_| USimpleError::new(1, format!("invalid date ts format {}", ts.quote())))?; - let mut local = to_local(tm); - if leap_sec { - // We are dealing with a leap second, add it - local = local.saturating_add(Duration::SECOND); + + // Chrono caps seconds at 59, but 60 is valid. It might be a leap second + // or wrap to the next minute. But that doesn't really matter, because we + // only care about the timestamp anyway. + // Tested in gnu/tests/touch/60-seconds + if local.second() == 59 && ts.ends_with(".60") { + local += Duration::seconds(1); } - let ft = local_dt_to_filetime(local); - - // // We have to check that ft is valid time. Due to daylight saving - // // time switch, local time can jump from 1:59 AM to 3:00 AM, - // // in which case any time between 2:00 AM and 2:59 AM is not valid. - // // Convert back to local time and see if we got the same value back. - // let ts = time::Timespec { - // sec: ft.unix_seconds(), - // nsec: 0, - // }; - // let tm2 = time::at(ts); - // if tm.tm_hour != tm2.tm_hour { - // return Err(USimpleError::new( - // 1, - // format!("invalid date format {}", s.quote()), - // )); - // } - - Ok(ft) + + // We have to check that ft is valid time. Due to daylight saving + // time switch, local time can jump from 1:59 AM to 3:00 AM, + // in which case any time between 2:00 AM and 2:59 AM is not valid. + // If we are within this jump, chrono assumes takes the offset from before + // the jump. If we then jump forward an hour, we get the new corrected offset. + // Jumping back will then now correctly take the jump into account. + let local2 = local + Duration::hours(1) - Duration::hours(1); + if local.hour() != local2.hour() { + return Err(USimpleError::new( + 1, + format!("invalid date format {}", s.quote()), + )); + } + + Ok(dt_to_filetime(&local)) } // TODO: this may be a good candidate to put in fsext.rs diff --git a/tests/by-util/test_touch.rs b/tests/by-util/test_touch.rs index cd2a70bdb57..0ebbee1d7b2 100644 --- a/tests/by-util/test_touch.rs +++ b/tests/by-util/test_touch.rs @@ -1,16 +1,8 @@ // spell-checker:ignore (formats) cymdhm cymdhms mdhm mdhms ymdhm ymdhms datetime mktime -// This test relies on -// --cfg unsound_local_offset -// https://github.com/time-rs/time/blob/deb8161b84f355b31e39ce09e40c4d6ce3fea837/src/sys/local_offset_at/unix.rs#L112-L120= -// See https://github.com/time-rs/time/issues/293#issuecomment-946382614= -// Defined in .cargo/config - -use filetime::FileTime; - -use time::macros::format_description; - use crate::common::util::{AtPath, TestScenario}; +use chrono::TimeZone; +use filetime::{self, FileTime}; use std::fs::remove_file; use std::path::PathBuf; @@ -35,21 +27,9 @@ fn set_file_times(at: &AtPath, path: &str, atime: FileTime, mtime: FileTime) { filetime::set_file_times(at.plus_as_string(path), atime, mtime).unwrap(); } -// Adjusts for local timezone fn str_to_filetime(format: &str, s: &str) -> FileTime { - let format_description = match format { - "%y%m%d%H%M" => format_description!("[year repr:last_two][month][day][hour][minute]"), - "%y%m%d%H%M.%S" => { - format_description!("[year repr:last_two][month][day][hour][minute].[second]") - } - "%Y%m%d%H%M" => format_description!("[year][month][day][hour][minute]"), - "%Y%m%d%H%M.%S" => format_description!("[year][month][day][hour][minute].[second]"), - _ => panic!("unexpected dt format"), - }; - let tm = time::PrimitiveDateTime::parse(s, &format_description).unwrap(); - let d = time::OffsetDateTime::now_utc(); - let offset_dt = tm.assume_offset(d.offset()); - FileTime::from_unix_time(offset_dt.unix_timestamp(), tm.nanosecond()) + let tm = chrono::Utc.datetime_from_str(s, format).unwrap(); + FileTime::from_unix_time(tm.timestamp(), tm.timestamp_subsec_nanos()) } #[test] @@ -667,7 +647,7 @@ fn test_touch_mtime_dst_succeeds() { } #[test] -#[ignore = "not implemented"] +#[cfg(unix)] fn test_touch_mtime_dst_fails() { let (_at, mut ucmd) = at_and_ucmd!(); let file = "test_touch_set_mtime_dst_fails";