Skip to content

Commit f6c77dd

Browse files
committed
gh-125318: Fix validation of zoneinfo false friends
Reject fold=2 before it can reach the transition array, and use the correct format specifier for the long fold value. Match the datetime year error message and use MAXYEAR for the upper bound. Add regression tests for invalid folds and years, conversion errors, and valid boundary values.
1 parent f3a267b commit f6c77dd

2 files changed

Lines changed: 52 additions & 8 deletions

File tree

‎Lib/test/test_zoneinfo/test_zoneinfo.py‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import shutil
1414
import struct
1515
import tempfile
16+
import types
1617
import unittest
1718
from datetime import date, datetime, time, timedelta, timezone
1819
from functools import cached_property
@@ -446,6 +447,52 @@ def __eq__(self, other):
446447
class CZoneInfoTest(ZoneInfoTest):
447448
module = c_zoneinfo
448449

450+
def test_duck_typed_invalid_values(self):
451+
long_max = (1 << (struct.calcsize("l") * 8 - 1)) - 1
452+
cases = [
453+
("fold", value, ValueError, f"fold must be 0 or 1, got {value}")
454+
for value in (-1, 2, 3, 256, long_max, -long_max - 1)
455+
]
456+
cases.extend(
457+
("year", value, ValueError, f"year must be in 1..9999, not {value}")
458+
for value in (0, -1, 10000)
459+
)
460+
cases.extend(
461+
(attr, value, exc, message)
462+
for attr in ("fold", "year")
463+
for value, exc, message in (
464+
(1 << 100, OverflowError, ""),
465+
(-(1 << 100), OverflowError, ""),
466+
(None, TypeError, ""),
467+
)
468+
)
469+
# Use a date beyond the last transition to exercise year validation.
470+
dt = datetime(2040, 1, 1)
471+
zi = self.zone_from_key("Europe/London")
472+
for attr, value, exc, message in cases:
473+
false_friend = types.SimpleNamespace(
474+
year=dt.year, hour=0, minute=0, second=0, fold=0,
475+
toordinal=dt.toordinal,
476+
)
477+
setattr(false_friend, attr, value)
478+
for method in (zi.utcoffset, zi.dst, zi.tzname):
479+
with self.subTest(attr=attr, value=value, method=method):
480+
with self.assertRaisesRegex(exc, re.escape(message)):
481+
method(false_friend)
482+
483+
def test_duck_typed_valid_boundaries(self):
484+
zi = self.zone_from_key("Europe/London")
485+
for year in (1, 9999):
486+
for fold in (0, 1):
487+
dt = datetime(year, 1, 1, fold=fold)
488+
false_friend = types.SimpleNamespace(
489+
year=year, hour=0, minute=0, second=0, fold=fold,
490+
toordinal=dt.toordinal,
491+
)
492+
for method in (zi.utcoffset, zi.dst, zi.tzname):
493+
with self.subTest(year=year, fold=fold, method=method):
494+
self.assertEqual(method(false_friend), method(dt))
495+
449496
@unittest.skipIf(MISSING_C_DOCSTRINGS,
450497
"Signature information for builtins requires docstrings")
451498
def test_signatures(self):

‎Modules/_zoneinfo.c‎

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2253,9 +2253,9 @@ find_ttinfo(zoneinfo_state *state, PyZoneInfo_ZoneInfo *self, PyObject *dt)
22532253
if (PyErr_Occurred()) {
22542254
return NULL;
22552255
}
2256-
if (fold_int < 0 || fold_int > 2) {
2256+
if (fold_int < 0 || fold_int > 1) {
22572257
PyErr_Format(PyExc_ValueError,
2258-
"fold must be 0 or 1, got %d", fold_int);
2258+
"fold must be 0 or 1, got %ld", fold_int);
22592259
return NULL;
22602260
}
22612261
fold = (unsigned char)fold_int;
@@ -2285,13 +2285,10 @@ find_ttinfo(zoneinfo_state *state, PyZoneInfo_ZoneInfo *self, PyObject *dt)
22852285
return NULL;
22862286
}
22872287

2288-
if (year < MINYEAR || year > 9999) {
2288+
if (year < MINYEAR || year > MAXYEAR) {
22892289
PyErr_Format(PyExc_ValueError,
2290-
"year out of range, should be in (%d, %d) but got %d",
2291-
MINYEAR,
2292-
MAXYEAR,
2293-
year
2294-
);
2290+
"year must be in %d..%d, not %d",
2291+
MINYEAR, MAXYEAR, year);
22952292
return NULL;
22962293
}
22972294
}

0 commit comments

Comments
 (0)