220 lines
7.6 KiB
Rust
220 lines
7.6 KiB
Rust
use crate::{
|
|
alphabet::Alphabet,
|
|
engine::{
|
|
general_purpose::{self, decode_table, encode_table},
|
|
Config, DecodeEstimate, DecodePaddingMode, Engine,
|
|
},
|
|
DecodeError, PAD_BYTE,
|
|
};
|
|
use alloc::ops::BitOr;
|
|
use std::ops::{BitAnd, Shl, Shr};
|
|
|
|
/// Comparatively simple implementation that can be used as something to compare against in tests
|
|
pub struct Naive {
|
|
encode_table: [u8; 64],
|
|
decode_table: [u8; 256],
|
|
config: NaiveConfig,
|
|
}
|
|
|
|
impl Naive {
|
|
const ENCODE_INPUT_CHUNK_SIZE: usize = 3;
|
|
const DECODE_INPUT_CHUNK_SIZE: usize = 4;
|
|
|
|
pub const fn new(alphabet: &Alphabet, config: NaiveConfig) -> Self {
|
|
Self {
|
|
encode_table: encode_table(alphabet),
|
|
decode_table: decode_table(alphabet),
|
|
config,
|
|
}
|
|
}
|
|
|
|
fn decode_byte_into_u32(&self, offset: usize, byte: u8) -> Result<u32, DecodeError> {
|
|
let decoded = self.decode_table[byte as usize];
|
|
|
|
if decoded == general_purpose::INVALID_VALUE {
|
|
return Err(DecodeError::InvalidByte(offset, byte));
|
|
}
|
|
|
|
Ok(decoded as u32)
|
|
}
|
|
}
|
|
|
|
impl Engine for Naive {
|
|
type Config = NaiveConfig;
|
|
type DecodeEstimate = NaiveEstimate;
|
|
|
|
fn internal_encode(&self, input: &[u8], output: &mut [u8]) -> usize {
|
|
// complete chunks first
|
|
|
|
const LOW_SIX_BITS: u32 = 0x3F;
|
|
|
|
let rem = input.len() % Self::ENCODE_INPUT_CHUNK_SIZE;
|
|
// will never underflow
|
|
let complete_chunk_len = input.len() - rem;
|
|
|
|
let mut input_index = 0_usize;
|
|
let mut output_index = 0_usize;
|
|
if let Some(last_complete_chunk_index) =
|
|
complete_chunk_len.checked_sub(Self::ENCODE_INPUT_CHUNK_SIZE)
|
|
{
|
|
while input_index <= last_complete_chunk_index {
|
|
let chunk = &input[input_index..input_index + Self::ENCODE_INPUT_CHUNK_SIZE];
|
|
|
|
// populate low 24 bits from 3 bytes
|
|
let chunk_int: u32 =
|
|
(chunk[0] as u32).shl(16) | (chunk[1] as u32).shl(8) | (chunk[2] as u32);
|
|
// encode 4x 6-bit output bytes
|
|
output[output_index] = self.encode_table[chunk_int.shr(18) as usize];
|
|
output[output_index + 1] =
|
|
self.encode_table[chunk_int.shr(12_u8).bitand(LOW_SIX_BITS) as usize];
|
|
output[output_index + 2] =
|
|
self.encode_table[chunk_int.shr(6_u8).bitand(LOW_SIX_BITS) as usize];
|
|
output[output_index + 3] =
|
|
self.encode_table[chunk_int.bitand(LOW_SIX_BITS) as usize];
|
|
|
|
input_index += Self::ENCODE_INPUT_CHUNK_SIZE;
|
|
output_index += 4;
|
|
}
|
|
}
|
|
|
|
// then leftovers
|
|
if rem == 2 {
|
|
let chunk = &input[input_index..input_index + 2];
|
|
|
|
// high six bits of chunk[0]
|
|
output[output_index] = self.encode_table[chunk[0].shr(2) as usize];
|
|
// bottom 2 bits of [0], high 4 bits of [1]
|
|
output[output_index + 1] =
|
|
self.encode_table[(chunk[0].shl(4_u8).bitor(chunk[1].shr(4_u8)) as u32)
|
|
.bitand(LOW_SIX_BITS) as usize];
|
|
// bottom 4 bits of [1], with the 2 bottom bits as zero
|
|
output[output_index + 2] =
|
|
self.encode_table[(chunk[1].shl(2_u8) as u32).bitand(LOW_SIX_BITS) as usize];
|
|
|
|
output_index += 3;
|
|
} else if rem == 1 {
|
|
let byte = input[input_index];
|
|
output[output_index] = self.encode_table[byte.shr(2) as usize];
|
|
output[output_index + 1] =
|
|
self.encode_table[(byte.shl(4_u8) as u32).bitand(LOW_SIX_BITS) as usize];
|
|
output_index += 2;
|
|
}
|
|
|
|
output_index
|
|
}
|
|
|
|
fn internal_decoded_len_estimate(&self, input_len: usize) -> Self::DecodeEstimate {
|
|
NaiveEstimate::new(input_len)
|
|
}
|
|
|
|
fn internal_decode(
|
|
&self,
|
|
input: &[u8],
|
|
output: &mut [u8],
|
|
estimate: Self::DecodeEstimate,
|
|
) -> Result<usize, DecodeError> {
|
|
if estimate.rem == 1 {
|
|
// trailing whitespace is so common that it's worth it to check the last byte to
|
|
// possibly return a better error message
|
|
if let Some(b) = input.last() {
|
|
if *b != PAD_BYTE
|
|
&& self.decode_table[*b as usize] == general_purpose::INVALID_VALUE
|
|
{
|
|
return Err(DecodeError::InvalidByte(input.len() - 1, *b));
|
|
}
|
|
}
|
|
|
|
return Err(DecodeError::InvalidLength);
|
|
}
|
|
|
|
let mut input_index = 0_usize;
|
|
let mut output_index = 0_usize;
|
|
const BOTTOM_BYTE: u32 = 0xFF;
|
|
|
|
// can only use the main loop on non-trailing chunks
|
|
if input.len() > Self::DECODE_INPUT_CHUNK_SIZE {
|
|
// skip the last chunk, whether it's partial or full, since it might
|
|
// have padding, and start at the beginning of the chunk before that
|
|
let last_complete_chunk_start_index = estimate.complete_chunk_len
|
|
- if estimate.rem == 0 {
|
|
// Trailing chunk is also full chunk, so there must be at least 2 chunks, and
|
|
// this won't underflow
|
|
Self::DECODE_INPUT_CHUNK_SIZE * 2
|
|
} else {
|
|
// Trailing chunk is partial, so it's already excluded in
|
|
// complete_chunk_len
|
|
Self::DECODE_INPUT_CHUNK_SIZE
|
|
};
|
|
|
|
while input_index <= last_complete_chunk_start_index {
|
|
let chunk = &input[input_index..input_index + Self::DECODE_INPUT_CHUNK_SIZE];
|
|
let decoded_int: u32 = self.decode_byte_into_u32(input_index, chunk[0])?.shl(18)
|
|
| self
|
|
.decode_byte_into_u32(input_index + 1, chunk[1])?
|
|
.shl(12)
|
|
| self.decode_byte_into_u32(input_index + 2, chunk[2])?.shl(6)
|
|
| self.decode_byte_into_u32(input_index + 3, chunk[3])?;
|
|
|
|
output[output_index] = decoded_int.shr(16_u8).bitand(BOTTOM_BYTE) as u8;
|
|
output[output_index + 1] = decoded_int.shr(8_u8).bitand(BOTTOM_BYTE) as u8;
|
|
output[output_index + 2] = decoded_int.bitand(BOTTOM_BYTE) as u8;
|
|
|
|
input_index += Self::DECODE_INPUT_CHUNK_SIZE;
|
|
output_index += 3;
|
|
}
|
|
}
|
|
|
|
general_purpose::decode_suffix::decode_suffix(
|
|
input,
|
|
input_index,
|
|
output,
|
|
output_index,
|
|
&self.decode_table,
|
|
self.config.decode_allow_trailing_bits,
|
|
self.config.decode_padding_mode,
|
|
)
|
|
}
|
|
|
|
fn config(&self) -> &Self::Config {
|
|
&self.config
|
|
}
|
|
}
|
|
|
|
pub struct NaiveEstimate {
|
|
/// remainder from dividing input by `Naive::DECODE_CHUNK_SIZE`
|
|
rem: usize,
|
|
/// Length of input that is in complete `Naive::DECODE_CHUNK_SIZE`-length chunks
|
|
complete_chunk_len: usize,
|
|
}
|
|
|
|
impl NaiveEstimate {
|
|
fn new(input_len: usize) -> Self {
|
|
let rem = input_len % Naive::DECODE_INPUT_CHUNK_SIZE;
|
|
let complete_chunk_len = input_len - rem;
|
|
|
|
Self {
|
|
rem,
|
|
complete_chunk_len,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl DecodeEstimate for NaiveEstimate {
|
|
fn decoded_len_estimate(&self) -> usize {
|
|
((self.complete_chunk_len / 4) + ((self.rem > 0) as usize)) * 3
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub struct NaiveConfig {
|
|
pub encode_padding: bool,
|
|
pub decode_allow_trailing_bits: bool,
|
|
pub decode_padding_mode: DecodePaddingMode,
|
|
}
|
|
|
|
impl Config for NaiveConfig {
|
|
fn encode_padding(&self) -> bool {
|
|
self.encode_padding
|
|
}
|
|
}
|