This is an automated email from the ASF dual-hosted git repository.
Jefffrey 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 98ad0f31b0 feat(arrow-buffer): add `OverflowError` and fallible offset
constructors (#10736)
98ad0f31b0 is described below
commit 98ad0f31b0f03a9d08e5208b2b94d918afa8f6c2
Author: Emil Ernerfeldt <[email protected]>
AuthorDate: Wed Sep 2 04:51:41 2026 +0200
feat(arrow-buffer): add `OverflowError` and fallible offset constructors
(#10736)
# Which issue does this PR close?
* Part of https://github.com/apache/arrow-rs/issues/10553
* Follows the `MutableBufferError` precedent from
https://github.com/apache/arrow-rs/pull/10317
# Rationale for this change
`OffsetBuffer::<i32>::from_lengths` panics once the lengths add up to
more than 2 GiB. The other overflow panics in `arrow-buffer` are the
same story.
# What changes are included in this PR?
A small `OverflowError` and a `try_` variant for functions that panics
on overflow:
* `OffsetBuffer::{try_new_zeroed, try_from_lengths,
try_from_repeated_length}`
* `OffsetBufferBuilder::{try_push_length, try_finish,
try_finish_cloned}`
* `NullBuffer::try_expand`
The panicking versions delegate to the fallible ones and re-panic with
the error's `Display`, so their panic messages are unchanged.
`try_push_length` leaves the builder unchanged when it fails.
# Are these changes tested?
Yes, new tests for the error paths. The existing `should_panic` tests
are untouched and still pass, which is what pins the panic messages.
# Are there any user-facing changes?
New public API only, no breaking changes.
---------
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
arrow-array/src/array/byte_array.rs | 4 +-
arrow-buffer/src/buffer/null.rs | 26 +++++--
arrow-buffer/src/buffer/offset.rs | 142 +++++++++++++++++++++++++++++++-----
arrow-buffer/src/builder/offset.rs | 76 ++++++++++++++++---
arrow-buffer/src/error.rs | 89 ++++++++++++++++++++++
arrow-buffer/src/lib.rs | 3 +
6 files changed, 306 insertions(+), 34 deletions(-)
diff --git a/arrow-array/src/array/byte_array.rs
b/arrow-array/src/array/byte_array.rs
index bf191c325b..b3f2025322 100644
--- a/arrow-array/src/array/byte_array.rs
+++ b/arrow-array/src/array/byte_array.rs
@@ -734,13 +734,13 @@ mod tests {
}
#[test]
- #[should_panic(expected = "usize overflow")]
+ #[should_panic(expected = "total length overflow: does not fit in usize")]
fn create_repeated_usize_overflow_1() {
let _arr = BinaryArray::new_repeated(b"hello", (usize::MAX /
"hello".len()) + 1);
}
#[test]
- #[should_panic(expected = "usize overflow")]
+ #[should_panic(expected = "total length overflow: does not fit in usize")]
fn create_repeated_usize_overflow_2() {
let _arr = BinaryArray::new_repeated(b"hello", usize::MAX);
}
diff --git a/arrow-buffer/src/buffer/null.rs b/arrow-buffer/src/buffer/null.rs
index 3303d8c350..003585d91b 100644
--- a/arrow-buffer/src/buffer/null.rs
+++ b/arrow-buffer/src/buffer/null.rs
@@ -17,7 +17,7 @@
use crate::bit_iterator::{BitIndexIterator, BitIterator, BitSliceIterator};
use crate::buffer::BooleanBuffer;
-use crate::{Buffer, MutableBuffer};
+use crate::{Buffer, MutableBuffer, OverflowError};
/// A [`BooleanBuffer`] used to encode validity (null values) for Arrow arrays
///
@@ -121,9 +121,25 @@ impl NullBuffer {
///
/// # Panics
///
- /// Panics if `self.len() * count` overflows `usize`
+ /// Panics if `self.len() * count` overflows `usize`.
+ /// Use [`Self::try_expand`] for a fallible version.
pub fn expand(&self, count: usize) -> Self {
- let capacity = self.buffer.len().checked_mul(count).unwrap();
+ self.try_expand(count).unwrap_or_else(|err| panic!("{err}"))
+ }
+
+ /// Returns a new [`NullBuffer`] where each bit in the current null buffer
+ /// is repeated `count` times. This is useful for masking the nulls of
+ /// the child of a FixedSizeListArray based on its parent
+ ///
+ /// # Errors
+ ///
+ /// Errors if `self.len() * count` overflows `usize`
+ pub fn try_expand(&self, count: usize) -> Result<Self, OverflowError> {
+ let capacity = self
+ .buffer
+ .len()
+ .checked_mul(count)
+ .ok_or_else(|| OverflowError::new::<usize>("buffer length"))?;
let mut buffer = MutableBuffer::new_null(capacity);
// Expand each bit within `null_mask` into `element_len`
@@ -136,10 +152,10 @@ impl NullBuffer {
crate::bit_util::set_bit(buffer.as_mut(), i * count + j)
}
}
- Self {
+ Ok(Self {
buffer: BooleanBuffer::new(buffer.into(), 0, capacity),
null_count: self.null_count * count,
- }
+ })
}
/// Returns the length of this [`NullBuffer`] in bits
diff --git a/arrow-buffer/src/buffer/offset.rs
b/arrow-buffer/src/buffer/offset.rs
index 12823c9eb5..f2a538d20d 100644
--- a/arrow-buffer/src/buffer/offset.rs
+++ b/arrow-buffer/src/buffer/offset.rs
@@ -16,7 +16,7 @@
// under the License.
use crate::buffer::ScalarBuffer;
-use crate::{ArrowNativeType, MutableBuffer, NullBuffer, OffsetBufferBuilder};
+use crate::{ArrowNativeType, MutableBuffer, NullBuffer, OffsetBufferBuilder,
OverflowError};
use std::ops::Deref;
/// A non-empty buffer of monotonically increasing, positive integers.
@@ -98,14 +98,24 @@ impl<O: ArrowNativeType> OffsetBuffer<O> {
///
/// # Panics
///
- /// Panics if `(len + 1) * size_of::<O>()` overflows `usize`
+ /// Panics if `(len + 1) * size_of::<O>()` overflows `usize`.
+ /// Use [`Self::try_new_zeroed`] for a fallible version.
pub fn new_zeroed(len: usize) -> Self {
+ Self::try_new_zeroed(len).unwrap_or_else(|err| panic!("{err}"))
+ }
+
+ /// Create a new [`OffsetBuffer`] containing `len + 1` `0` values
+ ///
+ /// # Errors
+ ///
+ /// Errors if `(len + 1) * size_of::<O>()` overflows `usize`
+ pub fn try_new_zeroed(len: usize) -> Result<Self, OverflowError> {
let len_bytes = len
.checked_add(1)
.and_then(|o| o.checked_mul(std::mem::size_of::<O>()))
- .expect("overflow");
+ .ok_or_else(|| OverflowError::new::<usize>("buffer length"))?;
let buffer = MutableBuffer::from_len_zeroed(len_bytes);
- Self(buffer.into_buffer().into())
+ Ok(Self(buffer.into_buffer().into()))
}
/// Create a new [`OffsetBuffer`] from the iterator of slice lengths
@@ -121,8 +131,27 @@ impl<O: ArrowNativeType> OffsetBuffer<O> {
///
/// # Panics
///
- /// Panics on overflow
+ /// Panics on overflow. Use [`Self::try_from_lengths`] for a fallible
version.
pub fn from_lengths<I>(lengths: I) -> Self
+ where
+ I: IntoIterator<Item = usize>,
+ {
+ Self::try_from_lengths(lengths).unwrap_or_else(|err| panic!("{err}"))
+ }
+
+ /// Create a new [`OffsetBuffer`] from the iterator of slice lengths
+ ///
+ /// ```
+ /// # use arrow_buffer::OffsetBuffer;
+ /// let offsets = OffsetBuffer::<i32>::try_from_lengths([1, 3,
5]).unwrap();
+ /// assert_eq!(offsets.as_ref(), &[0, 1, 4, 9]);
+ /// ```
+ ///
+ /// # Errors
+ ///
+ /// Errors if the total length overflows `usize` or `O`, e.g. if the
lengths
+ /// add up to more than `i32::MAX` for a `OffsetBuffer<i32>`.
+ pub fn try_from_lengths<I>(lengths: I) -> Result<Self, OverflowError>
where
I: IntoIterator<Item = usize>,
{
@@ -132,12 +161,14 @@ impl<O: ArrowNativeType> OffsetBuffer<O> {
let mut acc = 0_usize;
for length in iter {
- acc = acc.checked_add(length).expect("usize overflow");
+ acc = acc
+ .checked_add(length)
+ .ok_or_else(|| OverflowError::new::<usize>("total length"))?;
out.push(O::usize_as(acc))
}
// Check for overflow
- O::from_usize(acc).expect("offset overflow");
- Self(out.into())
+ O::from_usize(acc).ok_or_else(||
OverflowError::new::<O>("offset").with_value(acc))?;
+ Ok(Self(out.into()))
}
/// Create a new [`OffsetBuffer`] where each slice has the same length
@@ -153,28 +184,45 @@ impl<O: ArrowNativeType> OffsetBuffer<O> {
///
/// # Panics
///
- /// Panics on overflow
+ /// Panics on overflow. Use [`Self::try_from_repeated_length`] for a
fallible version.
pub fn from_repeated_length(length: usize, n: usize) -> Self {
+ Self::try_from_repeated_length(length, n).unwrap_or_else(|err|
panic!("{err}"))
+ }
+
+ /// Create a new [`OffsetBuffer`] where each slice has the same length
+ /// `length`, repeated `n` times.
+ ///
+ /// ```
+ /// # use arrow_buffer::OffsetBuffer;
+ /// let offsets = OffsetBuffer::<i32>::try_from_repeated_length(4,
3).unwrap();
+ /// assert_eq!(offsets.as_ref(), &[0, 4, 8, 12]);
+ /// ```
+ ///
+ /// # Errors
+ ///
+ /// Errors if `length * n` overflows `usize` or `O`.
+ pub fn try_from_repeated_length(length: usize, n: usize) -> Result<Self,
OverflowError> {
if n == 0 {
- return Self::new_empty();
+ return Ok(Self::new_empty());
}
if length == 0 {
- return Self::new_zeroed(n);
+ return Self::try_new_zeroed(n);
}
- // Check for overflow
// Making sure we don't overflow usize or O when calculating the total
length
- length.checked_mul(n).expect("usize overflow");
+ let total_length = length
+ .checked_mul(n)
+ .ok_or_else(|| OverflowError::new::<usize>("total length"))?;
- // Check for overflow
- O::from_usize(length * n).expect("offset overflow");
+ O::from_usize(total_length)
+ .ok_or_else(||
OverflowError::new::<O>("offset").with_value(total_length))?;
let offsets = (0..=n)
.map(|index| O::usize_as(index * length))
.collect::<Vec<O>>();
- Self(ScalarBuffer::from(offsets))
+ Ok(Self(ScalarBuffer::from(offsets)))
}
/// The first offset, i.e. the start of the first range.
@@ -481,6 +529,64 @@ mod tests {
OffsetBuffer::new(vec![-1, 0, 1].into());
}
+ #[test]
+ fn try_from_lengths_overflow() {
+ // Fits in an i64, but not in an i32:
+ let lengths = [u32::MAX as usize, 1];
+
+ let err = OffsetBuffer::<i32>::try_from_lengths(lengths).unwrap_err();
+ assert_eq!(
+ err.to_string(),
+ "offset overflow: 4294967296 does not fit in i32"
+ );
+ assert!(OffsetBuffer::<i64>::try_from_lengths(lengths).is_ok());
+
+ let err = OffsetBuffer::<i32>::try_from_lengths([usize::MAX,
1]).unwrap_err();
+ assert_eq!(
+ err.to_string(),
+ "total length overflow: does not fit in usize"
+ );
+
+ // The panicking version agrees:
+ assert!(std::panic::catch_unwind(||
OffsetBuffer::<i32>::from_lengths(lengths)).is_err());
+ }
+
+ #[test]
+ fn try_from_repeated_length_overflow() {
+ assert_eq!(
+ OffsetBuffer::<i32>::try_from_repeated_length(2, usize::MAX)
+ .unwrap_err()
+ .to_string(),
+ "total length overflow: does not fit in usize"
+ );
+ assert_eq!(
+ OffsetBuffer::<i32>::try_from_repeated_length(u32::MAX as usize, 2)
+ .unwrap_err()
+ .to_string(),
+ "offset overflow: 8589934590 does not fit in i32"
+ );
+ assert_eq!(
+ OffsetBuffer::<i32>::try_from_repeated_length(4, 3)
+ .unwrap()
+ .as_ref(),
+ &[0, 4, 8, 12]
+ );
+ }
+
+ #[test]
+ fn try_new_zeroed_overflow() {
+ assert_eq!(
+ OffsetBuffer::<i64>::try_new_zeroed(usize::MAX)
+ .unwrap_err()
+ .to_string(),
+ "buffer length overflow: does not fit in usize"
+ );
+ assert_eq!(
+ OffsetBuffer::<i32>::try_new_zeroed(3).unwrap().as_ref(),
+ &[0; 4]
+ );
+ }
+
#[test]
fn offsets() {
OffsetBuffer::new(vec![0, 1, 2, 3].into());
@@ -521,7 +627,7 @@ mod tests {
}
#[test]
- #[should_panic(expected = "usize overflow")]
+ #[should_panic(expected = "total length overflow: does not fit in usize")]
fn from_lengths_usize_overflow() {
OffsetBuffer::<i32>::from_lengths([usize::MAX, 1]);
}
@@ -545,7 +651,7 @@ mod tests {
}
#[test]
- #[should_panic(expected = "usize overflow")]
+ #[should_panic(expected = "total length overflow: does not fit in usize")]
fn from_repeated_lengths_usize_length_usize_overflow() {
OffsetBuffer::<i32>::from_repeated_length(usize::MAX, 2);
}
diff --git a/arrow-buffer/src/builder/offset.rs
b/arrow-buffer/src/builder/offset.rs
index a51ca5f01d..ee5ba02ed9 100644
--- a/arrow-buffer/src/builder/offset.rs
+++ b/arrow-buffer/src/builder/offset.rs
@@ -17,7 +17,7 @@
use std::ops::Deref;
-use crate::{ArrowNativeType, OffsetBuffer};
+use crate::{ArrowNativeType, OffsetBuffer, OverflowError};
/// Builder of [`OffsetBuffer`]
#[derive(Debug)]
@@ -41,11 +41,27 @@ impl<O: ArrowNativeType> OffsetBufferBuilder<O> {
///
/// # Panics
///
- /// Panics if adding `length` would overflow `usize`
+ /// Panics if adding `length` would overflow `usize`.
+ /// Use [`Self::try_push_length`] for a fallible version.
#[inline]
pub fn push_length(&mut self, length: usize) {
- self.last_offset =
self.last_offset.checked_add(length).expect("overflow");
- self.offsets.push(O::usize_as(self.last_offset))
+ self.try_push_length(length)
+ .unwrap_or_else(|err| panic!("{err}"))
+ }
+
+ /// Push a slice of `length` bytes
+ ///
+ /// # Errors
+ ///
+ /// Errors if adding `length` would overflow `usize`. The builder is left
unchanged.
+ #[inline]
+ pub fn try_push_length(&mut self, length: usize) -> Result<(),
OverflowError> {
+ self.last_offset = self
+ .last_offset
+ .checked_add(length)
+ .ok_or_else(|| OverflowError::new::<usize>("total length"))?;
+ self.offsets.push(O::usize_as(self.last_offset));
+ Ok(())
}
/// Reserve space for at least `additional` further offsets
@@ -58,23 +74,44 @@ impl<O: ArrowNativeType> OffsetBufferBuilder<O> {
///
/// # Panics
///
- /// Panics if offsets overflow `O`
+ /// Panics if offsets overflow `O`. Use [`Self::try_finish`] for a
fallible version.
pub fn finish(self) -> OffsetBuffer<O> {
- O::from_usize(self.last_offset).expect("overflow");
- unsafe { OffsetBuffer::new_unchecked(self.offsets.into()) }
+ self.try_finish().unwrap_or_else(|err| panic!("{err}"))
+ }
+
+ /// Takes the builder itself and returns an [`OffsetBuffer`]
+ ///
+ /// # Errors
+ ///
+ /// Errors if offsets overflow `O`, e.g. if they add up to more than
`i32::MAX`
+ /// for a `OffsetBufferBuilder<i32>`.
+ pub fn try_finish(self) -> Result<OffsetBuffer<O>, OverflowError> {
+ O::from_usize(self.last_offset)
+ .ok_or_else(||
OverflowError::new::<O>("offset").with_value(self.last_offset))?;
+ Ok(unsafe { OffsetBuffer::new_unchecked(self.offsets.into()) })
}
/// Builds the [OffsetBuffer] without resetting the builder.
///
/// # Panics
///
- /// Panics if offsets overflow `O`
+ /// Panics if offsets overflow `O`. Use [`Self::try_finish_cloned`] for a
fallible version.
pub fn finish_cloned(&self) -> OffsetBuffer<O> {
+ self.try_finish_cloned()
+ .unwrap_or_else(|err| panic!("{err}"))
+ }
+
+ /// Builds the [OffsetBuffer] without resetting the builder.
+ ///
+ /// # Errors
+ ///
+ /// Errors for the same reasons as [`Self::try_finish`].
+ pub fn try_finish_cloned(&self) -> Result<OffsetBuffer<O>, OverflowError> {
let cloned = Self {
offsets: self.offsets.clone(),
last_offset: self.last_offset,
};
- cloned.finish()
+ cloned.try_finish()
}
}
@@ -88,6 +125,27 @@ impl<O: ArrowNativeType> Deref for OffsetBufferBuilder<O> {
#[cfg(test)]
mod tests {
+
+ #[test]
+ fn try_finish_overflow() {
+ let mut builder = OffsetBufferBuilder::<i32>::new(2);
+ builder.try_push_length(u32::MAX as usize).unwrap();
+ let expected = "offset overflow: 4294967295 does not fit in i32";
+ assert_eq!(
+ builder.try_finish_cloned().unwrap_err().to_string(),
+ expected
+ );
+ assert_eq!(builder.try_finish().unwrap_err().to_string(), expected);
+
+ let mut builder = OffsetBufferBuilder::<i32>::new(2);
+ builder.try_push_length(usize::MAX).unwrap();
+ // The builder is unchanged by a failed push:
+ assert_eq!(
+ builder.try_push_length(1).unwrap_err().to_string(),
+ "total length overflow: does not fit in usize"
+ );
+ assert_eq!(builder.len(), 2);
+ }
use crate::OffsetBufferBuilder;
#[test]
diff --git a/arrow-buffer/src/error.rs b/arrow-buffer/src/error.rs
new file mode 100644
index 0000000000..ad769848f5
--- /dev/null
+++ b/arrow-buffer/src/error.rs
@@ -0,0 +1,89 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+//! Errors returned by `arrow-buffer`.
+
+/// An arithmetic overflow.
+///
+/// Returned by the `try_` alternatives to the functions that panic on
overflow,
+/// for instance
[`OffsetBuffer::try_from_lengths`](crate::OffsetBuffer::try_from_lengths).
+///
+/// ```
+/// # use arrow_buffer::OffsetBuffer;
+/// // 32 bit offsets cannot describe more than 2 GiB of data:
+/// let err = OffsetBuffer::<i32>::try_from_lengths([u32::MAX as
usize]).unwrap_err();
+/// assert_eq!(err.to_string(), "offset overflow: 4294967295 does not fit in
i32");
+///
+/// // 64 bit offsets can:
+/// assert!(OffsetBuffer::<i64>::try_from_lengths([u32::MAX as
usize]).is_ok());
+/// ```
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct OverflowError {
+ what: &'static str,
+ value: Option<usize>,
+ type_name: &'static str,
+}
+
+impl OverflowError {
+ /// `what` names what overflowed, for instance `"offset"`,
+ /// and `T` is the type it did not fit in, for instance `i32`.
+ pub fn new<T>(what: &'static str) -> Self {
+ Self {
+ what,
+ value: None,
+ type_name: std::any::type_name::<T>(),
+ }
+ }
+
+ /// The value that did not fit.
+ pub const fn with_value(mut self, value: usize) -> Self {
+ self.value = Some(value);
+ self
+ }
+
+ /// What overflowed, for instance `"offset"`.
+ pub const fn what(&self) -> &'static str {
+ self.what
+ }
+
+ /// The value that did not fit, if known.
+ pub const fn value(&self) -> Option<usize> {
+ self.value
+ }
+
+ /// The name of the type that the value did not fit in, for instance
`"i32"`.
+ pub const fn type_name(&self) -> &'static str {
+ self.type_name
+ }
+}
+
+impl std::fmt::Display for OverflowError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let Self {
+ what,
+ value,
+ type_name,
+ } = self;
+ write!(f, "{what} overflow: ")?;
+ match value {
+ Some(value) => write!(f, "{value} does not fit in {type_name}"),
+ None => write!(f, "does not fit in {type_name}"),
+ }
+ }
+}
+
+impl std::error::Error for OverflowError {}
diff --git a/arrow-buffer/src/lib.rs b/arrow-buffer/src/lib.rs
index 230747b8b8..3e4fc828f6 100644
--- a/arrow-buffer/src/lib.rs
+++ b/arrow-buffer/src/lib.rs
@@ -46,6 +46,9 @@ pub use buffer::*;
pub mod builder;
pub use builder::*;
+mod error;
+pub use error::OverflowError;
+
mod bigint;
pub use bigint::i256;