1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-30 04:27:45 +00:00

touch: move from time to chrono

This allows us to work with daylight savings time which is necessary to enable one of the tests. The leap second calculation and parsing are also ported over. A bump in the chrono version is necessary to use NaiveTime::MIN.
This commit is contained in:
Terts Diepraam 2023-03-24 11:51:38 +01:00
parent 4004281f34
commit c2997718cd
4 changed files with 85 additions and 190 deletions

2
Cargo.lock generated
View file

@ -3124,10 +3124,10 @@ dependencies = [
name = "uu_touch" name = "uu_touch"
version = "0.0.20" version = "0.0.20"
dependencies = [ dependencies = [
"chrono",
"clap", "clap",
"filetime", "filetime",
"parse_datetime", "parse_datetime",
"time",
"uucore", "uucore",
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]

View file

@ -18,13 +18,8 @@ path = "src/touch.rs"
[dependencies] [dependencies]
filetime = { workspace = true } filetime = { workspace = true }
clap = { workspace = true } clap = { workspace = true }
chrono = { workspace = true }
parse_datetime = { workspace = true } parse_datetime = { workspace = true }
time = { workspace = true, features = [
"parsing",
"formatting",
"local-offset",
"macros",
] }
uucore = { workspace = true, features = ["libc"] } uucore = { workspace = true, features = ["libc"] }
[target.'cfg(target_os = "windows")'.dependencies] [target.'cfg(target_os = "windows")'.dependencies]

View file

@ -6,16 +6,16 @@
// For the full copyright and license information, please view the LICENSE file // For the full copyright and license information, please view the LICENSE file
// that was distributed with this source code. // 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::builder::ValueParser;
use clap::{crate_version, Arg, ArgAction, ArgGroup, Command}; 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::ffi::OsString;
use std::fs::{self, File}; use std::fs::{self, File};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use time::macros::{format_description, offset, time};
use time::Duration;
use uucore::display::Quotable; use uucore::display::Quotable;
use uucore::error::{FromIo, UError, UResult, USimpleError}; use uucore::error::{FromIo, UError, UResult, USimpleError};
use uucore::{format_usage, help_about, help_usage, show}; use uucore::{format_usage, help_about, help_usage, show};
@ -41,27 +41,31 @@ pub mod options {
static ARG_FILES: &str = "files"; static ARG_FILES: &str = "files";
// Convert a date/time to a date with a TZ offset mod format {
fn to_local(tm: time::PrimitiveDateTime) -> time::OffsetDateTime { pub(crate) const POSIX_LOCALE: &str = "%a %b %e %H:%M:%S %Y";
let offset = match time::OffsetDateTime::now_local() { pub(crate) const ISO_8601: &str = "%Y-%m-%d";
Ok(lo) => lo.offset(), // "%Y%m%d%H%M.%S" 15 chars
Err(e) => { pub(crate) const YYYYMMDDHHMM_DOT_SS: &str = "%Y%m%d%H%M.%S";
panic!("error: {e}"); // "%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
tm.assume_offset(offset) 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 /// Convert a DateTime 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()) /// The DateTime is converted into a unix timestamp from which the FileTime is
} /// constructed.
fn datetime_to_filetime<T: TimeZone>(dt: &DateTime<T>) -> FileTime {
// Convert a date/time, considering that the input is in UTC time FileTime::from_unix_time(dt.timestamp(), dt.timestamp_subsec_nanos())
// 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)
} }
#[uucore::main] #[uucore::main]
@ -120,7 +124,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
{ {
parse_timestamp(ts)? parse_timestamp(ts)?
} else { } else {
local_dt_to_filetime(time::OffsetDateTime::now_local().unwrap()) datetime_to_filetime(&Local::now())
}; };
(timestamp, timestamp) (timestamp, timestamp)
} }
@ -212,7 +216,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
} else if matches.get_flag(options::NO_DEREF) { } else if matches.get_flag(options::NO_DEREF) {
set_symlink_file_times(path, atime, mtime) set_symlink_file_times(path, atime, mtime)
} else { } else {
filetime::set_file_times(path, atime, mtime) set_file_times(path, atime, mtime)
} }
.map_err_context(|| format!("setting times of {}", path.quote()))?; .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<FileTime> { fn parse_date(s: &str) -> UResult<FileTime> {
// This isn't actually compatible with GNU touch, but there doesn't seem to // 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 // be any simple specification for what format this parameter allows and I'm
@ -405,66 +351,61 @@ fn parse_date(s: &str) -> UResult<FileTime> {
// Tue Dec 3 ... // Tue Dec 3 ...
// ("%c", POSIX_LOCALE_FORMAT), // ("%c", POSIX_LOCALE_FORMAT),
// //
if let Ok(parsed) = time::PrimitiveDateTime::parse(s, &POSIX_LOCALE_FORMAT) { if let Ok(parsed) = Local.datetime_from_str(s, format::POSIX_LOCALE) {
return Ok(local_dt_to_filetime(to_local(parsed))); return Ok(datetime_to_filetime(&parsed));
} }
// Also support other formats found in the GNU tests like // Also support other formats found in the GNU tests like
// in tests/misc/stat-nanoseconds.sh // in tests/misc/stat-nanoseconds.sh
// or tests/touch/no-rights.sh // or tests/touch/no-rights.sh
for fmt in [ for fmt in [
YYYYMMDDHHMMS_FORMAT, format::YYYYMMDDHHMMS,
YYYYMMDDHHMMSS_FORMAT, format::YYYYMMDDHHMMSS,
YYYY_MM_DD_HH_MM_FORMAT, format::YYYY_MM_DD_HH_MM,
YYYYMMDDHHMM_OFFSET_FORMAT, format::YYYYMMDDHHMM_OFFSET,
] { ] {
if let Ok(parsed) = time::PrimitiveDateTime::parse(s, &fmt) { if let Ok(parsed) = Utc.datetime_from_str(s, fmt) {
return Ok(dt_to_filename(parsed)); return Ok(datetime_to_filetime(&parsed));
} }
} }
// "Equivalent to %Y-%m-%d (the ISO 8601 date format). (C99)" // "Equivalent to %Y-%m-%d (the ISO 8601 date format). (C99)"
// ("%F", ISO_8601_FORMAT), // ("%F", ISO_8601_FORMAT),
if let Ok(parsed) = time::Date::parse(s, &ISO_8601_FORMAT) { if let Ok(parsed_date) = NaiveDate::parse_from_str(s, format::ISO_8601) {
return Ok(local_dt_to_filetime(to_local( let parsed = Local
time::PrimitiveDateTime::new(parsed, time!(00:00)), .from_local_datetime(&parsed_date.and_time(NaiveTime::MIN))
))); .unwrap();
return Ok(datetime_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).)" // "@%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 s.bytes().next() == Some(b'@') {
if let Ok(ts) = &s[1..].parse::<i64>() { if let Ok(ts) = &s[1..].parse::<i64>() {
// Don't convert to local time in this case - seconds since epoch are not time-zone dependent return Ok(FileTime::from_unix_time(*ts, 0));
return Ok(local_dt_to_filetime(
time::OffsetDateTime::from_unix_timestamp(*ts).unwrap(),
));
} }
} }
if let Ok(duration) = parse_datetime::from_str(s) { if let Ok(duration) = parse_datetime::from_str(s) {
let now_local = time::OffsetDateTime::now_local().unwrap(); let dt = Local::now() + duration;
let diff = now_local return Ok(datetime_to_filetime(&dt));
.checked_add(time::Duration::nanoseconds(
duration.num_nanoseconds().unwrap(),
))
.unwrap();
return Ok(local_dt_to_filetime(diff));
} }
Err(USimpleError::new(1, format!("Unable to parse date: {s}"))) Err(USimpleError::new(1, format!("Unable to parse date: {s}")))
} }
fn parse_timestamp(s: &str) -> UResult<FileTime> { fn parse_timestamp(s: &str) -> UResult<FileTime> {
// TODO: handle error use format::*;
let now = time::OffsetDateTime::now_utc();
let (mut format, mut ts) = match s.chars().count() { let current_year = || Local::now().year();
15 => (YYYYMMDDHHMM_DOT_SS_FORMAT, s.to_owned()),
12 => (YYYYMMDDHHMM_FORMAT, s.to_owned()), let (format, ts) = match s.chars().count() {
13 => (YYMMDDHHMM_DOT_SS_FORMAT, s.to_owned()), 15 => (YYYYMMDDHHMM_DOT_SS, s.to_owned()),
10 => (YYMMDDHHMM_FORMAT, s.to_owned()), 12 => (YYYYMMDDHHMM, s.to_owned()),
11 => (YYYYMMDDHHMM_DOT_SS_FORMAT, format!("{}{}", now.year(), s)), // If we don't add "20", we have insufficient information to parse
8 => (YYYYMMDDHHMM_FORMAT, format!("{}{}", now.year(), s)), 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( return Err(USimpleError::new(
1, 1,
@ -472,54 +413,33 @@ fn parse_timestamp(s: &str) -> UResult<FileTime> {
)) ))
} }
}; };
// 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) let mut local = chrono::Local
&& ts.ends_with(".60") .datetime_from_str(&ts, format)
{
// 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)
.map_err(|_| USimpleError::new(1, format!("invalid date ts format {}", ts.quote())))?; .map_err(|_| USimpleError::new(1, format!("invalid date ts format {}", ts.quote())))?;
let mut local = to_local(tm);
if leap_sec { // Chrono caps seconds at 59, but 60 is valid. It might be a leap second
// We are dealing with a leap second, add it // or wrap to the next minute. But that doesn't really matter, because we
local = local.saturating_add(Duration::SECOND); // 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 // Due to daylight saving time switch, local time can jump from 1:59 AM to
// // time switch, local time can jump from 1:59 AM to 3:00 AM, // 3:00 AM, in which case any time between 2:00 AM and 2:59 AM is not
// // in which case any time between 2:00 AM and 2:59 AM is not valid. // valid. If we are within this jump, chrono takes the offset from before
// // Convert back to local time and see if we got the same value back. // the jump. If we then jump forward an hour, we get the new corrected
// let ts = time::Timespec { // offset. Jumping back will then now correctly take the jump into account.
// sec: ft.unix_seconds(), let local2 = local + Duration::hours(1) - Duration::hours(1);
// nsec: 0, if local.hour() != local2.hour() {
// }; return Err(USimpleError::new(
// let tm2 = time::at(ts); 1,
// if tm.tm_hour != tm2.tm_hour { format!("invalid date format {}", s.quote()),
// return Err(USimpleError::new( ));
// 1, }
// format!("invalid date format {}", s.quote()),
// ));
// }
Ok(ft) Ok(datetime_to_filetime(&local))
} }
// TODO: this may be a good candidate to put in fsext.rs // TODO: this may be a good candidate to put in fsext.rs

View file

@ -1,16 +1,8 @@
// spell-checker:ignore (formats) cymdhm cymdhms mdhm mdhms ymdhm ymdhms datetime mktime // 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 crate::common::util::{AtPath, TestScenario};
use chrono::TimeZone;
use filetime::{self, FileTime};
use std::fs::remove_file; use std::fs::remove_file;
use std::path::PathBuf; 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(); 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 { fn str_to_filetime(format: &str, s: &str) -> FileTime {
let format_description = match format { let tm = chrono::Utc.datetime_from_str(s, format).unwrap();
"%y%m%d%H%M" => format_description!("[year repr:last_two][month][day][hour][minute]"), FileTime::from_unix_time(tm.timestamp(), tm.timestamp_subsec_nanos())
"%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())
} }
#[test] #[test]
@ -667,7 +647,7 @@ fn test_touch_mtime_dst_succeeds() {
} }
#[test] #[test]
#[ignore = "not implemented"] #[cfg(unix)]
fn test_touch_mtime_dst_fails() { fn test_touch_mtime_dst_fails() {
let (_at, mut ucmd) = at_and_ucmd!(); let (_at, mut ucmd) = at_and_ucmd!();
let file = "test_touch_set_mtime_dst_fails"; let file = "test_touch_set_mtime_dst_fails";