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

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


The following commit(s) were added to refs/heads/main by this push:
     new 2b0692e  refactor: polish release-facing APIs before 0.5.0 (#256)
2b0692e is described below

commit 2b0692e0d3b818c7f2d5dfcc1a4a1915f213ef42
Author: tison <[email protected]>
AuthorDate: Tue Sep 1 09:06:26 2026 +0800

    refactor: polish release-facing APIs before 0.5.0 (#256)
---
 CHANGELOG.md                                       |  3 ++-
 LICENSE                                            |  7 +++---
 NOTICE                                             |  2 --
 RELEASE.md                                         |  5 ++++-
 datasketches/LICENSE                               |  1 +
 datasketches/NOTICE                                |  1 +
 datasketches/src/countmin/sketch.rs                | 23 ++++++++++++++++++-
 datasketches/src/cpc/sketch.rs                     | 26 +++++-----------------
 datasketches/src/cpc/wrapper.rs                    | 10 +++++++--
 datasketches/src/frequencies/sketch.rs             |  5 +++++
 datasketches/src/hll/sketch.rs                     |  5 +++++
 datasketches/src/tdigest/sketch.rs                 | 18 +++++++++++++++
 datasketches/src/thetafamily/theta/intersection.rs |  5 +++++
 datasketches/src/thetafamily/theta/sketch.rs       |  5 +++++
 datasketches/src/thetafamily/theta/union.rs        |  5 +++++
 datasketches/src/thetafamily/tuple/sketch.rs       |  5 +++++
 tests-integration/tests/countmin_test/sketch.rs    | 11 +++++++++
 tests-integration/tests/cpc_test/union.rs          |  1 -
 tests-integration/tests/cpc_test/update.rs         |  3 ---
 tests-integration/tests/serde_tests/cpc.rs         | 12 ----------
 20 files changed, 105 insertions(+), 48 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 89e3b79..332fe0f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,7 +15,7 @@ All significant changes to this project will be documented in 
this file.
 * `FrequentItemsSketch::new` now rejects map sizes below the minimum of 8 
instead of silently rounding them up.
 * Replace `FrequentItemsSketch::epsilon_for_lg` with the fallible 
`epsilon_for_max_map_size`, and change `apriori_error` to accept the same 
maximum map size plus an unsigned stream weight. These helpers now match the 
constructor's units, and `max_map_size` exposes the configured value.
 * Replace the `is_f32` flag on `TDigestMut::deserialize` with separate 
`deserialize` and `deserialize_f32` entry points, making the serialized 
precision explicit at the call site.
-* Remove `CpcUnion::num_coupons`, which exposed internal union state solely 
for tests. Inspect the resulting `CpcSketch` when diagnostics are needed.
+* Remove `CpcSketch::{validate, num_coupons}` and `CpcUnion::num_coupons`, 
which exposed internal state solely for tests. Use cardinality estimates, 
confidence bounds, and serialization round trips to inspect observable sketch 
behavior.
 * Tuple sketch iterators now yield `&TupleEntry<_>` values instead of `(hash, 
&summary)` pairs. Use `entry.hash()` and `entry.summary()` to inspect each 
retained entry.
 * `ThetaIntersection::to_sketch` and `TupleIntersection::to_sketch` now return 
`Option`. Callers must handle `None` until the intersection receives its first 
successful update.
 * `BloomFilterBuilder`, `ThetaSketchBuilder`, `ThetaUnionBuilder`, 
`TupleSketchBuilder`, and `TupleUnionBuilder` now validate their configuration 
when `build` is called, and `build` returns `Result`. Callers must propagate or 
handle construction errors.
@@ -37,6 +37,7 @@ All significant changes to this project will be documented in 
this file.
 * Bloom filter accuracy construction now rejects targets that exceed the 
maximum serialized filter size instead of silently reducing capacity and 
violating the requested false-positive probability.
 * T-Digest CDF and PMF queries now accept an empty split-point slice and 
return the single all-values bin instead of panicking.
 * Bloom filter deserialization now rejects malformed images with inconsistent 
counts or payload lengths, while valid images with a dirty cached count are 
restored correctly.
+* Count-Min deserialization now rejects truncated counter payloads before 
allocating the table declared by the image header.
 * `FrequentItemsSketch` now enforces the cross-language map-size limit of 
`2^30` consistently. Oversized construction returns `InvalidArgument`, and 
malformed or oversized serialized images return `InvalidData` instead of 
panicking or attempting excessive allocation.
 * `FrequentItemsSketch<String>` now rejects an encoded string length that 
exceeds the remaining input before allocating the string buffer.
 * T-Digest compression now supports `k = u16::MAX` without overflowing.
diff --git a/LICENSE b/LICENSE
index af4da02..1db2a3b 100644
--- a/LICENSE
+++ b/LICENSE
@@ -208,11 +208,10 @@ APPENDIX A: How to apply the Apache License to your work.
 
 APPENDIX B: Additional licenses relevant to this product:
 
-    This product includes a number of source files with code that has been 
-    adapted from 3rd party sources including sources that may be subject 
-    to different copyright notices and license terms. Your use of 
+    This product includes a number of source files with code that has been
+    adapted from 3rd party sources including sources that may be subject
+    to different copyright notices and license terms. Your use of
     the source code for these subcomponents is subject to the terms and
     conditions of the following licenses.
 
 (EMPTY)
-
diff --git a/NOTICE b/NOTICE
index 6e2d05e..ecfbe6c 100644
--- a/NOTICE
+++ b/NOTICE
@@ -3,5 +3,3 @@ Copyright 2025-2026 The Apache Software Foundation
 
 This product includes software developed at
 The Apache Software Foundation (http://www.apache.org/).
-
-
diff --git a/RELEASE.md b/RELEASE.md
index 64f4b01..87ba7f1 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -125,7 +125,10 @@ cargo x prepare-testdata
 cargo x lint
 cargo x check
 cargo x test
-cargo package --list -p datasketches
+package_files="$(cargo package --list -p datasketches)"
+printf '%s\n' "$package_files"
+grep -Fx LICENSE <<<"$package_files"
+grep -Fx NOTICE <<<"$package_files"
 cargo publish --dry-run --locked -p datasketches
 ```
 
diff --git a/datasketches/LICENSE b/datasketches/LICENSE
new file mode 120000
index 0000000..ea5b606
--- /dev/null
+++ b/datasketches/LICENSE
@@ -0,0 +1 @@
+../LICENSE
\ No newline at end of file
diff --git a/datasketches/NOTICE b/datasketches/NOTICE
new file mode 120000
index 0000000..7e1b82f
--- /dev/null
+++ b/datasketches/NOTICE
@@ -0,0 +1 @@
+../NOTICE
\ No newline at end of file
diff --git a/datasketches/src/countmin/sketch.rs 
b/datasketches/src/countmin/sketch.rs
index 484e6c4..7970c0c 100644
--- a/datasketches/src/countmin/sketch.rs
+++ b/datasketches/src/countmin/sketch.rs
@@ -339,6 +339,11 @@ impl<T: CountMinValue> CountMinSketch<T> {
 
     /// Deserializes a sketch from bytes using the default seed.
     ///
+    /// # Errors
+    ///
+    /// Returns `InvalidData` if the image is malformed or its seed hash does 
not match the default
+    /// seed.
+    ///
     /// # Examples
     ///
     /// ```
@@ -417,8 +422,24 @@ impl<T: CountMinValue> CountMinSketch<T> {
         )?;
 
         let entries = entries_for_config_checked(num_hashes, num_buckets)?;
+        let is_empty = (flags & FLAGS_IS_EMPTY) != 0;
+        if !is_empty {
+            let payload_values = entries
+                .checked_add(1)
+                .ok_or_else(|| Error::deserial("CountMin payload value count 
overflows"))?;
+            let payload_bytes = payload_values
+                .checked_mul(LONG_SIZE_BYTES)
+                .ok_or_else(|| Error::deserial("CountMin payload size 
overflows"))?;
+            if payload_bytes > cursor.remaining().len() {
+                return Err(Error::insufficient_data(format!(
+                    "CountMin payload requires {payload_bytes} bytes, got {}",
+                    cursor.remaining().len()
+                )));
+            }
+        }
+
         let mut sketch = Self::make(num_hashes, num_buckets, seed, 
expected_seed_hash, entries);
-        if (flags & FLAGS_IS_EMPTY) != 0 {
+        if is_empty {
             return Ok(sketch);
         }
 
diff --git a/datasketches/src/cpc/sketch.rs b/datasketches/src/cpc/sketch.rs
index 8599e46..a9db202 100644
--- a/datasketches/src/cpc/sketch.rs
+++ b/datasketches/src/cpc/sketch.rs
@@ -36,7 +36,6 @@ use crate::cpc::compression::encode_pairs;
 use crate::cpc::compression::encode_window;
 use crate::cpc::compression_data::COLUMN_PERMUTATIONS_FOR_DECODING;
 use crate::cpc::compression_data::COLUMN_PERMUTATIONS_FOR_ENCODING;
-use crate::cpc::count_bits_set_in_matrix;
 use crate::cpc::determine_correct_offset;
 use crate::cpc::determine_flavor;
 use crate::cpc::estimator::estimate;
@@ -606,6 +605,11 @@ impl CpcSketch {
     }
 
     /// Deserializes a `CpcSketch` from bytes.
+    ///
+    /// # Errors
+    ///
+    /// Returns `InvalidData` if the image is malformed or its seed hash does 
not match the default
+    /// seed.
     pub fn deserialize(bytes: &[u8]) -> Result<Self, Error> {
         Self::deserialize_with_seed(bytes, DEFAULT_UPDATE_SEED)
     }
@@ -926,23 +930,3 @@ impl CpcSketch {
         Ok(max_bytes)
     }
 }
-
-impl CpcSketch {
-    /// Returns `true` if the sketch's internal state is valid.
-    ///
-    /// This is intended for testing and validation purposes.
-    #[doc(hidden)]
-    pub fn validate(&self) -> bool {
-        let bit_matrix = self.build_bit_matrix();
-        let num_bits_set = count_bits_set_in_matrix(&bit_matrix);
-        num_bits_set == self.num_coupons
-    }
-
-    /// Returns the number of coupons in the sketch.
-    ///
-    /// This is intended for testing and validation purposes.
-    #[doc(hidden)]
-    pub fn num_coupons(&self) -> u32 {
-        self.num_coupons
-    }
-}
diff --git a/datasketches/src/cpc/wrapper.rs b/datasketches/src/cpc/wrapper.rs
index c0ee945..b32df27 100644
--- a/datasketches/src/cpc/wrapper.rs
+++ b/datasketches/src/cpc/wrapper.rs
@@ -34,7 +34,7 @@ use crate::cpc::serialization::SERIAL_VERSION;
 use crate::cpc::serialization::make_preamble_ints;
 use crate::error::Error;
 
-/// A read-only view of a serialized `CpcSketch` image.
+/// Cardinality metadata extracted from a serialized `CpcSketch` image.
 #[derive(Debug, Clone)]
 pub struct CpcWrapper {
     lg_k: u8,
@@ -44,7 +44,13 @@ pub struct CpcWrapper {
 }
 
 impl CpcWrapper {
-    /// Creates a new `CpcWrapper` from the given byte slice without copying 
bytes.
+    /// Reads the cardinality metadata without copying or fully deserializing 
the compressed
+    /// payload.
+    ///
+    /// # Errors
+    ///
+    /// Returns `InvalidData` if the preamble is malformed or does not 
describe a compressed CPC
+    /// image.
     pub fn new(bytes: &[u8]) -> Result<Self, Error> {
         let mut cursor = SketchSlice::new(bytes);
         let preamble_ints = cursor
diff --git a/datasketches/src/frequencies/sketch.rs 
b/datasketches/src/frequencies/sketch.rs
index f96bc90..31383e9 100644
--- a/datasketches/src/frequencies/sketch.rs
+++ b/datasketches/src/frequencies/sketch.rs
@@ -748,6 +748,11 @@ impl<T: FrequentItemValue> FrequentItemsSketch<T> {
 
     /// Deserializes a sketch from bytes.
     ///
+    /// # Errors
+    ///
+    /// Returns `InvalidData` if the image is truncated, its metadata is 
inconsistent, or an item
+    /// cannot be decoded by `T`.
+    ///
     /// # Examples
     ///
     /// Built-in support for `i64`:
diff --git a/datasketches/src/hll/sketch.rs b/datasketches/src/hll/sketch.rs
index 4e0ed71..a229b3b 100644
--- a/datasketches/src/hll/sketch.rs
+++ b/datasketches/src/hll/sketch.rs
@@ -294,6 +294,11 @@ impl HllSketch {
 
     /// Deserializes an HLL sketch from bytes.
     ///
+    /// # Errors
+    ///
+    /// Returns `InvalidData` if the image is truncated or contains an invalid 
preamble,
+    /// configuration, or payload.
+    ///
     /// # Examples
     ///
     /// ```
diff --git a/datasketches/src/tdigest/sketch.rs 
b/datasketches/src/tdigest/sketch.rs
index f55c426..942c735 100644
--- a/datasketches/src/tdigest/sketch.rs
+++ b/datasketches/src/tdigest/sketch.rs
@@ -537,6 +537,11 @@ impl TDigestMut {
     /// auto-detected. Use [`deserialize_f32()`](Self::deserialize_f32) for 
the compact
     /// DataSketches C++ `tdigest<float>` format.
     ///
+    /// # Errors
+    ///
+    /// Returns `InvalidData` if the image is truncated, has an unsupported 
format, or contains
+    /// invalid extrema, centroids, or weights.
+    ///
     /// # Examples
     ///
     /// ```
@@ -558,6 +563,11 @@ impl TDigestMut {
     /// This format stores centroid means and weights as `(f32, u32)` and is 
emitted by the C++
     /// `tdigest<float>` implementation. Its header does not identify the 
scalar width, so callers
     /// must select this entry point explicitly.
+    ///
+    /// # Errors
+    ///
+    /// Returns `InvalidData` if the image is truncated, has an unsupported 
format, or contains
+    /// invalid extrema, centroids, or weights.
     pub fn deserialize_f32(bytes: &[u8]) -> Result<Self, Error> {
         Self::deserialize_impl(bytes, true)
     }
@@ -1056,11 +1066,19 @@ impl TDigest {
     /// The format of the [reference 
implementation](https://github.com/tdunning/t-digest) is
     /// auto-detected. Use [`deserialize_f32()`](Self::deserialize_f32) for 
the compact
     /// DataSketches C++ `tdigest<float>` format.
+    ///
+    /// # Errors
+    ///
+    /// Returns `InvalidData` under the same conditions as 
[`TDigestMut::deserialize`].
     pub fn deserialize(bytes: &[u8]) -> Result<Self, Error> {
         Ok(TDigestMut::deserialize(bytes)?.freeze())
     }
 
     /// Deserializes an immutable t-digest from the compact single-precision 
DataSketches format.
+    ///
+    /// # Errors
+    ///
+    /// Returns `InvalidData` under the same conditions as 
[`TDigestMut::deserialize_f32`].
     pub fn deserialize_f32(bytes: &[u8]) -> Result<Self, Error> {
         Ok(TDigestMut::deserialize_f32(bytes)?.freeze())
     }
diff --git a/datasketches/src/thetafamily/theta/intersection.rs 
b/datasketches/src/thetafamily/theta/intersection.rs
index 8f59f9a..ace0d41 100644
--- a/datasketches/src/thetafamily/theta/intersection.rs
+++ b/datasketches/src/thetafamily/theta/intersection.rs
@@ -63,6 +63,11 @@ impl ThetaIntersection {
     /// The intersection can be viewed as starting from the "universe" set,
     /// and every update can reduce the current set to leave the overlapping
     /// subset only.
+    ///
+    /// # Errors
+    ///
+    /// Returns `InvalidArgument` if a non-empty `sketch` has a different seed 
hash or its retained
+    /// entries are inconsistent with its metadata.
     pub fn update<'a>(&mut self, sketch: impl Into<ThetaSketchView<'a>>) -> 
Result<(), Error> {
         let sketch = sketch.into();
         self.state.update(sketch)
diff --git a/datasketches/src/thetafamily/theta/sketch.rs 
b/datasketches/src/thetafamily/theta/sketch.rs
index 88f002e..7fb9ea0 100644
--- a/datasketches/src/thetafamily/theta/sketch.rs
+++ b/datasketches/src/thetafamily/theta/sketch.rs
@@ -686,6 +686,11 @@ impl CompactThetaSketch {
     }
 
     /// Deserializes a compact theta sketch from bytes.
+    ///
+    /// # Errors
+    ///
+    /// Returns `InvalidData` if the image is malformed or its seed hash does 
not match the default
+    /// seed.
     pub fn deserialize(bytes: &[u8]) -> Result<Self, Error> {
         Self::deserialize_with_seed(bytes, DEFAULT_UPDATE_SEED)
     }
diff --git a/datasketches/src/thetafamily/theta/union.rs 
b/datasketches/src/thetafamily/theta/union.rs
index 12dff15..c315c4f 100644
--- a/datasketches/src/thetafamily/theta/union.rs
+++ b/datasketches/src/thetafamily/theta/union.rs
@@ -40,6 +40,11 @@ impl UnionMergePolicy<ThetaEntry> for NoopUnionPolicy {
 
 impl ThetaUnion {
     /// Updates this union with the given sketch.
+    ///
+    /// # Errors
+    ///
+    /// Returns `InvalidArgument` if a non-empty `sketch` has a different seed 
hash from this
+    /// union.
     pub fn update<'a>(&mut self, sketch: impl Into<ThetaSketchView<'a>>) -> 
Result<(), Error> {
         let sketch = sketch.into();
         self.state.update(sketch)
diff --git a/datasketches/src/thetafamily/tuple/sketch.rs 
b/datasketches/src/thetafamily/tuple/sketch.rs
index 796e17e..0fddc98 100644
--- a/datasketches/src/thetafamily/tuple/sketch.rs
+++ b/datasketches/src/thetafamily/tuple/sketch.rs
@@ -618,6 +618,11 @@ impl<S> CompactTupleSketch<S> {
     }
 
     /// Deserializes a compact Tuple sketch using the default seed.
+    ///
+    /// # Errors
+    ///
+    /// Returns `InvalidData` if the image is malformed, its seed hash does 
not match the default
+    /// seed, or a summary cannot be decoded by `S`.
     pub fn deserialize(bytes: &[u8]) -> Result<Self, Error>
     where
         S: TupleSummaryValue,
diff --git a/tests-integration/tests/countmin_test/sketch.rs 
b/tests-integration/tests/countmin_test/sketch.rs
index d22cf30..42f8530 100644
--- a/tests-integration/tests/countmin_test/sketch.rs
+++ b/tests-integration/tests/countmin_test/sketch.rs
@@ -259,6 +259,17 @@ fn test_serialize_deserialize_non_empty_u64() {
     assert_eq!(decoded.estimate(42u64), sketch.estimate(42u64));
 }
 
+#[test]
+fn test_truncated_non_empty_payload_is_rejected_before_table_allocation() {
+    let mut bytes = CountMinSketch::<i64>::new(1, 3).unwrap().serialize();
+    bytes[3] = 0;
+    bytes[8..12].copy_from_slice(&(1u32 << 29).to_le_bytes());
+
+    let error = CountMinSketch::<i64>::deserialize(&bytes).unwrap_err();
+    assert_eq!(error.kind(), ErrorKind::InvalidData);
+    assert!(error.message().contains("payload requires"));
+}
+
 #[test]
 fn test_invalid_hashes_return_error() {
     let error = CountMinSketch::<i64>::new(0, 5).unwrap_err();
diff --git a/tests-integration/tests/cpc_test/union.rs 
b/tests-integration/tests/cpc_test/union.rs
index 9180165..7b28034 100644
--- a/tests-integration/tests/cpc_test/union.rs
+++ b/tests-integration/tests/cpc_test/union.rs
@@ -97,7 +97,6 @@ fn test_sliding_union_matches_single_sketch() {
     }
     let result = union.to_sketch();
     assert!(!result.is_empty());
-    assert!(result.num_coupons() >= 27 * (1 << 11) / 8);
     let estimate = sketch.estimate();
     assert_that!(
         result.estimate(),
diff --git a/tests-integration/tests/cpc_test/update.rs 
b/tests-integration/tests/cpc_test/update.rs
index ab806f6..99a1cb5 100644
--- a/tests-integration/tests/cpc_test/update.rs
+++ b/tests-integration/tests/cpc_test/update.rs
@@ -32,7 +32,6 @@ fn test_empty() {
     assert_eq!(sketch.estimate(), 0.0);
     assert_eq!(sketch.lower_bound(NumStdDev::One), 0.0);
     assert_eq!(sketch.upper_bound(NumStdDev::One), 0.0);
-    assert!(sketch.validate());
 }
 
 #[test]
@@ -43,7 +42,6 @@ fn test_one_value() {
     assert_eq!(sketch.estimate(), 1.0);
     assert_that!(sketch.estimate(), ge(sketch.lower_bound(NumStdDev::One)));
     assert_that!(sketch.estimate(), le(sketch.upper_bound(NumStdDev::One)));
-    assert!(sketch.validate());
 }
 
 #[test]
@@ -59,7 +57,6 @@ fn test_many_values() {
     );
     assert_that!(sketch.estimate(), ge(sketch.lower_bound(NumStdDev::One)));
     assert_that!(sketch.estimate(), le(sketch.upper_bound(NumStdDev::One)));
-    assert!(sketch.validate());
 }
 
 #[test]
diff --git a/tests-integration/tests/serde_tests/cpc.rs 
b/tests-integration/tests/serde_tests/cpc.rs
index cf27650..78c61fd 100644
--- a/tests-integration/tests/serde_tests/cpc.rs
+++ b/tests-integration/tests/serde_tests/cpc.rs
@@ -59,29 +59,17 @@ fn test_sketch_file(path: &Path, expected_cardinality: 
usize) -> CpcSketch {
 
 fn test_sketch_replay(path: &Path, sketch: CpcSketch, inputs: impl 
Iterator<Item = usize>) {
     let initial_estimate = sketch.estimate();
-    let initial_num_coupons = sketch.num_coupons();
 
     let mut sketch = sketch;
     for value in inputs {
         sketch.update(value);
     }
-    assert_eq!(
-        initial_num_coupons,
-        sketch.num_coupons(),
-        "Coupon count changed after replaying input for {}",
-        path.display()
-    );
     assert_eq!(
         initial_estimate,
         sketch.estimate(),
         "Estimate changed after replaying input for {}",
         path.display()
     );
-    assert!(
-        sketch.validate(),
-        "Sketch became invalid after replaying input for {}",
-        path.display()
-    );
 }
 
 #[test]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to