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
9 changes: 9 additions & 0 deletions Lib/test/datetimetester.py
Original file line number Diff line number Diff line change
Expand Up @@ -1462,6 +1462,15 @@ def test_insane_fromtimestamp(self):
self.assertRaises(OverflowError, self.theclass.fromtimestamp,
insane)

def test_fromtimestamp_large_year(self):
timestamp = 67_767_976_249_084_800 # Year 2147483648.
try:
year = _time.localtime(timestamp).tm_year
except (OverflowError, OSError):
self.skipTest('timestamp out of range on this platform')
with self.assertRaisesRegex(ValueError, rf'\b{year}\b'):
self.theclass.fromtimestamp(timestamp)

def test_fromtimestamp_with_none_arg(self):
# See gh-120268 for more details
with self.assertRaises(TypeError):
Expand Down
28 changes: 23 additions & 5 deletions Lib/test/test_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@

from test.support import skip_if_buggy_ucrt_strfptime, SuppressCrashReport

# Max year is only limited by the size of C int.
# Years are limited by the range of struct tm's int tm_year field.
SIZEOF_INT = sysconfig.get_config_var('SIZEOF_INT') or 4
TIME_MAXYEAR = (1 << 8 * SIZEOF_INT - 1) - 1
TIME_MINYEAR = -TIME_MAXYEAR - 1 + 1900
C_INT_MAX = (1 << 8 * SIZEOF_INT - 1) - 1
TIME_MAXYEAR = C_INT_MAX + 1900
TIME_MINYEAR = -C_INT_MAX - 1 + 1900

SEC_TO_US = 10 ** 6
US_TO_NS = 10 ** 3
Expand Down Expand Up @@ -153,6 +154,23 @@ def test_clock_settime(self):
self.assertRaises(OSError,
time.clock_settime, time.CLOCK_MONOTONIC, 0)

def test_large_timestamp_year(self):
# 2147483648-06-29 00:00:00 UTC, away from local year boundaries.
timestamp = 67_767_976_249_084_800
for convert in (time.gmtime, time.localtime, time.ctime):
with self.subTest(convert=convert):
try:
result = convert(timestamp)
except (OverflowError, OSError):
self.skipTest('timestamp out of range on this platform')
if isinstance(result, str):
year = int(result.rsplit(' ', 1)[-1])
else:
year = result.tm_year
self.assertEqual(year, 2**31)
if convert is time.localtime:
self.assertEqual(time.asctime(result), time.ctime(timestamp))

def test_conversions(self):
self.assertEqual(time.ctime(self.t),
time.asctime(time.localtime(self.t)))
Expand Down Expand Up @@ -398,7 +416,7 @@ def test_strptime_leap_year(self):
def test_asctime(self):
time.asctime(time.gmtime(self.t))

# Max year is only limited by the size of C int.
# The year offset must fit into a C int.
for bigyear in TIME_MAXYEAR, TIME_MINYEAR:
asc = time.asctime((bigyear, 6, 1) + (0,) * 6)
self.assertEqual(asc[-len(str(bigyear)):], str(bigyear))
Expand Down Expand Up @@ -769,7 +787,7 @@ def test_year(self, fmt=None, func=None):
def test_large_year(self):
self.assertEqual(self.yearstr(12345).lstrip('+'), '12345')
self.assertEqual(self.yearstr(123456789).lstrip('+'), '123456789')
self.assertEqual(self.yearstr(TIME_MAXYEAR).lstrip('+'), str(TIME_MAXYEAR))
self.assertEqual(self.yearstr(C_INT_MAX).lstrip('+'), str(C_INT_MAX))
self.assertRaises(OverflowError, self.yearstr, TIME_MAXYEAR + 1)

def test_negative(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fix :mod:`time` functions returning a negative year or failing to accept
their own output for large timestamps on platforms that support them. Avoid
reporting an incorrect year in errors raised by :mod:`datetime` for those
timestamps.
22 changes: 20 additions & 2 deletions Modules/_datetimemodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,18 @@ check_date_args(int year, int month, int day)
return 0;
}

static int
year_from_tm(const struct tm *tm, int *year)
{
if (tm->tm_year > INT_MAX - 1900) {
PyErr_Format(PyExc_ValueError, "year must be in %d..%d, not %lld",
MINYEAR, MAXYEAR, (long long)tm->tm_year + 1900);
return -1;
}
*year = tm->tm_year + 1900;
return 0;
}

/* Check that time arguments are in range. Return 0 if they are. If they
* aren't, raise ValueError and return -1.
*/
Expand Down Expand Up @@ -3304,14 +3316,18 @@ date_fromtimestamp(PyTypeObject *cls, PyObject *obj)
{
struct tm tm;
time_t t;
int year;

if (_PyTime_ObjectToTime_t(obj, &t, _PyTime_ROUND_FLOOR) == -1)
return NULL;

if (_PyTime_localtime(t, &tm) != 0)
return NULL;
if (year_from_tm(&tm, &year) < 0) {
return NULL;
}

return new_date_subclass_ex(tm.tm_year + 1900,
return new_date_subclass_ex(year,
tm.tm_mon + 1,
tm.tm_mday,
cls);
Expand Down Expand Up @@ -5647,7 +5663,9 @@ datetime_from_timet_and_us(PyTypeObject *cls, TM_FUNC f, time_t timet, int us,
if (f(timet, &tm) != 0)
return NULL;

year = tm.tm_year + 1900;
if (year_from_tm(&tm, &year) < 0) {
return NULL;
}
month = tm.tm_mon + 1;
day = tm.tm_mday;
hour = tm.tm_hour;
Expand Down
20 changes: 10 additions & 10 deletions Modules/timemodule.c

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.

Won't datetime have the same issue?

Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,7 @@ tmtotuple(time_module_state *state, struct tm *p
#define SET(INDEX, VAL) \
SET_ITEM((INDEX), PyLong_FromLong((long) (VAL)))

SET(0, p->tm_year + 1900);
SET_ITEM(0, PyLong_FromLongLong((long long)p->tm_year + 1900));

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.

This fixes the forward direction, but gettmarg() still reads the year into a C int:

else if (!gettmarg(state, tup, &buf,
"iiiiiiiii;strftime(): illegal time tuple argument") ||
!checktm(&buf))

We should preserve the round trip.

SET(1, p->tm_mon + 1); /* Want January == 1 */
SET(2, p->tm_mday);
SET(3, p->tm_hour);
Expand Down Expand Up @@ -629,7 +629,7 @@ static int
gettmarg(time_module_state *state, PyObject *args,
struct tm *p, const char *format)
{
int y;
long long y;

memset((void *) p, '\0', sizeof(struct tm));

Expand All @@ -645,12 +645,12 @@ gettmarg(time_module_state *state, PyObject *args,
&p->tm_wday, &p->tm_yday, &p->tm_isdst))
return 0;

if (y < INT_MIN + 1900) {
if (y < (long long)INT_MIN + 1900 || y > (long long)INT_MAX + 1900) {
PyErr_SetString(PyExc_OverflowError, "year out of range");
return 0;
}

p->tm_year = y - 1900;
p->tm_year = (int)(y - 1900);
p->tm_mon--;
p->tm_wday = (p->tm_wday + 1) % 7;
p->tm_yday--;
Expand Down Expand Up @@ -906,7 +906,7 @@ time_strftime(PyObject *module, PyObject *args)
return NULL;
}
else if (!gettmarg(state, tup, &buf,
"iiiiiiiii;strftime(): illegal time tuple argument") ||
"Liiiiiiii;strftime(): illegal time tuple argument") ||
!checktm(&buf))
{
return NULL;
Expand All @@ -918,7 +918,7 @@ time_strftime(PyObject *module, PyObject *args)
// physical devices (#123017).
#if defined(_MSC_VER) || (defined(__sun) && defined(__SVR4)) || defined(_AIX) \
|| defined(__VXWORKS__) || defined(__ANDROID__)
if (buf.tm_year + 1900 < 1 || 9999 < buf.tm_year + 1900) {
if (buf.tm_year < 1 - 1900 || buf.tm_year > 9999 - 1900) {
PyErr_SetString(PyExc_ValueError,
"strftime() requires year in [1; 9999]");
return NULL;
Expand Down Expand Up @@ -1041,12 +1041,12 @@ _asctime(struct tm *timeptr)
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
};
return PyUnicode_FromFormat(
"%s %s%3d %.2d:%.2d:%.2d %d",
"%s %s%3d %.2d:%.2d:%.2d %lld",
wday_name[timeptr->tm_wday],
mon_name[timeptr->tm_mon],
timeptr->tm_mday, timeptr->tm_hour,
timeptr->tm_min, timeptr->tm_sec,
1900 + timeptr->tm_year);
(long long)timeptr->tm_year + 1900);
}

/*[clinic input]
Expand Down Expand Up @@ -1074,7 +1074,7 @@ time_asctime_impl(PyObject *module, PyObject *tup)
return NULL;
}
else if (!gettmarg(state, tup, &buf,
"iiiiiiiii;asctime(): illegal time tuple argument") ||
"Liiiiiiii;asctime(): illegal time tuple argument") ||
!checktm(&buf))
{
return NULL;
Expand Down Expand Up @@ -1130,7 +1130,7 @@ time_mktime(PyObject *module, PyObject *tm_tuple)

time_module_state *state = get_time_state(module);
if (!gettmarg(state, tm_tuple, &tm,
"iiiiiiiii;mktime(): illegal time tuple argument"))
"Liiiiiiii;mktime(): illegal time tuple argument"))
{
return NULL;
}
Expand Down
Loading