From 61c2c611ec7071c1ae19a8847877d0c0308b3fd2 Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Tue, 30 Jul 2024 12:20:25 +0800 Subject: [PATCH 01/11] gh-122272: guarantee specifiers %F and %C for datetime.strftime to be 0-padded --- Lib/_pydatetime.py | 13 +++++++++---- Lib/test/datetimetester.py | 14 ++++++++++---- Modules/_datetimemodule.c | 30 +++++++++++++++++++++++++----- 3 files changed, 44 insertions(+), 13 deletions(-) diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index 27cacb8e01ff3b..3af54d0252a793 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -272,14 +272,19 @@ def _wrap_strftime(object, format, timetuple): # strftime is going to have at this: escape % Zreplace = s.replace('%', '%%') newformat.append(Zreplace) - elif ch in 'YG' and object.year < 1000 and _need_normalize_century(): - # Note that datetime(1000, 1, 1).strftime('%G') == '1000' so - # year 1000 for %G can go on the fast path. + # Note that datetime(1000, 1, 1).strftime('%G') == '1000' so + # year 1000 for %G can go on the fast path. + elif ch in 'YGFC' and object.year < 1000 and _need_normalize_century(): if ch == 'G': year = int(_time.strftime("%G", timetuple)) else: year = object.year - push('{:04}'.format(year)) + if ch == 'C': + push('{:02}'.format(year // 100)) + else: + push('{:04}'.format(year)) + if ch == 'F': + push('-{:02}-{:02}'.format(*timetuple[1:3])) else: push('%') push(ch) diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index b756413d7bf375..79e64c6f754af9 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1710,13 +1710,19 @@ def test_strftime_y2k(self): (1000, 0), (1970, 0), ) - for year, offset in dataset: - for specifier in 'YG': + for year, g_offset in dataset: + for specifier in 'YGFC': with self.subTest(year=year, specifier=specifier): d = self.theclass(year, 1, 1) if specifier == 'G': - year += offset - self.assertEqual(d.strftime(f"%{specifier}"), f"{year:04d}") + year += g_offset + if specifier == 'C': + expected = f"{year // 100:02d}" + else: + expected = f"{year:04d}" + if specifier == 'F': + expected += f"-01-01" + self.assertEqual(d.strftime(f"%{specifier}"), expected) def test_replace(self): cls = self.theclass diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 67b49aa6ac2301..26faa17bbf0b96 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1852,8 +1852,10 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, Py_ssize_t ntoappend; /* # of bytes to append to output buffer */ #ifdef Py_NORMALIZE_CENTURY - /* Buffer of maximum size of formatted year permitted by long. */ - char buf[SIZEOF_LONG*5/2+2]; + /* Buffer of maximum size of formatted year permitted by long. + * Adding 6 just to accomodate %F with dashes, 2-digit month and day. + */ + char buf[SIZEOF_LONG*5/2+2+6]; #endif assert(object && format && timetuple); @@ -1950,7 +1952,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, ntoappend = PyBytes_GET_SIZE(freplacement); } #ifdef Py_NORMALIZE_CENTURY - else if (ch == 'Y' || ch == 'G') { + else if (ch == 'Y' || ch == 'G' || ch == 'F' || ch == 'C') { /* 0-pad year with century as necessary */ PyObject *item = PyTuple_GET_ITEM(timetuple, 0); long year_long = PyLong_AsLong(item); @@ -1980,8 +1982,26 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, goto Done; } } - - ntoappend = PyOS_snprintf(buf, sizeof(buf), "%04ld", year_long); + if (ch == 'F') { + item = PyTuple_GET_ITEM(timetuple, 1); + long month = PyLong_AsLong(item); + if (month == -1 && PyErr_Occurred()) { + goto Done; + } + item = PyTuple_GET_ITEM(timetuple, 2); + long day = PyLong_AsLong(item); + if (day == -1 && PyErr_Occurred()) { + goto Done; + } + ntoappend = PyOS_snprintf(buf, sizeof(buf), "%04ld-%02ld-%02ld", + year_long, month, day); + } + else { + ntoappend = PyOS_snprintf(buf, sizeof(buf), "%04ld", year_long); + if (ch == 'C') { + ntoappend -= 2; + } + } ptoappend = buf; } #endif From 8dc11004908b8dac26c1e55a693c15b15f4a06be Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Tue, 30 Jul 2024 12:24:03 +0800 Subject: [PATCH 02/11] removed trailing spaces --- Modules/_datetimemodule.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 26faa17bbf0b96..baab2a7bf6f8b7 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1987,7 +1987,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, long month = PyLong_AsLong(item); if (month == -1 && PyErr_Occurred()) { goto Done; - } + } item = PyTuple_GET_ITEM(timetuple, 2); long day = PyLong_AsLong(item); if (day == -1 && PyErr_Occurred()) { From f85cd69a9df92cc786b6aaf8823be209bc7767cb Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Tue, 30 Jul 2024 04:28:01 +0000 Subject: [PATCH 03/11] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2024-07-30-04-27-55.gh-issue-122272.6Wwa1V.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2024-07-30-04-27-55.gh-issue-122272.6Wwa1V.rst diff --git a/Misc/NEWS.d/next/Library/2024-07-30-04-27-55.gh-issue-122272.6Wwa1V.rst b/Misc/NEWS.d/next/Library/2024-07-30-04-27-55.gh-issue-122272.6Wwa1V.rst new file mode 100644 index 00000000000000..3279170c0f2bd6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-07-30-04-27-55.gh-issue-122272.6Wwa1V.rst @@ -0,0 +1,2 @@ +On some platforms such as Linux, year with century was not 0-padded when formatted by :meth:`datetime.datetime.strftime` with specifiers ``%C`` or ``%F``. The 0-padding behavior is now guaranteed. +Patch by Ben Hsing From 2fd709358c9da3a0f74c744ea951bbb5b0753f17 Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Tue, 30 Jul 2024 14:06:20 +0800 Subject: [PATCH 04/11] handle %C and %F only when the C library supports them (at least C99) --- Lib/_pydatetime.py | 14 +++++++++++++- Lib/test/datetimetester.py | 5 ++++- .../2024-07-30-04-27-55.gh-issue-122272.6Wwa1V.rst | 2 +- Modules/_datetimemodule.c | 11 ++++++++++- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index 3af54d0252a793..f15def01c35654 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -215,6 +215,17 @@ def _need_normalize_century(): _normalize_century = True return _normalize_century +_supports_c99 = None +def _can_support_c99(): + global _supports_c99 + if _supports_c99 is None: + try: + _supports_c99 = ( + _time.strftime("%F", (99, 1, 1, 0, 0, 0, 0, 1, 0)) != "%F") + except ValueError: + _supports_c99 = True + return _supports_c99 + # Correctly substitute for %z and %Z escapes in strftime formats. def _wrap_strftime(object, format, timetuple): # Don't call utcoffset() or tzname() unless actually needed. @@ -274,7 +285,8 @@ def _wrap_strftime(object, format, timetuple): newformat.append(Zreplace) # Note that datetime(1000, 1, 1).strftime('%G') == '1000' so # year 1000 for %G can go on the fast path. - elif ch in 'YGFC' and object.year < 1000 and _need_normalize_century(): + elif ((ch in 'YG' or ch in 'FC' and _can_support_c99()) and + object.year < 1000 and _need_normalize_century()): if ch == 'G': year = int(_time.strftime("%G", timetuple)) else: diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index 79e64c6f754af9..a6056dcf8d472c 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1710,8 +1710,11 @@ def test_strftime_y2k(self): (1000, 0), (1970, 0), ) + specifiers = 'YG' + if _time.strftime('%F', (99, 1, 1, 0, 0, 0, 0, 1, 0)) != '%F': + specifiers += 'FC' for year, g_offset in dataset: - for specifier in 'YGFC': + for specifier in specifiers: with self.subTest(year=year, specifier=specifier): d = self.theclass(year, 1, 1) if specifier == 'G': diff --git a/Misc/NEWS.d/next/Library/2024-07-30-04-27-55.gh-issue-122272.6Wwa1V.rst b/Misc/NEWS.d/next/Library/2024-07-30-04-27-55.gh-issue-122272.6Wwa1V.rst index 3279170c0f2bd6..943010b9c16c3c 100644 --- a/Misc/NEWS.d/next/Library/2024-07-30-04-27-55.gh-issue-122272.6Wwa1V.rst +++ b/Misc/NEWS.d/next/Library/2024-07-30-04-27-55.gh-issue-122272.6Wwa1V.rst @@ -1,2 +1,2 @@ -On some platforms such as Linux, year with century was not 0-padded when formatted by :meth:`datetime.datetime.strftime` with specifiers ``%C`` or ``%F``. The 0-padding behavior is now guaranteed. +On some platforms such as Linux, year with century was not 0-padded when formatted by :meth:`~.datetime.strftime` with C99-specific specifiers ``'%C'`` or ``'%F'``. The 0-padding behavior is now guaranteed when the format specifiers ``'%C'`` and ``'%F'`` are supported by the C library. Patch by Ben Hsing diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index baab2a7bf6f8b7..7818e1c178a5eb 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1952,7 +1952,12 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, ntoappend = PyBytes_GET_SIZE(freplacement); } #ifdef Py_NORMALIZE_CENTURY - else if (ch == 'Y' || ch == 'G' || ch == 'F' || ch == 'C') { + else if (ch == 'Y' || ch == 'G' +#if __STDC_VERSION__ >= 199901L +#define Py_STRFTIME_C99_SUPPORT + || ch == 'F' || ch == 'C' +#endif + ) { /* 0-pad year with century as necessary */ PyObject *item = PyTuple_GET_ITEM(timetuple, 0); long year_long = PyLong_AsLong(item); @@ -1982,6 +1987,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, goto Done; } } +#ifdef Py_STRFTIME_C99_SUPPORT if (ch == 'F') { item = PyTuple_GET_ITEM(timetuple, 1); long month = PyLong_AsLong(item); @@ -1997,11 +2003,14 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, year_long, month, day); } else { +#endif ntoappend = PyOS_snprintf(buf, sizeof(buf), "%04ld", year_long); +#ifdef Py_STRFTIME_C99_SUPPORT if (ch == 'C') { ntoappend -= 2; } } +#endif ptoappend = buf; } #endif From 8fe9521eb49d6cdd1d03c8aff7010062cdf35de8 Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Tue, 30 Jul 2024 15:10:28 +0800 Subject: [PATCH 05/11] use autoconf instead to test if c99-specific specifiers are supported --- Modules/_datetimemodule.c | 3 +-- configure | 52 +++++++++++++++++++++++++++++++++++++++ configure.ac | 28 +++++++++++++++++++++ 3 files changed, 81 insertions(+), 2 deletions(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 7818e1c178a5eb..651c1035bf9eed 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1953,8 +1953,7 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, } #ifdef Py_NORMALIZE_CENTURY else if (ch == 'Y' || ch == 'G' -#if __STDC_VERSION__ >= 199901L -#define Py_STRFTIME_C99_SUPPORT +#ifdef Py_STRFTIME_C99_SUPPORT || ch == 'F' || ch == 'C' #endif ) { diff --git a/configure b/configure index 39ab48fa4e2526..353008f72c3caf 100755 --- a/configure +++ b/configure @@ -26195,6 +26195,58 @@ printf "%s\n" "#define Py_NORMALIZE_CENTURY 1" >>confdefs.h fi +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether C99-specific strftime specifiers are supported" >&5 +printf %s "checking whether C99-specific strftime specifiers are supported... " >&6; } +if test ${ac_cv_strftime_c99_support+y} +then : + printf %s "(cached) " >&6 +else $as_nop + +if test "$cross_compiling" = yes +then : + ac_cv_strftime_c99_support=yes +else $as_nop + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +#include +#include + +int main(void) +{ + char full_date[11]; + struct tm date = { + .tm_year = -1801, + .tm_mon = 0, + .tm_mday = 1 + }; + if (strftime(full_date, sizeof(full_date), "%F", &date) && !strcmp(full_date, "%F")) { + return 1; + } + return 0; +} + +_ACEOF +if ac_fn_c_try_run "$LINENO" +then : + ac_cv_strftime_c99_support=yes +else $as_nop + ac_cv_strftime_c99_support=no +fi +rm -f core *.core core.conftest.* gmon.out bb.out conftest$ac_exeext \ + conftest.$ac_objext conftest.beam conftest.$ac_ext +fi + +fi +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_strftime_c99_support" >&5 +printf "%s\n" "$ac_cv_strftime_c99_support" >&6; } +if test "$ac_cv_strftime_c99_support" = yes +then + +printf "%s\n" "#define Py_STRFTIME_C99_SUPPORT 1" >>confdefs.h + +fi + have_curses=no have_panel=no diff --git a/configure.ac b/configure.ac index 62ed812991fc4e..20ffec00f6285d 100644 --- a/configure.ac +++ b/configure.ac @@ -6700,6 +6700,34 @@ then [Define if year with century should be normalized for strftime.]) fi +AC_CACHE_CHECK([whether C99-specific strftime specifiers are supported], [ac_cv_strftime_c99_support], [ +AC_RUN_IFELSE([AC_LANG_SOURCE([[ +#include +#include + +int main(void) +{ + char full_date[11]; + struct tm date = { + .tm_year = -1801, + .tm_mon = 0, + .tm_mday = 1 + }; + if (strftime(full_date, sizeof(full_date), "%F", &date) && !strcmp(full_date, "%F")) { + return 1; + } + return 0; +} +]])], +[ac_cv_strftime_c99_support=yes], +[ac_cv_strftime_c99_support=no], +[ac_cv_strftime_c99_support=yes])]) +if test "$ac_cv_strftime_c99_support" = yes +then + AC_DEFINE([Py_STRFTIME_C99_SUPPORT], [1], + [Define if C99-specific strftime specifiers are supported.]) +fi + dnl check for ncursesw/ncurses and panelw/panel dnl NOTE: old curses is not detected. dnl have_curses=[no, yes] From 4e1ea89a2bd542a42302a420d6794d79d7144c79 Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Tue, 30 Jul 2024 15:17:26 +0800 Subject: [PATCH 06/11] generate header --- pyconfig.h.in | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyconfig.h.in b/pyconfig.h.in index 8fbba7ed3b949e..d6b3cecbe19f82 100644 --- a/pyconfig.h.in +++ b/pyconfig.h.in @@ -1698,6 +1698,9 @@ /* Define if you want to enable internal statistics gathering. */ #undef Py_STATS +/* Define if C99-specific strftime specifiers are supported. */ +#undef Py_STRFTIME_C99_SUPPORT + /* The version of SunOS/Solaris as reported by `uname -r' without the dot. */ #undef Py_SUNOS_VERSION From bfd2f18648bf4137b8e72505efe0d0c9ad10ed7a Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Tue, 30 Jul 2024 13:43:54 +0000 Subject: [PATCH 07/11] corrected logics of checks for c99 support --- Lib/_pydatetime.py | 4 ++-- Lib/test/datetimetester.py | 2 +- configure | 10 +++++----- configure.ac | 10 +++++----- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index f15def01c35654..b609583944be57 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -221,9 +221,9 @@ def _can_support_c99(): if _supports_c99 is None: try: _supports_c99 = ( - _time.strftime("%F", (99, 1, 1, 0, 0, 0, 0, 1, 0)) != "%F") + _time.strftime("%F", (1900, 1, 1, 0, 0, 0, 0, 1, 0)) == "1900-01-01") except ValueError: - _supports_c99 = True + pass return _supports_c99 # Correctly substitute for %z and %Z escapes in strftime formats. diff --git a/Lib/test/datetimetester.py b/Lib/test/datetimetester.py index a6056dcf8d472c..e7adea0af5526a 100644 --- a/Lib/test/datetimetester.py +++ b/Lib/test/datetimetester.py @@ -1711,7 +1711,7 @@ def test_strftime_y2k(self): (1970, 0), ) specifiers = 'YG' - if _time.strftime('%F', (99, 1, 1, 0, 0, 0, 0, 1, 0)) != '%F': + if _time.strftime('%F', (1900, 1, 1, 0, 0, 0, 0, 1, 0)) == '1900-01-01': specifiers += 'FC' for year, g_offset in dataset: for specifier in specifiers: diff --git a/configure b/configure index 353008f72c3caf..f9da540dc09fb7 100755 --- a/configure +++ b/configure @@ -26204,7 +26204,7 @@ else $as_nop if test "$cross_compiling" = yes then : - ac_cv_strftime_c99_support=yes + ac_cv_strftime_c99_support=no else $as_nop cat confdefs.h - <<_ACEOF >conftest.$ac_ext /* end confdefs.h. */ @@ -26216,14 +26216,14 @@ int main(void) { char full_date[11]; struct tm date = { - .tm_year = -1801, + .tm_year = 0, .tm_mon = 0, .tm_mday = 1 }; - if (strftime(full_date, sizeof(full_date), "%F", &date) && !strcmp(full_date, "%F")) { - return 1; + if (strftime(full_date, sizeof(full_date), "%F", &date) && !strcmp(full_date, "1900-01-01")) { + return 0; } - return 0; + return 1; } _ACEOF diff --git a/configure.ac b/configure.ac index 20ffec00f6285d..0d3455dea9b0a5 100644 --- a/configure.ac +++ b/configure.ac @@ -6709,19 +6709,19 @@ int main(void) { char full_date[11]; struct tm date = { - .tm_year = -1801, + .tm_year = 0, .tm_mon = 0, .tm_mday = 1 }; - if (strftime(full_date, sizeof(full_date), "%F", &date) && !strcmp(full_date, "%F")) { - return 1; + if (strftime(full_date, sizeof(full_date), "%F", &date) && !strcmp(full_date, "1900-01-01")) { + return 0; } - return 0; + return 1; } ]])], [ac_cv_strftime_c99_support=yes], [ac_cv_strftime_c99_support=no], -[ac_cv_strftime_c99_support=yes])]) +[ac_cv_strftime_c99_support=no])]) if test "$ac_cv_strftime_c99_support" = yes then AC_DEFINE([Py_STRFTIME_C99_SUPPORT], [1], From fd8dd3099de4a4817bfabd8a88205e7e17aef172 Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Wed, 31 Jul 2024 09:03:41 +0800 Subject: [PATCH 08/11] allocate 6 more bytes for the formatted date buffer only when %F is supported --- Modules/_datetimemodule.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 651c1035bf9eed..25196e6d7dabe8 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1852,10 +1852,13 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, Py_ssize_t ntoappend; /* # of bytes to append to output buffer */ #ifdef Py_NORMALIZE_CENTURY - /* Buffer of maximum size of formatted year permitted by long. - * Adding 6 just to accomodate %F with dashes, 2-digit month and day. - */ - char buf[SIZEOF_LONG*5/2+2+6]; + /* Buffer of maximum size of formatted year permitted by long. */ + char buf[SIZEOF_LONG * 5 / 2 + 2 +#ifdef Py_STRFTIME_C99_SUPPORT + /* Need 6 more to accomodate dashes, 2-digit month and day for %F. */ + + 6 +#endif + ]; #endif assert(object && format && timetuple); From 3e36832fc40ccab7de986ed91b39d030474517c3 Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Fri, 16 Aug 2024 17:20:27 +0800 Subject: [PATCH 09/11] pass through month and day to time.strftime for %F; made _supports_c99 return False on error --- Lib/_pydatetime.py | 2 +- Modules/_datetimemodule.c | 30 +++++++++--------------------- 2 files changed, 10 insertions(+), 22 deletions(-) diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index b609583944be57..e1773cd52585d9 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -223,7 +223,7 @@ def _can_support_c99(): _supports_c99 = ( _time.strftime("%F", (1900, 1, 1, 0, 0, 0, 0, 1, 0)) == "1900-01-01") except ValueError: - pass + return False return _supports_c99 # Correctly substitute for %z and %Z escapes in strftime formats. diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 25196e6d7dabe8..77d51ca521375b 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1961,9 +1961,11 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, #endif ) { /* 0-pad year with century as necessary */ - PyObject *item = PyTuple_GET_ITEM(timetuple, 0); + PyObject *item = PyTuple_GetItem(timetuple, 0); + if (item == NULL) { + goto Done; + } long year_long = PyLong_AsLong(item); - if (year_long == -1 && PyErr_Occurred()) { goto Done; } @@ -1989,28 +1991,14 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, goto Done; } } + ntoappend = PyOS_snprintf(buf, sizeof(buf), #ifdef Py_STRFTIME_C99_SUPPORT - if (ch == 'F') { - item = PyTuple_GET_ITEM(timetuple, 1); - long month = PyLong_AsLong(item); - if (month == -1 && PyErr_Occurred()) { - goto Done; - } - item = PyTuple_GET_ITEM(timetuple, 2); - long day = PyLong_AsLong(item); - if (day == -1 && PyErr_Occurred()) { - goto Done; - } - ntoappend = PyOS_snprintf(buf, sizeof(buf), "%04ld-%02ld-%02ld", - year_long, month, day); - } - else { + ch == 'F' ? "%04ld-%%m-%%d" : #endif - ntoappend = PyOS_snprintf(buf, sizeof(buf), "%04ld", year_long); + "%04ld", year_long); #ifdef Py_STRFTIME_C99_SUPPORT - if (ch == 'C') { - ntoappend -= 2; - } + if (ch == 'C') { + ntoappend -= 2; } #endif ptoappend = buf; From 3530288d42ceffc9222b9ae3983884dd7261a6bb Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Thu, 22 Aug 2024 16:46:12 +0800 Subject: [PATCH 10/11] validate timetuple as a tuple; cache result of _can_support_c99 upon error --- Lib/_pydatetime.py | 2 +- Modules/_datetimemodule.c | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Lib/_pydatetime.py b/Lib/_pydatetime.py index e1773cd52585d9..78432d46506be8 100644 --- a/Lib/_pydatetime.py +++ b/Lib/_pydatetime.py @@ -223,7 +223,7 @@ def _can_support_c99(): _supports_c99 = ( _time.strftime("%F", (1900, 1, 1, 0, 0, 0, 0, 1, 0)) == "1900-01-01") except ValueError: - return False + _supports_c99 = False return _supports_c99 # Correctly substitute for %z and %Z escapes in strftime formats. diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 77d51ca521375b..6259a051a41843 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1961,6 +1961,9 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, #endif ) { /* 0-pad year with century as necessary */ + if (!PyTuple_Check(timetuple)) { + goto Done; + } PyObject *item = PyTuple_GetItem(timetuple, 0); if (item == NULL) { goto Done; From c9c51d766f6ad37ea7ac98868fb4509b9dd469ee Mon Sep 17 00:00:00 2001 From: Ben Hsing Date: Fri, 23 Aug 2024 17:33:44 +0800 Subject: [PATCH 11/11] use PySequence_GetItem instead of PyTuple_GetItem for better error handling --- Modules/_datetimemodule.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Modules/_datetimemodule.c b/Modules/_datetimemodule.c index 6259a051a41843..79314e06c82f69 100644 --- a/Modules/_datetimemodule.c +++ b/Modules/_datetimemodule.c @@ -1961,14 +1961,12 @@ wrap_strftime(PyObject *object, PyObject *format, PyObject *timetuple, #endif ) { /* 0-pad year with century as necessary */ - if (!PyTuple_Check(timetuple)) { - goto Done; - } - PyObject *item = PyTuple_GetItem(timetuple, 0); + PyObject *item = PySequence_GetItem(timetuple, 0); if (item == NULL) { goto Done; } long year_long = PyLong_AsLong(item); + Py_DECREF(item); if (year_long == -1 && PyErr_Occurred()) { goto Done; }