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 cbd0e02f96 Return errors instead of panicking in fallible functions 
(#10755)
cbd0e02f96 is described below

commit cbd0e02f9653c17836ec58bdc889947e4d550984
Author: Emil Ernerfeldt <[email protected]>
AuthorDate: Tue Sep 1 08:55:14 2026 +0200

    Return errors instead of panicking in fallible functions (#10755)
    
    # Which issue does this PR close?
    
    - Part of https://github.com/apache/arrow-rs/issues/10553
    
    # Rationale for this change
    
    While clearing `clippy::missing_panics_doc`, a pattern turned up:
    functions that
    already return `Result`, but report some failures by panicking. Several
    are
    reachable from untrusted input, so the panic is a denial of service
    rather than a
    bug report.
    
    Documenting those panics would normalise them, so this returns errors
    instead.
    
    This is one of four PRs splitting up the work.
    
    1. **this PR** - return errors from fallible functions
    2. #10759 - remove unreachable panics
    3. #10760 - document the panics that genuinely remain
    4. #10761 - `#[expect]` the unreachable ones, so the lint can be turned
    on
    
    # What changes are included in this PR?
    
    Functions that could report the failure and did not.
    
    # Are these changes tested?
    
    Covered by the existing tests, plus new tests for the `b64_decode` and
    `ArrayData::validate_values` error paths.
    
    # Are there any user-facing changes?
    
    No API changes. Some calls that used to panic now return an `Err`, which
    is the
    point of the PR.
    
    ---------
    
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
    Co-authored-by: Jeffrey Vo <[email protected]>
---
 .../fixed_size_binary_dictionary_builder.rs        |  10 +-
 .../builder/generic_bytes_dictionary_builder.rs    |  10 +-
 .../src/builder/primitive_dictionary_builder.rs    |  10 +-
 arrow-cast/src/base64.rs                           |  19 +-
 arrow-data/src/data.rs                             | 198 +++++++++++++++++---
 arrow-flight/src/sql/client.rs                     |  31 +++-
 arrow-integration-test/src/datatype.rs             |  30 +++-
 arrow-integration-test/src/field.rs                |   6 +-
 arrow-integration-testing/src/lib.rs               |  35 ++--
 arrow-ipc/src/reader.rs                            | 200 ++++++++++++++++-----
 arrow-json/src/reader/mod.rs                       |   8 +-
 arrow-schema/src/ffi.rs                            |  47 +++--
 arrow-select/src/dictionary.rs                     |   4 +-
 arrow-string/src/concat_elements.rs                |  20 ++-
 parquet/src/arrow/arrow_reader/mod.rs              |  13 +-
 parquet/src/arrow/arrow_writer/mod.rs              |  13 +-
 parquet/src/arrow/async_reader/mod.rs              |  13 +-
 parquet/src/column/reader.rs                       |  24 ++-
 parquet/src/file/writer.rs                         |   2 +-
 19 files changed, 542 insertions(+), 151 deletions(-)

diff --git a/arrow-array/src/builder/fixed_size_binary_dictionary_builder.rs 
b/arrow-array/src/builder/fixed_size_binary_dictionary_builder.rs
index a2a10ac4c4..98b7fed0a6 100644
--- a/arrow-array/src/builder/fixed_size_binary_dictionary_builder.rs
+++ b/arrow-array/src/builder/fixed_size_binary_dictionary_builder.rs
@@ -159,9 +159,13 @@ where
         Ok(Self {
             state,
             dedup,
-            keys_builder: new_keys
-                .into_builder()
-                .expect("underlying buffer has no references"),
+            keys_builder: new_keys.into_builder().map_err(|_| {
+                ArrowError::ComputeError(
+                    "Internal Error: the keys just derived from the source 
builder are \
+                     unexpectedly shared, so they cannot be reused as a 
builder"
+                        .to_string(),
+                )
+            })?,
             values_builder,
             byte_width,
         })
diff --git a/arrow-array/src/builder/generic_bytes_dictionary_builder.rs 
b/arrow-array/src/builder/generic_bytes_dictionary_builder.rs
index aed195a033..2a7e0e9173 100644
--- a/arrow-array/src/builder/generic_bytes_dictionary_builder.rs
+++ b/arrow-array/src/builder/generic_bytes_dictionary_builder.rs
@@ -214,9 +214,13 @@ where
         Ok(Self {
             state,
             dedup,
-            keys_builder: new_keys
-                .into_builder()
-                .expect("underlying buffer has no references"),
+            keys_builder: new_keys.into_builder().map_err(|_| {
+                ArrowError::ComputeError(
+                    "Internal Error: the keys just derived from the source 
builder are \
+                     unexpectedly shared, so they cannot be reused as a 
builder"
+                        .to_string(),
+                )
+            })?,
             values_builder,
         })
     }
diff --git a/arrow-array/src/builder/primitive_dictionary_builder.rs 
b/arrow-array/src/builder/primitive_dictionary_builder.rs
index 90b0c14a75..bc5d878500 100644
--- a/arrow-array/src/builder/primitive_dictionary_builder.rs
+++ b/arrow-array/src/builder/primitive_dictionary_builder.rs
@@ -227,9 +227,13 @@ where
 
         Ok(Self {
             map,
-            keys_builder: new_keys
-                .into_builder()
-                .expect("underlying buffer has no references"),
+            keys_builder: new_keys.into_builder().map_err(|_| {
+                ArrowError::ComputeError(
+                    "Internal Error: the keys just derived from the source 
builder are \
+                     unexpectedly shared, so they cannot be reused as a 
builder"
+                        .to_string(),
+                )
+            })?,
             values_builder,
         })
     }
diff --git a/arrow-cast/src/base64.rs b/arrow-cast/src/base64.rs
index 0cd724df7c..e3cc03ff09 100644
--- a/arrow-cast/src/base64.rs
+++ b/arrow-cast/src/base64.rs
@@ -61,6 +61,10 @@ pub fn b64_encode<E: Engine, O: OffsetSizeTrait>(
 }
 
 /// Base64 decode each element of `array` with the provided [`Engine`]
+///
+/// # Errors
+///
+/// Returns an error if a value is not valid base64 for `engine`.
 pub fn b64_decode<E: Engine, O: OffsetSizeTrait>(
     engine: &E,
     array: &GenericBinaryArray<O>,
@@ -74,7 +78,11 @@ pub fn b64_decode<E: Engine, O: OffsetSizeTrait>(
 
     for v in array {
         if let Some(v) = v {
-            let len = engine.decode_slice(v, &mut buffer[offset..]).unwrap();
+            let len = engine
+                .decode_slice(v, &mut buffer[offset..])
+                .map_err(|err| {
+                    ArrowError::InvalidArgumentError(format!("Failed to decode 
base64: {err}"))
+                })?;
             // This cannot overflow as `len` is less than `v.len()` and `a` is 
valid
             offset += len;
         }
@@ -120,6 +128,15 @@ mod tests {
         test_engine(&BASE64_STANDARD_NO_PAD, &data);
     }
 
+    #[test]
+    fn test_b64_decode_invalid_input() {
+        let data: BinaryArray = vec![Some(b"!!!not base64!!!".to_vec())]
+            .into_iter()
+            .collect();
+        let err = b64_decode(&BASE64_STANDARD, &data).unwrap_err().to_string();
+        assert!(err.contains("Failed to decode base64"), "{err}");
+    }
+
     /// Safe-Rust `Engine` that writes invalid UTF-8 into the encode buffer
     /// (#10284). `b64_encode` must reject it rather than build an unsound
     /// `StringArray`.
diff --git a/arrow-data/src/data.rs b/arrow-data/src/data.rs
index 7bf9520785..ed73bf6273 100644
--- a/arrow-data/src/data.rs
+++ b/arrow-data/src/data.rs
@@ -1090,7 +1090,7 @@ impl ArrayData {
     /// For an empty array, the `buffer` can also be empty.
     fn typed_offsets<T: ArrowNativeType + num_traits::Num>(&self) -> 
Result<&[T], ArrowError> {
         // An empty list-like array can have 0 offsets
-        if self.len == 0 && self.buffers[0].is_empty() {
+        if self.len == 0 && self.buffer_at(0)?.is_empty() {
             return Ok(&[]);
         }
 
@@ -1105,7 +1105,7 @@ impl ArrayData {
         idx: usize,
         len: usize,
     ) -> Result<&[T], ArrowError> {
-        let buffer = &self.buffers[idx];
+        let buffer = self.buffer_at(idx)?;
 
         let required_elements = checked_len_plus_offset(&self.data_type, len, 
self.offset)?;
         let byte_width = mem::size_of::<T>();
@@ -1376,6 +1376,36 @@ impl ArrayData {
         self.get_valid_child_data(0, expected_type)
     }
 
+    /// Returns `buffers[idx]`, or an error if there is no such buffer.
+    ///
+    /// [`Self::validate_values`] can be called on its own, without the buffer 
counts
+    /// having been checked by [`Self::validate`] first, so the index may be 
missing.
+    fn buffer_at(&self, idx: usize) -> Result<&Buffer, ArrowError> {
+        self.buffers.get(idx).ok_or_else(|| {
+            ArrowError::InvalidArgumentError(format!(
+                "{} should contain at least {} buffer(s), had {}",
+                self.data_type,
+                idx + 1,
+                self.buffers.len()
+            ))
+        })
+    }
+
+    /// Returns `child_data[idx]`, or an error if there is no such child.
+    ///
+    /// [`Self::validate_values`] can be called on its own, without the child 
counts
+    /// having been checked by [`Self::validate`] first, so the index may be 
missing.
+    fn child_at(&self, idx: usize) -> Result<&ArrayData, ArrowError> {
+        self.child_data.get(idx).ok_or_else(|| {
+            ArrowError::InvalidArgumentError(format!(
+                "{} should contain at least {} child data array(s), had {}",
+                self.data_type,
+                idx + 1,
+                self.child_data.len()
+            ))
+        })
+    }
+
     /// Returns `Err` if self.child_data does not have exactly `expected_len` 
elements
     fn validate_num_child_data(&self, expected_len: usize) -> Result<(), 
ArrowError> {
         if self.child_data.len() != expected_len {
@@ -1552,8 +1582,8 @@ impl ArrayData {
         match &self.data_type {
             DataType::Utf8 => self.validate_utf8::<i32>(),
             DataType::LargeUtf8 => self.validate_utf8::<i64>(),
-            DataType::Binary => 
self.validate_offsets_full::<i32>(self.buffers[1].len()),
-            DataType::LargeBinary => 
self.validate_offsets_full::<i64>(self.buffers[1].len()),
+            DataType::Binary => 
self.validate_offsets_full::<i32>(self.buffer_at(1)?.len()),
+            DataType::LargeBinary => 
self.validate_offsets_full::<i64>(self.buffer_at(1)?.len()),
             DataType::BinaryView => {
                 let views = self.typed_buffer::<u128>(0, self.len)?;
                 validate_binary_view(views, &self.buffers[1..])
@@ -1563,11 +1593,11 @@ impl ArrayData {
                 validate_string_view(views, &self.buffers[1..])
             }
             DataType::List(_) | DataType::Map(_, _) => {
-                let child = &self.child_data[0];
+                let child = self.child_at(0)?;
                 self.validate_offsets_full::<i32>(child.len)
             }
             DataType::LargeList(_) => {
-                let child = &self.child_data[0];
+                let child = self.child_at(0)?;
                 self.validate_offsets_full::<i64>(child.len)
             }
             DataType::Union(_, _) => {
@@ -1579,7 +1609,12 @@ impl ArrayData {
                 Ok(())
             }
             DataType::Dictionary(key_type, _value_type) => {
-                let dictionary_length: i64 = 
self.child_data[0].len.try_into().unwrap();
+                let dictionary_length = self.child_at(0)?.len;
+                let dictionary_length = 
i64::try_from(dictionary_length).map_err(|_| {
+                    ArrowError::InvalidArgumentError(format!(
+                        "Dictionary of {dictionary_length} values is too long 
for an i64"
+                    ))
+                })?;
                 let max_value = dictionary_length - 1;
                 match key_type.as_ref() {
                     DataType::UInt8 => self.check_bounds::<u8>(max_value),
@@ -1590,16 +1625,20 @@ impl ArrayData {
                     DataType::Int16 => self.check_bounds::<i16>(max_value),
                     DataType::Int32 => self.check_bounds::<i32>(max_value),
                     DataType::Int64 => self.check_bounds::<i64>(max_value),
-                    _ => unreachable!(),
+                    _ => Err(ArrowError::InvalidArgumentError(format!(
+                        "Dictionary key type must be an integer, got 
{key_type}"
+                    ))),
                 }
             }
             DataType::RunEndEncoded(run_ends, _values) => {
-                let run_ends_data = self.child_data()[0].clone();
+                let run_ends_data = self.child_at(0)?;
                 match run_ends.data_type() {
                     DataType::Int16 => run_ends_data.check_run_ends::<i16>(),
                     DataType::Int32 => run_ends_data.check_run_ends::<i32>(),
                     DataType::Int64 => run_ends_data.check_run_ends::<i64>(),
-                    _ => unreachable!(),
+                    data_type => Err(ArrowError::InvalidArgumentError(format!(
+                        "Run end type must be Int16, Int32 or Int64, got 
{data_type}"
+                    ))),
                 }
             }
             _ => {
@@ -1670,7 +1709,7 @@ impl ArrayData {
     where
         T: ArrowNativeType + TryInto<usize> + num_traits::Num + 
std::fmt::Display,
     {
-        let values_buffer = &self.buffers[1].as_slice();
+        let values_buffer = &self.buffer_at(1)?.as_slice();
         if let Ok(values_str) = std::str::from_utf8(values_buffer) {
             // Validate Offsets are correct
             self.validate_each_offset::<T, _>(values_buffer.len(), 
|string_index, range| {
@@ -1715,15 +1754,9 @@ impl ArrayData {
     where
         T: ArrowNativeType + TryInto<i64> + num_traits::Num + 
std::fmt::Display,
     {
-        let required_len = checked_len_plus_offset(&self.data_type, self.len, 
self.offset)?;
-        let buffer = &self.buffers[0];
-
-        // This should have been checked as part of `validate()` prior
-        // to calling `validate_full()` but double check to be sure
-        assert!(buffer.len() / mem::size_of::<T>() >= required_len);
-
-        // Justification: buffer size was validated above
-        let indexes: &[T] = 
&buffer.typed_data::<T>()[self.offset..required_len];
+        // `validate()` checks the buffer size too, but `validate_values()` 
can be called
+        // on its own, so do not assume it has run.
+        let indexes: &[T] = self.typed_buffer::<T>(0, self.len)?;
 
         indexes.iter().enumerate().try_for_each(|(i, &dict_index)| {
             // Do not check the value is null (value can be arbitrary)
@@ -2958,6 +2991,131 @@ mod tests {
         );
     }
 
+    /// Without `force_validate`, `build_unchecked` skips validation, so these 
can
+    /// reach `validate_values` with a data type that has an invalid child 
type.
+    #[test]
+    #[cfg(not(feature = "force_validate"))]
+    fn test_validate_values_rejects_a_non_integer_dictionary_key() {
+        let values = valid_non_nullable_int32_array_data(2);
+        let data_type = DataType::Dictionary(Box::new(DataType::Utf8), 
Box::new(DataType::Int32));
+        let dictionary = unsafe {
+            ArrayData::builder(data_type)
+                .len(1)
+                .add_child_data(values)
+                .build_unchecked()
+        };
+
+        let err = dictionary.validate_values().expect_err("should get error");
+        assert_eq!(
+            err.to_string(),
+            "Invalid argument error: Dictionary key type must be an integer, 
got Utf8"
+        );
+    }
+
+    #[test]
+    #[cfg(not(feature = "force_validate"))]
+    fn test_validate_values_rejects_a_non_integer_run_end() {
+        let data_type = DataType::RunEndEncoded(
+            Arc::new(Field::new("run_ends", DataType::Utf8, false)),
+            Arc::new(Field::new("values", DataType::Int32, true)),
+        );
+        let run_end_encoded = unsafe {
+            ArrayData::builder(data_type)
+                .len(1)
+                .add_child_data(valid_non_nullable_int32_array_data(1))
+                .add_child_data(valid_non_nullable_int32_array_data(1))
+                .build_unchecked()
+        };
+
+        let err = run_end_encoded
+            .validate_values()
+            .expect_err("should get error");
+        assert_eq!(
+            err.to_string(),
+            "Invalid argument error: Run end type must be Int16, Int32 or 
Int64, got Utf8"
+        );
+    }
+
+    /// `validate_values` must report missing children rather than index out 
of bounds.
+    #[test]
+    #[cfg(not(feature = "force_validate"))]
+    fn test_validate_values_rejects_missing_child_data() {
+        let int32 = Box::new(DataType::Int32);
+        let field = || Arc::new(Field::new("f", DataType::Int32, true));
+        let data_types = [
+            DataType::Dictionary(int32.clone(), int32.clone()),
+            DataType::List(field()),
+            DataType::LargeList(field()),
+            DataType::RunEndEncoded(field(), field()),
+        ];
+
+        for data_type in data_types {
+            let data = unsafe {
+                ArrayData::builder(data_type.clone())
+                    .len(1)
+                    .build_unchecked()
+            };
+            let err = data.validate_values().expect_err("should get error");
+            assert_eq!(
+                err.to_string(),
+                format!(
+                    "Invalid argument error: {data_type} should contain at 
least 1 child data array(s), had 0"
+                )
+            );
+        }
+    }
+
+    /// `validate_values` must report missing buffers rather than index out of 
bounds.
+    #[test]
+    #[cfg(not(feature = "force_validate"))]
+    fn test_validate_values_rejects_missing_buffers() {
+        // (data type, index of the first missing buffer)
+        let cases = [
+            (DataType::Utf8, 1),
+            (DataType::LargeUtf8, 1),
+            (DataType::Binary, 1),
+            (DataType::LargeBinary, 1),
+            (DataType::BinaryView, 0),
+            (DataType::Utf8View, 0),
+        ];
+
+        for (data_type, missing) in cases {
+            let data = unsafe {
+                ArrayData::builder(data_type.clone())
+                    .len(1)
+                    .build_unchecked()
+            };
+            let err = data.validate_values().expect_err("should get error");
+            assert_eq!(
+                err.to_string(),
+                format!(
+                    "Invalid argument error: {data_type} should contain at 
least {} buffer(s), had 0",
+                    missing + 1
+                )
+            );
+        }
+    }
+
+    /// A dictionary whose keys buffer is too small must be reported, not 
asserted on.
+    #[test]
+    #[cfg(not(feature = "force_validate"))]
+    fn test_validate_values_rejects_a_short_dictionary_keys_buffer() {
+        let data_type = DataType::Dictionary(Box::new(DataType::Int32), 
Box::new(DataType::Int32));
+        let dictionary = unsafe {
+            ArrayData::builder(data_type)
+                .len(4)
+                .add_buffer(Buffer::from_slice_ref([1_i32, 0]))
+                .add_child_data(valid_non_nullable_int32_array_data(2))
+                .build_unchecked()
+        };
+
+        let err = dictionary.validate_values().expect_err("should get error");
+        assert_eq!(
+            err.to_string(),
+            "Invalid argument error: Buffer 0 of Dictionary(Int32, Int32) 
isn't large enough. Expected 16 bytes got 8"
+        );
+    }
+
     #[test]
     fn should_fail_validation_when_having_map_field_type_is_not_struct() {
         let map_field = Field::new("key", DataType::Int32, false);
diff --git a/arrow-flight/src/sql/client.rs b/arrow-flight/src/sql/client.rs
index 6ea1a04fc1..e00f7254a3 100644
--- a/arrow-flight/src/sql/client.rs
+++ b/arrow-flight/src/sql/client.rs
@@ -222,7 +222,9 @@ where
             .into_request(),
         )?;
         let mut result = self.flight_client.do_put(req).await?.into_inner();
-        let result = result.message().await?.unwrap();
+        let result = result.message().await?.ok_or_else(|| {
+            FlightError::protocol("Server closed the stream without sending a 
result")
+        })?;
         let result: DoPutUpdateResult = 
Message::decode(&*result.app_metadata)?;
         Ok(result.record_count)
     }
@@ -258,7 +260,9 @@ where
             return Err(FlightError::ExternalError(Box::new(msg)));
         }
 
-        let result = result.message().await?.unwrap();
+        let result = result.message().await?.ok_or_else(|| {
+            FlightError::protocol("Server closed the stream without sending a 
result")
+        })?;
         let result: DoPutUpdateResult = 
Message::decode(&*result.app_metadata)?;
         Ok(result.record_count)
     }
@@ -387,9 +391,16 @@ where
         };
         let req = self.set_request_headers(action.into_request())?;
         let mut result = self.flight_client.do_action(req).await?.into_inner();
-        let result = result.message().await?.unwrap();
+        let result = result.message().await?.ok_or_else(|| {
+            FlightError::protocol("Server closed the stream without sending a 
result")
+        })?;
         let any = Any::decode(&*result.body)?;
-        let prepared_result: ActionCreatePreparedStatementResult = 
any.unpack()?.unwrap();
+        let prepared_result: ActionCreatePreparedStatementResult =
+            any.unpack()?.ok_or_else(|| {
+                FlightError::protocol(
+                    "Server did not return an 
ActionCreatePreparedStatementResult",
+                )
+            })?;
         let dataset_schema = match prepared_result.dataset_schema.len() {
             0 => Schema::empty(),
             _ => Schema::try_from(IpcMessage(prepared_result.dataset_schema))?,
@@ -415,9 +426,13 @@ where
         };
         let req = self.set_request_headers(action.into_request())?;
         let mut result = self.flight_client.do_action(req).await?.into_inner();
-        let result = result.message().await?.unwrap();
+        let result = result.message().await?.ok_or_else(|| {
+            FlightError::protocol("Server closed the stream without sending a 
result")
+        })?;
         let any = Any::decode(&*result.body)?;
-        let begin_result: ActionBeginTransactionResult = 
any.unpack()?.unwrap();
+        let begin_result: ActionBeginTransactionResult = 
any.unpack()?.ok_or_else(|| {
+            FlightError::protocol("Server did not return an 
ActionBeginTransactionResult")
+        })?;
         Ok(begin_result.transaction_id)
     }
 
@@ -542,7 +557,9 @@ where
                 ..Default::default()
             }]))
             .await?;
-        let result = result.message().await?.unwrap();
+        let result = result.message().await?.ok_or_else(|| {
+            FlightError::protocol("Server closed the stream without sending a 
result")
+        })?;
         let result: DoPutUpdateResult = 
Message::decode(&*result.app_metadata)?;
         Ok(result.record_count)
     }
diff --git a/arrow-integration-test/src/datatype.rs 
b/arrow-integration-test/src/datatype.rs
index 69174a1c22..9ff0077639 100644
--- a/arrow-integration-test/src/datatype.rs
+++ b/arrow-integration-test/src/datatype.rs
@@ -19,6 +19,15 @@ use arrow::datatypes::{DataType, Field, Fields, 
IntervalUnit, TimeUnit, UnionMod
 use arrow::error::{ArrowError, Result};
 use std::sync::Arc;
 
+/// Read a JSON number as an integer of type `T`.
+fn json_int<T: TryFrom<i64>>(what: &str, value: &serde_json::Value) -> 
Result<T> {
+    let int = value
+        .as_i64()
+        .ok_or_else(|| ArrowError::ParseError(format!("Expecting {what} to be 
an integer")))?;
+    T::try_from(int)
+        .map_err(|_| ArrowError::ParseError(format!("{what} is out of range 
for its type: {int}")))
+}
+
 /// Parse a data type from a JSON representation.
 pub fn data_type_from_json(json: &serde_json::Value) -> Result<DataType> {
     use serde_json::Value;
@@ -36,7 +45,10 @@ pub fn data_type_from_json(json: &serde_json::Value) -> 
Result<DataType> {
             Some(s) if s == "fixedsizebinary" => {
                 // return a list with any type as its child isn't defined in 
the map
                 if let Some(Value::Number(size)) = map.get("byteWidth") {
-                    Ok(DataType::FixedSizeBinary(size.as_i64().unwrap() as 
i32))
+                    Ok(DataType::FixedSizeBinary(json_int(
+                        "byteWidth",
+                        &Value::Number(size.clone()),
+                    )?))
                 } else {
                     Err(ArrowError::ParseError(
                         "Expecting a byteWidth for 
fixedsizebinary".to_string(),
@@ -46,19 +58,19 @@ pub fn data_type_from_json(json: &serde_json::Value) -> 
Result<DataType> {
             Some(s) if s == "decimal" => {
                 // return a list with any type as its child isn't defined in 
the map
                 let precision = match map.get("precision") {
-                    Some(p) => Ok(p.as_u64().unwrap().try_into().unwrap()),
+                    Some(p) => json_int("precision", p),
                     None => Err(ArrowError::ParseError(
                         "Expecting a precision for decimal".to_string(),
                     )),
                 }?;
                 let scale = match map.get("scale") {
-                    Some(s) => Ok(s.as_u64().unwrap().try_into().unwrap()),
+                    Some(s) => json_int("scale", s),
                     _ => Err(ArrowError::ParseError(
                         "Expecting a scale for decimal".to_string(),
                     )),
                 }?;
                 let bit_width: usize = match map.get("bitWidth") {
-                    Some(b) => b.as_u64().unwrap() as usize,
+                    Some(b) => json_int("bitWidth", b)?,
                     _ => 128, // Default bit width
                 };
 
@@ -197,7 +209,7 @@ pub fn data_type_from_json(json: &serde_json::Value) -> 
Result<DataType> {
                 if let Some(Value::Number(size)) = map.get("listSize") {
                     Ok(DataType::FixedSizeList(
                         default_field,
-                        size.as_i64().unwrap() as i32,
+                        json_int("listSize", &Value::Number(size.clone()))?,
                     ))
                 } else {
                     Err(ArrowError::ParseError(
@@ -238,10 +250,14 @@ pub fn data_type_from_json(json: &serde_json::Value) -> 
Result<DataType> {
                         )));
                     };
                     if let Some(values) = map.get("typeIds") {
-                        let values = values.as_array().unwrap();
+                        let values = values.as_array().ok_or_else(|| {
+                            ArrowError::ParseError("Expecting typeIds to be an 
array".to_string())
+                        })?;
                         let fields = values
                             .iter()
-                            .map(|t| (t.as_i64().unwrap() as i8, 
default_field.clone()))
+                            .map(|t| Ok((json_int::<i8>("a type id", t)?, 
default_field.clone())))
+                            .collect::<Result<Vec<_>>>()?
+                            .into_iter()
                             .collect();
 
                         Ok(DataType::Union(fields, union_mode))
diff --git a/arrow-integration-test/src/field.rs 
b/arrow-integration-test/src/field.rs
index da30217431..68019cc537 100644
--- a/arrow-integration-test/src/field.rs
+++ b/arrow-integration-test/src/field.rs
@@ -259,7 +259,11 @@ pub fn field_from_json(json: &serde_json::Value) -> 
Result<Field> {
                         }
                     };
                     dict_id = match dictionary.get("id") {
-                        Some(Value::Number(n)) => n.as_i64().unwrap(),
+                        Some(Value::Number(n)) => n.as_i64().ok_or_else(|| {
+                            ArrowError::ParseError(
+                                "Field 'id' attribute is not an 
integer".to_string(),
+                            )
+                        })?,
                         _ => {
                             return Err(ArrowError::ParseError(
                                 "Field missing 'id' attribute".to_string(),
diff --git a/arrow-integration-testing/src/lib.rs 
b/arrow-integration-testing/src/lib.rs
index 613408ae59..a1f7d2a70a 100644
--- a/arrow-integration-testing/src/lib.rs
+++ b/arrow-integration-testing/src/lib.rs
@@ -58,23 +58,27 @@ pub struct ArrowFile {
 impl ArrowFile {
     /// Read a single [RecordBatch] from the file
     pub fn read_batch(&self, batch_num: usize) -> Result<RecordBatch> {
-        let b = self.arrow_json["batches"].get(batch_num).unwrap();
-        let json_batch: ArrowJsonBatch = 
serde_json::from_value(b.clone()).unwrap();
-        record_batch_from_json(&self.schema, json_batch, 
Some(&self.dictionaries))
+        let b = self.arrow_json["batches"].get(batch_num).ok_or_else(|| {
+            ArrowError::ParseError(format!("Arrow JSON has no batch 
{batch_num}"))
+        })?;
+        self.batch_from_json(b)
     }
 
     /// Read all [RecordBatch]es from the file
     pub fn read_batches(&self) -> Result<Vec<RecordBatch>> {
         self.arrow_json["batches"]
             .as_array()
-            .unwrap()
+            .ok_or_else(|| ArrowError::ParseError("Arrow JSON has no 'batches' 
array".to_string()))?
             .iter()
-            .map(|b| {
-                let json_batch: ArrowJsonBatch = 
serde_json::from_value(b.clone()).unwrap();
-                record_batch_from_json(&self.schema, json_batch, 
Some(&self.dictionaries))
-            })
+            .map(|b| self.batch_from_json(b))
             .collect()
     }
+
+    fn batch_from_json(&self, batch: &Value) -> Result<RecordBatch> {
+        let json_batch: ArrowJsonBatch = serde_json::from_value(batch.clone())
+            .map_err(|err| ArrowError::ParseError(format!("Invalid Arrow JSON 
batch: {err}")))?;
+        record_batch_from_json(&self.schema, json_batch, 
Some(&self.dictionaries))
+    }
 }
 
 /// Canonicalize the names of map fields in a schema
@@ -122,17 +126,20 @@ pub fn canonicalize_schema(schema: &Schema) -> Schema {
 pub fn open_json_file(json_name: &str) -> Result<ArrowFile> {
     let json_file = File::open(json_name)?;
     let reader = BufReader::new(json_file);
-    let arrow_json: Value = serde_json::from_reader(reader).unwrap();
+    let arrow_json: Value = serde_json::from_reader(reader)
+        .map_err(|err| ArrowError::ParseError(format!("Invalid Arrow JSON: 
{err}")))?;
     let schema = schema_from_json(&arrow_json["schema"])?;
     // read dictionaries
     let mut dictionaries = HashMap::new();
     if let Some(dicts) = arrow_json.get("dictionaries") {
-        for d in dicts
-            .as_array()
-            .expect("Unable to get dictionaries as array")
-        {
+        let dicts = dicts.as_array().ok_or_else(|| {
+            ArrowError::ParseError("Arrow JSON 'dictionaries' is not an 
array".to_string())
+        })?;
+        for d in dicts {
             let json_dict: ArrowJsonDictionaryBatch =
-                serde_json::from_value(d.clone()).expect("Unable to get 
dictionary from JSON");
+                serde_json::from_value(d.clone()).map_err(|err| {
+                    ArrowError::ParseError(format!("Invalid Arrow JSON 
dictionary: {err}"))
+                })?;
             // TODO: convert to a concrete Arrow type
             dictionaries.insert(json_dict.id, json_dict);
         }
diff --git a/arrow-ipc/src/reader.rs b/arrow-ipc/src/reader.rs
index c8eb45515b..3b409d2a2f 100644
--- a/arrow-ipc/src/reader.rs
+++ b/arrow-ipc/src/reader.rs
@@ -102,12 +102,7 @@ impl RecordBatchDecoder<'_> {
                 self.create_primitive_array(field_node, data_type, &buffers)
             }
             BinaryView | Utf8View => {
-                let count = variadic_counts
-                    .pop_front()
-                    .ok_or(ArrowError::IpcError(format!(
-                        "Missing variadic count for {data_type} column"
-                    )))?;
-                let count = count + 2; // view and null buffer.
+                let count = self.next_variadic_buffer_count(variadic_counts, 
data_type)?;
                 let buffers = (0..count)
                     .map(|_| self.next_buffer())
                     .collect::<Result<Vec<_>, _>>()?;
@@ -546,6 +541,11 @@ impl<'a> RecordBatchDecoder<'a> {
     }
 
     /// Read the record batch, consuming the reader
+    ///
+    /// # Errors
+    ///
+    /// Returns an error if the message does not describe a batch matching the 
schema,
+    /// for example if it declares more variadic buffer counts than the schema 
uses.
     pub fn read_record_batch(mut self) -> Result<RecordBatch, ArrowError> {
         let mut variadic_counts: VecDeque<i64> = self
             .batch
@@ -598,7 +598,7 @@ impl<'a> RecordBatchDecoder<'a> {
                     ))
                 }
             } else {
-                assert!(variadic_counts.is_empty());
+                check_variadic_counts_consumed(&variadic_counts)?;
                 RecordBatch::try_new_with_options(schema, columns, &options)
             }
         } else {
@@ -619,7 +619,7 @@ impl<'a> RecordBatchDecoder<'a> {
                     ))
                 }
             } else {
-                assert!(variadic_counts.is_empty());
+                check_variadic_counts_consumed(&variadic_counts)?;
                 RecordBatch::try_new_with_options(schema, children, &options)
             }
         }
@@ -637,8 +637,11 @@ impl<'a> RecordBatchDecoder<'a> {
         )
     }
 
-    fn skip_buffer(&mut self) {
-        self.buffers.next().unwrap();
+    fn skip_buffer(&mut self) -> Result<(), ArrowError> {
+        self.buffers.next().ok_or_else(|| {
+            ArrowError::IpcError("Buffer count mismatched with 
metadata".to_string())
+        })?;
+        Ok(())
     }
 
     fn next_node(&mut self, field: &Field) -> Result<&'a FieldNode, 
ArrowError> {
@@ -659,42 +662,36 @@ impl<'a> RecordBatchDecoder<'a> {
         match field.data_type() {
             Utf8 | Binary | LargeBinary | LargeUtf8 => {
                 for _ in 0..3 {
-                    self.skip_buffer()
+                    self.skip_buffer()?;
                 }
             }
             Utf8View | BinaryView => {
-                let count = variadic_count
-                    .pop_front()
-                    .ok_or(ArrowError::IpcError(format!(
-                        "Missing variadic count for {} column",
-                        field.data_type()
-                    )))?;
-                let count = count + 2; // view and null buffer.
-                for _i in 0..count {
-                    self.skip_buffer()
+                let count = self.next_variadic_buffer_count(variadic_count, 
field.data_type())?;
+                for _ in 0..count {
+                    self.skip_buffer()?;
                 }
             }
             FixedSizeBinary(_) => {
-                self.skip_buffer();
-                self.skip_buffer();
+                self.skip_buffer()?;
+                self.skip_buffer()?;
             }
             List(list_field) | LargeList(list_field) | Map(list_field, _) => {
-                self.skip_buffer();
-                self.skip_buffer();
+                self.skip_buffer()?;
+                self.skip_buffer()?;
                 self.skip_field(list_field, variadic_count)?;
             }
             ListView(list_field) | LargeListView(list_field) => {
-                self.skip_buffer(); // Null buffer
-                self.skip_buffer(); // Offsets
-                self.skip_buffer(); // Sizes
+                self.skip_buffer()?; // Null buffer
+                self.skip_buffer()?; // Offsets
+                self.skip_buffer()?; // Sizes
                 self.skip_field(list_field, variadic_count)?;
             }
             FixedSizeList(list_field, _) => {
-                self.skip_buffer();
+                self.skip_buffer()?;
                 self.skip_field(list_field, variadic_count)?;
             }
             Struct(struct_fields) => {
-                self.skip_buffer();
+                self.skip_buffer()?;
 
                 // skip for each field
                 for struct_field in struct_fields {
@@ -706,17 +703,17 @@ impl<'a> RecordBatchDecoder<'a> {
                 self.skip_field(values_field, variadic_count)?;
             }
             Dictionary(_, _) => {
-                self.skip_buffer(); // Nulls
-                self.skip_buffer(); // Indices
+                self.skip_buffer()?; // Nulls
+                self.skip_buffer()?; // Indices
             }
             Union(fields, mode) => {
                 if self.version < MetadataVersion::V5 {
-                    self.skip_buffer(); // Null buffer
+                    self.skip_buffer()?; // Null buffer
                 }
-                self.skip_buffer(); // Type ids
+                self.skip_buffer()?; // Type ids
 
                 match mode {
-                    UnionMode::Dense => self.skip_buffer(), // Offsets
+                    UnionMode::Dense => self.skip_buffer()?, // Offsets
                     UnionMode::Sparse => {}
                 }
 
@@ -751,14 +748,45 @@ impl<'a> RecordBatchDecoder<'a> {
             | Decimal64(_, _)
             | Decimal128(_, _)
             | Decimal256(_, _) => {
-                self.skip_buffer();
-                self.skip_buffer();
+                self.skip_buffer()?;
+                self.skip_buffer()?;
             }
         }
         Ok(())
     }
 }
 
+impl RecordBatchDecoder<'_> {
+    /// Takes the number of variadic buffers declared for one `BinaryView` or 
`Utf8View`
+    /// column, and returns the total number of buffers to read for it.
+    ///
+    /// The count comes from the IPC message, so it may be missing, negative, 
or larger
+    /// than the number of buffers the message actually has.
+    fn next_variadic_buffer_count(
+        &self,
+        variadic_counts: &mut VecDeque<i64>,
+        data_type: &DataType,
+    ) -> Result<usize, ArrowError> {
+        let count = variadic_counts.pop_front().ok_or_else(|| {
+            ArrowError::IpcError(format!("Missing variadic count for 
{data_type} column"))
+        })?;
+
+        let remaining = self.buffers.len();
+
+        // The view buffer and the null buffer are not counted as variadic.
+        usize::try_from(count)
+            .ok()
+            .and_then(|count| count.checked_add(2))
+            .filter(|total| *total <= remaining)
+            .ok_or_else(|| {
+                ArrowError::IpcError(format!(
+                    "Invalid variadic count {count} for {data_type} column, \
+                     with {remaining} buffer(s) left in the message"
+                ))
+            })
+    }
+}
+
 /// Creates a record batch from binary data using the `crate::RecordBatch` 
indexes and the `Schema`.
 ///
 /// If `require_alignment` is true, this function will return an error if any 
array data in the
@@ -930,6 +958,22 @@ fn read_block<R: Read + Seek>(mut reader: R, block: 
&Block) -> Result<Buffer, Ar
     Ok(buf.into())
 }
 
+/// One variadic buffer count is consumed per `BinaryView` or `Utf8View` 
column in
+/// the schema, so any count left over means the message and the schema 
disagree.
+///
+/// The opposite case, too few counts, is reported by 
[`RecordBatchDecoder::create_array`].
+fn check_variadic_counts_consumed(variadic_counts: &VecDeque<i64>) -> 
Result<(), ArrowError> {
+    if variadic_counts.is_empty() {
+        Ok(())
+    } else {
+        Err(ArrowError::IpcError(format!(
+            "Mismatch between schema and data: the IPC message declares {} 
more variadic \
+             buffer count(s) than the schema has BinaryView or Utf8View 
columns",
+            variadic_counts.len()
+        )))
+    }
+}
+
 /// Parse an encapsulated message
 ///
 /// 
<https://arrow.apache.org/docs/format/Columnar.html#encapsulated-message-format>
@@ -1273,10 +1317,12 @@ impl FileReaderBuilder {
         let mut custom_metadata = HashMap::new();
         if let Some(fb_custom_metadata) = footer.custom_metadata() {
             for kv in fb_custom_metadata {
-                custom_metadata.insert(
-                    kv.key().unwrap().to_string(),
-                    kv.value().unwrap().to_string(),
-                );
+                let (Some(key), Some(value)) = (kv.key(), kv.value()) else {
+                    return Err(ArrowError::ParseError(
+                        "Custom metadata in the IPC footer is missing a key or 
a value".to_string(),
+                    ));
+                };
+                custom_metadata.insert(key.to_string(), value.to_string());
             }
         }
 
@@ -2200,6 +2246,80 @@ mod tests {
         }
     }
 
+    /// A `Utf8View` batch whose variadic buffer count is `count`, with two 
buffers
+    /// in the message. Returns the error from reading it with the given 
projection.
+    fn read_batch_with_variadic_count(count: i64, projection: 
Option<&[usize]>) -> ArrowError {
+        use crate::r#gen::Message::*;
+        use flatbuffers::FlatBufferBuilder;
+
+        let schema = Arc::new(Schema::new(vec![Field::new(
+            "col",
+            DataType::Utf8View,
+            true,
+        )]));
+
+        let mut fbb = FlatBufferBuilder::new();
+        let nodes = fbb.create_vector(&[FieldNode::new(1, 0)]);
+        let buffers = fbb.create_vector(&[crate::Buffer::new(0, 8), 
crate::Buffer::new(8, 8)]);
+        let variadic_buffer_counts = fbb.create_vector(&[count]);
+        let batch_offset = RecordBatch::create(
+            &mut fbb,
+            &RecordBatchArgs {
+                length: 1,
+                nodes: Some(nodes),
+                buffers: Some(buffers),
+                compression: None,
+                variadicBufferCounts: Some(variadic_buffer_counts),
+            },
+        );
+        fbb.finish_minimal(batch_offset);
+        let batch_bytes = fbb.finished_data().to_vec();
+        let batch = flatbuffers::root::<RecordBatch>(&batch_bytes).unwrap();
+
+        let data_buffer = Buffer::from(vec![0u8; 16]);
+        let dictionaries: HashMap<i64, ArrayRef> = HashMap::new();
+
+        RecordBatchDecoder::try_new(
+            &data_buffer,
+            batch,
+            schema,
+            &dictionaries,
+            &MetadataVersion::V5,
+        )
+        .unwrap()
+        .with_projection(projection)
+        .read_record_batch()
+        .expect_err("should get error")
+    }
+
+    /// A variadic count the message cannot honour used to panic while slicing 
the
+    /// buffers it did not read, both when reading the column and when 
skipping it.
+    #[test]
+    fn test_invalid_variadic_buffer_count_error() {
+        // -2 leaves no buffers at all, -1 leaves too few, and 1 asks for more 
than the
+        // message has. The projection selects nothing, so the column is 
skipped instead.
+        for count in [-2, -1, 1, i64::MAX] {
+            for projection in [None, Some([].as_slice())] {
+                let err = read_batch_with_variadic_count(count, projection);
+                assert_eq!(
+                    err.to_string(),
+                    format!(
+                        "Ipc error: Invalid variadic count {count} for 
Utf8View column, \
+                         with 2 buffer(s) left in the message"
+                    ),
+                    "count {count}, projection {projection:?}"
+                );
+            }
+        }
+    }
+
+    /// The valid count for a message with two buffers is zero.
+    #[test]
+    fn test_valid_variadic_buffer_count_is_accepted() {
+        let err = read_batch_with_variadic_count(0, None);
+        assert!(!err.to_string().contains("Invalid variadic count"), "{err}");
+    }
+
     #[test]
     fn test_missing_footer_schema_error() {
         use crate::r#gen::File::{Footer, FooterArgs};
diff --git a/arrow-json/src/reader/mod.rs b/arrow-json/src/reader/mod.rs
index 643e45f238..ad17402ff8 100644
--- a/arrow-json/src/reader/mod.rs
+++ b/arrow-json/src/reader/mod.rs
@@ -714,12 +714,12 @@ impl Decoder {
 
         // First offset is null sentinel
         let mut next_object = 1;
-        let pos: Vec<_> = (0..tape.num_rows())
+        let pos = (0..tape.num_rows())
             .map(|_| {
-                let next = tape.next(next_object, "row").unwrap();
-                std::mem::replace(&mut next_object, next)
+                let next = tape.next(next_object, "row")?;
+                Ok(std::mem::replace(&mut next_object, next))
             })
-            .collect();
+            .collect::<Result<Vec<_>, ArrowError>>()?;
 
         let decoded = self.decoder.decode(&tape, &pos)?;
         self.tape_decoder.clear();
diff --git a/arrow-schema/src/ffi.rs b/arrow-schema/src/ffi.rs
index 679ced42d5..064b4ac16d 100644
--- a/arrow-schema/src/ffi.rs
+++ b/arrow-schema/src/ffi.rs
@@ -433,54 +433,47 @@ impl FFI_ArrowSchema {
             // On some platforms, c_char = u8, and on some, c_char = i8.
             let buffer = self.metadata.cast::<u8>();
 
-            fn next_four_bytes(buffer: *const u8, pos: &mut isize) -> [u8; 4] {
+            fn next_four_bytes(buffer: *const u8, pos: &mut usize) -> [u8; 4] {
                 // Safety: the caller advances `pos` only by the number of 
bytes consumed,
                 // so `*pos..*pos+4` is always within the bounds of the 
metadata buffer.
                 let out = unsafe {
                     [
-                        *buffer.offset(*pos),
-                        *buffer.offset(*pos + 1),
-                        *buffer.offset(*pos + 2),
-                        *buffer.offset(*pos + 3),
+                        *buffer.add(*pos),
+                        *buffer.add(*pos + 1),
+                        *buffer.add(*pos + 2),
+                        *buffer.add(*pos + 3),
                     ]
                 };
                 *pos += 4;
                 out
             }
 
-            fn next_n_bytes(buffer: *const u8, pos: &mut isize, n: i32) -> 
&[u8] {
+            fn next_n_bytes(buffer: *const u8, pos: &mut usize, n: usize) -> 
&[u8] {
                 // Safety: same as `next_four_bytes`; `*pos..*pos+n` is within 
the metadata buffer.
-                let out = unsafe {
-                    std::slice::from_raw_parts(buffer.offset(*pos), 
n.try_into().unwrap())
-                };
-                *pos += isize::try_from(n).unwrap();
+                let out = unsafe { 
std::slice::from_raw_parts(buffer.add(*pos), n) };
+                *pos += n;
                 out
             }
 
-            let num_entries = i32::from_ne_bytes(next_four_bytes(buffer, &mut 
pos));
-            if num_entries < 0 {
-                return Err(ArrowError::CDataInterface(
-                    "Negative number of metadata entries".to_string(),
-                ));
+            /// A length read from the metadata, which the producer may have 
got wrong.
+            fn checked_length(what: &str, length: i32) -> Result<usize, 
ArrowError> {
+                usize::try_from(length).map_err(|_| {
+                    ArrowError::CDataInterface(format!("Invalid {what} in 
metadata: {length}"))
+                })
             }
 
-            let mut metadata =
-                HashMap::with_capacity(num_entries.try_into().expect("Too many 
metadata entries"));
+            let num_entries = i32::from_ne_bytes(next_four_bytes(buffer, &mut 
pos));
+            let num_entries = checked_length("number of entries", 
num_entries)?;
+
+            // The count comes from the producer, so do not preallocate all of 
it
+            let mut metadata = HashMap::with_capacity(num_entries.min(128));
 
             for _ in 0..num_entries {
                 let key_length = i32::from_ne_bytes(next_four_bytes(buffer, 
&mut pos));
-                if key_length < 0 {
-                    return Err(ArrowError::CDataInterface(
-                        "Negative key length in metadata".to_string(),
-                    ));
-                }
+                let key_length = checked_length("key length", key_length)?;
                 let key = String::from_utf8(next_n_bytes(buffer, &mut pos, 
key_length).to_vec())?;
                 let value_length = i32::from_ne_bytes(next_four_bytes(buffer, 
&mut pos));
-                if value_length < 0 {
-                    return Err(ArrowError::CDataInterface(
-                        "Negative value length in metadata".to_string(),
-                    ));
-                }
+                let value_length = checked_length("value length", 
value_length)?;
                 let value =
                     String::from_utf8(next_n_bytes(buffer, &mut pos, 
value_length).to_vec())?;
                 metadata.insert(key, value);
diff --git a/arrow-select/src/dictionary.rs b/arrow-select/src/dictionary.rs
index 2f0418e1dc..80a18abb04 100644
--- a/arrow-select/src/dictionary.rs
+++ b/arrow-select/src/dictionary.rs
@@ -58,8 +58,8 @@ pub fn garbage_collect_dictionary<K: ArrowDictionaryKeyType>(
     // Create a mapping from the old keys to the new keys, use a Vec for easy 
indexing
     let mut key_remap = vec![K::Native::ZERO; values.len()];
     for (new_idx, old_idx) in mask.set_indices().enumerate() {
-        key_remap[old_idx] = K::Native::from_usize(new_idx)
-            .expect("new index should fit in K::Native, as old index was in 
range");
+        key_remap[old_idx] =
+            
K::Native::from_usize(new_idx).ok_or(ArrowError::DictionaryKeyOverflowError)?;
     }
 
     // ... and then build the new keys array
diff --git a/arrow-string/src/concat_elements.rs 
b/arrow-string/src/concat_elements.rs
index 5abe66fd05..8934f62987 100644
--- a/arrow-string/src/concat_elements.rs
+++ b/arrow-string/src/concat_elements.rs
@@ -28,6 +28,11 @@ use arrow_data::{ArrayDataBuilder, MAX_INLINE_VIEW_LEN};
 use arrow_schema::{ArrowError, DataType};
 
 /// Returns the elementwise concatenation of a [`GenericByteArray`].
+///
+/// # Errors
+///
+/// Returns an error if the arrays have different lengths, or if the 
concatenated
+/// data is too long for the offset type.
 pub fn concat_elements_bytes<T: ByteArrayType>(
     left: &GenericByteArray<T>,
     right: &GenericByteArray<T>,
@@ -61,7 +66,10 @@ pub fn concat_elements_bytes<T: ByteArrayType>(
             
.extend_from_slice(&left_values[left_idx[0].as_usize()..left_idx[1].as_usize()]);
         output_values
             
.extend_from_slice(&right_values[right_idx[0].as_usize()..right_idx[1].as_usize()]);
-        
output_offsets.push(T::Offset::from_usize(output_values.len()).unwrap());
+        let output_len = output_values.len();
+        let offset =
+            
T::Offset::from_usize(output_len).ok_or(ArrowError::OffsetOverflowError(output_len))?;
+        output_offsets.push(offset);
     }
 
     let builder = ArrayDataBuilder::new(T::DATA_TYPE)
@@ -110,6 +118,11 @@ pub fn concat_element_binary<Offset: OffsetSizeTrait>(
 /// ```
 ///
 /// An error will be returned if the [`StringArray`] are of different lengths
+///
+/// # Errors
+///
+/// Returns an error if the arrays have different lengths, or if the 
concatenated
+/// data is too long for the offset type.
 pub fn concat_elements_utf8_many<Offset: OffsetSizeTrait>(
     arrays: &[&GenericStringArray<Offset>],
 ) -> Result<GenericStringArray<Offset>, ArrowError> {
@@ -159,7 +172,10 @@ pub fn concat_elements_utf8_many<Offset: OffsetSizeTrait>(
                 let index_end = offset.peek().unwrap().as_usize();
                 
output_values.extend_from_slice(&values[index_start..index_end]);
             });
-        output_offsets.push(Offset::from_usize(output_values.len()).unwrap());
+        let output_len = output_values.len();
+        let offset =
+            
Offset::from_usize(output_len).ok_or(ArrowError::OffsetOverflowError(output_len))?;
+        output_offsets.push(offset);
     }
 
     let builder = 
ArrayDataBuilder::new(GenericStringArray::<Offset>::DATA_TYPE)
diff --git a/parquet/src/arrow/arrow_reader/mod.rs 
b/parquet/src/arrow/arrow_reader/mod.rs
index ab3e015455..fed4541e14 100644
--- a/parquet/src/arrow/arrow_reader/mod.rs
+++ b/parquet/src/arrow/arrow_reader/mod.rs
@@ -1300,10 +1300,15 @@ impl<T: ChunkReader + 'static> 
ParquetRecordBatchReaderBuilder<T> {
         }
 
         let bitset = match column_metadata.bloom_filter_length() {
-            Some(_) => buffer.slice(
-                (TryInto::<usize>::try_into(bitset_offset).unwrap()
-                    - TryInto::<usize>::try_into(offset).unwrap())..,
-            ),
+            Some(_) => {
+                let bitset_start = bitset_offset
+                    .checked_sub(offset)
+                    .and_then(|start| usize::try_from(start).ok())
+                    .ok_or_else(|| {
+                        ParquetError::General("Bloom filter offset is 
invalid".to_string())
+                    })?;
+                buffer.slice(bitset_start..)
+            }
             None => {
                 let bitset_length: usize = 
header.num_bytes.try_into().map_err(|_| {
                     ParquetError::General("Bloom filter length is 
invalid".to_string())
diff --git a/parquet/src/arrow/arrow_writer/mod.rs 
b/parquet/src/arrow/arrow_writer/mod.rs
index ac38a11f10..267a52d4c9 100644
--- a/parquet/src/arrow/arrow_writer/mod.rs
+++ b/parquet/src/arrow/arrow_writer/mod.rs
@@ -1186,6 +1186,11 @@ impl ArrowColumnWriter {
     }
 
     /// Close this column returning the written [`ArrowColumnChunk`]
+    ///
+    /// # Errors
+    ///
+    /// Returns an error if the column could not be finalised, or if another 
thread
+    /// panicked while holding the column chunk. The caller cannot cause 
either.
     pub fn close(self) -> Result<ArrowColumnChunk> {
         let distinct_count = self
             .distinct_values_seen
@@ -1206,8 +1211,12 @@ impl ArrowColumnWriter {
                 c.close()?
             }
         };
-        let chunk = Arc::try_unwrap(self.chunk).ok().unwrap();
-        let data = chunk.into_inner().unwrap();
+        // Closing the writer above dropped the only other handle on the chunk.
+        let chunk = Arc::try_unwrap(self.chunk)
+            .map_err(|_| general_err!("Internal Error: the column chunk is 
still shared"))?;
+        let data = chunk
+            .into_inner()
+            .map_err(|_| general_err!("The column chunk lock is poisoned"))?;
         Ok(ArrowColumnChunk { data, close })
     }
 
diff --git a/parquet/src/arrow/async_reader/mod.rs 
b/parquet/src/arrow/async_reader/mod.rs
index 3eddd54935..16cbea14c2 100644
--- a/parquet/src/arrow/async_reader/mod.rs
+++ b/parquet/src/arrow/async_reader/mod.rs
@@ -630,10 +630,15 @@ impl<T: AsyncFileReader + Send + 'static> 
ParquetRecordBatchStreamBuilder<T> {
         }
 
         let bitset = match column_metadata.bloom_filter_length() {
-            Some(_) => buffer.slice(
-                (TryInto::<usize>::try_into(bitset_offset).unwrap()
-                    - TryInto::<usize>::try_into(offset).unwrap())..,
-            ),
+            Some(_) => {
+                let bitset_start = bitset_offset
+                    .checked_sub(offset)
+                    .and_then(|start| usize::try_from(start).ok())
+                    .ok_or_else(|| {
+                        ParquetError::General("Bloom filter offset is 
invalid".to_string())
+                    })?;
+                buffer.slice(bitset_start..)
+            }
             None => {
                 let bitset_length: u64 = 
header.num_bytes.try_into().map_err(|_| {
                     ParquetError::General("Bloom filter length is 
invalid".to_string())
diff --git a/parquet/src/column/reader.rs b/parquet/src/column/reader.rs
index 42e8ccdc7a..9c2345b097 100644
--- a/parquet/src/column/reader.rs
+++ b/parquet/src/column/reader.rs
@@ -254,9 +254,7 @@ where
                         ));
                     }
                     if levels_read == remaining_levels && 
self.has_record_delimiter {
-                        // Reached end of page, which implies records_read < 
remaining_records
-                        // as otherwise would have stopped reading before 
reaching the end
-                        assert!(records_read < remaining_records); // Sanity 
check
+                        check_partial_record_fits(records_read, 
remaining_records)?;
                         records_read += reader.flush_partial() as usize;
                     }
                     (records_read, levels_read)
@@ -359,9 +357,7 @@ where
                         decoder.skip_rep_levels(remaining_records, 
remaining_levels)?;
 
                     if levels_read == remaining_levels && 
self.has_record_delimiter {
-                        // Reached end of page, which implies records_read < 
remaining_records
-                        // as otherwise would have stopped reading before 
reaching the end
-                        assert!(records_read < remaining_records); // Sanity 
check
+                        check_partial_record_fits(records_read, 
remaining_records)?;
                         records_read += decoder.flush_partial() as usize;
                     }
 
@@ -585,6 +581,22 @@ where
     }
 }
 
+/// Checks that a partial record can still be flushed into the caller's record 
budget.
+///
+/// Reaching the end of a page with a record delimiter means the decoder 
stopped because
+/// it ran out of levels, not records, so it cannot have used up the whole 
budget. A page
+/// whose repetition levels disagree with its record count breaks that, and 
flushing the
+/// partial record would then take the count past what was asked for.
+fn check_partial_record_fits(records_read: usize, remaining_records: usize) -> 
Result<()> {
+    if remaining_records <= records_read {
+        return Err(general_err!(
+            "page ended after {records_read} record(s), which is already all 
of the \
+             {remaining_records} record(s) asked for, so there is no partial 
record to flush"
+        ));
+    }
+    Ok(())
+}
+
 fn parse_v1_level(
     max_level: i16,
     num_buffered_values: u32,
diff --git a/parquet/src/file/writer.rs b/parquet/src/file/writer.rs
index 62c1157b7f..4b7a12c274 100644
--- a/parquet/src/file/writer.rs
+++ b/parquet/src/file/writer.rs
@@ -260,7 +260,7 @@ impl<W: Write + Send> SerializedFileWriter<W> {
         self.row_group_index = self
             .row_group_index
             .checked_add(1)
-            .expect("SerializedFileWriter::row_group_index overflowed");
+            .ok_or_else(|| ParquetError::General("Row group index 
overflowed".to_string()))?;
 
         let bloom_filter_position = self.properties().bloom_filter_position();
         let row_groups = &mut self.row_groups;

Reply via email to