Initial commit

This commit is contained in:
Nathan Fisher 2024-12-19 23:54:16 -05:00
commit 66d5702da0
7 changed files with 273 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 = 4
[[package]]
name = "b16"
version = "0.1.0"

6
Cargo.toml Normal file
View file

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

1
README.md Normal file
View file

@ -0,0 +1 @@
Hexadecimal encoder and decoder written in Rust

131
src/decode.rs Normal file
View file

@ -0,0 +1,131 @@
use std::{
fmt,
io::{self, ErrorKind, Read, Write},
num::TryFromIntError,
};
#[derive(Debug)]
/// Errors which might possibly occur while decoding base64 encoded data
pub enum Error {
Io(io::Error),
IllegalChar(char),
IntError(TryFromIntError),
MissingChar,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(e) => write!(f, "{e}"),
Self::IllegalChar(c) => write!(f, "Illegal Character ({c})"),
Self::IntError(e) => write!(f, "Int Error: {e}"),
Self::MissingChar => write!(f, "Missing character"),
}
}
}
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 std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(e) => Some(e),
Self::IntError(e) => Some(e),
_ => None,
}
}
}
pub struct Decoder<R: Read, W: Write> {
reader: R,
writer: W,
}
impl<R: Read, W: Write> Decoder<R, W> {
pub fn new(reader: R, writer: W) -> Self {
Self { reader, writer }
}
/// Decodes the bytes provided by `self.reader` and writes the resulting
/// bytes into `self.writer`.
/// # Errors
/// - io failure will return `Error::Io`
/// - a character not in the alphabet, or encountering any character after
/// the first occurance of the padding character, will return `Error::IllegalChar`
/// - if the reader comes up short of the final four byte block will return
/// `Error::MissingPadding`
/// - `intError` should never be returned. If this error is recieved the code
/// is somehow broken. Please file a bug.
pub fn decode(&mut self) -> Result<(), Error> {
loop {
let mut in_buf = [0_u8; 2];
let mut out_buf = [0_u8; 1];
let mut n_bytes = 0;
loop {
n_bytes += match self.reader.read(&mut in_buf) {
Ok(n) => n,
Err(e) if e.kind() == ErrorKind::Interrupted => continue,
Err(e) => return Err(e.into()),
};
break;
}
match n_bytes {
0 => break,
1 => return Err(Error::MissingChar),
2 => {}
_ => unreachable!(),
}
let mut c = char::from(in_buf[0]);
if let Some(idx) = super::get_idx(c.to_ascii_uppercase()) {
out_buf[0] |= (idx << 4) as u8;
} else {
return Err(Error::IllegalChar(c));
}
c = char::from(in_buf[1]);
if let Some(idx) = super::get_idx(c.to_ascii_uppercase()) {
out_buf[0] |= idx as u8;
} else {
return Err(Error::IllegalChar(c));
}
self.writer.write_all(&out_buf)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decode() {
let mut decoder = Decoder::new("48656C6C6F2C20576F726C64".as_bytes(), vec![]);
decoder.decode().unwrap();
assert_eq!(String::from_utf8(decoder.writer).unwrap(), "Hello, World");
decoder = Decoder::new("48656C6C6F2C20576F726C6421".as_bytes(), vec![]);
decoder.decode().unwrap();
assert_eq!(String::from_utf8(decoder.writer).unwrap(), "Hello, World!");
decoder = Decoder::new("48656C6C6F2C20576F726C64210A".as_bytes(), vec![]);
decoder.decode().unwrap();
assert_eq!(
String::from_utf8(decoder.writer).unwrap(),
"Hello, World!\n"
);
}
}

111
src/encode.rs Normal file
View file

@ -0,0 +1,111 @@
use {
super::B16_ALPHABET,
std::{
fmt::{self, Write},
io::{self, ErrorKind, Read},
},
};
#[derive(Debug)]
pub enum Error {
Io(io::Error),
Fmt(fmt::Error),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(e) => write!(f, "{e}"),
Self::Fmt(e) => write!(f, "{e}"),
}
}
}
impl From<io::Error> for Error {
fn from(value: io::Error) -> Self {
Self::Io(value)
}
}
impl From<fmt::Error> for Error {
fn from(value: fmt::Error) -> Self {
Self::Fmt(value)
}
}
impl std::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),
}
}
}
pub struct Encoder<R: Read, W: Write> {
reader: R,
writer: W,
}
impl<R: Read, W: Write> Encoder<R, W> {
/// Creates a new encoder with the given reader and writer. If alphabet is
/// `None` the encoder will use the default rfc4648 alphabet.
pub fn new(reader: R, writer: W) -> Self {
Self { reader, writer }
}
/// Encodes the given data as hexadecimal
/// # Errors
/// May return an error on IO failure
pub fn encode(&mut self) -> Result<(), Error> {
loop {
let mut ibuf = [0; 1];
let mut obuf = [' '; 2];
let mut n_bytes = 0;
loop {
n_bytes += match self.reader.read(&mut ibuf) {
Ok(n) => n,
Err(e) if e.kind() == ErrorKind::Interrupted => continue,
Err(e) => return Err(e.into()),
};
break;
}
if n_bytes == 0 {
break;
}
let mut idx = usize::from(ibuf[0] & 0b1111);
obuf[1] = B16_ALPHABET[idx];
idx = usize::from((ibuf[0] & 0b1111_0000) >> 4);
obuf[0] = B16_ALPHABET[idx];
write!(self.writer, "{}{}", obuf[0], obuf[1])?;
}
Ok(())
}
}
impl<R: Read> Encoder<R, String> {
pub fn output(self) -> String {
self.writer
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encode() {
let mut encoder = Encoder::new("Hello, World".as_bytes(), String::new());
encoder.encode().unwrap();
assert_eq!(encoder.output(), "48656C6C6F2C20576F726C64");
encoder = Encoder {
reader: "Hello, World!".as_bytes(),
writer: String::new(),
};
encoder.encode().unwrap();
assert_eq!(encoder.output(), "48656C6C6F2C20576F726C6421");
encoder = Encoder::new("Hello, World!\n".as_bytes(), String::new());
encoder.encode().unwrap();
assert_eq!(encoder.output(), "48656C6C6F2C20576F726C64210A");
}
}

16
src/lib.rs Normal file
View file

@ -0,0 +1,16 @@
pub mod decode;
#[warn(clippy::all, clippy::pedantic)]
pub mod encode;
pub static B16_ALPHABET: [char; 16] = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F',
];
pub(crate) fn get_idx(c: char) -> Option<usize> {
for (idx, x) in B16_ALPHABET.iter().enumerate() {
if *x == c {
return Some(idx);
}
}
None
}