Rust: Binary I/O

Home · Blog

6 April 2023

I’ve written about Text I/O in Rust before. Today I’ll take a look at binary I/O.

Writing a file

Let’s start by writing some binary data to a file. We’ll keep it simple and just write 48 bytes to the file, containing the numbers 0 through 47:

fn write_file() -> io::Result<()> {
    let buf: Vec<u8> = (0..0x30).collect();
    let mut f = File::create("foo")?;
    f.write_all(&buf)
}

We open the file with File::create, same as for text files, and use write_all to write the data. (You’ll need use std::Write to have write_all available.)

We can use the od command to check that it worked:

$ od -A x -t x1 foo
000000 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f
000010 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f
000020 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f
000030

This is printing the contents of the file as hexadecimal numbers, one byte at a time. If you prefer decimal numbers try od -A d -t u1 foo, or see man od.

Reading from a file

As an example of reading binary data, let’s open the file we just created and take a look at the very last byte:

fn print_last() -> io::Result<()> {
    let mut f = File::open("foo")?;
    f.seek(SeekFrom::End(-1))?;
    let mut buf = [0u8; 1];
    f.read_exact(&mut buf)?;
    println!("last byte is: {}", buf[0]);
    Ok(())
}

The seek method lets you jump to a different location in the file, and the SeekFrom enum lets you choose a location relative to the start of the file, the end of the file, or the current position. We call SeekFrom::End(-1) to get to one byte before the end, then use read_exact to fill a one-byte buffer with data from the file.

When we run this code, it prints

last byte is: 47

We initially stored the values 0 through 47, so that looks correct.

Modifying a file

As an example of changing an existing file, let’s take the same file and change the numbers in the middle -- 16 through 31, or 0x10 to 0x1f -- and set all of the bits to 1. In other words, change those bytes to 256 (0xff). We want to write to the file, but we don’t want to discard its current content, so we’ll open it with File::options. Next, we’ll seek to the position where we want to start writing, prepare a buffer with the data we want to write, and write the data:

fn change_file() -> io::Result<()> {
    let mut f = File::options().write(true).open("foo")?;
    f.seek(SeekFrom::Start(0x10))?;
    let mut buf = [0xffu8; 0x10];
    f.write_all(&mut buf)?;
    Ok(())
}

Here’s what the file looks like now:

$ od -A x -t x1 foo
000000 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f
000010 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff
000020 20 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f
000030

What if instead of changing existing data, we want to add data to the end of the file? We could use the same code as before, except seeking to the end like this:

    f.seek(SeekFrom::End(0))?;

but we can also open the file in “append mode”:

fn append_to_file() -> io::Result<()> {
    let mut f = File::options().append(true).open("foo")?;
    let mut buf = [0xffu8; 0x10];
    f.write_all(&mut buf)?;
    Ok(())
}

Finally, what if we want to remove data from the end of the file? There’s no method in File to delete data -- that’s just not how files work -- but File::set_len will truncate the file if the new size is less than the current size. If we want to remove the block of data we just added, we can do it with this short function:

fn truncate_file() -> io::Result<()> {
    File::options().write(true).open("foo")?.set_len(0x30)
}