Hone logo
Hone
Problems

Robust Resource Management with Strong Exception Guarantees in Rust

Rust's ownership system and borrowing rules provide memory safety without garbage collection. However, dealing with external resources (files, network connections, mutexes) and ensuring they are properly cleaned up even in the face of panics (Rust's equivalent of exceptions) can be tricky. This challenge focuses on implementing a mechanism to guarantee that resources are released, regardless of how a function exits, mimicking a "strong exception guarantee."

Problem Description

You are tasked with creating a function process_file that safely opens, processes, and closes a file. The function should take a file path as input and return a Result indicating success or failure. Crucially, even if an error occurs during file processing (e.g., a panic during reading or writing), the file must always be closed. You must use Rust's RAII (Resource Acquisition Is Initialization) principles and the Drop trait to achieve this strong exception guarantee.

The process_file function should:

  1. Attempt to open the file at the given path in read-write mode.
  2. If the file opening fails, return an appropriate Err variant.
  3. If the file opens successfully, wrap the File object in a custom struct called ScopedFile. This struct will implement the Drop trait.
  4. Inside the ScopedFile's drop implementation, ensure the file is closed using file.flush().
  5. Read the entire contents of the file into a String.
  6. If reading fails, return an appropriate Err variant.
  7. If reading succeeds, convert the String to uppercase and return it wrapped in an Ok variant.

Examples

Example 1:

Input: "test.txt" (where test.txt contains "hello world")
Output: Ok("HELLO WORLD")
Explanation: The file is opened, read, converted to uppercase, and then closed.

Example 2:

Input: "nonexistent_file.txt"
Output: Err(std::io::Error)
Explanation: The file cannot be opened, so an `Err` is returned, and no file is left open.

Example 3: (Edge Case - Panic during processing)

Input: "test.txt" (where test.txt contains "hello world")
Output: Err(std::io::Error) // Assuming a panic during the uppercase conversion results in an error.
Explanation: The file is opened, read, but a panic occurs during the uppercase conversion. The `Drop` implementation of `ScopedFile` *still* closes the file before the panic propagates.

Constraints

  • The file path will be a string slice (&str).
  • The function must handle potential std::io::Errors during file opening and reading.
  • The process_file function must return a Result<String, std::io::Error>.
  • The Drop implementation in ScopedFile must call file.flush() to ensure the file is properly closed and any buffered data is written.
  • Assume the uppercase conversion can potentially panic. The strong exception guarantee must hold regardless.

Notes

  • Rust's RAII is key to solving this problem. The Drop trait allows you to define cleanup logic that is automatically executed when an object goes out of scope, even if a panic occurs.
  • Consider using Result to handle potential errors gracefully.
  • The ScopedFile struct is a crucial part of the solution. It encapsulates the File object and provides a place to implement the Drop trait.
  • Think about how to ensure the file is closed even if a panic occurs during the processing step (uppercase conversion). The Drop implementation is your safety net.
use std::fs::File;
use std::io::{self, Read};

struct ScopedFile {
    file: File,
}

impl ScopedFile {
    fn new(file: File) -> Self {
        ScopedFile { file }
    }
}

impl Drop for ScopedFile {
    fn drop(&mut self) {
        if let Err(e) = self.file.flush() {
            eprintln!("Error flushing file: {}", e); // Log the error, but don't panic.
        }
    }
}

fn process_file(path: &str) -> Result<String, io::Error> {
    let file = File::options().read(true).write(true).open(path)?;
    let mut scoped_file = ScopedFile::new(file);

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

    let uppercase_contents = contents.to_uppercase();

    // Simulate a potential panic during processing.  Remove this in a real application.
    // if path == "test.txt" {
    //     panic!("Simulated panic during processing!");
    // }

    Ok(uppercase_contents)
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use std::io::Write;
    use tempfile::NamedTempFile;

    #[test]
    fn test_process_file_success() -> Result<(), io::Error> {
        let mut temp_file = NamedTempFile::new()?;
        write!(temp_file, "hello world")?;
        let path = temp_file.path().to_str().unwrap();
        let result = process_file(path)?;
        assert_eq!(result, "HELLO WORLD");
        temp_file.close(); // Explicitly close the file handle.
        Ok(())
    }

    #[test]
    fn test_process_file_failure() -> Result<(), io::Error> {
        let path = "nonexistent_file.txt";
        let result = process_file(path);
        assert!(result.is_err());
        Ok(())
    }

    #[test]
    fn test_process_file_panic() -> Result<(), io::Error> {
        let mut temp_file = NamedTempFile::new()?;
        write!(temp_file, "hello world")?;
        let path = temp_file.path().to_str().unwrap();

        let result = std::panic::catch_unwind(|| process_file(path));
        assert!(result.is_err());

        // Verify that the file was closed even after the panic.
        // This is difficult to verify directly without more complex setup.
        // The key is that no file handles are leaked.

        temp_file.close();
        Ok(())
    }
}
Loading editor...
rust