This is an automated email from the ASF dual-hosted git repository.
kou pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow.git
The following commit(s) were added to refs/heads/main by this push:
new b9479757dd5 GH-54646: [C++] Reject years out of range in the strptime
timestamp parser (#54658)
b9479757dd5 is described below
commit b9479757dd5c1914c2b007f11f6679c4fc080c05
Author: Pearu Peterson <[email protected]>
AuthorDate: Thu Oct 22 17:22:24 2026 +0300
GH-54646: [C++] Reject years out of range in the strptime timestamp parser
(#54658)
### Rationale for this change
The parser computed the year as `tm_year + 1900`, which can overflow `int`
(e.g. with `%s`), and passed it to `date::year`, which holds 16 bits, so a year
out of range gave a wrong timestamp.
### What changes are included in this PR?
The year is computed in 64 bits and rejected if `date::year` cannot hold it.
### Are these changes tested?
Yes, `TimestampParser.StrptimeYearOutOfRange` covers a year that wraps
around and one where `tm_year + 1900` overflows `int`. It needs `%s` and is
skipped where `strptime` does not parse `%s` into 64 bits (e.g. on Windows).
### Are there any user-facing changes?
Values whose year does not fit in 16 bits no longer parse with `strptime`
formats.
### AI usage disclosure
Developed with AI assistance (Claude Code): the fix, the test and this
description were AI-generated under my direction and reviewed line by line by
me. I own and can debug these changes. No copyrighted material is included.
* GitHub Issue: #54646
Authored-by: Pearu Peterson <[email protected]>
Signed-off-by: Sutou Kouhei <[email protected]>
---
cpp/src/arrow/util/value_parsing.h | 9 ++++++++-
cpp/src/arrow/util/value_parsing_test.cc | 19 +++++++++++++++++++
2 files changed, 27 insertions(+), 1 deletion(-)
diff --git a/cpp/src/arrow/util/value_parsing.h
b/cpp/src/arrow/util/value_parsing.h
index 1b300074851..1c2102fe370 100644
--- a/cpp/src/arrow/util/value_parsing.h
+++ b/cpp/src/arrow/util/value_parsing.h
@@ -829,11 +829,18 @@ static inline bool ParseTimestampStrptime(const char*
buf, size_t length,
if (!allow_trailing_chars && static_cast<size_t>(ret - clean_copy.c_str())
!= length) {
return false;
}
+ // tm_year + 1900 can overflow int (e.g. with %s), and date::year only
+ // holds 16 bits
+ const int64_t year = static_cast<int64_t>(result.tm_year) + 1900;
+ if (year < static_cast<int>(arrow_vendored::date::year::min()) ||
+ year > static_cast<int>(arrow_vendored::date::year::max())) {
+ return false;
+ }
// Reject impossible dates such as February 30 or a zero day, which strptime
// may accept (the day of month is checked against 31 at most), instead of
// rolling them over into the next month; consistent with the ISO-8601
parser.
const arrow_vendored::date::year_month_day ymd =
- arrow_vendored::date::year(result.tm_year + 1900) / (result.tm_mon + 1) /
+ arrow_vendored::date::year(static_cast<int>(year)) / (result.tm_mon + 1)
/
result.tm_mday;
if (!ymd.ok()) {
return false;
diff --git a/cpp/src/arrow/util/value_parsing_test.cc
b/cpp/src/arrow/util/value_parsing_test.cc
index 69baba169ff..875418f3195 100644
--- a/cpp/src/arrow/util/value_parsing_test.cc
+++ b/cpp/src/arrow/util/value_parsing_test.cc
@@ -928,6 +928,25 @@ TEST(TimestampParser, StrptimeWithoutDay) {
ASSERT_EQ(expected, converted);
}
+TEST(TimestampParser, StrptimeYearOutOfRange) {
+ auto parser = TimestampParser::MakeStrptime("%s");
+ int64_t converted = 0;
+ // A value past 2038 (2100-01-01), as a strptime that parses %s into a 32-bit
+ // time_t wraps larger values into ordinary dates
+ if (!(*parser)("4102444800", 10, TimeUnit::SECOND, &converted) ||
+ converted != 4102444800) {
+ GTEST_SKIP() << "strptime does not support 64-bit %s values on this
platform";
+ }
+
+ // Years that do not fit in a date::year are rejected rather than wrapped,
+ // also where tm_year + 1900 would overflow int
+ for (const std::string value :
+ {"1000000000000000", "67768036000000000", "67768036191676799"}) {
+ SCOPED_TRACE(value);
+ ASSERT_FALSE((*parser)(value.c_str(), value.size(), TimeUnit::SECOND,
&converted));
+ }
+}
+
TEST(TimestampParser, StrptimeZoneOffset) {
if (!kStrptimeSupportsZone) {
GTEST_SKIP() << "strptime does not support %z on this platform";