You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@arrow.apache.org by al...@apache.org on 2026/10/20 11:35:17 UTC

(arrow-rs) branch 59_maintenance updated: [59_maintenance] fix(arrow-schema): validate `UnionFields` when deserializing (#11672)

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

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


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

commit 5573ddaf8e94cf462b0218a9c34b9d4dc5df96e4
Author: Jeffrey Vo <je...@gmail.com>
AuthorDate: Tue Oct 20 20:35:10 2026 +0900

    [59_maintenance] fix(arrow-schema): validate `UnionFields` when deserializing (#11672)
    
    - Backport https://github.com/apache/arrow-rs/pull/11638
    - For inclusion in https://github.com/apache/arrow-rs/issues/11664
    
    Cherry-pick of
    https://github.com/apache/arrow-rs/commit/a2b405fe22b6657fd4f69eb139e6836cafa3b55f
    
    ---------
    
    Co-authored-by: ump45nose <52...@users.noreply.github.com>
    Co-authored-by: ump45nose <um...@users.noreply.github.com>
    Co-authored-by: Andrew Lamb <an...@nerdnetworks.org>
---
 Cargo.lock                 | 28 +++++++++++++++++--------
 arrow-schema/Cargo.toml    |  1 +
 arrow-schema/src/fields.rs | 51 +++++++++++++++++++++++++++++++++++++++++++++-
 3 files changed, 70 insertions(+), 10 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index bf1cc46919..93d2d411f5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1475,11 +1475,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
 dependencies = [
  "cfg-if",
- "js-sys",
  "libc",
  "r-efi 5.3.0",
  "wasip2",
- "wasm-bindgen",
 ]
 
 [[package]]
@@ -1489,11 +1487,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
 dependencies = [
  "cfg-if",
+ "js-sys",
  "libc",
  "r-efi 6.0.0",
  "rand_core 0.10.1",
  "wasip2",
  "wasip3",
+ "wasm-bindgen",
 ]
 
 [[package]]
@@ -2737,14 +2737,15 @@ dependencies = [
 
 [[package]]
 name = "quinn-proto"
-version = "0.11.15"
+version = "0.11.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e"
+checksum = "0e750cca55fe4f0439a15d0bb529da9651e79993e8e72c61a899a36d462befbe"
 dependencies = [
  "bytes",
- "getrandom 0.3.4",
+ "getrandom 0.4.2",
  "lru-slab",
- "rand 0.9.4",
+ "rand 0.10.1",
+ "rand_pcg",
  "ring",
  "rustc-hash",
  "rustls",
@@ -2847,6 +2848,15 @@ dependencies = [
  "rand 0.9.4",
 ]
 
+[[package]]
+name = "rand_pcg"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
+dependencies = [
+ "rand_core 0.10.1",
+]
+
 [[package]]
 name = "redox_syscall"
 version = "0.5.18"
@@ -2977,9 +2987,9 @@ dependencies = [
 
 [[package]]
 name = "rustls"
-version = "0.23.45"
+version = "0.23.46"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634"
+checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf"
 dependencies = [
  "aws-lc-rs",
  "log",
@@ -3356,7 +3366,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
 dependencies = [
  "fastrand",
- "getrandom 0.3.4",
+ "getrandom 0.4.2",
  "once_cell",
  "rustix",
  "windows-sys 0.61.2",
diff --git a/arrow-schema/Cargo.toml b/arrow-schema/Cargo.toml
index 52e1082ecb..4b8938e2cc 100644
--- a/arrow-schema/Cargo.toml
+++ b/arrow-schema/Cargo.toml
@@ -55,6 +55,7 @@ all-features = true
 criterion = { workspace = true, default-features = false }
 insta = { workspace = true, default-features = true }
 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 b4421b25f0..f5348c91af 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)
@@ -1009,6 +1022,42 @@ mod tests {
         assert_eq!(res.unwrap().len(), 3);
     }
 
+    #[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}"
+        );
+    }
+
     #[test]
     #[should_panic(
         expected = "invalid arguments passed: InvalidArgumentError(\"type ids must be non-negative: -1\")"