diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index d9399a051f6..b7249a2b54f 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -7,7 +7,7 @@ // spell-checker:ignore (FORMATS) MMDDhhmm YYYYMMDDHHMM YYMMDDHHMM YYYYMMDDHHMMS use chrono::{ - DateTime, Datelike, Duration, Local, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, + DateTime, Datelike, Duration, Local, LocalResult, Months, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Timelike, }; use clap::builder::ValueParser; @@ -15,9 +15,11 @@ use clap::{crate_version, Arg, ArgAction, ArgGroup, Command}; use filetime::{set_file_times, set_symlink_file_times, FileTime}; use std::ffi::OsString; use std::fs::{self, File}; +use std::ops::{Add, Sub}; use std::path::{Path, PathBuf}; use uucore::display::Quotable; use uucore::error::{FromIo, UError, UResult, USimpleError}; +use uucore::parse_time::{ChronoUnit, DateModParser}; use uucore::{format_usage, help_about, help_usage, show}; const ABOUT: &str = help_about!("touch.md"); @@ -339,31 +341,45 @@ fn parse_date(ref_time: DateTime, s: &str) -> UResult { // Tue Dec 3 ... // ("%c", POSIX_LOCALE_FORMAT), // - if let Ok(parsed) = NaiveDateTime::parse_from_str(s, format::POSIX_LOCALE) { - return Ok(datetime_to_filetime(&parsed.and_utc())); + if let Ok((parsed, modifier)) = NaiveDateTime::parse_and_remainder(s, format::POSIX_LOCALE) { + return if modifier.is_empty() { + return Ok(datetime_to_filetime(&parsed.and_utc())); + } else { + date_from_modifier(modifier, parsed) + .map(|new_date| datetime_to_filetime(&new_date.and_utc())) + }; } // 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 [ - format::YYYYMMDDHHMMS, format::YYYYMMDDHHMMSS, - format::YYYY_MM_DD_HH_MM, + format::YYYYMMDDHHMMS, format::YYYYMMDDHHMM_OFFSET, + format::YYYY_MM_DD_HH_MM, ] { - if let Ok(parsed) = NaiveDateTime::parse_from_str(s, fmt) { - return Ok(datetime_to_filetime(&parsed.and_utc())); + if let Ok((parsed, modifier)) = NaiveDateTime::parse_and_remainder(s, fmt) { + return if modifier.is_empty() { + Ok(datetime_to_filetime(&parsed.and_utc())) + } else { + date_from_modifier(modifier, parsed) + .map(|new_date| datetime_to_filetime(&new_date.and_utc())) + }; } } // "Equivalent to %Y-%m-%d (the ISO 8601 date format). (C99)" // ("%F", ISO_8601_FORMAT), - if let Ok(parsed_date) = NaiveDate::parse_from_str(s, format::ISO_8601) { + if let Ok((parsed_date, modifier)) = NaiveDate::parse_and_remainder(s, format::ISO_8601) { let parsed = Local .from_local_datetime(&parsed_date.and_time(NaiveTime::MIN)) .unwrap(); - return Ok(datetime_to_filetime(&parsed)); + return if modifier.is_empty() { + Ok(datetime_to_filetime(&parsed)) + } else { + date_from_modifier(modifier, parsed).map(|new_date| datetime_to_filetime(&new_date)) + }; } // "@%s" is "The number of seconds since the Epoch, 1970-01-01 00:00:00 +0000 (UTC). (TZ) (Calculated from mktime(tm).)" @@ -437,6 +453,96 @@ fn parse_timestamp(s: &str) -> UResult { Ok(datetime_to_filetime(&local)) } +// Take a date and given an arbitrary string such as "+01 Month -20 YEARS -90 dayS" +// will parse the string and modify the date accordingly. +fn date_from_modifier(modifier: &str, mut date: D) -> UResult +where + D: Add + + Sub + + Add + + Sub, +{ + match DateModParser::parse(modifier) { + Ok(map) => { + // Convert to a sorted Vector here because order of operations does matter due to leap years. + // We want to make sure that we go *back* in time before we go forward. + let sorted = { + let mut v = map.into_iter().collect::>(); + v.sort_by(|a, b| a.1.cmp(&b.1)); + v + }; + for (chrono, time) in sorted { + match chrono { + ChronoUnit::Year => { + if time > (i64::MAX / 12) { + return Err(USimpleError::new( + 1, + format!("Unable to parse modifier: {modifier}"), + )); + } + date = if time >= 0 { + date.add(Months::new((12 * time) as u32)) + } else { + date.sub(Months::new(12 * time.unsigned_abs() as u32)) + } + } + ChronoUnit::Month => { + date = if time >= 0 { + date.add(Months::new(time as u32)) + } else { + date.sub(Months::new(time.unsigned_abs() as u32)) + } + } + ChronoUnit::Week => { + if !((i64::MIN / 604_800)..=(i64::MAX / 604_800)).contains(&time) { + return Err(USimpleError::new( + 1, + format!("Unable to parse modifier: {modifier}"), + )); + } + date = date.add(Duration::weeks(time)); + } + ChronoUnit::Day => { + if time > (i32::MAX as i64) || time < (i32::MIN as i64) { + return Err(USimpleError::new( + 1, + format!("Unable to parse modifier: {modifier}"), + )); + } + date = date.add(Duration::days(time)); + } + ChronoUnit::Hour => { + if !((i64::MIN / 3600)..=(i64::MAX / 3600)).contains(&time) { + return Err(USimpleError::new( + 1, + format!("Unable to parse modifier: {modifier}"), + )); + } + date = date.add(Duration::hours(time)); + } + ChronoUnit::Minute => { + if !((i64::MIN / 60)..=(i64::MAX / 60)).contains(&time) { + return Err(USimpleError::new( + 1, + format!("Unable to parse modifier: {modifier}"), + )); + } + date = date.add(Duration::minutes(time)); + } + ChronoUnit::Second => { + date = date.add(Duration::seconds(time)); + } + } + } + Ok(date) + } + Err(_) => Err(USimpleError::new( + 1, + format!("Unable to parse modifier: {modifier}"), + )), + } +} + // TODO: this may be a good candidate to put in fsext.rs /// Returns a PathBuf to stdout. /// @@ -511,6 +617,9 @@ fn pathbuf_from_stdout() -> UResult { #[cfg(test)] mod tests { + use crate::{date_from_modifier, format}; + use chrono::{NaiveDate, NaiveDateTime}; + #[cfg(windows)] #[test] fn test_get_pathbuf_from_stdout_fails_if_stdout_is_not_a_file() { @@ -521,4 +630,128 @@ mod tests { .to_string() .contains("GetFinalPathNameByHandleW failed with code 1")); } + + #[test] + fn test_parse_date_from_modifier_ok() { + const MODIFIER_OK_0: &str = "+01month"; + const MODIFIER_OK_1: &str = "00001year-000000001year+\t12months"; + const MODIFIER_OK_2: &str = ""; + const MODIFIER_OK_3: &str = "30SecONDS1houR"; + + const MODIFIER_OK_4: &str = "30 \t\n\t SECONDS000050000houR-10000yearS"; + + const MODIFIER_OK_5: &str = "+0000111MONTHs - 20 yearS 100000day"; + const MODIFIER_OK_6: &str = "100 week + 0024HOUrs - 50 minutes"; + + const MODIFIER_OK_7: &str = "-100 MONTHS 300 days + 20 \t YEARS"; + + let date0 = NaiveDate::parse_from_str("2022-05-15", format::ISO_8601).unwrap(); + + if let Ok(modified_date) = date_from_modifier(MODIFIER_OK_0, date0) { + let expected = NaiveDate::parse_from_str("2022-06-15", format::ISO_8601).unwrap(); + assert_eq!(modified_date, expected); + } else { + assert!(false); + } + + if let Ok(modified_date) = date_from_modifier(MODIFIER_OK_1, date0) { + let expected = NaiveDate::parse_from_str("2023-05-15", format::ISO_8601).unwrap(); + assert_eq!(modified_date, expected); + } else { + assert!(false); + } + + if let Ok(modified_date) = date_from_modifier(MODIFIER_OK_2, date0) { + let expected = NaiveDate::parse_from_str("2022-05-15", format::ISO_8601).unwrap(); + assert_eq!(modified_date, expected); + } else { + assert!(false); + } + + let date1 = + NaiveDateTime::parse_from_str("2022-05-15 18:30:00.0", format::YYYYMMDDHHMMSS).unwrap(); + if let Ok(modified_date) = date_from_modifier(MODIFIER_OK_3, date1) { + let expected = + NaiveDateTime::parse_from_str("2022-05-15 19:30:30.0", format::YYYYMMDDHHMMSS) + .unwrap(); + assert_eq!(modified_date, expected); + } else { + assert!(false); + } + + if let Ok(modified_date) = date_from_modifier(MODIFIER_OK_4, date1) { + let expected = + NaiveDateTime::parse_from_str("-7972-01-28 2:30:30.0", format::YYYYMMDDHHMMSS) + .unwrap(); + assert_eq!(modified_date, expected); + } else { + assert!(false); + } + + if let Ok(modified_date) = date_from_modifier(MODIFIER_OK_5, date0) { + let expected = NaiveDate::parse_from_str("2285-05-30", format::ISO_8601).unwrap(); + assert_eq!(modified_date, expected); + } else { + assert!(false); + } + + let date1 = + NaiveDateTime::parse_from_str("2022-05-15 0:0:00.0", format::YYYYMMDDHHMMSS).unwrap(); + if let Ok(modified_date) = date_from_modifier(MODIFIER_OK_6, date1) { + let expected = + NaiveDateTime::parse_from_str("2024-04-14 23:10:0.0", format::YYYYMMDDHHMMSS) + .unwrap(); + assert_eq!(modified_date, expected); + } else { + assert!(false); + } + + if let Ok(modified_date) = date_from_modifier(MODIFIER_OK_7, date0) { + let expected = NaiveDate::parse_from_str("2034-11-11", format::ISO_8601).unwrap(); + assert_eq!(modified_date, expected); + } else { + assert!(false); + } + } + + #[test] + fn test_parse_date_from_modifier_err() { + const MODIFIER_F_0: &str = "100000000000000000000000000000000000000 Years"; + const MODIFIER_F_1: &str = "1000"; + const MODIFIER_F_2: &str = " 1000 [YEARS]"; + const MODIFIER_F_3: &str = "-100 Years + 20.0 days "; + const MODIFIER_F_4: &str = "days + 10 weeks"; + // i64::MAX / 12 + 1 + const MODIFIER_F_5: &str = "768614336404564651 years"; + // i64::MAX / 604_800 (seconds/week) + const MODIFIER_F_6: &str = "15250284452472 weeks"; + // i32::MAX + const MODIFIER_F_7: &str = "9223372036854775808 days "; + + let date0 = NaiveDate::parse_from_str("2022-05-15", format::ISO_8601).unwrap(); + + let modified_date = date_from_modifier(MODIFIER_F_0, date0); + assert!(modified_date.is_err()); + + let modified_date = date_from_modifier(MODIFIER_F_1, date0); + assert!(modified_date.is_err()); + + let modified_date = date_from_modifier(MODIFIER_F_2, date0); + assert!(modified_date.is_err()); + + let modified_date = date_from_modifier(MODIFIER_F_3, date0); + assert!(modified_date.is_err()); + + let modified_date = date_from_modifier(MODIFIER_F_4, date0); + assert!(modified_date.is_err()); + + let modified_date = date_from_modifier(MODIFIER_F_5, date0); + assert!(modified_date.is_err()); + + let modified_date = date_from_modifier(MODIFIER_F_6, date0); + assert!(modified_date.is_err()); + + let modified_date = date_from_modifier(MODIFIER_F_7, date0); + assert!(modified_date.is_err()); + } } diff --git a/src/uucore/src/lib/parser/parse_time.rs b/src/uucore/src/lib/parser/parse_time.rs index 727ee28b1bf..c08f0714063 100644 --- a/src/uucore/src/lib/parser/parse_time.rs +++ b/src/uucore/src/lib/parser/parse_time.rs @@ -8,9 +8,14 @@ //! //! Use the [`from_str`] function to parse a [`Duration`] from a string. +use std::collections::HashMap; use std::time::Duration; use crate::display::Quotable; +use crate::parse_time::ParseError::{ + EndOfString, IntOverflow, InvalidUnit, NoUnitPresent, UnexpectedToken, +}; +use crate::parse_time::ParseState::{ExpectChrono, ExpectNum}; /// Parse a duration from a string. /// @@ -82,10 +87,191 @@ pub fn from_str(string: &str) -> Result { Ok(duration.saturating_mul(times)) } +/// Struct to parse a string such as '+0001 day 100years + 27 HOURS' +/// and return the parsed values as a `HashMap`. +/// +/// Functionality is exposed via `DateModParser::parse(haystack)` +/// +/// # Example +/// +/// ``` +/// use std::collections::HashMap; +/// use uucore::parse_time::{ChronoUnit, DateModParser}; +/// +/// let map: HashMap = DateModParser::parse("+0001 day 100years + 27 HOURS").unwrap(); +/// let expected = HashMap::from([ +/// (ChronoUnit::Day, 1), +/// (ChronoUnit::Year, 100), +/// (ChronoUnit::Hour, 27) +/// ]); +/// assert_eq!(map, expected); +/// ``` +pub struct DateModParser<'a> { + state: ParseState, + cursor: usize, + haystack: &'a [u8], +} + +impl<'a> DateModParser<'a> { + pub fn parse(haystack: &'a str) -> Result, ParseError> { + Self { + state: ExpectNum, + cursor: 0, + haystack: haystack.as_bytes(), + } + ._parse() + } + + #[allow(clippy::map_entry)] + fn _parse(&mut self) -> Result, ParseError> { + let mut map = HashMap::new(); + if self.haystack.is_empty() { + return Ok(map); + } + let mut curr_num = 0; + while self.cursor < self.haystack.len() { + match self.state { + ExpectNum => match self.parse_num() { + Ok(num) => { + curr_num = num; + self.state = ExpectChrono; + } + Err(EndOfString) => { + return Ok(map); + } + Err(e) => { + return Err(e); + } + }, + ExpectChrono => match self.parse_unit() { + Ok(chrono) => { + if map.contains_key(&chrono) { + *map.get_mut(&chrono).unwrap() += curr_num; + } else { + map.insert(chrono, curr_num); + } + self.state = ExpectNum; + } + Err(err) => { + return Err(err); + } + }, + } + } + Ok(map) + } + + fn parse_num(&mut self) -> Result { + self.skip_whitespace(); + if self.cursor >= self.haystack.len() { + return Err(EndOfString); + } + + const ASCII_0: u8 = 48; + const ASCII_9: u8 = 57; + let bytes = &self.haystack[self.cursor..]; + if bytes[0] == b'+' || bytes[0] == b'-' || (bytes[0] >= ASCII_0 && bytes[0] <= ASCII_9) { + let mut nums = vec![bytes[0] as char]; + let mut i = 1; + loop { + if i >= bytes.len() { + break; + } + if let n @ ASCII_0..=ASCII_9 = bytes[i] { + nums.push(n as char); + if bytes[i].is_ascii_whitespace() { + self.cursor += 1; + break; + } + self.cursor += 1; + i += 1; + } else if bytes[i].is_ascii_whitespace() { + self.cursor += 1; + i += 1; + } else { + self.cursor += 1; + break; + } + } + let n_as_string = nums.iter().collect::(); + n_as_string.parse::().map_err(|_| IntOverflow) + } else { + Err(UnexpectedToken) + } + } + + fn parse_unit(&mut self) -> Result { + self.skip_whitespace(); + if self.cursor >= self.haystack.len() { + return Err(NoUnitPresent); + } + + let units = [ + ("days", ChronoUnit::Day), + ("day", ChronoUnit::Day), + ("weeks", ChronoUnit::Week), + ("week", ChronoUnit::Week), + ("months", ChronoUnit::Month), + ("month", ChronoUnit::Month), + ("years", ChronoUnit::Year), + ("year", ChronoUnit::Year), + ("hours", ChronoUnit::Hour), + ("hour", ChronoUnit::Hour), + ("minutes", ChronoUnit::Minute), + ("minute", ChronoUnit::Minute), + ("seconds", ChronoUnit::Second), + ("second", ChronoUnit::Second), + ]; + let bytes = &self.haystack[self.cursor..].to_ascii_lowercase(); + for &(unit_str, chrono_unit) in &units { + if bytes.starts_with(unit_str.as_bytes()) { + self.cursor += unit_str.len(); + return Ok(chrono_unit); + } + } + Err(InvalidUnit) + } + + fn skip_whitespace(&mut self) { + while self.cursor < self.haystack.len() && self.haystack[self.cursor].is_ascii_whitespace() + { + self.cursor += 1; + } + } +} +/// Enum to represent units of time. +#[derive(Eq, Hash, PartialEq, Debug, Copy, Clone)] +pub enum ChronoUnit { + Day, + Week, + Month, + Year, + Hour, + Minute, + Second, +} + +#[derive(Eq, Hash, PartialEq, Debug, Copy, Clone)] +enum ParseState { + ExpectNum, + ExpectChrono, +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum ParseError { + UnexpectedToken, + NoUnitPresent, + EndOfString, + InvalidUnit, + IntOverflow, +} + #[cfg(test)] mod tests { + use super::{ChronoUnit, DateModParser}; use crate::parse_time::from_str; + use std::collections::HashMap; use std::time::Duration; #[test] @@ -136,4 +322,96 @@ mod tests { assert!(from_str("1H").is_err()); assert!(from_str("1D").is_err()); } + + #[test] + fn test_parse_ok() { + const HAYSTACK_OK_0: &str = "-10 year-10month +000011day +10year"; + const HAYSTACK_OK_1: &str = "-1000yeAR10MONTH-1 day "; + const HAYSTACK_OK_2: &str = "-000000100MONTH-1 seconds "; + const HAYSTACK_OK_3: &str = "+1000SECONDS-1yearS+000111HOURs "; + const HAYSTACK_OK_4: &str = "+1000SECONDS-1yearS000420minuTES "; + const HAYSTACK_OK_5: &str = "1 Month"; + const HAYSTACK_OK_6: &str = ""; + + let expected0 = HashMap::from([ + (ChronoUnit::Year, 0), + (ChronoUnit::Day, 11), + (ChronoUnit::Month, -10), + ]); + let test0 = DateModParser::parse(HAYSTACK_OK_0).unwrap(); + assert_eq!(expected0, test0); + + let expected1 = HashMap::from([ + (ChronoUnit::Year, -1000), + (ChronoUnit::Day, -1), + (ChronoUnit::Month, 10), + ]); + let test1 = DateModParser::parse(HAYSTACK_OK_1).unwrap(); + assert_eq!(expected1, test1); + + let expected2 = HashMap::from([(ChronoUnit::Second, -1), (ChronoUnit::Month, -100)]); + let test2 = DateModParser::parse(HAYSTACK_OK_2).unwrap(); + assert_eq!(expected2, test2); + + let expected3 = HashMap::from([ + (ChronoUnit::Second, 1000), + (ChronoUnit::Year, -1), + (ChronoUnit::Hour, 111), + ]); + let test3 = DateModParser::parse(HAYSTACK_OK_3).unwrap(); + assert_eq!(expected3, test3); + + let expected4 = HashMap::from([ + (ChronoUnit::Second, 1000), + (ChronoUnit::Year, -1), + (ChronoUnit::Minute, 420), + ]); + let test4 = DateModParser::parse(HAYSTACK_OK_4).unwrap(); + assert_eq!(expected4, test4); + + let expected5 = HashMap::from([(ChronoUnit::Month, 1)]); + let test5 = DateModParser::parse(HAYSTACK_OK_5).unwrap(); + assert_eq!(expected5, test5); + + let expected5 = HashMap::from([(ChronoUnit::Month, 1)]); + let test5 = DateModParser::parse(HAYSTACK_OK_5).unwrap(); + assert_eq!(expected5, test5); + + let expected6 = HashMap::new(); + let test6 = DateModParser::parse(HAYSTACK_OK_6).unwrap(); + assert_eq!(expected6, test6); + } + + #[test] + fn test_parse_err() { + const HAYSTACK_ERR_0: &str = "-10 yearz-10month +000011day +10year"; + const HAYSTACK_ERR_1: &str = "-10o0yeAR10MONTH-1 day "; + const HAYSTACK_ERR_2: &str = "+1000SECONDS-1yearS+000111HURs "; + const HAYSTACK_ERR_3: &str = + "+100000000000000000000000000000000000000000000000000000000SECONDS "; + const HAYSTACK_ERR_4: &str = "+100000"; + const HAYSTACK_ERR_5: &str = "years"; + const HAYSTACK_ERR_6: &str = "----"; + + let test0 = DateModParser::parse(HAYSTACK_ERR_0); + assert!(test0.is_err()); + + let test1 = DateModParser::parse(HAYSTACK_ERR_1); + assert!(test1.is_err()); + + let test2 = DateModParser::parse(HAYSTACK_ERR_2); + assert!(test2.is_err()); + + let test3 = DateModParser::parse(HAYSTACK_ERR_3); + assert!(test3.is_err()); + + let test4 = DateModParser::parse(HAYSTACK_ERR_4); + assert!(test4.is_err()); + + let test5 = DateModParser::parse(HAYSTACK_ERR_5); + assert!(test5.is_err()); + + let test6 = DateModParser::parse(HAYSTACK_ERR_6); + assert!(test6.is_err()); + } } diff --git a/tests/by-util/test_touch.rs b/tests/by-util/test_touch.rs index 7b659fc5155..1b185670433 100644 --- a/tests/by-util/test_touch.rs +++ b/tests/by-util/test_touch.rs @@ -500,22 +500,59 @@ fn test_touch_set_date6() { } #[test] -fn test_touch_set_date7() { - let (at, mut ucmd) = at_and_ucmd!(); +fn test_touch_set_with_modifier_ok() { let file = "test_touch_set_date"; - ucmd.args(&["-d", "2004-01-16 12:00 +0000", file]) - .succeeds() - .no_stderr(); + let times = [ + ("2004-01-16 12:00 +0000", 1_074_254_400), + ("2022-05-15 +01 month", 1655251200), + ("2022-05-15 00001year-000000001year+\t12months", 1684108800), + ("2022-05-15 18:30 -0400 30SecONDS1houR", 1652643030), + ("2022-05-15 +0000111MONTHs - 20 yearS 100000day", 9953366400), + ("2022-05-15 100 week + 0024HOUrs - 50 minutes", 1713136200), + ("2022-05-15 -100 MONTHS 300 days + 20 \t YEARS", 2046816000), + ("2022-05-15 0:0:0.0 -100 MONTHS 300 days + 20 \t YEARS", 2046816000), + ]; - assert!(at.file_exists(file)); + for (date, unix) in times { + let (at, mut ucmd) = at_and_ucmd!(); + ucmd.args(&["-d", date, file]) + .succeeds() + .no_stderr(); - let expected = FileTime::from_unix_time(1_074_254_400, 0); + assert!(at.file_exists(file)); - let (atime, mtime) = get_file_times(&at, file); - assert_eq!(atime, mtime); - assert_eq!(atime, expected); - assert_eq!(mtime, expected); + let expected = FileTime::from_unix_time(unix, 0); + + let (atime, mtime) = get_file_times(&at, file); + assert_eq!(atime, mtime); + assert_eq!(atime, expected); + assert_eq!(mtime, expected); + } +} + +#[test] +fn test_touch_set_with_modifier_err() { + let file = "test_touch_set_date"; + + let times = [ + ("2022-05-15", "100000000000000000000000000000000000000 Years"), + ("2022-05-15", "1000"), + ("2022-05-15", "1000 [YEARS]"), + ("2022-05-15", "-100 Years + 20.0 days "), + ("2022-05-15", "days + 10 weeks"), + ("2022-05-15", "768614336404564651 years"), + ("2022-05-15", "15250284452472 weeks"), + ("2022-05-15", "9223372036854775808 days ") + ]; + + for (date, modifier) in times { + let (_, mut ucmd) = at_and_ucmd!(); + let date_arg = format!("{}{}", date, modifier); + ucmd.args(&["-d", &date_arg, file]) + .fails() + .stderr_contains(format!("touch: Unable to parse modifier: {}", modifier)); + } } /// Test for setting the date by a relative time unit.