Skip to main content

oxihuman_core/
base64_stub.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Full-featured Base64 encoder/decoder.
5//!
6//! Supports standard and URL-safe alphabets, optional padding,
7//! configurable line wrapping, streaming encode/decode, and
8//! whitespace-tolerant decoding.
9
10#![allow(dead_code)]
11
12use std::fmt;
13use std::io::{self, Read, Write};
14use std::mem::ManuallyDrop;
15
16// ---------------------------------------------------------------------------
17// Alphabets
18// ---------------------------------------------------------------------------
19
20const STANDARD_ALPHABET: &[u8; 64] =
21    b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
22
23const URL_SAFE_ALPHABET: &[u8; 64] =
24    b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
25
26/// Pre-computed 256-entry decode tables.  `255` means invalid.
27const fn build_decode_table(alphabet: &[u8; 64]) -> [u8; 256] {
28    let mut table = [255u8; 256];
29    let mut i = 0usize;
30    while i < 64 {
31        table[alphabet[i] as usize] = i as u8;
32        i += 1;
33    }
34    table
35}
36
37const STANDARD_DECODE: [u8; 256] = build_decode_table(STANDARD_ALPHABET);
38const URL_SAFE_DECODE: [u8; 256] = build_decode_table(URL_SAFE_ALPHABET);
39
40/// Kept for backward compatibility: same as [`STANDARD_ALPHABET`].
41const ALPHABET: &[u8; 64] = STANDARD_ALPHABET;
42
43// ---------------------------------------------------------------------------
44// Error types
45// ---------------------------------------------------------------------------
46
47/// Specific reason a base64 decode failed.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub enum Base64DecodeError {
50    /// Encountered a byte that is not part of the chosen alphabet,
51    /// not padding, and not whitespace.
52    InvalidCharacter {
53        /// The offending byte value.
54        byte: u8,
55        /// Zero-based position inside the (whitespace-stripped) input.
56        position: usize,
57    },
58    /// Padding characters appear at an unexpected position.
59    InvalidPadding,
60    /// After stripping whitespace, the remaining length is not valid
61    /// (i.e. `len % 4 == 1` which can never be produced by encoding).
62    InvalidLength {
63        /// Length after whitespace removal.
64        length: usize,
65    },
66}
67
68impl fmt::Display for Base64DecodeError {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        match self {
71            Self::InvalidCharacter { byte, position } => {
72                write!(
73                    f,
74                    "invalid base64 character 0x{byte:02X} at position {position}"
75                )
76            }
77            Self::InvalidPadding => write!(f, "invalid base64 padding"),
78            Self::InvalidLength { length } => {
79                write!(f, "invalid base64 length {length} (mod 4 == 1)")
80            }
81        }
82    }
83}
84
85impl std::error::Error for Base64DecodeError {}
86
87// ---------------------------------------------------------------------------
88// Configuration
89// ---------------------------------------------------------------------------
90
91/// Selects the 64-character alphabet to use.
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
93pub enum Base64Variant {
94    /// Standard alphabet (`+`, `/`).
95    #[default]
96    Standard,
97    /// URL-safe alphabet (`-`, `_`).
98    UrlSafe,
99}
100
101/// Controls whether `=` padding is emitted / required.
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
103pub enum Base64Padding {
104    /// Always emit padding on encode; tolerate missing padding on decode.
105    #[default]
106    Pad,
107    /// Never emit padding on encode; tolerate present padding on decode.
108    NoPad,
109}
110
111/// Full configuration for encode / decode operations.
112#[derive(Debug, Clone)]
113pub struct Base64Config {
114    pub variant: Base64Variant,
115    pub padding: Base64Padding,
116    /// If `Some(n)`, insert `\r\n` every `n` output characters during
117    /// encoding (common value: 76 for MIME).  `None` means no wrapping.
118    pub line_wrap: Option<usize>,
119}
120
121impl Default for Base64Config {
122    fn default() -> Self {
123        Self {
124            variant: Base64Variant::Standard,
125            padding: Base64Padding::Pad,
126            line_wrap: None,
127        }
128    }
129}
130
131impl Base64Config {
132    /// Standard Base64 with padding, no wrapping.
133    pub fn standard() -> Self {
134        Self::default()
135    }
136
137    /// Standard Base64 with MIME line wrapping at 76 chars.
138    pub fn mime() -> Self {
139        Self {
140            line_wrap: Some(76),
141            ..Self::default()
142        }
143    }
144
145    /// URL-safe Base64, no padding, no wrapping.
146    pub fn url_safe() -> Self {
147        Self {
148            variant: Base64Variant::UrlSafe,
149            padding: Base64Padding::NoPad,
150            line_wrap: None,
151        }
152    }
153
154    /// URL-safe Base64 with padding.
155    pub fn url_safe_padded() -> Self {
156        Self {
157            variant: Base64Variant::UrlSafe,
158            padding: Base64Padding::Pad,
159            line_wrap: None,
160        }
161    }
162
163    fn alphabet(&self) -> &'static [u8; 64] {
164        match self.variant {
165            Base64Variant::Standard => STANDARD_ALPHABET,
166            Base64Variant::UrlSafe => URL_SAFE_ALPHABET,
167        }
168    }
169
170    fn decode_table(&self) -> &'static [u8; 256] {
171        match self.variant {
172            Base64Variant::Standard => &STANDARD_DECODE,
173            Base64Variant::UrlSafe => &URL_SAFE_DECODE,
174        }
175    }
176
177    fn emit_padding(&self) -> bool {
178        matches!(self.padding, Base64Padding::Pad)
179    }
180}
181
182// ---------------------------------------------------------------------------
183// Length helpers (original public API preserved)
184// ---------------------------------------------------------------------------
185
186/// Returns the encoded length **without** line-wrapping overhead.
187pub fn base64_encoded_len(input_len: usize) -> usize {
188    input_len.div_ceil(3) * 4
189}
190
191/// Upper-bound decoded length (may over-estimate by up to 2 bytes
192/// because padding/remainder is not accounted for).
193pub fn base64_decoded_len(encoded_len: usize) -> usize {
194    if encoded_len == 0 {
195        return 0;
196    }
197    encoded_len / 4 * 3
198}
199
200// ---------------------------------------------------------------------------
201// Core encode (configurable)
202// ---------------------------------------------------------------------------
203
204/// Encode `data` using the given configuration.
205pub fn base64_encode_config(data: &[u8], config: &Base64Config) -> String {
206    let alpha = config.alphabet();
207    let pad = config.emit_padding();
208    let raw_len = base64_encoded_len(data.len());
209    let mut out = Vec::with_capacity(raw_len + raw_len / 38);
210
211    let mut i = 0;
212    while i + 2 < data.len() {
213        let b0 = data[i] as u32;
214        let b1 = data[i + 1] as u32;
215        let b2 = data[i + 2] as u32;
216        let n = (b0 << 16) | (b1 << 8) | b2;
217        out.push(alpha[((n >> 18) & 63) as usize]);
218        out.push(alpha[((n >> 12) & 63) as usize]);
219        out.push(alpha[((n >> 6) & 63) as usize]);
220        out.push(alpha[(n & 63) as usize]);
221        i += 3;
222    }
223    let rem = data.len() - i;
224    if rem == 1 {
225        let n = (data[i] as u32) << 16;
226        out.push(alpha[((n >> 18) & 63) as usize]);
227        out.push(alpha[((n >> 12) & 63) as usize]);
228        if pad {
229            out.push(b'=');
230            out.push(b'=');
231        }
232    } else if rem == 2 {
233        let n = ((data[i] as u32) << 16) | ((data[i + 1] as u32) << 8);
234        out.push(alpha[((n >> 18) & 63) as usize]);
235        out.push(alpha[((n >> 12) & 63) as usize]);
236        out.push(alpha[((n >> 6) & 63) as usize]);
237        if pad {
238            out.push(b'=');
239        }
240    }
241
242    // Apply line wrapping if configured
243    if let Some(width) = config.line_wrap {
244        if width > 0 {
245            return apply_line_wrap(&out, width);
246        }
247    }
248
249    // SAFETY: `out` only contains ASCII alpha, digits, `+/=` or `-_=`
250    // so it is always valid UTF-8.
251    unsafe { String::from_utf8_unchecked(out) }
252}
253
254/// Insert `\r\n` every `width` bytes of raw base64 output.
255fn apply_line_wrap(raw: &[u8], width: usize) -> String {
256    let num_breaks = if raw.is_empty() {
257        0
258    } else {
259        (raw.len() - 1) / width
260    };
261    let mut wrapped = Vec::with_capacity(raw.len() + num_breaks * 2);
262    for (idx, chunk) in raw.chunks(width).enumerate() {
263        if idx > 0 {
264            wrapped.push(b'\r');
265            wrapped.push(b'\n');
266        }
267        wrapped.extend_from_slice(chunk);
268    }
269    // SAFETY: same ASCII guarantee as above.
270    unsafe { String::from_utf8_unchecked(wrapped) }
271}
272
273// ---------------------------------------------------------------------------
274// Core decode (configurable, whitespace-tolerant)
275// ---------------------------------------------------------------------------
276
277/// Decode a base64 string using the given configuration.
278///
279/// Whitespace (spaces, tabs, `\r`, `\n`) is silently stripped before
280/// decoding, so MIME-wrapped input is handled transparently.
281pub fn base64_decode_config(s: &str, config: &Base64Config) -> Result<Vec<u8>, Base64DecodeError> {
282    let table = config.decode_table();
283
284    // Strip whitespace and collect clean bytes
285    let clean: Vec<u8> = s
286        .bytes()
287        .filter(|&b| !matches!(b, b' ' | b'\t' | b'\r' | b'\n'))
288        .collect();
289
290    if clean.is_empty() {
291        return Ok(Vec::new());
292    }
293
294    // Strip trailing padding and record how many we had
295    let pad_count = clean.iter().rev().take_while(|&&b| b == b'=').count();
296    if pad_count > 2 {
297        return Err(Base64DecodeError::InvalidPadding);
298    }
299    let body_len = clean.len() - pad_count;
300
301    // Validate: padding must only appear at the end
302    for (pos, &b) in clean[..body_len].iter().enumerate() {
303        if b == b'=' {
304            return Err(Base64DecodeError::InvalidPadding);
305        }
306        if table[b as usize] == 255 {
307            return Err(Base64DecodeError::InvalidCharacter {
308                byte: b,
309                position: pos,
310            });
311        }
312    }
313
314    // Determine effective length (with virtual padding to make mod-4 == 0)
315    let effective_len = body_len + pad_count;
316    let remainder = effective_len % 4;
317    // If padded, must be multiple of 4.
318    if pad_count > 0 && remainder != 0 {
319        return Err(Base64DecodeError::InvalidLength {
320            length: effective_len,
321        });
322    }
323
324    // For un-padded input, figure out the virtual padding
325    let total_chars = body_len;
326    let tail_chars = if pad_count > 0 {
327        0 // all quads are complete (padded)
328    } else {
329        total_chars % 4
330    };
331
332    if tail_chars == 1 {
333        return Err(Base64DecodeError::InvalidLength { length: body_len });
334    }
335
336    let full_quads = if pad_count > 0 {
337        (effective_len / 4).saturating_sub(1)
338    } else {
339        total_chars / 4
340    };
341
342    let out_len = full_quads * 3
343        + if pad_count > 0 {
344            3 - pad_count
345        } else {
346            match tail_chars {
347                2 => 1,
348                3 => 2,
349                _ => 0,
350            }
351        };
352    let mut out = Vec::with_capacity(out_len);
353
354    // Decode full 4-char groups (no padding)
355    let mut pos = 0;
356    for _ in 0..full_quads {
357        let c0 = table[clean[pos] as usize] as u32;
358        let c1 = table[clean[pos + 1] as usize] as u32;
359        let c2 = table[clean[pos + 2] as usize] as u32;
360        let c3 = table[clean[pos + 3] as usize] as u32;
361        let n = (c0 << 18) | (c1 << 12) | (c2 << 6) | c3;
362        out.push(((n >> 16) & 0xFF) as u8);
363        out.push(((n >> 8) & 0xFF) as u8);
364        out.push((n & 0xFF) as u8);
365        pos += 4;
366    }
367
368    // Handle padded last group
369    if pad_count > 0 {
370        let c0 = table[clean[pos] as usize] as u32;
371        let c1 = table[clean[pos + 1] as usize] as u32;
372        let n = if pad_count == 2 {
373            (c0 << 18) | (c1 << 12)
374        } else {
375            let c2 = table[clean[pos + 2] as usize] as u32;
376            (c0 << 18) | (c1 << 12) | (c2 << 6)
377        };
378        out.push(((n >> 16) & 0xFF) as u8);
379        if pad_count < 2 {
380            out.push(((n >> 8) & 0xFF) as u8);
381        }
382    }
383
384    // Handle un-padded tail (remainder chars without padding)
385    if tail_chars == 2 {
386        let c0 = table[clean[pos] as usize] as u32;
387        let c1 = table[clean[pos + 1] as usize] as u32;
388        let n = (c0 << 18) | (c1 << 12);
389        out.push(((n >> 16) & 0xFF) as u8);
390    } else if tail_chars == 3 {
391        let c0 = table[clean[pos] as usize] as u32;
392        let c1 = table[clean[pos + 1] as usize] as u32;
393        let c2 = table[clean[pos + 2] as usize] as u32;
394        let n = (c0 << 18) | (c1 << 12) | (c2 << 6);
395        out.push(((n >> 16) & 0xFF) as u8);
396        out.push(((n >> 8) & 0xFF) as u8);
397    }
398
399    Ok(out)
400}
401
402// ---------------------------------------------------------------------------
403// Original public API (backward compatible)
404// ---------------------------------------------------------------------------
405
406/// Encode bytes to standard padded Base64 (no line wrapping).
407pub fn base64_encode(data: &[u8]) -> String {
408    base64_encode_config(data, &Base64Config::standard())
409}
410
411/// Decode a standard padded Base64 string.
412pub fn base64_decode(s: &str) -> Result<Vec<u8>, &'static str> {
413    base64_decode_config(s, &Base64Config::standard()).map_err(|e| match e {
414        Base64DecodeError::InvalidCharacter { .. } => "invalid char",
415        Base64DecodeError::InvalidPadding => "invalid base64",
416        Base64DecodeError::InvalidLength { .. } => "invalid base64",
417    })
418}
419
420/// Original validation function (backward compatible).
421pub fn base64_is_valid(s: &str) -> bool {
422    let b = s.as_bytes();
423    if !b.len().is_multiple_of(4) {
424        return false;
425    }
426    for (i, &ch) in b.iter().enumerate() {
427        if ch == b'=' {
428            if i < b.len().saturating_sub(2) {
429                return false;
430            }
431        } else if decode_char(ch).is_none() {
432            return false;
433        }
434    }
435    true
436}
437
438/// Backward-compatible single-character decode (standard alphabet).
439fn decode_char(c: u8) -> Option<u8> {
440    match c {
441        b'A'..=b'Z' => Some(c - b'A'),
442        b'a'..=b'z' => Some(c - b'a' + 26),
443        b'0'..=b'9' => Some(c - b'0' + 52),
444        b'+' => Some(62),
445        b'/' => Some(63),
446        b'=' => Some(0),
447        _ => None,
448    }
449}
450
451// ---------------------------------------------------------------------------
452// URL-safe convenience functions
453// ---------------------------------------------------------------------------
454
455/// Encode bytes to URL-safe Base64 without padding.
456pub fn base64_encode_url_safe(data: &[u8]) -> String {
457    base64_encode_config(data, &Base64Config::url_safe())
458}
459
460/// Decode a URL-safe Base64 string (padding optional).
461pub fn base64_decode_url_safe(s: &str) -> Result<Vec<u8>, Base64DecodeError> {
462    base64_decode_config(s, &Base64Config::url_safe())
463}
464
465/// Encode bytes to MIME Base64 (padded, line-wrapped at 76 chars).
466pub fn base64_encode_mime(data: &[u8]) -> String {
467    base64_encode_config(data, &Base64Config::mime())
468}
469
470// ---------------------------------------------------------------------------
471// Streaming encoder
472// ---------------------------------------------------------------------------
473
474/// A streaming Base64 encoder that wraps an [`io::Write`] sink.
475pub struct Base64Encoder<W: Write> {
476    inner: ManuallyDrop<W>,
477    config: Base64Config,
478    buf: [u8; 2],
479    buf_len: usize,
480    line_pos: usize,
481    finished: bool,
482}
483
484impl<W: Write> Base64Encoder<W> {
485    /// Create a new streaming encoder with the given configuration.
486    pub fn new(inner: W, config: Base64Config) -> Self {
487        Self {
488            inner: ManuallyDrop::new(inner),
489            config,
490            buf: [0; 2],
491            buf_len: 0,
492            line_pos: 0,
493            finished: false,
494        }
495    }
496
497    /// Finish encoding, flushing any buffered partial group and
498    /// returning the inner writer.
499    pub fn finish(mut self) -> io::Result<W> {
500        self.flush_final()?;
501        self.finished = true;
502        // SAFETY: after setting finished=true, Drop will not call flush_final
503        // again, and we take ownership of inner before drop runs.
504        Ok(unsafe { ManuallyDrop::take(&mut self.inner) })
505    }
506
507    fn flush_final(&mut self) -> io::Result<()> {
508        if self.buf_len == 0 {
509            return Ok(());
510        }
511        let alpha = self.config.alphabet();
512        let pad = self.config.emit_padding();
513        let mut quartet = [0u8; 4];
514        let written = if self.buf_len == 1 {
515            let n = (self.buf[0] as u32) << 16;
516            quartet[0] = alpha[((n >> 18) & 63) as usize];
517            quartet[1] = alpha[((n >> 12) & 63) as usize];
518            if pad {
519                quartet[2] = b'=';
520                quartet[3] = b'=';
521                4
522            } else {
523                2
524            }
525        } else {
526            let n = ((self.buf[0] as u32) << 16) | ((self.buf[1] as u32) << 8);
527            quartet[0] = alpha[((n >> 18) & 63) as usize];
528            quartet[1] = alpha[((n >> 12) & 63) as usize];
529            quartet[2] = alpha[((n >> 6) & 63) as usize];
530            if pad {
531                quartet[3] = b'=';
532                4
533            } else {
534                3
535            }
536        };
537        self.write_wrapped(&quartet[..written])?;
538        self.buf_len = 0;
539        Ok(())
540    }
541
542    fn write_wrapped(&mut self, data: &[u8]) -> io::Result<()> {
543        let width = match self.config.line_wrap {
544            Some(w) if w > 0 => w,
545            _ => {
546                self.inner.write_all(data)?;
547                self.line_pos += data.len();
548                return Ok(());
549            }
550        };
551
552        let mut offset = 0;
553        while offset < data.len() {
554            let remaining_on_line = width.saturating_sub(self.line_pos);
555            if remaining_on_line == 0 {
556                self.inner.write_all(b"\r\n")?;
557                self.line_pos = 0;
558                continue;
559            }
560            let chunk = std::cmp::min(remaining_on_line, data.len() - offset);
561            self.inner.write_all(&data[offset..offset + chunk])?;
562            self.line_pos += chunk;
563            offset += chunk;
564        }
565        Ok(())
566    }
567
568    fn encode_triple(&mut self, b0: u8, b1: u8, b2: u8) -> io::Result<()> {
569        let alpha = self.config.alphabet();
570        let n = ((b0 as u32) << 16) | ((b1 as u32) << 8) | (b2 as u32);
571        let quartet = [
572            alpha[((n >> 18) & 63) as usize],
573            alpha[((n >> 12) & 63) as usize],
574            alpha[((n >> 6) & 63) as usize],
575            alpha[(n & 63) as usize],
576        ];
577        self.write_wrapped(&quartet)
578    }
579}
580
581impl<W: Write> Write for Base64Encoder<W> {
582    fn write(&mut self, input: &[u8]) -> io::Result<usize> {
583        if input.is_empty() {
584            return Ok(0);
585        }
586
587        let mut pos = 0;
588
589        if self.buf_len == 1 {
590            if pos < input.len() {
591                let b1 = input[pos];
592                pos += 1;
593                if pos < input.len() {
594                    let b2 = input[pos];
595                    pos += 1;
596                    self.encode_triple(self.buf[0], b1, b2)?;
597                    self.buf_len = 0;
598                } else {
599                    self.buf[1] = b1;
600                    self.buf_len = 2;
601                    return Ok(input.len());
602                }
603            }
604        } else if self.buf_len == 2 {
605            if pos < input.len() {
606                let b2 = input[pos];
607                pos += 1;
608                self.encode_triple(self.buf[0], self.buf[1], b2)?;
609                self.buf_len = 0;
610            } else {
611                return Ok(input.len());
612            }
613        }
614
615        let remaining = &input[pos..];
616        let full_triples = remaining.len() / 3;
617        for i in 0..full_triples {
618            let base = i * 3;
619            self.encode_triple(remaining[base], remaining[base + 1], remaining[base + 2])?;
620        }
621
622        let leftover_start = pos + full_triples * 3;
623        let leftover = input.len() - leftover_start;
624        if leftover >= 1 {
625            self.buf[0] = input[leftover_start];
626            self.buf_len = 1;
627            if leftover >= 2 {
628                self.buf[1] = input[leftover_start + 1];
629                self.buf_len = 2;
630            }
631        }
632
633        Ok(input.len())
634    }
635
636    fn flush(&mut self) -> io::Result<()> {
637        self.inner.flush()
638    }
639}
640
641impl<W: Write> Drop for Base64Encoder<W> {
642    fn drop(&mut self) {
643        if !self.finished {
644            let _ = self.flush_final();
645        }
646        // SAFETY: drop is called exactly once, and if finish() was called
647        // it already took inner out. If not, we drop it here.
648        if !self.finished {
649            unsafe {
650                ManuallyDrop::drop(&mut self.inner);
651            }
652        }
653    }
654}
655
656// ---------------------------------------------------------------------------
657// Streaming decoder
658// ---------------------------------------------------------------------------
659
660/// A streaming Base64 decoder that wraps an [`io::Read`] source.
661pub struct Base64Decoder<R: Read> {
662    inner: R,
663    config: Base64Config,
664    out_buf: Vec<u8>,
665    out_pos: usize,
666    quad: [u8; 4],
667    quad_len: usize,
668    finished: bool,
669}
670
671impl<R: Read> Base64Decoder<R> {
672    /// Create a new streaming decoder.
673    pub fn new(inner: R, config: Base64Config) -> Self {
674        Self {
675            inner,
676            config,
677            out_buf: Vec::with_capacity(768),
678            out_pos: 0,
679            quad: [0; 4],
680            quad_len: 0,
681            finished: false,
682        }
683    }
684
685    fn decode_quad_full(&mut self) -> io::Result<()> {
686        let table = self.config.decode_table();
687        let q = &self.quad;
688        let pad_count = q.iter().rev().take_while(|&&b| b == b'=').count();
689        let c0 = table[q[0] as usize] as u32;
690        let c1 = table[q[1] as usize] as u32;
691        let c2 = if q[2] == b'=' {
692            0u32
693        } else {
694            table[q[2] as usize] as u32
695        };
696        let c3 = if q[3] == b'=' {
697            0u32
698        } else {
699            table[q[3] as usize] as u32
700        };
701        let n = (c0 << 18) | (c1 << 12) | (c2 << 6) | c3;
702        self.out_buf.push(((n >> 16) & 0xFF) as u8);
703        if pad_count < 2 {
704            self.out_buf.push(((n >> 8) & 0xFF) as u8);
705        }
706        if pad_count == 0 {
707            self.out_buf.push((n & 0xFF) as u8);
708        }
709        if pad_count > 0 {
710            self.finished = true;
711        }
712        self.quad_len = 0;
713        Ok(())
714    }
715
716    fn decode_quad_partial(&mut self) -> io::Result<()> {
717        let table = self.config.decode_table();
718        let q = &self.quad;
719        let qlen = self.quad_len;
720        match qlen {
721            0 => {}
722            2 => {
723                let c0 = table[q[0] as usize] as u32;
724                let c1 = table[q[1] as usize] as u32;
725                let n = (c0 << 18) | (c1 << 12);
726                self.out_buf.push(((n >> 16) & 0xFF) as u8);
727            }
728            3 => {
729                let c0 = table[q[0] as usize] as u32;
730                let c1 = table[q[1] as usize] as u32;
731                let c2 = table[q[2] as usize] as u32;
732                let n = (c0 << 18) | (c1 << 12) | (c2 << 6);
733                self.out_buf.push(((n >> 16) & 0xFF) as u8);
734                self.out_buf.push(((n >> 8) & 0xFF) as u8);
735            }
736            1 => {
737                return Err(io::Error::new(
738                    io::ErrorKind::InvalidData,
739                    "invalid base64 stream: incomplete quad (1 char)",
740                ));
741            }
742            _ => {
743                return Err(io::Error::new(
744                    io::ErrorKind::InvalidData,
745                    "invalid base64 stream: unexpected quad length",
746                ));
747            }
748        }
749        self.quad_len = 0;
750        self.finished = true;
751        Ok(())
752    }
753
754    fn drain_output(&mut self, buf: &mut [u8]) -> usize {
755        let avail = self.out_buf.len() - self.out_pos;
756        if avail == 0 {
757            return 0;
758        }
759        let n = std::cmp::min(avail, buf.len());
760        buf[..n].copy_from_slice(&self.out_buf[self.out_pos..self.out_pos + n]);
761        self.out_pos += n;
762        if self.out_pos == self.out_buf.len() {
763            self.out_buf.clear();
764            self.out_pos = 0;
765        }
766        n
767    }
768}
769
770impl<R: Read> Read for Base64Decoder<R> {
771    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
772        let drained = self.drain_output(buf);
773        if drained > 0 {
774            return Ok(drained);
775        }
776
777        if self.finished {
778            return Ok(0);
779        }
780
781        let table = self.config.decode_table();
782
783        let mut read_buf = [0u8; 1024];
784        let bytes_read = self.inner.read(&mut read_buf)?;
785        if bytes_read == 0 {
786            if self.quad_len > 0 {
787                self.decode_quad_partial()?;
788            } else {
789                self.finished = true;
790            }
791            let n = self.drain_output(buf);
792            if n == 0 {
793                self.finished = true;
794            }
795            return Ok(n);
796        }
797
798        for &b in &read_buf[..bytes_read] {
799            if matches!(b, b' ' | b'\t' | b'\r' | b'\n') {
800                continue;
801            }
802            if b != b'=' && table[b as usize] == 255 {
803                return Err(io::Error::new(
804                    io::ErrorKind::InvalidData,
805                    format!("invalid base64 character: 0x{b:02X}"),
806                ));
807            }
808            self.quad[self.quad_len] = b;
809            self.quad_len += 1;
810            if self.quad_len == 4 {
811                self.decode_quad_full()?;
812            }
813        }
814
815        let n = self.drain_output(buf);
816        if n == 0 && !self.finished {
817            return self.read(buf);
818        }
819        Ok(n)
820    }
821}
822
823// ---------------------------------------------------------------------------
824// Validation helpers
825// ---------------------------------------------------------------------------
826
827/// Check if a string is valid base64 for the given configuration.
828pub fn base64_is_valid_config(s: &str, config: &Base64Config) -> bool {
829    base64_decode_config(s, config).is_ok()
830}
831
832/// Validate that a string is valid URL-safe base64.
833pub fn base64_is_valid_url_safe(s: &str) -> bool {
834    base64_decode_config(s, &Base64Config::url_safe()).is_ok()
835}
836
837// ---------------------------------------------------------------------------
838// Tests
839// ---------------------------------------------------------------------------
840
841#[cfg(test)]
842mod tests {
843    use super::*;
844    use std::io::Cursor;
845
846    #[test]
847    fn encode_empty() {
848        assert_eq!(base64_encode(b""), "");
849    }
850
851    #[test]
852    fn encode_decode_roundtrip() {
853        let data = b"Hello, World!";
854        let enc = base64_encode(data);
855        let dec = base64_decode(&enc).expect("should succeed");
856        assert_eq!(dec, data);
857    }
858
859    #[test]
860    fn encode_one_byte() {
861        let enc = base64_encode(b"M");
862        assert_eq!(enc, "TQ==");
863    }
864
865    #[test]
866    fn encode_two_bytes() {
867        let enc = base64_encode(b"Ma");
868        assert_eq!(enc, "TWE=");
869    }
870
871    #[test]
872    fn is_valid_true() {
873        assert!(base64_is_valid("SGVsbG8="));
874    }
875
876    #[test]
877    fn is_valid_false_bad_char() {
878        assert!(!base64_is_valid("SG?s"));
879    }
880
881    #[test]
882    fn decoded_len_estimate() {
883        assert_eq!(base64_decoded_len(8), 6);
884    }
885
886    #[test]
887    fn url_safe_encode_decode() {
888        let data = b"\xfb\xff\xfe";
889        let standard = base64_encode(data);
890        assert!(standard.contains('+') || standard.contains('/'));
891
892        let url = base64_encode_url_safe(data);
893        assert!(!url.contains('+'));
894        assert!(!url.contains('/'));
895
896        let decoded = base64_decode_url_safe(&url).expect("should succeed");
897        assert_eq!(decoded, data);
898    }
899
900    #[test]
901    fn url_safe_roundtrip_various() {
902        for input in &[b"" as &[u8], b"a", b"ab", b"abc", b"abcd", b"Hello, World!"] {
903            let enc = base64_encode_url_safe(input);
904            let dec = base64_decode_url_safe(&enc).expect("should succeed");
905            assert_eq!(&dec, input, "failed for input len={}", input.len());
906        }
907    }
908
909    #[test]
910    fn no_padding_encode() {
911        let config = Base64Config {
912            variant: Base64Variant::Standard,
913            padding: Base64Padding::NoPad,
914            line_wrap: None,
915        };
916        let enc = base64_encode_config(b"M", &config);
917        assert_eq!(enc, "TQ");
918        let enc2 = base64_encode_config(b"Ma", &config);
919        assert_eq!(enc2, "TWE");
920
921        let dec = base64_decode_config("TQ", &config).expect("should succeed");
922        assert_eq!(dec, b"M");
923        let dec2 = base64_decode_config("TWE", &config).expect("should succeed");
924        assert_eq!(dec2, b"Ma");
925    }
926
927    #[test]
928    fn no_padding_accepts_padding_on_decode() {
929        let config = Base64Config {
930            variant: Base64Variant::Standard,
931            padding: Base64Padding::NoPad,
932            line_wrap: None,
933        };
934        let dec = base64_decode_config("TQ==", &config).expect("should succeed");
935        assert_eq!(dec, b"M");
936    }
937
938    #[test]
939    fn line_wrapping_mime() {
940        let data = vec![0xAA; 57];
941        let enc = base64_encode_mime(&data);
942        assert!(
943            !enc.contains("\r\n"),
944            "57 bytes should fit in one 76-char line"
945        );
946        assert_eq!(enc.len(), 76);
947
948        let data2 = vec![0xBB; 58];
949        let enc2 = base64_encode_mime(&data2);
950        assert!(enc2.contains("\r\n"), "58 bytes should cause wrapping");
951
952        let dec = base64_decode_config(&enc2, &Base64Config::standard()).expect("should succeed");
953        assert_eq!(dec, data2);
954    }
955
956    #[test]
957    fn whitespace_tolerance() {
958        let encoded = "SGVs\r\n bG8s\tIFdv\ncmxkIQ==";
959        let dec = base64_decode_config(encoded, &Base64Config::standard()).expect("should succeed");
960        assert_eq!(dec, b"Hello, World!");
961    }
962
963    #[test]
964    fn error_invalid_char() {
965        let result = base64_decode_config("SG?s", &Base64Config::standard());
966        match result {
967            Err(Base64DecodeError::InvalidCharacter { byte, .. }) => {
968                assert_eq!(byte, b'?');
969            }
970            other => panic!("expected InvalidCharacter, got {other:?}"),
971        }
972    }
973
974    #[test]
975    fn error_invalid_padding() {
976        let result = base64_decode_config("S=Gs", &Base64Config::standard());
977        assert!(matches!(result, Err(Base64DecodeError::InvalidPadding)));
978    }
979
980    #[test]
981    fn error_invalid_length() {
982        let result = base64_decode_config("A", &Base64Config::standard());
983        assert!(matches!(
984            result,
985            Err(Base64DecodeError::InvalidLength { .. })
986        ));
987    }
988
989    #[test]
990    fn error_display() {
991        let e1 = Base64DecodeError::InvalidCharacter {
992            byte: 0x3F,
993            position: 2,
994        };
995        assert_eq!(
996            format!("{e1}"),
997            "invalid base64 character 0x3F at position 2"
998        );
999        let e2 = Base64DecodeError::InvalidPadding;
1000        assert_eq!(format!("{e2}"), "invalid base64 padding");
1001        let e3 = Base64DecodeError::InvalidLength { length: 5 };
1002        assert_eq!(format!("{e3}"), "invalid base64 length 5 (mod 4 == 1)");
1003    }
1004
1005    #[test]
1006    fn streaming_encode_matches_oneshot() {
1007        let data = b"Hello, World! This is a streaming test with enough data.";
1008        let expected = base64_encode(data);
1009
1010        let mut output = Vec::new();
1011        {
1012            let mut encoder = Base64Encoder::new(&mut output, Base64Config::standard());
1013            for chunk in data.chunks(7) {
1014                encoder.write_all(chunk).expect("should succeed");
1015            }
1016            let _ = encoder.finish().expect("should succeed");
1017        }
1018        let result = String::from_utf8(output).expect("should succeed");
1019        assert_eq!(result, expected);
1020    }
1021
1022    #[test]
1023    fn streaming_encode_single_byte_writes() {
1024        let data = b"ABC";
1025        let expected = base64_encode(data);
1026
1027        let mut output = Vec::new();
1028        {
1029            let mut encoder = Base64Encoder::new(&mut output, Base64Config::standard());
1030            for &b in data.iter() {
1031                encoder.write_all(&[b]).expect("should succeed");
1032            }
1033            let _ = encoder.finish().expect("should succeed");
1034        }
1035        let result = String::from_utf8(output).expect("should succeed");
1036        assert_eq!(result, expected);
1037    }
1038
1039    #[test]
1040    fn streaming_encode_with_wrapping() {
1041        let data = vec![0xCC; 120];
1042        let expected = base64_encode_mime(&data);
1043
1044        let mut output = Vec::new();
1045        {
1046            let mut encoder = Base64Encoder::new(&mut output, Base64Config::mime());
1047            encoder.write_all(&data).expect("should succeed");
1048            let _ = encoder.finish().expect("should succeed");
1049        }
1050        let result = String::from_utf8(output).expect("should succeed");
1051        assert_eq!(result, expected);
1052    }
1053
1054    #[test]
1055    fn streaming_decode_matches_oneshot() {
1056        let original = b"Streaming decode test data!";
1057        let encoded = base64_encode(original);
1058
1059        let cursor = Cursor::new(encoded.as_bytes());
1060        let mut decoder = Base64Decoder::new(cursor, Base64Config::standard());
1061        let mut decoded = Vec::new();
1062        decoder.read_to_end(&mut decoded).expect("should succeed");
1063        assert_eq!(decoded, original);
1064    }
1065
1066    #[test]
1067    fn streaming_decode_with_whitespace() {
1068        let original = b"whitespace tolerant streaming";
1069        let encoded = base64_encode(original);
1070        let with_ws: String = encoded
1071            .chars()
1072            .enumerate()
1073            .flat_map(|(i, c)| {
1074                if i > 0 && i % 10 == 0 {
1075                    vec!['\n', c]
1076                } else {
1077                    vec![c]
1078                }
1079            })
1080            .collect();
1081
1082        let cursor = Cursor::new(with_ws.as_bytes());
1083        let mut decoder = Base64Decoder::new(cursor, Base64Config::standard());
1084        let mut decoded = Vec::new();
1085        decoder.read_to_end(&mut decoded).expect("should succeed");
1086        assert_eq!(decoded, original);
1087    }
1088
1089    #[test]
1090    fn streaming_encode_url_safe_no_pad() {
1091        let data = b"url safe streaming!";
1092        let expected = base64_encode_url_safe(data);
1093
1094        let mut output = Vec::new();
1095        {
1096            let mut encoder = Base64Encoder::new(&mut output, Base64Config::url_safe());
1097            encoder.write_all(data).expect("should succeed");
1098            let _ = encoder.finish().expect("should succeed");
1099        }
1100        let result = String::from_utf8(output).expect("should succeed");
1101        assert_eq!(result, expected);
1102    }
1103
1104    #[test]
1105    fn streaming_decode_url_safe() {
1106        let original = b"url safe decode test";
1107        let encoded = base64_encode_url_safe(original);
1108
1109        let cursor = Cursor::new(encoded.as_bytes());
1110        let mut decoder = Base64Decoder::new(cursor, Base64Config::url_safe());
1111        let mut decoded = Vec::new();
1112        decoder.read_to_end(&mut decoded).expect("should succeed");
1113        assert_eq!(decoded, original);
1114    }
1115
1116    #[test]
1117    fn roundtrip_all_byte_values() {
1118        let data: Vec<u8> = (0..=255).collect();
1119        let enc = base64_encode(&data);
1120        let dec = base64_decode(&enc).expect("should succeed");
1121        assert_eq!(dec, data);
1122
1123        let enc_url = base64_encode_url_safe(&data);
1124        let dec_url = base64_decode_url_safe(&enc_url).expect("should succeed");
1125        assert_eq!(dec_url, data);
1126    }
1127
1128    #[test]
1129    fn config_presets() {
1130        let s = Base64Config::standard();
1131        assert_eq!(s.variant, Base64Variant::Standard);
1132        assert_eq!(s.padding, Base64Padding::Pad);
1133        assert!(s.line_wrap.is_none());
1134
1135        let m = Base64Config::mime();
1136        assert_eq!(m.line_wrap, Some(76));
1137
1138        let u = Base64Config::url_safe();
1139        assert_eq!(u.variant, Base64Variant::UrlSafe);
1140        assert_eq!(u.padding, Base64Padding::NoPad);
1141
1142        let up = Base64Config::url_safe_padded();
1143        assert_eq!(up.variant, Base64Variant::UrlSafe);
1144        assert_eq!(up.padding, Base64Padding::Pad);
1145    }
1146
1147    #[test]
1148    fn is_valid_url_safe() {
1149        let enc = base64_encode_url_safe(b"test");
1150        assert!(base64_is_valid_url_safe(&enc));
1151        assert!(!base64_is_valid_url_safe("not+valid/url=safe=="));
1152    }
1153
1154    #[test]
1155    fn decode_empty() {
1156        let dec = base64_decode("").expect("should succeed");
1157        assert!(dec.is_empty());
1158
1159        let dec2 = base64_decode_config("", &Base64Config::url_safe()).expect("should succeed");
1160        assert!(dec2.is_empty());
1161    }
1162
1163    #[test]
1164    fn encoded_len_correctness() {
1165        assert_eq!(base64_encoded_len(0), 0);
1166        assert_eq!(base64_encoded_len(1), 4);
1167        assert_eq!(base64_encoded_len(2), 4);
1168        assert_eq!(base64_encoded_len(3), 4);
1169        assert_eq!(base64_encoded_len(4), 8);
1170    }
1171
1172    #[test]
1173    fn base64_error_is_std_error() {
1174        let e: Box<dyn std::error::Error> = Box::new(Base64DecodeError::InvalidPadding);
1175        assert!(!e.to_string().is_empty());
1176    }
1177
1178    #[test]
1179    fn streaming_roundtrip_large() {
1180        let data: Vec<u8> = (0..4096).map(|i| (i % 256) as u8).collect();
1181        let encoded = base64_encode(&data);
1182
1183        let mut enc_out = Vec::new();
1184        {
1185            let mut encoder = Base64Encoder::new(&mut enc_out, Base64Config::standard());
1186            for chunk in data.chunks(100) {
1187                encoder.write_all(chunk).expect("should succeed");
1188            }
1189            let _ = encoder.finish().expect("should succeed");
1190        }
1191        assert_eq!(String::from_utf8(enc_out).expect("should succeed"), encoded);
1192
1193        let cursor = Cursor::new(encoded.as_bytes());
1194        let mut decoder = Base64Decoder::new(cursor, Base64Config::standard());
1195        let mut decoded = Vec::new();
1196        decoder.read_to_end(&mut decoded).expect("should succeed");
1197        assert_eq!(decoded, data);
1198    }
1199
1200    #[test]
1201    fn backward_compat_decode_error() {
1202        let result = base64_decode("????");
1203        assert!(result.is_err());
1204        let err_msg = result.unwrap_err();
1205        assert!(!err_msg.is_empty());
1206    }
1207}