How to read a file into bytes
Short answer: std::fs::read(path) reads a whole file and gives back an io::Result<Vec<u8>> — every byte on disk, with no assumption about what they mean. Reach for it whenever the file isn’t guaranteed to be UTF-8 text: the same file that fails read_to_string with InvalidData reads fine here.
From a fresh project — cargo new read-demo, then replace the contents of src/main.rs with each block below and run it with cargo run. The project name shows up only in cargo’s own output; nothing else on this page depends on it. Each block writes the file it reads before reading it, so it runs on its own; in your own code the file is already there and you delete that line.
Read the whole file as bytes
Section titled “Read the whole file as bytes”use std::fs;
fn main() -> std::io::Result<()> { // The first eight bytes of a PNG file, plus one byte that isn't valid UTF-8. fs::write("sample.bin", [0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A, 0xE9]).unwrap(); let bytes = fs::read("sample.bin")?; let hex = bytes.iter().map(|b| format!("{b:02X}")).collect::<Vec<_>>().join(" "); println!("{hex}"); println!("{} bytes", bytes.len()); Ok(())}89 50 4E 47 0D 0A 1A 0A E99 bytesOne call opens the file, reads it to the end and closes it. What comes back is a Vec<u8> — one element per byte, so len() is the size on disk and indexing gives you a single byte. {:?} on a Vec<u8> prints decimals ([137, 80, 78, 71, …]), and file formats are written in hex: 89 50 4E 47 0D 0A 1A 0A is the PNG signature, the first eight bytes of the file.
The same read, spelled out
Section titled “The same read, spelled out”fs::read is File::open plus read_to_end, with the file’s size taken from its metadata as a capacity hint. The long form is what you want when there’s already a buffer to fill — reusing one Vec across files avoids reallocating each time:
use std::fs::{self, File};use std::io::{self, Read};
fn main() -> io::Result<()> { fs::write("sample.bin", [0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A, 0xE9]).unwrap();
let mut file = File::open("sample.bin")?; let mut bytes = Vec::new(); file.read_to_end(&mut bytes)?; println!("{} bytes", bytes.len()); Ok(())}9 bytesread_to_end grows the buffer as bytes arrive, which is also the cost to know about: both forms hold the entire file in memory at once. A file you aren’t willing to hold in a Vec wants a chunked read off a File instead.
When a String would have failed
Section titled “When a String would have failed”Here is the same Latin-1 file from the String page, through both readers:
use std::fs;
fn main() { // "café" in Latin-1: 0xE9 on its own isn't valid UTF-8. fs::write("latin1.txt", [b'c', b'a', b'f', 0xE9, b'\n']).unwrap();
match fs::read_to_string("latin1.txt") { Ok(text) => println!("{text}"), Err(err) => println!("{err:?}"), }
let bytes = fs::read("latin1.txt").unwrap(); println!("{} bytes read anyway", bytes.len());}Error { kind: InvalidData, message: "stream did not contain valid UTF-8" }5 bytes read anywayread_to_string has to produce a String, and a String can only hold valid UTF-8, so it reads the bytes and then checks them. The file was read fine — the error is about the encoding, not the disk, which is why the kind is InvalidData rather than something from the OS. fs::read skips the check, so an image, an archive, or a payload in a fixed-width encoding all come back the same way. Both readers enforce the same rule — a String can only hold valid UTF-8. One refuses the bytes; the other hands them over.
Getting back to text
Section titled “Getting back to text”Bytes you’ve read can still be text — you only have to say so. String::from_utf8 takes the Vec<u8> and checks every byte; on failure the Vec comes back through into_bytes():
use std::fs;
fn main() { fs::write("latin1.txt", [b'c', b'a', b'f', 0xE9, b'\n']).unwrap(); let bytes = fs::read("latin1.txt").unwrap(); match String::from_utf8(bytes) { Ok(text) => println!("{text}"), Err(err) => { println!("{err}"); println!("{:?}", err.into_bytes()); } }}invalid utf-8 sequence of 1 bytes from index 3[99, 97, 102, 233, 10]The error names the byte where the text stops being valid — index 3, the lone 0xE9. from_utf8_lossy, str::from_utf8, and when to pick which are on the conversion page.
Related
Section titled “Related”- How to read a file into a String — the whole file in one call, and the UTF-8 check behind both pages
- How to read a file line by line — the buffered version, one line at a time
- How to convert a Vec of bytes to a String — the checked conversion in both directions
- Reading and writing files — the rest of the chapter