diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index df7d540cb362f75..a00ef2102f32450 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -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): diff --git a/Lib/test/test_time.py b/Lib/test/test_time.py index 985984b55123ce6..c4a9683992c08f5 100644 --- a/Lib/test/test_time.py +++ b/Lib/test/test_time.py @@ -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 @@ -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))) @@ -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)) @@ -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): diff --git a/Misc/NEWS.d/next/Library/2026-10-22-03-55-33.gh-issue-159739.ce26e1.rst b/Misc/NEWS.d/next/Library/2026-10-22-03-55-33.gh-issue-159739.ce26e1.rst new file mode 100644 index 000000000000000..94f97951af2712d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-22-03-55-33.gh-issue-159739.ce26e1.rst @@ -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. diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index cd2dd94c8164a53..24845e7fc8fe19e 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -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. */ @@ -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); @@ -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; diff --git a/Modules/timemodule.c b/Modules/timemodule.c index e5162fd3b678557..60cf6e38b8285b0 100644 --- a/Modules/timemodule.c +++ b/Modules/timemodule.c @@ -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)); SET(1, p->tm_mon + 1); /* Want January == 1 */ SET(2, p->tm_mday); SET(3, p->tm_hour); @@ -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)); @@ -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--; @@ -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; @@ -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; @@ -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] @@ -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; @@ -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; }