radix64 0.6.2

A base64 library by another name
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
use proptest::prelude::Strategy;
use radix64::io::EncodeWriter;
use radix64::{Config, CRYPT, FAST, STD, STD_NO_PAD, URL_SAFE, URL_SAFE_NO_PAD};
use std::io;

// Create a custom config that should match each of the builtin configs.
mod custom_configs {
    use radix64::CustomConfig;
    lazy_static::lazy_static! {

        pub static ref STD: CustomConfig = CustomConfig::with_alphabet(
            "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
        )
        .with_padding(b'=')
        .build()
        .expect("failed to build custom base64 config");

        pub static ref STD_NO_PAD: CustomConfig = CustomConfig::with_alphabet(
            "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
        )
        .no_padding()
        .build()
        .expect("failed to build custom base64 config");

        pub static ref URL_SAFE: CustomConfig = CustomConfig::with_alphabet(
            "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",
        )
        .with_padding(b'=')
        .build()
        .expect("failed to build custom base64 config");

        pub static ref URL_SAFE_NO_PAD: CustomConfig = CustomConfig::with_alphabet(
            "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",
        )
        .no_padding()
        .build()
        .expect("failed to build custom base64 config");

        pub static ref CRYPT: CustomConfig = CustomConfig::with_alphabet(
            "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
        )
        .no_padding()
        .build()
        .expect("failed to build custom base64 config");
        pub static ref FAST: CustomConfig = CustomConfig::with_alphabet(
            r#">?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}"#
        )
        .no_padding()
        .build()
        .expect("failed to build custom base64 config");

    }
}

macro_rules! tests_for_configs {
    ($( $cfg:ident ),+) => {
        #[cfg(test)]
        mod property_tests {
            $(
            #[allow(non_snake_case)]
            mod $cfg {
                use crate::*;
                use proptest::prelude::{any, proptest};
                use proptest::collection::vec;
                proptest! {
                    #[test]
                    fn roundtrip(input in any::<Vec<u8>>()) {
                        let encoded = $cfg.encode(&input);
                        let decoded = $cfg.decode(&encoded).expect("decode failed");
                        assert_eq!(input, decoded);
                    }

                    #[test]
                    fn custom_can_be_decoded_by_builtin(input in any::<Vec<u8>>()) {
                        let encoded = custom_configs::$cfg.encode(&input);
                        let decoded = $cfg.decode(&encoded).expect("decode failed");
                        assert_eq!(input, decoded);
                    }

                    #[test]
                    fn custom_can_decode_builtin(input in any::<Vec<u8>>()) {
                        let encoded = $cfg.encode(&input);
                        let decoded = custom_configs::$cfg.decode(&encoded).expect("decode failed");
                        assert_eq!(input, decoded);
                    }

                    #[test]
                    fn encode_with_buffer_matches_encode(input in any::<Vec<u8>>()) {
                        let encoded = $cfg.encode(&input);
                        let mut buf = Vec::new();
                        let encoded_buf = $cfg.encode_with_buffer(&input, &mut buf);
                        assert_eq!(encoded, encoded_buf);
                    }

                    #[test]
                    fn encode_slice_matches_encode(input in any::<Vec<u8>>()) {
                        let mut encoded_vec = vec![0; input.len() * 4 / 3 + 3];
                        let bytes_written = $cfg.encode_slice(&input, encoded_vec.as_mut_slice());
                        encoded_vec.truncate(bytes_written);
                        let encoded_string = $cfg.encode(&input);
                        assert_eq!(encoded_vec.as_slice(), encoded_string.as_bytes())
                    }

                    #[test]
                    fn display_matches_encode(input in any::<Vec<u8>>()) {
                        let encoded = $cfg.encode(&input);
                        let display = radix64::Display::new($cfg, &input).to_string();
                        assert_eq!(encoded, display);
                    }

                    #[test]
                    fn decode_with_buffer_matches_decode(input in any::<Vec<u8>>()) {
                        let encoded = $cfg.encode(&input);
                        let mut buf = Vec::new();
                        let decoded_buf = $cfg.decode_with_buffer(&encoded, &mut buf).expect("decode failed");
                        let decoded_vec = $cfg.decode(&encoded).expect("decode failed");
                        assert_eq!(decoded_buf, decoded_vec.as_slice());
                    }

                    #[test]
                    fn decode_slice_matches_decode(input in any::<Vec<u8>>()) {
                        let encoded = $cfg.encode(&input);
                        let mut decoded_slice = vec![0; input.len()];
                        let bytes_written = $cfg.decode_slice(&encoded, decoded_slice.as_mut_slice()).expect("decode failed");
                        decoded_slice.truncate(bytes_written);
                        let decoded_vec = $cfg.decode(&encoded).expect("decode failed");
                        assert_eq!(decoded_slice, decoded_vec.as_slice());
                    }

                    #[test]
                    fn encode_slice_always_panics_or_returns_ascii(input in any::<Vec<u8>>(), output_len in 0..1000usize) {
                        let res = std::panic::catch_unwind(|| {
                            let mut encoded = vec![255; output_len];
                            let bytes_written = $cfg.encode_slice(&input, encoded.as_mut_slice());
                            encoded.truncate(bytes_written);
                            encoded
                        });
                        match res {
                            Ok(encoded) => assert!(encoded.iter().all(u8::is_ascii)),
                            Err(_) => {}, // Panic is expected when output len is too short.
                        }
                    }

                    // encode_with_buffer does an unchecked conversion from a
                    // slice of bytes to a &str. This is just a sanity test to
                    // verify the string returned is valid UTF-8.
                    #[test]
                    fn encode_buffer_returns_valid_str(input in any::<Vec<u8>>()) {
                        let mut buffer = Vec::new();
                        let encoded = $cfg.encode_with_buffer(&input, &mut buffer);
                        std::str::from_utf8(encoded.as_bytes()).expect("invalid UTF-8 returned from encode_with_buffer");
                    }

                    // encode does an unchecked conversion from a slice of bytes
                    // to a &str. This is just a sanity test to verify the
                    // string returned is valid UTF-8.
                    #[test]
                    fn encode_returns_valid_str(input in any::<Vec<u8>>()) {
                        let encoded = $cfg.encode(&input);
                        std::str::from_utf8(encoded.as_bytes()).expect("invalid UTF-8 returned from encode_with_buffer");
                    }

                    // Write input through an EncodeWriter ensuring that the output matches.
                    // The reads are done with varying buffer sizes to try and
                    // catch edge cases around chunking.
                    #[test]
                    fn encode_writer_matches((input, flaky_behavior) in vec_and_flaky_writer_behavior()) {
                        use radix64::io::EncodeWriter;
                        let encoded = $cfg.encode(&input);
                        let mut writer_encoded = Vec::new();
                        {
                            let flaky_writer = FlakyWriter::new(&mut writer_encoded, flaky_behavior.into_iter());
                            let mut writer = EncodeWriter::new($cfg, flaky_writer);
                            write_all_with_retries(&mut writer, &input);
                            finish_encode_writer_with_retries(writer);
                        }
                        assert_eq!(encoded.as_bytes(), writer_encoded.as_slice());
                    }

                    #[test]
                    fn encode_writer_one_byte_writes(input in any::<Vec<u8>>()) {
                        use radix64::io::EncodeWriter;
                        use std::io::{Cursor, Write};
                        let encoded = $cfg.encode(&input);
                        let mut writer_encoded = Vec::new();
                        {
                            let mut writer = EncodeWriter::new($cfg, Cursor::new(&mut writer_encoded));
                            for b in input {
                                writer.write(&[b][..]).expect("write failed");
                                // invoking flush is not necessary, but nice to
                                // exercise that codepath somewhere.
                                writer.flush().expect("flush failed");
                            }
                            writer.finish().expect("finish failed");
                        }
                        assert_eq!(encoded.as_bytes(), writer_encoded.as_slice());
                    }

                    // Ensure that EncodeWriter writes the final partial chunk on Drop.
                    #[test]
                    fn encode_writer_writes_final_chunk_on_drop(input in any::<Vec<u8>>()) {
                        use std::io::Write;
                        use radix64::io::EncodeWriter;
                        let encoded = $cfg.encode(&input);
                        let mut writer_encoded = Vec::new();
                        {
                            let mut writer = EncodeWriter::new($cfg, &mut writer_encoded);
                            writer.write_all(&input).expect("failed to write all input");
                            // do not call finish explicitly.
                        }
                        assert_eq!(encoded.as_bytes(), writer_encoded.as_slice());
                    }

                    // read a vector from a DecodeReader, ensuring that it matches the encoded input.
                    // The reads are done with varying buffer sizes to try and
                    // catch edge cases around chunking.
                    #[test]
                    fn decode_reader_roundtrip((input, buffer_sizes) in vec_and_buffer_sizes()) {
                        use radix64::io::DecodeReader;
                        use std::io::Cursor;
                        let encoded = $cfg.encode(&input);
                        let reader = DecodeReader::new($cfg, Cursor::new(encoded));
                        let decoded = read_to_end_using_varying_buffer_sizes(reader, buffer_sizes.iter().cloned()).expect("failed to read to the end of input");
                        assert_eq!(input, decoded);
                    }

                    // ensure that padding in the middle of the input stream is not silently accepted.
                    // The buffer sizes to use are randomly chosen between 1 and 5.
                    #[test]
                    fn decode_reader_reject_middle_padding(buffer_sizes in vec(1 as usize ..5, 1..3)) {
                        use radix64::io::DecodeReader;
                        use std::io::Cursor;
                        let mut input = $cfg.encode("A");
                        input.push_str(&$cfg.encode("BBB"));
                        let reader = DecodeReader::new($cfg, Cursor::new(&input));
                        match read_to_end_using_varying_buffer_sizes(reader, buffer_sizes.iter().cloned()) {
                            Ok(_) => panic!("incorrect padding accepted"),
                            Err(_) => {}, // this is good
                        }
                    }

                    // ensure that reading from a DecodeReader and decoding from
                    // a vector result in the same response.
                    #[test]
                    fn decode_reader_error_matches_decode(input in any::<String>()) {
                        use radix64::io::DecodeReader;
                        use std::io::{Cursor, Read};
                        let mut reader = DecodeReader::new($cfg, Cursor::new(&input));
                        let mut buffer = Vec::new();
                        let reader_res = match reader.read_to_end(&mut buffer) {
                            Ok(_) => Ok(buffer),
                            Err(_) => Err(()),
                        };
                        let res = $cfg.decode(&input).map_err(|_| ());
                        assert_eq!(res, reader_res);
                    }
                }
            })+
        }
    }
}

// define a proptest strategy that returns a random buffer and an additional
// vector that contains usize values of buffer sizes to read from the buffer
// with. The buffer sizes are kept significantly smaller than the size of the
// random buffer to try and catch edge cases around chunked reads.
fn vec_and_buffer_sizes() -> impl Strategy<Value = (Vec<u8>, Vec<usize>)> {
    use proptest::collection::vec;
    use proptest::prelude::{any, Just};
    vec(any::<u8>(), 1..100).prop_flat_map(|v| {
        let len = v.len();
        let max_buffer_size = std::cmp::max(2, len / 3);
        (Just(v), vec(1..max_buffer_size, 1..10))
    })
}

// define a proptest strategy that returns a random buffer and an additional
// vector that contains a series of writer behaviors (max number of bytes the
// writer should consume, return an error, etc.). The max number of bytes
// consumed are kept significantly smaller than the size of the random buffer to
// try and catch edge cases around chunking.
fn vec_and_flaky_writer_behavior() -> impl Strategy<Value = (Vec<u8>, Vec<FlakyWriterBehavior>)> {
    use proptest::collection::vec;
    use proptest::prelude::{any, Just};
    vec(any::<u8>(), 1..1024).prop_flat_map(|v| {
        let len = v.len();
        let max_write_size = std::cmp::max(2, len / 3);
        // Flaky behavior cycles between 9 random conditions, and one condition
        // that consumes 1 bytes. This ensures that all flaky writers makes some
        // amount of progress.
        let flaky_behavior = vec![
            flaky_writer_behavior_strategy(max_write_size).boxed(),
            flaky_writer_behavior_strategy(max_write_size).boxed(),
            flaky_writer_behavior_strategy(max_write_size).boxed(),
            flaky_writer_behavior_strategy(max_write_size).boxed(),
            flaky_writer_behavior_strategy(max_write_size).boxed(),
            flaky_writer_behavior_strategy(max_write_size).boxed(),
            flaky_writer_behavior_strategy(max_write_size).boxed(),
            flaky_writer_behavior_strategy(max_write_size).boxed(),
            flaky_writer_behavior_strategy(max_write_size).boxed(),
            Just(FlakyWriterBehavior::ConsumeBytes(1)).boxed(),
        ];
        (Just(v), flaky_behavior)
    })
}

// A proptest strategry to return a FlakyWriterBehavior that never consumes more
// than max_write_size bytes.
fn flaky_writer_behavior_strategy(
    max_write_size: usize,
) -> impl Strategy<Value = FlakyWriterBehavior> {
    use proptest::prelude::{prop_oneof, Just};
    prop_oneof![
        // For cases without data, `Just` is all you need
        Just(FlakyWriterBehavior::Err(std::io::ErrorKind::Other)),
        // For cases with data, write a strategy for the interior data, then
        // map into the actual enum case.
        (0..max_write_size).prop_map(FlakyWriterBehavior::ConsumeBytes)
    ]
}

// read to the end of the provided reader collecting the results into a vector.
// The read calls to the reader are done in buffer sizes according to the passed
// in iterator.
// For example if the passed in iterator returns [1, 10, 5]. It will first issue
// a read of 1 byte in length, then 10 bytes, then 5 bytes, then 1 byte, rinse
// and repeat until EOF is reached.
fn read_to_end_using_varying_buffer_sizes<R, I>(
    mut rdr: R,
    buffer_sizes: I,
) -> std::io::Result<Vec<u8>>
where
    R: io::Read,
    I: Iterator<Item = usize> + Clone,
{
    let mut v = Vec::new();
    for buffer_size in buffer_sizes.cycle() {
        let prev_len = v.len();
        v.resize(prev_len + buffer_size, 0);
        let n = rdr.read(&mut v[prev_len..])?;
        v.truncate(prev_len + n);
        if n == 0 {
            return Ok(v);
        }
    }
    unreachable!();
}

// Not a generally useful utility. You wouldn't want to retry indefinitely, but
// in our case the errors are known to be intermittent and will resolve in a
// timely fashion.
fn write_all_with_retries<W>(mut writer: W, mut input: &[u8])
where
    W: io::Write,
{
    while !input.is_empty() {
        match writer.write(input) {
            Ok(n) => input = &input[n..],
            Err(_) => {}
        }
    }
}

// Again, not generally useful. Continue retrying EncodeWriter::finish until it
// eventually succeeds.
fn finish_encode_writer_with_retries<C, W>(mut writer: EncodeWriter<C, W>)
where
    C: Config,
    W: io::Write,
{
    loop {
        writer = match writer.finish() {
            Ok(_) => break,
            Err(finish_err) => finish_err.into_encode_writer(),
        }
    }
}

#[derive(Debug, Clone, Copy)]
enum FlakyWriterBehavior {
    ConsumeBytes(usize),
    Err(io::ErrorKind),
}

struct FlakyWriter<W, I> {
    writer: W,
    behavior_iter: std::iter::Cycle<I>,
}

impl<W, I> FlakyWriter<W, I>
where
    W: io::Write,
    I: Iterator<Item = FlakyWriterBehavior> + Clone,
{
    fn new(writer: W, behavior: I) -> Self {
        FlakyWriter {
            writer,
            behavior_iter: behavior.cycle(),
        }
    }
}

impl<W, I> io::Write for FlakyWriter<W, I>
where
    W: io::Write,
    std::iter::Cycle<I>: Iterator<Item = FlakyWriterBehavior>,
{
    fn write(&mut self, input: &[u8]) -> io::Result<usize> {
        let behavior = self.behavior_iter.next().unwrap();
        match behavior {
            FlakyWriterBehavior::ConsumeBytes(num_bytes) => {
                let num_bytes = std::cmp::min(input.len(), num_bytes);
                self.writer.write(&input[..num_bytes])
            }
            FlakyWriterBehavior::Err(kind) => {
                Err(io::Error::new(kind, "flaky writer error".to_owned()))
            }
        }
    }

    fn flush(&mut self) -> io::Result<()> {
        self.writer.flush()
    }
}

tests_for_configs!(STD, STD_NO_PAD, URL_SAFE, URL_SAFE_NO_PAD, CRYPT, FAST);