Skip to content

Commit b07e0cc

Browse files
Return errors instead of panicking on _slice methods
1 parent e580966 commit b07e0cc

File tree

14 files changed

+437
-223
lines changed

14 files changed

+437
-223
lines changed

RELEASE-NOTES.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,16 @@
1010
usually expect from a `from` call.
1111
- `encode*` and `decode*` top level functions are now methods on `Engine`.
1212
- `DEFAULT_ENGINE` was replaced by `engine::general_purpose::STANDARD`
13-
- Predefined engine consts `engine::general_purpose::{STANDARD, URL_SAFE, URL_SAFE_NO_PAD}`
13+
- Predefined engine consts `engine::general_purpose::{STANDARD, STANDARD_NO_PAD, URL_SAFE, URL_SAFE_NO_PAD}`
1414
- These are `pub use`d into `engine` as well
15+
- The `*_slice` decode/encode functions now return an error instead of panicking when the output slice is too small
16+
- As part of this, there isn't now a public way to decode into a slice _exactly_ the size needed for inputs that
17+
aren't multiples of 4 tokens. If adding up to 2 bytes to always be a multiple of 3 bytes for the decode buffer is
18+
a problem, file an issue.
19+
20+
## Other changes
21+
22+
- `decoded_len_estimate()` is provided to make it easy to size decode buffers correctly.
1523

1624
## Migration
1725

@@ -32,8 +40,10 @@ The short-lived 0.20 functions were the 0.13 functions with `config` replaced wi
3240

3341
### Padding
3442

35-
Where possible, use `engine::STANDARD`, `engine::URL_SAFE`, or `engine::URL_SAFE_NO_PAD`. The first two requires that
36-
canonical padding is present when decoding, and the last requires that padding is absent.
43+
If applicable, use the preset engines `engine::STANDARD`, `engine::STANDARD_NO_PAD`, `engine::URL_SAFE`,
44+
or `engine::URL_SAFE_NO_PAD`.
45+
The `NO_PAD` ones require that padding is absent when decoding, and the others require that
46+
canonical padding is present .
3747

3848
If you need the < 0.20 behavior that did not care about padding, or want to recreate < 0.20.0's predefined `Config`s
3949
precisely, see the following table.

benches/benchmarks.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,7 @@ fn do_encode_bench_slice(b: &mut Bencher, &size: &usize) {
9999
let mut buf = Vec::new();
100100
// conservative estimate of encoded size
101101
buf.resize(v.len() * 2, 0);
102-
b.iter(|| {
103-
STANDARD.encode_slice(&v, &mut buf);
104-
});
102+
b.iter(|| STANDARD.encode_slice(&v, &mut buf).unwrap());
105103
}
106104

107105
fn do_encode_bench_stream(b: &mut Bencher, &size: &usize) {

src/chunked_encoder.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ impl<'e, E: Engine + ?Sized> ChunkedEncoder<'e, E> {
4141

4242
let chunk = &bytes[input_index..(input_index + input_chunk_len)];
4343

44-
let mut b64_bytes_written = self.engine.inner_encode(chunk, &mut encode_buf);
44+
let mut b64_bytes_written = self.engine.internal_encode(chunk, &mut encode_buf);
4545

4646
input_index += input_chunk_len;
4747
let more_input_left = input_index < bytes.len();

src/decode.rs

Lines changed: 93 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
use crate::engine::Engine;
2-
#[cfg(any(feature = "alloc", feature = "std", test))]
3-
use crate::engine::STANDARD;
1+
use crate::engine::{DecodeEstimate, Engine, STANDARD};
42
#[cfg(any(feature = "alloc", feature = "std", test))]
53
use alloc::vec::Vec;
64
use core::fmt;
@@ -44,17 +42,44 @@ impl fmt::Display for DecodeError {
4442

4543
#[cfg(any(feature = "std", test))]
4644
impl error::Error for DecodeError {
47-
fn description(&self) -> &str {
48-
match *self {
49-
Self::InvalidByte(_, _) => "invalid byte",
50-
Self::InvalidLength => "invalid length",
51-
Self::InvalidLastSymbol(_, _) => "invalid last symbol",
52-
Self::InvalidPadding => "invalid padding",
45+
fn cause(&self) -> Option<&dyn error::Error> {
46+
None
47+
}
48+
}
49+
50+
/// Errors that can occur while decoding into a slice.
51+
#[derive(Clone, Debug, PartialEq, Eq)]
52+
pub enum DecodeSliceError {
53+
/// A [DecodeError] occurred
54+
DecodeError(DecodeError),
55+
/// The provided slice _may_ be too small.
56+
///
57+
/// The check is conservative (assumes the last triplet of output bytes will all be needed).
58+
OutputSliceTooSmall,
59+
}
60+
61+
impl fmt::Display for DecodeSliceError {
62+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63+
match self {
64+
Self::DecodeError(e) => write!(f, "DecodeError: {}", e),
65+
Self::OutputSliceTooSmall => write!(f, "Output slice too small"),
5366
}
5467
}
68+
}
5569

70+
#[cfg(any(feature = "std", test))]
71+
impl error::Error for DecodeSliceError {
5672
fn cause(&self) -> Option<&dyn error::Error> {
57-
None
73+
match self {
74+
DecodeSliceError::DecodeError(e) => Some(e),
75+
DecodeSliceError::OutputSliceTooSmall => None,
76+
}
77+
}
78+
}
79+
80+
impl From<DecodeError> for DecodeSliceError {
81+
fn from(e: DecodeError) -> Self {
82+
DecodeSliceError::DecodeError(e)
5883
}
5984
}
6085

@@ -101,10 +126,39 @@ pub fn decode_engine_slice<E: Engine, T: AsRef<[u8]>>(
101126
input: T,
102127
output: &mut [u8],
103128
engine: &E,
104-
) -> Result<usize, DecodeError> {
129+
) -> Result<usize, DecodeSliceError> {
105130
engine.decode_slice(input, output)
106131
}
107132

133+
/// Returns a conservative estimate of the decoded size of `encoded_len` base64 symbols (rounded up
134+
/// to the next group of 3 decoded bytes).
135+
///
136+
/// The resulting length will be a safe choice for the size of a decode buffer, but may have up to
137+
/// 2 trailing bytes that won't end up being needed.
138+
///
139+
/// # Examples
140+
///
141+
/// ```
142+
/// use base64::decoded_len_estimate;
143+
///
144+
/// assert_eq!(3, decoded_len_estimate(1));
145+
/// assert_eq!(3, decoded_len_estimate(2));
146+
/// assert_eq!(3, decoded_len_estimate(3));
147+
/// assert_eq!(3, decoded_len_estimate(4));
148+
/// // start of the next quad of encoded symbols
149+
/// assert_eq!(6, decoded_len_estimate(5));
150+
/// ```
151+
///
152+
/// # Panics
153+
///
154+
/// Panics if decoded length estimation overflows.
155+
/// This would happen for sizes within a few bytes of the maximum value of `usize`.
156+
pub fn decoded_len_estimate(encoded_len: usize) -> usize {
157+
STANDARD
158+
.internal_decoded_len_estimate(encoded_len)
159+
.decoded_len_estimate()
160+
}
161+
108162
#[cfg(test)]
109163
mod tests {
110164
use super::*;
@@ -235,42 +289,6 @@ mod tests {
235289
}
236290
}
237291

238-
#[test]
239-
fn decode_into_slice_fits_in_precisely_sized_slice() {
240-
let mut orig_data = Vec::new();
241-
let mut encoded_data = String::new();
242-
let mut decode_buf = Vec::new();
243-
244-
let input_len_range = Uniform::new(0, 1000);
245-
let mut rng = rand::rngs::SmallRng::from_entropy();
246-
247-
for _ in 0..10_000 {
248-
orig_data.clear();
249-
encoded_data.clear();
250-
decode_buf.clear();
251-
252-
let input_len = input_len_range.sample(&mut rng);
253-
254-
for _ in 0..input_len {
255-
orig_data.push(rng.gen());
256-
}
257-
258-
let engine = random_engine(&mut rng);
259-
engine.encode_string(&orig_data, &mut encoded_data);
260-
assert_encode_sanity(&encoded_data, engine.config().encode_padding(), input_len);
261-
262-
decode_buf.resize(input_len, 0);
263-
264-
// decode into the non-empty buf
265-
let decode_bytes_written = engine
266-
.decode_slice(&encoded_data, &mut decode_buf[..])
267-
.unwrap();
268-
269-
assert_eq!(orig_data.len(), decode_bytes_written);
270-
assert_eq!(orig_data, decode_buf);
271-
}
272-
}
273-
274292
#[test]
275293
fn decode_engine_estimation_works_for_various_lengths() {
276294
let engine = GeneralPurpose::new(&alphabet::STANDARD, general_purpose::NO_PAD);
@@ -284,4 +302,32 @@ mod tests {
284302
}
285303
}
286304
}
305+
306+
#[test]
307+
fn decode_slice_output_length_errors() {
308+
for num_quads in 1..100 {
309+
let input = "AAAA".repeat(num_quads);
310+
let mut vec = vec![0; (num_quads - 1) * 3];
311+
assert_eq!(
312+
DecodeSliceError::OutputSliceTooSmall,
313+
STANDARD.decode_slice(&input, &mut vec).unwrap_err()
314+
);
315+
vec.push(0);
316+
assert_eq!(
317+
DecodeSliceError::OutputSliceTooSmall,
318+
STANDARD.decode_slice(&input, &mut vec).unwrap_err()
319+
);
320+
vec.push(0);
321+
assert_eq!(
322+
DecodeSliceError::OutputSliceTooSmall,
323+
STANDARD.decode_slice(&input, &mut vec).unwrap_err()
324+
);
325+
vec.push(0);
326+
// now it works
327+
assert_eq!(
328+
num_quads * 3,
329+
STANDARD.decode_slice(&input, &mut vec).unwrap()
330+
);
331+
}
332+
}
287333
}

src/encode.rs

Lines changed: 32 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
#[cfg(any(feature = "alloc", feature = "std", test))]
22
use alloc::string::String;
3+
use core::fmt;
4+
#[cfg(any(feature = "std", test))]
5+
use std::error;
36

47
#[cfg(any(feature = "alloc", feature = "std", test))]
58
use crate::engine::STANDARD;
@@ -49,9 +52,10 @@ pub fn encode_engine_slice<E: Engine, T: AsRef<[u8]>>(
4952
input: T,
5053
output_buf: &mut [u8],
5154
engine: &E,
52-
) -> usize {
55+
) -> Result<usize, EncodeSliceError> {
5356
engine.encode_slice(input, output_buf)
5457
}
58+
5559
/// B64-encode and pad (if configured).
5660
///
5761
/// This helper exists to avoid recalculating encoded_size, which is relatively expensive on short
@@ -70,7 +74,7 @@ pub(crate) fn encode_with_padding<E: Engine + ?Sized>(
7074
) {
7175
debug_assert_eq!(expected_encoded_size, output.len());
7276

73-
let b64_bytes_written = engine.inner_encode(input, output);
77+
let b64_bytes_written = engine.internal_encode(input, output);
7478

7579
let padding_bytes = if engine.config().encode_padding() {
7680
add_padding(input.len(), &mut output[b64_bytes_written..])
@@ -88,7 +92,8 @@ pub(crate) fn encode_with_padding<E: Engine + ?Sized>(
8892
/// Calculate the base64 encoded length for a given input length, optionally including any
8993
/// appropriate padding bytes.
9094
///
91-
/// Returns `None` if the encoded length can't be represented in `usize`.
95+
/// Returns `None` if the encoded length can't be represented in `usize`. This will happen for
96+
/// input lengths in approximately the top quarter of the range of `usize`.
9297
pub fn encoded_len(bytes_len: usize, padding: bool) -> Option<usize> {
9398
let rem = bytes_len % 3;
9499

@@ -128,6 +133,28 @@ pub(crate) fn add_padding(input_len: usize, output: &mut [u8]) -> usize {
128133
bytes_written
129134
}
130135

136+
/// Errors that can occur while encoding into a slice.
137+
#[derive(Clone, Debug, PartialEq, Eq)]
138+
pub enum EncodeSliceError {
139+
/// The provided slice is too small.
140+
OutputSliceTooSmall,
141+
}
142+
143+
impl fmt::Display for EncodeSliceError {
144+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145+
match self {
146+
Self::OutputSliceTooSmall => write!(f, "Output slice too small"),
147+
}
148+
}
149+
}
150+
151+
#[cfg(any(feature = "std", test))]
152+
impl error::Error for EncodeSliceError {
153+
fn cause(&self) -> Option<&dyn error::Error> {
154+
None
155+
}
156+
}
157+
131158
#[cfg(test)]
132159
mod tests {
133160
use super::*;
@@ -304,7 +331,7 @@ mod tests {
304331

305332
assert_eq!(
306333
encoded_size,
307-
engine.encode_slice(&orig_data, &mut encoded_data)
334+
engine.encode_slice(&orig_data, &mut encoded_data).unwrap()
308335
);
309336

310337
assert_encode_sanity(
@@ -325,51 +352,6 @@ mod tests {
325352
}
326353
}
327354

328-
#[test]
329-
fn encode_engine_slice_fits_into_precisely_sized_slice() {
330-
let mut orig_data = Vec::new();
331-
let mut encoded_data = Vec::new();
332-
let mut decoded = Vec::new();
333-
334-
let input_len_range = Uniform::new(0, 1000);
335-
336-
let mut rng = rand::rngs::SmallRng::from_entropy();
337-
338-
for _ in 0..10_000 {
339-
orig_data.clear();
340-
encoded_data.clear();
341-
decoded.clear();
342-
343-
let input_len = input_len_range.sample(&mut rng);
344-
345-
for _ in 0..input_len {
346-
orig_data.push(rng.gen());
347-
}
348-
349-
let engine = random_engine(&mut rng);
350-
351-
let encoded_size = encoded_len(input_len, engine.config().encode_padding()).unwrap();
352-
353-
encoded_data.resize(encoded_size, 0);
354-
355-
assert_eq!(
356-
encoded_size,
357-
engine.encode_slice(&orig_data, &mut encoded_data)
358-
);
359-
360-
assert_encode_sanity(
361-
str::from_utf8(&encoded_data[0..encoded_size]).unwrap(),
362-
engine.config().encode_padding(),
363-
input_len,
364-
);
365-
366-
engine
367-
.decode_vec(&encoded_data[0..encoded_size], &mut decoded)
368-
.unwrap();
369-
assert_eq!(orig_data, decoded);
370-
}
371-
}
372-
373355
#[test]
374356
fn encode_to_slice_random_valid_utf8() {
375357
let mut input = Vec::new();
@@ -400,7 +382,7 @@ mod tests {
400382

401383
let orig_output_buf = output.clone();
402384

403-
let bytes_written = engine.inner_encode(&input, &mut output);
385+
let bytes_written = engine.internal_encode(&input, &mut output);
404386

405387
// make sure the part beyond bytes_written is the same garbage it was before
406388
assert_eq!(orig_output_buf[bytes_written..], output[bytes_written..]);

0 commit comments

Comments
 (0)