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 292000d refactor: harden library usability before 0.5.0 (#253)
292000d is described below
commit 292000dd636be3074cb73700539910b30cee26b1
Author: tison <[email protected]>
AuthorDate: Mon Aug 31 20:08:33 2026 +0800
refactor: harden library usability before 0.5.0 (#253)
---
CHANGELOG.md | 3 ++-
datasketches/src/common/num_std_dev.rs | 6 ++---
datasketches/src/frequencies/serialization.rs | 11 +++++++--
datasketches/src/frequencies/sketch.rs | 2 --
datasketches/src/req/iter.rs | 8 +++----
datasketches/src/req/sketch.rs | 27 ++++++++++++++++------
datasketches/src/tdigest/sketch.rs | 18 +++++++++++++++
tests-integration/tests/req_test/bounds.rs | 24 ++++++++++---------
tests-integration/tests/req_test/property.rs | 5 ++--
tests-integration/tests/req_test/structure.rs | 6 ++---
tests-integration/tests/serde_tests/frequencies.rs | 10 ++++++++
tests-integration/tests/serde_tests/req.rs | 5 ++++
12 files changed, 90 insertions(+), 35 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3f1f1bc..153a92f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,7 +25,7 @@ All significant changes to this project will be documented in
this file.
### New features
* `TDigest` can now be serialized and deserialized directly without converting
through `TDigestMut` at the call site.
-* Add Relative Error Quantiles (REQ) sketches behind the `req` feature,
including configurable high- or low-rank accuracy, rank, quantile, PMF, and CDF
queries, merging, totally ordered custom item types, the `ReqFloat` adapter for
non-NaN floating-point values, and C++/Java-compatible serialization.
+* Add Relative Error Quantiles (REQ) sketches behind the `req` feature,
including configurable high- or low-rank accuracy, rank, quantile, PMF, and CDF
queries, typed rank confidence bounds, merging, totally ordered custom item
types, the `ReqFloat` adapter for non-NaN floating-point values, and
C++/Java-compatible serialization.
### Performance improvements
@@ -38,6 +38,7 @@ All significant changes to this project will be documented in
this file.
* 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.
* `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.
* T-Digest rejects truncated serialized payloads before allocating, and
updating a deserialized digest no longer allows its buffered state to grow
without bound.
* Compact HLL4 images now restore all register values correctly.
diff --git a/datasketches/src/common/num_std_dev.rs
b/datasketches/src/common/num_std_dev.rs
index 2aa908f..99ef21f 100644
--- a/datasketches/src/common/num_std_dev.rs
+++ b/datasketches/src/common/num_std_dev.rs
@@ -31,9 +31,9 @@ static DELTA_OF_NUM_STD_DEVS: [f64; 4] = [
/// Number of standard deviations for confidence bounds.
///
/// This enum specifies the number of standard deviations to use when computing
-/// upper and lower bounds for cardinality estimates. Higher values provide
wider
-/// confidence intervals with greater certainty that the true cardinality falls
-/// within the bounds.
+/// upper and lower bounds for sketch estimates. Higher values provide wider
+/// confidence intervals with greater certainty that the true value falls
within
+/// the bounds.
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum NumStdDev {
diff --git a/datasketches/src/frequencies/serialization.rs
b/datasketches/src/frequencies/serialization.rs
index f1f8f0c..5ef3bf2 100644
--- a/datasketches/src/frequencies/serialization.rs
+++ b/datasketches/src/frequencies/serialization.rs
@@ -56,9 +56,16 @@ impl FrequentItemValue for String {
fn deserialize_value(cursor: &mut SketchSlice<'_>) -> Result<Self, Error> {
let len = cursor.read_u32_le().map_err(|_| {
Error::insufficient_data("failed to read string item
length".to_string())
- })?;
+ })? as usize;
+
+ let remaining = cursor.remaining().len();
+ if len > remaining {
+ return Err(Error::insufficient_data(format!(
+ "string item length ({len}) exceeds the remaining {remaining}
bytes"
+ )));
+ }
- let mut slice = vec![0; len as usize];
+ let mut slice = vec![0; len];
cursor.read_exact(&mut slice).map_err(|_| {
Error::insufficient_data("failed to read string item
bytes".to_string())
})?;
diff --git a/datasketches/src/frequencies/sketch.rs
b/datasketches/src/frequencies/sketch.rs
index 7a52045..f96bc90 100644
--- a/datasketches/src/frequencies/sketch.rs
+++ b/datasketches/src/frequencies/sketch.rs
@@ -365,7 +365,6 @@ impl<T: Eq + Hash> FrequentItemsSketch<T> {
if count == 0 {
return;
}
- assert!(count > 0, "count may not be negative");
self.stream_weight += count;
self.hash_map.adjust_or_put_value(item, count);
self.maybe_resize_or_purge();
@@ -418,7 +417,6 @@ impl<T: Eq + Hash> FrequentItemsSketch<T> {
if count == 0 {
return;
}
- assert!(count > 0, "count may not be negative");
self.stream_weight += count;
self.hash_map.adjust_or_put_value_ref(item, count);
self.maybe_resize_or_purge();
diff --git a/datasketches/src/req/iter.rs b/datasketches/src/req/iter.rs
index ba5c0f3..b36c2f4 100644
--- a/datasketches/src/req/iter.rs
+++ b/datasketches/src/req/iter.rs
@@ -24,7 +24,7 @@ use crate::req::compactor::Compactor;
/// Provides access to all items in the sketch along with their weights,
/// which depend on the level of the compactor they're stored in.
///
-/// Zero-allocation implementation that works directly with slices.
+/// Items are borrowed from the sketch, so iteration does not clone or
allocate.
pub struct ReqSketchIterator<'a, T> {
compactors: &'a [Compactor<T>],
current_level: usize,
@@ -64,14 +64,14 @@ impl<'a, T: Clone + Ord> ReqSketchIterator<'a, T> {
}
}
-impl<T: Clone + Ord> Iterator for ReqSketchIterator<'_, T> {
- type Item = (T, u64);
+impl<'a, T: Clone + Ord> Iterator for ReqSketchIterator<'a, T> {
+ type Item = (&'a T, u64);
fn next(&mut self) -> Option<Self::Item> {
loop {
if let Some(ref mut level_iter) = self.current_level_iter {
if let Some(item) = level_iter.next() {
- return Some((item.clone(), self.current_weight));
+ return Some((item, self.current_weight));
}
}
diff --git a/datasketches/src/req/sketch.rs b/datasketches/src/req/sketch.rs
index af93c6c..5826ce3 100644
--- a/datasketches/src/req/sketch.rs
+++ b/datasketches/src/req/sketch.rs
@@ -21,6 +21,7 @@ use crate::codec::SketchBytes;
use crate::codec::SketchSlice;
use crate::codec::assert::insufficient_data;
use crate::codec::family::Family;
+use crate::common::NumStdDev;
use crate::error::Error;
use crate::req::DEFAULT_K;
use crate::req::INITIAL_SECTIONS_PER_COMPACTOR;
@@ -351,25 +352,25 @@ where
Ok(())
}
- /// Returns the lower bound for the rank of a quantile at `num_std_dev`
confidence.
- pub fn rank_lower_bound(&self, rank: f64, num_std_dev: u8) -> f64 {
+ /// Returns the lower bound for the normalized `rank` at the requested
confidence level.
+ pub fn rank_lower_bound(&self, rank: f64, num_std_dev: NumStdDev) -> f64 {
self.compute_rank_lower_bound(
self.k,
self.compactors.len() as u8,
rank,
- num_std_dev,
+ num_std_dev.as_u8(),
self.n,
matches!(self.rank_accuracy, RankAccuracy::HighRank),
)
}
- /// Returns the upper bound for the rank of a quantile at `num_std_dev`
confidence.
- pub fn rank_upper_bound(&self, rank: f64, num_std_dev: u8) -> f64 {
+ /// Returns the upper bound for the normalized `rank` at the requested
confidence level.
+ pub fn rank_upper_bound(&self, rank: f64, num_std_dev: NumStdDev) -> f64 {
self.compute_rank_upper_bound(
self.k,
self.compactors.len() as u8,
rank,
- num_std_dev,
+ num_std_dev.as_u8(),
self.n,
matches!(self.rank_accuracy, RankAccuracy::HighRank),
)
@@ -689,9 +690,21 @@ where
}
}
- if n == 0 || min_item.is_none() || max_item.is_none() {
+ if n == 0 {
return Err(Error::deserial("non-empty REQ sketch contains no
items"));
}
+ let (Some(min), Some(max)) = (&min_item, &max_item) else {
+ return Err(Error::deserial("non-empty REQ sketch contains no
items"));
+ };
+ if compactors
+ .iter()
+ .flat_map(Compactor::iter)
+ .any(|item| item < min || item > max)
+ {
+ return Err(Error::deserial(
+ "REQ retained item falls outside the min/max range",
+ ));
+ }
let expected_raw_items = num_levels == 1 && n <= RAW_ITEMS_THRESHOLD;
if raw_items != expected_raw_items {
diff --git a/datasketches/src/tdigest/sketch.rs
b/datasketches/src/tdigest/sketch.rs
index b70ce51..f55c426 100644
--- a/datasketches/src/tdigest/sketch.rs
+++ b/datasketches/src/tdigest/sketch.rs
@@ -384,6 +384,11 @@ impl TDigestMut {
/// Returns the cumulative distribution approximation described by
[`TDigest::cdf`].
///
+ /// # Panics
+ ///
+ /// Panics if `split_points` is not unique, not monotonically increasing,
or contains `NaN`
+ /// values.
+ ///
/// # Examples
///
/// ```
@@ -408,6 +413,11 @@ impl TDigestMut {
/// Returns the probability mass approximation described by
[`TDigest::pmf`].
///
+ /// # Panics
+ ///
+ /// Panics if `split_points` is not unique, not monotonically increasing,
or contains `NaN`
+ /// values.
+ ///
/// # Examples
///
/// ```
@@ -432,6 +442,10 @@ impl TDigestMut {
/// Returns the normalized rank described by [`TDigest::rank`].
///
+ /// # Panics
+ ///
+ /// Panics if `value` is `NaN`.
+ ///
/// # Examples
///
/// ```
@@ -466,6 +480,10 @@ impl TDigestMut {
/// Returns the quantile described by [`TDigest::quantile`].
///
+ /// # Panics
+ ///
+ /// Panics if `rank` is outside `[0.0, 1.0]`.
+ ///
/// # Examples
///
/// ```
diff --git a/tests-integration/tests/req_test/bounds.rs
b/tests-integration/tests/req_test/bounds.rs
index b89b743..5caab94 100644
--- a/tests-integration/tests/req_test/bounds.rs
+++ b/tests-integration/tests/req_test/bounds.rs
@@ -17,6 +17,7 @@
//! Rank error bounds and sigma coverage for ReqSketch.
+use datasketches::common::NumStdDev;
use datasketches::error::Error;
use datasketches::req::RankAccuracy;
use datasketches::req::ReqSketch;
@@ -39,7 +40,8 @@ fn bounds_are_nested_and_in_unit_interval() {
}
for rank in [0.01, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99, 0.999] {
- let bounds: Vec<(f64, f64)> = (1..=3u8)
+ let bounds: Vec<(f64, f64)> = [NumStdDev::One, NumStdDev::Two,
NumStdDev::Three]
+ .into_iter()
.map(|sigma| {
(
sketch.rank_lower_bound(rank, sigma),
@@ -76,8 +78,8 @@ fn theoretical_error_bounds_cover_uniform_quantiles() ->
Result<(), Error> {
] {
let true_quantile = req_f64(rank * (n - 1) as f64);
let estimated_rank = sketch.rank(&true_quantile,
SearchCriteria::Inclusive)?;
- let lower = sketch.rank_lower_bound(rank, 3);
- let upper = sketch.rank_upper_bound(rank, 3);
+ let lower = sketch.rank_lower_bound(rank, NumStdDev::Three);
+ let upper = sketch.rank_upper_bound(rank, NumStdDev::Three);
assert_that!(estimated_rank, all!(ge(lower), le(upper)), "rank:
{rank}");
}
@@ -95,10 +97,10 @@ fn hra_and_lra_bounds_are_tighter_at_their_target_end() ->
Result<(), Error> {
lra.update(req_f64(i as f64));
}
- let hra_error =
- (rank - hra.rank_lower_bound(rank,
2)).max(hra.rank_upper_bound(rank, 2) - rank);
- let lra_error =
- (rank - lra.rank_lower_bound(rank,
2)).max(lra.rank_upper_bound(rank, 2) - rank);
+ let hra_error = (rank - hra.rank_lower_bound(rank, NumStdDev::Two))
+ .max(hra.rank_upper_bound(rank, NumStdDev::Two) - rank);
+ let lra_error = (rank - lra.rank_lower_bound(rank, NumStdDev::Two))
+ .max(lra.rank_upper_bound(rank, NumStdDev::Two) - rank);
if rank >= 0.75 {
assert_that!(hra_error, le(lra_error));
@@ -121,8 +123,8 @@ fn exact_mode_bounds_are_tight() {
assert!(!sketch.is_estimation_mode());
for rank in [0.1, 0.25, 0.5, 0.75, 0.9] {
- let lower = sketch.rank_lower_bound(rank, 2);
- let upper = sketch.rank_upper_bound(rank, 2);
+ let lower = sketch.rank_lower_bound(rank, NumStdDev::Two);
+ let upper = sketch.rank_upper_bound(rank, NumStdDev::Two);
assert_that!((upper - lower) / 2.0, lt(0.05));
}
}
@@ -156,8 +158,8 @@ fn high_rank_accuracy_matches_tight_thresholds() {
}
for rank in [0.9, 0.99, 0.999] {
- let lower = sketch.rank_lower_bound(rank, 3);
- let upper = sketch.rank_upper_bound(rank, 3);
+ let lower = sketch.rank_lower_bound(rank, NumStdDev::Three);
+ let upper = sketch.rank_upper_bound(rank, NumStdDev::Three);
assert_that!(rank, all!(ge(lower), le(upper)));
}
}
diff --git a/tests-integration/tests/req_test/property.rs
b/tests-integration/tests/req_test/property.rs
index e993c4d..9df8c2c 100644
--- a/tests-integration/tests/req_test/property.rs
+++ b/tests-integration/tests/req_test/property.rs
@@ -17,6 +17,7 @@
//! Property-based ReqSketch tests.
+use datasketches::common::NumStdDev;
use datasketches::req::ReqSketch;
use datasketches::req::SearchCriteria;
use quickcheck::Gen;
@@ -56,8 +57,8 @@ fn prop_quantile_rank_consistency() {
// interval for the target rank (plus a small cushion for snapping
to a
// stored item). This scales with k and n, unlike a fixed slack,
so it
// actually constrains the result instead of always passing.
- let lower = sketch.rank_lower_bound(rank, 3) - 0.02;
- let upper = sketch.rank_upper_bound(rank, 3) + 0.02;
+ let lower = sketch.rank_lower_bound(rank, NumStdDev::Three) - 0.02;
+ let upper = sketch.rank_upper_bound(rank, NumStdDev::Three) + 0.02;
assert!(
(lower..=upper).contains(&recovered),
"rank {rank} -> quantile {quantile} -> recovered {recovered},
expected within [{lower:.4}, {upper:.4}]"
diff --git a/tests-integration/tests/req_test/structure.rs
b/tests-integration/tests/req_test/structure.rs
index e14cd79..8eff732 100644
--- a/tests-integration/tests/req_test/structure.rs
+++ b/tests-integration/tests/req_test/structure.rs
@@ -41,8 +41,8 @@ fn iterator_weights_sum_to_n_and_items_are_in_range() {
for (item, weight) in sketch.iter() {
assert_that!(weight, ge(1));
- assert_that!(item, ge(*sketch.min_item().expect("non-empty sketch")));
- assert_that!(item, le(*sketch.max_item().expect("non-empty sketch")));
+ assert_that!(*item, ge(*sketch.min_item().expect("non-empty sketch")));
+ assert_that!(*item, le(*sketch.max_item().expect("non-empty sketch")));
}
}
@@ -54,7 +54,7 @@ fn small_sketch_iterator_reports_unit_weights() {
sketch.update(req_f64(i as f64));
}
- let items: Vec<(ReqF64, u64)> = sketch.iter().collect();
+ let items: Vec<(&ReqF64, u64)> = sketch.iter().collect();
assert_eq!(items.len(), 10);
let weights: Vec<_> = items.iter().map(|&(_, weight)| weight).collect();
assert_that!(weights, each(eq(&1)));
diff --git a/tests-integration/tests/serde_tests/frequencies.rs
b/tests-integration/tests/serde_tests/frequencies.rs
index 6801b57..71bd62c 100644
--- a/tests-integration/tests/serde_tests/frequencies.rs
+++ b/tests-integration/tests/serde_tests/frequencies.rs
@@ -78,6 +78,16 @@ fn test_items_round_trip() {
assert_eq!(restored.maximum_error(), sketch.maximum_error());
}
+#[test]
+fn test_string_deserialize_rejects_length_larger_than_input() {
+ let bytes = 1024u32.to_le_bytes();
+ let mut cursor = SketchSlice::new(&bytes);
+
+ let error = String::deserialize_value(&mut cursor).unwrap_err();
+ assert_eq!(error.kind(), ErrorKind::InvalidData);
+ assert_that!(error.message(), contains_substring("exceeds the remaining"));
+}
+
#[test]
fn test_non_clone_item_round_trip() {
let mut sketch =
FrequentItemsSketch::<NonCloneSerializableItem>::new(32).unwrap();
diff --git a/tests-integration/tests/serde_tests/req.rs
b/tests-integration/tests/serde_tests/req.rs
index 0138664..b097ed0 100644
--- a/tests-integration/tests/serde_tests/req.rs
+++ b/tests-integration/tests/serde_tests/req.rs
@@ -462,6 +462,11 @@ fn deserialize_rejects_invalid_extrema_and_raw_nan() {
reversed[20..24].copy_from_slice(&1.0f32.to_le_bytes());
assert_invalid_data(&reversed);
+ let mut extrema_exclude_retained_items = estimation_image(12, 1_000);
+
extrema_exclude_retained_items[16..20].copy_from_slice(&500.0f32.to_le_bytes());
+
extrema_exclude_retained_items[20..24].copy_from_slice(&500.0f32.to_le_bytes());
+ assert_invalid_data(&extrema_exclude_retained_items);
+
let mut raw_nan = vec![2u8, 1, 17, 8 | 16, 12, 0, 1, 1];
raw_nan.extend_from_slice(&f32::NAN.to_le_bytes());
assert_invalid_data(&raw_nan);
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]