3

I'm trying to do something such as the following:

FILE* f = fopen_unless_exists("example.txt");

if (f != NULL) {
    fprintf(f, "foo bar baz\n");
} else {
    // f should be NULL if example.txt already exists
    fprintf(stderr, "Error: cannot write to file or file already exists");
}

I could of course use one of the methods mentioned in a related question, but as stated in the comments on the answers there, this would be a race condition (specifically, TOCTOU).

What's the simplest way to safely create and write to a file, unless the file already exists, without creating a race condition?

Community
  • 1
  • 1
tckmn
  • 57,719
  • 27
  • 114
  • 156
  • 1
    use open() with O_EXCL flag. O_EXCL Ensure that this call creates the file: if this flag is specified in conjunction with O_CREAT, and pathname already exists, then open() will fail. – Alon Jul 29 '15 at 02:58
  • @Alon `O_EXCL` is not portable. – Dai Jul 29 '15 at 02:59

3 Answers3

1

You have to use the open(2) syscall with O_EXCL|O_CREAT|O_WRONLY and then call fdopen(3) on that descriptor.

#include <sys/errno.h>
#include <fcntl.h>

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[]) {
    char *program  = argv[0];
    char *filename = argv[1];

    if (argc != 2) {
        fprintf(stderr, "%s: expected a single file argument.\n", program);
        exit(1);
    }   

    int flags = O_EXCL|O_CREAT|O_WRONLY;
    int mode  = 0666;

    int fd; 
    if ((fd = open(filename, flags, mode)) == -1) {
        fprintf(stderr, "%s: cannot open %s with flags 0x%04X: %s\n",
            program, filename, flags, strerror(errno));
        exit(2);
    }   

    FILE *fp = fdopen(fd, "w");
    if (fp == NULL) {
        fprintf(stderr, "%s: cannot fdopen file descriptor %d: %s\n",
            program, fd, strerror(errno));
        exit(3);
    }   

    if (fprintf(fp, "12345\n") == EOF) {
        fprintf(stderr, "%s: cannot write to %s: %s\n",
            program, filename, strerror(errno));
        exit(4);
    }   

    if (fclose(fp) == EOF) {
        fprintf(stderr, "%s: cannot close %s: %s\n",
            program, filename, strerror(errno));
        exit(5);
    }   

    exit(0);
}

Those open(2) flags are amongst the few guaranteed by POSIX.

This is not guaranteed to work reliably on remote filesystems. In particular, NFS breaks the POSIX rules about atomic creats. [sic]

tchrist
  • 78,834
  • 30
  • 123
  • 180
0

There is no cross-platform, portable way to do this in pure C as C's standard library lacks advanced IO operations, such as file-locking, nor a systemwide mutex library. C doesn't even have a "list directories" function.

I feel the best solution would be to handle the error condition by prompting the user to intervene and let them choose how to proceed (e.g. delete the file, force overwrite, etc).

A more fully-featured IO library is present in C++11 (...finally) but that would mean abandoning pure-C. Another alternative is to use a cross-platform wrapper library that wraps platform-specific IO functions.

Dai
  • 141,631
  • 28
  • 261
  • 374
  • 1
    This would make sense, but would still require two separate syscalls (`if (fileexists(foo)) { prompt... } else { fprintf(foo, bar); }` or something like that), which still makes a race condition possible. How can I do this without creating a race condition? – tckmn Jul 29 '15 at 03:07
  • @Doorknob No, every (modern) platform does offer an atomic file-locking API and you can use them from C, but there's just no way to do it from within portable-C. – Dai Jul 29 '15 at 03:09
  • Okay, could you provide a link or some kind of resource that shows how to do file locking cross-platform? Thanks! – tckmn Jul 29 '15 at 03:11
  • @JonathanLeffler sorry, I was a bit too zealous. I've changed my assertion to refer to C's lack of directory-handling functions. – Dai Jul 29 '15 at 03:29
  • C11 has the `x` mode character mentioned in @NZD's answer. – cremno Jul 29 '15 at 05:01
0

If you use the GNU C library, then the call fopen("filename", "xw+") probably does what you want. The string x does the following:

x: Open the file exclusively (like the O_EXCL flag of open(2)). If the file already exists, fopen() fails, and sets errno to EEXIST. This flag is ignored for fdopen().

The function of the other option is:

w+: Open for reading and writing. The file is created if it does not exist, otherwise it is truncated. The stream is positioned at the beginning of the file.

You can also use:

a: Open for appending (writing at end of file). The file is created if it does not exist. The stream is positioned at the end of the file.

a+: Open for reading and appending (writing at end of file). The file is created if it does not exist. The initial file position for reading is at the beginning of the file, but output is always appended to the end of the file.

There are even more options. See fopen for details

NZD
  • 1,780
  • 2
  • 20
  • 29