Skip to main content

urna_format/encoding/
intpack.rs

1//! shared integer bitpacking primitive (the `intpack` wire codec family,
2//! encoding id 4). values are stored frame-of-reference per 128-block:
3//! each block records its `min` and a bit-width, then packs `value - min`
4//! at that width, lsb-first. a block directory of absolute byte offsets
5//! gives O(1) block lookup, so `IntpackReader::get` reaches any element
6//! without scanning the whole payload.
7//!
8//! the primitive is order-preserving: it never sorts, so hnsw neighbour
9//! lists keep their exact build order (zero recall change). callers that
10//! want delta coding pass pre-differenced, monotonic sequences (e.g.
11//! sorted bm25 doc-id gaps); the small gaps shrink the per-block width.
12//!
13//! every read is bounds-checked and returns a typed `UrnaError` on
14//! truncated or malformed input, never a panic on a hostile mmap. one
15//! payload holds at most a few GiB of packed body (the block directory uses
16//! u32 byte offsets); that is far above any real index or metadata column.
17
18use crate::bytes::{le_u32, le_u64};
19use crate::error::UrnaError;
20
21/// values per frame-of-reference block.
22pub const INTPACK_BLOCK: usize = 128;
23const HEADER: usize = 8; // u32 count + u32 n_blocks
24
25/// bits needed to store the values `0..=range` (0 when `range == 0`).
26#[inline]
27fn bit_width(range: u64) -> u8 {
28    (64 - range.leading_zeros()) as u8
29}
30
31#[inline]
32fn mask(width: u8) -> u64 {
33    if width >= 64 {
34        u64::MAX
35    } else {
36        (1u64 << width) - 1
37    }
38}
39
40fn malformed(reason: impl Into<String>) -> UrnaError {
41    UrnaError::MalformedSectionPayload {
42        section_id: 0,
43        reason: reason.into(),
44    }
45}
46
47/// pack a slice of u64 with per-128-block frame-of-reference bitpacking.
48/// the inverse is [`unpack_u64s`] (and random access via [`IntpackReader`]).
49pub fn pack_u64s(values: &[u64]) -> Vec<u8> {
50    let count = values.len();
51    let n_blocks = count.div_ceil(INTPACK_BLOCK);
52    let blocks_start = HEADER + n_blocks * 4;
53    let mut dir: Vec<u32> = Vec::with_capacity(n_blocks);
54    let mut blocks: Vec<u8> = Vec::new();
55    for chunk in values.chunks(INTPACK_BLOCK) {
56        dir.push((blocks_start + blocks.len()) as u32);
57        // `chunks()` never yields an empty slice, so the fold sees >= 1 value.
58        let (min, max) = chunk
59            .iter()
60            .fold((u64::MAX, 0u64), |(lo, hi), &v| (lo.min(v), hi.max(v)));
61        let width = bit_width(max - min);
62        blocks.extend_from_slice(&min.to_le_bytes());
63        blocks.push(width);
64        pack_block(&mut blocks, chunk, min, width);
65    }
66    let mut out = Vec::with_capacity(blocks_start + blocks.len());
67    out.extend_from_slice(&(count as u32).to_le_bytes());
68    out.extend_from_slice(&(n_blocks as u32).to_le_bytes());
69    for off in &dir {
70        out.extend_from_slice(&off.to_le_bytes());
71    }
72    out.extend_from_slice(&blocks);
73    out
74}
75
76fn pack_block(out: &mut Vec<u8>, chunk: &[u64], min: u64, width: u8) {
77    if width == 0 {
78        return;
79    }
80    let mut acc: u128 = 0;
81    let mut nbits: u32 = 0;
82    for &v in chunk {
83        acc |= ((v - min) as u128 & mask(width) as u128) << nbits;
84        nbits += width as u32;
85        while nbits >= 8 {
86            out.push((acc & 0xff) as u8);
87            acc >>= 8;
88            nbits -= 8;
89        }
90    }
91    if nbits > 0 {
92        out.push((acc & 0xff) as u8);
93    }
94}
95
96#[inline]
97fn read_u32(bytes: &[u8], pos: usize) -> Result<u32, UrnaError> {
98    let end = pos + 4;
99    if end > bytes.len() {
100        return Err(malformed("intpack: truncated u32"));
101    }
102    le_u32(&bytes[pos..end])
103}
104
105#[inline]
106fn read_u64(bytes: &[u8], pos: usize) -> Result<u64, UrnaError> {
107    let end = pos + 8;
108    if end > bytes.len() {
109        return Err(malformed("intpack: truncated u64"));
110    }
111    le_u64(&bytes[pos..end])
112}
113
114/// number of packed value-bytes a block of `block_len` values at `width`
115/// occupies (after the 9-byte `min`+`width` block header).
116#[inline]
117fn block_body_len(block_len: usize, width: u8) -> usize {
118    (block_len * width as usize).div_ceil(8)
119}
120
121/// extract value `idx` (0-based within the block) from a block body of
122/// `width`-bit lsb-first packed values. `body` must hold the block.
123fn extract(body: &[u8], idx: usize, width: u8) -> u64 {
124    if width == 0 {
125        return 0;
126    }
127    let bit = idx * width as usize;
128    let mut acc: u128 = 0;
129    let first = bit / 8;
130    let last = (bit + width as usize - 1) / 8;
131    for (k, &b) in body[first..=last].iter().enumerate() {
132        acc |= (b as u128) << (k * 8);
133    }
134    ((acc >> (bit % 8)) as u64) & mask(width)
135}
136
137/// decode an entire intpack payload back to its `u64` values.
138pub fn unpack_u64s(bytes: &[u8]) -> Result<Vec<u64>, UrnaError> {
139    let reader = IntpackReader::parse(bytes)?;
140    // cap the up-front reservation: a hostile header can claim a huge count
141    // (the per-block bodies still get bounds-checked as we go), so grow on
142    // demand rather than reserve gigabytes from a claim alone.
143    let mut out = Vec::with_capacity(reader.len().min(1 << 20));
144    for b in 0..reader.n_blocks {
145        let (min, width, body, block_len) = reader.block(b)?;
146        for i in 0..block_len {
147            // wrapping: for valid data min + (v - min) == v exactly; on a
148            // hostile payload (tampered min/width) this must not panic.
149            out.push(min.wrapping_add(extract(body, i, width)));
150        }
151    }
152    Ok(out)
153}
154
155/// random-access reader over an intpack payload. `parse` validates the
156/// header and directory once; `get` then reaches any element in O(1)
157/// block lookups, decoding only the touched block.
158pub struct IntpackReader<'a> {
159    bytes: &'a [u8],
160    count: usize,
161    n_blocks: usize,
162}
163
164impl<'a> IntpackReader<'a> {
165    pub fn parse(bytes: &'a [u8]) -> Result<Self, UrnaError> {
166        let count = read_u32(bytes, 0)? as usize;
167        let n_blocks = read_u32(bytes, 4)? as usize;
168        if n_blocks != count.div_ceil(INTPACK_BLOCK) {
169            return Err(malformed("intpack: block count inconsistent with count"));
170        }
171        // directory must be fully present.
172        if HEADER + n_blocks * 4 > bytes.len() {
173            return Err(malformed("intpack: truncated directory"));
174        }
175        Ok(Self {
176            bytes,
177            count,
178            n_blocks,
179        })
180    }
181
182    #[inline]
183    pub fn len(&self) -> usize {
184        self.count
185    }
186
187    #[inline]
188    pub fn is_empty(&self) -> bool {
189        self.count == 0
190    }
191
192    /// `(min, width, body, block_len)` for block `b`, all bounds-checked.
193    fn block(&self, b: usize) -> Result<(u64, u8, &'a [u8], usize), UrnaError> {
194        let off = read_u32(self.bytes, HEADER + b * 4)? as usize;
195        let min = read_u64(self.bytes, off)?;
196        let width_pos = off + 8;
197        if width_pos >= self.bytes.len() {
198            return Err(malformed("intpack: truncated block header"));
199        }
200        let width = self.bytes[width_pos];
201        if width > 64 {
202            return Err(malformed("intpack: block width out of range"));
203        }
204        let block_len = (self.count - b * INTPACK_BLOCK).min(INTPACK_BLOCK);
205        let body_start = width_pos + 1;
206        let body_end = body_start + block_body_len(block_len, width);
207        if body_end > self.bytes.len() {
208            return Err(malformed("intpack: truncated block body"));
209        }
210        Ok((min, width, &self.bytes[body_start..body_end], block_len))
211    }
212
213    /// value at index `i`, or a typed error if `i` is out of range or the
214    /// payload is truncated. never panics.
215    pub fn get(&self, i: usize) -> Result<u64, UrnaError> {
216        if i >= self.count {
217            return Err(malformed("intpack: index out of range"));
218        }
219        let (min, width, body, _) = self.block(i / INTPACK_BLOCK)?;
220        Ok(min.wrapping_add(extract(body, i % INTPACK_BLOCK, width)))
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    fn roundtrip(values: &[u64]) {
229        let packed = pack_u64s(values);
230        let back = unpack_u64s(&packed).unwrap();
231        assert_eq!(back, values, "unpack_u64s mismatch");
232        let reader = IntpackReader::parse(&packed).unwrap();
233        assert_eq!(reader.len(), values.len());
234        for (i, &v) in values.iter().enumerate() {
235            assert_eq!(reader.get(i).unwrap(), v, "get({}) mismatch", i);
236        }
237        assert!(reader.get(values.len()).is_err(), "oob index must error");
238    }
239
240    #[test]
241    fn empty_roundtrips() {
242        roundtrip(&[]);
243    }
244
245    #[test]
246    fn single_block_widths() {
247        roundtrip(&[0]);
248        roundtrip(&[7, 7, 7, 7]); // width 0 (all equal min)
249        roundtrip(&[0, 1, 2, 3, 4, 5]);
250        roundtrip(&[5, 1, 9, 2, 30724, 0]); // unsorted, order preserved
251    }
252
253    #[test]
254    fn crosses_block_boundary() {
255        let v: Vec<u64> = (0..300).map(|i| (i * 7) % 31).collect();
256        roundtrip(&v);
257    }
258
259    #[test]
260    fn wide_values_and_for_offset() {
261        roundtrip(&[1_000_000, 1_000_001, 1_000_005, 1_000_002]);
262        roundtrip(&[u64::MAX, 0, u64::MAX / 2]);
263    }
264
265    #[test]
266    fn tampered_min_does_not_panic() {
267        // hostile payload: a block min near u64::MAX with nonzero packed
268        // offsets. decode must wrap, not overflow-panic in debug builds.
269        let mut packed = pack_u64s(&[1, 2, 3, 200]);
270        // blocks_start = HEADER(8) + n_blocks(1)*4 = 12; min is bytes 12..20.
271        for byte in packed.iter_mut().skip(12).take(8) {
272            *byte = 0xFF;
273        }
274        let _ = unpack_u64s(&packed); // must not panic
275        if let Ok(r) = IntpackReader::parse(&packed) {
276            for i in 0..r.len() {
277                let _ = r.get(i);
278            }
279        }
280    }
281
282    #[test]
283    fn truncated_inputs_error_never_panic() {
284        let packed = pack_u64s(&[1, 2, 3, 4, 5]);
285        for cut in 0..packed.len() {
286            let _ = unpack_u64s(&packed[..cut]);
287            if let Ok(r) = IntpackReader::parse(&packed[..cut]) {
288                let _ = r.get(0);
289                let _ = r.get(r.len().saturating_sub(1));
290            }
291        }
292        // a header claiming more blocks than bytes back must error, not panic.
293        let mut evil = Vec::new();
294        evil.extend_from_slice(&1_000_000u32.to_le_bytes());
295        evil.extend_from_slice(&7813u32.to_le_bytes());
296        assert!(IntpackReader::parse(&evil).is_err());
297    }
298}