This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git
The following commit(s) were added to refs/heads/main by this push:
new dad78c3243 fix(arrow-cast): bump atoi to 3.1.1 to fix Int16 parse
overflow (#11626)
dad78c3243 is described below
commit dad78c3243db92fefd57b8b4f3cf65901f874263
Author: Matt Butrovich <[email protected]>
AuthorDate: Sat Oct 17 06:58:30 2026 +0000
fix(arrow-cast): bump atoi to 3.1.1 to fix Int16 parse overflow (#11626)
# Which issue does this PR close?
- Closes #11591
# Rationale for this change
atoi 3.1.1 fixes pacman82/atoi-rs#54, which caused the bug in #11591.
This is a regression in 60.0.0. #10219 bumped atoi from 2.0.0 to 3.1.0,
and 59.3.1 still uses atoi 2.0.0. The integer parser in
`arrow-cast/src/parse.rs` is otherwise unchanged between 59.3.1 and
`main`, and the new tests pass on 59.3.1. This makes the fix a candidate
for a 60.x backport.
# What changes are included in this PR?
The minimum atoi version in `arrow-cast` is now 3.1.1.
# Are these changes tested?
Yes. `test_parse_integer_bounds` in `arrow-cast/src/parse.rs` parses
each integer type's bounds, one past each bound, and the all-nines
values on either side of each bound, with and without leading
whitespace. `test_cast_string_to_integral_overflow` in
`arrow-cast/src/cast/mod.rs` now includes `"-40000"` cast to `Int16`.
Both tests fail with atoi 3.1.0.
# Are there any significant user-facing changes?
No.
## AI Disclosure
I wrote this PR with help from Claude but understand and support the
core approach, implementation, and test coverage.
---
Cargo.lock | 4 ++--
arrow-cast/Cargo.toml | 2 +-
arrow-cast/src/cast/mod.rs | 10 ++++++++--
arrow-cast/src/parse.rs | 42 ++++++++++++++++++++++++++++++++++++++++++
4 files changed, 53 insertions(+), 5 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 90152321ad..045c6a552f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -609,9 +609,9 @@ checksum =
"f2d85fd3d291fabcc40c7232c92c280ec1754fd7b5d7ea769f143222143e179a"
[[package]]
name = "atoi"
-version = "3.1.0"
+version = "3.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e7a8bbe9949e43a1edaa043038c68703b04774156afdfb62ba2cef5bf93d67be"
+checksum = "d12c37257553c19e9aee6cba77814fdc9e07844abfc8b3a2c377cd920ee5a993"
dependencies = [
"num-traits",
]
diff --git a/arrow-cast/Cargo.toml b/arrow-cast/Cargo.toml
index 2d1364e525..25c1ae358d 100644
--- a/arrow-cast/Cargo.toml
+++ b/arrow-cast/Cargo.toml
@@ -48,7 +48,7 @@ chrono = { workspace = true }
half = { version = "2.1", default-features = false }
num-traits = { version = "0.2.19", default-features = false, features =
["std"] }
lexical-core = { version = "1.0", default-features = false, features =
["write-integers", "write-floats", "parse-integers", "parse-floats"] }
-atoi = "3.1.0"
+atoi = "3.1.1"
comfy-table = { version = "8", optional = true, default-features = false }
base64 = "0.23"
ryu = "1.0.16"
diff --git a/arrow-cast/src/cast/mod.rs b/arrow-cast/src/cast/mod.rs
index 94de6548f4..bfebdcdfe7 100644
--- a/arrow-cast/src/cast/mod.rs
+++ b/arrow-cast/src/cast/mod.rs
@@ -4728,6 +4728,7 @@ mod tests {
Some("123"),
Some("-123"),
Some("86374"),
+ Some("-40000"),
None,
])) as ArrayRef;
@@ -4736,8 +4737,13 @@ mod tests {
format_options: FormatOptions::default(),
};
let res = cast_with_options(&str, &DataType::Int16,
&options).expect("should cast to i16");
- let expected =
- Arc::new(Int16Array::from(vec![Some(123), Some(-123), None,
None])) as ArrayRef;
+ let expected = Arc::new(Int16Array::from(vec![
+ Some(123),
+ Some(-123),
+ None,
+ None,
+ None,
+ ])) as ArrayRef;
assert_eq!(&res, &expected);
}
diff --git a/arrow-cast/src/parse.rs b/arrow-cast/src/parse.rs
index 384eba2007..a571368d63 100644
--- a/arrow-cast/src/parse.rs
+++ b/arrow-cast/src/parse.rs
@@ -3631,6 +3631,48 @@ mod tests {
assert_eq!(Date32Type::parse(""), None);
}
+ #[test]
+ fn test_parse_integer_bounds() {
+ // The all-nines cases have as many digits as a bound but lie past it.
atoi 3.1.0
+ // parsed -99999 as i16 without an overflow check (#11591).
+ macro_rules! check {
+ ($t:ty) => {{
+ type Native = <$t as ArrowPrimitiveType>::Native;
+ let (min, max) = (i128::from(Native::MIN),
i128::from(Native::MAX));
+ let nines = 10_i128.pow(max.ilog10() + 1) - 1;
+ for v in [
+ min,
+ max,
+ min - 1,
+ max + 1,
+ -nines,
+ nines,
+ -nines - 1,
+ nines + 1,
+ ] {
+ let expected = Native::try_from(v).ok();
+ // The leading space takes the path that trims before
parsing
+ for s in [v.to_string(), format!(" {v}")] {
+ assert_eq!(
+ <$t>::parse(&s),
+ expected,
+ "{s:?} as {}",
+ <$t as ArrowPrimitiveType>::DATA_TYPE
+ );
+ }
+ }
+ }};
+ }
+ check!(Int8Type);
+ check!(Int16Type);
+ check!(Int32Type);
+ check!(Int64Type);
+ check!(UInt8Type);
+ check!(UInt16Type);
+ check!(UInt32Type);
+ check!(UInt64Type);
+ }
+
#[test]
fn test_parse_interval_month_day_nano_config() {
let interval = parse_interval_month_day_nano_config(