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/// Increments the big-endian integer in `v` by one, in place.
101///
102/// Callers must ensure `v` is not all-`0xFF` (each one checks `is_max` and
103/// handles that case separately).
104///
105/// # Panics
106///
107/// Panics if `v` is all-`0xFF`. Incrementing it would overflow, and silently
108/// wrapping to zero would emit a wrong key; panicking instead keeps a buggy
109/// caller from corrupting the keyspace.
110fn big_endian_add_one(v: &mut [u8]) {
111 for i in (0..v.len()).rev() {
112 if v[i] == u8::MAX {
113 v[i] = 0;
114 } else {
115 v[i] += 1;
116 return;
117 }
118 }
119 // Reaching here means no byte could be incremented, i.e. `v` was all-`0xFF`
120 // and the carry ran off the top — a violated precondition. Panic rather than
121 // return a silently-wrapped, incorrect key.
122 unreachable!("big_endian_add_one called on an all-0xFF value")
123}
124
125/// Check if all the bytes in the vector are 0xFF
126fn is_max(v: &[u8]) -> bool {
127 v.iter().all(|&x| x == u8::MAX)
128}
129
130#[expect(clippy::assign_op_pattern, clippy::manual_div_ceil)]
131#[test]
132fn test_helpers() {
133 let v = vec![];
134 assert!(is_max(&v));
135
136 fn check_add(v: Vec<u8>) {
137 let mut v = v;
138 let num = Num32::from_big_endian(&v);
139 big_endian_add_one(&mut v);
140 assert!(num + 1 == Num32::from_big_endian(&v));
141 }
142
143 uint::construct_uint! {
144 // 32 byte number
145 struct Num32(4);
146 }
147
148 check_add(vec![1; 32]);
149 check_add(vec![6; 32]);
150 check_add(vec![254; 32]);
151
152 // TBD: More tests coming with randomized arrays
153}
154
155#[test]
156#[should_panic(expected = "all-0xFF")]
157fn big_endian_add_one_panics_on_max() {
158 big_endian_add_one(&mut [0xFFu8; 4]);
159}
160
161#[test]
162fn test_inclusive_upper_bound_at_max() {
163 // The missed case: an inclusive upper bound whose serialization is all-`0xFF`.
164 // The exclusive rocksdb upper bound must be the first key PAST it
165 // (`ser(hi) ++ [0]`), not `None`. With `None` the scan is unbounded, so a
166 // longer key that extends the max (and therefore sorts *after* it) is wrongly
167 // included in `..=hi`. Verify the actual inclusion/exclusion the bound yields,
168 // for single-byte, multi-byte and array maxima.
169 fn check_max<K: Serialize>(max: K) {
170 let hi = be_fix_int_ser(&max);
171 assert!(is_max(&hi), "test value must serialize to all-0xFF");
172 let (_, upper) = iterator_bounds_with_range::<K>((Bound::Unbounded, Bound::Included(max)));
173 let upper = upper.expect("inclusive upper at the max must be bounded, not None");
174
175 // `hi` itself stays inside the (exclusive) bound; a key extending it does not.
176 let mut extension = hi.clone();
177 extension.push(0);
178 assert!(hi < upper, "the max key itself must stay in range");
179 assert!(
180 extension >= upper,
181 "a key extending the max must be excluded"
182 );
183 assert_eq!(upper, extension, "bound must be exactly ser(hi) ++ [0]");
184 }
185 check_max(u8::MAX);
186 check_max(u64::MAX);
187 check_max([0xFFu8; 32]);
188
189 // Symmetric with the excluded-lower arm at the max.
190 let (lower, _) = iterator_bounds_with_range::<u8>((Bound::Excluded(u8::MAX), Bound::Unbounded));
191 assert_eq!(lower, Some(vec![0xFF, 0x00]));
192}