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");