You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@datasketches.apache.org by ti...@apache.org on 2026/07/28 07:06:55 UTC

(datasketches-rust) 06/08: test(serde): reject truncated public inputs

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

tisonkun pushed a commit to branch codex/analyze-crate-tests
in repository https://gitbox.apache.org/repos/asf/datasketches-rust.git

commit 95197af2fa42c2c5c69d38376a62115046ba6ea6
Author: tison <wa...@gmail.com>
AuthorDate: Tue Jul 28 14:46:04 2026 +0800

    test(serde): reject truncated public inputs
---
 datasketches/src/theta/sketch.rs              | 49 ---------------------------
 datasketches/src/tuple/sketch.rs              | 39 ---------------------
 datasketches/tests/serde_tests.rs             | 25 ++++++++++++++
 datasketches/tests/serde_tests/bloom.rs       | 10 ++++++
 datasketches/tests/serde_tests/countmin.rs    |  9 +++++
 datasketches/tests/serde_tests/cpc.rs         | 11 ++++++
 datasketches/tests/serde_tests/frequencies.rs |  9 +++++
 datasketches/tests/serde_tests/hll.rs         | 11 ++++++
 datasketches/tests/serde_tests/tdigest.rs     | 11 ++++++
 datasketches/tests/serde_tests/theta.rs       | 27 +++++++++++++++
 datasketches/tests/serde_tests/tuple.rs       | 25 ++++++++++++++
 11 files changed, 138 insertions(+), 88 deletions(-)

diff --git a/datasketches/src/theta/sketch.rs b/datasketches/src/theta/sketch.rs
index 9ced2b4..cc78b9c 100644
--- a/datasketches/src/theta/sketch.rs
+++ b/datasketches/src/theta/sketch.rs
@@ -1135,53 +1135,4 @@ mod tests {
 
         assert_compressed_round_trip(&theta, &compact);
     }
-
-    #[test]
-    fn deserialize_rejects_seed_hash_mismatch() {
-        let mut theta = ThetaSketchBuilder::default().seed(7).build();
-        theta.update("apple");
-        let bytes = theta.compact(true).serialize();
-
-        let err = CompactThetaSketch::deserialize_with_seed(&bytes, 8).unwrap_err();
-        assert_eq!(err.kind(), crate::error::ErrorKind::InvalidData);
-        assert!(err.message().contains("incompatible seed hash"));
-    }
-
-    #[test]
-    fn deserialize_rejects_invalid_family_id() {
-        let mut theta = ThetaSketchBuilder::default().build();
-        theta.update("apple");
-        let mut bytes = theta.compact(true).serialize();
-        bytes[2] = 0;
-
-        let err = CompactThetaSketch::deserialize(&bytes).unwrap_err();
-        assert_eq!(err.kind(), crate::error::ErrorKind::InvalidData);
-        assert!(err.message().contains("invalid family"));
-    }
-
-    #[test]
-    fn deserialize_rejects_unsupported_serial_version() {
-        let mut theta = ThetaSketchBuilder::default().build();
-        theta.update("apple");
-        let mut bytes = theta.compact(true).serialize();
-        bytes[1] = 99;
-
-        let err = CompactThetaSketch::deserialize(&bytes).unwrap_err();
-        assert_eq!(err.kind(), crate::error::ErrorKind::InvalidData);
-        assert!(err.message().contains("unsupported serial version"));
-    }
-
-    #[test]
-    fn deserialize_rejects_truncated_payload() {
-        let mut theta = ThetaSketchBuilder::default().lg_k(5).build();
-        for i in 0..5000 {
-            theta.update(i);
-        }
-        let mut bytes = theta.compact(true).serialize();
-        bytes.pop();
-
-        let err = CompactThetaSketch::deserialize(&bytes).unwrap_err();
-        assert_eq!(err.kind(), crate::error::ErrorKind::InvalidData);
-        assert!(err.message().contains("insufficient data"));
-    }
 }
diff --git a/datasketches/src/tuple/sketch.rs b/datasketches/src/tuple/sketch.rs
index b43adeb..bb0832d 100644
--- a/datasketches/src/tuple/sketch.rs
+++ b/datasketches/src/tuple/sketch.rs
@@ -713,7 +713,6 @@ where
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::error::ErrorKind;
     use crate::tuple::policy::DefaultUpdatePolicy;
     use crate::tuple::policy::SummaryPolicy;
     use crate::tuple::policy::SummaryUpdatePolicy;
@@ -1026,42 +1025,4 @@ mod tests {
         assert_eq!(restored.num_retained(), 50);
         assert!(restored.iter().all(|(_, &s)| s == 2));
     }
-
-    #[test]
-    fn deserialize_rejects_wrong_family() {
-        let policy = DefaultUpdatePolicy::<u64>::default();
-        let mut sketch = TupleSketchBuilder::new(policy).build();
-        for i in 0..10 {
-            sketch.update(i, 1u64);
-        }
-        let mut bytes = sketch.compact(true).serialize();
-        bytes[2] = 3; // pretend it is a THETA sketch
-        let err = CompactTupleSketch::<u64>::deserialize(&bytes).unwrap_err();
-        assert_eq!(err.kind(), ErrorKind::InvalidData);
-    }
-
-    #[test]
-    fn deserialize_rejects_seed_mismatch() {
-        let policy = DefaultUpdatePolicy::<u64>::default();
-        let mut sketch = TupleSketchBuilder::new(policy).build();
-        for i in 0..10 {
-            sketch.update(i, 1u64);
-        }
-        let bytes = sketch.compact(true).serialize();
-        let err = CompactTupleSketch::<u64>::deserialize_with_seed(&bytes, 999).unwrap_err();
-        assert_eq!(err.kind(), ErrorKind::InvalidData);
-    }
-
-    #[test]
-    fn deserialize_rejects_truncated_summary() {
-        let policy = DefaultUpdatePolicy::<u64>::default();
-        let mut sketch = TupleSketchBuilder::new(policy).build();
-        for i in 0..100 {
-            sketch.update(i, 1u64);
-        }
-        let bytes = sketch.compact(true).serialize();
-        let truncated = &bytes[..bytes.len() - 4]; // cut the last summary in half
-        let err = CompactTupleSketch::<u64>::deserialize(truncated).unwrap_err();
-        assert_eq!(err.kind(), ErrorKind::InvalidData);
-    }
 }
diff --git a/datasketches/tests/serde_tests.rs b/datasketches/tests/serde_tests.rs
index 92cdc42..bb8f866 100644
--- a/datasketches/tests/serde_tests.rs
+++ b/datasketches/tests/serde_tests.rs
@@ -17,6 +17,9 @@
 
 use std::path::PathBuf;
 
+use datasketches::error::Error;
+use datasketches::error::ErrorKind;
+
 pub fn serialization_test_data(sub_dir: &str, name: &str) -> PathBuf {
     const SERDE_TESTS_DIR: &str = "tests/serde_tests";
 
@@ -42,6 +45,28 @@ pub fn serialization_test_data(sub_dir: &str, name: &str) -> PathBuf {
     path
 }
 
+pub fn assert_truncated_inputs_rejected<T>(
+    bytes: &[u8],
+    deserialize: impl Fn(&[u8]) -> Result<T, Error>,
+) {
+    assert!(!bytes.is_empty(), "valid serialization must not be empty");
+
+    for len in 0..bytes.len() {
+        match deserialize(&bytes[..len]) {
+            Ok(_) => panic!(
+                "deserializer accepted a truncated input of {len}/{} bytes",
+                bytes.len()
+            ),
+            Err(err) => assert_eq!(
+                err.kind(),
+                ErrorKind::InvalidData,
+                "unexpected error for a truncated input of {len}/{} bytes",
+                bytes.len()
+            ),
+        }
+    }
+}
+
 #[cfg(feature = "bloom")]
 #[path = "serde_tests/bloom.rs"]
 mod bloom;
diff --git a/datasketches/tests/serde_tests/bloom.rs b/datasketches/tests/serde_tests/bloom.rs
index 5875b1f..13b4495 100644
--- a/datasketches/tests/serde_tests/bloom.rs
+++ b/datasketches/tests/serde_tests/bloom.rs
@@ -19,7 +19,9 @@ use std::fs;
 use std::path::PathBuf;
 
 use datasketches::bloom::BloomFilter;
+use datasketches::bloom::BloomFilterBuilder;
 
+use crate::assert_truncated_inputs_rejected;
 use crate::serialization_test_data;
 
 fn test_bloom_filter_file(path: PathBuf, expected_num_items: u64, expected_num_hashes: u16) {
@@ -219,3 +221,11 @@ fn test_cpp_bloom_n30000000_h5() {
     let path = serialization_test_data("cpp_generated_files", "bf_n30000000_h5_cpp.sk");
     test_bloom_filter_file(path, 30000000, 5);
 }
+
+#[test]
+fn truncated_input_is_rejected() {
+    let mut filter = BloomFilterBuilder::with_size(64, 3).build();
+    filter.insert("value");
+
+    assert_truncated_inputs_rejected(&filter.serialize(), BloomFilter::deserialize);
+}
diff --git a/datasketches/tests/serde_tests/countmin.rs b/datasketches/tests/serde_tests/countmin.rs
index 49745f1..b0a5233 100644
--- a/datasketches/tests/serde_tests/countmin.rs
+++ b/datasketches/tests/serde_tests/countmin.rs
@@ -21,6 +21,7 @@ use datasketches::countmin::CountMinSketch;
 use googletest::assert_that;
 use googletest::prelude::contains_substring;
 
+use crate::assert_truncated_inputs_rejected;
 use crate::serialization_test_data;
 
 // This test validates binary format compatibility (deserialize + byte round-trip) for
@@ -66,3 +67,11 @@ fn test_deserialize_cpp_snapshot_with_wrong_seed() {
     let err = CountMinSketch::<u64>::deserialize_with_seed(&bytes, 9000).unwrap_err();
     assert_that!(err.message(), contains_substring("incompatible seed hash"));
 }
+
+#[test]
+fn truncated_input_is_rejected() {
+    let mut sketch = CountMinSketch::<u64>::new(1, 3);
+    sketch.update("value");
+
+    assert_truncated_inputs_rejected(&sketch.serialize(), CountMinSketch::<u64>::deserialize);
+}
diff --git a/datasketches/tests/serde_tests/cpc.rs b/datasketches/tests/serde_tests/cpc.rs
index 7d90250..c30fc31 100644
--- a/datasketches/tests/serde_tests/cpc.rs
+++ b/datasketches/tests/serde_tests/cpc.rs
@@ -22,6 +22,7 @@ use datasketches::cpc::CpcSketch;
 use googletest::assert_that;
 use googletest::prelude::near;
 
+use crate::assert_truncated_inputs_rejected;
 use crate::serialization_test_data;
 
 fn test_sketch_file(path: PathBuf, expected_cardinality: usize) {
@@ -81,3 +82,13 @@ fn test_cpp_compatibility() {
         test_sketch_file(path, n);
     }
 }
+
+#[test]
+fn truncated_input_is_rejected() {
+    let mut sketch = CpcSketch::new(4);
+    for value in 0..20 {
+        sketch.update(value);
+    }
+
+    assert_truncated_inputs_rejected(&sketch.serialize(), CpcSketch::deserialize);
+}
diff --git a/datasketches/tests/serde_tests/frequencies.rs b/datasketches/tests/serde_tests/frequencies.rs
index 07051ba..78bca03 100644
--- a/datasketches/tests/serde_tests/frequencies.rs
+++ b/datasketches/tests/serde_tests/frequencies.rs
@@ -24,6 +24,7 @@ use datasketches::error::ErrorKind;
 use datasketches::frequencies::FrequentItemValue;
 use datasketches::frequencies::FrequentItemsSketch;
 
+use crate::assert_truncated_inputs_rejected;
 use crate::serialization_test_data;
 
 #[derive(Debug, PartialEq, Eq, Hash)]
@@ -114,6 +115,14 @@ fn test_purged_to_empty_round_trip() {
     assert!(restored.is_empty());
 }
 
+#[test]
+fn truncated_input_is_rejected() {
+    let mut sketch = FrequentItemsSketch::<i64>::new(8);
+    sketch.update_with_count(42, 3);
+
+    assert_truncated_inputs_rejected(&sketch.serialize(), FrequentItemsSketch::<i64>::deserialize);
+}
+
 #[test]
 fn test_java_frequent_longs_compatibility() {
     let test_cases = [0, 1, 10, 100, 1000, 10000, 100000, 1000000];
diff --git a/datasketches/tests/serde_tests/hll.rs b/datasketches/tests/serde_tests/hll.rs
index fb1e4fb..7c6db05 100644
--- a/datasketches/tests/serde_tests/hll.rs
+++ b/datasketches/tests/serde_tests/hll.rs
@@ -22,6 +22,7 @@ use datasketches::hash_value::natural_extend;
 use datasketches::hll::HllSketch;
 use datasketches::hll::HllType;
 
+use crate::assert_truncated_inputs_rejected;
 use crate::serialization_test_data;
 
 fn test_sketch_file(path: PathBuf, expected_cardinality: usize, expected_lg_k: u8) {
@@ -130,6 +131,16 @@ fn test_update_after_deserialize_list_mode() {
     }
 }
 
+#[test]
+fn truncated_input_is_rejected() {
+    let mut sketch = HllSketch::new(4, HllType::Hll8);
+    for value in 0..20 {
+        sketch.update(value);
+    }
+
+    assert_truncated_inputs_rejected(&sketch.serialize(), HllSketch::deserialize);
+}
+
 #[test]
 fn test_serialized_bytes_match_reference_files_for_coupon_modes() {
     fn serialized_mode_name(bytes: &[u8]) -> &'static str {
diff --git a/datasketches/tests/serde_tests/tdigest.rs b/datasketches/tests/serde_tests/tdigest.rs
index bfe4d40..41ad177 100644
--- a/datasketches/tests/serde_tests/tdigest.rs
+++ b/datasketches/tests/serde_tests/tdigest.rs
@@ -23,6 +23,7 @@ use googletest::assert_that;
 use googletest::prelude::eq;
 use googletest::prelude::near;
 
+use crate::assert_truncated_inputs_rejected;
 use crate::serialization_test_data;
 
 fn test_sketch_file(path: PathBuf, n: u64, with_buffer: bool, is_f32: bool) {
@@ -131,6 +132,16 @@ fn test_deserialize_from_java_snapshots() {
     }
 }
 
+#[test]
+fn truncated_input_is_rejected() {
+    let mut digest = TDigestMut::new(10);
+    digest.update(1.0);
+    digest.update(2.0);
+    let bytes = digest.serialize();
+
+    assert_truncated_inputs_rejected(&bytes, |bytes| TDigestMut::deserialize(bytes, false));
+}
+
 #[test]
 fn test_empty() {
     let mut td = TDigestMut::new(100);
diff --git a/datasketches/tests/serde_tests/theta.rs b/datasketches/tests/serde_tests/theta.rs
index a1206aa..c385d65 100644
--- a/datasketches/tests/serde_tests/theta.rs
+++ b/datasketches/tests/serde_tests/theta.rs
@@ -18,10 +18,13 @@
 use std::fs;
 use std::path::PathBuf;
 
+use datasketches::error::ErrorKind;
 use datasketches::theta::CompactThetaSketch;
+use datasketches::theta::ThetaSketchBuilder;
 use googletest::assert_that;
 use googletest::prelude::near;
 
+use crate::assert_truncated_inputs_rejected;
 use crate::serialization_test_data;
 
 fn test_sketch_file(path: PathBuf, expected_cardinality: usize, use_compressed_round_trip: bool) {
@@ -113,3 +116,27 @@ fn test_cpp_compatibility() {
     let path = serialization_test_data("cpp_generated_files", "theta_non_empty_no_entries_cpp.sk");
     test_sketch_file(path, 0, false);
 }
+
+#[test]
+fn malformed_input_is_rejected() {
+    let mut sketch = ThetaSketchBuilder::default().lg_k(5).build();
+    for value in 0..100 {
+        sketch.update(value);
+    }
+    let bytes = sketch.compact(true).serialize();
+
+    assert_truncated_inputs_rejected(&bytes, CompactThetaSketch::deserialize);
+
+    let err = CompactThetaSketch::deserialize_with_seed(&bytes, 8).unwrap_err();
+    assert_eq!(err.kind(), ErrorKind::InvalidData);
+
+    let mut wrong_family = bytes.clone();
+    wrong_family[2] = 0;
+    let err = CompactThetaSketch::deserialize(&wrong_family).unwrap_err();
+    assert_eq!(err.kind(), ErrorKind::InvalidData);
+
+    let mut unsupported_version = bytes;
+    unsupported_version[1] = 99;
+    let err = CompactThetaSketch::deserialize(&unsupported_version).unwrap_err();
+    assert_eq!(err.kind(), ErrorKind::InvalidData);
+}
diff --git a/datasketches/tests/serde_tests/tuple.rs b/datasketches/tests/serde_tests/tuple.rs
index 00a3ca2..e076617 100644
--- a/datasketches/tests/serde_tests/tuple.rs
+++ b/datasketches/tests/serde_tests/tuple.rs
@@ -18,10 +18,14 @@
 use std::fs;
 use std::path::PathBuf;
 
+use datasketches::error::ErrorKind;
 use datasketches::tuple::CompactTupleSketch;
+use datasketches::tuple::DefaultUpdatePolicy;
+use datasketches::tuple::TupleSketchBuilder;
 use googletest::assert_that;
 use googletest::prelude::near;
 
+use crate::assert_truncated_inputs_rejected;
 use crate::serialization_test_data;
 
 fn test_sketch_file(path: PathBuf, expected_cardinality: usize) {
@@ -90,3 +94,24 @@ fn test_cpp_compatibility() {
         test_sketch_file(path, n);
     }
 }
+
+#[test]
+fn malformed_input_is_rejected() {
+    let mut sketch = TupleSketchBuilder::new(DefaultUpdatePolicy::<u64>::default())
+        .lg_k(5)
+        .build();
+    for value in 0..100 {
+        sketch.update(value, 1);
+    }
+    let bytes = sketch.compact(true).serialize();
+
+    assert_truncated_inputs_rejected(&bytes, CompactTupleSketch::<u64>::deserialize);
+
+    let err = CompactTupleSketch::<u64>::deserialize_with_seed(&bytes, 8).unwrap_err();
+    assert_eq!(err.kind(), ErrorKind::InvalidData);
+
+    let mut wrong_family = bytes;
+    wrong_family[2] = 0;
+    let err = CompactTupleSketch::<u64>::deserialize(&wrong_family).unwrap_err();
+    assert_eq!(err.kind(), ErrorKind::InvalidData);
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@datasketches.apache.org
For additional commands, e-mail: commits-help@datasketches.apache.org