From 44cf46a08e98b6eca47c626bea929b93b01cb1fc Mon Sep 17 00:00:00 2001 From: dcechano Date: Sun, 29 Oct 2023 18:03:02 -0400 Subject: [PATCH 1/2] Add date modification parsing --- src/uu/touch/src/touch.rs | 227 +++++++++++- src/uucore/src/lib/lib.rs | 1 + src/uucore/src/lib/parser.rs | 1 + .../src/lib/parser/parse_date_modifier.rs | 328 ++++++++++++++++++ 4 files changed, 550 insertions(+), 7 deletions(-) create mode 100644 src/uucore/src/lib/parser/parse_date_modifier.rs diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index d9399a051f6..8cc554ca913 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_date_modifier::{ChronoUnit, DateModParser}; use uucore::{format_usage, help_about, help_usage, show}; const ABOUT: &str = help_about!("touch.md"); @@ -339,8 +341,14 @@ 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) { + if modifier.is_empty() { + return Ok(datetime_to_filetime(&parsed.and_utc())); + } + return match date_from_modifier(modifier, parsed) { + Ok(new_date) => Ok(datetime_to_filetime(&new_date.and_utc())), + Err(e) => Err(e), + }; } // Also support other formats found in the GNU tests like @@ -352,18 +360,30 @@ fn parse_date(ref_time: DateTime, s: &str) -> UResult { format::YYYY_MM_DD_HH_MM, format::YYYYMMDDHHMM_OFFSET, ] { - 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) { + if modifier.is_empty() { + return Ok(datetime_to_filetime(&parsed.and_utc())); + } + return match date_from_modifier(modifier, parsed) { + Ok(new_date) => Ok(datetime_to_filetime(&new_date.and_utc())), + Err(e) => Err(e), + }; } } // "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)); + if modifier.is_empty() { + return Ok(datetime_to_filetime(&parsed)); + } + return match date_from_modifier(modifier, parsed) { + Ok(new_date) => Ok(datetime_to_filetime(&new_date)), + Err(e) => Err(e), + }; } // "@%s" is "The number of seconds since the Epoch, 1970-01-01 00:00:00 +0000 (UTC). (TZ) (Calculated from mktime(tm).)" @@ -437,6 +457,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 +621,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 +634,104 @@ 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"; + + 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); + } + } + + #[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"; + + 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()); + } } diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index 2f28195dae1..0abc1276f23 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -27,6 +27,7 @@ pub use crate::mods::os; pub use crate::mods::panic; // * string parsing modules +pub use crate::parser::parse_date_modifier; pub use crate::parser::parse_glob; pub use crate::parser::parse_size; pub use crate::parser::parse_time; diff --git a/src/uucore/src/lib/parser.rs b/src/uucore/src/lib/parser.rs index a0de6c0d4ef..7ea74cd6fba 100644 --- a/src/uucore/src/lib/parser.rs +++ b/src/uucore/src/lib/parser.rs @@ -2,6 +2,7 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +pub mod parse_date_modifier; pub mod parse_glob; pub mod parse_size; pub mod parse_time; diff --git a/src/uucore/src/lib/parser/parse_date_modifier.rs b/src/uucore/src/lib/parser/parse_date_modifier.rs new file mode 100644 index 00000000000..c72415f6a20 --- /dev/null +++ b/src/uucore/src/lib/parser/parse_date_modifier.rs @@ -0,0 +1,328 @@ +use std::collections::HashMap; +use ParseError::{EndOfString, IntOverflow, InvalidUnit, NoUnitPresent, UnexpectedToken}; +use ParseState::{ExpectChrono, ExpectNum}; + +/// 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_date_modifier::{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 { + while self.cursor < self.haystack.len() && self.haystack[self.cursor].is_ascii_whitespace() + { + self.cursor += 1; + } + if self.cursor >= self.haystack.len() { + return Err(EndOfString); + } + let bytes = &self.haystack[self.cursor..]; + if bytes[0] == b'+' || bytes[0] == b'-' || (bytes[0] > 47 && bytes[0] <= 57) { + let mut nums = vec![bytes[0] as char]; + let mut i = 1; + loop { + if i >= bytes.len() { + break; + } + if let n @ 48..=57 = 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; + continue; + } 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 { + while self.cursor < self.haystack.len() && self.haystack[self.cursor].is_ascii_whitespace() + { + self.cursor += 1; + } + if self.cursor >= self.haystack.len() { + return Err(NoUnitPresent); + } + let bytes = &self.haystack[self.cursor..].to_ascii_lowercase(); + match bytes[0] { + b'd' => { + if let Some(slice) = bytes.get(0..) { + if slice.starts_with(b"days") { + self.cursor += 4; + return Ok(ChronoUnit::Day); + } else if slice.starts_with(b"day") { + self.cursor += 3; + return Ok(ChronoUnit::Day); + } + } + } + b'w' => { + if let Some(slice) = bytes.get(0..) { + if slice.starts_with(b"weeks") { + self.cursor += 5; + return Ok(ChronoUnit::Week); + } else if slice.starts_with(b"week") { + self.cursor += 4; + return Ok(ChronoUnit::Week); + } + } + } + b'm' => { + if let Some(slice) = bytes.get(0..) { + if slice.starts_with(b"months") { + self.cursor += 6; + return Ok(ChronoUnit::Month); + } else if slice.starts_with(b"month") { + self.cursor += 5; + return Ok(ChronoUnit::Month); + } else if slice.starts_with(b"minutes") { + self.cursor += 7; + return Ok(ChronoUnit::Minute); + } else if slice.starts_with(b"minute") { + self.cursor += 6; + return Ok(ChronoUnit::Minute); + } + } + } + b'y' => { + if let Some(slice) = bytes.get(0..) { + if slice.starts_with(b"years") { + self.cursor += 5; + return Ok(ChronoUnit::Year); + } else if slice.starts_with(b"year") { + self.cursor += 4; + return Ok(ChronoUnit::Year); + } + } + } + b'h' => { + if let Some(slice) = bytes.get(0..) { + if slice.starts_with(b"hours") { + self.cursor += 5; + return Ok(ChronoUnit::Hour); + } else if slice.starts_with(b"hour") { + self.cursor += 4; + return Ok(ChronoUnit::Hour); + } + } + } + b's' => { + if let Some(slice) = bytes.get(0..) { + if slice.starts_with(b"seconds") { + self.cursor += 7; + return Ok(ChronoUnit::Second); + } else if slice.starts_with(b"second") { + self.cursor += 6; + return Ok(ChronoUnit::Second); + } + } + } + _ => { + return Err(InvalidUnit); + } + } + Err(InvalidUnit) + } +} +/// 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 std::collections::HashMap; + + 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_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 = "----"; + + #[test] + fn test_parse_ok() { + 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); + } + + #[test] + fn test_parse_err() { + 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()); + } +} From f27d28bef6d55aa7b37540fe58056959a5f58b2d Mon Sep 17 00:00:00 2001 From: dcechano Date: Mon, 30 Oct 2023 22:38:10 -0400 Subject: [PATCH 2/2] Add date modification parsing --- src/uu/touch/src/touch.rs | 60 ++-- src/uucore/src/lib/lib.rs | 1 - src/uucore/src/lib/parser.rs | 1 - .../src/lib/parser/parse_date_modifier.rs | 328 ------------------ src/uucore/src/lib/parser/parse_time.rs | 278 +++++++++++++++ tests/by-util/test_touch.rs | 59 +++- 6 files changed, 366 insertions(+), 361 deletions(-) delete mode 100644 src/uucore/src/lib/parser/parse_date_modifier.rs diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index 8cc554ca913..b7249a2b54f 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -19,7 +19,7 @@ use std::ops::{Add, Sub}; use std::path::{Path, PathBuf}; use uucore::display::Quotable; use uucore::error::{FromIo, UError, UResult, USimpleError}; -use uucore::parse_date_modifier::{ChronoUnit, DateModParser}; +use uucore::parse_time::{ChronoUnit, DateModParser}; use uucore::{format_usage, help_about, help_usage, show}; const ABOUT: &str = help_about!("touch.md"); @@ -342,12 +342,11 @@ fn parse_date(ref_time: DateTime, s: &str) -> UResult { // ("%c", POSIX_LOCALE_FORMAT), // if let Ok((parsed, modifier)) = NaiveDateTime::parse_and_remainder(s, format::POSIX_LOCALE) { - if modifier.is_empty() { + return if modifier.is_empty() { return Ok(datetime_to_filetime(&parsed.and_utc())); - } - return match date_from_modifier(modifier, parsed) { - Ok(new_date) => Ok(datetime_to_filetime(&new_date.and_utc())), - Err(e) => Err(e), + } else { + date_from_modifier(modifier, parsed) + .map(|new_date| datetime_to_filetime(&new_date.and_utc())) }; } @@ -355,18 +354,17 @@ fn parse_date(ref_time: DateTime, s: &str) -> UResult { // 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, modifier)) = NaiveDateTime::parse_and_remainder(s, fmt) { - if modifier.is_empty() { - return Ok(datetime_to_filetime(&parsed.and_utc())); - } - return match date_from_modifier(modifier, parsed) { - Ok(new_date) => Ok(datetime_to_filetime(&new_date.and_utc())), - Err(e) => Err(e), + 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())) }; } } @@ -377,12 +375,10 @@ fn parse_date(ref_time: DateTime, s: &str) -> UResult { let parsed = Local .from_local_datetime(&parsed_date.and_time(NaiveTime::MIN)) .unwrap(); - if modifier.is_empty() { - return Ok(datetime_to_filetime(&parsed)); - } - return match date_from_modifier(modifier, parsed) { - Ok(new_date) => Ok(datetime_to_filetime(&new_date)), - Err(e) => Err(e), + return if modifier.is_empty() { + Ok(datetime_to_filetime(&parsed)) + } else { + date_from_modifier(modifier, parsed).map(|new_date| datetime_to_filetime(&new_date)) }; } @@ -647,6 +643,8 @@ mod tests { 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) { @@ -707,6 +705,13 @@ mod tests { } 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] @@ -716,6 +721,12 @@ mod tests { 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(); @@ -733,5 +744,14 @@ mod tests { 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/lib.rs b/src/uucore/src/lib/lib.rs index 0abc1276f23..2f28195dae1 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -27,7 +27,6 @@ pub use crate::mods::os; pub use crate::mods::panic; // * string parsing modules -pub use crate::parser::parse_date_modifier; pub use crate::parser::parse_glob; pub use crate::parser::parse_size; pub use crate::parser::parse_time; diff --git a/src/uucore/src/lib/parser.rs b/src/uucore/src/lib/parser.rs index 7ea74cd6fba..a0de6c0d4ef 100644 --- a/src/uucore/src/lib/parser.rs +++ b/src/uucore/src/lib/parser.rs @@ -2,7 +2,6 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -pub mod parse_date_modifier; pub mod parse_glob; pub mod parse_size; pub mod parse_time; diff --git a/src/uucore/src/lib/parser/parse_date_modifier.rs b/src/uucore/src/lib/parser/parse_date_modifier.rs deleted file mode 100644 index c72415f6a20..00000000000 --- a/src/uucore/src/lib/parser/parse_date_modifier.rs +++ /dev/null @@ -1,328 +0,0 @@ -use std::collections::HashMap; -use ParseError::{EndOfString, IntOverflow, InvalidUnit, NoUnitPresent, UnexpectedToken}; -use ParseState::{ExpectChrono, ExpectNum}; - -/// 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_date_modifier::{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 { - while self.cursor < self.haystack.len() && self.haystack[self.cursor].is_ascii_whitespace() - { - self.cursor += 1; - } - if self.cursor >= self.haystack.len() { - return Err(EndOfString); - } - let bytes = &self.haystack[self.cursor..]; - if bytes[0] == b'+' || bytes[0] == b'-' || (bytes[0] > 47 && bytes[0] <= 57) { - let mut nums = vec![bytes[0] as char]; - let mut i = 1; - loop { - if i >= bytes.len() { - break; - } - if let n @ 48..=57 = 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; - continue; - } 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 { - while self.cursor < self.haystack.len() && self.haystack[self.cursor].is_ascii_whitespace() - { - self.cursor += 1; - } - if self.cursor >= self.haystack.len() { - return Err(NoUnitPresent); - } - let bytes = &self.haystack[self.cursor..].to_ascii_lowercase(); - match bytes[0] { - b'd' => { - if let Some(slice) = bytes.get(0..) { - if slice.starts_with(b"days") { - self.cursor += 4; - return Ok(ChronoUnit::Day); - } else if slice.starts_with(b"day") { - self.cursor += 3; - return Ok(ChronoUnit::Day); - } - } - } - b'w' => { - if let Some(slice) = bytes.get(0..) { - if slice.starts_with(b"weeks") { - self.cursor += 5; - return Ok(ChronoUnit::Week); - } else if slice.starts_with(b"week") { - self.cursor += 4; - return Ok(ChronoUnit::Week); - } - } - } - b'm' => { - if let Some(slice) = bytes.get(0..) { - if slice.starts_with(b"months") { - self.cursor += 6; - return Ok(ChronoUnit::Month); - } else if slice.starts_with(b"month") { - self.cursor += 5; - return Ok(ChronoUnit::Month); - } else if slice.starts_with(b"minutes") { - self.cursor += 7; - return Ok(ChronoUnit::Minute); - } else if slice.starts_with(b"minute") { - self.cursor += 6; - return Ok(ChronoUnit::Minute); - } - } - } - b'y' => { - if let Some(slice) = bytes.get(0..) { - if slice.starts_with(b"years") { - self.cursor += 5; - return Ok(ChronoUnit::Year); - } else if slice.starts_with(b"year") { - self.cursor += 4; - return Ok(ChronoUnit::Year); - } - } - } - b'h' => { - if let Some(slice) = bytes.get(0..) { - if slice.starts_with(b"hours") { - self.cursor += 5; - return Ok(ChronoUnit::Hour); - } else if slice.starts_with(b"hour") { - self.cursor += 4; - return Ok(ChronoUnit::Hour); - } - } - } - b's' => { - if let Some(slice) = bytes.get(0..) { - if slice.starts_with(b"seconds") { - self.cursor += 7; - return Ok(ChronoUnit::Second); - } else if slice.starts_with(b"second") { - self.cursor += 6; - return Ok(ChronoUnit::Second); - } - } - } - _ => { - return Err(InvalidUnit); - } - } - Err(InvalidUnit) - } -} -/// 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 std::collections::HashMap; - - 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_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 = "----"; - - #[test] - fn test_parse_ok() { - 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); - } - - #[test] - fn test_parse_err() { - 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/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.