Initial Commit

This commit is contained in:
Nathan Fisher 2025-01-07 14:13:15 -05:00
commit 4b165a6736
9 changed files with 412 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

7
Cargo.lock generated Normal file
View file

@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "b32_rs"
version = "0.1.0"

6
Cargo.toml Normal file
View file

@ -0,0 +1,6 @@
[package]
name = "b32_rs"
version = "0.1.0"
edition = "2021"
[dependencies]

91
src/decode.rs Normal file
View file

@ -0,0 +1,91 @@
use std::io::{self, Read, Write};
use super::*;
#[derive(Debug)]
pub enum DecoderError {
IO(io::Error),
IllegalChar,
}
impl From<io::Error> for DecoderError {
fn from(value: io::Error) -> Self {
Self::IO(value)
}
}
pub struct Decoder<R: Read, W: Write> {
reader: R,
writer: W,
alphabet: B32Alphabet,
}
impl<R: Read, W: Write> Decoder<R, W> {
pub fn new(reader: R, writer: W, alphabet: Option<B32Alphabet>) -> Self {
Self { reader, writer, alphabet: alphabet.unwrap_or_default() }
}
pub fn decode(&mut self) -> Result<(), DecoderError> {
let mut buf = [0; 8];
let mut num: u64 = 0;
'outer: loop {
if let Err(e) = self.reader.read_exact(&mut buf) {
if e.kind() == io::ErrorKind::UnexpectedEof {
break;
}
}
for c in &buf {
let c = char::from(*c);
num <<= 5;
if !matches!(self.alphabet.pad(), Some(ch) if ch == c) {
let idx = self.alphabet.idx(c).ok_or(DecoderError::IllegalChar)?;
num |= idx as u64;
}
}
let mut buf = [0; 5];
for i in (0..5).rev() {
let b = (num & 0xff) as u8;
buf[i] = b;
num >>= 8;
}
for c in &buf {
if *c == b'\0' {
break 'outer;
} else {
self.writer.write_all(&[*c])?;
}
}
}
self.writer.flush()?;
Ok(())
}
}
impl<R: Read> Decoder<R, Vec<u8>> {
pub fn bytes(self) -> Vec<u8> {
self.writer
}
}
#[cfg(test)]
mod tests {
use super::*;
static HELLO: &'static str = "Hello, World!";
static ENCODED: &'static str = "JBSWY3DPFQQFO33SNRSCC===";
#[test]
fn get_idx() {
let idx = B32_RFC4648_ALPHABET.idx('S').unwrap();
assert_eq!(idx, 18);
}
#[test]
fn hello() {
let reader = ENCODED.as_bytes();
let writer = Vec::<u8>::new();
let mut decoder = Decoder::new(reader, writer, None);
decoder.decode().unwrap();
assert_eq!(HELLO, String::from_utf8(decoder.bytes()).unwrap());
}
}

153
src/encode.rs Normal file
View file

@ -0,0 +1,153 @@
use {
super::*,
std::{
fmt::{self, Write},
io::{self, Read},
},
};
#[derive(Debug)]
pub enum EncoderError {
IO(io::Error),
FMT(fmt::Error),
}
impl From<io::Error> for EncoderError {
fn from(value: io::Error) -> Self {
Self::IO(value)
}
}
impl From<fmt::Error> for EncoderError {
fn from(value: fmt::Error) -> Self {
Self::FMT(value)
}
}
pub struct Encoder<R: Read, W: Write> {
reader: R,
writer: W,
alphabet: B32Alphabet,
wrap: Option<usize>,
}
impl<R: Read, W: Write> Encoder<R, W> {
pub fn new(reader: R, writer: W, alphabet: Option<B32Alphabet>, wrap: Option<usize>) -> Self {
Self {
reader,
writer,
alphabet: alphabet.unwrap_or_default(),
wrap,
}
}
pub fn encode(&mut self) -> Result<(), EncoderError> {
let mut total: usize = 0;
loop {
let mut buf = [0; 5];
let mut num: u64 = 0;
let n_bytes;
loop {
n_bytes = match self.reader.read(&mut buf) {
Ok(n) => n,
Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e.into()),
};
break;
}
if n_bytes == 0 {
break;
}
for (idx, c) in buf.iter().enumerate() {
num <<= 8;
if idx < n_bytes {
num |= *c as u64;
}
}
let mut buf = ['\0'; 8];
for i in (0..8).rev() {
let b = (num & 0x1f) as u8;
buf[i] = char::try_from(self.alphabet.items[b as usize]).unwrap();
num >>= 5;
}
let mut outlen = n_bytes * 8 / 5;
let rem = n_bytes * 8 % 5;
if rem != 0 {
outlen += 1;
}
buf[0..outlen].iter().try_for_each(|c| {
self.writer.write_char(*c)?;
total += 1;
if let Some(wrap) = self.wrap {
if total % wrap == 0 {
self.writer.write_char('\n')?;
}
}
Ok::<(), fmt::Error>(())
})?;
if let Some(pad) = self.alphabet.pad {
if rem != 0 {
for _c in 0..8 - outlen {
self.writer.write_char(char::try_from(pad).unwrap())?;
}
}
}
if n_bytes < 5 {
break;
}
}
Ok(())
}
}
impl<R: Read> Encoder<R, String> {
pub fn output(self) -> String {
self.writer
}
}
#[cfg(test)]
mod test {
use {
super::*,
std::{
fs::{self, File},
io::BufReader,
},
};
#[test]
fn hello() {
let reader = "Hello, World".as_bytes();
let writer = String::new();
let mut encoder = Encoder::new(reader, writer, None, None);
encoder.encode().unwrap();
assert_eq!(encoder.output(), "JBSWY3DPFQQFO33SNRSA====");
encoder = Encoder::new(
"Hello, World!".as_bytes(),
String::new(),
None,
None
);
encoder.encode().unwrap();
assert_eq!(encoder.output(), "JBSWY3DPFQQFO33SNRSCC===");
encoder = Encoder::new(
"Hello, World!\n".as_bytes(),
String::new(),
None,
None,
);
encoder.encode().unwrap();
assert_eq!(encoder.output(), "JBSWY3DPFQQFO33SNRSCCCQ=");
}
#[test]
fn encode_from_file() {
let infile = File::open("src/testdata/lorem.txt").unwrap();
let reader = BufReader::new(infile);
let outfile = fs::read_to_string("src/testdata/lorem_b32.txt").unwrap();
let mut encoder = Encoder::new(reader, String::new(), None, Some(76));
encoder.encode().unwrap();
assert_eq!(encoder.output(), outfile);
}
}

57
src/error.rs Normal file
View file

@ -0,0 +1,57 @@
use std::{error, fmt, io, num::TryFromIntError};
#[derive(Debug)]
pub enum Error {
Fmt(fmt::Error),
Io(io::Error),
IllegalChar(char),
IntError(TryFromIntError),
MissingPadding,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Fmt(e) => write!(f, "{e}"),
Self::Io(e) => write!(f, "{e}"),
Self::IllegalChar(c) => write!(f, "Illegal Character ({c})"),
Self::MissingPadding => write!(f, "Missing Padding"),
Self::IntError(e) => write!(f, "Int Error: {e}"),
}
}
}
impl From<fmt::Error> for Error {
fn from(value: fmt::Error) -> Self {
Self::Fmt(value)
}
}
impl From<io::Error> for Error {
fn from(value: io::Error) -> Self {
Self::Io(value)
}
}
impl From<char> for Error {
fn from(value: char) -> Self {
Self::IllegalChar(value)
}
}
impl From<TryFromIntError> for Error {
fn from(value: TryFromIntError) -> Self {
Self::IntError(value)
}
}
impl error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Fmt(e) => Some(e),
Self::Io(e) => Some(e),
Self::IntError(e) => Some(e),
_ => None,
}
}
}

39
src/lib.rs Normal file
View file

@ -0,0 +1,39 @@
#[warn(clippy::all, clippy::pedantic)]
pub mod decode;
pub mod encode;
pub mod error;
#[derive(Clone, Copy)]
pub struct B32Alphabet {
items: [char; 32],
pad: Option<char>,
}
pub static B32_RFC4648_ALPHABET: B32Alphabet = B32Alphabet {
items: [
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R',
'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '2', '3', '4', '5', '6', '7',
],
pad: Some('='),
};
impl Default for B32Alphabet {
fn default() -> Self {
B32_RFC4648_ALPHABET
}
}
impl B32Alphabet {
pub(crate) fn idx(&self, c: char) -> Option<usize> {
for (idx, x) in self.items.iter().enumerate() {
if *x == c {
return Some(idx);
}
}
None
}
pub(crate) fn pad(&self) -> Option<char> {
self.pad
}
}

9
src/testdata/lorem.txt vendored Normal file
View file

@ -0,0 +1,9 @@
Lorem ipsum odor amet, consectetuer adipiscing elit. Hendrerit eu semper lectus amet est proin. Tempus himenaeos at fusce lacus habitasse sociosqu torquent varius arcu. Odio netus fermentum nisl natoque per cubilia himenaeos urna. Elit venenatis eros risus vulputate duis interdum. Mauris dis ad pharetra dui aptent. Imperdiet orci auctor condimentum adipiscing arcu. Nullam tempor magna odio volutpat nisi. Condimentum scelerisque risus faucibus; duis sollicitudin nullam.
Condimentum ex fringilla tempus ultricies consectetur odio egestas venenatis vulputate. Sem sapien pellentesque ornare; viverra scelerisque consequat. Platea posuere mi senectus felis dolor dictumst mattis ultricies est. Malesuada elit adipiscing suspendisse est a. Justo luctus turpis tempus leo finibus rutrum nostra magnis aliquam. Felis posuere molestie praesent porta justo nisi. Etiam aptent conubia consectetur dis; penatibus elementum ut.
Morbi nostra netus ad nunc egestas neque nascetur fusce. Consequat ultrices scelerisque ut rhoncus volutpat, hendrerit convallis massa. Tortor himenaeos nullam malesuada suspendisse consectetur potenti volutpat. In suscipit sit maximus; lacinia eu nulla porta. Tempus commodo libero auctor cras bibendum sit. Metus aptent mauris nascetur venenatis diam metus. Inceptos mi ultrices class morbi donec. Orci pulvinar risus accumsan faucibus litora duis commodo commodo.
Aliquam etiam habitasse fames massa, hendrerit purus interdum nullam. Curabitur placerat tortor placerat nec adipiscing habitasse. Massa litora interdum pretium fusce nascetur primis faucibus magna. Dui curabitur molestie justo ullamcorper leo molestie mauris. Inceptos malesuada taciti litora libero maecenas neque nam laoreet. Tristique vivamus eget maecenas; porta fames curae mollis cras. Ipsum vulputate ullamcorper et nascetur nascetur vel orci.
Curae et odio; urna eros pulvinar malesuada eget dignissim. Cursus fermentum lectus phasellus tempor posuere vulputate quam. Mauris commodo commodo mauris inceptos metus varius urna sagittis. Ligula duis erat ipsum pretium eu facilisis. Duis lobortis proin facilisi ad suscipit vestibulum hac tortor interdum. Ultrices tincidunt maximus aptent phasellus in ullamcorper nisl varius dis. Venenatis enim potenti potenti sodales massa id elementum.

49
src/testdata/lorem_b32.txt vendored Normal file
View file

@ -0,0 +1,49 @@
JRXXEZLNEBUXA43VNUQG6ZDPOIQGC3LFOQWCAY3PNZZWKY3UMV2HKZLSEBQWI2LQNFZWG2LOM4QG
K3DJOQXCASDFNZSHEZLSNF2CAZLVEBZWK3LQMVZCA3DFMN2HK4ZAMFWWK5BAMVZXIIDQOJXWS3RO
EBKGK3LQOVZSA2DJNVSW4YLFN5ZSAYLUEBTHK43DMUQGYYLDOVZSA2DBMJUXIYLTONSSA43PMNUW
643ROUQHI33SOF2WK3TUEB3GC4TJOVZSAYLSMN2S4ICPMRUW6IDOMV2HK4ZAMZSXE3LFNZ2HK3JA
NZUXG3BANZQXI33ROVSSA4DFOIQGG5LCNFWGSYJANBUW2ZLOMFSW64ZAOVZG4YJOEBCWY2LUEB3G
K3TFNZQXI2LTEBSXE33TEBZGS43VOMQHM5LMOB2XIYLUMUQGI5LJOMQGS3TUMVZGI5LNFYQE2YLV
OJUXGIDENFZSAYLEEBYGQYLSMV2HEYJAMR2WSIDBOB2GK3TUFYQES3LQMVZGI2LFOQQG64TDNEQG
C5LDORXXEIDDN5XGI2LNMVXHI5LNEBQWI2LQNFZWG2LOM4QGC4TDOUXCATTVNRWGC3JAORSW24DP
OIQG2YLHNZQSA33ENFXSA5TPNR2XI4DBOQQG42LTNEXCAQ3PNZSGS3LFNZ2HK3JAONRWK3DFOJUX
G4LVMUQHE2LTOVZSAZTBOVRWSYTVOM5SAZDVNFZSA43PNRWGSY3JOR2WI2LOEBXHK3DMMFWS4CQK
INXW4ZDJNVSW45DVNUQGK6BAMZZGS3THNFWGYYJAORSW24DVOMQHK3DUOJUWG2LFOMQGG33OONSW
G5DFOR2XEIDPMRUW6IDFM5SXG5DBOMQHMZLOMVXGC5DJOMQHM5LMOB2XIYLUMUXCAU3FNUQHGYLQ
NFSW4IDQMVWGYZLOORSXG4LVMUQG64TOMFZGKOZAOZUXMZLSOJQSA43DMVWGK4TJONYXKZJAMNXW
443FOF2WC5BOEBIGYYLUMVQSA4DPON2WK4TFEBWWSIDTMVXGKY3UOVZSAZTFNRUXGIDEN5WG64RA
MRUWG5DVNVZXIIDNMF2HI2LTEB2WY5DSNFRWSZLTEBSXG5BOEBGWC3DFON2WCZDBEBSWY2LUEBQW
I2LQNFZWG2LOM4QHG5LTOBSW4ZDJONZWKIDFON2CAYJOEBFHK43UN4QGY5LDOR2XGIDUOVZHA2LT
EB2GK3LQOVZSA3DFN4QGM2LONFRHK4ZAOJ2XI4TVNUQG433TORZGCIDNMFTW42LTEBQWY2LROVQW
2LRAIZSWY2LTEBYG643VMVZGKIDNN5WGK43UNFSSA4DSMFSXGZLOOQQHA33SORQSA2TVON2G6IDO
NFZWSLRAIV2GSYLNEBQXA5DFNZ2CAY3PNZ2WE2LBEBRW63TTMVRXIZLUOVZCAZDJOM5SA4DFNZQX
I2LCOVZSAZLMMVWWK3TUOVWSA5LUFYFAUTLPOJRGSIDON5ZXI4TBEBXGK5DVOMQGCZBANZ2W4YZA
MVTWK43UMFZSA3TFOF2WKIDOMFZWGZLUOVZCAZTVONRWKLRAINXW443FOF2WC5BAOVWHI4TJMNSX
GIDTMNSWYZLSNFZXC5LFEB2XIIDSNBXW4Y3VOMQHM33MOV2HAYLUFQQGQZLOMRZGK4TJOQQGG33O
OZQWY3DJOMQG2YLTONQS4ICUN5ZHI33SEBUGS3LFNZQWK33TEBXHK3DMMFWSA3LBNRSXG5LBMRQS
A43VONYGK3TENFZXGZJAMNXW443FMN2GK5DVOIQHA33UMVXHI2JAOZXWY5LUOBQXILRAJFXCA43V
ONRWS4DJOQQHG2LUEBWWC6DJNV2XGOZANRQWG2LONFQSAZLVEBXHK3DMMEQHA33SORQS4ICUMVWX
A5LTEBRW63LNN5SG6IDMNFRGK4TPEBQXKY3UN5ZCAY3SMFZSAYTJMJSW4ZDVNUQHG2LUFYQE2ZLU
OVZSAYLQORSW45BANVQXK4TJOMQG4YLTMNSXI5LSEB3GK3TFNZQXI2LTEBSGSYLNEBWWK5DVOMXC
ASLOMNSXA5DPOMQG22JAOVWHI4TJMNSXGIDDNRQXG4ZANVXXEYTJEBSG63TFMMXCAT3SMNUSA4DV
NR3GS3TBOIQHE2LTOVZSAYLDMN2W243BNYQGMYLVMNUWE5LTEBWGS5DPOJQSAZDVNFZSAY3PNVWW
6ZDPEBRW63LNN5SG6LQKBJAWY2LROVQW2IDFORUWC3JANBQWE2LUMFZXGZJAMZQW2ZLTEBWWC43T
MEWCA2DFNZSHEZLSNF2CA4DVOJ2XGIDJNZ2GK4TEOVWSA3TVNRWGC3JOEBBXK4TBMJUXI5LSEBYG
YYLDMVZGC5BAORXXE5DPOIQHA3DBMNSXEYLUEBXGKYZAMFSGS4DJONRWS3THEBUGCYTJORQXG43F
FYQE2YLTONQSA3DJORXXEYJANFXHIZLSMR2W2IDQOJSXI2LVNUQGM5LTMNSSA3TBONRWK5DVOIQH
A4TJNVUXGIDGMF2WG2LCOVZSA3LBM5XGCLRAIR2WSIDDOVZGCYTJOR2XEIDNN5WGK43UNFSSA2TV
ON2G6IDVNRWGC3LDN5ZHAZLSEBWGK3ZANVXWYZLTORUWKIDNMF2XE2LTFYQES3TDMVYHI33TEBWW
C3DFON2WCZDBEB2GCY3JORUSA3DJORXXEYJANRUWEZLSN4QG2YLFMNSW4YLTEBXGK4LVMUQG4YLN
EBWGC33SMVSXILRAKRZGS43UNFYXKZJAOZUXMYLNOVZSAZLHMV2CA3LBMVRWK3TBOM5SA4DPOJ2G
CIDGMFWWK4ZAMN2XEYLFEBWW63DMNFZSAY3SMFZS4ICJOBZXK3JAOZ2WY4DVORQXIZJAOVWGYYLN
MNXXE4DFOIQGK5BANZQXGY3FOR2XEIDOMFZWGZLUOVZCA5TFNQQG64TDNEXAUCSDOVZGCZJAMV2C
A33ENFXTWIDVOJXGCIDFOJXXGIDQOVWHM2LOMFZCA3LBNRSXG5LBMRQSAZLHMV2CAZDJM5XGS43T
NFWS4ICDOVZHG5LTEBTGK4TNMVXHI5LNEBWGKY3UOVZSA4DIMFZWK3DMOVZSA5DFNVYG64RAOBXX
G5LFOJSSA5TVNRYHK5DBORSSA4LVMFWS4ICNMF2XE2LTEBRW63LNN5SG6IDDN5WW233EN4QG2YLV
OJUXGIDJNZRWK4DUN5ZSA3LFOR2XGIDWMFZGS5LTEB2XE3TBEBZWCZ3JOR2GS4ZOEBGGSZ3VNRQS
AZDVNFZSAZLSMF2CA2LQON2W2IDQOJSXI2LVNUQGK5JAMZQWG2LMNFZWS4ZOEBCHK2LTEBWG6YTP
OJ2GS4ZAOBZG62LOEBTGCY3JNRUXG2JAMFSCA43VONRWS4DJOQQHMZLTORUWE5LMOVWSA2DBMMQH
I33SORXXEIDJNZ2GK4TEOVWS4ICVNR2HE2LDMVZSA5DJNZRWSZDVNZ2CA3LBPBUW25LTEBQXA5DF
NZ2CA4DIMFZWK3DMOVZSA2LOEB2WY3DBNVRW64TQMVZCA3TJONWCA5TBOJUXK4ZAMRUXGLRAKZSW
4ZLOMF2GS4ZAMVXGS3JAOBXXIZLOORUSA4DPORSW45DJEBZW6ZDBNRSXGIDNMFZXGYJANFSCAZLM
MVWWK3TUOVWS4CQ=