Repository: toml-rs/toml (language: 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)


Discussion / hints:
Options
- Revert the change
- Add a feature flag for full date validation, which pulls in chrono
- Do nothing (like `toml-rs`)
Curious how other parsers handle that (e.g. in other languages).  It seems toml spec specifies the date should follow the https://datatracker.ietf.org/doc/html/rfc3339. And if chrono panics on construction instead of returning an error, that's not good either, but I would lean towards Do nothing and documenting this behavior.
Chrono offers panicing and `Option` variants of its constructors

Looking at the RFC, it seems like it shouldn't be too bad to address days of month.  I assume the complexity in chrono is coming from other functionality.

Leap seconds is an interesting one.  If you don't bother with a lookup table, it simplifies things but knowing whether you can have a leap second or not is dependent on the month.  So a date-less time can't do validation and combining a date and time can error.
As for other languages, `toml-test` validates that toml parsers reject months with 50 days but doesn't validate further.  So parsers using that test (like go) at least validate that much.

Python's `toml` loads into `datetime` which handles leap years.  I did not verify how it handles leap seconds
If my preference is at all useful here, I would like `chrono` behind a feature flag if you intend to add it again.

The project where I use `toml_edit` already has a ton of dependencies. And while `chonro` is not large compared to everything else I depend on, I do try to keep the tree as thin as possible by removing unnecessary features. A feature flag would just be really nice to have.

Fix 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