1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-29 12:07:46 +00:00

Merge pull request #4600 from tertsdiepraam/touch-move-from-time-to-chrono

`touch`: move from `time` to `chrono`
This commit is contained in:
Daniel Hofstetter 2023-07-27 17:57:51 +02:00 committed by GitHub
commit b24f91c4d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
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";