Add line wrapping to encoder

This commit is contained in:
Nathan Fisher 2025-01-04 19:04:56 -05:00
parent 13fda08e1b
commit 0493911702
6 changed files with 103 additions and 97 deletions

View file

@ -1,60 +1,8 @@
use { use {
super::B64Alphabet, crate::{error::Error, B64Alphabet},
std::{ std::io::{ErrorKind, Read, Write},
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),
MissingPadding,
IntError(TryFromIntError),
}
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::MissingPadding => write!(f, "Missing Padding"),
Self::IntError(e) => write!(f, "Int Error: {e}"),
}
}
}
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> { pub struct Decoder<R: Read, W: Write> {
reader: R, reader: R,
writer: W, writer: W,

View file

@ -1,65 +1,32 @@
use { use {
super::B64Alphabet, crate::{error::Error, B64Alphabet},
std::{ std::{
fmt::{self, Write}, fmt::Write,
io::{self, ErrorKind, Read}, io::{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> { pub struct Encoder<R: Read, W: Write> {
reader: R, reader: R,
writer: W, writer: W,
alphabet: B64Alphabet, alphabet: B64Alphabet,
wrap: Option<usize>,
} }
impl<R: Read, W: Write> Encoder<R, W> { impl<R: Read, W: Write> Encoder<R, W> {
/// Creates a new encoder with the given reader and writer. If alphabet is /// Creates a new encoder with the given reader and writer. If alphabet is
/// `None` the encoder will use the default rfc4648 alphabet. /// `None` the encoder will use the default rfc4648 alphabet.
pub fn new(reader: R, writer: W, alphabet: Option<B64Alphabet>) -> Self { pub fn new(reader: R, writer: W, alphabet: Option<B64Alphabet>, wrap: Option<usize>) -> Self {
Self { Self {
reader, reader,
writer, writer,
alphabet: alphabet.unwrap_or_default(), alphabet: alphabet.unwrap_or_default(),
wrap,
} }
} }
pub fn encode(&mut self) -> Result<(), Error> { pub fn encode(&mut self) -> Result<(), Error> {
let mut total: usize = 0;
loop { loop {
let mut ibuf = [0; 3]; let mut ibuf = [0; 3];
let mut obuf = [self.alphabet.pad(); 4]; let mut obuf = [self.alphabet.pad(); 4];
@ -93,7 +60,17 @@ impl<R: Read, W: Write> Encoder<R, W> {
} }
num >>= 6; num >>= 6;
} }
write!(self.writer, "{}{}{}{}", obuf[0], obuf[1], obuf[2], obuf[3])?; if let Some(wrap) = self.wrap {
for idx in 0..=3 {
write!(self.writer, "{}", obuf[idx])?;
total += 1;
if total % wrap == 0 {
writeln!(self.writer)?;
}
}
} else {
write!(self.writer, "{}{}{}{}", obuf[0], obuf[1], obuf[2], obuf[3])?;
}
if outlen < 4 { if outlen < 4 {
break; break;
} }
@ -111,21 +88,32 @@ impl<R: Read> Encoder<R, String> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use std::fs::{self, File};
#[test] #[test]
fn encode() { fn encode() {
let mut encoder = Encoder::new("Hello, World".as_bytes(), String::new(), None); let mut encoder = Encoder::new("Hello, World".as_bytes(), String::new(), None, None);
encoder.encode().unwrap(); encoder.encode().unwrap();
assert_eq!(encoder.output(), "SGVsbG8sIFdvcmxk"); assert_eq!(encoder.output(), "SGVsbG8sIFdvcmxk");
encoder = Encoder { encoder = Encoder {
reader: "Hello, World!".as_bytes(), reader: "Hello, World!".as_bytes(),
writer: String::new(), writer: String::new(),
alphabet: B64Alphabet::default(), alphabet: B64Alphabet::default(),
wrap: None,
}; };
encoder.encode().unwrap(); encoder.encode().unwrap();
assert_eq!(encoder.output(), "SGVsbG8sIFdvcmxkIQ=="); assert_eq!(encoder.output(), "SGVsbG8sIFdvcmxkIQ==");
encoder = Encoder::new("Hello, World!\n".as_bytes(), String::new(), None); encoder = Encoder::new("Hello, World!\n".as_bytes(), String::new(), None, None);
encoder.encode().unwrap(); encoder.encode().unwrap();
assert_eq!(encoder.output(), "SGVsbG8sIFdvcmxkIQo="); assert_eq!(encoder.output(), "SGVsbG8sIFdvcmxkIQo=");
} }
#[test]
fn wrapping() {
let infile = File::open("src/testdata/lorem.txt").unwrap();
let outfile = fs::read_to_string("src/testdata/lorem_b64.txt").unwrap();
let mut encoder = Encoder::new(infile, 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,
}
}
}

View file

@ -1,6 +1,7 @@
#[warn(clippy::all, clippy::pedantic)] #[warn(clippy::all, clippy::pedantic)]
pub mod decode; pub mod decode;
pub mod encode; pub mod encode;
pub mod error;
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct B64Alphabet { pub struct B64Alphabet {

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

@ -0,0 +1 @@
Lorem ipsum odor amet, consectetuer adipiscing elit. Mauris praesent senectus vivamus urna donec magna bibendum aenean. Nam nec sodales ex cursus inceptos vitae non habitant consectetur. Ex montes quis pellentesque tempor neque. Class ipsum fringilla mauris quam tempus. Nullam habitasse arcu ac nascetur class ad in. Senectus nostra arcu habitant arcu blandit potenti. Congue penatibus sollicitudin ullamcorper ridiculus semper. Ac porttitor bibendum nam; rhoncus pellentesque natoque pretium sit feugiat. Litora convallis maecenas elit netus blandit mus duis gravida sollicitudin.

11
src/testdata/lorem_b64.txt vendored Normal file
View file

@ -0,0 +1,11 @@
TG9yZW0gaXBzdW0gb2RvciBhbWV0LCBjb25zZWN0ZXR1ZXIgYWRpcGlzY2luZyBlbGl0LiBNYXVy
aXMgcHJhZXNlbnQgc2VuZWN0dXMgdml2YW11cyB1cm5hIGRvbmVjIG1hZ25hIGJpYmVuZHVtIGFl
bmVhbi4gTmFtIG5lYyBzb2RhbGVzIGV4IGN1cnN1cyBpbmNlcHRvcyB2aXRhZSBub24gaGFiaXRh
bnQgY29uc2VjdGV0dXIuIEV4IG1vbnRlcyBxdWlzIHBlbGxlbnRlc3F1ZSB0ZW1wb3IgbmVxdWUu
IENsYXNzIGlwc3VtIGZyaW5naWxsYSBtYXVyaXMgcXVhbSB0ZW1wdXMuIE51bGxhbSBoYWJpdGFz
c2UgYXJjdSBhYyBuYXNjZXR1ciBjbGFzcyBhZCBpbi4gU2VuZWN0dXMgbm9zdHJhIGFyY3UgaGFi
aXRhbnQgYXJjdSBibGFuZGl0IHBvdGVudGkuIENvbmd1ZSBwZW5hdGlidXMgc29sbGljaXR1ZGlu
IHVsbGFtY29ycGVyIHJpZGljdWx1cyBzZW1wZXIuIEFjIHBvcnR0aXRvciBiaWJlbmR1bSBuYW07
IHJob25jdXMgcGVsbGVudGVzcXVlIG5hdG9xdWUgcHJldGl1bSBzaXQgZmV1Z2lhdC4gTGl0b3Jh
IGNvbnZhbGxpcyBtYWVjZW5hcyBlbGl0IG5ldHVzIGJsYW5kaXQgbXVzIGR1aXMgZ3JhdmlkYSBz
b2xsaWNpdHVkaW4uCg==