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
- Understand file operations using std::fs and File types.
- Master Read and Write traits for flexible I/O operations.
- Learn buffered I/O for better performance.
- Handle file paths, directories, and metadata.
- Implement proper error handling for file operations.
- Work with different file formats and encodings.
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
- Use
Result
types for all file operations - Handle specific error kinds when appropriate
- Provide meaningful error messages
2. Use Buffered I/O for Performance
- Use
BufReader
for reading large files - Use
BufWriter
for writing many small pieces - Call
flush()
when necessary
3. Handle Resource Cleanup
- Files are automatically closed when dropped
- Use temporary files for atomic operations
- Consider using
std::fs::remove_dir_all
for cleanup
4. Cross-Platform Considerations
- Use
Path
andPathBuf
for path handling - Be aware of platform-specific features
- Test on different operating systems
Summary
File I/O in Rust provides safe, efficient file operations:
- std::fs: High-level file system operations
- File, Read, Write: Core traits for file I/O
- Buffered I/O: Better performance for large operations
- Error handling: Robust error management with Result types
- Metadata: Access file information and permissions
- Directory operations: Create, list, and traverse directories
- Cross-platform: Works consistently across operating systems
Always handle errors appropriately and choose the right I/O approach based on your performance and safety requirements.