Bad file descriptor with `std::io::Read`

#1

I am quite new to Rust’s File I/O system.
With this snippet I am trying to append a String to a ‘.gitignore’ file:

pub fn add(path: &mut PathBuf, item: &mut String) -> Result<(), String> {
    path.push(".gitignore");

    let file_options = OpenOptions::new()
        .append(true)
        .open(path.clone());

    match file_options {
        Ok(mut file) => {
            match file.read_to_string(item) {
                Ok(_) => Ok(()),
                Err(e) => {
                    println!("{}", path.to_str().unwrap());
                    Err(e.to_string())
                }
            }
        },
        Err(e) => Err(e.to_string())
    }
}

However, this results in an error with the following output:

<path to the .gitignore file>
Bad file descriptor (os error 9)

The path is valid, I tried to open the path with nano, no problem. Why does my program throw a Bad file descriptor error?

0 Likes

#2

You aren’t asking for read permissions when opening it.

1 Like

#3

Hmm, I’m not very experienced with file I/O in rust either, but this seems like it should work.


Taking a look at this example, on windows it produces a nicer error:

.\.gitignore
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: "Access is denied. (os error 5)"', libcore\result.rs:1009:5

and on WSL, it produces an error like the one you posted:

./.gitignore
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: "Bad file descriptor (os error 9)"', src/libcore/result.rs:1009:5

Ah, from this we can presume that you don’t have read permission, which you’re trying to do, which when we change your file options to the following:

let file_options = OpenOptions::new()
    .append(true)
    .read(true)
    .open(path.clone());

It works!

0 Likes

#4

Thanks OptimisticPeach and sfackler!

Adding read permissions does solve the problem.

I am still thinking a bit too C++'ish. So I presumed I needed to read the file first, and then write to it. I actually just wanted to append some text to the .gitignore file, so I ended up with this:

pub fn add(path: &mut PathBuf, item: &mut String) -> Result<(), String> {
    path.push(".gitignore");

    let file_options = OpenOptions::new()
        .append(true)
        .open(path);

    match file_options {
        Ok(mut file) => {
                match file.write(item.as_bytes()) {
                    Ok(_) => Ok(()),
                    Err(e) => Err(e.to_string())
                }
        },
        Err(e) => Err(e.to_string())
    }
}

I read the Rust Docs a bit too uncarefully, thought the append option would imply read and write permissions :sweat_smile:
I probably am going to do some file reading with my current project, so this is a very useful lesson for me. Thanks again! :grinning:

1 Like

#5

I’m generally partial to read-write-rename over append mode, for two reasons.

First: modifying a file in place is a lot more likely to corrupt the file if your program is terminated mid-write by an oomkill, power failure, or some other unavoidable outside problem. Individual writes are not atomic, and any portion, including the least convenient, may actually be committed to disk in that situation. With gitignore this isn’t so bad, as the file is text and the user can easily repair the damage, but it is avoidable and it’s good practice for when it’s more urgent.

Second: the “append” file mode is usually implemented by the OS to behave quite differently from opening in write mode and seeking to the end. In particular, each write occurs at the end of the file regardless of attempts to seek backwards, and taking writes by other programs into account invisibly. (It’s largely meant for logs.) This degree of OS magic can be surprising and is often counterintuitive. If it works for you, great - but be sure you understand what you’re asking the OS to do.

Instead, consider reading in the whole file, modifying it in memory, writing it out to a temporary file in the same directory, and renaming the file over the original. Renames within a filesystem are generally atomic, ensuring that no program sees a partial change - even if your power goes out.

2 Likes

#6

I did not know that. Thanks! I probably end up doing that. But does appending files with ‘append-mode’ have any advantages in your opinion?

0 Likes

#7

But what should happen if the file is waaaay too large, I.E. a 20gb movie file? It’d be illogical to load 20gb into memory and edit it there?

0 Likes