Skip to content
Open
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
130 changes: 128 additions & 2 deletions Lib/test/test_zoneinfo/test_zoneinfo_property.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@
import pickle
import unittest
import zoneinfo

from test.support.hypothesis_helper import hypothesis

import test.test_zoneinfo._support as test_support

ZoneInfoTestBase = test_support.ZoneInfoTestBase
Expand Down Expand Up @@ -61,6 +59,50 @@ def valid_key(key):
return output


def _make_datetime_class(missing):
all_attrs = (
"year",
"month",
"day",
"hour",
"minute",
"second",
"microsecond",
"tzinfo",
"fold",
)
included_attrs = set(all_attrs) - set(missing)

class ClassWithMissing:
def __init__(self, *args, **kwargs):
self._datetime = datetime.datetime(*args, **kwargs)
for attr, arg in zip(all_attrs, args):
if attr in kwargs:
raise ValueError(
f"{attr} specified more than once in argument list"
)

kwargs[attr] = arg

for attr, arg in kwargs.items():
if attr in included_attrs:
setattr(self, attr, arg)

def utcoffset(self):
return self.tzinfo.utcoffset(self)

def dst(self):
return self.tzinfo.dst(self)

def tzname(self):
return self.tzinfo.tzname(self)

def toordinal(self):
return self._datetime.toordinal()

return ClassWithMissing


VALID_KEYS = _valid_keys()
if not VALID_KEYS:
raise unittest.SkipTest("No time zone data available")
Expand Down Expand Up @@ -127,6 +169,90 @@ def test_utc(self, dt):
self.assertEqual(dt_zi.dst(), ZERO)
self.assertEqual(dt_zi.tzname(), "UTC")

@hypothesis.given(
dt=hypothesis.strategies.datetimes(),
key=valid_keys(),
missing=hypothesis.strategies.lists(
hypothesis.strategies.sampled_from(
(
"year",
"month",
"day",
"hour",
"minute",
"second",
"microsecond",
"tzinfo",
"fold",
)
),
unique=True,
min_size=1,
),
)
@hypothesis.example(
dt=datetime.datetime(1995, 9, 2, 14, 34, 56),
key="Europe/Berlin",
missing=["fold"],
)
@hypothesis.example(
dt=datetime.datetime(1995, 9, 2, 14, 34, 56),
key="Europe/Berlin",
missing=["year"],
)
@hypothesis.example(
dt=datetime.datetime(1995, 9, 2, 14, 34, 56),
key="Europe/Berlin",
missing=["tzinfo"],
)
@hypothesis.example(
dt=datetime.datetime(1995, 9, 2, 14, 34, 56),
key="Europe/Berlin",
missing=[
"year",
"month",
"day",
"hour",
"minute",
"second",
"microsecond",
"tzinfo",
"fold",
],
)
def test_bad_duck_typed_class(self, dt, key, missing):
# Passing duck typed `datetime`-lookalikes is not actively supported,
# but it also shouldn't raise segfaults.

tzi = self.module.ZoneInfo(key)
DateTimeClass = _make_datetime_class(missing=missing)
false_friend = DateTimeClass(
dt.year,
dt.month,
dt.day,
dt.hour,
dt.minute,
dt.second,
dt.microsecond,
tzinfo=tzi,
fold=dt.fold,
)

try:
false_friend.utcoffset()
except Exception:
pass

try:
false_friend.dst()
except Exception:
pass

try:
false_friend.tzname()
except Exception:
pass


class CZoneInfoTest(ZoneInfoTest):
module = c_zoneinfo
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fixes a segmentation fault that would happen if :class:`zoneinfo.ZoneInfo`
were used with certain non-:class:`datetime.datetime` classes. Patch by Paul
Ganssle
56 changes: 53 additions & 3 deletions Modules/_zoneinfo.c
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ static const int SOURCE_FILE = 2;

static const size_t ZONEINFO_STRONG_CACHE_MAX_SIZE = 8;

static const int MINYEAR = 1;
static const int MAXYEAR = 9999;

// Forward declarations
static int
load_data(zoneinfo_state *state, PyZoneInfo_ZoneInfo *self,
Expand Down Expand Up @@ -2197,7 +2200,29 @@ find_ttinfo(zoneinfo_state *state, PyZoneInfo_ZoneInfo *self, PyObject *dt)
return NULL;
}

unsigned char fold = PyDateTime_DATE_GET_FOLD(dt);
unsigned char fold;
if (PyDateTime_Check(dt)) {
fold = PyDateTime_DATE_GET_FOLD(dt);
}
else {
PyObject *fold_obj = PyObject_GetAttrString(dt, "fold");
if (fold_obj == NULL) {
return NULL;
}

long fold_int = PyLong_AsLong(fold_obj);
Py_DECREF(fold_obj);
if (PyErr_Occurred()) {
return NULL;
}
if (fold_int < 0 || fold_int > 2) {
PyErr_Format(PyExc_ValueError,
"fold must be 0 or 1, got %d", fold_int);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"fold must be 0 or 1, got %d", fold_int);
"fold must be 0 or 1, got %ld", fold_int);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or use int and PyLong_AsLong() for fold_int.

return NULL;
}
fold = (unsigned char)fold_int;
Comment thread
serhiy-storchaka marked this conversation as resolved.
}

assert(fold < 2);
int64_t *local_transitions = self->trans_list_wall[fold];
size_t num_trans = self->num_transitions;
Expand All @@ -2206,8 +2231,33 @@ find_ttinfo(zoneinfo_state *state, PyZoneInfo_ZoneInfo *self, PyObject *dt)
return self->ttinfo_before;
}
else if (!num_trans || ts > local_transitions[self->num_transitions - 1]) {
return find_tzrule_ttinfo(&(self->tzrule_after), ts, fold,
PyDateTime_GET_YEAR(dt));
int year;
if (PyDateTime_Check(dt)) {
year = PyDateTime_GET_YEAR(dt);
}
else {
PyObject *year_obj = PyObject_GetAttrString(dt, "year");
if (year_obj == NULL) {
return NULL;
}

year = PyLong_AsInt(year_obj);
Py_DECREF(year_obj);
if (PyErr_Occurred()) {
return NULL;
}

if (year < MINYEAR || year > 9999) {
PyErr_Format(PyExc_ValueError,
"year out of range, should be in (%d, %d) but got %d",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See error messages in check_date_args(), datetime_date_fromisocalendar_impl(), utc_to_seconds().

MINYEAR,
MAXYEAR,
year
);
return NULL;
}
}
return find_tzrule_ttinfo(&(self->tzrule_after), ts, fold, year);
}
else {
size_t idx = _bisect(ts, local_transitions, self->num_transitions) - 1;
Expand Down
Loading