Skip to main content

netindex/
values.rs

1//! Optional sharing of byte values across records. Schemas and scalar encoding remain caller-owned.
2
3use std::{collections::hash_map::RandomState, fmt, hash::BuildHasher, io::Write};
4
5use hashbrown::HashTable;
6
7use crate::{Error, Limits, layout::u32_at};
8
9const MAGIC: &[u8; 8] = b"NIVALUES";
10const HEADER: usize = 12;
11
12/// Build a shared value pool without retaining a second copy of each value.
13pub struct ValuePoolBuilder {
14    limits: Limits,
15    offsets: Vec<u32>,
16    bytes: Vec<u8>,
17    hashes: HashTable<(u64, u32)>,
18    hash_state: RandomState,
19}
20
21impl ValuePoolBuilder {
22    /// Bound unique values with `limits.records` and complete pool bytes with `limits.bytes`.
23    pub fn new(limits: Limits) -> Self {
24        Self {
25            limits,
26            offsets: Vec::new(),
27            bytes: Vec::new(),
28            hashes: HashTable::new(),
29            hash_state: RandomState::new(),
30        }
31    }
32
33    /// Return a stable zero-based ID, reusing identical byte values.
34    ///
35    /// Failed insertions leave existing IDs and bytes intact. Hash collisions are
36    /// resolved by byte equality. IDs belong to this pool, not another release.
37    ///
38    /// # Errors
39    /// Fails on configured limits, unrepresentable offsets, or allocation failure.
40    pub fn intern(&mut self, value: &[u8]) -> Result<u32, Error> {
41        let hash = self.hash_state.hash_one(value);
42
43        for &(_, id) in self.hashes.iter_hash(hash) {
44            let start = *self
45                .offsets
46                .get(id as usize)
47                .ok_or(Error::Invalid("value ID"))? as usize;
48            let end = self
49                .offsets
50                .get(id as usize + 1)
51                .map_or(self.bytes.len(), |v| *v as usize);
52
53            if self.bytes.get(start..end) == Some(value) {
54                return Ok(id);
55            }
56        }
57
58        let count = self
59            .offsets
60            .len()
61            .checked_add(1)
62            .ok_or(Error::Limit("shared values"))?;
63        let size = self
64            .bytes
65            .len()
66            .checked_add(value.len())
67            .ok_or(Error::Limit("shared value bytes"))?;
68        check_size(count, size, self.limits)?;
69
70        let id = u32::try_from(self.offsets.len()).map_err(|_| Error::Limit("shared values"))?;
71        let offset =
72            u32::try_from(self.bytes.len()).map_err(|_| Error::Limit("shared value bytes"))?;
73
74        self.offsets.try_reserve(1)?;
75        self.bytes.try_reserve(value.len())?;
76        self.hashes.try_reserve(1, |row| row.0)?;
77
78        self.offsets.push(offset);
79        self.bytes.extend_from_slice(value);
80        self.hashes.insert_unique(hash, (hash, id), |row| row.0);
81
82        Ok(id)
83    }
84
85    /// Number of unique values.
86    pub fn len(&self) -> usize {
87        self.offsets.len()
88    }
89
90    /// Whether no values have been inserted.
91    pub fn is_empty(&self) -> bool {
92        self.offsets.is_empty()
93    }
94
95    /// Encode the pool into owned bytes.
96    ///
97    /// # Errors
98    /// Fails on limits, allocation, or output failure.
99    pub fn into_bytes(self) -> Result<Vec<u8>, Error> {
100        let size = self.encoded_length()?;
101        let mut output = Vec::new();
102        output.try_reserve_exact(size)?;
103        self.write_to(&mut output)?;
104
105        Ok(output)
106    }
107
108    /// Write offsets followed by value bytes. Failure may leave partial output.
109    ///
110    /// # Errors
111    /// Fails on limits or output failure. Publication and synchronization belong to the caller.
112    pub fn write_to(self, mut output: impl Write) -> Result<(), Error> {
113        self.encoded_length()?;
114        drop(self.hashes);
115
116        output.write_all(MAGIC)?;
117        output.write_all(&(self.offsets.len() as u32).to_le_bytes())?;
118
119        for offset in self.offsets {
120            output.write_all(&offset.to_le_bytes())?;
121        }
122
123        output.write_all(&(self.bytes.len() as u32).to_le_bytes())?;
124        output.write_all(&self.bytes)?;
125        output.flush()?;
126
127        Ok(())
128    }
129
130    pub(crate) fn encoded_length(&self) -> Result<usize, Error> {
131        check_size(self.offsets.len(), self.bytes.len(), self.limits)
132    }
133}
134
135impl fmt::Debug for ValuePoolBuilder {
136    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
137        formatter
138            .debug_struct("ValuePoolBuilder")
139            .field("values", &self.len())
140            .field("value_bytes", &self.bytes.len())
141            .field("limits", &self.limits)
142            .finish()
143    }
144}
145
146/// Validated shared values borrowing immutable caller-owned bytes.
147#[derive(Clone, Copy)]
148pub struct ValuePool<'a> {
149    offsets: &'a [[u8; 4]],
150    body: &'a [u8],
151}
152
153impl<'a> ValuePool<'a> {
154    /// Validate offsets, exact length, and resource limits without allocating.
155    ///
156    /// Value bodies remain opaque. A pool may live in index metadata or a separate
157    /// mapped file. Replace records and their pool together so IDs stay consistent.
158    ///
159    /// # Errors
160    /// Fails on malformed bytes or configured limits.
161    pub fn open(bytes: &'a [u8], limits: Limits) -> Result<Self, Error> {
162        if bytes.len() > limits.bytes {
163            return Err(Error::Limit("shared value bytes"));
164        }
165
166        if bytes.get(..8) != Some(MAGIC.as_slice()) {
167            return Err(Error::Invalid("value pool magic"));
168        }
169
170        let count = u32_at(bytes, 8)? as usize;
171        if count > limits.records {
172            return Err(Error::Limit("shared values"));
173        }
174
175        let data = count
176            .checked_add(1)
177            .and_then(|v| v.checked_mul(4))
178            .and_then(|v| v.checked_add(HEADER))
179            .ok_or(Error::Invalid("value offsets"))?;
180        let body = bytes.get(data..).ok_or(Error::Invalid("value offsets"))?;
181
182        if u32_at(bytes, HEADER)? != 0 || u32_at(bytes, data - 4)? as usize != body.len() {
183            return Err(Error::Invalid("value pool length"));
184        }
185
186        let mut previous = 0;
187        for id in 0..count {
188            let next = u32_at(bytes, HEADER + (id + 1) * 4)? as usize;
189            if next < previous || next > body.len() {
190                return Err(Error::Invalid("value offset order"));
191            }
192
193            previous = next;
194        }
195
196        let directory = bytes
197            .get(HEADER..data)
198            .ok_or(Error::Invalid("value offsets"))?;
199        let (offsets, _) = directory.as_chunks::<4>();
200
201        Ok(Self { offsets, body })
202    }
203
204    /// Number of values in the pool.
205    pub fn len(self) -> usize {
206        self.offsets.len() - 1
207    }
208
209    /// Whether the pool contains no values.
210    pub fn is_empty(self) -> bool {
211        self.len() == 0
212    }
213
214    /// Borrow a value by ID, without allocation or schema decoding.
215    ///
216    /// # Errors
217    /// Fails when the ID is outside this pool.
218    #[inline]
219    pub fn get(self, id: u32) -> Result<&'a [u8], Error> {
220        let id = id as usize;
221        let &[start, end] = self
222            .offsets
223            .get(id..)
224            .and_then(|offsets| offsets.first_chunk::<2>())
225            .ok_or(Error::Invalid("value ID"))?;
226
227        self.body
228            .get(u32::from_le_bytes(start) as usize..u32::from_le_bytes(end) as usize)
229            .ok_or(Error::Invalid("truncated section"))
230    }
231}
232
233impl fmt::Debug for ValuePool<'_> {
234    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
235        formatter
236            .debug_struct("ValuePool")
237            .field("values", &self.len())
238            .field(
239                "pool_bytes",
240                &(HEADER + self.offsets.len() * 4 + self.body.len()),
241            )
242            .finish()
243    }
244}
245
246/// Shared strings validated once over immutable bytes, without a decoded-string cache.
247#[derive(Debug, Clone, Copy)]
248pub struct StringPool<'a> {
249    values: ValuePool<'a>,
250}
251
252impl<'a> StringPool<'a> {
253    /// Validate the value directory and every string's UTF-8 before serving.
254    ///
255    /// # Errors
256    /// Fails on malformed data, exceeded limits, or invalid UTF-8.
257    pub fn open(bytes: &'a [u8], limits: Limits) -> Result<Self, Error> {
258        let values = ValuePool::open(bytes, limits)?;
259
260        #[cfg(feature = "simd-utf8")]
261        let body = simdutf8::compat::from_utf8(values.body);
262        #[cfg(not(feature = "simd-utf8"))]
263        let body = std::str::from_utf8(values.body);
264
265        let valid = body.is_ok_and(|body| {
266            values
267                .offsets
268                .iter()
269                .all(|offset| body.is_char_boundary(u32::from_le_bytes(*offset) as usize))
270        });
271
272        if !valid {
273            // Keep errors relative to the first invalid value, including split code points.
274            for id in 0..values.len() {
275                std::str::from_utf8(values.get(id as u32)?)?;
276            }
277        }
278
279        Ok(Self { values })
280    }
281
282    /// Number of shared strings.
283    pub fn len(self) -> usize {
284        self.values.len()
285    }
286
287    /// Whether there are no strings.
288    pub fn is_empty(self) -> bool {
289        self.values.is_empty()
290    }
291
292    /// Borrow a string by ID without copying or repeating UTF-8 validation.
293    ///
294    /// # Errors
295    /// Fails if the ID is outside this pool.
296    #[inline]
297    pub fn get(self, id: u32) -> Result<&'a str, Error> {
298        let bytes = self.values.get(id)?;
299
300        // SAFETY: `open` validates every value as UTF-8. Fields are private and
301        // the backing slice remains immutable for the lifetime of this pool.
302        Ok(unsafe { std::str::from_utf8_unchecked(bytes) })
303    }
304}
305
306fn check_size(count: usize, bytes: usize, limits: Limits) -> Result<usize, Error> {
307    if count > limits.records || count > u32::MAX as usize {
308        return Err(Error::Limit("shared values"));
309    }
310
311    let size = count
312        .checked_add(1)
313        .and_then(|v| v.checked_mul(4))
314        .and_then(|v| v.checked_add(HEADER))
315        .and_then(|v| v.checked_add(bytes))
316        .ok_or(Error::Limit("shared value bytes"))?;
317
318    if size > limits.bytes || bytes > u32::MAX as usize {
319        return Err(Error::Limit("shared value bytes"));
320    }
321
322    Ok(size)
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328
329    #[test]
330    fn hash_collisions_compare_value_bytes_before_reusing_ids() -> Result<(), Error> {
331        let mut builder = ValuePoolBuilder::new(Limits::default());
332        let first = builder.intern(b"first")?;
333        let hash = builder.hash_state.hash_one(b"other");
334        builder
335            .hashes
336            .insert_unique(hash, (hash, first), |row| row.0);
337
338        let other = builder.intern(b"other")?;
339        assert_ne!(first, other);
340        assert_eq!(builder.intern(b"other")?, other);
341
342        let bytes = builder.into_bytes()?;
343        let pool = ValuePool::open(&bytes, Limits::default())?;
344
345        assert_eq!(pool.get(first)?, b"first");
346        assert_eq!(pool.get(other)?, b"other");
347
348        Ok(())
349    }
350}