Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions Lib/test/test_zoneinfo/test_zoneinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,38 @@ def test_empty_zone(self):
with self.assertRaises(ValueError):
self.klass.from_file(zf)

def test_invalid_transition_index(self):
STD = ZoneOffset("STD", ZERO)
DST = ZoneOffset("DST", ONE_H, ONE_H)

zf = self.construct_zone([
ZoneTransition(datetime(2026, 3, 1, 2), STD, DST),
ZoneTransition(datetime(2026, 11, 1, 2), DST, STD),
], after="", version=1)

data = bytearray(zf.read())
timecnt = struct.unpack_from(">l", data, 32)[0]
idx_offset = 44 + timecnt * 4
data[idx_offset + 1] = 2 # typecnt is 2, so index 2 is OOB
f = io.BytesIO(bytes(data))

with self.assertRaises(ValueError):
self.klass.from_file(f)

def test_transition_lookahead_out_of_bounds(self):
STD = ZoneOffset("STD", ZERO)
DST = ZoneOffset("DST", ONE_H, ONE_H)
EXT = ZoneOffset("EXT", ONE_H)

zf = self.construct_zone([
ZoneTransition(datetime(2026, 3, 1), STD, DST),
ZoneTransition(datetime(2026, 6, 1), DST, EXT),
ZoneTransition(datetime(2026, 9, 1), EXT, DST),
], after="")

zi = self.klass.from_file(zf)
self.assertIsNotNone(zi)

def test_zone_very_large_timestamp(self):
"""Test when a transition is in the far past or future.

Expand Down Expand Up @@ -1577,6 +1609,59 @@ class EvilZoneInfo(self.klass):
class CZoneInfoCacheTest(ZoneInfoCacheTest):
module = c_zoneinfo

@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: RuntimeError not raised
def test_inconsistent_weak_cache_get(self):
class Cache:
def get(self, key, default=None):
return 1337

class ZI(self.klass):
pass
# Class attribute must be set after class creation
# to override zoneinfo.ZoneInfo.__init_subclass__.
ZI._weak_cache = Cache()

with self.assertRaises(RuntimeError) as te:
ZI("America/Los_Angeles")
self.assertEqual(
str(te.exception),
"Unexpected instance of int in ZI weak cache for key 'America/Los_Angeles'"
)

@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: AttributeError not raised
def test_deleted_weak_cache(self):
class ZI(self.klass):
pass
delattr(ZI, '_weak_cache')

# These should not segfault
with self.assertRaises(AttributeError):
ZI("UTC")

with self.assertRaises(AttributeError):
ZI.clear_cache()

@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError: 'int' object has no attribute '_from_cache'
def test_inconsistent_weak_cache_setdefault(self):
class Cache:
def get(self, key, default=None):
return default
def setdefault(self, key, value):
return 1337

class ZI(self.klass):
pass
# Class attribute must be set after class creation
# to override zoneinfo.ZoneInfo.__init_subclass__.
ZI._weak_cache = Cache()

with self.assertRaises(RuntimeError) as te:
ZI("America/Los_Angeles")
self.assertEqual(
str(te.exception),
"Unexpected instance of int in ZI weak cache for key 'America/Los_Angeles'"
)


class ZoneInfoPickleTest(TzPathUserMixin, ZoneInfoTestBase):
module = py_zoneinfo
Expand Down
4 changes: 4 additions & 0 deletions Lib/zoneinfo/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ def load_data(fobj):
f">{timecnt}{time_type}", fobj.read(timecnt * time_size)
)
trans_idx = struct.unpack(f">{timecnt}B", fobj.read(timecnt))

if max(trans_idx) >= typecnt:
raise ValueError("Invalid transition index found while reading TZif: "
f"{max(trans_idx)}")
else:
trans_list_utc = ()
trans_idx = ()
Expand Down
8 changes: 6 additions & 2 deletions Lib/zoneinfo/_zoneinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@ def __new__(cls, key):
cls._strong_cache[key] = cls._strong_cache.pop(key, instance)

if len(cls._strong_cache) > cls._strong_cache_size:
cls._strong_cache.popitem(last=False)
try:
cls._strong_cache.popitem(last=False)
except KeyError:
# another thread may have already emptied the cache
pass

return instance

Expand Down Expand Up @@ -334,7 +338,7 @@ def _utcoff_to_dstoff(trans_idx, utcoffsets, isdsts):
if not isdsts[comp_idx]:
dstoff = utcoff - utcoffsets[comp_idx]

if not dstoff and idx < (typecnt - 1):
if not dstoff and idx < (typecnt - 1) and i + 1 < len(trans_idx):
comp_idx = trans_idx[i + 1]

# If the following transition is also DST and we couldn't
Expand Down
Loading