From 245ca88af615ae840316bf033db454d1b8e80ba2 Mon Sep 17 00:00:00 2001 From: "nikita.hohlov" Date: Fri, 4 Sep 2026 06:52:28 -0300 Subject: [PATCH] fix: validate calendar date in fixed-offset due_at Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01BCrwHHnGCB5XH968Nxokqw --- core/app/Ingest/DueAtParser.php | 7 +++++-- core/tests/Unit/Ingest/DueAtParserTest.php | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/core/app/Ingest/DueAtParser.php b/core/app/Ingest/DueAtParser.php index af08256..96eaa6e 100644 --- a/core/app/Ingest/DueAtParser.php +++ b/core/app/Ingest/DueAtParser.php @@ -10,7 +10,7 @@ final class DueAtParser { private const DATE = '/^(\d{4})-(\d{2})-(\d{2})$/'; private const LOCAL_DATETIME = '/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?$/'; - private const FIXED_DATETIME = '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2})?(Z|[+-]\d{2}:\d{2})$/'; + private const FIXED_DATETIME = '/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?(Z|[+-]\d{2}:\d{2})$/'; public static function parse(string $raw): ParsedDue { @@ -30,7 +30,10 @@ final class DueAtParser ); } - if (preg_match(self::FIXED_DATETIME, $raw)) { + if (preg_match(self::FIXED_DATETIME, $raw, $m)) { + self::assertDate((int) $m[1], (int) $m[2], (int) $m[3], $raw); + self::assertTime((int) $m[4], (int) $m[5], (int) ($m[6] ?? 0), $raw); + try { return ParsedDue::fixed(CarbonImmutable::parse($raw)); } catch (\Throwable $e) { diff --git a/core/tests/Unit/Ingest/DueAtParserTest.php b/core/tests/Unit/Ingest/DueAtParserTest.php index 335ce9c..2753526 100644 --- a/core/tests/Unit/Ingest/DueAtParserTest.php +++ b/core/tests/Unit/Ingest/DueAtParserTest.php @@ -43,7 +43,7 @@ class DueAtParserTest extends TestCase public function test_garbage_is_rejected(): void { - foreach (['', 'tomorrow', '2026-13-01', '14:00', '2026-09-10 14:00', '2026-09-10T25:00'] as $bad) { + foreach (['', 'tomorrow', '2026-13-01', '14:00', '2026-09-10 14:00', '2026-09-10T25:00', '2026-02-30T08:15:00+02:00', '2026-09-10T25:00Z'] as $bad) { try { DueAtParser::parse($bad); $this->fail("accepted: $bad");