Standard Input and Output
BufRead
std::io::BufRead is a subtrait of std::io::Read (pub trait BufRead: Read) for input streams that maintain an internal buffer of unread bytes. Because a BufRead implementation can peek at its internal buffer (fill_buf) before advancing its read cursor (consume), it unlocks delimiter-based parsing methods (read_line, read_until, lines, split) that are impossible to implement efficiently on an unbuffered Read.
The Two Required Methods of BufRead: fill_buf and consume
pub trait BufRead: Read {
fn fill_buf(&mut self) -> Result<&[u8]>;
fn consume(&mut self, amt: usize);
// Provided default methods:
fn has_data_left(&mut self) -> Result<bool> { ... }
fn read_until(&mut self, byte: u8, buf: &mut Vec<u8>) -> Result<usize> { ... }
fn skip_until(&mut self, byte: u8) -> Result<usize> { ... }
fn read_line(&mut self, buf: &mut String) -> Result<usize> { ... }
fn split(self, byte: u8) -> Split<Self> where Self: Sized { ... }
fn lines(self) -> Lines<Self> where Self: Sized { ... }
}
How `fill_buf()` and `consume(amt)` Enable Zero-Copy Lookahead:
Internal Buffer: [ 'G', 'E', 'T', ' ', '/', 'a', 'p', 'i' ]
^~~~~~~~~~~~~~~~^
1. `let available: &[u8] = reader.fill_buf()?;`
(Returns a slice of all unread bytes currently in buffer; does NOT advance cursor!)
2. Inspect `available` in place (e.g. find `' '` at index 3).
3. `reader.consume(4);`
(Advances the internal cursor past `'G', 'E', 'T', ' '` without copying bytes!)
Types That Implement BufRead Out of the Box
| Type | Why It Implements BufRead |
|---|---|
BufReader<R> (where R: Read) |
Wraps any File, TcpStream, or ChildStdout in an 8 KiB heap buffer |
StdinLock<'_> (io::stdin().lock()) |
Exposes the global Stdin internal BufReader directly |
&[u8] (Byte slice!) |
A byte slice is already an in-memory buffer! Calling fill_buf() returns the slice itself, and consume(n) advances &self[n..]! |
Cursor<T> (where T: AsRef<[u8]>) |
Wraps Vec<u8>, &[u8], or String with a seekable position cursor |
Zero-Copy Parsing Using fill_buf, consume, and read_until
use std::io::{self, BufRead};
fn main() -> io::Result<()> {
// A plain byte slice `&[u8]` implements `BufRead` directly!
let mut stream: &[u8] = b"TOKEN_A|TOKEN_B|REMAINDER";
// 1. Read up to delimiter `b'|'` into a byte vector:
let mut first_token = Vec::new();
stream.read_until(b'|', &mut first_token)?;
println!(
"read_until '|': {:?}",
std::str::from_utf8(&first_token).unwrap()
);
// 2. Skip past the next `b'|'` without allocating a buffer (`skip_until`, Rust 1.80+):
let skipped = stream.skip_until(b'|')?;
println!("skipped {} bytes up to second '|'", skipped);
// 3. Peek at the remaining buffer with `fill_buf()` and `consume()`:
let peeked = stream.fill_buf()?;
println!("peeked remainder: {:?}", std::str::from_utf8(peeked).unwrap());
let len = peeked.len();
stream.consume(len);
println!("has_data_left after consume: {}", stream.has_data_left()?);
Ok(())
}
read_until '|': "TOKEN_A|"
skipped 8 bytes up to second '|'
peeked remainder: "REMAINDER"
has_data_left after consume: false