Skip to content

Commit d3ae31e

Browse files
committed
move more stuff - TODO
1 parent 0eebd74 commit d3ae31e

File tree

3 files changed

+314
-0
lines changed

3 files changed

+314
-0
lines changed
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// This file is part of the uutils coreutils package.
2+
//
3+
// For the full copyright and license information, please view the LICENSE
4+
// file that was distributed with this source code.
5+
use crate::error::{FromIo, UError, UResult, USimpleError};
6+
use crate::parse_date_common::{
7+
dt_to_filename, local_dt_to_filetime, to_local, ISO_8601_FORMAT, POSIX_LOCALE_FORMAT,
8+
YYMMDDHHMM_DOT_SS_FORMAT, YYMMDDHHMM_FORMAT, YYYYMMDDHHMMSS_FORMAT, YYYYMMDDHHMMS_FORMAT,
9+
YYYYMMDDHHMM_DOT_SS_FORMAT, YYYYMMDDHHMM_FORMAT, YYYYMMDDHHMM_OFFSET_FORMAT,
10+
YYYY_MM_DD_HH_MM_FORMAT,
11+
};
12+
use crate::parse_relative_time;
13+
use filetime::{set_symlink_file_times, FileTime};
14+
use time::macros::{format_description, offset, time};
15+
use time::{format_description, OffsetDateTime, PrimitiveDateTime};
16+
17+
pub fn from_str(s: &str) -> UResult<FileTime> {
18+
// This isn't actually compatible with GNU touch, but there doesn't seem to
19+
// be any simple specification for what format this parameter allows and I'm
20+
// not about to implement GNU parse_datetime.
21+
// http://git.savannah.gnu.org/gitweb/?p=gnulib.git;a=blob_plain;f=lib/parse-datetime.y
22+
23+
// TODO: match on char count?
24+
25+
// "The preferred date and time representation for the current locale."
26+
// "(In the POSIX locale this is equivalent to %a %b %e %H:%M:%S %Y.)"
27+
// time 0.1.43 parsed this as 'a b e T Y'
28+
// which is equivalent to the POSIX locale: %a %b %e %H:%M:%S %Y
29+
// Tue Dec 3 ...
30+
// ("%c", POSIX_LOCALE_FORMAT),
31+
//
32+
if let Ok(parsed) = time::OffsetDateTime::parse(s, &POSIX_LOCALE_FORMAT) {
33+
return Ok(local_dt_to_filetime(to_local(parsed)));
34+
}
35+
36+
// Also support other formats found in the GNU tests like
37+
// in tests/misc/stat-nanoseconds.sh
38+
// or tests/touch/no-rights.sh
39+
for fmt in [
40+
YYYYMMDDHHMMS_FORMAT,
41+
YYYYMMDDHHMMSS_FORMAT,
42+
YYYY_MM_DD_HH_MM_FORMAT,
43+
YYYYMMDDHHMM_OFFSET_FORMAT,
44+
] {
45+
if let Ok(parsed) = time::PrimitiveDateTime::parse(s, &fmt) {
46+
return Ok(dt_to_filename(parsed));
47+
}
48+
}
49+
50+
// "Equivalent to %Y-%m-%d (the ISO 8601 date format). (C99)"
51+
// ("%F", ISO_8601_FORMAT),
52+
if let Ok(parsed) = time::Date::parse(s, &ISO_8601_FORMAT) {
53+
return Ok(local_dt_to_filetime(to_local(
54+
time::PrimitiveDateTime::new(parsed, time!(00:00)),
55+
)));
56+
}
57+
58+
// "@%s" is "The number of seconds since the Epoch, 1970-01-01 00:00:00 +0000 (UTC). (TZ) (Calculated from mktime(tm).)"
59+
if s.bytes().next() == Some(b'@') {
60+
if let Ok(ts) = &s[1..].parse::<i64>() {
61+
// Don't convert to local time in this case - seconds since epoch are not time-zone dependent
62+
return Ok(local_dt_to_filetime(
63+
time::OffsetDateTime::from_unix_timestamp(*ts).unwrap(),
64+
));
65+
}
66+
}
67+
68+
if let Some(duration) = parse_relative_time::from_str(s) {
69+
let now_local = time::OffsetDateTime::now_local().unwrap();
70+
let diff = now_local.checked_add(duration).unwrap();
71+
return Ok(local_dt_to_filetime(diff));
72+
}
73+
74+
Err(USimpleError::new(1, format!("Unable to parse date: {s}")))
75+
}
76+
77+
#[cfg(test)]
78+
mod tests {
79+
use super::*;
80+
use time::OffsetDateTime;
81+
82+
fn assert_file_time_eq(s: &str, expected: FileTime) {
83+
let parsed = from_str(s).unwrap();
84+
assert_eq!(parsed, expected);
85+
}
86+
87+
#[test]
88+
fn test_from_str_posix_locale_format() {
89+
let s = "2023-04-23T12:34:56";
90+
let expected = local_dt_to_filetime(OffsetDateTime::from_str(s).unwrap());
91+
assert_file_time_eq(s, expected);
92+
}
93+
94+
#[test]
95+
fn test_from_str_various_formats() {
96+
let formats = [
97+
("20230423123456", YYYYMMDDHHMMS_FORMAT),
98+
("20230423123456.123", YYYYMMDDHHMMSS_FORMAT),
99+
("2023-04-23_12-34", YYYY_MM_DD_HH_MM_FORMAT),
100+
("202304231234-0500", YYYYMMDDHHMM_OFFSET_FORMAT),
101+
];
102+
103+
for (s, fmt) in formats.iter() {
104+
let expected = dt_to_filename(time::PrimitiveDateTime::parse(s, fmt).unwrap());
105+
assert_file_time_eq(s, expected);
106+
}
107+
}
108+
109+
#[test]
110+
fn test_from_str_iso_8601_format() {
111+
let s = "2023-04-23";
112+
let expected = local_dt_to_filetime(
113+
to_local(time::PrimitiveDateTime::new(
114+
time::Date::parse(s, &ISO_8601_FORMAT).unwrap(),
115+
time!(00:00),
116+
)),
117+
);
118+
assert_file_time_eq(s, expected);
119+
}
120+
121+
#[test]
122+
fn test_from_str_seconds_since_epoch() {
123+
let s = "@1609459200";
124+
let expected = local_dt_to_filetime(
125+
time::OffsetDateTime::from_unix_timestamp(1609459200).unwrap(),
126+
);
127+
assert_file_time_eq(s, expected);
128+
}
129+
130+
#[test]
131+
fn test_from_str_relative_time() {
132+
let s = "+1d";
133+
let duration = parse_relative_time::from_str(s).unwrap();
134+
let now_local = time::OffsetDateTime::now_local().unwrap();
135+
let expected = local_dt_to_filetime(now_local.checked_add(duration).unwrap());
136+
let parsed = from_str(s).unwrap();
137+
assert!(parsed.duration_since(expected).unwrap().abs() < time::Duration::seconds(1));
138+
}
139+
140+
#[test]
141+
fn test_from_str_invalid_input() {
142+
let s = "invalid";
143+
assert!(from_str(s).is_err());
144+
}
145+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// This file is part of the uutils coreutils package.
2+
//
3+
// For the full copyright and license information, please view the LICENSE
4+
// file that was distributed with this source code.
5+
use filetime::FileTime;
6+
use time::macros::{format_description, offset, time};
7+
8+
pub const POSIX_LOCALE_FORMAT: &[time::format_description::FormatItem] = format_description!(
9+
"[weekday repr:short] [month repr:short] [day padding:space] \
10+
[hour]:[minute]:[second] [year]"
11+
);
12+
13+
pub const ISO_8601_FORMAT: &[time::format_description::FormatItem] =
14+
format_description!("[year]-[month]-[day]");
15+
16+
// "%Y%m%d%H%M.%S" 15 chars
17+
pub const YYYYMMDDHHMM_DOT_SS_FORMAT: &[time::format_description::FormatItem] = format_description!(
18+
"[year repr:full][month repr:numerical padding:zero]\
19+
[day][hour][minute].[second]"
20+
);
21+
22+
// "%Y-%m-%d %H:%M:%S.%SS" 12 chars
23+
pub const YYYYMMDDHHMMSS_FORMAT: &[time::format_description::FormatItem] = format_description!(
24+
"[year repr:full]-[month repr:numerical padding:zero]-\
25+
[day] [hour]:[minute]:[second].[subsecond]"
26+
);
27+
28+
// "%Y-%m-%d %H:%M:%S" 12 chars
29+
pub const YYYYMMDDHHMMS_FORMAT: &[time::format_description::FormatItem] = format_description!(
30+
"[year repr:full]-[month repr:numerical padding:zero]-\
31+
[day] [hour]:[minute]:[second]"
32+
);
33+
34+
// "%Y-%m-%d %H:%M" 12 chars
35+
// Used for example in tests/touch/no-rights.sh
36+
pub const YYYY_MM_DD_HH_MM_FORMAT: &[time::format_description::FormatItem] = format_description!(
37+
"[year repr:full]-[month repr:numerical padding:zero]-\
38+
[day] [hour]:[minute]"
39+
);
40+
41+
// "%Y%m%d%H%M" 12 chars
42+
pub const YYYYMMDDHHMM_FORMAT: &[time::format_description::FormatItem] = format_description!(
43+
"[year repr:full][month repr:numerical padding:zero]\
44+
[day][hour][minute]"
45+
);
46+
47+
// "%y%m%d%H%M.%S" 13 chars
48+
pub const YYMMDDHHMM_DOT_SS_FORMAT: &[time::format_description::FormatItem] = format_description!(
49+
"[year repr:last_two padding:none][month][day]\
50+
[hour][minute].[second]"
51+
);
52+
53+
// "%y%m%d%H%M" 10 chars
54+
pub const YYMMDDHHMM_FORMAT: &[time::format_description::FormatItem] = format_description!(
55+
"[year repr:last_two padding:none][month padding:zero][day padding:zero]\
56+
[hour repr:24 padding:zero][minute padding:zero]"
57+
);
58+
59+
// "%Y-%m-%d %H:%M +offset"
60+
// Used for example in tests/touch/relative.sh
61+
pub const YYYYMMDDHHMM_OFFSET_FORMAT: &[time::format_description::FormatItem] = format_description!(
62+
"[year]-[month]-[day] [hour repr:24]:[minute] \
63+
[offset_hour sign:mandatory][offset_minute]"
64+
);
65+
66+
// Convert a date/time with a TZ offset into a FileTime
67+
pub fn local_dt_to_filetime(dt: time::OffsetDateTime) -> FileTime {
68+
FileTime::from_unix_time(dt.unix_timestamp(), dt.nanosecond())
69+
}
70+
71+
// Convert a date/time to a date with a TZ offset
72+
pub fn to_local(tm: time::PrimitiveDateTime) -> time::OffsetDateTime {
73+
let offset = match time::OffsetDateTime::now_local() {
74+
Ok(lo) => lo.offset(),
75+
Err(e) => {
76+
panic!("error: {e}");
77+
}
78+
};
79+
tm.assume_offset(offset)
80+
}
81+
82+
// Convert a date/time, considering that the input is in UTC time
83+
// Used for touch -d 1970-01-01 18:43:33.023456789 for example
84+
pub fn dt_to_filename(tm: time::PrimitiveDateTime) -> FileTime {
85+
let dt = tm.assume_offset(offset!(UTC));
86+
local_dt_to_filetime(dt)
87+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// This file is part of the uutils coreutils package.
2+
//
3+
// For the full copyright and license information, please view the LICENSE
4+
// file that was distributed with this source code.
5+
use crate::error::{FromIo, UError, UResult, USimpleError};
6+
use crate::parse_date_common::{
7+
local_dt_to_filetime, to_local, ISO_8601_FORMAT, POSIX_LOCALE_FORMAT, YYMMDDHHMM_DOT_SS_FORMAT,
8+
YYMMDDHHMM_FORMAT, YYYYMMDDHHMMSS_FORMAT, YYYYMMDDHHMMS_FORMAT, YYYYMMDDHHMM_DOT_SS_FORMAT,
9+
YYYYMMDDHHMM_FORMAT, YYYYMMDDHHMM_OFFSET_FORMAT, YYYY_MM_DD_HH_MM_FORMAT,
10+
};
11+
use filetime::FileTime;
12+
use os_display::Quotable;
13+
#[cfg(feature = "time")]
14+
use time::Duration;
15+
16+
pub fn from_str(s: &str) -> UResult<FileTime> {
17+
// TODO: handle error
18+
let now = time::OffsetDateTime::now_utc();
19+
20+
let (mut format, mut ts) = match s.chars().count() {
21+
15 => (YYYYMMDDHHMM_DOT_SS_FORMAT, s.to_owned()),
22+
12 => (YYYYMMDDHHMM_FORMAT, s.to_owned()),
23+
13 => (YYMMDDHHMM_DOT_SS_FORMAT, s.to_owned()),
24+
10 => (YYMMDDHHMM_FORMAT, s.to_owned()),
25+
11 => (YYYYMMDDHHMM_DOT_SS_FORMAT, format!("{}{}", now.year(), s)),
26+
8 => (YYYYMMDDHHMM_FORMAT, format!("{}{}", now.year(), s)),
27+
_ => {
28+
return Err(USimpleError::new(
29+
1,
30+
format!("invalid date format {}", s.quote()),
31+
))
32+
}
33+
};
34+
// workaround time returning Err(TryFromParsed(InsufficientInformation)) for year w/
35+
// repr:last_two
36+
// https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=1ccfac7c07c5d1c7887a11decf0e1996
37+
if s.chars().count() == 10 {
38+
format = YYYYMMDDHHMM_FORMAT;
39+
ts = "20".to_owned() + &ts;
40+
} else if s.chars().count() == 13 {
41+
format = YYYYMMDDHHMM_DOT_SS_FORMAT;
42+
ts = "20".to_owned() + &ts;
43+
}
44+
45+
let leap_sec = if (format == YYYYMMDDHHMM_DOT_SS_FORMAT || format == YYMMDDHHMM_DOT_SS_FORMAT)
46+
&& ts.ends_with(".60")
47+
{
48+
// Work around to disable leap seconds
49+
// Used in gnu/tests/touch/60-seconds
50+
ts = ts.replace(".60", ".59");
51+
true
52+
} else {
53+
false
54+
};
55+
56+
let tm = time::PrimitiveDateTime::parse(&ts, &format)
57+
.map_err(|_| USimpleError::new(1, format!("invalid date ts format {}", ts.quote())))?;
58+
let mut local = to_local(tm);
59+
if leap_sec {
60+
// We are dealing with a leap second, add it
61+
local = local.saturating_add(Duration::SECOND);
62+
}
63+
let ft = local_dt_to_filetime(local);
64+
65+
// // We have to check that ft is valid time. Due to daylight saving
66+
// // time switch, local time can jump from 1:59 AM to 3:00 AM,
67+
// // in which case any time between 2:00 AM and 2:59 AM is not valid.
68+
// // Convert back to local time and see if we got the same value back.
69+
// let ts = time::Timespec {
70+
// sec: ft.unix_seconds(),
71+
// nsec: 0,
72+
// };
73+
// let tm2 = time::at(ts);
74+
// if tm.tm_hour != tm2.tm_hour {
75+
// return Err(USimpleError::new(
76+
// 1,
77+
// format!("invalid date format {}", s.quote()),
78+
// ));
79+
// }
80+
81+
Ok(ft)
82+
}

0 commit comments

Comments
 (0)