Skip to main content

protocache_core/
utils.rs

1//! Utility surface matching `utils.h`.
2
3use core::mem;
4
5/// Borrowed encoded ProtoCache words.
6pub type Words<'a> = &'a [u32];
7/// Borrowed raw bytes.
8pub type Bytes<'a> = &'a [u8];
9/// Numeric protobuf enum representation used by generated bindings.
10pub type EnumValue = i32;
11
12/// Converts a byte length to the number of four-byte ProtoCache words.
13#[inline(always)]
14pub const fn word_size(size: usize) -> usize {
15    size.div_ceil(4)
16}
17
18/// A fixed-width scalar that can be read from and written to little-endian words.
19pub trait Scalar: Copy + Sized {
20    /// Encoded width in `u32` words.
21    const WIDTH: usize;
22    /// Decodes from exactly [`Self::WIDTH`] words.
23    fn from_words(words: &[u32]) -> Option<Self>;
24    /// Encodes into exactly [`Self::WIDTH`] words.
25    fn write_words(self, words: &mut [u32]) -> Option<()>;
26}
27
28macro_rules! impl_scalar {
29    ($($ty:ty),* $(,)?) => {
30        $(
31            impl Scalar for $ty {
32                const WIDTH: usize = word_size(mem::size_of::<$ty>());
33
34                #[inline(always)]
35                fn from_words(words: &[u32]) -> Option<Self> {
36                    if words.len() != Self::WIDTH {
37                        return None;
38                    }
39                    let mut bytes = [0u8; mem::size_of::<$ty>()];
40                    let raw = unsafe {
41                        core::slice::from_raw_parts(
42                            words.as_ptr().cast::<u8>(),
43                            words.len() * mem::size_of::<u32>(),
44                        )
45                    };
46                    bytes.copy_from_slice(raw.get(..mem::size_of::<$ty>())?);
47                    Some(<$ty>::from_le_bytes(bytes))
48                }
49
50                #[inline(always)]
51                fn write_words(self, words: &mut [u32]) -> Option<()> {
52                    if words.len() != Self::WIDTH {
53                        return None;
54                    }
55                    words.fill(0);
56                    let raw = unsafe {
57                        core::slice::from_raw_parts_mut(
58                            words.as_mut_ptr().cast::<u8>(),
59                            words.len() * mem::size_of::<u32>(),
60                        )
61                    };
62                    raw.get_mut(..mem::size_of::<$ty>())?
63                        .copy_from_slice(&self.to_le_bytes());
64                    Some(())
65                }
66            }
67        )*
68    };
69}
70
71impl_scalar!(u32, i32, u64, i64, f32, f64);
72
73impl Scalar for bool {
74    const WIDTH: usize = 1;
75
76    #[inline(always)]
77    fn from_words(words: &[u32]) -> Option<Self> {
78        Some(u32::from_words(words)? != 0)
79    }
80
81    #[inline(always)]
82    fn write_words(self, words: &mut [u32]) -> Option<()> {
83        if words.len() != Self::WIDTH {
84            return None;
85        }
86        words[0] = u32::from(self);
87        Some(())
88    }
89}
90
91#[derive(Clone, Copy, Debug, Eq, PartialEq)]
92/// Broad reason that encoded or compressed input could not be read.
93pub enum CorruptionKind {
94    Truncated,
95    InvalidHeader,
96    InvalidUtf8,
97    IntegerOverflow,
98}
99
100#[derive(Clone, Copy, Debug, Eq, PartialEq)]
101/// Error returned when compressed input is malformed or incomplete.
102pub struct ReadError {
103    /// The detected corruption category.
104    pub kind: CorruptionKind,
105}
106
107impl ReadError {
108    pub const fn new(kind: CorruptionKind) -> Self {
109        Self { kind }
110    }
111}
112
113impl core::fmt::Display for ReadError {
114    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
115        write!(f, "{:?}", self.kind)
116    }
117}
118
119impl std::error::Error for ReadError {}
120
121#[derive(Clone, Debug, Default)]
122/// Reusable reverse-growing word buffer used by ProtoCache encoders.
123///
124/// Encoders prepend data, so existing segment positions remain stable as the
125/// allocation grows. Call [`Self::clear`] to reuse allocated capacity.
126pub struct Buffer {
127    data: Vec<u32>,
128    off: usize,
129}
130
131impl Buffer {
132    /// Creates an empty buffer without an allocation.
133    #[inline(always)]
134    pub fn new() -> Self {
135        Self::default()
136    }
137
138    /// Creates an empty buffer with capacity for at least `words` words.
139    #[inline(always)]
140    pub fn with_capacity_words(words: usize) -> Self {
141        Self {
142            data: vec![0; words],
143            off: words,
144        }
145    }
146
147    /// Removes active data while retaining allocated capacity.
148    #[inline(always)]
149    pub fn clear(&mut self) {
150        self.off = self.data.len();
151    }
152
153    #[inline(always)]
154    pub fn allocated_words(&self) -> usize {
155        self.data.len()
156    }
157
158    #[inline(always)]
159    pub fn len(&self) -> usize {
160        self.data.len().saturating_sub(self.off)
161    }
162
163    #[inline(always)]
164    pub fn is_empty(&self) -> bool {
165        self.len() == 0
166    }
167
168    /// Returns the active encoded words.
169    #[inline(always)]
170    pub fn view(&self) -> &[u32] {
171        &self.data[self.off..]
172    }
173
174    #[inline(always)]
175    pub fn head(&self) -> &[u32] {
176        self.view()
177    }
178
179    #[inline(always)]
180    pub fn head_mut(&mut self) -> &mut [u32] {
181        let off = self.off;
182        &mut self.data[off..]
183    }
184
185    #[inline(always)]
186    pub fn at_from_end(&self, words: usize) -> Option<&[u32]> {
187        if words > self.data.len() {
188            return None;
189        }
190        let start = self.data.len() - words;
191        Some(&self.data[start..])
192    }
193
194    #[inline(always)]
195    pub fn at_from_end_mut(&mut self, words: usize) -> Option<&mut [u32]> {
196        if words > self.data.len() {
197            return None;
198        }
199        let start = self.data.len() - words;
200        Some(&mut self.data[start..])
201    }
202
203    #[inline(always)]
204    pub fn reserve_words(&mut self, words: usize) {
205        if words > self.data.len() {
206            self.grow(words);
207        }
208    }
209
210    /// Prepends `delta` zero-initialized words and returns them for writing.
211    #[inline(always)]
212    pub fn expand(&mut self, delta: usize) -> &mut [u32] {
213        if self.off < delta {
214            let needed = self.len().saturating_add(delta);
215            self.grow(needed);
216        }
217        self.off -= delta;
218        let ptr = unsafe { self.data.as_mut_ptr().add(self.off) };
219        // `off + delta` is within `data` because we either had capacity or just grew.
220        unsafe { core::slice::from_raw_parts_mut(ptr, delta) }
221    }
222
223    /// Discards `delta` words from the front of the active view.
224    ///
225    /// # Panics
226    ///
227    /// Panics when `delta` is greater than [`Self::len`].
228    #[inline(always)]
229    pub fn shrink(&mut self, delta: usize) {
230        assert!(delta <= self.len());
231        self.off += delta;
232    }
233
234    #[inline(always)]
235    pub fn put(&mut self, value: u32) {
236        self.expand(1)[0] = value;
237    }
238
239    #[inline(always)]
240    pub fn put_words(&mut self, words: &[u32]) {
241        let dst = self.expand(words.len());
242        // `expand` returned a non-overlapping destination slice with the exact length.
243        unsafe { core::ptr::copy_nonoverlapping(words.as_ptr(), dst.as_mut_ptr(), words.len()) };
244    }
245
246    #[inline(always)]
247    fn grow(&mut self, min_words: usize) {
248        let active_len = self.len();
249        let mut new_words = self.data.len().max(8);
250        while new_words < min_words {
251            new_words = new_words.saturating_mul(2);
252        }
253        if new_words < min_words {
254            new_words = min_words;
255        }
256        let mut new_data = vec![0; new_words];
257        let new_off = new_words - active_len;
258        new_data[new_off..].copy_from_slice(self.view());
259        self.data = new_data;
260        self.off = new_off;
261    }
262}
263
264/// Reads an entire file into memory.
265pub fn load_file(path: impl AsRef<std::path::Path>) -> std::io::Result<Vec<u8>> {
266    std::fs::read(path)
267}
268
269/// Compresses bytes using ProtoCache's lightweight run encoding.
270pub fn compress(src: &[u8]) -> Vec<u8> {
271    let mut out = Vec::new();
272    compress_into(src, &mut out);
273    out
274}
275
276/// Compresses into `out`, clearing its previous contents while reusing capacity.
277pub fn compress_into(src: &[u8], out: &mut Vec<u8>) {
278    if src.is_empty() {
279        out.clear();
280        return;
281    }
282
283    let header_len = varint_len(src.len());
284    let capacity = header_len + src.len() + src.len().div_ceil(14);
285    out.clear();
286    if out.capacity() < capacity {
287        out.reserve(capacity - out.capacity());
288    }
289
290    let mut size = src.len();
291    while (size & !0x7f) != 0 {
292        out.push(0x80 | (size as u8 & 0x7f));
293        size >>= 7;
294    }
295    out.push(size as u8);
296
297    // After the varint header, write the compressed body via a raw pointer so
298    // that the hot loop does not repeatedly reload Vec metadata (data ptr +
299    // length) from memory.  We call set_len once at the end.
300    // SAFETY: we reserved `capacity` bytes above; all writes stay within that.
301    let base = out.len();
302    let mut off = 0usize;
303    let dst = unsafe { out.as_mut_ptr().add(base) };
304
305    let mut pos = 0usize;
306    while pos < src.len() {
307        let a_start = pos;
308        let a = pick_run(src, &mut pos);
309        if pos == src.len() {
310            unsafe {
311                dst.add(off).write(a);
312            }
313            off += 1;
314            if (a & 0x8) == 0 {
315                off = emit_run_raw(src, a_start, pos, dst, off);
316            }
317            break;
318        }
319        let b_start = pos;
320        let b = pick_run(src, &mut pos);
321        unsafe {
322            dst.add(off).write(a | (b << 4));
323        }
324        off += 1;
325        if (a & 0x8) == 0 {
326            off = emit_run_raw(src, a_start, b_start, dst, off);
327        }
328        if (b & 0x8) == 0 {
329            off = emit_run_raw(src, b_start, pos, dst, off);
330        }
331    }
332
333    unsafe {
334        out.set_len(base + off);
335    }
336}
337
338/// Decompresses data produced by [`compress`].
339pub fn decompress(src: &[u8]) -> Result<Vec<u8>, ReadError> {
340    let mut out = Vec::new();
341    decompress_into(src, &mut out)?;
342    Ok(out)
343}
344
345/// Decompresses into `out`, reusing its allocation.
346///
347/// Malformed input clears `out` before returning an error.
348pub fn decompress_into(src: &[u8], out: &mut Vec<u8>) -> Result<(), ReadError> {
349    if src.is_empty() {
350        out.clear();
351        return Ok(());
352    }
353    let (target, mut pos) = parse_varint(src)?;
354    // Overallocate by 7 bytes so unpack can safely write 8 bytes at a time
355    // without a per-write bounds check on the destination.  Mirrors the C++
356    // technique of resize(size+7) followed by a final trim.
357    out.resize(target + 7, 0u8);
358    let mut out_pos = 0usize;
359    let buf = out.as_mut_slice();
360    while pos < src.len() {
361        let mark = src[pos];
362        pos += 1;
363        unpack(mark & 0x0f, src, &mut pos, buf, &mut out_pos, target)?;
364        unpack(mark >> 4, src, &mut pos, buf, &mut out_pos, target)?;
365    }
366    if out_pos != target {
367        out.clear();
368        return Err(ReadError::new(CorruptionKind::Truncated));
369    }
370    out.truncate(target);
371    Ok(())
372}
373
374#[inline]
375fn varint_len(mut value: usize) -> usize {
376    let mut len = 1usize;
377    while (value & !0x7f) != 0 {
378        value >>= 7;
379        len += 1;
380    }
381    len
382}
383
384#[inline(always)]
385fn pick_run(src: &[u8], pos: &mut usize) -> u8 {
386    let start = *pos;
387    let first = src[start];
388    *pos += 1;
389    // Same trick as C++: arithmetic right-shift of i8 is true only for 0x00 and 0xff,
390    // letting us handle both special bytes in a single branch instead of two.
391    let fi = first as i8;
392    if fi == fi >> 1 {
393        while *pos < src.len() && *pos - start < 4 && src[*pos] == first {
394            *pos += 1;
395        }
396        // first & 0x4 == 0 for 0x00, 0x4 for 0xff — encodes the fill value
397        0x8 | (first & 0x4) | ((*pos - start - 1) as u8)
398    } else {
399        while *pos < src.len() && *pos - start < 7 && src[*pos] != 0 && src[*pos] != 0xff {
400            *pos += 1;
401        }
402        (*pos - start) as u8
403    }
404}
405
406// Raw-pointer variant used by compress_into to avoid Vec metadata reloads.
407// SAFETY: caller must ensure dst has room for `end - start` bytes at offset `off`.
408#[inline(always)]
409fn emit_run_raw(src: &[u8], start: usize, end: usize, dst: *mut u8, off: usize) -> usize {
410    let len = end - start;
411    if len == 0 {
412        return off;
413    }
414    unsafe {
415        // When the source window has >= 8 bytes left, a single 8-byte word
416        // copy avoids a memcpy function call (mirrors the C++ optimisation).
417        if start + 8 <= src.len() {
418            let src_ptr = src.as_ptr().add(start) as *const u64;
419            (dst.add(off) as *mut u64).write_unaligned(src_ptr.read_unaligned());
420        } else {
421            std::ptr::copy_nonoverlapping(src.as_ptr().add(start), dst.add(off), len);
422        }
423    }
424    off + len
425}
426
427fn parse_varint(src: &[u8]) -> Result<(usize, usize), ReadError> {
428    let mut size = 0usize;
429    let mut pos = 0usize;
430    for shift in (0..32).step_by(7) {
431        let byte = *src
432            .get(pos)
433            .ok_or(ReadError::new(CorruptionKind::Truncated))?;
434        pos += 1;
435        if (byte & 0x80) != 0 {
436            size |= ((byte & 0x7f) as usize) << shift;
437        } else {
438            size |= (byte as usize) << shift;
439            return Ok((size, pos));
440        }
441    }
442    Err(ReadError::new(CorruptionKind::InvalidHeader))
443}
444
445#[inline(always)]
446fn unpack(
447    mark: u8,
448    src: &[u8],
449    pos: &mut usize,
450    out: &mut [u8],
451    out_pos: &mut usize,
452    target: usize,
453) -> Result<(), ReadError> {
454    // out.len() == target + 7; use `target` for logical end.
455    if *out_pos >= target {
456        return Ok(());
457    }
458    if (mark & 0x8) != 0 {
459        let count = ((mark & 0x3) + 1) as usize;
460        if count > target - *out_pos {
461            return Err(ReadError::new(CorruptionKind::Truncated));
462        }
463        // Write 4 bytes at once: out_pos < target, so out_pos + 4 <= target + 3 < target + 7.
464        // SAFETY: out is target+7 bytes, so this is always in bounds.
465        let fill_val: u32 = if (mark & 0x4) != 0 { u32::MAX } else { 0 };
466        unsafe {
467            (out.as_mut_ptr().add(*out_pos) as *mut u32).write_unaligned(fill_val);
468        }
469        *out_pos += count;
470        return Ok(());
471    }
472    let len = (mark & 0x7) as usize;
473    if len == 0 {
474        return Ok(());
475    }
476    if len > src.len() - *pos || len > target - *out_pos {
477        return Err(ReadError::new(CorruptionKind::Truncated));
478    }
479    // Fast path: when the source has >= 8 bytes left, copy 8 bytes at once.
480    // Writing 8 bytes is safe: out_pos <= target - len <= target - 1,
481    // so out_pos + 8 <= target + 7 == out.len().
482    if src.len() - *pos >= 8 {
483        unsafe {
484            let src_ptr = src.as_ptr().add(*pos) as *const u64;
485            let dst_ptr = out.as_mut_ptr().add(*out_pos) as *mut u64;
486            dst_ptr.write_unaligned(src_ptr.read_unaligned());
487        }
488        *pos += len;
489        *out_pos += len;
490    } else {
491        let end = *pos + len;
492        let out_end = *out_pos + len;
493        out[*out_pos..out_end].copy_from_slice(&src[*pos..end]);
494        *pos = end;
495        *out_pos = out_end;
496    }
497    Ok(())
498}
499
500#[cfg(test)]
501mod tests {
502    use super::{Buffer, compress, compress_into, decompress, decompress_into};
503
504    #[test]
505    fn put_prepends_words() {
506        let mut buffer = Buffer::new();
507        buffer.put(3);
508        buffer.put(2);
509        buffer.put(1);
510        assert_eq!(buffer.view(), &[1, 2, 3]);
511    }
512
513    #[test]
514    fn expand_preserves_existing_payload() {
515        let mut buffer = Buffer::with_capacity_words(4);
516        buffer.put_words(&[3, 4]);
517        buffer.put_words(&[1, 2]);
518        assert_eq!(buffer.view(), &[1, 2, 3, 4]);
519
520        buffer.put_words(&[0]);
521        assert_eq!(buffer.view(), &[0, 1, 2, 3, 4]);
522        assert!(buffer.allocated_words() >= 5);
523    }
524
525    #[test]
526    fn shrink_discards_from_front() {
527        let mut buffer = Buffer::new();
528        buffer.put_words(&[1, 2, 3, 4]);
529        buffer.shrink(2);
530        assert_eq!(buffer.view(), &[3, 4]);
531        buffer.clear();
532        assert!(buffer.is_empty());
533    }
534
535    #[test]
536    fn compression_roundtrip_preserves_payload() {
537        let src = b"\0\0\0\0abcd\xff\xff\xffefgh\0";
538        let compressed = compress(src);
539        let restored = decompress(&compressed).unwrap();
540        assert_eq!(restored, src);
541    }
542
543    #[test]
544    fn compression_into_reuses_buffers() {
545        let src = b"\0\0\0\0abcd\xff\xff\xffefgh\0";
546        let mut compressed = Vec::with_capacity(128);
547        let compressed_capacity = compressed.capacity();
548        compress_into(src, &mut compressed);
549        assert_eq!(compressed.capacity(), compressed_capacity);
550
551        let mut restored = Vec::with_capacity(128);
552        let restored_capacity = restored.capacity();
553        decompress_into(&compressed, &mut restored).unwrap();
554        assert_eq!(restored, src);
555        assert_eq!(restored.capacity(), restored_capacity);
556    }
557}