@@ -4,6 +4,8 @@ import { parseStrictPositiveInteger } from "../infra/parse-finite-number.js";
44const ISO_TZ_RE = / ( Z | [ + - ] \d { 2 } : ? \d { 2 } ) $ / i;
55const ISO_DATE_RE = / ^ \d { 4 } - \d { 2 } - \d { 2 } $ / ;
66const ISO_DATE_TIME_RE = / ^ \d { 4 } - \d { 2 } - \d { 2 } T / ;
7+ const ISO_ABSOLUTE_RE =
8+ / ^ ( \d { 4 } ) - ( \d { 2 } ) - ( \d { 2 } ) (?: T ( \d { 2 } ) : ( \d { 2 } ) (?: : ( \d { 2 } ) ( \. \d + ) ? ) ? (?: [ Z z ] | [ + - ] \d { 2 } : ? \d { 2 } ) ? ) ? $ / ;
79
810function normalizeUtcIso ( raw : string ) {
911 if ( ISO_TZ_RE . test ( raw ) ) {
@@ -18,6 +20,47 @@ function normalizeUtcIso(raw: string) {
1820 return raw ;
1921}
2022
23+ function isValidIsoAbsolute ( raw : string ) {
24+ const match = ISO_ABSOLUTE_RE . exec ( raw ) ;
25+ if ( ! match ) {
26+ return false ;
27+ }
28+
29+ const [
30+ ,
31+ yearRaw ,
32+ monthRaw ,
33+ dayRaw ,
34+ hourRaw = "0" ,
35+ minuteRaw = "0" ,
36+ secondRaw = "0" ,
37+ fractionRaw ,
38+ ] = match ;
39+ const year = Number ( yearRaw ) ;
40+ const month = Number ( monthRaw ) ;
41+ const day = Number ( dayRaw ) ;
42+ const hour = Number ( hourRaw ) ;
43+ const minute = Number ( minuteRaw ) ;
44+ const second = Number ( secondRaw ) ;
45+ const millisecond = fractionRaw ? Number ( fractionRaw . slice ( 1 , 4 ) . padEnd ( 3 , "0" ) ) : 0 ;
46+ const isEndOfDay = hour === 24 && minute === 0 && second === 0 && millisecond === 0 ;
47+
48+ // Date.parse rolls invalid calendar dates; cron must reject them before scheduling.
49+ const probe = new Date ( 0 ) ;
50+ probe . setUTCFullYear ( year , month - 1 , day ) ;
51+ probe . setUTCHours ( isEndOfDay ? 0 : hour , minute , second , millisecond ) ;
52+
53+ return (
54+ probe . getUTCFullYear ( ) === year &&
55+ probe . getUTCMonth ( ) === month - 1 &&
56+ probe . getUTCDate ( ) === day &&
57+ probe . getUTCHours ( ) === ( isEndOfDay ? 0 : hour ) &&
58+ probe . getUTCMinutes ( ) === minute &&
59+ probe . getUTCSeconds ( ) === second &&
60+ probe . getUTCMilliseconds ( ) === millisecond
61+ ) ;
62+ }
63+
2164/** Parses absolute cron timestamps from epoch milliseconds or ISO-like strings normalized to UTC. */
2265export function parseAbsoluteTimeMs ( input : string ) : number | null {
2366 const raw = input . trim ( ) ;
@@ -31,6 +74,9 @@ export function parseAbsoluteTimeMs(input: string): number | null {
3174 }
3275 return null ;
3376 }
77+ if ( ! isValidIsoAbsolute ( raw ) ) {
78+ return null ;
79+ }
3480 const parsed = Date . parse ( normalizeUtcIso ( raw ) ) ;
3581 return Number . isFinite ( parsed ) ? parsed : null ;
3682}
0 commit comments