Skip to Content
Course content

98: The Read and Write Traits

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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 writer W and a key: u8.
  • Implement the std::io::Write trait for XorWriter.
  • In the write method, iterate through the provided buffer, XOR each byte with the key, and write the result to the inner writer.
  • Write a main function that uses XorWriter to write the string "Secret Message" into a Vec<u8>, then prints the resulting bytes to verify they are encrypted.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.