Skip to content

Commit 75c0bfc

Browse files
committed
Add std stream encoder reader
1 parent 5caa13b commit 75c0bfc

4 files changed

Lines changed: 198 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
- Added caller-owned encode/decode buffers and in-place decode.
88
- Added in-place encoding.
99
- Added optional `alloc` vector helpers.
10-
- Added `std::io::Write` streaming encoder behind the `stream` feature.
10+
- Added `std::io::Write` and `std::io::Read` streaming encoders behind the `stream` feature.
1111
- Added checked encoded-length helpers.
1212
- Added exact decoded-length helpers.
1313
- Hardened decode errors to report absolute input indexes.

README.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Implemented now:
1818
- In-place encoding when the caller provides enough spare capacity.
1919
- Optional `alloc` vector helpers.
2020
- In-place decode API built on the same strict scalar decoder.
21-
- `std::io::Write` streaming encoder behind the `stream` feature.
21+
- `std::io::Write` and `std::io::Read` streaming encoders behind the `stream` feature.
2222
- Focused unit and integration tests.
2323
- Local check scripts, release gate, dependency policy, audit config, CI, SBOM script, and reproducible build check.
2424

@@ -110,17 +110,22 @@ let decoded = STANDARD.decode_vec(&encoded).unwrap();
110110
assert_eq!(decoded, b"hello");
111111
```
112112

113-
With the `stream` feature, a `std::io::Write` encoder is available:
113+
With the `stream` feature, `std::io` encoders are available:
114114

115115
```rust
116-
use std::io::Write;
117-
use base64_ng::{STANDARD, stream::Encoder};
116+
use std::io::{Read, Write};
117+
use base64_ng::{STANDARD, stream::{Encoder, EncoderReader}};
118118

119119
let mut encoder = Encoder::new(Vec::new(), STANDARD);
120120
encoder.write_all(b"he").unwrap();
121121
encoder.write_all(b"llo").unwrap();
122122
let encoded = encoder.finish().unwrap();
123123
assert_eq!(encoded, b"aGVsbG8=");
124+
125+
let mut reader = EncoderReader::new(&b"hello"[..], STANDARD);
126+
let mut encoded = String::new();
127+
reader.read_to_string(&mut encoded).unwrap();
128+
assert_eq!(encoded, "aGVsbG8=");
124129
```
125130

126131
URL-safe, no-padding encoding:

src/lib.rs

Lines changed: 151 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,18 +46,24 @@ pub mod stream {
4646
//! Streaming Base64 wrappers for `std::io`.
4747
//!
4848
//! ```
49-
//! use std::io::Write;
50-
//! use base64_ng::{STANDARD, stream::Encoder};
49+
//! use std::io::{Read, Write};
50+
//! use base64_ng::{STANDARD, stream::{Encoder, EncoderReader}};
5151
//!
5252
//! let mut encoder = Encoder::new(Vec::new(), STANDARD);
5353
//! encoder.write_all(b"he").unwrap();
5454
//! encoder.write_all(b"llo").unwrap();
5555
//! let encoded = encoder.finish().unwrap();
5656
//! assert_eq!(encoded, b"aGVsbG8=");
57+
//!
58+
//! let mut reader = EncoderReader::new(&b"hello"[..], STANDARD);
59+
//! let mut encoded = String::new();
60+
//! reader.read_to_string(&mut encoded).unwrap();
61+
//! assert_eq!(encoded, "aGVsbG8=");
5762
//! ```
5863
5964
use super::{Alphabet, EncodeError, Engine};
60-
use std::io::{self, Write};
65+
use std::collections::VecDeque;
66+
use std::io::{self, Read, Write};
6167

6268
/// A streaming Base64 encoder for `std::io::Write`.
6369
pub struct Encoder<W, A, const PAD: bool>
@@ -199,6 +205,148 @@ pub mod stream {
199205
fn encode_error_to_io(err: EncodeError) -> io::Error {
200206
io::Error::new(io::ErrorKind::InvalidInput, err)
201207
}
208+
209+
/// A streaming Base64 encoder for `std::io::Read`.
210+
pub struct EncoderReader<R, A, const PAD: bool>
211+
where
212+
A: Alphabet,
213+
{
214+
inner: R,
215+
engine: Engine<A, PAD>,
216+
pending: [u8; 2],
217+
pending_len: usize,
218+
output: VecDeque<u8>,
219+
finished: bool,
220+
}
221+
222+
impl<R, A, const PAD: bool> EncoderReader<R, A, PAD>
223+
where
224+
A: Alphabet,
225+
{
226+
/// Creates a new streaming encoder reader.
227+
#[must_use]
228+
pub fn new(inner: R, engine: Engine<A, PAD>) -> Self {
229+
Self {
230+
inner,
231+
engine,
232+
pending: [0; 2],
233+
pending_len: 0,
234+
output: VecDeque::new(),
235+
finished: false,
236+
}
237+
}
238+
239+
/// Returns a shared reference to the wrapped reader.
240+
#[must_use]
241+
pub const fn get_ref(&self) -> &R {
242+
&self.inner
243+
}
244+
245+
/// Returns a mutable reference to the wrapped reader.
246+
pub fn get_mut(&mut self) -> &mut R {
247+
&mut self.inner
248+
}
249+
250+
/// Consumes the encoder reader and returns the wrapped reader.
251+
#[must_use]
252+
pub fn into_inner(self) -> R {
253+
self.inner
254+
}
255+
}
256+
257+
impl<R, A, const PAD: bool> Read for EncoderReader<R, A, PAD>
258+
where
259+
R: Read,
260+
A: Alphabet,
261+
{
262+
fn read(&mut self, output: &mut [u8]) -> io::Result<usize> {
263+
if output.is_empty() {
264+
return Ok(0);
265+
}
266+
267+
while self.output.is_empty() && !self.finished {
268+
self.fill_output()?;
269+
}
270+
271+
let mut written = 0;
272+
while written < output.len() {
273+
let Some(byte) = self.output.pop_front() else {
274+
break;
275+
};
276+
output[written] = byte;
277+
written += 1;
278+
}
279+
280+
Ok(written)
281+
}
282+
}
283+
284+
impl<R, A, const PAD: bool> EncoderReader<R, A, PAD>
285+
where
286+
R: Read,
287+
A: Alphabet,
288+
{
289+
fn fill_output(&mut self) -> io::Result<()> {
290+
let mut input = [0u8; 768];
291+
let read = self.inner.read(&mut input)?;
292+
if read == 0 {
293+
self.finished = true;
294+
self.push_final_pending()?;
295+
return Ok(());
296+
}
297+
298+
let mut consumed = 0;
299+
if self.pending_len > 0 {
300+
let needed = 3 - self.pending_len;
301+
if read < needed {
302+
self.pending[self.pending_len..self.pending_len + read]
303+
.copy_from_slice(&input[..read]);
304+
self.pending_len += read;
305+
return Ok(());
306+
}
307+
308+
let mut chunk = [0u8; 3];
309+
chunk[..self.pending_len].copy_from_slice(&self.pending[..self.pending_len]);
310+
chunk[self.pending_len..].copy_from_slice(&input[..needed]);
311+
self.push_encoded(&chunk)?;
312+
self.pending_len = 0;
313+
consumed += needed;
314+
}
315+
316+
let remaining = &input[consumed..read];
317+
let full_len = remaining.len() / 3 * 3;
318+
if full_len > 0 {
319+
self.push_encoded(&remaining[..full_len])?;
320+
}
321+
322+
let tail = &remaining[full_len..];
323+
self.pending[..tail.len()].copy_from_slice(tail);
324+
self.pending_len = tail.len();
325+
Ok(())
326+
}
327+
328+
fn push_final_pending(&mut self) -> io::Result<()> {
329+
if self.pending_len == 0 {
330+
return Ok(());
331+
}
332+
333+
let mut pending = [0u8; 2];
334+
pending[..self.pending_len].copy_from_slice(&self.pending[..self.pending_len]);
335+
let pending_len = self.pending_len;
336+
self.pending_len = 0;
337+
self.push_encoded(&pending[..pending_len])
338+
}
339+
340+
fn push_encoded(&mut self, input: &[u8]) -> io::Result<()> {
341+
let mut encoded = [0u8; 1024];
342+
let written = self
343+
.engine
344+
.encode_slice(input, &mut encoded)
345+
.map_err(encode_error_to_io)?;
346+
self.output.extend(&encoded[..written]);
347+
Ok(())
348+
}
349+
}
202350
}
203351

204352
/// Standard Base64 engine with padding.

tests/rfc4648.rs

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ use base64_ng::{
44
};
55

66
#[cfg(feature = "stream")]
7-
use base64_ng::stream::Encoder;
7+
use base64_ng::stream::{Encoder, EncoderReader};
88

99
#[cfg(feature = "stream")]
10-
use std::io::Write;
10+
use std::io::{Read, Write};
1111

1212
#[test]
1313
fn rfc4648_standard_round_trips() {
@@ -310,6 +310,41 @@ fn stream_encoder_exposes_inner_writer() {
310310
assert_eq!(encoded, b"-_8");
311311
}
312312

313+
#[cfg(feature = "stream")]
314+
#[test]
315+
fn stream_encoder_reader_handles_small_reads() {
316+
let mut reader = EncoderReader::new(&b"hello"[..], STANDARD);
317+
let mut output = [0u8; 8];
318+
let mut written = 0;
319+
while written < output.len() {
320+
let read = reader.read(&mut output[written..written + 1]).unwrap();
321+
if read == 0 {
322+
break;
323+
}
324+
written += read;
325+
}
326+
assert_eq!(&output[..written], b"aGVsbG8=");
327+
}
328+
329+
#[cfg(feature = "stream")]
330+
#[test]
331+
fn stream_encoder_reader_supports_no_padding() {
332+
let mut reader = EncoderReader::new(&b"hello"[..], STANDARD_NO_PAD);
333+
let mut encoded = Vec::new();
334+
reader.read_to_end(&mut encoded).unwrap();
335+
assert_eq!(encoded, b"aGVsbG8");
336+
}
337+
338+
#[cfg(feature = "stream")]
339+
#[test]
340+
fn stream_encoder_reader_supports_url_safe() {
341+
let mut reader = EncoderReader::new(&b"\xfb\xff"[..], URL_SAFE_NO_PAD);
342+
let mut encoded = Vec::new();
343+
assert_eq!(reader.get_ref().len(), 2);
344+
reader.read_to_end(&mut encoded).unwrap();
345+
assert_eq!(encoded, b"-_8");
346+
}
347+
313348
fn assert_round_trip<A, const PAD: bool>(engine: &base64_ng::Engine<A, PAD>, input: &[u8])
314349
where
315350
A: base64_ng::Alphabet,

0 commit comments

Comments
 (0)