btt 0.5.0

Binary to text encodings
Documentation
use core::array;

use crate::{
	chunked::{ChunkedDecoder, ChunkedEncoder, ChunkedEncoderImpl},
	padding::Unpadded
};

type Base85EncoderImpl<A, D, P> = ChunkedEncoder<A, D, P, Base85Impl, 85, 4, 5>;
type Base85DecoderImpl<D, P> = ChunkedDecoder<D, P, Base85Impl, 85, 4, 5>;

impl_encoding!(Base85Encoder, Base85Decoder, 85, Base85EncoderImpl, Base85DecoderImpl);

impl_encoder!(
	Z85,
	85,
	b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-:+=^!/*?&<>()[]{}@%$#",
	Unpadded
);

impl_encoder!(
	Adobe,
	85,
	br##"!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstu"##,
	Unpadded
);

impl_encoder!(
	HtmlSafe,
	85,
	b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-:+=^!/*?()[]{}@%$#,;_",
	Unpadded
);

#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
struct Base85Impl;

impl ChunkedEncoderImpl<4, 5> for Base85Impl {
	#[inline]
	fn padding_len(len: usize) -> usize {
		debug_assert!(len <= 4);
		len + usize::from(len != 0)
	}

	#[inline]
	fn encoded_len(len: usize, padding: bool) -> Option<usize> {
		if padding {
			len.div_ceil(4).checked_mul(5)
		} else {
			let rem = len % 4;
			(len / 4).checked_mul(5)?.checked_add(rem + usize::from(rem != 0))
		}
	}

	#[inline]
	fn decoded_len(src: &[u8], padding: Option<u8>) -> Option<usize> {
		if src.is_empty() {
			return Some(0);
		}

		let (q, r) = if let Some(padding) = padding {
			if !src.len().is_multiple_of(5) {
				return None;
			}

			let padding = src.iter().rev().take(5).take_while(|&&b| b == padding).count();

			if padding > 3 {
				return None;
			}

			((src.len() / 5).saturating_sub(1), 5 - padding)
		} else {
			let r = src.len() % 5;

			if r == 1 {
				return None;
			}

			(src.len() / 5, r)
		};

		Some(q * 4 + (r - usize::from(r != 0)))
	}

	#[inline]
	fn encode_chunk_raw(src: &[u8; 4], dst: &mut [u8; 5]) {
		let mut src = u32::from_be_bytes(*src);

		for dst in dst.iter_mut().rev() {
			*dst = (src % 85) as u8;
			src /= 85;
		}

		debug_assert!(src == 0);
	}

	#[inline]
	fn decode_raw_chunk(chunk: &[u8; 5], dst: &mut [u8; 4]) -> Result<(), ()> {
		let acc = chunk.iter().fold(0, |acc, &c| acc * 85 + u64::from(c));
		*dst = u32::try_from(acc).map_err(|_| ())?.to_be_bytes();
		Ok(())
	}

	#[inline]
	fn decode_raw_last_chunk(src: &[u8], dst: &mut [u8; 4]) -> Result<(), ()> {
		Self::decode_raw_chunk(&array::from_fn(|i| src.get(i).copied().unwrap_or(84)), dst)
	}
}

#[cfg(test)]
mod tests {
	use super::*;

	#[track_caller]
	fn encode_test(encoder: &dyn DynEncoder, dst: &mut [u8], src: &[u8], expected: &str) {
		assert_eq!(encoder.encode_into_exact(src, dst).unwrap(), expected);
		assert!(
			encoder.decode_into_exact(expected.as_bytes(), &mut dst[..src.len()]).unwrap() == src
		);
	}

	#[test]
	fn z85() {
		encode_test(&Z85, &mut [], &[], "");
		encode_test(&Z85, &mut [0; 5], &[0xFF; 4], "%nSc0");
		encode_test(&Z85, &mut [0; 10], b"\x86\x4F\xD2\x6F\xB5\x59\xF7\x5B", "HelloWorld");
	}

	#[test]
	fn adobe() {
		encode_test(&Adobe, &mut [], &[], "");
		encode_test(&Adobe, &mut [0; 5], b"Man ", "9jqo^");
		encode_test(&Adobe, &mut [0; 5], b"sure", "F*2M7");
		encode_test(&Adobe, &mut [0; 2], b".", "/c");
	}

	#[test]
	fn canonical() {
		assert!(matches!(
			Z85.decode_into_exact(b"==", &mut [0; 1]),
			Err(DecodeIntoExactError::NonCanonical)
		));
	}
}