fix: reject a year Date.UTC would silently move
`iso()` read back the month and the day to catch February 30 rolling over to March 2, and took the year on trust. `Date.UTC` maps years 0–99 into the 1900s, so a four-digit "0050" came back as 1950 with the month and day intact — past both existing checks, because nothing about the resulting date is impossible. Same class of failure as the rollover the guard was written for: a boundary the source never stated, published as though it had. Reading the year back too is the skip-rather-than-guess rule this module runs on, applied to the one field that was not checked. It narrows only — `parseShortSlashRange`'s two-digit pivot produces full years and is untouched. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
0b5aa12173
commit
23ad2ad797
+13
-2
@@ -40,8 +40,19 @@ function iso(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const date = new Date(Date.UTC(y, m - 1, d, hh, mm, ss, 0));
|
const date = new Date(Date.UTC(y, m - 1, d, hh, mm, ss, 0));
|
||||||
// Rejects impossible calendar dates such as February 30.
|
// Rejects impossible calendar dates such as February 30 — and a year that did
|
||||||
if (date.getUTCMonth() !== m - 1 || date.getUTCDate() !== d) return null;
|
// not survive the round trip. `Date.UTC` maps 0–99 into the 1900s, so a
|
||||||
|
// four-digit "0050" becomes 1950 with the month and day intact, which the two
|
||||||
|
// checks beside this one cannot see. Reading the year back is the same
|
||||||
|
// skip-rather-than-guess rule the rest of this module runs on, applied to the
|
||||||
|
// one field that was taken on trust.
|
||||||
|
if (
|
||||||
|
date.getUTCFullYear() !== y ||
|
||||||
|
date.getUTCMonth() !== m - 1 ||
|
||||||
|
date.getUTCDate() !== d
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return date.toISOString();
|
return date.toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,20 @@ describe("parseMonthDayYear", () => {
|
|||||||
test("rejects an unknown month name", () => {
|
test("rejects an unknown month name", () => {
|
||||||
expect(parseMonthDayYear("Smarch 3, 2026")).toBeNull();
|
expect(parseMonthDayYear("Smarch 3, 2026")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("rejects a year that Date.UTC would silently move", () => {
|
||||||
|
// `Date.UTC` maps years 0–99 into the 1900s, so "0050" came back as 1950
|
||||||
|
// with the month and day intact — which the impossible-date guard cannot
|
||||||
|
// see, because nothing about the date is impossible. It is the same class of
|
||||||
|
// failure as February 30 rolling over to March 2: a boundary the source
|
||||||
|
// never stated, published as though it had.
|
||||||
|
expect(parseMonthDayYear("August 12, 0050")).toBeNull();
|
||||||
|
expect(parseMonthDayYear("August 12, 0099")).toBeNull();
|
||||||
|
// And a real four-digit year is untouched.
|
||||||
|
expect(parseMonthDayYear("August 12, 2026")?.iso).toBe(
|
||||||
|
"2026-08-12T00:00:00.000Z",
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("parseMonthDayRange", () => {
|
describe("parseMonthDayRange", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user