Sage-Code Laboratory
index<--

Rust File IO

In computing, a file is a structured or unstructured collection of data that is stored on a filesystem or in memory. Files can be used to persist data between different runs of a program, or to communicate data between different programs.

Page bookmarks

FS Module

In Rust, files are handled using the "std::fs" module, which provides functions for reading from and writing to files, as well as querying metadata such as file size and permissions.

Example

Here's a quick example of how to write some data to a file in Rust:

use std::fs::File;
use std::io::prelude::*;

fn main() -> std::io::Result<()> {
    let mut file = File::create("output.txt")?;
    file.write_all(b"Hello, world!")?;
    Ok(())
}

This program creates a new file called "output.txt" and writes the string "Hello, world!" to it using the "write_all()" method. Note that the "File::create()" method returns a "Result<File, std::io::Error>", which we can handle using Rust's built-in error handling facilities.

Reading data from a file is also straightforward, thanks to Rust's "io::BufReader" and "io::Read" traits. Here's an example that reads the contents of a file into a "String":


use std::fs::File;
use std::io::{BufReader, prelude::*};

fn main() -> std::io::Result<()> {
    let file = File::open("input.txt")?;
    let reader = BufReader::new(file);

    let mut contents = String::new();
    reader.read_to_string(&mut contents)?;

    println!("{}", contents);
    Ok(())
}

This program opens a file called "input.txt" and reads its contents into a "String" using the "read_to_string()" method. The "BufReader" is used to read the data in chunks, which can improve performance when reading large files.

Organize data

You can organize data using a database or using independent files. The choice between using files or databases to store data depends on the specific application requirements, the amount and complexity of the data, and the resources available for development and maintenance.

Using databases

Databases can offer more advanced features, such as querying, indexing, and data integrity checks, which can be especially useful for larger or more complex data sets.

Using files

Using files to store data can have some advantages over using a database, such as being lightweight and easy to set up, not requiring a separate server process, and providing more control over the data structure and file format.

Note: Storing data in files can be a simple and effective way to organize data, especially for small-scale applications or data sets. There are different file formats that can be used to store data, such as CSV, XML, JSON, or even custom formats.

Access methods

In Rust, there are a few different ways to access files:

std::fs::File

This provides a simple way to open a file and read its contents using Rust's built-in I/O functionality. Here is an example:

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

fn main() {
    let file = File::open("file.txt").unwrap();
    let reader = BufReader::new(file);

    for line in reader.lines() {
        println!("{}", line.unwrap());
    }
}

Advantages:

Disadvantages:

std::fs::read

This is a function that reads the contents of a file directly into a byte array. Here is an example:

use std::fs;

fn main() {
    let contents = fs::read("file.txt").unwrap();
    println!("{:?}", contents);
}

Advantages:

Disadvantages:

std::io::BufReader

This is a buffered reader that reads a file line by line, which can help improve performance by reducing the number of system calls made when reading data. Here is an example:

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

fn main() {
    let file = File::open("file.txt").unwrap();
    let reader = BufReader::new(file);

    for line in reader.lines() {
        println!("{}", line.unwrap());
    }
}

Advantages:

Disadvantages:

std::io::Read

This is a trait that provides basic read functionality, and can be implemented by any type that can be read from (e.g. a file, a network socket, etc.). Here is an example:

use std::fs::File;
use std::io::Read;

fn main() {
    let mut file = File::open("file.txt").unwrap();
    let mut contents = String::new();
    file.read_to_string(&mut contents).unwrap();
    println!("{}", contents);
}

Advantages:

Disadvantages:

In general, the choice of file access method depends on factors such as performance requirements, the size and complexity of the data being read or written, and the specific use case of the application. In many cases, a combination of these methods may provide the best balance of performance and flexibility.

JSON Files

JSON (JavaScript Object Notation) is a lightweight data interchange format. It is easy for humans to read and write, and easy for machines to parse and generate. It is commonly used for transmitting data between a server and web application, as an alternative to XML.

JSON Syntax

A JSON file is a collection of key-value pairs, separated by commas and enclosed in curly braces {}. Keys must be strings, while values can be strings, numbers, nested objects, or arrays. Here's an example:


{
    "name": "John",
    "age": 30,
    "address": {
      "street": "123 Main St",
      "city": "Anytown",
      "state": "CA",
      "zip": "12345"
    },
    "phone": [
      {
        "type": "home",
        "number": "555-555-5555"
      },
      {
        "type": "work",
        "number": "555-555-1234"
      }
    ]
}

JSON in Rust

Adding Serde and Serde JSON to Cargo.toml

  [dependencies]
  serde = { version = "1.0", features = ["derive"] }
  serde_json = "1.0"

Examples

To read and write data from/to a JSON file in Rust, there are several ways to achieve this, but the most common one is by using serde, a Rust crate for creating and parsing data formats.

Here's an example Rust code that demonstrates how to read and write data to a JSON file:


// Importing necessary modules
use serde::{Serialize, Deserialize};
use std::fs::File;
use std::io::prelude::*;

// Define structs to serialize
#[derive(Serialize, Deserialize, Debug)]
struct Person {
  name: String,
  age: i32,
  address: Address
}

#[derive(Serialize, Deserialize, Debug)]
struct Address {
  street: String,
  city: String,
  state: String,
  zip: String
}

fn main() -> std::io::Result<()> {
  // Create a Person object
  let person = Person {
      name: String::from("John"),
      age: 30,
      address: Address {
          street: String::from("123 Main St"),
          city: String::from("Anytown"),
          state: String::from("CA"),
          zip: String::from("12345")
      }
  };

  // Convert Person object to JSON string
  let serialized_person = serde_json::to_string(&person).unwrap();

  // Write JSON string to file
  let mut file = File::create("person.json")?;
  file.write_all(serialized_person.as_bytes())?;

  // Read JSON file
  let file = File::open("person.json")?;
  let mut contents = String::new();
  file.read_to_string(&mut contents)?;

  // Convert JSON string to Person object
  let deserialized_person: Person = serde_json::from_str(&contents).unwrap();

  // Print Person object
  println!("{:?}", deserialized_person);

  Ok(())
}

In this code, we have defined two Rust structs - Person and Address - that represent a person and their address. The #[derive(Serialize, Deserialize, Debug)] macro on top of each struct tells serde to generate code for serializing and deserializing the structs to and from JSON.

We then create a Person object and convert it to a JSON string using serde_json::to_string(). We then write the JSON string to a file using Rust's standard File and Write APIs.

To read the JSON string back from a file, we use the File and Read APIs to open the file and read its contents into a string. We then use serde_json::from_str() to deserialize the string back into a Person object.

Finally, we print the Person object to the console using Rust's standard println!() macro. The output in the console should be:

Person { 
    name: "John",
    age: 30,
    address: Address { 
      street: "123 Main St",
      city: "Anytown",
      state: "CA",
      zip: "12345"
    }
}

Advantages of JSON:

Disadvantages of JSON:

Fixed Records

Organizing Data in Rust with Structured Files using Fixed-Size Records

Fixed-sized record organization is a common way of organizing data in structured files. In Rust, we can define our own data structures with fixed-length fields, and then use these structures to read from and write to files on disk. In this example, we'll define an employee record with the following fields:

CRUD Operations

We'll now demonstrate how to create, read, update, and delete an employee record from a file using fixed-size records. For this example, we'll assume that the file is sorted by employee ID.

Create a New Record

To create a new record, we'll open the file in append mode, add the new record to the end of the file, and then close the file. Here's an example code in Rust:


use std::io::{Seek, SeekFrom, Write};
use std::fs::OpenOptions;

#[derive(Debug)]
struct Employee {
  id: u32,
  name: String,
  age: u16,
  salary: f32,
}

fn main() -> std::io::Result<()> {
  let employee = Employee {
      id: 100,
      name: String::from("Jane Doe"),
      age: 27,
      salary: 50000.0,
  };
  let mut file = OpenOptions::new()
      .append(true)
      .create(true)
      .open("employees.dat")?;
  file.write_all(&employee.id.to_ne_bytes())?;
  file.write_all(employee.name.as_bytes())?;
  file.write_all(&employee.age.to_ne_bytes())?;
  file.write_all(&employee.salary.to_ne_bytes())?;
  Ok(())
}

Read a Record

To read a record, we'll open the file in read mode, seek to the position of the record in the file, and then read the fixed-size record into our data structure. Here's an example code in Rust:


fn read_record(file: &mut std::fs::File, id: u32) -> std::io::Result<Employee> {
  let mut buffer = [0_u8; 40];
  file.seek(SeekFrom::Start((id - 1) as u64 * 40))?;
  file.read_exact(&mut buffer)?;
  let name_end = buffer
      .iter()
      .position(|&x| x == 0)
      .unwrap_or(buffer.len());
  let employee = Employee {
      id: u32::from_ne_bytes([buffer[0], buffer[1], buffer[2], buffer[3]]),
      name: String::from_utf8_lossy(&buffer[4..name_end]).trim().to_string(),
      age: u16::from_ne_bytes([buffer[34], buffer[35]]),
      salary: f32::from_ne_bytes([
          buffer[36], buffer[37], buffer[38], buffer[39],
      ]),
  };
  Ok(employee)
}

Update a Record

To update a record, we'll open the file in read-write mode, seek to the position of the record in the file, and then write the updated record back. Here's an example code in Rust:


fn update_record(file: &mut std::fs::File, id: u32, employee: &Employee) -> std::io::Result<()> {
  file.seek(SeekFrom::Start((id - 1) as u64 * 40))?;
  file.write_all(&employee.id.to_ne_bytes())?;
  let mut name_bytes = vec![0_u8; 30];
  name_bytes[..employee.name.len()].copy_from_slice(employee.name.as_bytes());
  file.write_all(&name_bytes)?;
  file.write_all(&employee.age.to_ne_bytes())?;
  file.write_all(&employee.salary.to_ne_bytes())?;
  Ok(())
}

Delete a Record

To delete a record, we'll open the file in read-write mode, move all the records after it one position back in the file, and then truncate the file to remove the last record. Here's an example code in Rust:


fn delete_record(file: &mut std::fs::File, id: u32, total_records: u32) -> std::io::Result<()> {
  let last_id = total_records;
  let mut buffer = vec![0_u8; 40];
  for i in id..last_id {
      file.seek(SeekFrom::Start((i as u64) * 40))?;
      file.read_exact(&mut buffer)?;
      file.seek(SeekFrom::Start(((i - 1) as u64) * 40))?;
      file.write_all(&buffer)?;
  }
  file.set_len(((last_id - 1) * 40) as u64)?;
  Ok(())
}

Advantages of Fixed-Size Records

Fixed-size records have several advantages over variable-length records. It's worth noting the following:

Advantages of fixed-size records:

Disadvantages of fixed-size records:

When you organize data you have these two popular choices. If you don't use a database, you can yet store your data in files.

Conclusion:

In conclusion, both fixed-size records and JSON have their own advantages and disadvantages, and the choice between them depends on the specific data requirements and use cases of a project. Fixed-size records are well-suited for data with a defined and unchanging schema, while JSON is more flexible and versatile. It ultimately comes down to the trade-off between performance and flexibility.


Read next: Frameworks