Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
98: The Read and Write Traits
I've been working on a small utility to export system configuration to different formats. Initially, I just wanted to dump a string into a file, so I wrote a quick helper function. It looked something like this:
use std::fs::File;
use std::io::Write;
fn save_config(destination: &mut File, data: &str) -> std::io::Result<()> {
destination.write_all(data.as_bytes())?;
Ok(())
}
The wall I hit during testing
This worked fine for actual files. But as soon as I started writing unit tests, I ran into a problem. I didn't want my tests to actually hit the disk—it's slow, it leaves junk files behind, and it's generally a pain. I wanted to just write the output into a Vec<u8> in memory and check if the bytes were correct.
I tried to pass a vector into my function, and the compiler immediately shut me down:
let mut buffer = Vec::new();
save_config(&mut buffer, "setting=true")?; // Error!
The compiler told me that Vec<u8> is not a File. Obviously. But here is the thing: both File and Vec<u8> have a write_all method. They both "act" like things you can write bytes into. In Rust, this shared behavior is captured by the std::io::Write trait.
Opening up the function signature
If I want my function to accept anything that can be written to, I shouldn't be asking for a File. I should be asking for something that implements Write. I'll rewrite the signature using a generic type.
use std::io::{Write, Result};
fn save_config<W: Write>(destination: &mut W, data: &str) -> Result<()> {
destination.write_all(data.as_bytes())?;
Ok(())
}
Now, the magic happens. I can pass a File in production, and I can pass a Vec<u8> in my tests. Rust doesn't care what the underlying type is, as long as it knows how to handle the Write methods. It's a huge win for testability.
Dealing with the input side
Naturally, the same logic applies when I need to read data. I have a function that parses a config string, but right now it takes a String. That's inefficient because it forces the caller to load the entire file into memory first.
I want to stream the data instead. I could use std::fs::File, but again, that limits me. If I use the std::io::Read trait, I can handle files, network sockets, or even just a byte slice &[u8].
Let's try to implement a basic reader that reads until it hits a specific delimiter:
use std::io::{Read, Result};
fn read_until_delimiter<R: Read>(source: &mut R, delimiter: u8) -> Result<Vec<u8>> {
let mut buffer = Vec::new();
let mut byte = [0u8; 1];
while source.read_exact(&mut byte).is_ok() {
if byte[0] == delimiter {
break;
}
buffer.push(byte[0]);
}
Ok(buffer)
}
The "Gotcha" with slices
While testing this, I tried passing a string slice like this: read_until_delimiter(&mut "hello\n", b'\n'). I got an error saying &str doesn't implement Read. I was confused—I thought slices were readable!
I realized I was trying to pass a string, but Read works on bytes. When I changed the input to a byte slice b"hello\n", it worked perfectly. This is a common stumbling block: &[u8] implements Read, but &str does not. You have to call .as_bytes() first.
By coding to the Read and Write traits rather than concrete types, I've effectively decoupled my business logic from the I/O layer. My functions no longer care if they are talking to a hard drive, a TCP stream, or a piece of RAM.
📋 Practical Task
Implement a Simple XOR Encryption Wrapper
Your goal is to create a "Transparent XOR Encryptor." Instead of writing a function that encrypts a whole buffer at once, you will create a struct that wraps any type implementing Write and XORs every byte as it passes through.
Requirements:
- Define a struct
XorWriter<W: Write>that holds a destination writerWand akey: u8. - Implement the
std::io::Writetrait forXorWriter. - In the
writemethod, iterate through the provided buffer, XOR each byte with thekey, and write the result to the inner writer. - Write a
mainfunction that usesXorWriterto write the string "Secret Message" into aVec<u8>, then prints the resulting bytes to verify they are encrypted.
There are no comments for now.