#![deny(unsafe_code)]
#![deny(missing_docs)]
#![deny(clippy::all)]
#![deny(clippy::pedantic)]
mod decoder_writer;
mod encoder_writer;
mod queue;
mod readers;
pub use decoder_writer::DecoderWriter;
pub use encoder_writer::EncoderWriter;
pub use readers::{DecoderReader, EncoderReader};
use base64_ng::{Alphabet, Engine};
use tokio::io::{self, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
const READ_ALL_EAGER_CAP: usize = 8192;
pub async fn encode_reader_to_writer<A, const PAD: bool, R, W>(
engine: &Engine<A, PAD>,
reader: &mut R,
writer: &mut W,
) -> io::Result<u64>
where
A: Alphabet,
R: AsyncRead + Unpin + ?Sized,
W: AsyncWrite + Unpin + ?Sized,
{
let input = read_to_end_guarded(reader).await?;
let output = WipingVec::from_vec(
engine
.encode_vec(input.as_slice())
.map_err(encode_io_error)?,
);
let written = output.len() as u64;
writer.write_all(output.as_slice()).await?;
Ok(written)
}
pub async fn encode_reader_to_writer_limited<A, const PAD: bool, R, W>(
engine: &Engine<A, PAD>,
reader: &mut R,
writer: &mut W,
max_input_len: usize,
) -> io::Result<u64>
where
A: Alphabet,
R: AsyncRead + Unpin + ?Sized,
W: AsyncWrite + Unpin + ?Sized,
{
let input = read_to_end_limited(reader, max_input_len).await?;
let output = WipingVec::from_vec(
engine
.encode_vec(input.as_slice())
.map_err(encode_io_error)?,
);
let written = output.len() as u64;
writer.write_all(output.as_slice()).await?;
Ok(written)
}
pub async fn decode_reader_to_writer<A, const PAD: bool, R, W>(
engine: &Engine<A, PAD>,
reader: &mut R,
writer: &mut W,
) -> io::Result<u64>
where
A: Alphabet,
R: AsyncRead + Unpin + ?Sized,
W: AsyncWrite + Unpin + ?Sized,
{
let input = read_to_end_guarded(reader).await?;
let output = WipingVec::from_vec(
engine
.decode_vec(input.as_slice())
.map_err(decode_io_error)?,
);
let written = output.len() as u64;
writer.write_all(output.as_slice()).await?;
Ok(written)
}
pub async fn decode_reader_to_writer_limited<A, const PAD: bool, R, W>(
engine: &Engine<A, PAD>,
reader: &mut R,
writer: &mut W,
max_input_len: usize,
) -> io::Result<u64>
where
A: Alphabet,
R: AsyncRead + Unpin + ?Sized,
W: AsyncWrite + Unpin + ?Sized,
{
let input = read_to_end_limited(reader, max_input_len).await?;
let output = WipingVec::from_vec(
engine
.decode_vec(input.as_slice())
.map_err(decode_io_error)?,
);
let written = output.len() as u64;
writer.write_all(output.as_slice()).await?;
Ok(written)
}
pub fn encode_to_vec<A, const PAD: bool>(
engine: &Engine<A, PAD>,
input: impl AsRef<[u8]>,
) -> io::Result<Vec<u8>>
where
A: Alphabet,
{
engine.encode_vec(input.as_ref()).map_err(encode_io_error)
}
pub fn decode_to_vec<A, const PAD: bool>(
engine: &Engine<A, PAD>,
input: impl AsRef<[u8]>,
) -> io::Result<Vec<u8>>
where
A: Alphabet,
{
engine.decode_vec(input.as_ref()).map_err(decode_io_error)
}
fn encode_io_error(error: base64_ng::EncodeError) -> io::Error {
io::Error::new(io::ErrorKind::InvalidInput, error)
}
fn decode_io_error(error: base64_ng::DecodeError) -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, error.kind().as_str())
}
fn wipe_bytes(bytes: &mut [u8]) {
base64_ng::secure_wipe(bytes);
}
struct WipingVec(Vec<u8>);
impl WipingVec {
fn new() -> Self {
Self(Vec::new())
}
fn with_capacity(capacity: usize) -> Self {
Self(Vec::with_capacity(capacity))
}
fn from_vec(bytes: Vec<u8>) -> Self {
Self(bytes)
}
fn as_slice(&self) -> &[u8] {
&self.0
}
fn len(&self) -> usize {
self.0.len()
}
fn extend_from_slice_wiping_old(
&mut self,
bytes: &[u8],
capacity_limit: usize,
) -> io::Result<()> {
let required = self.len().checked_add(bytes.len()).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
"base64-ng-tokio input is too large",
)
})?;
if required > capacity_limit {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"base64-ng-tokio input exceeds configured limit",
));
}
if required <= self.0.capacity() {
self.0.extend_from_slice(bytes);
return Ok(());
}
let grown_capacity = self.0.capacity().saturating_mul(2).max(required);
let replacement_capacity = grown_capacity.min(capacity_limit);
let mut replacement = Self::with_capacity(replacement_capacity);
replacement.0.extend_from_slice(&self.0);
replacement.0.extend_from_slice(bytes);
core::mem::swap(self, &mut replacement);
drop(replacement);
Ok(())
}
}
impl Drop for WipingVec {
fn drop(&mut self) {
self.0.resize(self.0.capacity(), 0);
wipe_bytes(&mut self.0);
self.0.clear();
}
}
struct WipingArray<const N: usize>([u8; N]);
impl<const N: usize> WipingArray<N> {
const fn new() -> Self {
Self([0; N])
}
}
impl<const N: usize> Drop for WipingArray<N> {
fn drop(&mut self) {
wipe_bytes(&mut self.0);
}
}
async fn read_to_end_guarded<R>(reader: &mut R) -> io::Result<WipingVec>
where
R: AsyncRead + Unpin + ?Sized,
{
let mut input = WipingVec::new();
let mut chunk = WipingArray::<8192>::new();
loop {
let read = reader.read(&mut chunk.0).await?;
if read == 0 {
return Ok(input);
}
input.extend_from_slice_wiping_old(&chunk.0[..read], usize::MAX)?;
wipe_bytes(&mut chunk.0[..read]);
}
}
async fn read_to_end_limited<R>(reader: &mut R, max_input_len: usize) -> io::Result<WipingVec>
where
R: AsyncRead + Unpin + ?Sized,
{
let mut input = WipingVec::with_capacity(max_input_len.min(READ_ALL_EAGER_CAP));
let mut chunk = WipingArray::<8192>::new();
loop {
let remaining = max_input_len - input.len();
let read_cap = if remaining < chunk.0.len() {
remaining + 1
} else {
chunk.0.len()
};
let read = reader.read(&mut chunk.0[..read_cap]).await?;
if read == 0 {
return Ok(input);
}
if read > remaining {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"base64-ng-tokio input exceeds configured limit",
));
}
input.extend_from_slice_wiping_old(&chunk.0[..read], max_input_len)?;
wipe_bytes(&mut chunk.0[..read]);
}
}