base64url_nopad

base64url without padding library.
git clone https://git.philomathiclife.com/repos/base64url_nopad
Log | Files | Refs | README

commit d4d6190c1cf37ec44e0cb488326425ca1b669227
parent 64c4859714b249442affa4104c67a0da57a08300
Author: Zack Newman <zack@philomathiclife.com>
Date:   Sat,  6 Jun 2026 15:38:48 -0600

update deps, move unit tests, simplify encode_write

Diffstat:
MCargo.toml | 8+++++---
MREADME.md | 3++-
Msrc/lib.rs | 318++++++++++++++++---------------------------------------------------------------
Asrc/test.rs | 153+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 225 insertions(+), 257 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -9,8 +9,8 @@ license = "MIT OR Apache-2.0" name = "base64url_nopad" readme = "README.md" repository = "https://git.philomathiclife.com/repos/base64url_nopad/" -rust-version = "1.95.0" -version = "0.1.5" +rust-version = "1.96.0" +version = "0.2.0" [lints.rust] deprecated-safe = { level = "deny", priority = -1 } @@ -33,6 +33,7 @@ deref-into-dyn-supertrait = { level = "deny", priority = -1 } ffi-unwind-calls = { level = "deny", priority = -1 } #fuzzy-provenance-casts = { level = "deny", priority = -1 } impl-trait-redundant-captures = { level = "deny", priority = -1 } +linker-info = { level = "deny", priority = -1 } linker-messages = { level = "deny", priority = -1 } #lossy-provenance-casts = { level = "deny", priority = -1 } macro-use-extern-crate = { level = "deny", priority = -1 } @@ -84,6 +85,7 @@ missing_trait_methods = "allow" question_mark_used = "allow" ref_patterns = "allow" return_and_then = "allow" +single_call_fn = "allow" single_char_lifetime_names = "allow" unseparated_literal_suffix = "allow" @@ -107,7 +109,7 @@ targets = [ ] [dev-dependencies] -rand = { version = "0.10.0", default-features = false, features = ["sys_rng"] } +rand = { version = "0.10.1", default-features = false, features = ["sys_rng"] } ### FEATURES ################################################################# diff --git a/README.md b/README.md @@ -15,7 +15,7 @@ _including_ `panic`s related to memory allocations. use base64url_nopad::DecodeErr; /// Length of our input to encode. const INPUT_LEN: usize = 259; -/// The base64url encoded value without padding of our input. +/// The base64url-encoded value without padding of our input. const ENCODED_VAL: &str = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0-P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn-AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq-wsbKztLW2t7i5uru8vb6_wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t_g4eLj5OXm5-jp6uvs7e7v8PHy8_T19vf4-fr7_P3-_1MH0Q"; fn main() -> Result<(), DecodeErr> { let mut input = [0; INPUT_LEN]; @@ -30,6 +30,7 @@ fn main() -> Result<(), DecodeErr> { assert!(base64url_nopad::decode_len(output.len()).is_some_and(|len| len == INPUT_LEN)); base64url_nopad::decode_buffer(ENCODED_VAL.as_bytes(), &mut output[..INPUT_LEN])?; assert_eq!(input, output[..INPUT_LEN]); + Ok(()) } ``` diff --git a/src/lib.rs b/src/lib.rs @@ -68,6 +68,9 @@ #![cfg_attr(docsrs, feature(doc_cfg))] #[cfg(any(doc, feature = "alloc"))] extern crate alloc; +/// Unit tests. +#[cfg(test)] +mod test; #[cfg(any(doc, feature = "alloc"))] use alloc::{collections::TryReserveError, string::String, vec::Vec}; use core::{ @@ -783,6 +786,17 @@ pub const fn encode_len(input_length: usize) -> usize { // A precondition for calling this function is to ensure `encode_len_checked` can't return `None`. encode_len_checked(input_length).unwrap() } +/// `const`-version of `unreachable`. +macro_rules! impossible { + ( $( $x:literal)? ) => { + { + cold_path(); + $( + panic!($x); + )? + } + }; +} /// Encodes `input` into `output` re-interpreting the encoded subset of `output` as a `str` before returning it. /// /// `Some` is returned iff `output.len()` is large enough to write the encoded data into. @@ -815,11 +829,6 @@ pub const fn encode_len(input_length: usize) -> usize { /// ``` #[expect(unsafe_code, reason = "comments justify correctness")] #[expect( - clippy::missing_panics_doc, - clippy::panic, - reason = "false positive in that the panic is impossible modulo bugs" -)] -#[expect( clippy::missing_asserts_for_indexing, reason = "trust the compiler to already optimize since we match on the length" )] @@ -889,10 +898,7 @@ pub const fn encode_buffer_checked<'a>(input: &[u8], output: &'a mut [u8]) -> Op // `ASCII.len() == 64 > (snd & 15) << 2`, so indexing won't `panic`. output[output_idx + 2] = ASCII[((snd & 15) << 2) as usize]; } - _ => { - cold_path(); - panic!("there is a bug in core::slice::as_chunks"); - } + _ => impossible!("there is a bug in core::slice::as_chunks"), } // SAFETY: // We verified `output.len() >= final_len`. @@ -1030,8 +1036,7 @@ pub fn encode(input: &[u8]) -> String { } /// Writes the base64url encoding of `input` using `writer`. /// -/// Internally a buffer of at most 1024 bytes is used to write the encoded data. Smaller buffers may be used -/// for small inputs. +/// Internally a buffer of at most 1024 bytes is used to write the encoded data. /// /// # Errors /// @@ -1069,17 +1074,11 @@ pub fn encode(input: &[u8]) -> String { reason = "comments justify correctness" )] #[inline] -pub fn encode_write<W: Write>(mut input: &[u8], writer: &mut W) -> fmt::Result { - /// The minimum buffer size we use. - const MIN_BUFFER_LEN: usize = 256; - /// The medium buffer size we use. - const MID_BUFFER_LEN: usize = MIN_BUFFER_LEN << 1; +pub fn encode_write<W: Write>(mut input: &[u8], mut writer: W) -> fmt::Result { /// The max buffer size. /// /// This must be at least 4, no more than `i16::MAX`, and must be a power of 2. - const MAX_BUFFER_LEN: usize = MID_BUFFER_LEN << 1; - /// The minimum length of the input until we must chunk encode the data. - const LOOP_LEN: usize = MAX_BUFFER_LEN + 1; + const MAX_BUFFER_LEN: usize = 1024; /// Want to ensure at compilation time that `MAX_BUFFER_LEN` upholds its invariants. Namely /// that it's at least as large as 4, doesn't exceed [`i16::MAX`], and is always a power of 2. const _: () = { @@ -1095,55 +1094,36 @@ pub fn encode_write<W: Write>(mut input: &[u8], writer: &mut W) -> fmt::Result { // This will never `panic` since `MAX_BUFFER_LEN` is a power of two at least as large as 4 // (i.e., `MAX_BUFFER_LEN` ≢ 1 (mod 4)). const INPUT_LEN: usize = decode_len(MAX_BUFFER_LEN).unwrap(); + let mut buffer = [0; MAX_BUFFER_LEN]; // This won't `panic` since `input.len()` is guaranteed to be no more than `isize::MAX`. let len = encode_len(input.len()); - match len { - 0 => Ok(()), - 1..=MIN_BUFFER_LEN => { - let mut buffer = [0; MIN_BUFFER_LEN]; - // `buffer.len() == MIN_BUFFER_LEN >= len`, so indexing is fine. - // `encode_buffer` won't `panic` since `len` is the exact number of bytes needed to encode - // the data. - writer.write_str(encode_buffer(input, &mut buffer[..len])) - } - 257..=MID_BUFFER_LEN => { - let mut buffer = [0; MID_BUFFER_LEN]; - // `buffer.len() == MID_BUFFER_LEN >= len`, so indexing is fine. - // `encode_buffer` won't `panic` since `len` is the exact number of bytes needed to encode - // the data. - writer.write_str(encode_buffer(input, &mut buffer[..len])) - } - 513..=MAX_BUFFER_LEN => { - let mut buffer = [0; MAX_BUFFER_LEN]; - // `buffer.len() == MAX_BUFFER_LEN >= len`, so indexing is fine. - // `encode_buffer` won't `panic` since `len` is the exact number of bytes needed to encode - // the data. - writer.write_str(encode_buffer(input, &mut buffer[..len])) - } - LOOP_LEN.. => { - let mut buffer = [0; MAX_BUFFER_LEN]; - let mut counter = 0; - // `len / MAX_BUFFER_LEN` is equal to ⌊len / MAX_BUFFER_LEN⌋ since `MAX_BUFFER_LEN` is a power of two. - // We can safely encode `term` chunks of `INPUT_LEN` length into `buffer`. - let term = len >> MAX_BUFFER_LEN.trailing_zeros(); - let mut input_buffer; - while counter < term { - // SAFETY: - // `input.len() >= INPUT_LEN`. - input_buffer = unsafe { input.split_at_unchecked(INPUT_LEN) }; - // `encode_buffer` won't `panic` since `buffer` has length `MAX_BUFFER_LEN` which - // is the exact length needed for `INPUT_LEN` length inputs which `input_buffer.0` is. - writer.write_str(encode_buffer(input_buffer.0, buffer.as_mut_slice()))?; - input = input_buffer.1; - // `counter < term`, so overflow cannot happen. - counter += 1; - } - // `encode_len` won't `panic` since `input.len() < MAX_ENCODE_INPUT_LEN`. - // `input.len() < INPUT_LEN`; thus `encode_len(input.len()) < MAX_BUFFER_LEN = buffer.len()` so - // indexing is fine. - // `encode_buffer` won't `panic` since the buffer is the exact length needed to encode `input`. - writer.write_str(encode_buffer(input, &mut buffer[..encode_len(input.len())])) + if len <= MAX_BUFFER_LEN { + // `buffer.len() == MAX_BUFFER_LEN >= len`, so indexing is fine. + // `encode_buffer` won't `panic` since `len` is the exact number of bytes needed to encode + // the data. + writer.write_str(encode_buffer(input, &mut buffer[..len])) + } else { + let mut counter = 0; + // `len / MAX_BUFFER_LEN` is equal to ⌊len / MAX_BUFFER_LEN⌋ since `MAX_BUFFER_LEN` is a power of two. + // We can safely encode `term` chunks of `INPUT_LEN` length into `buffer`. + let term = len >> MAX_BUFFER_LEN.trailing_zeros(); + let mut input_buffer; + while counter < term { + // SAFETY: + // `input.len() >= INPUT_LEN`. + input_buffer = unsafe { input.split_at_unchecked(INPUT_LEN) }; + // `encode_buffer` won't `panic` since `buffer` has length `MAX_BUFFER_LEN` which + // is the exact length needed for `INPUT_LEN` length inputs which `input_buffer.0` is. + writer.write_str(encode_buffer(input_buffer.0, buffer.as_mut_slice()))?; + input = input_buffer.1; + // `counter < term`, so overflow cannot happen. + counter += 1; } + // `encode_len` won't `panic` since `input.len() < MAX_ENCODE_INPUT_LEN`. + // `input.len() < INPUT_LEN`; thus `encode_len(input.len()) < MAX_BUFFER_LEN = buffer.len()` so + // indexing is fine. + // `encode_buffer` won't `panic` since the buffer is the exact length needed to encode `input`. + writer.write_str(encode_buffer(input, &mut buffer[..encode_len(input.len())])) } } /// Appends the base64url encoding of `input` to `s` returning the `str` that was appended. @@ -1442,12 +1422,6 @@ impl Error for DecodeErr {} /// ``` #[expect(unsafe_code, reason = "comment justifies correctness")] #[expect( - clippy::missing_panics_doc, - clippy::panic, - clippy::panic_in_result_fn, - reason = "want to crash when there is a bug" -)] -#[expect( clippy::missing_asserts_for_indexing, reason = "trust the compiler to already optimize since we match on the length" )] @@ -1489,10 +1463,7 @@ pub const fn decode_buffer<'a>( } match rem.len() { 0 => {} - 1 => { - cold_path(); - panic!("there is a bug in base64url_nopad::decode_len"); - } + 1 => impossible!("there is a bug in base64url_nopad::decode_len"), 2 => { if let Some(base64_fst) = Alphabet::from_ascii(rem[0]) && let Some(base64_snd) = Alphabet::from_ascii(rem[1]) @@ -1527,10 +1498,7 @@ pub const fn decode_buffer<'a>( return Err(DecodeErr::InvalidByte); } } - _ => { - cold_path(); - panic!("there is a bug in core::slice::as_chunks"); - } + _ => impossible!("there is a bug in core::slice::as_chunks"), } // SAFETY: // `output.len() >= output_len`. @@ -1624,12 +1592,6 @@ pub fn decode(input: &[u8]) -> Result<Vec<u8>, DecodeErr> { /// # Ok::<_, DecodeErr>(()) /// ``` #[expect( - clippy::missing_panics_doc, - clippy::panic, - clippy::panic_in_result_fn, - reason = "want to crash when there is a bug" -)] -#[expect( clippy::missing_asserts_for_indexing, reason = "trust the compiler to already optimize since we match on the length" )] @@ -1686,10 +1648,7 @@ pub const fn validate_encoded_data(input: &[u8]) -> Result<(), DecodeErr> { Err(DecodeErr::InvalidByte) } } - _ => { - cold_path(); - panic!("there is a bug in core::slice::as_chunks"); - } + _ => impossible!("there is a bug in core::slice::as_chunks"), } } /// Same as [`encode_buffer`] except `output` must have the _exact_ length needed to encode `input`, and the @@ -1728,8 +1687,9 @@ pub const fn encode_buffer_exact(input: &[u8], output: &mut [u8]) { /// /// # Panics /// -/// `panic`s iff `output` does not have the _exact_ length needed to contain the decoded data. Note when `input` -/// contains an invalid length, [`DecodeErr::EncodedLen`] is returned _not_ a `panic`. +/// `panic`s iff `output` does not have the _exact_ length needed to contain the decoded data and `input` +/// has a valid length (i.e., [`DecodeErr::EncodedLen`] is returned _not_ a `panic` when `input` has invalid +/// length). /// /// # Examples /// @@ -1755,178 +1715,30 @@ pub const fn encode_buffer_exact(input: &[u8], output: &mut [u8]) { /// # Ok::<_, DecodeErr>(()) /// ``` #[expect( + clippy::panic, clippy::panic_in_result_fn, reason = "purpose of this function is to panic when output does not have the exact length needed" )] #[inline] pub const fn decode_buffer_exact(input: &[u8], output: &mut [u8]) -> Result<(), DecodeErr> { - if let Some(output_len) = decode_len(input.len()) { - assert!( - output.len() == output_len, - "decode_buffer_exact must be passed an output buffer whose length is exactly the length needed to decode the data" - ); - if let Err(e) = decode_buffer(input, output) { - Err(e) - } else { - Ok(()) - } - } else { - Err(DecodeErr::EncodedLen) - } -} -#[cfg(test)] -mod test { - #[cfg(any( - target_pointer_width = "16", - target_pointer_width = "32", - target_pointer_width = "64", - ))] - use super::MAX_ENCODE_INPUT_LEN; - #[cfg(feature = "alloc")] - use alloc::string::String; - #[cfg(any( - target_pointer_width = "16", - target_pointer_width = "32", - target_pointer_width = "64", - ))] - use rand::{RngExt as _, rngs::SmallRng}; - #[expect( - clippy::as_conversions, - clippy::cast_possible_truncation, - reason = "comment justifies correctness" - )] - #[cfg(any( - target_pointer_width = "16", - target_pointer_width = "32", - target_pointer_width = "64", - ))] - #[ignore = "slow"] - #[test] - fn encode_decode_len() { - assert_eq!(MAX_ENCODE_INPUT_LEN, 3 * (usize::MAX.div_ceil(4)) - 1); - let mut rng = rand::make_rng::<SmallRng>(); - for _ in 0u32..10_000_000 { - // `uN as usize` is fine since we `cfg` by pointer width. - #[cfg(target_pointer_width = "16")] - let len = rng.random::<u16>() as usize; - #[cfg(target_pointer_width = "32")] - let len = rng.random::<u32>() as usize; - #[cfg(target_pointer_width = "64")] - let len = rng.random::<u64>() as usize; - if len <= MAX_ENCODE_INPUT_LEN { - assert_eq!( - super::encode_len_checked(len).map(super::decode_len), - Some(Some(len)) - ); - } else { - assert!(super::encode_len_checked(len).is_none()); - } - } - for i in 0..1025 { - assert_eq!( - super::encode_len_checked(i).map(super::decode_len), - Some(Some(i)) + let output_len = output.len(); + match decode_buffer(input, output) { + Ok(v) => { + assert!( + v.len() == output_len, + "decode_buffer_exact must be passed an output buffer whose length is exactly the length needed to decode the data" ); + Ok(()) } - #[cfg(target_pointer_width = "16")] - for i in MAX_ENCODE_INPUT_LEN + 1.. { - assert!(super::encode_len_checked(i).is_none()); - } - #[cfg(not(target_pointer_width = "16"))] - for i in MAX_ENCODE_INPUT_LEN + 1..MAX_ENCODE_INPUT_LEN + 1_000_000 { - assert!(super::encode_len_checked(i).is_none()); - } - assert!(super::encode_len_checked(usize::MAX).is_none()); - assert_eq!( - super::encode_len_checked(MAX_ENCODE_INPUT_LEN), - Some(usize::MAX) - ); - for _ in 0u32..10_000_000 { - #[cfg(target_pointer_width = "16")] - let len = rng.random::<u16>() as usize; - #[cfg(target_pointer_width = "32")] - let len = rng.random::<u32>() as usize; - #[cfg(target_pointer_width = "64")] - let len = rng.random::<u64>() as usize; - if len & 3 == 1 { - assert!(super::decode_len(len).is_none()); - } else { - assert_eq!( - super::decode_len(len).map(super::encode_len_checked), - Some(Some(len)) - ); - } - } - for i in 0..1025 { - if i & 3 == 1 { - assert!(super::decode_len(i).is_none()); - } else { - assert_eq!( - super::decode_len(i).map(super::encode_len_checked), - Some(Some(i)) - ); - } - } - #[cfg(target_pointer_width = "16")] - for i in 0..=usize::MAX { - if i & 3 == 1 { - assert!(super::decode_len(i).is_none()); - } else { - assert_eq!( - super::decode_len(i).map(super::encode_len_checked), - Some(Some(i)) + Err(e) => { + if matches!(e, DecodeErr::BufferLen) { + cold_path(); + panic!( + "decode_buffer_exact must be passed an output buffer whose length is exactly the length needed to decode the data" ); - } - } - #[cfg(not(target_pointer_width = "16"))] - for i in usize::MAX - 1_000_000..=usize::MAX { - if i & 3 == 1 { - assert!(super::decode_len(i).is_none()); } else { - assert_eq!( - super::decode_len(i).map(super::encode_len_checked), - Some(Some(i)) - ); + Err(e) } } - assert_eq!(super::decode_len(usize::MAX), Some(MAX_ENCODE_INPUT_LEN)); - } - #[expect(clippy::indexing_slicing, reason = "comments justify correctness")] - #[cfg(feature = "alloc")] - #[test] - fn encode_write() { - let input = [9; 8192]; - let mut buffer = String::with_capacity(super::encode_len(input.len())); - let cap = buffer.capacity(); - let mut write_len; - for len in 0..input.len() { - write_len = super::encode_len(len); - match write_len.checked_add(buffer.len()) { - None => { - buffer.clear(); - // Indexing is fine since `len <= input.len()`. - assert_eq!(super::encode_write(&input[..len], &mut buffer), Ok(())); - assert_eq!(buffer.len(), write_len); - } - Some(l) => { - if l > cap { - buffer.clear(); - // Indexing is fine since `len <= input.len()`. - assert_eq!(super::encode_write(&input[..len], &mut buffer), Ok(())); - assert_eq!(buffer.len(), write_len); - } else { - // Indexing is fine since `len <= input.len()`. - assert_eq!(super::encode_write(&input[..len], &mut buffer), Ok(())); - assert_eq!(buffer.len(), l); - } - } - } - assert!( - buffer - .as_bytes() - .iter() - .all(|b| { matches!(*b, b'C' | b'J' | b'Q' | b'k') }) - ); - } } } diff --git a/src/test.rs b/src/test.rs @@ -0,0 +1,153 @@ +#[cfg(any( + target_pointer_width = "16", + target_pointer_width = "32", + target_pointer_width = "64", +))] +use super::MAX_ENCODE_INPUT_LEN; +#[cfg(feature = "alloc")] +use alloc::string::String; +#[cfg(any( + target_pointer_width = "16", + target_pointer_width = "32", + target_pointer_width = "64", +))] +use rand::{RngExt as _, rngs::SmallRng}; +#[expect( + clippy::as_conversions, + clippy::cast_possible_truncation, + reason = "comment justifies correctness" +)] +#[cfg(any( + target_pointer_width = "16", + target_pointer_width = "32", + target_pointer_width = "64", +))] +#[ignore = "slow"] +#[test] +fn encode_decode_len() { + assert_eq!(MAX_ENCODE_INPUT_LEN, 3 * (usize::MAX.div_ceil(4)) - 1); + let mut rng = rand::make_rng::<SmallRng>(); + for _ in 0u32..10_000_000 { + // `uN as usize` is fine since we `cfg` by pointer width. + #[cfg(target_pointer_width = "16")] + let len = rng.random::<u16>() as usize; + #[cfg(target_pointer_width = "32")] + let len = rng.random::<u32>() as usize; + #[cfg(target_pointer_width = "64")] + let len = rng.random::<u64>() as usize; + if len <= MAX_ENCODE_INPUT_LEN { + assert_eq!( + super::encode_len_checked(len).map(super::decode_len), + Some(Some(len)) + ); + } else { + assert!(super::encode_len_checked(len).is_none()); + } + } + for i in 0..1025 { + assert_eq!( + super::encode_len_checked(i).map(super::decode_len), + Some(Some(i)) + ); + } + #[cfg(target_pointer_width = "16")] + for i in MAX_ENCODE_INPUT_LEN + 1.. { + assert!(super::encode_len_checked(i).is_none()); + } + #[cfg(not(target_pointer_width = "16"))] + for i in MAX_ENCODE_INPUT_LEN + 1..MAX_ENCODE_INPUT_LEN + 1_000_000 { + assert!(super::encode_len_checked(i).is_none()); + } + assert!(super::encode_len_checked(usize::MAX).is_none()); + assert_eq!( + super::encode_len_checked(MAX_ENCODE_INPUT_LEN), + Some(usize::MAX) + ); + for _ in 0u32..10_000_000 { + #[cfg(target_pointer_width = "16")] + let len = rng.random::<u16>() as usize; + #[cfg(target_pointer_width = "32")] + let len = rng.random::<u32>() as usize; + #[cfg(target_pointer_width = "64")] + let len = rng.random::<u64>() as usize; + if len & 3 == 1 { + assert!(super::decode_len(len).is_none()); + } else { + assert_eq!( + super::decode_len(len).map(super::encode_len_checked), + Some(Some(len)) + ); + } + } + for i in 0..1025 { + if i & 3 == 1 { + assert!(super::decode_len(i).is_none()); + } else { + assert_eq!( + super::decode_len(i).map(super::encode_len_checked), + Some(Some(i)) + ); + } + } + #[cfg(target_pointer_width = "16")] + for i in 0..=usize::MAX { + if i & 3 == 1 { + assert!(super::decode_len(i).is_none()); + } else { + assert_eq!( + super::decode_len(i).map(super::encode_len_checked), + Some(Some(i)) + ); + } + } + #[cfg(not(target_pointer_width = "16"))] + for i in usize::MAX - 1_000_000..=usize::MAX { + if i & 3 == 1 { + assert!(super::decode_len(i).is_none()); + } else { + assert_eq!( + super::decode_len(i).map(super::encode_len_checked), + Some(Some(i)) + ); + } + } + assert_eq!(super::decode_len(usize::MAX), Some(MAX_ENCODE_INPUT_LEN)); +} +#[expect(clippy::indexing_slicing, reason = "comments justify correctness")] +#[cfg(feature = "alloc")] +#[test] +fn encode_write() { + let input = [9; 8192]; + let mut buffer = String::with_capacity(super::encode_len(input.len())); + let cap = buffer.capacity(); + let mut write_len; + for len in 0..input.len() { + write_len = super::encode_len(len); + match write_len.checked_add(buffer.len()) { + None => { + buffer.clear(); + // Indexing is fine since `len <= input.len()`. + assert_eq!(super::encode_write(&input[..len], &mut buffer), Ok(())); + assert_eq!(buffer.len(), write_len); + } + Some(l) => { + if l > cap { + buffer.clear(); + // Indexing is fine since `len <= input.len()`. + assert_eq!(super::encode_write(&input[..len], &mut buffer), Ok(())); + assert_eq!(buffer.len(), write_len); + } else { + // Indexing is fine since `len <= input.len()`. + assert_eq!(super::encode_write(&input[..len], &mut buffer), Ok(())); + assert_eq!(buffer.len(), l); + } + } + } + assert!( + buffer + .as_bytes() + .iter() + .all(|b| { matches!(*b, b'C' | b'J' | b'Q' | b'k') }) + ); + } +}