Skip to main content

base94_simd/
lib.rs

1//! SIMD-accelerated base94 codec: binary data in printable ASCII.
2//!
3//! Each byte `b` maps to `(b - kf) mod 256`; mixed values `>= 93` escape into
4//! two chars (`0x7D`/`0x7E` leader + follower) so every emitted char stays in
5//! `0x20..=0x7E` and the output is at most 2x the input. `kf` is a key-mixing
6//! parameter (only its low byte participates); pass `0` for the plain codec.
7//! The format is a faithful port of openppp2 `ppp/cryptography/ssea.cpp`.
8//!
9//! Performance notes (wire output unchanged):
10//! * encode/decode run SIMD fast paths over 16-byte blocks (see `simd`): the decoder solves the
11//!   leader/follower alternation and escape reconstruction in-register and compacts leaders via a
12//!   byte-shuffle LUT (pshufb on x86_64, vqtbl1 on aarch64; ~3x over scalar); the encoder
13//!   precomputes interleaved leader/follower pairs and deletes non-escape followers through the
14//!   same LUT (~2.5x).
15//! * Invalid input and sub-block tails fall back to the scalar reference loop, so error semantics
16//!   stay bit-exact (pinned by fuzz + unit tests).
17
18// Intentional truncating/wrapping casts below mirror the C++ `Byte(int)`
19// conversions: the format relies on low-byte / modulo-256 semantics.
20#![allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
21// Hot loops use raw-pointer stores to skip bounds checks that the compiler
22// cannot elide.
23#![allow(unsafe_code)]
24
25mod simd;
26
27use std::fmt;
28
29/// Number of printable symbols: 0x20..=0x7E.
30pub const SYMBOL_COUNT: u8 = 94;
31/// Escape radix: mixed values >= 93 are encoded as two characters.
32const ESCAPE_RADIX: u8 = 93;
33/// Max digits of a u64 in base 94 (94^10 > 2^64 > 94^9).
34pub const DECIMAL_MAX_LEN: usize = 10;
35
36/// The input contains characters outside the printable alphabet or a
37/// truncated/overflowing escape sequence.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub struct DecodeError;
40
41impl fmt::Display for DecodeError {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        f.write_str("invalid base94 data")
44    }
45}
46
47impl std::error::Error for DecodeError {}
48
49/// Encodes binary bytes into printable 0x20..=0x7E chars, appending to `out`.
50///
51/// Branchless scalar tail: the leader char is always stored, the follower
52/// slot always written (garbage when unused, overwritten by the next
53/// leader), and the write cursor advances by `1 + escape` — no mispredicted
54/// branches on the ~50/50 escape mix.
55pub fn encode_into(out: &mut Vec<u8>, src: &[u8], kf: u32) {
56    // 8-wide unroll: the output offsets form a short serial prefix chain
57    // (7 adds amortized over 8 bytes) while the 16 leader/follower stores
58    // issue independently — a per-byte `p += 1 + esc` chain would otherwise
59    // cap the loop at the address-generation latency.
60    const UNROLL: usize = 8;
61
62    let kf8 = kf as u8;
63    let total = src.len() + simd::count_sub_ge(src, kf8, ESCAPE_RADIX);
64    let start = out.len();
65    // +16 slack: the SIMD kernel's per-quarter compaction stores are
66    // 8-byte writes whose padding past the count is only overwritten by
67    // *later* quarters/blocks; the final store may overrun the logical
68    // length by up to 8 bytes. set_len below erases the padding.
69    out.reserve(total + 16);
70    let dst = out.as_mut_ptr();
71    let mut p = start;
72    // SIMD fast path over whole 16-byte blocks first.
73    let simd_consumed = simd::encode_simd(dst, &mut p, src, kf8);
74    let mut idx = simd_consumed;
75    while idx + UNROLL <= src.len() {
76        let chunk: [u8; UNROLL] = src[idx..idx + UNROLL].try_into().expect("fixed size");
77        let mut offs = [0usize; UNROLL];
78        let mut cursor = 0usize;
79        for (k, &b) in chunk.iter().enumerate() {
80            offs[k] = cursor;
81            cursor += 1 + usize::from(b.wrapping_sub(kf8) >= ESCAPE_RADIX);
82        }
83        for (k, &b) in chunk.iter().enumerate() {
84            let v = b.wrapping_sub(kf8);
85            let esc = u8::from(v >= ESCAPE_RADIX);
86            // Escape: c1 = 0x7D + (v >= 186), c2 = 0x20 + v - 93 - 93*(v >= 186).
87            // Single: c1 = 0x20 + v (c2 unused, overwritten by a later leader).
88            let q2 = u8::from(v >= 2 * ESCAPE_RADIX);
89            let c1 = if esc != 0 {
90                0x7d + q2
91            } else {
92                0x20 + v
93            };
94            let c2 = 0x20u8.wrapping_add(
95                v.wrapping_sub(ESCAPE_RADIX)
96                    .wrapping_sub(ESCAPE_RADIX.wrapping_mul(q2)),
97            );
98            // SAFETY: offsets stay within `total + 1` reserved capacity.
99            unsafe {
100                let q = dst.add(p + offs[k]);
101                *q = c1;
102                *q.add(1) = c2;
103            }
104        }
105        p += cursor;
106        idx += UNROLL;
107    }
108    for &b in &src[idx..] {
109        let v = b.wrapping_sub(kf8);
110        let esc = u8::from(v >= ESCAPE_RADIX);
111        let q2 = u8::from(v >= 2 * ESCAPE_RADIX);
112        let c1 = if esc != 0 {
113            0x7d + q2
114        } else {
115            0x20 + v
116        };
117        let c2 = 0x20u8.wrapping_add(
118            v.wrapping_sub(ESCAPE_RADIX)
119                .wrapping_sub(ESCAPE_RADIX.wrapping_mul(q2)),
120        );
121        // SAFETY: same invariants as the unrolled body.
122        unsafe {
123            *dst.add(p) = c1;
124            *dst.add(p + 1) = c2;
125        }
126        p += 1 + usize::from(esc);
127    }
128    // SAFETY: exactly `total` bytes were committed by leader stores.
129    unsafe { out.set_len(start + total) };
130}
131
132/// Number of chars [`encode_into`] would emit for `src`.
133#[must_use]
134pub fn encoded_len(src: &[u8], kf: u32) -> usize {
135    src.len() + simd::count_sub_ge(src, kf as u8, ESCAPE_RADIX)
136}
137
138/// Decodes base94 text (see [`encode_into`]) and appends the bytes to `out`.
139/// On invalid input `out` is left unchanged and an error is returned.
140///
141/// A SIMD bulk pass proves `>= 0x20` for every char up front and a vectorized
142/// kernel decodes whole 16-char blocks (leader/follower pairing, escape
143/// reconstruction and leader compaction in-register). Invalid constructs fall
144/// back to the scalar reference loop, which reports the exact wire-legal
145/// error; both paths are bit-exact (pinned by the fuzz test below).
146pub fn decode_into(out: &mut Vec<u8>, src: &[u8], kf: u32) -> Result<(), DecodeError> {
147    if !simd::all_ge(src, 0x20) {
148        return Err(DecodeError);
149    }
150    let kf8 = kf as u8;
151    let kf16 = u16::from(kf8);
152    let start = out.len();
153    // +16 slack for the SIMD kernel's fixed 8-byte compact stores (see
154    // encode_into); the final set_len erases the padding.
155    out.reserve(src.len() + 16);
156    let dst = out.as_mut_ptr();
157    let mut i;
158    let mut p = start;
159    let n = src.len();
160    // SIMD fast path over whole 16-char blocks. `Ok` leaves the sub-block
161    // tail; `Err` hands back the last valid prefix. Either way the scalar
162    // reference loop resumes exactly where the kernel stopped.
163    match simd::decode_simd(dst, &mut p, src, kf8) {
164        Ok((consumed, _)) | Err((consumed, _)) => i = consumed,
165    }
166    // Main loop: every position still has a potential follower char. The
167    // escape validation folds into ONE branch on a value that is zero for
168    // all valid input — branching on `esc` itself would mispredict on the
169    // ~50/50 escape mix.
170    while i + 1 < n {
171        let b = u16::from(src[i]) - 0x20;
172        let b2 = u16::from(src[i + 1]) - 0x20;
173        let esc = u16::from(b >= u16::from(ESCAPE_RADIX));
174        // Escape reconstruction: v = (b - 92) * 93 + b2. Only meaningful when
175        // escaping; wrapped for the single-char arm (b < 93).
176        let v_esc = (b.wrapping_sub(92))
177            .wrapping_mul(u16::from(ESCAPE_RADIX))
178            .wrapping_add(b2);
179        // Invalid: leader > 0x7E, follower > 0x7C, or value overflow past
180        // 0xFF. All combined into a single (cold) taken-never branch.
181        let bad = esc
182            & (u16::from(b > 94)
183                | u16::from(b2 > u16::from(ESCAPE_RADIX))
184                | u16::from(v_esc > 0xff));
185        if bad != 0 {
186            return Err(DecodeError);
187        }
188        let val = if esc != 0 {
189            v_esc
190        } else {
191            b
192        };
193        // SAFETY: at most one output byte per input char, and `p` advanced
194        // only by committed bytes within the reserved capacity.
195        unsafe { *dst.add(p) = val.wrapping_add(kf16) as u8 };
196        p += 1;
197        i += 1 + esc as usize;
198    }
199    if i < n {
200        // Trailing single char; an escape leader here is a truncated pair.
201        let b = src[i] - 0x20;
202        if b >= ESCAPE_RADIX {
203            return Err(DecodeError);
204        }
205        unsafe { *dst.add(p) = b.wrapping_add(kf8) };
206        p += 1;
207    }
208    // SAFETY: p - start bytes were committed by the loop above.
209    unsafe { out.set_len(p) };
210    Ok(())
211}
212
213/// Minimal-length base94 digits of `v`; returns the digit count written.
214#[must_use]
215pub fn decimal_encode(v: u64, out: &mut [u8; DECIMAL_MAX_LEN]) -> usize {
216    let mut n = v;
217    let mut len = 0;
218    loop {
219        out[len] = (n % u64::from(SYMBOL_COUNT)) as u8 + 0x20;
220        len += 1;
221        n /= u64::from(SYMBOL_COUNT);
222        if n == 0 {
223            break;
224        }
225    }
226    out[..len].reverse();
227    len
228}
229
230/// Parses base94 digits (produced by [`decimal_encode`], possibly zero-padded
231/// with 0x20 chars) back into a u64.
232pub fn decimal_decode(s: &[u8]) -> Result<u64, DecodeError> {
233    if s.is_empty() {
234        return Err(DecodeError);
235    }
236    let mut n: u64 = 0;
237    for &c in s {
238        if c < 0x20 {
239            return Err(DecodeError);
240        }
241        let d = c - 0x20;
242        if d >= SYMBOL_COUNT {
243            return Err(DecodeError);
244        }
245        n = n
246            .checked_mul(u64::from(SYMBOL_COUNT))
247            .and_then(|n| n.checked_add(u64::from(d)))
248            .ok_or(DecodeError)?;
249    }
250    Ok(n)
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    #[test]
258    fn roundtrip_and_printable() {
259        for kf in [0u32, 1, 93, 94, 0xff, 0xdead_beef] {
260            let original: Vec<u8> = (0..=u8::MAX).collect();
261            let mut encoded = Vec::new();
262            encode_into(&mut encoded, &original, kf);
263            assert!(encoded.iter().all(|&c| (0x20..=0x7e).contains(&c)));
264            assert_eq!(encoded.len(), encoded_len(&original, kf));
265            let mut decoded = Vec::new();
266            decode_into(&mut decoded, &encoded, kf).unwrap();
267            assert_eq!(decoded, original);
268        }
269    }
270
271    #[test]
272    fn roundtrip_large_and_edge_shapes() {
273        let mut s = 0x0bad_c0deu64;
274        let mut step = || {
275            s ^= s << 13;
276            s ^= s >> 7;
277            s ^= s << 17;
278            (s >> 56) as u8
279        };
280        // Random data (~50% escapes), all-escape and no-escape shapes, plus
281        // lengths that hit the SIMD tails and the trailing-single-char path.
282        let all_escape: Vec<u8> = (0..2000)
283            .map(|i| 93u8.wrapping_add(i as u8 % 163))
284            .collect();
285        let no_escape: Vec<u8> = vec![7u8; 2001];
286        for dataset in [all_escape, no_escape] {
287            for kf in [0u32, 0x5a5a_5a5a] {
288                let mut encoded = Vec::new();
289                encode_into(&mut encoded, &dataset, kf);
290                assert_eq!(encoded.len(), encoded_len(&dataset, kf));
291                let mut decoded = Vec::new();
292                decode_into(&mut decoded, &encoded, kf).unwrap();
293                assert_eq!(decoded, dataset);
294            }
295        }
296        let mut random: Vec<u8> = (0..65_537).map(|_| step()).collect();
297        random[0] = 0x93; // force at least one boundary escape
298        let mut encoded = Vec::new();
299        encode_into(&mut encoded, &random, 154_543_927);
300        let mut decoded = Vec::new();
301        decode_into(&mut decoded, &encoded, 154_543_927).unwrap();
302        assert_eq!(decoded, random);
303    }
304
305    #[test]
306    fn decode_uses_existing_out_prefix() {
307        // Appending must respect pre-existing content (callers rely on
308        // encode-into semantics; decode mirrors it).
309        let original = vec![0xde, 0xad, 0xbe, 0xef];
310        let mut encoded = Vec::new();
311        encode_into(&mut encoded, &original, 123);
312        let mut out = b"prefix".to_vec();
313        decode_into(&mut out, &encoded, 123).unwrap();
314        assert_eq!(out, [b"prefix".as_slice(), original.as_slice()].concat());
315    }
316
317    #[test]
318    fn escape_boundaries() {
319        // kf = 0: values 93/94 escape, values 0..92 stay single.
320        let mut encoded = Vec::new();
321        encode_into(&mut encoded, &[0, 92, 93, 94, 255], 0);
322        // 0 -> 0x20, 92 -> 0x7C, 93 -> 0x7D 0x20, 94 -> 0x7D 0x21,
323        // 255 -> (0x20 + 94, 0x20 + 69) = 0x7E 0x65
324        assert_eq!(encoded, [0x20, 0x7c, 0x7d, 0x20, 0x7d, 0x21, 0x7e, 0x65]);
325    }
326
327    #[test]
328    fn decode_rejects_garbage() {
329        let mut out = Vec::new();
330        assert!(decode_into(&mut out, &[0x1f], 0).is_err());
331        assert!(decode_into(&mut out, &[0x7f], 0).is_err());
332        assert!(decode_into(&mut out, &[0x7d], 0).is_err()); // truncated escape
333        assert!(decode_into(&mut out, &[0x7d, 0x7e], 0).is_err()); // v > 0xFF
334        assert!(decode_into(&mut out, &[0x7e, 0x7e], 0).is_err()); // follower is escape
335        // Non-0x20 leader of length 1 (0x7F => b=95) is an invalid escape.
336        assert!(decode_into(&mut out, &[0x7f, 0x20], 0).is_err());
337        assert!(out.is_empty(), "no partial output on failure");
338    }
339
340    /// The pre-optimization reference decoder: greedy scalar parse. The
341    /// optimized fast path must match it bit-for-bit, including on crafted
342    /// inputs (e.g. legal 0x7D 0x7D pairs) and error cases.
343    fn decode_reference(out: &mut Vec<u8>, src: &[u8], kf: u32) -> Result<(), DecodeError> {
344        let kf8 = kf as u8;
345        let start = out.len();
346        let mut i = 0;
347        while i < src.len() {
348            let c = src[i];
349            if c < 0x20 {
350                out.truncate(start);
351                return Err(DecodeError);
352            }
353            let b = c - 0x20;
354            if b < ESCAPE_RADIX {
355                out.push(b.wrapping_add(kf8));
356                i += 1;
357                continue;
358            }
359            if b > 94 {
360                out.truncate(start);
361                return Err(DecodeError);
362            }
363            let Some(&c2) = src.get(i + 1) else {
364                out.truncate(start);
365                return Err(DecodeError);
366            };
367            if c2 < 0x20 {
368                out.truncate(start);
369                return Err(DecodeError);
370            }
371            let b2 = c2 - 0x20;
372            if b2 > ESCAPE_RADIX {
373                out.truncate(start);
374                return Err(DecodeError);
375            }
376            if b == 94 && b2 > 0xff - 2 * ESCAPE_RADIX {
377                out.truncate(start);
378                return Err(DecodeError);
379            }
380            let v = u32::from(b - ESCAPE_RADIX + 1) * u32::from(ESCAPE_RADIX) + u32::from(b2);
381            out.push((v as u8).wrapping_add(kf8));
382            i += 2;
383        }
384        Ok(())
385    }
386
387    #[test]
388    fn decode_fuzz_matches_reference() {
389        let mut s = 0x00c0_ffee_d00d_feedu64;
390        let mut next = || {
391            s ^= s << 13;
392            s ^= s >> 7;
393            s ^= s << 17;
394            s
395        };
396        // Bias the alphabet toward the tricky boundary range 0x7B..=0x7F so
397        // the adjacent-escape fallback and leader/follower boundaries get
398        // hammered; mix in ordinary printable chars and invalid bytes.
399        for len in [
400            0usize, 1, 15, 16, 17, 31, 33, 64, 100, 199, 200, 201, 255, 256, 257, 513,
401        ] {
402            for _ in 0..40 {
403                let input: Vec<u8> = (0..len)
404                    .map(|_| {
405                        let r = (next() >> 32) as u8;
406                        match r % 8 {
407                            0..=4 => 0x20 + (r % 93),
408                            5 => 0x7b,
409                            6 => 0x7d + (r % 2),
410                            _ => r, // sometimes < 0x20 or > 0x7E (invalid)
411                        }
412                    })
413                    .collect();
414                for kf in [0u32, 77, 0x5a5a_5a5a] {
415                    let mut got = Vec::new();
416                    let mut want = Vec::new();
417                    let a = decode_into(&mut got, &input, kf);
418                    let b = decode_reference(&mut want, &input, kf);
419                    assert_eq!(a.is_ok(), b.is_ok(), "len={len} kf={kf} ok-ness");
420                    if let (Ok(()), Ok(())) = (a, b) {
421                        assert_eq!(got, want, "len={len} kf={kf}");
422                    }
423                }
424            }
425        }
426    }
427
428    #[test]
429    fn decode_rejects_7e_follower_in_simd_block() {
430        // cargo-fuzz crash: a 0x7D leader with a 0x7E follower evades the
431        // escape-overflow check (93 + 94 = 187 <= 0xFF) yet the follower is
432        // out of range. Exactly 16 bytes to land in the SIMD kernel.
433        let input: Vec<u8> = b"ppppppppp#i}~C}(".to_vec();
434        assert_eq!(input.len(), 16);
435        let mut out = Vec::new();
436        assert!(decode_into(&mut out, &input, 125).is_err());
437        assert_eq!(out, [] as [u8; 0]);
438        // Same construct shifted to other lanes / with a leading carry.
439        for shift in 0..15usize {
440            let mut v = vec![0x41u8; 16];
441            v[shift] = 0x7d;
442            v[shift + 1] = 0x7e;
443            let mut out = Vec::new();
444            assert!(decode_into(&mut out, &v, 0).is_err(), "shift {shift}");
445        }
446        // Overflowing escape (ev > 0xFF) inside a SIMD block: 0x7E leader
447        // with a large follower, e.g. "7e 7d" -> 186 + 93 = 279.
448        let mut v = vec![0x41u8; 16];
449        v[0] = 0x7e;
450        v[1] = 0x7d;
451        let mut out = Vec::new();
452        assert!(decode_into(&mut out, &v, 0).is_err());
453        assert_eq!(out, [] as [u8; 0]);
454    }
455
456    #[test]
457    fn decode_legal_adjacent_escape_pair() {
458        // 0x7D 0x7D decodes to a single byte (v = 93 + 93 = 186), exercising
459        // the adjacent-escape boundary in the optimized path.
460        let mut big = vec![0x7du8; 64];
461        big.extend_from_slice(&[0x41; 8]);
462        for kf in [0u32, 200] {
463            let mut got = Vec::new();
464            decode_into(&mut got, &big, kf).unwrap();
465            let mut want = Vec::new();
466            decode_reference(&mut want, &big, kf).unwrap();
467            assert_eq!(got, want);
468            assert_eq!(got.len(), 32 + 8);
469        }
470    }
471
472    #[test]
473    fn decimal_roundtrip() {
474        let mut buf = [0u8; DECIMAL_MAX_LEN];
475        for v in [0u64, 1, 93, 94, 830_583, u64::from(u32::MAX), u64::MAX] {
476            let len = decimal_encode(v, &mut buf);
477            let digits = &buf[..len];
478            assert!(digits.iter().all(|&c| c >= 0x20));
479            if v > 0 {
480                assert_ne!(digits[0], 0x20, "no leading zero");
481            }
482            assert_eq!(decimal_decode(digits).unwrap(), v);
483            // Zero padding (as used in fixed 3-digit fields) also decodes,
484            // for values that fit.
485            if len <= 3 {
486                let mut padded = [0x20u8; 3];
487                padded[3 - len..].copy_from_slice(digits);
488                assert_eq!(decimal_decode(&padded).unwrap(), v);
489            }
490        }
491    }
492
493    #[test]
494    fn decimal_known_value() {
495        // 94^2 = 8836 -> digits (1, 0, 0) -> chars 0x21, 0x20, 0x20
496        let mut buf = [0u8; DECIMAL_MAX_LEN];
497        let len = decimal_encode(8836, &mut buf);
498        assert_eq!(len, 3);
499        assert_eq!(&buf[..3], &[0x21, 0x20, 0x20]);
500    }
501}