This is an automated email from the ASF dual-hosted git repository.

alamb pushed a commit to branch 57_maintenance
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git


The following commit(s) were added to refs/heads/57_maintenance by this push:
     new f42d0c42ad [57_maintenance] fix(arrow-schema): validate `UnionFields` 
when deserializing (#11813)
f42d0c42ad is described below

commit f42d0c42add18f99bd96bb8bb1cba16c8c446f5a
Author: Jeffrey Vo <[email protected]>
AuthorDate: Fri Oct 23 00:23:26 2026 +0900

    [57_maintenance] fix(arrow-schema): validate `UnionFields` when 
deserializing (#11813)
    
    - Backport https://github.com/apache/arrow-rs/pull/11638
    - For inclusion in https://github.com/apache/arrow-rs/issues/11666
    
    Cherry-pick of
    
https://github.com/apache/arrow-rs/commit/a2b405fe22b6657fd4f69eb139e6836cafa3b55f
    
    Co-authored-by: ump45nose <[email protected]>
    Co-authored-by: ump45nose <[email protected]>
---
 Cargo.lock                 | 34 +++++++++++++++++++++++++++++++
 arrow-schema/Cargo.toml    |  2 ++
 arrow-schema/src/fields.rs | 51 +++++++++++++++++++++++++++++++++++++++++++++-
 3 files changed, 86 insertions(+), 1 deletion(-)

diff --git a/Cargo.lock b/Cargo.lock
index c7967e9d3c..eec34b17b7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -479,6 +479,7 @@ dependencies = [
  "bitflags",
  "criterion",
  "insta",
+ "postcard",
  "serde",
  "serde_core",
  "serde_json",
@@ -922,6 +923,15 @@ dependencies = [
  "cc",
 ]
 
+[[package]]
+name = "cobs"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"
+dependencies = [
+ "thiserror 2.0.21",
+]
+
 [[package]]
 name = "colorchoice"
 version = "1.0.5"
@@ -1172,6 +1182,18 @@ version = "1.19.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be"
 
+[[package]]
+name = "embedded-io"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
+
+[[package]]
+name = "embedded-io"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
+
 [[package]]
 name = "encode_unicode"
 version = "1.0.0"
@@ -2468,6 +2490,18 @@ version = "1.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index";
 checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
 
+[[package]]
+name = "postcard"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"
+dependencies = [
+ "cobs",
+ "embedded-io 0.4.0",
+ "embedded-io 0.6.1",
+ "serde",
+]
+
 [[package]]
 name = "potential_utf"
 version = "0.1.6"
diff --git a/arrow-schema/Cargo.toml b/arrow-schema/Cargo.toml
index 8164652843..4e307b3992 100644
--- a/arrow-schema/Cargo.toml
+++ b/arrow-schema/Cargo.toml
@@ -58,6 +58,8 @@ bincode = { version = "2.0.1", default-features = false, 
features = [
 ] }
 criterion = { workspace = true, default-features = false }
 insta = "1.43.1"
+postcard = { version = "1.0.10", default-features = false, features = 
["use-std"] }
+serde_json = "1.0"
 
 [[bench]]
 name = "ffi"
diff --git a/arrow-schema/src/fields.rs b/arrow-schema/src/fields.rs
index 3b33d161b7..1dad368990 100644
--- a/arrow-schema/src/fields.rs
+++ b/arrow-schema/src/fields.rs
@@ -324,10 +324,23 @@ impl<'a> IntoIterator for &'a Fields {
 /// - All type ids are unique
 /// - All type ids are non-negative
 #[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
-#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
+#[cfg_attr(feature = "serde", derive(serde::Serialize))]
 #[cfg_attr(feature = "serde", serde(transparent))]
 pub struct UnionFields(Arc<[(i8, FieldRef)]>);
 
+// Manual implementation to enforce invariants per docstring above
+#[cfg(feature = "serde")]
+impl<'de> serde::Deserialize<'de> for UnionFields {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        let entries = Vec::<(i8, FieldRef)>::deserialize(deserializer)?;
+        let (type_ids, fields): (Vec<i8>, Vec<FieldRef>) = 
entries.into_iter().unzip();
+        Self::try_new(type_ids, fields).map_err(serde::de::Error::custom)
+    }
+}
+
 impl std::fmt::Debug for UnionFields {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         self.0.as_ref().fmt(f)
@@ -1069,4 +1082,40 @@ mod tests {
         ];
         UnionFields::from_iter(fields);
     }
+
+    #[cfg(feature = "serde")]
+    #[test]
+    fn test_union_fields_serde_round_trip() {
+        let fields = UnionFields::try_new(
+            vec![1, 3],
+            vec![
+                Field::new("a", DataType::Int32, true),
+                Field::new("b", DataType::Utf8, true),
+            ],
+        )
+        .unwrap();
+        let serialized = postcard::to_stdvec(&fields).unwrap();
+        let deserialized: UnionFields = 
postcard::from_bytes(&serialized).unwrap();
+        assert_eq!(fields, deserialized);
+    }
+
+    #[cfg(feature = "serde")]
+    #[test]
+    fn test_union_fields_deserialize_should_validate() {
+        let field = Arc::new(Field::new("a", DataType::Int32, true));
+
+        let duplicate: Vec<(i8, FieldRef)> = vec![(1, Arc::clone(&field)), (1, 
Arc::clone(&field))];
+        let serialized = serde_json::to_string(&duplicate).unwrap();
+        let err = 
serde_json::from_str::<UnionFields>(&serialized).unwrap_err();
+        assert!(err.to_string().contains("duplicate type id: 1"), "{err}");
+
+        let negative: Vec<(i8, FieldRef)> = vec![(0, Arc::clone(&field)), (-1, 
field)];
+        let serialized = serde_json::to_string(&negative).unwrap();
+        let err = 
serde_json::from_str::<UnionFields>(&serialized).unwrap_err();
+        assert!(
+            err.to_string()
+                .contains("type ids must be non-negative: -1"),
+            "{err}"
+        );
+    }
 }

Reply via email to