From 83ed70acce3090c139fab3449db25b6f0dc4ce9b Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:42:44 +0000 Subject: [PATCH 1/2] Validate hyphen placement and days in month in parse_date Found while testing crates/engine/src/cmd/fileio/naming.rs:103 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/cmd/fileio/naming.rs | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/fileio/naming.rs b/crates/engine/src/cmd/fileio/naming.rs index 9e18e8212..b79998170 100644 --- a/crates/engine/src/cmd/fileio/naming.rs +++ b/crates/engine/src/cmd/fileio/naming.rs @@ -100,13 +100,26 @@ pub fn render(template: &str, p: &NameParts<'_>) -> String { /// `YYYY-MM-DD` (or `YYYYMMDD`) as `[year, month, day, 0, 0, 0]`; `None` when it isn't a date. pub fn parse_date(s: &str) -> Option<[i64; 6]> { - let digits: String = s.trim().chars().filter(|c| *c != '-').collect(); + let s = s.trim(); + let digits: String = match s.len() { + 8 => s.to_string(), + 10 if s.as_bytes()[4] == b'-' && s.as_bytes()[7] == b'-' => s.chars().filter(|c| *c != '-').collect(), + _ => return None, + }; if digits.len() != 8 || !digits.bytes().all(|b| b.is_ascii_digit()) { return None; } let num = |a: usize, b: usize| digits.get(a..b).and_then(|t| t.parse::().ok()); let (y, m, d) = (num(0, 4)?, num(4, 6)?, num(6, 8)?); - ((1..=12).contains(&m) && (1..=31).contains(&d)).then_some([y, m, d, 0, 0, 0]) + let leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0; + let days = match m { + 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, + 4 | 6 | 9 | 11 => 30, + 2 if leap => 29, + 2 => 28, + _ => return None, + }; + (1..=days).contains(&d).then_some([y, m, d, 0, 0, 0]) } #[cfg(test)] @@ -149,6 +162,14 @@ mod tests { assert_eq!(render("#", &NameParts { counter: 12345, ..parts() }), "12345"); } + #[test] + fn dates_are_strict_about_hyphens_and_days() { + for bad in ["2026-1-011", "2-0-2-6-1-0-1-6", "2026--10--16", "2026-02-31", "2026-10-16-", "202610-16"] { + assert_eq!(parse_date(bad), None, "{bad}"); + } + assert_eq!(parse_date("2024-02-29"), Some([2024, 2, 29, 0, 0, 0])); + } + #[test] fn dates_parse() { assert_eq!(parse_date("2026-10-16"), Some([2026, 10, 16, 0, 0, 0])); From 88d8a7839ead799509b773d001a3580b061a2d26 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Sun, 25 Oct 2026 10:40:27 +0000 Subject: [PATCH 2/2] fix: validate parse_date calendar dates with chrono Co-Authored-By: Claude Sonnet 5.5 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- Cargo.lock | 1 + crates/engine/Cargo.toml | 1 + crates/engine/src/cmd/fileio/naming.rs | 11 ++--------- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 01b2e0e81..8e85bad40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6228,6 +6228,7 @@ name = "vectorcraft-engine" version = "0.20.0" dependencies = [ "cfb", + "chrono", "craft-docpath", "crc32fast", "flate2", diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml index 9d4ad7399..87e2d48be 100644 --- a/crates/engine/Cargo.toml +++ b/crates/engine/Cargo.toml @@ -38,6 +38,7 @@ vectorcraft-scriptcheck = { workspace = true } image = { workspace = true } hayro-jpeg2000 = { version = "=0.3.5", default-features = false, features = ["std", "simd"] } flate2 = { workspace = true } +chrono = { workspace = true } # The XML parts of Word documents placed as type (place/docx.rs). roxmltree = "0.21" # The compound file that holds a Word 97-2003 document placed as type (place/doc.rs). diff --git a/crates/engine/src/cmd/fileio/naming.rs b/crates/engine/src/cmd/fileio/naming.rs index b79998170..e701a03d0 100644 --- a/crates/engine/src/cmd/fileio/naming.rs +++ b/crates/engine/src/cmd/fileio/naming.rs @@ -111,15 +111,8 @@ pub fn parse_date(s: &str) -> Option<[i64; 6]> { } let num = |a: usize, b: usize| digits.get(a..b).and_then(|t| t.parse::().ok()); let (y, m, d) = (num(0, 4)?, num(4, 6)?, num(6, 8)?); - let leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0; - let days = match m { - 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, - 4 | 6 | 9 | 11 => 30, - 2 if leap => 29, - 2 => 28, - _ => return None, - }; - (1..=days).contains(&d).then_some([y, m, d, 0, 0, 0]) + chrono::NaiveDate::from_ymd_opt(i32::try_from(y).ok()?, u32::try_from(m).ok()?, u32::try_from(d).ok()?)?; + Some([y, m, d, 0, 0, 0]) } #[cfg(test)]