Skip to main content

typed_store/
util.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2026 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::ops::{Bound, RangeBounds};
6
7use bincode::Options;
8use serde::Serialize;
9
10#[inline]
11pub fn be_fix_int_ser<S>(t: &S) -> Vec<u8>
12where
13    S: ?Sized + serde::Serialize,
14{
15    bincode::DefaultOptions::new()
16        .with_big_endian()
17        .with_fixint_encoding()
18        .serialize(t)
19        .expect("failed to serialize via be_fix_int_ser method")
20}
21
22pub(crate) fn iterator_bounds_with_range<K>(
23    range: impl RangeBounds<K>,
24) -> (Option<Vec<u8>>, Option<Vec<u8>>)
25where
26    K: Serialize,
27{
28    let iterator_lower_bound = match range.start_bound() {
29        Bound::Included(lower_bound) => {
30            // Rocksdb lower bound is inclusive by default so nothing to do
31            Some(be_fix_int_ser(&lower_bound))
32        }
33        Bound::Excluded(lower_bound) => {
34            let mut key_buf = be_fix_int_ser(&lower_bound);
35
36            if is_max(&key_buf) {
37                // No representable key strictly greater than the maximum at this byte
38                // length. Append a zero byte so the lower bound is lexicographically
39                // greater than any same-length key, ensuring the iterator yields
40                // nothing — matching the user's intent of excluding the max key.
41                key_buf.push(0);
42            } else {
43                // Since we want exclusive, we need to increment the key to exclude the previous
44                big_endian_add_one(&mut key_buf);
45            }
46            Some(key_buf)
47        }
48        Bound::Unbounded => None,
49    };
50    let iterator_upper_bound = match range.end_bound() {
51        Bound::Included(upper_bound) => {
52            let mut key_buf = be_fix_int_ser(&upper_bound);
53
54            if is_max(&key_buf) {
55                // No same-length key is greater than the maximum, but a longer key
56                // extending it still is. Append a zero byte so the (exclusive) upper
57                // bound is the first such key, keeping any extension out of an
58                // inclusive `..=max` range.
59                key_buf.push(0);
60            } else {
61                // Rocksdb upper bound is exclusive, so increment to make the
62                // caller's inclusive bound inclusive.
63                big_endian_add_one(&mut key_buf);
64            }
65            Some(key_buf)
66        }
67        Bound::Excluded(upper_bound) => {
68            // Rocksdb upper bound is exclusive by default so nothing to do
69            Some(be_fix_int_ser(&upper_bound))
70        }
71        Bound::Unbounded => None,
72    };
73    (iterator_lower_bound, iterator_upper_bound)
74}
75
76/// Computes the raw byte bounds for a prefix scan over keys serialized with
77/// [`be_fix_int_ser`].
78///
79/// `prefix` must serialize to a prefix of the column family's key encoding —
80/// typically the leading field(s) of a tuple key. Returns the half-open byte
81/// range `[ser(prefix), ser(prefix) + 1)`, which selects exactly the keys that
82/// begin with `ser(prefix)`. When the serialized prefix is all `0xFF` there is
83/// no representable upper bound, so the scan runs to the end of the column
84/// family.
85pub(crate) fn prefix_iterator_bounds<P>(prefix: &P) -> (Option<Vec<u8>>, Option<Vec<u8>>)
86where
87    P: ?Sized + Serialize,
88{
89    let lower = be_fix_int_ser(prefix);
90    let upper = if is_max(&lower) {
91        None
92    } else {
93        let mut upper = lower.clone();
94        big_endian_add_one(&mut upper);
95        Some(upper)
96    };
97    (Some(lower), upper)
98}
99
100/// Computes the raw byte bounds for scanning `range` within the key range of
101/// `prefix`, for keys serialized with [`be_fix_int_ser`].
102///
103/// `range` bounds the part of the key that follows `prefix`; see
104/// [`prefix_iterator_bounds`] for the requirements on `prefix`. The returned
105/// bounds never leave the prefix, including at an inclusive bound on the
106/// maximum key.
107pub(crate) fn prefix_iterator_bounds_with_range<P, K>(
108    prefix: &P,
109    range: impl RangeBounds<K>,
110) -> (Option<Vec<u8>>, Option<Vec<u8>>)
111where
112    P: ?Sized + Serialize,
113    K: Serialize,
114{
115    let prefix_buf = be_fix_int_ser(prefix);
116    let (lower_bound, upper_bound) = iterator_bounds_with_range(range);
117
118    let mut iterator_lower_bound = prefix_buf.clone();
119    if let Some(lower_bound) = lower_bound {
120        iterator_lower_bound.extend_from_slice(&lower_bound);
121    }
122
123    let iterator_upper_bound = match upper_bound {
124        Some(upper_bound) => {
125            let mut key_buf = prefix_buf;
126            key_buf.extend_from_slice(&upper_bound);
127            Some(key_buf)
128        }
129        // An unbounded range ends where the prefix ends.
130        None => prefix_iterator_bounds(prefix).1,
131    };
132
133    (Some(iterator_lower_bound), iterator_upper_bound)
134}
135
136/// Increments the big-endian integer in `v` by one, in place.
137///
138/// Callers must ensure `v` is not all-`0xFF` (each one checks `is_max` and
139/// handles that case separately).
140///
141/// # Panics
142///
143/// Panics if `v` is all-`0xFF`. Incrementing it would overflow, and silently
144/// wrapping to zero would emit a wrong key; panicking instead keeps a buggy
145/// caller from corrupting the keyspace.
146fn big_endian_add_one(v: &mut [u8]) {
147    for i in (0..v.len()).rev() {
148        if v[i] == u8::MAX {
149            v[i] = 0;
150        } else {
151            v[i] += 1;
152            return;
153        }
154    }
155    // Reaching here means no byte could be incremented, i.e. `v` was all-`0xFF`
156    // and the carry ran off the top — a violated precondition. Panic rather than
157    // return a silently-wrapped, incorrect key.
158    unreachable!("big_endian_add_one called on an all-0xFF value")
159}
160
161/// Check if all the bytes in the vector are 0xFF
162fn is_max(v: &[u8]) -> bool {
163    v.iter().all(|&x| x == u8::MAX)
164}
165
166#[expect(clippy::assign_op_pattern, clippy::manual_div_ceil)]
167#[test]
168fn test_helpers() {
169    let v = vec![];
170    assert!(is_max(&v));
171
172    fn check_add(v: Vec<u8>) {
173        let mut v = v;
174        let num = Num32::from_big_endian(&v);
175        big_endian_add_one(&mut v);
176        assert!(num + 1 == Num32::from_big_endian(&v));
177    }
178
179    uint::construct_uint! {
180        // 32 byte number
181        struct Num32(4);
182    }
183
184    check_add(vec![1; 32]);
185    check_add(vec![6; 32]);
186    check_add(vec![254; 32]);
187
188    // TBD: More tests coming with randomized arrays
189}
190
191#[test]
192#[should_panic(expected = "all-0xFF")]
193fn big_endian_add_one_panics_on_max() {
194    big_endian_add_one(&mut [0xFFu8; 4]);
195}
196
197#[test]
198fn test_inclusive_upper_bound_at_max() {
199    // The missed case: an inclusive upper bound whose serialization is all-`0xFF`.
200    // The exclusive rocksdb upper bound must be the first key PAST it
201    // (`ser(hi) ++ [0]`), not `None`. With `None` the scan is unbounded, so a
202    // longer key that extends the max (and therefore sorts *after* it) is wrongly
203    // included in `..=hi`. Verify the actual inclusion/exclusion the bound yields,
204    // for single-byte, multi-byte and array maxima.
205    fn check_max<K: Serialize>(max: K) {
206        let hi = be_fix_int_ser(&max);
207        assert!(is_max(&hi), "test value must serialize to all-0xFF");
208        let (_, upper) = iterator_bounds_with_range::<K>((Bound::Unbounded, Bound::Included(max)));
209        let upper = upper.expect("inclusive upper at the max must be bounded, not None");
210
211        // `hi` itself stays inside the (exclusive) bound; a key extending it does not.
212        let mut extension = hi.clone();
213        extension.push(0);
214        assert!(hi < upper, "the max key itself must stay in range");
215        assert!(
216            extension >= upper,
217            "a key extending the max must be excluded"
218        );
219        assert_eq!(upper, extension, "bound must be exactly ser(hi) ++ [0]");
220    }
221    check_max(u8::MAX);
222    check_max(u64::MAX);
223    check_max([0xFFu8; 32]);
224
225    // Symmetric with the excluded-lower arm at the max.
226    let (lower, _) = iterator_bounds_with_range::<u8>((Bound::Excluded(u8::MAX), Bound::Unbounded));
227    assert_eq!(lower, Some(vec![0xFF, 0x00]));
228}
229
230#[test]
231fn prefixed_bounds_stay_within_the_prefix() {
232    // An inclusive upper bound at the maximum key must stay inside the prefix.
233    // Computing it over the whole `(prefix, key)` buffer would carry into the
234    // next prefix, whose keys can serialize more compactly and would then be
235    // scanned.
236    fn check_max<K: Serialize + Copy>(max: K) {
237        assert!(
238            is_max(&be_fix_int_ser(&max)),
239            "test value must serialize to all-0xFF"
240        );
241        let (lower, upper) = prefix_iterator_bounds_with_range(&0u8, ..=max);
242        let upper = upper.expect("an inclusive upper bound must be bounded, not None");
243
244        assert_eq!(lower, Some(be_fix_int_ser(&0u8)));
245        assert!(
246            be_fix_int_ser(&(0u8, max)) < upper,
247            "the maximum key of the prefix must stay in range"
248        );
249        assert!(
250            upper <= be_fix_int_ser(&1u8),
251            "the bound must not reach into the next prefix"
252        );
253    }
254    check_max(u8::MAX);
255    check_max(u32::MAX);
256    check_max(u64::MAX);
257
258    // The excluded-lower arm at the maximum is prefixed the same way.
259    let (lower, _) = prefix_iterator_bounds_with_range::<u8, u8>(
260        &0u8,
261        (Bound::Excluded(u8::MAX), Bound::Unbounded),
262    );
263    assert_eq!(lower, Some(vec![0x00, 0xFF, 0x00]));
264
265    // An unbounded range ends where the prefix ends, and at the maximum prefix
266    // it runs to the end of the column family — as for a plain prefix scan.
267    assert_eq!(
268        prefix_iterator_bounds_with_range::<u8, u32>(&0u8, ..),
269        (Some(vec![0x00]), Some(vec![0x01]))
270    );
271    assert_eq!(
272        prefix_iterator_bounds_with_range::<u8, u32>(&u8::MAX, ..),
273        prefix_iterator_bounds(&u8::MAX)
274    );
275}