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 76326e8377 fix(arrow-ipc): return an error instead of panicking on 
malformed IPC schema (#10647)
76326e8377 is described below

commit 76326e837767957f001b198527a5bd8d38e0fc56
Author: Aditya Mishra <[email protected]>
AuthorDate: Fri Aug 14 12:43:40 2026 +0530

    fix(arrow-ipc): return an error instead of panicking on malformed IPC 
schema (#10647)
    
    # Which issue does this PR close?
    
    - Closes #10575.
    - Closes #10437.
    
    # Rationale for this change
    
    `arrow_ipc::convert::fb_to_schema` is infallible and panics on schema
    messages that the flatbuffer verifier itself accepts. the first thing it
    does is `fb.fields().unwrap()`, and `fields` is optional in the
    flatbuffer schema, so a message with no fields aborts the process.
    `get_data_type` has the same problem deeper down: about 20 more
    `panic!`/`unimplemented!` and 30 `unwrap()` reachable from decoder input
    (unknown float precision, `Type NONE`, out of range enum tags, and so
    on).
    
    every reader path funnels through `fb_to_schema`, so there was no way to
    read Arrow IPC without exposing the process to an abort on untrusted
    input. it also reaches parquet (an untrusted `.parquet` whose footer
    carries the `ARROW:schema` key) and arrow-flight.
    `StreamReader::try_new` returns `Result`, so malformed input should be
    an `Err`, not a process abort.
    
    # What changes are included in this PR?
    
    - `get_data_type` (already `pub(crate)`) now returns `Result<DataType,
    ArrowError>`; every input reachable `unwrap`/`panic!`/`unimplemented!`
    is now a returned `ParseError`.
    - new `pub fn try_fb_to_schema` and a private fallible field conversion.
    the readers in arrow-ipc, the parquet `ARROW:schema` path, and
    arrow-flight are switched onto it.
    - `fb_to_schema` and the public `From<crate::Field>` impl keep their
    signatures for compatibility and delegate to the fallible path.
    
    # Are these changes tested?
    
    yes. a schema message with no fields, one with an unknown float
    precision, and one with `Type NONE` now return `Err` instead of
    aborting, asserted by new tests in `arrow-ipc`. the flatbuffer verifier
    accepts all three, so they exercise the exact gap. existing round trip
    tests still pass.
    
    # Are there any user-facing changes?
    
    no breaking changes. `try_fb_to_schema` is additive, and `fb_to_schema`
    still exists with the same signature.
---
 arrow-flight/src/sql/client.rs                     |   4 +-
 arrow-flight/src/utils.rs                          |   4 +-
 .../flight_client_scenarios/integration_test.rs    |   2 +-
 arrow-ipc/benches/ipc_reader.rs                    |   4 +-
 arrow-ipc/src/convert.rs                           | 385 +++++++++++++++------
 arrow-ipc/src/reader.rs                            |  16 +-
 arrow-ipc/src/reader/stream.rs                     |   8 +-
 arrow-ipc/src/writer.rs                            |   6 +-
 arrow/examples/zero_copy_ipc.rs                    |   4 +-
 parquet/src/arrow/schema/mod.rs                    |   6 +-
 10 files changed, 305 insertions(+), 134 deletions(-)

diff --git a/arrow-flight/src/sql/client.rs b/arrow-flight/src/sql/client.rs
index 8ef80da50b..6ea1a04fc1 100644
--- a/arrow-flight/src/sql/client.rs
+++ b/arrow-flight/src/sql/client.rs
@@ -19,7 +19,7 @@
 
 use arrow_buffer::Buffer;
 use arrow_ipc::MessageHeader;
-use arrow_ipc::convert::fb_to_schema;
+use arrow_ipc::convert::try_fb_to_schema;
 use arrow_ipc::reader::read_record_batch;
 use arrow_ipc::root_as_message;
 use arrow_schema::SchemaRef;
@@ -664,7 +664,7 @@ pub fn arrow_data_from_flight_data(
                 )
             })?;
 
-            let arrow_schema = fb_to_schema(ipc_schema);
+            let arrow_schema = try_fb_to_schema(ipc_schema)?;
             Ok(ArrowFlightData::Schema(arrow_schema))
         }
         MessageHeader::DictionaryBatch => {
diff --git a/arrow-flight/src/utils.rs b/arrow-flight/src/utils.rs
index 07a1801914..0cc55ee75d 100644
--- a/arrow-flight/src/utils.rs
+++ b/arrow-flight/src/utils.rs
@@ -23,7 +23,7 @@ use std::sync::Arc;
 
 use arrow_array::{ArrayRef, RecordBatch};
 use arrow_buffer::Buffer;
-use arrow_ipc::convert::fb_to_schema;
+use arrow_ipc::convert::try_fb_to_schema;
 use arrow_ipc::writer::IpcWriteContext;
 use arrow_ipc::{reader, root_as_message, writer, writer::IpcWriteOptions};
 use arrow_schema::{ArrowError, Schema, SchemaRef};
@@ -39,7 +39,7 @@ pub fn flight_data_to_batches(flight_data: &[FlightData]) -> 
Result<Vec<RecordBa
     let ipc_schema: arrow_ipc::Schema = message
         .header_as_schema()
         .ok_or_else(|| ArrowError::CastError("Cannot get header as 
Schema".to_string()))?;
-    let schema = fb_to_schema(ipc_schema);
+    let schema = try_fb_to_schema(ipc_schema)?;
     let schema = Arc::new(schema);
 
     let mut batches = vec![];
diff --git 
a/arrow-integration-testing/src/flight_client_scenarios/integration_test.rs 
b/arrow-integration-testing/src/flight_client_scenarios/integration_test.rs
index 950a64562e..559ede93d1 100644
--- a/arrow-integration-testing/src/flight_client_scenarios/integration_test.rs
+++ b/arrow-integration-testing/src/flight_client_scenarios/integration_test.rs
@@ -286,7 +286,7 @@ async fn receive_schema_flight_data(resp: &mut 
Streaming<FlightData>) -> Option<
     let ipc_schema: ipc::Schema = message
         .header_as_schema()
         .expect("Unable to read IPC message as schema");
-    let schema = ipc::convert::fb_to_schema(ipc_schema);
+    let schema = ipc::convert::try_fb_to_schema(ipc_schema).unwrap();
 
     Some(schema)
 }
diff --git a/arrow-ipc/benches/ipc_reader.rs b/arrow-ipc/benches/ipc_reader.rs
index ef1de88d32..5d7c373ee5 100644
--- a/arrow-ipc/benches/ipc_reader.rs
+++ b/arrow-ipc/benches/ipc_reader.rs
@@ -18,7 +18,7 @@
 use arrow_array::builder::{Date32Builder, Decimal128Builder, Int32Builder};
 use arrow_array::{RecordBatch, builder::StringBuilder};
 use arrow_buffer::Buffer;
-use arrow_ipc::convert::fb_to_schema;
+use arrow_ipc::convert::try_fb_to_schema;
 use arrow_ipc::reader::{FileDecoder, FileReader, StreamReader, 
read_footer_length};
 use arrow_ipc::writer::{FileWriter, IpcWriteOptions, StreamWriter};
 use arrow_ipc::{Block, CompressionType, root_as_footer};
@@ -215,7 +215,7 @@ impl IPCBufferDecoder {
         let footer_len = 
read_footer_length(buffer[trailer_start..].try_into().unwrap()).unwrap();
         let footer = root_as_footer(&buffer[trailer_start - 
footer_len..trailer_start]).unwrap();
 
-        let schema = fb_to_schema(footer.schema().unwrap());
+        let schema = try_fb_to_schema(footer.schema().unwrap()).unwrap();
 
         let mut decoder = FileDecoder::new(Arc::new(schema), footer.version());
 
diff --git a/arrow-ipc/src/convert.rs b/arrow-ipc/src/convert.rs
index 6429f76f8a..7e1fc962f4 100644
--- a/arrow-ipc/src/convert.rs
+++ b/arrow-ipc/src/convert.rs
@@ -34,11 +34,11 @@ use DataType::*;
 
 /// Low level Arrow [Schema] to IPC bytes converter
 ///
-/// See also [`fb_to_schema`] for the reverse operation
+/// See also [`try_fb_to_schema`] for the reverse operation
 ///
 /// # Example
 /// ```
-/// # use arrow_ipc::convert::{fb_to_schema, IpcSchemaEncoder};
+/// # use arrow_ipc::convert::{try_fb_to_schema, IpcSchemaEncoder};
 /// # use arrow_ipc::root_as_schema;
 /// # use arrow_ipc::writer::DictionaryTracker;
 /// # use arrow_schema::{DataType, Field, Schema};
@@ -59,7 +59,7 @@ use DataType::*;
 ///
 ///  // convert the IPC bytes back to an Arrow schema
 ///  let ipc_schema = root_as_schema(ipc_bytes).unwrap();
-///  let schema2 = fb_to_schema(ipc_schema);
+///  let schema2 = try_fb_to_schema(ipc_schema).unwrap();
 /// assert_eq!(schema, schema2);
 /// ```
 #[derive(Debug)]
@@ -158,52 +158,73 @@ pub fn schema_to_fb_offset<'a>(
 }
 
 /// Convert an IPC Field to Arrow Field
+///
+/// This panics on malformed input; every reader path uses the fallible
+/// conversion instead. kept for backwards compatibility only.
 impl From<crate::Field<'_>> for Field {
     fn from(field: crate::Field) -> Field {
-        let arrow_field = if let Some(dictionary) = field.dictionary() {
-            #[allow(deprecated)]
-            Field::new_dict(
-                field.name().unwrap_or_default(),
-                get_data_type(field, true),
-                field.nullable(),
-                dictionary.id(),
-                dictionary.isOrdered(),
-            )
-        } else {
-            Field::new(
-                field.name().unwrap_or_default(),
-                get_data_type(field, true),
-                field.nullable(),
-            )
-        };
+        try_field_from(field).expect("invalid IPC field")
+    }
+}
 
-        let mut metadata_map = HashMap::default();
-        if let Some(list) = field.custom_metadata() {
-            for kv in list {
-                if let (Some(k), Some(v)) = (kv.key(), kv.value()) {
-                    metadata_map.insert(k.to_string(), v.to_string());
-                }
+/// Convert an IPC Field to Arrow Field
+fn try_field_from(field: crate::Field) -> Result<Field, ArrowError> {
+    let arrow_field = if let Some(dictionary) = field.dictionary() {
+        #[allow(deprecated)]
+        Field::new_dict(
+            field.name().unwrap_or_default(),
+            get_data_type(field, true)?,
+            field.nullable(),
+            dictionary.id(),
+            dictionary.isOrdered(),
+        )
+    } else {
+        Field::new(
+            field.name().unwrap_or_default(),
+            get_data_type(field, true)?,
+            field.nullable(),
+        )
+    };
+
+    let mut metadata_map = HashMap::default();
+    if let Some(list) = field.custom_metadata() {
+        for kv in list {
+            if let (Some(k), Some(v)) = (kv.key(), kv.value()) {
+                metadata_map.insert(k.to_string(), v.to_string());
             }
         }
-
-        arrow_field.with_metadata(metadata_map)
     }
+
+    Ok(arrow_field.with_metadata(metadata_map))
 }
 
 /// Deserialize an ipc [`crate::Schema`] from flat buffers to an arrow 
[Schema].
+#[deprecated(since = "60.0.0", note = "Use `try_fb_to_schema` instead")]
 pub fn fb_to_schema(fb: crate::Schema) -> Schema {
+    try_fb_to_schema(fb).expect("invalid IPC schema")
+}
+
+/// Deserialize an ipc [`crate::Schema`] from flat buffers to an arrow 
[Schema].
+///
+/// Returns an error on schema messages that the flatbuffer
+/// verifier accepts but that are not valid Arrow.
+pub fn try_fb_to_schema(fb: crate::Schema) -> Result<Schema, ArrowError> {
     let mut fields: Vec<Field> = vec![];
-    let c_fields = fb.fields().unwrap();
+    let c_fields = fb
+        .fields()
+        .ok_or_else(|| ArrowError::ParseError("IPC schema has no 
fields".to_string()))?;
     let len = c_fields.len();
     for i in 0..len {
         let c_field: crate::Field = c_fields.get(i);
         match c_field.type_type() {
             crate::Type::Decimal if fb.endianness() == crate::Endianness::Big 
=> {
-                unimplemented!("Big Endian is not supported for Decimal!")
+                return Err(ArrowError::ParseError(
+                    "Big Endian is not supported for Decimal!".to_string(),
+                ));
             }
             _ => (),
         };
-        fields.push(c_field.into());
+        fields.push(try_field_from(c_field)?);
     }
 
     let mut metadata: HashMap<String, String> = HashMap::default();
@@ -220,14 +241,14 @@ pub fn fb_to_schema(fb: crate::Schema) -> Schema {
             }
         }
     }
-    Schema::new_with_metadata(fields, metadata)
+    Ok(Schema::new_with_metadata(fields, metadata))
 }
 
 /// Try deserialize flat buffer format bytes into a schema
 pub fn try_schema_from_flatbuffer_bytes(bytes: &[u8]) -> Result<Schema, 
ArrowError> {
     if let Ok(ipc) = crate::root_as_message(bytes) {
-        if let Some(schema) = ipc.header_as_schema().map(fb_to_schema) {
-            Ok(schema)
+        if let Some(schema) = ipc.header_as_schema() {
+            try_fb_to_schema(schema)
         } else {
             Err(ArrowError::ParseError(
                 "Unable to get head as schema".to_string(),
@@ -287,15 +308,27 @@ pub fn try_schema_from_ipc_buffer(buffer: &[u8]) -> 
Result<Schema, ArrowError> {
     let ipc_schema = msg.header_as_schema().ok_or_else(|| {
         ArrowError::ParseError("Unable to convert flight info to a 
schema".to_string())
     })?;
-    Ok(fb_to_schema(ipc_schema))
+    try_fb_to_schema(ipc_schema)
 }
 
 /// Get the Arrow data type from the flatbuffer Field table
-pub(crate) fn get_data_type(field: crate::Field, may_be_dictionary: bool) -> 
DataType {
+pub(crate) fn get_data_type(
+    field: crate::Field,
+    may_be_dictionary: bool,
+) -> Result<DataType, ArrowError> {
+    // helper: the flatbuffer verifier accepts a Field whose `type_type` tag 
does
+    // not match the union value present, so every `type_as_*` accessor can 
return
+    // None. surface that as an error instead of unwrapping.
+    fn type_err(what: &str) -> ArrowError {
+        ArrowError::ParseError(format!("IPC schema field is missing its {what} 
type"))
+    }
+
     if let Some(dictionary) = field.dictionary()
         && may_be_dictionary
     {
-        let int = dictionary.indexType().unwrap();
+        let int = dictionary
+            .indexType()
+            .ok_or_else(|| type_err("dictionary index"))?;
         let index_type = match (int.bitWidth(), int.is_signed()) {
             (8, true) => DataType::Int8,
             (8, false) => DataType::UInt8,
@@ -305,16 +338,25 @@ pub(crate) fn get_data_type(field: crate::Field, 
may_be_dictionary: bool) -> Dat
             (32, false) => DataType::UInt32,
             (64, true) => DataType::Int64,
             (64, false) => DataType::UInt64,
-            _ => panic!("Unexpected bitwidth and signed"),
+            _ => {
+                return Err(ArrowError::ParseError(format!(
+                    "Index type with bit width of {} and signed of {} not 
supported",
+                    int.bitWidth(),
+                    int.is_signed()
+                )));
+            }
         };
-        return DataType::Dictionary(Box::new(index_type), 
Box::new(get_data_type(field, false)));
+        return Ok(DataType::Dictionary(
+            Box::new(index_type),
+            Box::new(get_data_type(field, false)?),
+        ));
     }
 
-    match field.type_type() {
+    let data_type = match field.type_type() {
         crate::Type::Null => DataType::Null,
         crate::Type::Bool => DataType::Boolean,
         crate::Type::Int => {
-            let int = field.type_as_int().unwrap();
+            let int = field.type_as_int().ok_or_else(|| type_err("int"))?;
             match (int.bitWidth(), int.is_signed()) {
                 (8, true) => DataType::Int8,
                 (8, false) => DataType::UInt8,
@@ -324,10 +366,12 @@ pub(crate) fn get_data_type(field: crate::Field, 
may_be_dictionary: bool) -> Dat
                 (32, false) => DataType::UInt32,
                 (64, true) => DataType::Int64,
                 (64, false) => DataType::UInt64,
-                z => panic!(
-                    "Int type with bit width of {} and signed of {} not 
supported",
-                    z.0, z.1
-                ),
+                z => {
+                    return Err(ArrowError::ParseError(format!(
+                        "Int type with bit width of {} and signed of {} not 
supported",
+                        z.0, z.1
+                    )));
+                }
             }
         }
         crate::Type::Binary => DataType::Binary,
@@ -337,41 +381,57 @@ pub(crate) fn get_data_type(field: crate::Field, 
may_be_dictionary: bool) -> Dat
         crate::Type::Utf8View => DataType::Utf8View,
         crate::Type::LargeUtf8 => DataType::LargeUtf8,
         crate::Type::FixedSizeBinary => {
-            let fsb = field.type_as_fixed_size_binary().unwrap();
+            let fsb = field
+                .type_as_fixed_size_binary()
+                .ok_or_else(|| type_err("fixed size binary"))?;
             DataType::FixedSizeBinary(fsb.byteWidth())
         }
         crate::Type::FloatingPoint => {
-            let float = field.type_as_floating_point().unwrap();
+            let float = field
+                .type_as_floating_point()
+                .ok_or_else(|| type_err("floating point"))?;
             match float.precision() {
                 crate::Precision::HALF => DataType::Float16,
                 crate::Precision::SINGLE => DataType::Float32,
                 crate::Precision::DOUBLE => DataType::Float64,
-                z => panic!("FloatingPoint type with precision of {z:?} not 
supported"),
+                z => {
+                    return Err(ArrowError::ParseError(format!(
+                        "FloatingPoint type with precision of {z:?} not 
supported"
+                    )));
+                }
             }
         }
         crate::Type::Date => {
-            let date = field.type_as_date().unwrap();
+            let date = field.type_as_date().ok_or_else(|| type_err("date"))?;
             match date.unit() {
                 crate::DateUnit::DAY => DataType::Date32,
                 crate::DateUnit::MILLISECOND => DataType::Date64,
-                z => panic!("Date type with unit of {z:?} not supported"),
+                z => {
+                    return Err(ArrowError::ParseError(format!(
+                        "Date type with unit of {z:?} not supported"
+                    )));
+                }
             }
         }
         crate::Type::Time => {
-            let time = field.type_as_time().unwrap();
+            let time = field.type_as_time().ok_or_else(|| type_err("time"))?;
             match (time.bitWidth(), time.unit()) {
                 (32, crate::TimeUnit::SECOND) => 
DataType::Time32(TimeUnit::Second),
                 (32, crate::TimeUnit::MILLISECOND) => 
DataType::Time32(TimeUnit::Millisecond),
                 (64, crate::TimeUnit::MICROSECOND) => 
DataType::Time64(TimeUnit::Microsecond),
                 (64, crate::TimeUnit::NANOSECOND) => 
DataType::Time64(TimeUnit::Nanosecond),
-                z => panic!(
-                    "Time type with bit width of {} and unit of {:?} not 
supported",
-                    z.0, z.1
-                ),
+                z => {
+                    return Err(ArrowError::ParseError(format!(
+                        "Time type with bit width of {} and unit of {:?} not 
supported",
+                        z.0, z.1
+                    )));
+                }
             }
         }
         crate::Type::Timestamp => {
-            let timestamp = field.type_as_timestamp().unwrap();
+            let timestamp = field
+                .type_as_timestamp()
+                .ok_or_else(|| type_err("timestamp"))?;
             let timezone: Option<_> = timestamp.timezone().map(|tz| tz.into());
             match timestamp.unit() {
                 crate::TimeUnit::SECOND => 
DataType::Timestamp(TimeUnit::Second, timezone),
@@ -382,132 +442,164 @@ pub(crate) fn get_data_type(field: crate::Field, 
may_be_dictionary: bool) -> Dat
                     DataType::Timestamp(TimeUnit::Microsecond, timezone)
                 }
                 crate::TimeUnit::NANOSECOND => 
DataType::Timestamp(TimeUnit::Nanosecond, timezone),
-                z => panic!("Timestamp type with unit of {z:?} not supported"),
+                z => {
+                    return Err(ArrowError::ParseError(format!(
+                        "Timestamp type with unit of {z:?} not supported"
+                    )));
+                }
             }
         }
         crate::Type::Interval => {
-            let interval = field.type_as_interval().unwrap();
+            let interval = field
+                .type_as_interval()
+                .ok_or_else(|| type_err("interval"))?;
             match interval.unit() {
                 crate::IntervalUnit::YEAR_MONTH => 
DataType::Interval(IntervalUnit::YearMonth),
                 crate::IntervalUnit::DAY_TIME => 
DataType::Interval(IntervalUnit::DayTime),
                 crate::IntervalUnit::MONTH_DAY_NANO => {
                     DataType::Interval(IntervalUnit::MonthDayNano)
                 }
-                z => panic!("Interval type with unit of {z:?} unsupported"),
+                z => {
+                    return Err(ArrowError::ParseError(format!(
+                        "Interval type with unit of {z:?} unsupported"
+                    )));
+                }
             }
         }
         crate::Type::Duration => {
-            let duration = field.type_as_duration().unwrap();
+            let duration = field
+                .type_as_duration()
+                .ok_or_else(|| type_err("duration"))?;
             match duration.unit() {
                 crate::TimeUnit::SECOND => 
DataType::Duration(TimeUnit::Second),
                 crate::TimeUnit::MILLISECOND => 
DataType::Duration(TimeUnit::Millisecond),
                 crate::TimeUnit::MICROSECOND => 
DataType::Duration(TimeUnit::Microsecond),
                 crate::TimeUnit::NANOSECOND => 
DataType::Duration(TimeUnit::Nanosecond),
-                z => panic!("Duration type with unit of {z:?} unsupported"),
+                z => {
+                    return Err(ArrowError::ParseError(format!(
+                        "Duration type with unit of {z:?} unsupported"
+                    )));
+                }
             }
         }
         crate::Type::List => {
-            let children = field.children().unwrap();
-            if children.len() != 1 {
-                panic!("expect a list to have one child")
-            }
-            DataType::List(Arc::new(children.get(0).into()))
+            let child = single_child(&field, "list")?;
+            DataType::List(Arc::new(try_field_from(child)?))
         }
         crate::Type::LargeList => {
-            let children = field.children().unwrap();
-            if children.len() != 1 {
-                panic!("expect a large list to have one child")
-            }
-            DataType::LargeList(Arc::new(children.get(0).into()))
+            let child = single_child(&field, "large list")?;
+            DataType::LargeList(Arc::new(try_field_from(child)?))
         }
         crate::Type::ListView => {
-            let children = field.children().unwrap();
-            if children.len() != 1 {
-                panic!("expect a listview to have one child")
-            }
-            DataType::ListView(Arc::new(children.get(0).into()))
+            let child = single_child(&field, "listview")?;
+            DataType::ListView(Arc::new(try_field_from(child)?))
         }
         crate::Type::LargeListView => {
-            let children = field.children().unwrap();
-            if children.len() != 1 {
-                panic!("expect a large listview to have one child")
-            }
-            DataType::LargeListView(Arc::new(children.get(0).into()))
+            let child = single_child(&field, "large listview")?;
+            DataType::LargeListView(Arc::new(try_field_from(child)?))
         }
         crate::Type::FixedSizeList => {
-            let children = field.children().unwrap();
-            if children.len() != 1 {
-                panic!("expect a list to have one child")
-            }
-            let fsl = field.type_as_fixed_size_list().unwrap();
-            DataType::FixedSizeList(Arc::new(children.get(0).into()), 
fsl.listSize())
+            let child = single_child(&field, "list")?;
+            let fsl = field
+                .type_as_fixed_size_list()
+                .ok_or_else(|| type_err("fixed size list"))?;
+            DataType::FixedSizeList(Arc::new(try_field_from(child)?), 
fsl.listSize())
         }
         crate::Type::Struct_ => {
             let fields = match field.children() {
-                Some(children) => children.iter().map(Field::from).collect(),
+                Some(children) => children
+                    .iter()
+                    .map(try_field_from)
+                    .collect::<Result<Vec<_>, _>>()?
+                    .into(),
                 None => Fields::empty(),
             };
             DataType::Struct(fields)
         }
         crate::Type::RunEndEncoded => {
-            let children = field.children().unwrap();
+            let children = field
+                .children()
+                .ok_or_else(|| ArrowError::ParseError("IPC list has no 
children".to_string()))?;
             if children.len() != 2 {
-                panic!(
+                return Err(ArrowError::ParseError(format!(
                     "RunEndEncoded type should have exactly two children. 
Found {}",
                     children.len()
-                )
+                )));
             }
-            let run_ends_field = children.get(0).into();
-            let values_field = children.get(1).into();
+            let run_ends_field = try_field_from(children.get(0))?;
+            let values_field = try_field_from(children.get(1))?;
             DataType::RunEndEncoded(Arc::new(run_ends_field), 
Arc::new(values_field))
         }
         crate::Type::Map => {
-            let map = field.type_as_map().unwrap();
-            let children = field.children().unwrap();
-            if children.len() != 1 {
-                panic!("expect a map to have one child")
-            }
-            DataType::Map(Arc::new(children.get(0).into()), map.keysSorted())
+            let map = field.type_as_map().ok_or_else(|| type_err("map"))?;
+            let child = single_child(&field, "map")?;
+            DataType::Map(Arc::new(try_field_from(child)?), map.keysSorted())
         }
         crate::Type::Decimal => {
-            let fsb = field.type_as_decimal().unwrap();
+            let fsb = field.type_as_decimal().ok_or_else(|| 
type_err("decimal"))?;
             let bit_width = fsb.bitWidth();
-            let precision: u8 = fsb.precision().try_into().unwrap();
-            let scale: i8 = fsb.scale().try_into().unwrap();
+            let precision: u8 = fsb.precision().try_into().map_err(|_| {
+                ArrowError::ParseError(format!("Invalid decimal precision {}", 
fsb.precision()))
+            })?;
+            let scale: i8 = fsb.scale().try_into().map_err(|_| {
+                ArrowError::ParseError(format!("Invalid decimal scale {}", 
fsb.scale()))
+            })?;
             match bit_width {
                 32 => DataType::Decimal32(precision, scale),
                 64 => DataType::Decimal64(precision, scale),
                 128 => DataType::Decimal128(precision, scale),
                 256 => DataType::Decimal256(precision, scale),
-                _ => panic!("Unexpected decimal bit width {bit_width}"),
+                _ => {
+                    return Err(ArrowError::ParseError(format!(
+                        "Unexpected decimal bit width {bit_width}"
+                    )));
+                }
             }
         }
         crate::Type::Union => {
-            let union = field.type_as_union().unwrap();
+            let union = field.type_as_union().ok_or_else(|| 
type_err("union"))?;
 
             let union_mode = match union.mode() {
                 crate::UnionMode::Dense => UnionMode::Dense,
                 crate::UnionMode::Sparse => UnionMode::Sparse,
-                mode => panic!("Unexpected union mode: {mode:?}"),
+                mode => {
+                    return Err(ArrowError::ParseError(format!(
+                        "Unexpected union mode: {mode:?}"
+                    )));
+                }
             };
 
             let mut fields = vec![];
             if let Some(children) = field.children() {
                 for i in 0..children.len() {
-                    fields.push(Field::from(children.get(i)));
+                    fields.push(try_field_from(children.get(i))?);
                 }
             };
 
             let fields = match union.typeIds() {
                 None => UnionFields::from_fields(fields),
-                Some(ids) => UnionFields::try_new(ids.iter().map(|i| i as i8), 
fields)
-                    .expect("invalid union field"),
+                Some(ids) => UnionFields::try_new(ids.iter().map(|i| i as i8), 
fields)?,
             };
 
             DataType::Union(fields, union_mode)
         }
-        t => unimplemented!("Type {:?} not supported", t),
+        t => return Err(ArrowError::ParseError(format!("Type {t:?} not 
supported"))),
+    };
+    Ok(data_type)
+}
+
+/// Return the single child of a flatbuffer Field, erroring if it does not have
+/// exactly one.
+fn single_child<'a>(field: &crate::Field<'a>, kind: &str) -> 
Result<crate::Field<'a>, ArrowError> {
+    let children = field
+        .children()
+        .ok_or_else(|| ArrowError::ParseError(format!("IPC {kind} has no 
children")))?;
+    if children.len() != 1 {
+        return Err(ArrowError::ParseError(format!(
+            "expect a {kind} to have one child"
+        )));
     }
+    Ok(children.get(0))
 }
 
 pub(crate) struct FBFieldType<'b> {
@@ -1002,6 +1094,76 @@ impl MessageBuffer {
 mod tests {
     use super::*;
 
+    /// Build a schema flatbuffer with no `fields` vector, using `build_field` 
to
+    /// optionally add a single field. Returns bytes that the flatbuffer 
verifier
+    /// accepts, so `try_fb_to_schema` is the layer that must reject them.
+    fn schema_bytes_without_fields() -> Vec<u8> {
+        let mut fbb = FlatBufferBuilder::new();
+        // Schema table with `fields` left unset -> fb.fields() == None.
+        let schema = crate::SchemaBuilder::new(&mut fbb).finish();
+        fbb.finish(schema, None);
+        fbb.finished_data().to_vec()
+    }
+
+    /// Finish `fbb` as a schema holding the single `field`, assert the 
verifier
+    /// accepts the bytes but `try_fb_to_schema` rejects them (rather than 
panics).
+    fn assert_single_field_schema_is_err<'a>(
+        mut fbb: FlatBufferBuilder<'a>,
+        field: WIPOffset<crate::Field<'a>>,
+    ) {
+        let fields = fbb.create_vector(&[field]);
+        let schema = {
+            let mut b = crate::SchemaBuilder::new(&mut fbb);
+            b.add_fields(fields);
+            b.finish()
+        };
+        fbb.finish(schema, None);
+        let fb = crate::root_as_schema(fbb.finished_data()).expect("verifier 
should accept");
+        assert!(try_fb_to_schema(fb).is_err());
+    }
+
+    #[test]
+    fn try_fb_to_schema_missing_fields_is_err() {
+        let bytes = schema_bytes_without_fields();
+        // the verifier accepts it...
+        let fb = crate::root_as_schema(&bytes).expect("verifier should 
accept");
+        // ...but conversion must return Err, not panic.
+        assert!(try_fb_to_schema(fb).is_err());
+    }
+
+    #[test]
+    fn try_fb_to_schema_unknown_float_precision_is_err() {
+        let mut fbb = FlatBufferBuilder::new();
+        let name = fbb.create_string("f");
+        let float = {
+            let mut b = crate::FloatingPointBuilder::new(&mut fbb);
+            // a precision the verifier does not range-check.
+            b.add_precision(crate::Precision(99));
+            b.finish()
+        };
+        let field = {
+            let mut b = crate::FieldBuilder::new(&mut fbb);
+            b.add_name(name);
+            b.add_type_type(crate::Type::FloatingPoint);
+            b.add_type_(float.as_union_value());
+            b.finish()
+        };
+        assert_single_field_schema_is_err(fbb, field);
+    }
+
+    #[test]
+    fn try_fb_to_schema_type_none_is_err() {
+        let mut fbb = FlatBufferBuilder::new();
+        let name = fbb.create_string("f");
+        let field = {
+            let mut b = crate::FieldBuilder::new(&mut fbb);
+            b.add_name(name);
+            // type_type left as the union default (Type::NONE).
+            b.finish()
+        };
+        assert_single_field_schema_is_err(fbb, field);
+    }
+
     #[test]
     fn convert_schema_round_trip() {
         let md: HashMap<String, String> = [("Key".to_string(), 
"value".to_string())]
@@ -1218,7 +1380,7 @@ mod tests {
 
         // read back fields
         let ipc = crate::root_as_schema(fb.finished_data()).unwrap();
-        let schema2 = fb_to_schema(ipc);
+        let schema2 = try_fb_to_schema(ipc).unwrap();
         assert_eq!(schema, schema2);
     }
 
@@ -1264,7 +1426,10 @@ mod tests {
         assert_eq!(schema.endianness(), schema2.endianness());
         assert!(schema.features().is_none());
         assert!(schema2.features().is_none());
-        assert_eq!(fb_to_schema(schema), fb_to_schema(schema2));
+        assert_eq!(
+            try_fb_to_schema(schema).unwrap(),
+            try_fb_to_schema(schema2).unwrap()
+        );
 
         assert_eq!(ipc.version(), ipc2.version());
         assert_eq!(ipc.header_type(), ipc2.header_type());
diff --git a/arrow-ipc/src/reader.rs b/arrow-ipc/src/reader.rs
index f3856b1d32..748330aad4 100644
--- a/arrow-ipc/src/reader.rs
+++ b/arrow-ipc/src/reader.rs
@@ -967,7 +967,7 @@ pub fn read_footer_length(buf: [u8; 10]) -> Result<usize, 
ArrowError> {
 /// # use arrow_array::*;
 /// # use arrow_array::types::Int32Type;
 /// # use arrow_buffer::Buffer;
-/// # use arrow_ipc::convert::fb_to_schema;
+/// # use arrow_ipc::convert::try_fb_to_schema;
 /// # use arrow_ipc::reader::{FileDecoder, read_footer_length};
 /// # use arrow_ipc::root_as_footer;
 /// # use arrow_ipc::writer::FileWriter;
@@ -995,7 +995,7 @@ pub fn read_footer_length(buf: [u8; 10]) -> Result<usize, 
ArrowError> {
 /// let footer_len = 
read_footer_length(buffer[trailer_start..].try_into().unwrap()).unwrap();
 /// let footer = root_as_footer(&buffer[trailer_start - 
footer_len..trailer_start]).unwrap();
 ///
-/// let back = fb_to_schema(footer.schema().unwrap());
+/// let back = try_fb_to_schema(footer.schema().unwrap()).unwrap();
 /// assert_eq!(&back, schema.as_ref());
 ///
 /// let mut decoder = FileDecoder::new(schema, footer.version());
@@ -1256,7 +1256,7 @@ impl FileReaderBuilder {
             ));
         }
 
-        let schema = Arc::new(crate::convert::fb_to_schema(ipc_schema));
+        let schema = Arc::new(crate::convert::try_fb_to_schema(ipc_schema)?);
 
         let projected_schema = match &self.projection {
             Some(projection) => Arc::new(schema.project(projection)?),
@@ -1604,7 +1604,7 @@ impl<R: Read> StreamReader<R> {
         let schema = message.header_as_schema().ok_or_else(|| {
             ArrowError::ParseError("Failed to parse schema from message 
header".to_string())
         })?;
-        let schema = crate::convert::fb_to_schema(schema);
+        let schema = crate::convert::try_fb_to_schema(schema)?;
 
         // Create an array of optional dictionary value arrays, one per field.
         let dictionaries_by_id = HashMap::new();
@@ -1686,7 +1686,7 @@ impl<R: Read> StreamReader<R> {
                 let schema = message.header_as_schema().ok_or_else(|| {
                     ArrowError::ParseError("Failed to parse schema from 
message header".to_string())
                 })?;
-                let arrow_schema = crate::convert::fb_to_schema(schema);
+                let arrow_schema = crate::convert::try_fb_to_schema(schema)?;
                 IpcMessage::Schema(arrow_schema)
             }
             Message::MessageHeader::RecordBatch => {
@@ -1955,7 +1955,7 @@ impl<R: Read> MessageReader<R> {
 mod tests {
     use std::io::Cursor;
 
-    use crate::convert::fb_to_schema;
+    use crate::convert::try_fb_to_schema;
     use crate::writer::{
         DictionaryTracker, IpcDataGenerator, IpcWriteOptions, 
unslice_run_array, write_message,
     };
@@ -2530,7 +2530,7 @@ mod tests {
         let footer = root_as_footer(&buffer[trailer_start - 
footer_len..trailer_start])
             .map_err(|e| ArrowError::InvalidArgumentError(format!("Invalid 
footer: {e}")))?;
 
-        let schema = fb_to_schema(footer.schema().unwrap());
+        let schema = try_fb_to_schema(footer.schema().unwrap()).unwrap();
 
         let mut decoder = unsafe {
             FileDecoder::new(Arc::new(schema), footer.version())
@@ -3654,7 +3654,7 @@ mod tests {
 
         let msg = parse_message(&schema_bytes).expect("parse_message");
         let ipc_schema = msg.header_as_schema().expect("header_as_schema");
-        let new_schema = fb_to_schema(ipc_schema);
+        let new_schema = try_fb_to_schema(ipc_schema).unwrap();
 
         assert_eq!(schema, new_schema);
     }
diff --git a/arrow-ipc/src/reader/stream.rs b/arrow-ipc/src/reader/stream.rs
index e18d32cea2..f136b6b69d 100644
--- a/arrow-ipc/src/reader/stream.rs
+++ b/arrow-ipc/src/reader/stream.rs
@@ -229,8 +229,12 @@ impl StreamDecoder {
                                 ));
                             }
 
-                            let ipc_schema = 
message.header_as_schema().unwrap();
-                            let schema = 
crate::convert::fb_to_schema(ipc_schema);
+                            let ipc_schema = 
message.header_as_schema().ok_or_else(|| {
+                                ArrowError::ParseError(
+                                    "Unable to read IPC message as 
schema".to_string(),
+                                )
+                            })?;
+                            let schema = 
crate::convert::try_fb_to_schema(ipc_schema)?;
                             self.state = DecoderState::default();
                             self.schema = Some(Arc::new(schema));
                         }
diff --git a/arrow-ipc/src/writer.rs b/arrow-ipc/src/writer.rs
index 20d3441a3f..a9cc68bc13 100644
--- a/arrow-ipc/src/writer.rs
+++ b/arrow-ipc/src/writer.rs
@@ -2771,7 +2771,7 @@ mod tests {
     use arrow_buffer::ScalarBuffer;
 
     use crate::MetadataVersion;
-    use crate::convert::fb_to_schema;
+    use crate::convert::try_fb_to_schema;
     use crate::reader::*;
     use crate::root_as_footer;
 
@@ -4630,7 +4630,7 @@ mod tests {
             let footer =
                 root_as_footer(&buffer[trailer_start - 
footer_len..trailer_start]).unwrap();
 
-            let schema = fb_to_schema(footer.schema().unwrap());
+            let schema = try_fb_to_schema(footer.schema().unwrap()).unwrap();
 
             // Importantly we set `require_alignment`, checking that 16-byte 
alignment is sufficient
             // for `read_record_batch` later on to read the data in a 
zero-copy manner.
@@ -4682,7 +4682,7 @@ mod tests {
         let trailer_start = buffer.len() - 10;
         let footer_len = 
read_footer_length(buffer[trailer_start..].try_into().unwrap()).unwrap();
         let footer = root_as_footer(&buffer[trailer_start - 
footer_len..trailer_start]).unwrap();
-        let schema = fb_to_schema(footer.schema().unwrap());
+        let schema = try_fb_to_schema(footer.schema().unwrap()).unwrap();
 
         // Importantly we set `require_alignment`, otherwise the error later 
is suppressed due to copying
         // to an aligned buffer in `ArrayDataBuilder.build_aligned`.
diff --git a/arrow/examples/zero_copy_ipc.rs b/arrow/examples/zero_copy_ipc.rs
index 0c80572cf4..c05fce42fd 100644
--- a/arrow/examples/zero_copy_ipc.rs
+++ b/arrow/examples/zero_copy_ipc.rs
@@ -24,7 +24,7 @@ use arrow::array::{RecordBatch, record_batch};
 use arrow::error::Result;
 use arrow_buffer::Buffer;
 use arrow_cast::pretty::pretty_format_batches;
-use arrow_ipc::convert::fb_to_schema;
+use arrow_ipc::convert::try_fb_to_schema;
 use arrow_ipc::reader::{FileDecoder, read_footer_length};
 use arrow_ipc::writer::FileWriter;
 use arrow_ipc::{Block, root_as_footer};
@@ -101,7 +101,7 @@ impl IPCBufferDecoder {
         let footer_len = 
read_footer_length(buffer[trailer_start..].try_into().unwrap()).unwrap();
         let footer = root_as_footer(&buffer[trailer_start - 
footer_len..trailer_start]).unwrap();
 
-        let schema = fb_to_schema(footer.schema().unwrap());
+        let schema = try_fb_to_schema(footer.schema().unwrap()).unwrap();
 
         let mut decoder = FileDecoder::new(Arc::new(schema), footer.version());
 
diff --git a/parquet/src/arrow/schema/mod.rs b/parquet/src/arrow/schema/mod.rs
index 110c799b42..be5577b2c4 100644
--- a/parquet/src/arrow/schema/mod.rs
+++ b/parquet/src/arrow/schema/mod.rs
@@ -269,8 +269,10 @@ fn get_arrow_schema_from_metadata(encoded_meta: &str) -> 
Result<Schema> {
             match arrow_ipc::root_as_message(slice) {
                 Ok(message) => message
                     .header_as_schema()
-                    .map(arrow_ipc::convert::fb_to_schema)
-                    .ok_or_else(|| arrow_err!("the message is not Arrow 
Schema")),
+                    .ok_or_else(|| arrow_err!("the message is not Arrow 
Schema"))
+                    .and_then(|schema| {
+                        
arrow_ipc::convert::try_fb_to_schema(schema).map_err(Into::into)
+                    }),
                 Err(err) => {
                     // The flatbuffers implementation returns an error on 
verification error.
                     Err(arrow_err!(

Reply via email to