1 use crate::{
2     encode::add_padding,
3     engine::{Config, Engine},
4 };
5 #[cfg(any(feature = "alloc", test))]
6 use alloc::string::String;
7 #[cfg(any(feature = "alloc", test))]
8 use core::str;
9 
10 /// The output mechanism for ChunkedEncoder's encoded bytes.
11 pub trait Sink {
12     type Error;
13 
14     /// Handle a chunk of encoded base64 data (as UTF-8 bytes)
write_encoded_bytes(&mut self, encoded: &[u8]) -> Result<(), Self::Error>15     fn write_encoded_bytes(&mut self, encoded: &[u8]) -> Result<(), Self::Error>;
16 }
17 
18 /// A base64 encoder that emits encoded bytes in chunks without heap allocation.
19 pub struct ChunkedEncoder<'e, E: Engine + ?Sized> {
20     engine: &'e E,
21 }
22 
23 impl<'e, E: Engine + ?Sized> ChunkedEncoder<'e, E> {
new(engine: &'e E) -> ChunkedEncoder<'e, E>24     pub fn new(engine: &'e E) -> ChunkedEncoder<'e, E> {
25         ChunkedEncoder { engine }
26     }
27 
encode<S: Sink>(&self, bytes: &[u8], sink: &mut S) -> Result<(), S::Error>28     pub fn encode<S: Sink>(&self, bytes: &[u8], sink: &mut S) -> Result<(), S::Error> {
29         const BUF_SIZE: usize = 1024;
30         const CHUNK_SIZE: usize = BUF_SIZE / 4 * 3;
31 
32         let mut buf = [0; BUF_SIZE];
33         for chunk in bytes.chunks(CHUNK_SIZE) {
34             let mut len = self.engine.internal_encode(chunk, &mut buf);
35             if chunk.len() != CHUNK_SIZE && self.engine.config().encode_padding() {
36                 // Final, potentially partial, chunk.
37                 // Only need to consider if padding is needed on a partial chunk since full chunk
38                 // is a multiple of 3, which therefore won't be padded.
39                 // Pad output to multiple of four bytes if required by config.
40                 len += add_padding(len, &mut buf[len..]);
41             }
42             sink.write_encoded_bytes(&buf[..len])?;
43         }
44 
45         Ok(())
46     }
47 }
48 
49 // A really simple sink that just appends to a string
50 #[cfg(any(feature = "alloc", test))]
51 pub(crate) struct StringSink<'a> {
52     string: &'a mut String,
53 }
54 
55 #[cfg(any(feature = "alloc", test))]
56 impl<'a> StringSink<'a> {
new(s: &mut String) -> StringSink57     pub(crate) fn new(s: &mut String) -> StringSink {
58         StringSink { string: s }
59     }
60 }
61 
62 #[cfg(any(feature = "alloc", test))]
63 impl<'a> Sink for StringSink<'a> {
64     type Error = ();
65 
write_encoded_bytes(&mut self, s: &[u8]) -> Result<(), Self::Error>66     fn write_encoded_bytes(&mut self, s: &[u8]) -> Result<(), Self::Error> {
67         self.string.push_str(str::from_utf8(s).unwrap());
68 
69         Ok(())
70     }
71 }
72 
73 #[cfg(test)]
74 pub mod tests {
75     use rand::{
76         distributions::{Distribution, Uniform},
77         Rng, SeedableRng,
78     };
79 
80     use crate::{
81         alphabet::STANDARD,
82         engine::general_purpose::{GeneralPurpose, GeneralPurposeConfig, PAD},
83         tests::random_engine,
84     };
85 
86     use super::*;
87 
88     #[test]
chunked_encode_empty()89     fn chunked_encode_empty() {
90         assert_eq!("", chunked_encode_str(&[], PAD));
91     }
92 
93     #[test]
chunked_encode_intermediate_fast_loop()94     fn chunked_encode_intermediate_fast_loop() {
95         // > 8 bytes input, will enter the pretty fast loop
96         assert_eq!("Zm9vYmFyYmF6cXV4", chunked_encode_str(b"foobarbazqux", PAD));
97     }
98 
99     #[test]
chunked_encode_fast_loop()100     fn chunked_encode_fast_loop() {
101         // > 32 bytes input, will enter the uber fast loop
102         assert_eq!(
103             "Zm9vYmFyYmF6cXV4cXV1eGNvcmdlZ3JhdWx0Z2FycGx5eg==",
104             chunked_encode_str(b"foobarbazquxquuxcorgegraultgarplyz", PAD)
105         );
106     }
107 
108     #[test]
chunked_encode_slow_loop_only()109     fn chunked_encode_slow_loop_only() {
110         // < 8 bytes input, slow loop only
111         assert_eq!("Zm9vYmFy", chunked_encode_str(b"foobar", PAD));
112     }
113 
114     #[test]
chunked_encode_matches_normal_encode_random_string_sink()115     fn chunked_encode_matches_normal_encode_random_string_sink() {
116         let helper = StringSinkTestHelper;
117         chunked_encode_matches_normal_encode_random(&helper);
118     }
119 
chunked_encode_matches_normal_encode_random<S: SinkTestHelper>(sink_test_helper: &S)120     pub fn chunked_encode_matches_normal_encode_random<S: SinkTestHelper>(sink_test_helper: &S) {
121         let mut input_buf: Vec<u8> = Vec::new();
122         let mut output_buf = String::new();
123         let mut rng = rand::rngs::SmallRng::from_entropy();
124         let input_len_range = Uniform::new(1, 10_000);
125 
126         for _ in 0..20_000 {
127             input_buf.clear();
128             output_buf.clear();
129 
130             let buf_len = input_len_range.sample(&mut rng);
131             for _ in 0..buf_len {
132                 input_buf.push(rng.gen());
133             }
134 
135             let engine = random_engine(&mut rng);
136 
137             let chunk_encoded_string = sink_test_helper.encode_to_string(&engine, &input_buf);
138             engine.encode_string(&input_buf, &mut output_buf);
139 
140             assert_eq!(output_buf, chunk_encoded_string, "input len={}", buf_len);
141         }
142     }
143 
chunked_encode_str(bytes: &[u8], config: GeneralPurposeConfig) -> String144     fn chunked_encode_str(bytes: &[u8], config: GeneralPurposeConfig) -> String {
145         let mut s = String::new();
146 
147         let mut sink = StringSink::new(&mut s);
148         let engine = GeneralPurpose::new(&STANDARD, config);
149         let encoder = ChunkedEncoder::new(&engine);
150         encoder.encode(bytes, &mut sink).unwrap();
151 
152         s
153     }
154 
155     // An abstraction around sinks so that we can have tests that easily to any sink implementation
156     pub trait SinkTestHelper {
encode_to_string<E: Engine>(&self, engine: &E, bytes: &[u8]) -> String157         fn encode_to_string<E: Engine>(&self, engine: &E, bytes: &[u8]) -> String;
158     }
159 
160     struct StringSinkTestHelper;
161 
162     impl SinkTestHelper for StringSinkTestHelper {
encode_to_string<E: Engine>(&self, engine: &E, bytes: &[u8]) -> String163         fn encode_to_string<E: Engine>(&self, engine: &E, bytes: &[u8]) -> String {
164             let encoder = ChunkedEncoder::new(engine);
165             let mut s = String::new();
166             let mut sink = StringSink::new(&mut s);
167             encoder.encode(bytes, &mut sink).unwrap();
168 
169             s
170         }
171     }
172 }
173