Skip to content

Commit afbb995

Browse files
committed
Make encoded length overflow recoverable
1 parent cbbd11c commit afbb995

5 files changed

Lines changed: 44 additions & 24 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
- Added `std::io::Read` streaming decoder behind the `stream` feature.
1414
- Added checked encoded-length helpers.
1515
- Added exact decoded-length helpers.
16+
- Changed public encoded-length helpers to return recoverable overflow errors
17+
instead of panicking.
1618
- Hardened decode errors to report absolute input indexes.
1719
- Hardened scalar encode to avoid input-derived alphabet table indexes.
1820
- Hardened alphabet decode to avoid branch-heavy match ladders.

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,14 @@ base64-ng = { version = "0.1", default-features = false }
6767
## Example
6868

6969
```rust
70-
use base64_ng::{STANDARD, encoded_len};
70+
use base64_ng::{STANDARD, checked_encoded_len};
7171

7272
let input = b"hello";
73-
let mut encoded = [0u8; encoded_len(5, true)];
73+
const ENCODED_CAPACITY: usize = match checked_encoded_len(5, true) {
74+
Some(len) => len,
75+
None => panic!("encoded length overflow"),
76+
};
77+
let mut encoded = [0u8; ENCODED_CAPACITY];
7478
let written = STANDARD.encode_slice(input, &mut encoded).unwrap();
7579
assert_eq!(&encoded[..written], b"aGVsbG8=");
7680

@@ -180,6 +184,8 @@ Security commitments:
180184
- No unsafe code in scalar code.
181185
- Future unsafe SIMD isolated under `src/simd/`.
182186
- Strict decoding rejects malformed padding and trailing data.
187+
- Public encoded-length overflow is recoverable through `Result` or `Option`;
188+
untrusted length metadata should never require a panic.
183189
- Scalar encode avoids input-derived alphabet table indexes, and scalar decode
184190
uses branch-minimized arithmetic. These paths are hardened against obvious
185191
timing pitfalls, but they are not documented as formally verified

SECURITY.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ arithmetic for ASCII classification. This reduces easy timing pitfalls, but
4242
`base64-ng` does not currently claim a formally verified cryptographic
4343
constant-time encode or decode API.
4444

45+
Public encoded-length helpers report overflow with `Result` or `Option` rather
46+
than panicking. Code that handles untrusted length metadata should use these
47+
helpers before allocating or accepting framed payloads.
48+
4549
Required before unsafe SIMD stabilizes:
4650

4751
- Scalar/SIMD differential tests.

src/lib.rs

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,14 @@
1616
//! Encode and decode with caller-owned buffers:
1717
//!
1818
//! ```
19-
//! use base64_ng::{STANDARD, encoded_len};
19+
//! use base64_ng::{STANDARD, checked_encoded_len};
2020
//!
2121
//! let input = b"hello";
22-
//! let mut encoded = [0u8; encoded_len(5, true)];
22+
//! const ENCODED_CAPACITY: usize = match checked_encoded_len(5, true) {
23+
//! Some(len) => len,
24+
//! None => panic!("encoded length overflow"),
25+
//! };
26+
//! let mut encoded = [0u8; ENCODED_CAPACITY];
2327
//! let encoded_len = STANDARD.encode_slice(input, &mut encoded).unwrap();
2428
//! assert_eq!(&encoded[..encoded_len], b"aGVsbG8=");
2529
//!
@@ -679,25 +683,22 @@ pub const URL_SAFE_NO_PAD: Engine<UrlSafe, false> = Engine::new();
679683

680684
/// Returns the encoded length for an input length and padding policy.
681685
///
682-
/// # Panics
683-
///
684-
/// Panics if the encoded length would overflow `usize`. Use
685-
/// [`checked_encoded_len`] when handling untrusted length metadata without an
686-
/// actual input slice.
686+
/// This function returns [`EncodeError::LengthOverflow`] instead of panicking.
687+
/// Use [`checked_encoded_len`] when an `Option<usize>` is more convenient.
687688
///
688689
/// # Examples
689690
///
690691
/// ```
691692
/// use base64_ng::encoded_len;
692693
///
693-
/// assert_eq!(encoded_len(5, true), 8);
694-
/// assert_eq!(encoded_len(5, false), 7);
694+
/// assert_eq!(encoded_len(5, true).unwrap(), 8);
695+
/// assert_eq!(encoded_len(5, false).unwrap(), 7);
696+
/// assert!(encoded_len(usize::MAX, true).is_err());
695697
/// ```
696-
#[must_use]
697-
pub const fn encoded_len(input_len: usize, padded: bool) -> usize {
698+
pub const fn encoded_len(input_len: usize, padded: bool) -> Result<usize, EncodeError> {
698699
match checked_encoded_len(input_len, padded) {
699-
Some(len) => len,
700-
None => panic!("encoded base64 length overflows usize"),
700+
Some(len) => Ok(len),
701+
None => Err(EncodeError::LengthOverflow),
701702
}
702703
}
703704

@@ -869,8 +870,7 @@ where
869870
}
870871

871872
/// Returns the encoded length for this engine's padding policy.
872-
#[must_use]
873-
pub const fn encoded_len(&self, input_len: usize) -> usize {
873+
pub const fn encoded_len(&self, input_len: usize) -> Result<usize, EncodeError> {
874874
encoded_len(input_len, PAD)
875875
}
876876

@@ -890,10 +890,10 @@ where
890890

891891
/// Encodes a fixed-size input into a fixed-size output array in const contexts.
892892
///
893-
/// Stable Rust does not yet allow this API to return `[u8; encoded_len(N)]`
894-
/// directly. Instead, the caller supplies the output length through the
895-
/// destination type and this function panics during const evaluation if the
896-
/// length is wrong.
893+
/// Stable Rust does not yet allow this API to return an array whose length
894+
/// is computed from `INPUT_LEN` directly. Instead, the caller supplies the
895+
/// output length through the destination type and this function panics
896+
/// during const evaluation if the length is wrong.
897897
///
898898
/// # Panics
899899
///

tests/rfc4648.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use base64_ng::{
22
DecodeError, EncodeError, STANDARD, STANDARD_NO_PAD, URL_SAFE, URL_SAFE_NO_PAD,
3-
checked_encoded_len, decoded_capacity, decoded_len,
3+
checked_encoded_len, decoded_capacity, decoded_len, encoded_len,
44
};
55

66
#[cfg(feature = "stream")]
@@ -143,6 +143,14 @@ fn decoded_len_rejects_bad_lengths_and_padding() {
143143

144144
#[test]
145145
fn checked_encoded_len_reports_overflow() {
146+
assert_eq!(
147+
encoded_len(usize::MAX, true),
148+
Err(EncodeError::LengthOverflow)
149+
);
150+
assert_eq!(
151+
STANDARD.encoded_len(usize::MAX),
152+
Err(EncodeError::LengthOverflow)
153+
);
146154
assert_eq!(checked_encoded_len(usize::MAX, true), None);
147155
assert_eq!(checked_encoded_len(usize::MAX, false), None);
148156
assert_eq!(STANDARD.checked_encoded_len(usize::MAX), None);
@@ -699,7 +707,7 @@ fn assert_in_place_encode_matches_slice<A, const PAD: bool>(
699707
) where
700708
A: base64_ng::Alphabet,
701709
{
702-
let required = engine.encoded_len(input.len());
710+
let required = engine.encoded_len(input.len()).unwrap();
703711
let mut expected = [0u8; 4];
704712
let expected_len = engine.encode_slice(input, &mut expected).unwrap();
705713
assert_eq!(required, expected_len);
@@ -718,7 +726,7 @@ fn assert_equivalent_round_trip<A, const PAD: bool>(
718726
) where
719727
A: base64_ng::Alphabet,
720728
{
721-
let encoded_len = engine.encoded_len(input.len());
729+
let encoded_len = engine.encoded_len(input.len()).unwrap();
722730
let mut encoded = vec![0u8; encoded_len];
723731
let written = engine.encode_slice(input, &mut encoded).unwrap();
724732
assert_eq!(written, encoded_len);

0 commit comments

Comments
 (0)