File System Operations

read_to_string()

Rust provides read_to_string in three places in the standard library—all of which read an input source until End-of-File (EOF) and validate that the bytes are valid UTF-8:

  1. std::fs::read_to_string(path) -> io::Result<String>: Opens a file at path, pre-allocates a String matching the file's metadata byte length, reads the entire file, and closes it.
  2. std::io::read_to_string(reader) -> io::Result<String>: Takes any R: Read (such as io::stdin() or a TcpStream / Take<File>) and returns a newly allocated String.
  3. Read::read_to_string(&mut self, buf: &mut String) -> io::Result<usize>: Method on any Read implementor that appends the read UTF-8 text onto an existing String buffer (reusing its capacity) and returns the number of bytes appended.

Comparing the Three read_to_string APIs

Function / Method Input Argument Allocation Behavior Error Behavior on Non-UTF-8 Bytes
fs::read_to_string(path) P: AsRef<Path> Queries file.metadata().len() to pre-allocate exact String capacity in 1 allocation Returns Err(ErrorKind::InvalidData)
io::read_to_string(reader) R: Read Allocates and grows a new String Returns Err(ErrorKind::InvalidData)
reader.read_to_string(&mut buf) &mut self, buf: &mut String Appends into caller's buf (can reuse buf across files after buf.clear()!) Returns Err(ErrorKind::InvalidData) and leaves buf unmodified!

Notice the strong exception-safety guarantee of reader.read_to_string(&mut buf): if the stream contains invalid UTF-8 bytes or encounters an I/O error mid-read, any partial bytes read during that call are rolled back so buf retains the exact valid UTF-8 string it had before the call!

use std::fs;
use std::io::{self, Read};

fn main() -> io::Result<()> {
    let path_a = std::env::temp_dir().join("rust_ch50_rts_a.txt");
    let path_b = std::env::temp_dir().join("rust_ch50_rts_b.txt");

    fs::write(&path_a, "First file content.\n")?;
    fs::write(&path_b, "Second file content.\n")?;

    // 1. One-shot `std::fs::read_to_string(path)`:
    let one_shot: String = fs::read_to_string(&path_a)?;
    print!("one_shot: {}", one_shot);

    // 2. Reusing a single `String` buffer across multiple files with `Read::read_to_string`:
    let mut reusable_buf = String::with_capacity(256);
    for path in [&path_a, &path_b] {
        reusable_buf.clear();
        fs::File::open(path)?.read_to_string(&mut reusable_buf)?;
        println!(
            "read {} bytes into reusable_buf (capacity stays {} B): {:?}",
            reusable_buf.len(),
            reusable_buf.capacity(),
            reusable_buf.trim()
        );
    }

    fs::remove_file(&path_a)?;
    fs::remove_file(&path_b)?;
    Ok(())
}
one_shot: First file content.
read 20 bytes into reusable_buf (capacity stays 256 B): "First file content."
read 21 bytes into reusable_buf (capacity stays 256 B): "Second file content."