Skip to main content

argon2_rust/
base64.rs

1//! Runtime-dispatched SIMD prefixes for the PHC Base64 codec.
2//!
3//! The public contract remains in [`crate::encoding`]. Backends here consume
4//! only complete 3-byte/4-character groups and stop before any invalid SIMD
5//! block; the scalar reference loop then handles the tail, locates the exact
6//! first invalid byte, and enforces the reference C's leftover-bit rules.
7//!
8//! # Algorithm
9//!
10//! This follows the structure used by `base64-simd` and `aklomp/base64`:
11//!
12//! * encoding reshuffles 12 or 24 input bytes into independent six-bit lanes,
13//!   then translates all lanes through the standard Base64 alphabet;
14//! * decoding classifies and translates a whole ASCII vector, rejects the
15//!   vector as a unit if any lane is invalid, then merges each four six-bit
16//!   lanes into three output bytes;
17//! * AVX2 handles 24/32 bytes per iteration, SSSE3 12/16, and AArch64 NEON
18//!   uses interleaved 8-lane loads for 24/32 bytes.
19//!
20//! The backend is detected once and cached with a relaxed atomic. Without
21//! `std`, selection uses compile-time target features, matching the crate's
22//! other runtime-dispatched kernels.
23
24use core::sync::atomic::{AtomicU8, Ordering};
25
26#[cfg(target_arch = "aarch64")]
27mod neon;
28#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
29mod x86;
30#[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
31mod wasm128;
32
33/// A Base64 SIMD implementation compiled into this crate.
34#[derive(Copy, Clone, Debug, PartialEq, Eq)]
35#[repr(u8)]
36pub enum Base64Backend {
37    /// Portable scalar code.
38    Scalar = 0,
39    /// AArch64 NEON, processing 24 input or 32 encoded bytes at a time.
40    Neon = 1,
41    /// x86/x86-64 SSSE3, processing 12 input or 16 encoded bytes at a time.
42    Ssse3 = 2,
43    /// x86/x86-64 AVX2 plus SSSE3, processing 24 input or 32 encoded bytes at
44    /// a time and using SSSE3 for the remaining vector-sized prefix.
45    Avx2 = 3,
46    /// WebAssembly SIMD128, processing 12 input or 16 encoded bytes at a time.
47    Wasm128 = 4,
48}
49
50impl Base64Backend {
51    /// Every backend compiled for the current architecture.
52    #[cfg(target_arch = "aarch64")]
53    pub const ALL: &'static [Base64Backend] =
54        &[Base64Backend::Scalar, Base64Backend::Neon];
55    /// Every backend compiled for the current architecture.
56    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
57    pub const ALL: &'static [Base64Backend] = &[
58        Base64Backend::Scalar,
59        Base64Backend::Ssse3,
60        Base64Backend::Avx2,
61    ];
62    /// Scalar and SIMD128 backends in a SIMD-enabled WebAssembly build.
63    #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
64    pub const ALL: &'static [Base64Backend] =
65        &[Base64Backend::Scalar, Base64Backend::Wasm128];
66    /// The scalar backend on architectures without an implementation above.
67    #[cfg(not(any(
68        target_arch = "aarch64",
69        target_arch = "x86",
70        target_arch = "x86_64",
71        all(target_arch = "wasm32", target_feature = "simd128")
72    )))]
73    pub const ALL: &'static [Base64Backend] = &[Base64Backend::Scalar];
74
75    /// Short lowercase diagnostic and benchmark name.
76    #[must_use]
77    pub const fn name(self) -> &'static str {
78        match self {
79            Base64Backend::Scalar => "scalar",
80            Base64Backend::Neon => "neon",
81            Base64Backend::Ssse3 => "ssse3",
82            Base64Backend::Avx2 => "avx2",
83            Base64Backend::Wasm128 => "wasm128",
84        }
85    }
86
87    /// Whether this CPU can execute the backend now.
88    #[must_use]
89    pub fn is_available(self) -> bool {
90        match self {
91            Base64Backend::Scalar => true,
92            Base64Backend::Neon => have_neon(),
93            Base64Backend::Ssse3 => have_ssse3(),
94            // The AVX2 entry point deliberately finishes with the SSSE3
95            // kernel, so its full feature contract includes both.
96            Base64Backend::Avx2 => have_avx2() && have_ssse3(),
97            Base64Backend::Wasm128 => have_wasm_simd128(),
98        }
99    }
100
101    #[inline]
102    const fn from_u8(value: u8) -> Base64Backend {
103        match value {
104            1 => Base64Backend::Neon,
105            2 => Base64Backend::Ssse3,
106            3 => Base64Backend::Avx2,
107            4 => Base64Backend::Wasm128,
108            _ => Base64Backend::Scalar,
109        }
110    }
111}
112
113impl core::fmt::Display for Base64Backend {
114    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
115        f.write_str(self.name())
116    }
117}
118
119#[cfg(all(feature = "std", any(target_arch = "x86", target_arch = "x86_64")))]
120#[inline]
121fn have_ssse3() -> bool {
122    std::arch::is_x86_feature_detected!("ssse3")
123}
124#[cfg(not(all(feature = "std", any(target_arch = "x86", target_arch = "x86_64"))))]
125#[inline]
126fn have_ssse3() -> bool {
127    cfg!(all(
128        any(target_arch = "x86", target_arch = "x86_64"),
129        target_feature = "ssse3"
130    ))
131}
132
133#[cfg(all(feature = "std", any(target_arch = "x86", target_arch = "x86_64")))]
134#[inline]
135fn have_avx2() -> bool {
136    std::arch::is_x86_feature_detected!("avx2")
137}
138#[cfg(not(all(feature = "std", any(target_arch = "x86", target_arch = "x86_64"))))]
139#[inline]
140fn have_avx2() -> bool {
141    cfg!(all(
142        any(target_arch = "x86", target_arch = "x86_64"),
143        target_feature = "avx2"
144    ))
145}
146
147#[cfg(all(feature = "std", target_arch = "aarch64"))]
148#[inline]
149fn have_neon() -> bool {
150    #[cfg(any(target_vendor = "apple", target_os = "windows"))]
151    {
152        true
153    }
154    #[cfg(not(any(target_vendor = "apple", target_os = "windows")))]
155    {
156        std::arch::is_aarch64_feature_detected!("neon")
157    }
158}
159
160#[inline]
161fn have_wasm_simd128() -> bool {
162    cfg!(all(target_arch = "wasm32", target_feature = "simd128"))
163}
164#[cfg(not(all(feature = "std", target_arch = "aarch64")))]
165#[inline]
166fn have_neon() -> bool {
167    cfg!(all(target_arch = "aarch64", target_feature = "neon"))
168}
169
170const UNINIT: u8 = u8::MAX;
171static CACHED_BACKEND: AtomicU8 = AtomicU8::new(UNINIT);
172
173/// Detect the fastest executable Base64 backend without reading the cache.
174#[must_use]
175pub fn detect_base64_backend() -> Base64Backend {
176    if cfg!(miri) {
177        Base64Backend::Scalar
178    } else if have_avx2() && have_ssse3() {
179        Base64Backend::Avx2
180    } else if have_ssse3() {
181        Base64Backend::Ssse3
182    } else if have_neon() {
183        Base64Backend::Neon
184    } else if have_wasm_simd128() {
185        Base64Backend::Wasm128
186    } else {
187        Base64Backend::Scalar
188    }
189}
190
191#[cold]
192#[inline(never)]
193fn detect_and_cache() -> Base64Backend {
194    let detected = detect_base64_backend();
195    CACHED_BACKEND.store(detected as u8, Ordering::Relaxed);
196    detected
197}
198
199/// Return the process-wide cached Base64 backend.
200#[inline]
201#[must_use]
202pub fn base64_backend() -> Base64Backend {
203    let cached = CACHED_BACKEND.load(Ordering::Relaxed);
204    if cached == UNINIT {
205        detect_and_cache()
206    } else {
207        Base64Backend::from_u8(cached)
208    }
209}
210
211/// The shortest input on which any backend for this architecture can encode
212/// a vector. Used to keep the dispatch cost completely off shorter inputs.
213#[cfg(target_arch = "aarch64")]
214pub const MIN_ENCODE_LEN: usize = 24;
215/// See the AArch64 definition.
216#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
217pub const MIN_ENCODE_LEN: usize = 16;
218/// WebAssembly SIMD128 consumes 12 bytes from each 16-byte vector load, as
219/// SSSE3 does, so the shortest safely readable input is 16 bytes.
220#[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
221pub const MIN_ENCODE_LEN: usize = 16;
222/// No SIMD encoder is compiled on this target.
223#[cfg(not(any(
224    target_arch = "aarch64",
225    target_arch = "x86",
226    target_arch = "x86_64",
227    all(target_arch = "wasm32", target_feature = "simd128")
228)))]
229pub const MIN_ENCODE_LEN: usize = usize::MAX;
230
231/// The shortest encoded input on which a backend can decode a vector.
232#[cfg(target_arch = "aarch64")]
233pub const MIN_DECODE_LEN: usize = 32;
234/// See the AArch64 definition.
235#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
236pub const MIN_DECODE_LEN: usize = 16;
237/// WebAssembly SIMD128 decodes one 16-character vector.
238#[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
239pub const MIN_DECODE_LEN: usize = 16;
240/// No SIMD decoder is compiled on this target.
241#[cfg(not(any(
242    target_arch = "aarch64",
243    target_arch = "x86",
244    target_arch = "x86_64",
245    all(target_arch = "wasm32", target_feature = "simd128")
246)))]
247pub const MIN_DECODE_LEN: usize = usize::MAX;
248
249/// Encode as many complete SIMD blocks as possible.
250///
251/// Returns `(input_consumed, output_written)`.
252///
253/// # Safety
254///
255/// `backend` must be executable on the current CPU. `dst` has already been
256/// sized for the complete encoding by the caller.
257#[inline]
258pub unsafe fn encode_prefix(
259    backend: Base64Backend,
260    dst: *mut u8,
261    src: &[u8],
262) -> (usize, usize) {
263    #[cfg(not(any(
264        target_arch = "aarch64",
265        target_arch = "x86",
266        target_arch = "x86_64",
267        all(target_arch = "wasm32", target_feature = "simd128")
268    )))]
269    let _ = (dst, src);
270
271    match backend {
272        Base64Backend::Scalar => (0, 0),
273
274        #[cfg(target_arch = "aarch64")]
275        // SAFETY: transferred from this function's caller; the slice pointers
276        // and lengths are passed together without alteration.
277        Base64Backend::Neon => unsafe { neon::encode(dst, src.as_ptr(), src.len()) },
278        #[cfg(not(target_arch = "aarch64"))]
279        Base64Backend::Neon => (0, 0),
280
281        #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
282        // SAFETY: transferred from this function's caller; the slices supply
283        // the pointer/length pairs unchanged.
284        Base64Backend::Ssse3 => unsafe { x86::encode_ssse3(dst, src.as_ptr(), src.len()) },
285        #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
286        Base64Backend::Ssse3 => (0, 0),
287
288        #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
289        // SAFETY: as above; the backend contract includes AVX2 and SSSE3.
290        Base64Backend::Avx2 => unsafe { x86::encode_avx2(dst, src.as_ptr(), src.len()) },
291        #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
292        Base64Backend::Avx2 => (0, 0),
293
294        #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
295        // SAFETY: the module only exists under the compile-time SIMD128
296        // contract, and the slices supply both pointer bounds.
297        Base64Backend::Wasm128 => unsafe { wasm128::encode(dst, src.as_ptr(), src.len()) },
298        #[cfg(not(all(target_arch = "wasm32", target_feature = "simd128")))]
299        Base64Backend::Wasm128 => (0, 0),
300    }
301}
302
303/// Decode as many complete, valid SIMD blocks as possible.
304///
305/// Returns `(input_consumed, output_written)`. An invalid vector is left
306/// entirely unconsumed so the scalar loop can find its first invalid byte.
307///
308/// # Safety
309///
310/// `backend` must be executable on the current CPU.
311#[inline]
312pub unsafe fn decode_prefix(
313    backend: Base64Backend,
314    dst: *mut u8,
315    dst_len: usize,
316    src: &[u8],
317) -> (usize, usize) {
318    #[cfg(not(any(
319        target_arch = "aarch64",
320        target_arch = "x86",
321        target_arch = "x86_64",
322        all(target_arch = "wasm32", target_feature = "simd128")
323    )))]
324    let _ = (dst, dst_len, src);
325
326    match backend {
327        Base64Backend::Scalar => (0, 0),
328
329        #[cfg(target_arch = "aarch64")]
330        // SAFETY: transferred from this function's caller; both pointer/length
331        // pairs come directly from live slices.
332        Base64Backend::Neon => unsafe { neon::decode(dst, dst_len, src.as_ptr(), src.len()) },
333        #[cfg(not(target_arch = "aarch64"))]
334        Base64Backend::Neon => (0, 0),
335
336        #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
337        // SAFETY: transferred from this function's caller; both pointer/length
338        // pairs come directly from live slices.
339        Base64Backend::Ssse3 => unsafe { x86::decode_ssse3(dst, dst_len, src.as_ptr(), src.len()) },
340        #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
341        Base64Backend::Ssse3 => (0, 0),
342
343        #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
344        // SAFETY: as above; the backend contract includes AVX2 and SSSE3.
345        Base64Backend::Avx2 => unsafe { x86::decode_avx2(dst, dst_len, src.as_ptr(), src.len()) },
346        #[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
347        Base64Backend::Avx2 => (0, 0),
348
349        #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
350        // SAFETY: the module only exists under the compile-time SIMD128
351        // contract, and both pointer/length pairs come from live slices.
352        Base64Backend::Wasm128 => unsafe {
353            wasm128::decode(dst, dst_len, src.as_ptr(), src.len())
354        },
355        #[cfg(not(all(target_arch = "wasm32", target_feature = "simd128")))]
356        Base64Backend::Wasm128 => (0, 0),
357    }
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363
364    #[test]
365    fn detection_is_cached_and_executable() {
366        let detected = detect_base64_backend();
367        assert!(detected.is_available());
368        assert_eq!(base64_backend(), detected);
369        assert_eq!(base64_backend(), detected);
370    }
371
372    #[test]
373    fn backend_discriminants_round_trip() {
374        for &backend in Base64Backend::ALL {
375            assert_eq!(Base64Backend::from_u8(backend as u8), backend);
376        }
377        assert_eq!(Base64Backend::from_u8(UNINIT), Base64Backend::Scalar);
378        assert_eq!(Base64Backend::from_u8(200), Base64Backend::Scalar);
379    }
380}