Rust - File I/O

Overview

Estimated time: 40–50 minutes

Master file input/output operations in Rust using the standard library. Learn about File operations, Read and Write traits, buffered I/O, directory operations, and robust error handling for file systems.

Learning Objectives

Prerequisites

Basic File Operations

Reading Files

use std::fs;
use std::io::Result;

fn read_file_examples() -> Result<()> {
    // Method 1: Read entire file to String
    let contents = fs::read_to_string("example.txt")?;
    println!("File contents:\n{}", contents);
    
    // Method 2: Read file as bytes
    let bytes = fs::read("example.txt")?;
    println!("File size: {} bytes", bytes.len());
    
    // Method 3: Using File directly
    use std::fs::File;
    use std::io::Read;
    
    let mut file = File::open("example.txt")?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    println!("Read using File: {} characters", contents.len());
    
    Ok(())
}

fn main() -> Result<()> {
    // Create a sample file first
    fs::write("example.txt", "Hello, Rust!\nThis is a test file.\nWith multiple lines.")?;
    
    read_file_examples()?;
    
    // Clean up
    fs::remove_file("example.txt")?;
    
    Ok(())
}

Expected Output:

File contents:
Hello, Rust!
This is a test file.
With multiple lines.
File size: 50 bytes
Read using File: 50 characters

Writing Files

use std::fs::{File, OpenOptions};
use std::io::{Result, Write};

fn write_file_examples() -> Result<()> {
    // Method 1: Write entire string to file (overwrites)
    std::fs::write("output1.txt", "Hello from Rust!")?;
    
    // Method 2: Using File and Write trait
    let mut file = File::create("output2.txt")?;
    file.write_all(b"Binary data: ")?;
    file.write_all(&[65, 66, 67, 68])?; // ASCII: ABCD
    file.write_all(b"\n")?;
    
    // Method 3: Appending to file
    let mut file = OpenOptions::new()
        .create(true)
        .append(true)
        .open("output3.txt")?;
    
    writeln!(file, "First line")?;
    writeln!(file, "Second line")?;
    writeln!(file, "Third line")?;
    
    // Method 4: Writing with specific options
    let mut file = OpenOptions::new()
        .create(true)
        .write(true)
        .truncate(true)
        .open("output4.txt")?;
    
    write!(file, "Formatted number: {}\n", 42)?;
    write!(file, "Formatted float: {:.2}\n", 3.14159)?;
    
    Ok(())
}

fn main() -> Result<()> {
    write_file_examples()?;
    
    // Read back and display the files
    println!("output1.txt: {}", std::fs::read_to_string("output1.txt")?);
    println!("output2.txt: {}", std::fs::read_to_string("output2.txt")?);
    println!("output3.txt: {}", std::fs::read_to_string("output3.txt")?);
    println!("output4.txt: {}", std::fs::read_to_string("output4.txt")?);
    
    // Clean up
    for file in &["output1.txt", "output2.txt", "output3.txt", "output4.txt"] {
        let _ = std::fs::remove_file(file);
    }
    
    Ok(())
}

Expected Output:

output1.txt: Hello from Rust!
output2.txt: Binary data: ABCD
output3.txt: First line
Second line
Third line
output4.txt: Formatted number: 42
Formatted float: 3.14

Buffered I/O for Performance

use std::fs::File;
use std::io::{BufRead, BufReader, BufWriter, Result, Write};
use std::time::Instant;

fn create_large_file() -> Result<()> {
    let mut file = File::create("large_file.txt")?;
    for i in 0..10000 {
        writeln!(file, "Line number: {}", i)?;
    }
    Ok(())
}

fn read_lines_unbuffered() -> Result<usize> {
    use std::io::Read;
    
    let mut file = File::open("large_file.txt")?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    
    let line_count = contents.lines().count();
    Ok(line_count)
}

fn read_lines_buffered() -> Result<usize> {
    let file = File::open("large_file.txt")?;
    let reader = BufReader::new(file);
    
    let line_count = reader.lines().count();
    Ok(line_count)
}

fn write_buffered_example() -> Result<()> {
    let file = File::create("buffered_output.txt")?;
    let mut writer = BufWriter::new(file);
    
    for i in 0..1000 {
        writeln!(writer, "Buffered line: {}", i)?;
    }
    
    // Important: flush to ensure all data is written
    writer.flush()?;
    
    Ok(())
}

fn main() -> Result<()> {
    create_large_file()?;
    
    // Benchmark unbuffered reading
    let start = Instant::now();
    let lines1 = read_lines_unbuffered()?;
    let unbuffered_time = start.elapsed();
    
    // Benchmark buffered reading
    let start = Instant::now();
    let lines2 = read_lines_buffered()?;
    let buffered_time = start.elapsed();
    
    println!("Unbuffered: {} lines in {:?}", lines1, unbuffered_time);
    println!("Buffered: {} lines in {:?}", lines2, buffered_time);
    
    write_buffered_example()?;
    
    // Clean up
    let _ = std::fs::remove_file("large_file.txt");
    let _ = std::fs::remove_file("buffered_output.txt");
    
    Ok(())
}

Expected Output:

Unbuffered: 10000 lines in 2.345ms
Buffered: 10000 lines in 1.123ms

Line-by-Line Processing

use std::fs::File;
use std::io::{BufRead, BufReader, Result};

fn process_csv_file() -> Result<()> {
    // Create sample CSV file
    let csv_content = "name,age,city\nAlice,30,New York\nBob,25,Los Angeles\nCharlie,35,Chicago";
    std::fs::write("data.csv", csv_content)?;
    
    let file = File::open("data.csv")?;
    let reader = BufReader::new(file);
    
    let mut lines = reader.lines();
    
    // Read header
    if let Some(header_line) = lines.next() {
        let header = header_line?;
        println!("Header: {}", header);
        
        let columns: Vec<&str> = header.split(',').collect();
        println!("Columns: {:?}", columns);
    }
    
    // Process data rows
    for (index, line) in lines.enumerate() {
        let line = line?;
        let fields: Vec<&str> = line.split(',').collect();
        
        if fields.len() >= 3 {
            println!("Record {}: Name={}, Age={}, City={}", 
                     index + 1, fields[0], fields[1], fields[2]);
        }
    }
    
    Ok(())
}

fn filter_log_file() -> Result<()> {
    // Create sample log file
    let log_content = "[INFO] Application started\n[ERROR] Connection failed\n[DEBUG] Processing request\n[ERROR] Database timeout\n[INFO] Request completed";
    std::fs::write("app.log", log_content)?;
    
    let file = File::open("app.log")?;
    let reader = BufReader::new(file);
    
    println!("Filtering ERROR messages:");
    for line in reader.lines() {
        let line = line?;
        if line.contains("[ERROR]") {
            println!("  {}", line);
        }
    }
    
    Ok(())
}

fn main() -> Result<()> {
    process_csv_file()?;
    println!();
    filter_log_file()?;
    
    // Clean up
    let _ = std::fs::remove_file("data.csv");
    let _ = std::fs::remove_file("app.log");
    
    Ok(())
}

Expected Output:

Header: name,age,city
Columns: ["name", "age", "city"]
Record 1: Name=Alice, Age=30, City=New York
Record 2: Name=Bob, Age=25, City=Los Angeles
Record 3: Name=Charlie, Age=35, City=Chicago

Filtering ERROR messages:
  [ERROR] Connection failed
  [ERROR] Database timeout

Directory Operations

use std::fs;
use std::io::Result;
use std::path::Path;

fn directory_operations() -> Result<()> {
    // Create directory
    fs::create_dir("test_dir")?;
    println!("Created directory: test_dir");
    
    // Create nested directories
    fs::create_dir_all("test_dir/nested/deep")?;
    println!("Created nested directories");
    
    // Create some files
    fs::write("test_dir/file1.txt", "Content of file 1")?;
    fs::write("test_dir/file2.txt", "Content of file 2")?;
    fs::write("test_dir/nested/file3.txt", "Content of file 3")?;
    
    // List directory contents
    println!("\nContents of test_dir:");
    for entry in fs::read_dir("test_dir")? {
        let entry = entry?;
        let path = entry.path();
        let metadata = entry.metadata()?;
        
        if metadata.is_file() {
            println!("  File: {} ({} bytes)", 
                     path.display(), metadata.len());
        } else if metadata.is_dir() {
            println!("  Directory: {}", path.display());
        }
    }
    
    // Recursive directory walking
    println!("\nAll files recursively:");
    walk_dir("test_dir")?;
    
    Ok(())
}

fn walk_dir(dir: &str) -> Result<()> {
    let path = Path::new(dir);
    if path.is_dir() {
        for entry in fs::read_dir(path)? {
            let entry = entry?;
            let path = entry.path();
            
            if path.is_dir() {
                walk_dir(&path.to_string_lossy())?;
            } else {
                println!("  {}", path.display());
            }
        }
    }
    Ok(())
}

fn main() -> Result<()> {
    directory_operations()?;
    
    // Clean up
    fs::remove_dir_all("test_dir")?;
    println!("\nCleaned up test directory");
    
    Ok(())
}

Expected Output:

Created directory: test_dir
Created nested directories

Contents of test_dir:
  File: test_dir/file1.txt (17 bytes)
  File: test_dir/file2.txt (17 bytes)
  Directory: test_dir/nested

All files recursively:
  test_dir/file1.txt
  test_dir/file2.txt
  test_dir/nested/file3.txt

Cleaned up test directory

File Metadata and Permissions

use std::fs::{self, File, Permissions};
use std::io::Result;
use std::os::unix::fs::PermissionsExt; // Unix-specific
use std::time::SystemTime;

fn file_metadata_example() -> Result<()> {
    // Create a test file
    fs::write("metadata_test.txt", "This is a test file for metadata exploration.")?;
    
    let metadata = fs::metadata("metadata_test.txt")?;
    
    println!("File metadata:");
    println!("  Size: {} bytes", metadata.len());
    println!("  Is file: {}", metadata.is_file());
    println!("  Is directory: {}", metadata.is_dir());
    println!("  Is symlink: {}", metadata.is_symlink());
    println!("  Read-only: {}", metadata.permissions().readonly());
    
    // Timestamps
    if let Ok(created) = metadata.created() {
        println!("  Created: {:?}", created);
    }
    
    if let Ok(modified) = metadata.modified() {
        println!("  Modified: {:?}", modified);
    }
    
    if let Ok(accessed) = metadata.accessed() {
        println!("  Accessed: {:?}", accessed);
    }
    
    // Unix-specific permissions (comment out on Windows)
    #[cfg(unix)]
    {
        let mode = metadata.permissions().mode();
        println!("  Permissions: {:o}", mode & 0o777);
    }
    
    Ok(())
}

#[cfg(unix)]
fn change_permissions() -> Result<()> {
    fs::write("permission_test.txt", "Test file for permissions")?;
    
    // Make file read-only
    let mut perms = fs::metadata("permission_test.txt")?.permissions();
    perms.set_readonly(true);
    fs::set_permissions("permission_test.txt", perms)?;
    
    println!("Set file to read-only");
    
    // Try to write (should fail)
    match fs::write("permission_test.txt", "New content") {
        Ok(_) => println!("Write succeeded (unexpected)"),
        Err(e) => println!("Write failed as expected: {}", e),
    }
    
    // Set specific Unix permissions
    let perms = Permissions::from_mode(0o644); // rw-r--r--
    fs::set_permissions("permission_test.txt", perms)?;
    
    println!("Set permissions to 644");
    
    Ok(())
}

fn main() -> Result<()> {
    file_metadata_example()?;
    
    #[cfg(unix)]
    {
        println!();
        change_permissions()?;
        let _ = fs::remove_file("permission_test.txt");
    }
    
    let _ = fs::remove_file("metadata_test.txt");
    
    Ok(())
}

Error Handling Patterns

use std::fs::File;
use std::io::{Error, ErrorKind, Result};

fn robust_file_operations() {
    // Pattern 1: Handle specific error types
    match File::open("nonexistent.txt") {
        Ok(_) => println!("File opened successfully"),
        Err(e) => match e.kind() {
            ErrorKind::NotFound => println!("File not found"),
            ErrorKind::PermissionDenied => println!("Permission denied"),
            ErrorKind::InvalidData => println!("Invalid data"),
            _ => println!("Other error: {}", e),
        }
    }
    
    // Pattern 2: Retry logic
    let mut attempts = 0;
    let max_attempts = 3;
    
    loop {
        attempts += 1;
        match File::open("might_exist.txt") {
            Ok(_) => {
                println!("File opened on attempt {}", attempts);
                break;
            }
            Err(e) if attempts < max_attempts => {
                println!("Attempt {} failed: {}", attempts, e);
                std::thread::sleep(std::time::Duration::from_millis(100));
            }
            Err(e) => {
                println!("All attempts failed: {}", e);
                break;
            }
        }
    }
}

fn safe_file_copy(source: &str, dest: &str) -> Result<u64> {
    use std::io::copy;
    
    let mut source_file = File::open(source)?;
    let mut dest_file = File::create(dest)?;
    
    let bytes_copied = copy(&mut source_file, &mut dest_file)?;
    
    // Ensure data is written to disk
    dest_file.sync_all()?;
    
    Ok(bytes_copied)
}

fn atomic_write(filename: &str, content: &str) -> Result<()> {
    let temp_filename = format!("{}.tmp", filename);
    
    // Write to temporary file first
    std::fs::write(&temp_filename, content)?;
    
    // Atomically rename (on most systems)
    std::fs::rename(&temp_filename, filename)?;
    
    Ok(())
}

fn main() -> Result<()> {
    robust_file_operations();
    
    // Test safe file copy
    std::fs::write("source.txt", "Content to copy")?;
    
    match safe_file_copy("source.txt", "destination.txt") {
        Ok(bytes) => println!("Copied {} bytes", bytes),
        Err(e) => println!("Copy failed: {}", e),
    }
    
    // Test atomic write
    match atomic_write("atomic_test.txt", "Atomically written content") {
        Ok(_) => println!("Atomic write succeeded"),
        Err(e) => println!("Atomic write failed: {}", e),
    }
    
    // Clean up
    let _ = std::fs::remove_file("source.txt");
    let _ = std::fs::remove_file("destination.txt");
    let _ = std::fs::remove_file("atomic_test.txt");
    
    Ok(())
}

Expected Output:

File not found
Attempt 1 failed: No such file or directory (os error 2)
Attempt 2 failed: No such file or directory (os error 2)
All attempts failed: No such file or directory (os error 2)
Copied 15 bytes
Atomic write succeeded

Working with JSON Files

Add to Cargo.toml:

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::io::{BufReader, BufWriter, Result};

#[derive(Serialize, Deserialize, Debug)]
struct Person {
    name: String,
    age: u32,
    email: String,
    active: bool,
}

#[derive(Serialize, Deserialize, Debug)]
struct Database {
    version: String,
    users: Vec<Person>,
}

fn json_file_operations() -> Result<()> {
    // Create sample data
    let database = Database {
        version: "1.0".to_string(),
        users: vec![
            Person {
                name: "Alice".to_string(),
                age: 30,
                email: "[email protected]".to_string(),
                active: true,
            },
            Person {
                name: "Bob".to_string(),
                age: 25,
                email: "[email protected]".to_string(),
                active: false,
            },
        ],
    };
    
    // Write JSON to file (pretty printed)
    let file = File::create("database.json")?;
    let writer = BufWriter::new(file);
    serde_json::to_writer_pretty(writer, &database)?;
    
    println!("Written database to JSON file");
    
    // Read JSON from file
    let file = File::open("database.json")?;
    let reader = BufReader::new(file);
    let loaded_database: Database = serde_json::from_reader(reader)?;
    
    println!("Loaded database: {:?}", loaded_database);
    
    // Process the data
    for user in &loaded_database.users {
        if user.active {
            println!("Active user: {} ({})", user.name, user.email);
        }
    }
    
    Ok(())
}

fn main() -> Result<()> {
    json_file_operations()?;
    
    let _ = std::fs::remove_file("database.json");
    
    Ok(())
}

Best Practices

1. Always Handle Errors

2. Use Buffered I/O for Performance

3. Handle Resource Cleanup

4. Cross-Platform Considerations

Summary

File I/O in Rust provides safe, efficient file operations:

Always handle errors appropriately and choose the right I/O approach based on your performance and safety requirements.


← PreviousNext →