1#![allow(dead_code)]
11
12use std::fmt;
13use std::io::{self, Read, Write};
14use std::mem::ManuallyDrop;
15
16const STANDARD_ALPHABET: &[u8; 64] =
21 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
22
23const URL_SAFE_ALPHABET: &[u8; 64] =
24 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
25
26const 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
40const ALPHABET: &[u8; 64] = STANDARD_ALPHABET;
42
43#[derive(Debug, Clone, PartialEq, Eq)]
49pub enum Base64DecodeError {
50 InvalidCharacter {
53 byte: u8,
55 position: usize,
57 },
58 InvalidPadding,
60 InvalidLength {
63 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
93pub enum Base64Variant {
94 #[default]
96 Standard,
97 UrlSafe,
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
103pub enum Base64Padding {
104 #[default]
106 Pad,
107 NoPad,
109}
110
111#[derive(Debug, Clone)]
113pub struct Base64Config {
114 pub variant: Base64Variant,
115 pub padding: Base64Padding,
116 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 pub fn standard() -> Self {
134 Self::default()
135 }
136
137 pub fn mime() -> Self {
139 Self {
140 line_wrap: Some(76),
141 ..Self::default()
142 }
143 }
144
145 pub fn url_safe() -> Self {
147 Self {
148 variant: Base64Variant::UrlSafe,
149 padding: Base64Padding::NoPad,
150 line_wrap: None,
151 }
152 }
153
154 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
182pub fn base64_encoded_len(input_len: usize) -> usize {
188 input_len.div_ceil(3) * 4
189}
190
191pub fn base64_decoded_len(encoded_len: usize) -> usize {
194 if encoded_len == 0 {
195 return 0;
196 }
197 encoded_len / 4 * 3
198}
199
200pub 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 if let Some(width) = config.line_wrap {
244 if width > 0 {
245 return apply_line_wrap(&out, width);
246 }
247 }
248
249 unsafe { String::from_utf8_unchecked(out) }
252}
253
254fn 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 unsafe { String::from_utf8_unchecked(wrapped) }
271}
272
273pub fn base64_decode_config(s: &str, config: &Base64Config) -> Result<Vec<u8>, Base64DecodeError> {
282 let table = config.decode_table();
283
284 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 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 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 let effective_len = body_len + pad_count;
316 let remainder = effective_len % 4;
317 if pad_count > 0 && remainder != 0 {
319 return Err(Base64DecodeError::InvalidLength {
320 length: effective_len,
321 });
322 }
323
324 let total_chars = body_len;
326 let tail_chars = if pad_count > 0 {
327 0 } 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 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 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 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
402pub fn base64_encode(data: &[u8]) -> String {
408 base64_encode_config(data, &Base64Config::standard())
409}
410
411pub 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
420pub 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
438fn 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
451pub fn base64_encode_url_safe(data: &[u8]) -> String {
457 base64_encode_config(data, &Base64Config::url_safe())
458}
459
460pub fn base64_decode_url_safe(s: &str) -> Result<Vec<u8>, Base64DecodeError> {
462 base64_decode_config(s, &Base64Config::url_safe())
463}
464
465pub fn base64_encode_mime(data: &[u8]) -> String {
467 base64_encode_config(data, &Base64Config::mime())
468}
469
470pub 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 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 pub fn finish(mut self) -> io::Result<W> {
500 self.flush_final()?;
501 self.finished = true;
502 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 if !self.finished {
649 unsafe {
650 ManuallyDrop::drop(&mut self.inner);
651 }
652 }
653 }
654}
655
656pub 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 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
823pub fn base64_is_valid_config(s: &str, config: &Base64Config) -> bool {
829 base64_decode_config(s, config).is_ok()
830}
831
832pub fn base64_is_valid_url_safe(s: &str) -> bool {
834 base64_decode_config(s, &Base64Config::url_safe()).is_ok()
835}
836
837#[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}