From 66d5702da08c779f8cb04500fc14cd23015f2f5b Mon Sep 17 00:00:00 2001 From: Nathan Fisher Date: Thu, 19 Dec 2024 23:54:16 -0500 Subject: [PATCH] Initial commit --- .gitignore | 1 + Cargo.lock | 7 +++ Cargo.toml | 6 +++ README.md | 1 + src/decode.rs | 131 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/encode.rs | 111 ++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 16 ++++++ 7 files changed, 273 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 src/decode.rs create mode 100644 src/encode.rs create mode 100644 src/lib.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..4678a17 --- /dev/null +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..1b2d1f6 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "b16" +version = "0.1.0" +edition = "2021" + +[dependencies] diff --git a/README.md b/README.md new file mode 100644 index 0000000..7d08fb6 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +Hexadecimal encoder and decoder written in Rust diff --git a/src/decode.rs b/src/decode.rs new file mode 100644 index 0000000..54cda4c --- /dev/null +++ b/src/decode.rs @@ -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 for Error { + fn from(value: io::Error) -> Self { + Self::Io(value) + } +} + +impl From for Error { + fn from(value: char) -> Self { + Self::IllegalChar(value) + } +} + +impl From 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 { + reader: R, + writer: W, +} + +impl Decoder { + 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" + ); + } +} diff --git a/src/encode.rs b/src/encode.rs new file mode 100644 index 0000000..3f0985e --- /dev/null +++ b/src/encode.rs @@ -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 for Error { + fn from(value: io::Error) -> Self { + Self::Io(value) + } +} + +impl From 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 { + reader: R, + writer: W, +} + +impl Encoder { + /// 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 Encoder { + 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"); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..5b562c3 --- /dev/null +++ b/src/lib.rs @@ -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 { + for (idx, x) in B16_ALPHABET.iter().enumerate() { + if *x == c { + return Some(idx); + } + } + None +}