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
28 changes: 28 additions & 0 deletions Lib/test/test_zoneinfo/test_zoneinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,34 @@ def test_invalid_transition_index(self):
with self.assertRaises(ValueError):
self.klass.from_file(f)

def test_invalid_charcnt(self):
# 128 local time types whose abbreviations are the suffixes of a
# single long abbreviation spanning the entire block: every index
# would get its own copy of most of the block.
types = b"".join(struct.pack(">lbb", 3600, 0, i) for i in range(128))
chars = b"A" * 65535 + b"\x00"

# With a negative charcnt the block would be the rest of the file
for charcnt in (len(chars), -1):
with self.subTest(charcnt=charcnt):
header = b"TZif" + b"\x00" * 16
header += struct.pack(">6l", 0, 0, 0, 0, 128, charcnt)
f = io.BytesIO(header + types + chars)

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

def test_abbreviation_index_above_127(self):
chars = b"X" * 129 + b"\x00" + b"ABC\x00"
header = b"TZif" + b"\x00" * 16
header += struct.pack(">6l", 0, 0, 0, 0, 1, len(chars))
types = struct.pack(">lbB", 3600, 0, 130)
zi = self.klass.from_file(io.BytesIO(header + types + chars))

dt = datetime(2026, 1, 1, tzinfo=zi)
self.assertEqual(dt.tzname(), "ABC")
self.assertEqual(dt.utcoffset(), ONE_H)

def test_transition_lookahead_out_of_bounds(self):
STD = ZoneOffset("STD", ZERO)
DST = ZoneOffset("DST", ONE_H, ONE_H)
Expand Down
10 changes: 9 additions & 1 deletion Lib/zoneinfo/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def load_data(fobj):
# Read the ttinfo struct, (utoff, isdst, abbrind)
if typecnt:
utcoff, isdst, abbrind = zip(
*(struct.unpack(">lbb", fobj.read(6)) for i in range(typecnt))
*(struct.unpack(">lbB", fobj.read(6)) for i in range(typecnt))
)
else:
utcoff = ()
Expand All @@ -89,6 +89,14 @@ def load_data(fobj):
# not by position in the array but by position in the unsplit
# abbreviation string. I suppose this makes more sense in C, which uses
# null to terminate the strings, but it's inconvenient here...
#
# Each abbreviation index is a single unsigned byte, so only about the
# first 256 characters of the block can be referenced. A much longer
# block holds nothing usable, and reading it would copy most of the
# block for every local time type.
if not 0 <= charcnt <= 512:
raise ValueError(f"Invalid TZif file: invalid charcnt: {charcnt}")

abbr_vals = {}
abbr_chars = fobj.read(charcnt)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fix :meth:`zoneinfo.ZoneInfo.from_file` accepting TZif data with a negative or
oversized abbreviation block, which could use hundreds of times the file size
in memory; it now raises :exc:`ValueError`. Abbreviation indexes above 127 now
also give the correct time zone abbreviation.
Loading