gh-152204: Validate date fields in pure-Python date.fromisoformat - #152205
gh-152204: Validate date fields in pure-Python date.fromisoformat#152205tonghuaroot wants to merge 6 commits into
Conversation
The pure-Python _parse_isoformat_date read each fixed-width field with int() on a slice, which silently accepts a leading sign or whitespace, or a short slice that runs off the end of the string. Malformed basic-format inputs such as '2020+12' or '2020061' were therefore parsed into a wrong-but-plausible date instead of raising, while the C accelerator rejects them via parse_digits(). Validate that each field slice is exactly N ASCII digits before converting.
serhiy-storchaka
left a comment
There was a problem hiding this comment.
This change has significant impact on performance.
Would not it be simpler to use .isascii() at the beginning and .isdigit() for each fragment?
|
Done in 46f6881: one |
| '2020061', # 7 chars: day slice reads a 1-character tail | ||
| '2020123', # 7 chars: day slice reads a 1-character tail | ||
| '9999121', # 7 chars: day slice reads a 1-character tail |
There was a problem hiding this comment.
All of these cases are identical?
There was a problem hiding this comment.
They duplicates can be removed.
| year = int(dtstr[0:4]) | ||
| if not dtstr.isascii(): | ||
| raise ValueError(f"Invalid isoformat string: {dtstr!r}") | ||
| def _read(s, n): |
There was a problem hiding this comment.
Let's move this out of the hotpath to reduce overhead.
| if not dtstr.isascii(): | ||
| raise ValueError(f"Invalid isoformat string: {dtstr!r}") | ||
| def _read(s, n): | ||
| # dtstr is ASCII, so isdigit() matches only ASCII digits. |
There was a problem hiding this comment.
We already have a helper for this:
Lines 298 to 299 in 20e6c2f
There was a problem hiding this comment.
It was in the the initial variant. It is slower.
| raise ValueError("Invalid isoformat string") | ||
| year = int(dtstr[0:4]) | ||
| if not dtstr.isascii(): | ||
| raise ValueError(f"Invalid isoformat string: {dtstr!r}") |
There was a problem hiding this comment.
Keep it the same as the other message, and these won't surface anyway I think.
| return 8 | ||
|
|
||
|
|
||
| def _parse_isoformat_date(dtstr): |
There was a problem hiding this comment.
What about the time?
>>> import _pydatetime
>>> _pydatetime.datetime.fromisoformat('20201212T0102٣٤')
datetime.datetime(2020, 12, 12, 1, 2, 34)
| '2020123', # 7 chars: day slice reads a 1-character tail | ||
| '9999121', # 7 chars: day slice reads a 1-character tail | ||
| '2020-W2', # 1-digit week number | ||
| '٢025-03-09' # Unicode characters |
There was a problem hiding this comment.
Please add the missing comma.
| if not dtstr.isascii(): | ||
| raise ValueError(f"Invalid isoformat string: {dtstr!r}") | ||
| def _read(s, n): | ||
| # dtstr is ASCII, so isdigit() matches only ASCII digits. |
There was a problem hiding this comment.
It was in the the initial variant. It is slower.
|
Done in a025467: moved the reader to a module-level |
|
There are conflicts now. |
|
Done in aed8e2d: merged main (conflicts resolved) and removed the duplicate 7-char cases. |
_pydatetime._parse_isoformat_datereads each fixed-width field withint()on a slice, without checking that the slice is exactly N ASCII digits.int()accepts a leading+/-/whitespace and a short string, so several malformed ISO 8601 basic-format dates are silently parsed into a wrong-but-plausibledateinstead of raisingValueError:The C accelerator rejects all of these via
parse_digits()(which requires the exact field width and digit-only content), so this is a C-vs-pure-Python divergence. The pure-Python path is used when the_datetimeC extension is unavailable, and directly via_pydatetime.This validates each field slice (
year/month/day/weekno/weekday) to be exactly N ASCII digits before converting, mirroring the Cparse_digits(), and extendsdatetimetester'stest_fromisoformat_failswith the affected inputs (the new cases now reject on both implementations).Fixes #152204.
Prepared with AI assistance (Claude Code) and verified by hand against a debug build, against both the C and pure-Python implementations.
date.fromisoformatsilently mis-parses malformed basic-format dates #152204