Repository: toml-rs/toml (rust)

Issue:
No validation on date parsing
With #187, we dropped most of our date validation, like 2021-02-31 now works.

Looks like chrono has some [pretty complex lookup logic](https://github.com/chronotope/chrono/blob/main/src/naive/internals.rs) to prevent that

[Playground of it erroring](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=7e544d4824015e502916e9a855107395)

Pull request description:
fix(datetime): Verify leap years
This also makes `toml_edit` and `toml_datetime` consistent in accepting leap-seconds

Fixes #188

Patch:
diff --git a/crates/toml_datetime/src/datetime.rs b/crates/toml_datetime/src/datetime.rs
index 871522df..1b37c700 100644
--- a/crates/toml_datetime/src/datetime.rs
+++ b/crates/toml_datetime/src/datetime.rs
@@ -308,7 +308,15 @@ impl FromStr for Datetime {
             if date.month < 1 || date.month > 12 {
                 return Err(DatetimeParseError {});
             }
-            if date.day < 1 || date.day > 31 {
+            let is_leap_year =
+                (date.year % 4 == 0) && ((date.year % 100 != 0) || (date.year % 400 == 0));
+            let max_days_in_month = match date.month {
+                2 if is_leap_year => 29,
+                2 => 28,
+                4 | 6 | 9 | 11 => 30,
+                _ => 31,
+            };
+            if date.day < 1 || date.day > max_days_in_month {
                 return Err(DatetimeParseError {});
             }
 
@@ -381,7 +389,8 @@ impl FromStr for Datetime {
             if time.minute > 59 {
                 return Err(DatetimeParseError {});
             }
-            if time.second > 59 {
+            // 00-58, 00-59, 00-60 based on leap second rules
+            if time.second > 60 {
                 return Err(DatetimeParseError {});
             }
             if time.nanosecond > 999_999_999 {
diff --git a/crates/toml_edit/src/parser/datetime.rs b/crates/toml_edit/src/parser/datetime.rs
index 6e89b977..96a3854d 100644
--- a/crates/toml_edit/src/parser/datetime.rs
+++ b/crates/toml_edit/src/parser/datetime.rs
@@ -9,6 +9,7 @@ use winnow::combinator::alt;
 use winnow::combinator::cut_err;
 use winnow::combinator::opt;
 use winnow::combinator::preceded;
+use winnow::stream::Stream as _;
 use winnow::token::one_of;
 use winnow::token::take_while;
 use winnow::trace::trace;
@@ -53,12 +54,35 @@ pub(crate) fn date_time(input: &mut Input<'_>) -> PResult<Datetime> {
 
 // full-date      = date-fullyear "-" date-month "-" date-mday
 pub(crate) fn full_date(input: &mut Input<'_>) -> PResult<Date> {
-    trace(
-        "full-date",
-        (date_fullyear, b'-', cut_err((date_month, b'-', date_mday)))
-            .map(|(year, _, (month, _, day))| Date { year, month, day }),
-    )
-    .parse_next(input)
+    trace("full-date", full_date_).parse_next(input)
+}
+
+fn full_date_(input: &mut Input<'_>) -> PResult<Date> {
+    let year = date_fullyear.parse_next(input)?;
+    let _ = b'-'.parse_next(input)?;
+    let month = cut_err(date_month).parse_next(input)?;
+    let _ = cut_err(b'-').parse_next(input)?;
+    let day_start = input.checkpoint();
+    let day = cut_err(date_mday).parse_next(input)?;
+
+    let is_leap_year = (year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0));
+    let max_days_in_month = match month {
+        2 if is_leap_year => 29,
+        2 => 28,
+        4 | 6 | 9 | 11 => 30,
+        _ => 31,
+    };
+    if max_days_in_month < day {
+        input.reset(day_start);
+        return Err(winnow::error::ErrMode::from_external_error(
+            input,
+            winnow::error::ErrorKind::Verify,
+            CustomError::OutOfRange,
+        )
+        .cut());
+    }
+
+    Ok(Date { year, month, day })
 }
 
 // partial-time   = time-hour ":" time-minute ":" time-second [time-secfrac]
diff --git a/deny.toml b/deny.toml
index 21fa937f..e473ea74 100644
--- a/deny.toml
+++ b/deny.toml
@@ -133,7 +133,7 @@ unknown-git = "deny"
 # if not specified. If it is specified but empty, no registries are allowed.
 allow-registry = ["https://github.com/rust-lang/crates.io-index"]
 # List of URLs for allowed Git repositories
-allow-git = []
+allow-git = ["https://github.com/rust-fuzz/libfuzzer-sys"]
 
 [sources.allow-org]
 # 1 or more github.com organizations to allow git sources for


Test patch:
diff --git a/crates/toml/tests/decoder_compliance.rs b/crates/toml/tests/decoder_compliance.rs
index a4ebbe58..58288837 100644
--- a/crates/toml/tests/decoder_compliance.rs
+++ b/crates/toml/tests/decoder_compliance.rs
@@ -5,16 +5,7 @@ fn main() {
     let decoder = decoder::Decoder;
     let mut harness = toml_test_harness::DecoderHarness::new(decoder);
     harness.version("1.0.0");
-    harness
-        .ignore([
-            "invalid/datetime/feb-30.toml",
-            "invalid/datetime/feb-29.toml",
-            "invalid/local-date/feb-30.toml",
-            "invalid/local-date/feb-29.toml",
-            "invalid/local-datetime/feb-29.toml",
-            "invalid/local-datetime/feb-30.toml",
-        ])
-        .unwrap();
+    harness.ignore([]).unwrap();
     harness.test();
 }
 
diff --git a/crates/toml_edit/tests/decoder_compliance.rs b/crates/toml_edit/tests/decoder_compliance.rs
index 357cfd51..37a9335e 100644
--- a/crates/toml_edit/tests/decoder_compliance.rs
+++ b/crates/toml_edit/tests/decoder_compliance.rs
@@ -4,15 +4,6 @@ fn main() {
     let decoder = decoder::Decoder;
     let mut harness = toml_test_harness::DecoderHarness::new(decoder);
     harness.version("1.0.0");
-    harness
-        .ignore([
-            "invalid/datetime/feb-30.toml",
-            "invalid/datetime/feb-29.toml",
-            "invalid/local-date/feb-30.toml",
-            "invalid/local-date/feb-29.toml",
-            "invalid/local-datetime/feb-29.toml",
-            "invalid/local-datetime/feb-30.toml",
-        ])
-        .unwrap();
+    harness.ignore([]).unwrap();
     harness.test();
 }
diff --git a/crates/toml_edit/tests/fixtures/invalid/datetime/feb-29.stderr b/crates/toml_edit/tests/fixtures/invalid/datetime/feb-29.stderr
index e69de29b..63fac67c 100644
--- a/crates/toml_edit/tests/fixtures/invalid/datetime/feb-29.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/datetime/feb-29.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 29
+  |
+1 | "not a leap year" = 2100-02-29T15:15:15Z
+  |                             ^
+invalid date-time
+value is out of range
diff --git a/crates/toml_edit/tests/fixtures/invalid/datetime/feb-30.stderr b/crates/toml_edit/tests/fixtures/invalid/datetime/feb-30.stderr
index e69de29b..2b5212af 100644
--- a/crates/toml_edit/tests/fixtures/invalid/datetime/feb-30.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/datetime/feb-30.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 44
+  |
+1 | "only 28 or 29 days in february" = 1988-02-30T15:15:15Z
+  |                                            ^
+invalid date-time
+value is out of range
diff --git a/crates/toml_edit/tests/fixtures/invalid/local-date/feb-29.stderr b/crates/toml_edit/tests/fixtures/invalid/local-date/feb-29.stderr
index e69de29b..b0ae76dd 100644
--- a/crates/toml_edit/tests/fixtures/invalid/local-date/feb-29.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/local-date/feb-29.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 29
+  |
+1 | "not a leap year" = 2100-02-29
+  |                             ^
+invalid date-time
+value is out of range
diff --git a/crates/toml_edit/tests/fixtures/invalid/local-date/feb-30.stderr b/crates/toml_edit/tests/fixtures/invalid/local-date/feb-30.stderr
index e69de29b..7f643481 100644
--- a/crates/toml_edit/tests/fixtures/invalid/local-date/feb-30.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/local-date/feb-30.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 44
+  |
+1 | "only 28 or 29 days in february" = 1988-02-30
+  |                                            ^
+invalid date-time
+value is out of range
diff --git a/crates/toml_edit/tests/fixtures/invalid/local-datetime/feb-29.stderr b/crates/toml_edit/tests/fixtures/invalid/local-datetime/feb-29.stderr
index e69de29b..8aac7125 100644
--- a/crates/toml_edit/tests/fixtures/invalid/local-datetime/feb-29.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/local-datetime/feb-29.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 29
+  |
+1 | "not a leap year" = 2100-02-29T15:15:15
+  |                             ^
+invalid date-time
+value is out of range
diff --git a/crates/toml_edit/tests/fixtures/invalid/local-datetime/feb-30.stderr b/crates/toml_edit/tests/fixtures/invalid/local-datetime/feb-30.stderr
index e69de29b..6175c5e8 100644
--- a/crates/toml_edit/tests/fixtures/invalid/local-datetime/feb-30.stderr
+++ b/crates/toml_edit/tests/fixtures/invalid/local-datetime/feb-30.stderr
@@ -0,0 +1,6 @@
+TOML parse error at line 1, column 44
+  |
+1 | "only 28 or 29 days in february" = 1988-02-30T15:15:15
+  |                                            ^
+invalid date-time
+value is out of range