Skip to content

Commit

Permalink
Find a compatibility intersection between chrono and GNU
Browse files Browse the repository at this point in the history
  • Loading branch information
dhilst committed Sep 26, 2024
1 parent 73f43a3 commit 543d529
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 47 deletions.
1 change: 0 additions & 1 deletion fuzz/fuzz_targets/parse_datetime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
#![allow(dead_code)]

use std::fmt::{Debug, Display};
use std::io::{self, Write};

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

Expand Down
7 changes: 1 addition & 6 deletions src/items/combined.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,7 @@ pub struct DateTime {
}

pub fn parse(input: &mut &str) -> PResult<DateTime> {
alt((
parse_basic,
//parse_8digits
))
.parse_next(input)
alt((parse_basic, parse_8digits)).parse_next(input)
}

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

#[allow(dead_code)]
fn parse_8digits(input: &mut &str) -> PResult<DateTime> {
s((
take(2usize).and_then(dec_uint),
Expand Down
36 changes: 27 additions & 9 deletions src/items/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ use winnow::{
ascii::{alpha1, dec_uint},
combinator::{alt, opt, preceded},
seq,
stream::AsChar,
token::take_while,
trace::trace,
PResult, Parser,
};
Expand Down Expand Up @@ -97,14 +99,26 @@ fn literal2(input: &mut &str) -> PResult<Date> {
}

pub fn year(input: &mut &str) -> PResult<u32> {
// 2147485547 is the maximum value accepted
// by GNU, but chrono only behave like GNU
// for years in the range: [0, 9999], so we
// keep in the range [0, 9999]
trace(
"year",
dec_uint.try_map(|x| {
(0..=2147485547)
.contains(&x)
.then_some(x)
.ok_or(ParseDateTimeError::InvalidInput)
}),
s(
take_while(1..=4, AsChar::is_dec_digit).map(|number_str: &str| {
let year = number_str.parse::<u32>().unwrap();
if number_str.len() == 2 {
if year <= 68 {
year + 2000
} else {
year + 1900
}
} else {
year
}
}),
),
)
.parse_next(input)
}
Expand Down Expand Up @@ -233,9 +247,13 @@ mod tests {
use super::year;

// the minimun input length is 2
assert!(year(&mut "0").is_err());
// assert!(year(&mut "0").is_err());
// -> GNU accepts year 0
// test $(date -d '1-1-1' '+%Y') -eq '0001'

// test $(date -d '68-1-1' '+%Y') -eq '2068'
// 2-characters are converted to 19XX/20XX
assert_eq!(year(&mut "00").unwrap(), 2000u32);
assert_eq!(year(&mut "10").unwrap(), 2010u32);
assert_eq!(year(&mut "68").unwrap(), 2068u32);
assert_eq!(year(&mut "69").unwrap(), 1969u32);
assert_eq!(year(&mut "99").unwrap(), 1999u32);
Expand All @@ -245,6 +263,6 @@ mod tests {
assert_eq!(year(&mut "1568").unwrap(), 1568u32);
assert_eq!(year(&mut "1569").unwrap(), 1569u32);
// consumes at most 4 characters from the input
assert_eq!(year(&mut "1234567").unwrap(), 1234u32);
//assert_eq!(year(&mut "1234567").unwrap(), 1234u32);
}
}
19 changes: 15 additions & 4 deletions src/items/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ mod timezone {
use chrono::NaiveDate;
use chrono::{DateTime, Datelike, FixedOffset, TimeZone, Timelike};

use winnow::error::ParseError;
use winnow::error::ParserError;
use winnow::error::{ContextError, ErrMode, ParseError};
use winnow::trace::trace;
use winnow::{
ascii::multispace0,
Expand Down Expand Up @@ -100,6 +100,17 @@ where
separated(0.., multispace0, alt((comment, ignored_hyphen_or_plus))).parse_next(input)
}

/// Check for the end of a token, without consuming the input
/// succeedes if the next character in the input is a space or
/// if the input is empty
pub(crate) fn eotoken(input: &mut &str) -> PResult<()> {
if input.is_empty() || input.chars().next().unwrap().is_space() {
return Ok(());
}

Err(ErrMode::Backtrack(ContextError::new()))
}

/// A hyphen or plus is ignored when it is not followed by a digit
///
/// This includes being followed by a comment! Compare these inputs:
Expand Down Expand Up @@ -141,7 +152,7 @@ where

// Parse an item
pub fn parse_one(input: &mut &str) -> PResult<Item> {
//eprintln!("parsing_one -> {input}");
// eprintln!("parsing_one -> {input}");
let result = trace(
"parse_one",
alt((
Expand All @@ -152,11 +163,11 @@ pub fn parse_one(input: &mut &str) -> PResult<Item> {
weekday::parse.map(Item::Weekday),
epoch::parse.map(Item::Timestamp),
timezone::parse.map(Item::TimeZone),
s(date::year).map(Item::Year),
date::year.map(Item::Year),
)),
)
.parse_next(input)?;
//eprintln!("parsing_one <- {input} {result:?}");
// eprintln!("parsing_one <- {input} {result:?}");

Ok(result)
}
Expand Down
10 changes: 7 additions & 3 deletions src/items/time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,15 @@ use std::fmt::Display;
use chrono::FixedOffset;
use winnow::{
ascii::{dec_uint, float},
combinator::{alt, opt, preceded},
combinator::{alt, opt, preceded, terminated},
error::{AddContext, ContextError, ErrMode, StrContext},
seq,
stream::AsChar,
token::take_while,
PResult, Parser,
};

use super::s;
use super::{eotoken, s};

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

pub(crate) fn timezone(input: &mut &str) -> PResult<Offset> {
alt((timezone_num, timezone_name_offset)).parse_next(input)
let result =
terminated(alt((timezone_num, timezone_name_offset)), eotoken).parse_next(input)?;

// space_or_eof(input, result)
Ok(result)
}

/// Parse a timezone starting with `+` or `-`
Expand Down
47 changes: 23 additions & 24 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,6 @@ mod tests {
env::set_var("TZ", "UTC");
let dt = "2021-02-15T06:37:47";
let actual = parse_datetime(dt).unwrap();
eprintln!("actual => {actual}");
assert_eq!(
actual,
Utc.timestamp_opt(TEST_TIME, 0).unwrap().fixed_offset()
Expand Down Expand Up @@ -393,7 +392,6 @@ mod tests {
#[test]
fn test_invalid_input() {
let result = parse_datetime("foobar");
println!("{result:?}");
assert_eq!(result, Err(ParseDateTimeError::InvalidInput));

let result = parse_datetime("invalid 1");
Expand All @@ -402,13 +400,9 @@ mod tests {
}

mod test_relative {
use chrono::NaiveDate;

use crate::{items, parse_datetime};
use std::{
env,
io::{self, Write},
};
use crate::parse_datetime;
use std::env;

#[test]
fn test_month() {
Expand Down Expand Up @@ -461,45 +455,50 @@ mod tests {
"2024-03-29T00:00:00+00:00",
);
}
}

mod test_gnu {
use crate::parse_datetime;

fn make_gnu_date(input: &str, fmt: &str) -> String {
std::process::Command::new("date")
.arg("-d")
.arg(input)
.arg(format!("+{fmt}"))
.output()
.map(|mut output| {
io::stdout().write_all(&output.stdout).unwrap();
//io::stdout().write_all(&output.stdout).unwrap();
output.stdout.pop(); // remove trailing \n
String::from_utf8(output.stdout).expect("from_utf8")
})
.unwrap()
}

#[test]
fn chrono_date() {
const FMT: &str = "%Y-%m-%d %H:%M:%S";
let year = 262144;
let input = format!("{year}-01-01 00:00:00");

assert!(NaiveDate::from_ymd_opt(year, 1, 1).is_none());
assert!(chrono::DateTime::parse_from_str(&input, FMT).is_err());
// the parsing works, but hydration fails
assert!(items::parse(&mut input.to_string().as_str()).is_ok());
assert!(parse_datetime(&input).is_err());
// GNU date works
assert_eq!(make_gnu_date(&input, FMT), input);
fn has_gnu_date() -> bool {
std::process::Command::new("date")
.arg("--version")
.output()
.map(|output| String::from_utf8(output.stdout).unwrap())
.map(|output| output.starts_with("date (GNU coreutils)"))
.unwrap_or(false)
}

#[test]
fn gnu_compat() {
// skip if GNU date is not present
if !has_gnu_date() {
eprintln!("GNU date not found, skipping gnu_compat tests");
return;
}

const FMT: &str = "%Y-%m-%d %H:%M:%S";
let input = "0000-03-02 00:00:00";
assert_eq!(
make_gnu_date(input, FMT),
parse_datetime(input).unwrap().format(FMT).to_string()
);

let input = "262144-03-10 00:00:00";
let input = "2621-03-10 00:00:00";
assert_eq!(
make_gnu_date(input, FMT),
parse_datetime(input)
Expand All @@ -508,7 +507,7 @@ mod tests {
.to_string()
);

let input = "10384-03-10 00:00:00";
let input = "1038-03-10 00:00:00";
assert_eq!(
make_gnu_date(input, FMT),
parse_datetime(input)
Expand Down

0 comments on commit 543d529

Please sign in to comment.