@@ -4,6 +4,8 @@ import { parseStrictPositiveInteger } from "../infra/parse-finite-number.js";
|
4 | 4 | const ISO_TZ_RE = /(Z|[+-]\d{2}:?\d{2})$/i; |
5 | 5 | const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/; |
6 | 6 | const 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+)?)?(?:[Zz]|[+-]\d{2}:?\d{2})?)?$/; |
7 | 9 | |
8 | 10 | function normalizeUtcIso(raw: string) { |
9 | 11 | if (ISO_TZ_RE.test(raw)) { |
@@ -18,6 +20,47 @@ function normalizeUtcIso(raw: string) {
|
18 | 20 | return raw; |
19 | 21 | } |
20 | 22 | |
| 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 | + |
21 | 64 | /** Parses absolute cron timestamps from epoch milliseconds or ISO-like strings normalized to UTC. */ |
22 | 65 | export function parseAbsoluteTimeMs(input: string): number | null { |
23 | 66 | const raw = input.trim(); |
@@ -31,6 +74,9 @@ export function parseAbsoluteTimeMs(input: string): number | null {
|
31 | 74 | } |
32 | 75 | return null; |
33 | 76 | } |
| 77 | +if (!isValidIsoAbsolute(raw)) { |
| 78 | +return null; |
| 79 | +} |
34 | 80 | const parsed = Date.parse(normalizeUtcIso(raw)); |
35 | 81 | return Number.isFinite(parsed) ? parsed : null; |
36 | 82 | } |