Skip to content

Commit e2d209d

Browse files
committed
fixup! gh-125318: Fix validation of zoneinfo false friends
1 parent f6c77dd commit e2d209d

3 files changed

Lines changed: 62 additions & 16 deletions

File tree

‎Lib/test/test_zoneinfo/test_zoneinfo.py‎

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -443,19 +443,16 @@ def __eq__(self, other):
443443
self.assertFalse(key.eq_called)
444444
self.assertIs(tz2, tz1)
445445

446-
447-
class CZoneInfoTest(ZoneInfoTest):
448-
module = c_zoneinfo
449-
450446
def test_duck_typed_invalid_values(self):
451447
long_max = (1 << (struct.calcsize("l") * 8 - 1)) - 1
448+
int_max = (1 << (struct.calcsize("i") * 8 - 1)) - 1
452449
cases = [
453-
("fold", value, ValueError, f"fold must be 0 or 1, got {value}")
450+
("fold", value, ValueError, f"fold must be 0 or 1, not {value}")
454451
for value in (-1, 2, 3, 256, long_max, -long_max - 1)
455452
]
456453
cases.extend(
457454
("year", value, ValueError, f"year must be in 1..9999, not {value}")
458-
for value in (0, -1, 10000)
455+
for value in (0, -1, 10000, int_max, -int_max - 1)
459456
)
460457
cases.extend(
461458
(attr, value, exc, message)
@@ -464,21 +461,29 @@ def test_duck_typed_invalid_values(self):
464461
(1 << 100, OverflowError, ""),
465462
(-(1 << 100), OverflowError, ""),
466463
(None, TypeError, ""),
464+
(1.0, TypeError, ""),
467465
)
468466
)
467+
cases.extend(
468+
(attr, value, OverflowError, "")
469+
for attr, maximum in (("fold", long_max), ("year", int_max))
470+
for value in (maximum + 1, -maximum - 2)
471+
)
469472
# Use a date beyond the last transition to exercise year validation.
470473
dt = datetime(2040, 1, 1)
471-
zi = self.zone_from_key("Europe/London")
472474
for attr, value, exc, message in cases:
473475
false_friend = types.SimpleNamespace(
474476
year=dt.year, hour=0, minute=0, second=0, fold=0,
475477
toordinal=dt.toordinal,
476478
)
477479
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)
480+
for key in ("Europe/London", "UTC"):
481+
zi = self.zone_from_key(key)
482+
for method in (zi.utcoffset, zi.dst, zi.tzname):
483+
with self.subTest(attr=attr, value=value, key=key,
484+
method=method):
485+
with self.assertRaisesRegex(exc, re.escape(message)):
486+
method(false_friend)
482487

483488
def test_duck_typed_valid_boundaries(self):
484489
zi = self.zone_from_key("Europe/London")
@@ -493,6 +498,30 @@ def test_duck_typed_valid_boundaries(self):
493498
with self.subTest(year=year, fold=fold, method=method):
494499
self.assertEqual(method(false_friend), method(dt))
495500

501+
def test_duck_typed_index(self):
502+
class Index:
503+
def __init__(self, value):
504+
self.value = value
505+
506+
def __index__(self):
507+
return self.value
508+
509+
zi = self.zone_from_key("Europe/London")
510+
for fold in (0, 1):
511+
# An ambiguous time beyond the last explicit transition.
512+
dt = datetime(2040, 10, 28, 1, 30, fold=fold)
513+
false_friend = types.SimpleNamespace(
514+
year=Index(dt.year), hour=dt.hour, minute=dt.minute,
515+
second=dt.second, fold=Index(fold), toordinal=dt.toordinal,
516+
)
517+
for method in (zi.utcoffset, zi.dst, zi.tzname):
518+
with self.subTest(fold=fold, method=method):
519+
self.assertEqual(method(false_friend), method(dt))
520+
521+
522+
class CZoneInfoTest(ZoneInfoTest):
523+
module = c_zoneinfo
524+
496525
@unittest.skipIf(MISSING_C_DOCSTRINGS,
497526
"Signature information for builtins requires docstrings")
498527
def test_signatures(self):

‎Lib/zoneinfo/_zoneinfo.py‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
import calendar
33
import collections
44
import functools
5+
import operator
56
import re
7+
import struct
68
import weakref
79
from datetime import datetime, timedelta, tzinfo
810

@@ -11,6 +13,10 @@
1113
EPOCH = datetime(1970, 1, 1)
1214
EPOCHORDINAL = datetime(1970, 1, 1).toordinal()
1315

16+
# Match the integer conversions in the C implementation.
17+
_LONG_MAX = (1 << (struct.calcsize("l") * 8 - 1)) - 1
18+
_INT_MAX = (1 << (struct.calcsize("i") * 8 - 1)) - 1
19+
1420
# It is relatively expensive to construct new timedelta objects, and in most
1521
# cases we're looking at the same deltas, like integer numbers of hours, etc.
1622
# To improve speed and memory use, we'll keep a dictionary with references
@@ -168,15 +174,26 @@ def _find_trans(self, dt):
168174

169175
ts = self._get_local_timestamp(dt)
170176

171-
lt = self._trans_local[dt.fold]
177+
fold = operator.index(dt.fold)
178+
if not -_LONG_MAX - 1 <= fold <= _LONG_MAX:
179+
raise OverflowError("Python int too large to convert to C long")
180+
if not 0 <= fold <= 1:
181+
raise ValueError(f"fold must be 0 or 1, not {fold}")
182+
183+
lt = self._trans_local[fold]
172184

173185
num_trans = len(lt)
174186

175187
if num_trans and ts < lt[0]:
176188
return self._tti_before
177189
elif not num_trans or ts > lt[-1]:
190+
year = operator.index(dt.year)
191+
if not -_INT_MAX - 1 <= year <= _INT_MAX:
192+
raise OverflowError("Python int too large to convert to C int")
193+
if not 1 <= year <= 9999:
194+
raise ValueError(f"year must be in 1..9999, not {year}")
178195
if isinstance(self._tz_after, _TZStr):
179-
return self._tz_after.get_trans_info(ts, dt.year, dt.fold)
196+
return self._tz_after.get_trans_info(ts, year, fold)
180197
else:
181198
return self._tz_after
182199
else:

‎Modules/_zoneinfo.c‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2250,12 +2250,12 @@ find_ttinfo(zoneinfo_state *state, PyZoneInfo_ZoneInfo *self, PyObject *dt)
22502250

22512251
long fold_int = PyLong_AsLong(fold_obj);
22522252
Py_DECREF(fold_obj);
2253-
if (PyErr_Occurred()) {
2253+
if (fold_int == -1 && PyErr_Occurred()) {
22542254
return NULL;
22552255
}
22562256
if (fold_int < 0 || fold_int > 1) {
22572257
PyErr_Format(PyExc_ValueError,
2258-
"fold must be 0 or 1, got %ld", fold_int);
2258+
"fold must be 0 or 1, not %ld", fold_int);
22592259
return NULL;
22602260
}
22612261
fold = (unsigned char)fold_int;
@@ -2281,7 +2281,7 @@ find_ttinfo(zoneinfo_state *state, PyZoneInfo_ZoneInfo *self, PyObject *dt)
22812281

22822282
year = PyLong_AsInt(year_obj);
22832283
Py_DECREF(year_obj);
2284-
if (PyErr_Occurred()) {
2284+
if (year == -1 && PyErr_Occurred()) {
22852285
return NULL;
22862286
}
22872287

0 commit comments

Comments
 (0)