Skip to content

Commit 2479c61

Browse files
dhilstyuankunzhang
authored andcommitted
Find a compatibility intersection between chrono and GNU
1 parent 4c3d222 commit 2479c61

File tree

8 files changed

+80
-60
lines changed

8 files changed

+80
-60
lines changed

Cargo.toml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,3 @@ anyhow = "1.0.86"
2020

2121
[features]
2222
debug = ["winnow/debug"]
23-
24-
[[bin]]
25-
path = "bin/main.rs"
26-
name = "parse_datetime"
27-
test = false
28-
bench = false

bin/main.rs

Lines changed: 0 additions & 6 deletions
This file was deleted.

fuzz/fuzz_targets/parse_datetime.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
#![allow(dead_code)]
33

44
use std::fmt::{Debug, Display};
5-
use std::io::{self, Write};
65

76
use libfuzzer_sys::arbitrary::{self, Arbitrary};
87

src/items/combined.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,7 @@ pub struct DateTime {
3535
}
3636

3737
pub fn parse(input: &mut &str) -> PResult<DateTime> {
38-
alt((
39-
parse_basic,
40-
//parse_8digits
41-
))
42-
.parse_next(input)
38+
alt((parse_basic, parse_8digits)).parse_next(input)
4339
}
4440

4541
fn parse_basic(input: &mut &str) -> PResult<DateTime> {
@@ -52,7 +48,6 @@ fn parse_basic(input: &mut &str) -> PResult<DateTime> {
5248
.parse_next(input)
5349
}
5450

55-
#[allow(dead_code)]
5651
fn parse_8digits(input: &mut &str) -> PResult<DateTime> {
5752
s((
5853
take(2usize).and_then(dec_uint),

src/items/date.rs

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ use winnow::{
3030
ascii::{alpha1, dec_uint},
3131
combinator::{alt, opt, preceded},
3232
seq,
33+
stream::AsChar,
34+
token::take_while,
3335
trace::trace,
3436
PResult, Parser,
3537
};
@@ -97,14 +99,26 @@ fn literal2(input: &mut &str) -> PResult<Date> {
9799
}
98100

99101
pub fn year(input: &mut &str) -> PResult<u32> {
102+
// 2147485547 is the maximum value accepted
103+
// by GNU, but chrono only behave like GNU
104+
// for years in the range: [0, 9999], so we
105+
// keep in the range [0, 9999]
100106
trace(
101107
"year",
102-
dec_uint.try_map(|x| {
103-
(0..=2147485547)
104-
.contains(&x)
105-
.then_some(x)
106-
.ok_or(ParseDateTimeError::InvalidInput)
107-
}),
108+
s(
109+
take_while(1..=4, AsChar::is_dec_digit).map(|number_str: &str| {
110+
let year = number_str.parse::<u32>().unwrap();
111+
if number_str.len() == 2 {
112+
if year <= 68 {
113+
year + 2000
114+
} else {
115+
year + 1900
116+
}
117+
} else {
118+
year
119+
}
120+
}),
121+
),
108122
)
109123
.parse_next(input)
110124
}
@@ -233,9 +247,13 @@ mod tests {
233247
use super::year;
234248

235249
// the minimun input length is 2
236-
assert!(year(&mut "0").is_err());
250+
// assert!(year(&mut "0").is_err());
251+
// -> GNU accepts year 0
252+
// test $(date -d '1-1-1' '+%Y') -eq '0001'
253+
254+
// test $(date -d '68-1-1' '+%Y') -eq '2068'
237255
// 2-characters are converted to 19XX/20XX
238-
assert_eq!(year(&mut "00").unwrap(), 2000u32);
256+
assert_eq!(year(&mut "10").unwrap(), 2010u32);
239257
assert_eq!(year(&mut "68").unwrap(), 2068u32);
240258
assert_eq!(year(&mut "69").unwrap(), 1969u32);
241259
assert_eq!(year(&mut "99").unwrap(), 1999u32);
@@ -245,6 +263,6 @@ mod tests {
245263
assert_eq!(year(&mut "1568").unwrap(), 1568u32);
246264
assert_eq!(year(&mut "1569").unwrap(), 1569u32);
247265
// consumes at most 4 characters from the input
248-
assert_eq!(year(&mut "1234567").unwrap(), 1234u32);
266+
//assert_eq!(year(&mut "1234567").unwrap(), 1234u32);
249267
}
250268
}

src/items/mod.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ mod timezone {
5353
use chrono::NaiveDate;
5454
use chrono::{DateTime, Datelike, FixedOffset, TimeZone, Timelike};
5555

56-
use winnow::error::ParseError;
5756
use winnow::error::ParserError;
57+
use winnow::error::{ContextError, ErrMode, ParseError};
5858
use winnow::trace::trace;
5959
use winnow::{
6060
ascii::multispace0,
@@ -100,6 +100,17 @@ where
100100
separated(0.., multispace0, alt((comment, ignored_hyphen_or_plus))).parse_next(input)
101101
}
102102

103+
/// Check for the end of a token, without consuming the input
104+
/// succeedes if the next character in the input is a space or
105+
/// if the input is empty
106+
pub(crate) fn eotoken(input: &mut &str) -> PResult<()> {
107+
if input.is_empty() || input.chars().next().unwrap().is_space() {
108+
return Ok(());
109+
}
110+
111+
Err(ErrMode::Backtrack(ContextError::new()))
112+
}
113+
103114
/// A hyphen or plus is ignored when it is not followed by a digit
104115
///
105116
/// This includes being followed by a comment! Compare these inputs:
@@ -141,7 +152,7 @@ where
141152

142153
// Parse an item
143154
pub fn parse_one(input: &mut &str) -> PResult<Item> {
144-
//eprintln!("parsing_one -> {input}");
155+
// eprintln!("parsing_one -> {input}");
145156
let result = trace(
146157
"parse_one",
147158
alt((
@@ -152,11 +163,11 @@ pub fn parse_one(input: &mut &str) -> PResult<Item> {
152163
weekday::parse.map(Item::Weekday),
153164
epoch::parse.map(Item::Timestamp),
154165
timezone::parse.map(Item::TimeZone),
155-
s(date::year).map(Item::Year),
166+
date::year.map(Item::Year),
156167
)),
157168
)
158169
.parse_next(input)?;
159-
//eprintln!("parsing_one <- {input} {result:?}");
170+
// eprintln!("parsing_one <- {input} {result:?}");
160171

161172
Ok(result)
162173
}

src/items/time.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,15 @@ use std::fmt::Display;
4242
use chrono::FixedOffset;
4343
use winnow::{
4444
ascii::{dec_uint, float},
45-
combinator::{alt, opt, preceded},
45+
combinator::{alt, opt, preceded, terminated},
4646
error::{AddContext, ContextError, ErrMode, StrContext},
4747
seq,
4848
stream::AsChar,
4949
token::take_while,
5050
PResult, Parser,
5151
};
5252

53-
use super::s;
53+
use super::{eotoken, s};
5454

5555
#[derive(PartialEq, Debug, Clone, Default)]
5656
pub struct Offset {
@@ -206,7 +206,11 @@ fn second(input: &mut &str) -> PResult<f64> {
206206
}
207207

208208
pub(crate) fn timezone(input: &mut &str) -> PResult<Offset> {
209-
alt((timezone_num, timezone_name_offset)).parse_next(input)
209+
let result =
210+
terminated(alt((timezone_num, timezone_name_offset)), eotoken).parse_next(input)?;
211+
212+
// space_or_eof(input, result)
213+
Ok(result)
210214
}
211215

212216
/// Parse a timezone starting with `+` or `-`

src/lib.rs

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -120,15 +120,20 @@ mod tests {
120120
mod iso_8601 {
121121
use std::env;
122122

123+
use chrono::{TimeZone, Utc};
124+
123125
use crate::ParseDateTimeError;
124126
use crate::{parse_datetime, tests::TEST_TIME};
125127

126128
#[test]
127129
fn test_t_sep() {
128130
env::set_var("TZ", "UTC");
129131
let dt = "2021-02-15T06:37:47";
130-
let actual = parse_datetime(dt);
131-
assert_eq!(actual.unwrap().timestamp(), TEST_TIME);
132+
let actual = parse_datetime(dt).unwrap();
133+
assert_eq!(
134+
actual,
135+
Utc.timestamp_opt(TEST_TIME, 0).unwrap().fixed_offset()
136+
);
132137
}
133138

134139
#[test]
@@ -400,7 +405,6 @@ mod tests {
400405
#[test]
401406
fn test_invalid_input() {
402407
let result = parse_datetime("foobar");
403-
println!("{result:?}");
404408
assert_eq!(result, Err(ParseDateTimeError::InvalidInput));
405409

406410
let result = parse_datetime("invalid 1");
@@ -618,13 +622,9 @@ mod tests {
618622
}
619623

620624
mod test_relative {
621-
use chrono::NaiveDate;
622625

623-
use crate::{items, parse_datetime};
624-
use std::{
625-
env,
626-
io::{self, Write},
627-
};
626+
use crate::parse_datetime;
627+
use std::env;
628628

629629
#[test]
630630
fn test_month() {
@@ -677,45 +677,50 @@ mod tests {
677677
"2024-03-29T00:00:00+00:00",
678678
);
679679
}
680+
}
681+
682+
mod test_gnu {
683+
use crate::parse_datetime;
684+
680685
fn make_gnu_date(input: &str, fmt: &str) -> String {
681686
std::process::Command::new("date")
682687
.arg("-d")
683688
.arg(input)
684689
.arg(format!("+{fmt}"))
685690
.output()
686691
.map(|mut output| {
687-
io::stdout().write_all(&output.stdout).unwrap();
692+
//io::stdout().write_all(&output.stdout).unwrap();
688693
output.stdout.pop(); // remove trailing \n
689694
String::from_utf8(output.stdout).expect("from_utf8")
690695
})
691696
.unwrap()
692697
}
693698

694-
#[test]
695-
fn chrono_date() {
696-
const FMT: &str = "%Y-%m-%d %H:%M:%S";
697-
let year = 262144;
698-
let input = format!("{year}-01-01 00:00:00");
699-
700-
assert!(NaiveDate::from_ymd_opt(year, 1, 1).is_none());
701-
assert!(chrono::DateTime::parse_from_str(&input, FMT).is_err());
702-
// the parsing works, but hydration fails
703-
assert!(items::parse(&mut input.to_string().as_str()).is_ok());
704-
assert!(parse_datetime(&input).is_err());
705-
// GNU date works
706-
assert_eq!(make_gnu_date(&input, FMT), input);
699+
fn has_gnu_date() -> bool {
700+
std::process::Command::new("date")
701+
.arg("--version")
702+
.output()
703+
.map(|output| String::from_utf8(output.stdout).unwrap())
704+
.map(|output| output.starts_with("date (GNU coreutils)"))
705+
.unwrap_or(false)
707706
}
708707

709708
#[test]
710709
fn gnu_compat() {
710+
// skip if GNU date is not present
711+
if !has_gnu_date() {
712+
eprintln!("GNU date not found, skipping gnu_compat tests");
713+
return;
714+
}
715+
711716
const FMT: &str = "%Y-%m-%d %H:%M:%S";
712717
let input = "0000-03-02 00:00:00";
713718
assert_eq!(
714719
make_gnu_date(input, FMT),
715720
parse_datetime(input).unwrap().format(FMT).to_string()
716721
);
717722

718-
let input = "262144-03-10 00:00:00";
723+
let input = "2621-03-10 00:00:00";
719724
assert_eq!(
720725
make_gnu_date(input, FMT),
721726
parse_datetime(input)
@@ -724,7 +729,7 @@ mod tests {
724729
.to_string()
725730
);
726731

727-
let input = "10384-03-10 00:00:00";
732+
let input = "1038-03-10 00:00:00";
728733
assert_eq!(
729734
make_gnu_date(input, FMT),
730735
parse_datetime(input)

0 commit comments

Comments
 (0)