Some types of files are consumed with mmap(), and partially cached in the memory of a process. For such files, if another program (e.g. 'cp' or 'install') copies new data into the file, the process may crash (by SIGBUS or SIGSEGV), simply because that program assumes that the contents of the file does not change.
There are many such file types: - executables, - shared libraries, - .mo files, - ... Writing into an executable file while it is being executed produces an error cp: cannot create regular file 'program': Text file busy For shared libraries, no such protection exists. You may get a "controlled" crash like this: ./program: relocation error: ./program: symbol acl_get_file, version ACL_1.0 not defined in file libacl.so.1 with link time reference For .mo files, no such protection exists either, and it has been reported that a program that uses gettext() crashes when the .mo file is overwritten while it is in use. The fix against such crashes is to not *overwrite* the file with new data, but to *supersede* it. (*) That is, you create the new file's contents in a new inode, and then make the new inode appear in the place of the existing file name. That's even possible atomically, through the 'rename()' function. The new module 'supersede' makes it easy to supersede a file. I'll be using it in GNU msgfmt for .mo files. Interestingly, the 'build-aux/install-sh' uses this technique to install files, whereas the GNU coreutils 'install' program uses a different technique: it first does unlink() (or renameat2() if the option '-b' was specified) and then open()s the file for writing. Thus it is not atomic: For some period of time, the file does not exist under the given name, and for some further period in time it exists but is truncated. (*) The terms "overwrite" and "supersede" are defined in ANSI Common Lisp: <http://www.ai.mit.edu/projects/iiip/doc/CommonLISP/HyperSpec/Body/fun_open.html> 2020-07-05 Bruno Haible <[email protected]> supersede: Add tests. * tests/test-supersede.c: New file. * tests/test-supersede-open.h: New file. * tests/test-supersede-fopen.h: New file. * modules/supersede-tests: New file. supersede: New module. * lib/supersede.h: New file. * lib/supersede.c: New file. * m4/supersede.m4: New file. * modules/supersede: New file.
>From 6dd23517fef58efe208ebcffbe55058c5c0091fe Mon Sep 17 00:00:00 2001 From: Bruno Haible <[email protected]> Date: Sun, 5 Jul 2020 12:27:24 +0200 Subject: [PATCH 1/2] supersede: New module. * lib/supersede.h: New file. * lib/supersede.c: New file. * m4/supersede.m4: New file. * modules/supersede: New file. --- ChangeLog | 8 + lib/supersede.c | 469 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ lib/supersede.h | 157 ++++++++++++++++++ m4/supersede.m4 | 10 ++ modules/supersede | 41 +++++ 5 files changed, 685 insertions(+) create mode 100644 lib/supersede.c create mode 100644 lib/supersede.h create mode 100644 m4/supersede.m4 create mode 100644 modules/supersede diff --git a/ChangeLog b/ChangeLog index 44d60f7..b1b82d3 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,13 @@ 2020-07-05 Bruno Haible <[email protected]> + supersede: New module. + * lib/supersede.h: New file. + * lib/supersede.c: New file. + * m4/supersede.m4: New file. + * modules/supersede: New file. + +2020-07-05 Bruno Haible <[email protected]> + Add some copyright headers. * lib/dev-ino.h: Add copyright header. * lib/di-set.h: Likewise. diff --git a/lib/supersede.c b/lib/supersede.c new file mode 100644 index 0000000..92317f2 --- /dev/null +++ b/lib/supersede.c @@ -0,0 +1,469 @@ +/* Open a file, without destroying an old file with the same name. + + Copyright (C) 2020 Free Software Foundation, Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. */ + +/* Written by Bruno Haible, 2020. */ + +#include <config.h> + +/* Specification. */ +#include "supersede.h" + +#include <errno.h> +#include <fcntl.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> + +#if defined _WIN32 && !defined __CYGWIN__ +/* A native Windows platform. */ +# define WIN32_LEAN_AND_MEAN /* avoid including junk */ +# include <windows.h> +# include <io.h> +#else +# include <unistd.h> +#endif + +#include "canonicalize.h" +#include "clean-temp.h" +#include "ignore-value.h" +#include "stat-time.h" +#include "utimens.h" +#include "acl.h" + +#if defined _WIN32 && !defined __CYGWIN__ +/* Don't assume that UNICODE is not defined. */ +# undef MoveFileEx +# define MoveFileEx MoveFileExA +#endif + +static int +create_temp_file (char *canon_filename, int flags, mode_t mode, + struct supersede_final_action *action) +{ + /* Use a temporary file always. */ + size_t canon_filename_length = strlen (canon_filename); + + /* The temporary file needs to be in the same directory, otherwise the + final rename may fail. */ + char *temp_filename = (char *) malloc (canon_filename_length + 7 + 1); + memcpy (temp_filename, canon_filename, canon_filename_length); + memcpy (temp_filename + canon_filename_length, ".XXXXXX", 7 + 1); + + int fd = gen_register_open_temp (temp_filename, 0, flags, mode); + if (fd < 0) + return -1; + + action->final_rename_temp = temp_filename; + action->final_rename_dest = canon_filename; + return fd; +} + +int +open_supersede (const char *filename, int flags, mode_t mode, + bool supersede_if_exists, bool supersede_if_does_not_exist, + struct supersede_final_action *action) +{ + int fd; + + if (supersede_if_exists) + { + if (supersede_if_does_not_exist) + { + struct stat statbuf; + + if (stat (filename, &statbuf) >= 0 + && ! S_ISREG (statbuf.st_mode) + /* The file exists and is possibly a character device, socket, or + something like that. */ + && ((fd = open (filename, flags, mode)) >= 0 + || errno != ENOENT)) + { + if (fd >= 0) + { + action->final_rename_temp = NULL; + action->final_rename_dest = NULL; + } + } + else + { + /* The file does not exist or is a regular file. + Use a temporary file. */ + char *canon_filename = + canonicalize_filename_mode (filename, CAN_ALL_BUT_LAST); + if (canon_filename == NULL) + fd = -1; + else + { + fd = create_temp_file (canon_filename, flags, mode, action); + if (fd < 0) + { + int saved_errno = errno; + free (canon_filename); + errno = saved_errno; + } + } + } + } + else + { + fd = open (filename, flags | O_CREAT | O_EXCL, mode); + if (fd >= 0) + { + /* The file did not exist. */ + action->final_rename_temp = NULL; + action->final_rename_dest = NULL; + } + else + { + /* The file exists or is a symbolic link to a nonexistent + file. */ + char *canon_filename = + canonicalize_filename_mode (filename, CAN_ALL_BUT_LAST); + if (canon_filename == NULL) + fd = -1; + else + { + fd = open (canon_filename, flags | O_CREAT | O_EXCL, mode); + if (fd >= 0) + { + /* It was a symbolic link to a nonexistent file. */ + free (canon_filename); + action->final_rename_temp = NULL; + action->final_rename_dest = NULL; + } + else + { + /* The file exists. */ + struct stat statbuf; + + if (stat (canon_filename, &statbuf) >= 0 + && S_ISREG (statbuf.st_mode)) + { + /* It is a regular file. Use a temporary file. */ + fd = create_temp_file (canon_filename, flags, mode, + action); + if (fd < 0) + { + int saved_errno = errno; + free (canon_filename); + errno = saved_errno; + } + } + else + { + /* It is possibly a character device, socket, or + something like that. */ + fd = open (canon_filename, flags, mode); + if (fd >= 0) + { + free (canon_filename); + action->final_rename_temp = NULL; + action->final_rename_dest = NULL; + } + else + { + int saved_errno = errno; + free (canon_filename); + errno = saved_errno; + } + } + } + } + } + } + } + else + { + if (supersede_if_does_not_exist) + { + fd = open (filename, flags, mode); + if (fd >= 0) + { + /* The file exists. */ + action->final_rename_temp = NULL; + action->final_rename_dest = NULL; + } + else if (errno == ENOENT) + { + /* The file does not exist. Use a temporary file. */ + char *canon_filename = + canonicalize_filename_mode (filename, CAN_ALL_BUT_LAST); + if (canon_filename == NULL) + fd = -1; + else + { + fd = create_temp_file (canon_filename, flags, mode, action); + if (fd < 0) + { + int saved_errno = errno; + free (canon_filename); + errno = saved_errno; + } + } + } + } + else + { + /* Never use a temporary file. */ + fd = open (filename, flags | O_CREAT, mode); + action->final_rename_temp = NULL; + action->final_rename_dest = NULL; + } + } + return fd; +} + +static int +after_close_actions (int ret, const struct supersede_final_action *action) +{ + if (ret < 0) + { + /* There was an error writing. Erase the temporary file. */ + if (action->final_rename_temp != NULL) + { + int saved_errno = errno; + ignore_value (unlink (action->final_rename_temp)); + free (action->final_rename_temp); + free (action->final_rename_dest); + errno = saved_errno; + } + return ret; + } + + if (action->final_rename_temp != NULL) + { + struct stat temp_statbuf; + struct stat dest_statbuf; + + if (stat (action->final_rename_temp, &temp_statbuf) < 0) + { + /* We just finished writing the temporary file, but now cannot access + it. There's something wrong. */ + int saved_errno = errno; + ignore_value (unlink (action->final_rename_temp)); + free (action->final_rename_temp); + free (action->final_rename_dest); + errno = saved_errno; + return -1; + } + + if (stat (action->final_rename_dest, &dest_statbuf) >= 0) + { + /* Copy the access time from the destination file to the temporary + file. */ + { + struct timespec ts[2]; + + ts[0] = get_stat_atime (&dest_statbuf); + ts[1] = get_stat_mtime (&temp_statbuf); + ignore_value (utimens (action->final_rename_temp, ts)); + } + +#if HAVE_CHOWN + /* Copy the owner and group from the destination file to the + temporary file. */ + ignore_value (chown (action->final_rename_temp, + dest_statbuf.st_uid, dest_statbuf.st_gid)); +#endif + + /* Copy the access permissions from the destination file to the + temporary file. */ +#if USE_ACL + switch (qcopy_acl (action->final_rename_dest, -1, + action->final_rename_temp, -1, + dest_statbuf.st_mode)) + { + case -2: + /* Could not get the ACL of the destination file. */ + case -1: + /* Could not set the ACL on the temporary file. */ + ignore_value (unlink (action->final_rename_temp)); + free (action->final_rename_temp); + free (action->final_rename_dest); + errno = EPERM; + return -1; + } +#else + chmod (action->final_rename_temp, dest_statbuf.st_mode); +#endif + } + else + /* No chmod needed, since the mode was already passed to + gen_register_open_temp. */ + ; + + /* Rename the temporary file to the destination file. */ +#if defined _WIN32 && !defined __CYGWIN__ + /* A native Windows platform. */ + /* ReplaceFile + <https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-replacefilea> + is atomic regarding the file's contents, says + https://stackoverflow.com/questions/167414/is-an-atomic-file-rename-with-overwrite-possible-on-windows> + But it fails with GetLastError () == ERROR_FILE_NOT_FOUND if + action->final_rename_dest does not exist. So better use + MoveFileEx + <https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-movefileexa>. */ + if (!MoveFileEx (action->final_rename_temp, action->final_rename_dest, + MOVEFILE_REPLACE_EXISTING)) + { + int saved_errno; + switch (GetLastError ()) + { + case ERROR_INVALID_PARAMETER: + saved_errno = EINVAL; break; + default: + saved_errno = EIO; break; + } + ignore_value (unlink (action->final_rename_temp)); + free (action->final_rename_temp); + free (action->final_rename_dest); + errno = saved_errno; + return -1; + } +#else + if (rename (action->final_rename_temp, action->final_rename_dest) < 0) + { + int saved_errno = errno; + ignore_value (unlink (action->final_rename_temp)); + free (action->final_rename_temp); + free (action->final_rename_dest); + errno = saved_errno; + return -1; + } +#endif + + unregister_temporary_file (action->final_rename_temp); + + free (action->final_rename_temp); + free (action->final_rename_dest); + } + + return ret; +} + +int +close_supersede (int fd, const struct supersede_final_action *action) +{ + if (fd < 0) + { + int saved_errno = errno; + free (action->final_rename_temp); + free (action->final_rename_dest); + errno = saved_errno; + return fd; + } + + int ret; + if (action->final_rename_temp != NULL) + ret = close_temp (fd); + else + ret = close (fd); + return after_close_actions (ret, action); +} + +FILE * +fopen_supersede (const char *filename, const char *mode, + bool supersede_if_exists, bool supersede_if_does_not_exist, + struct supersede_final_action *action) +{ + /* Parse the mode. */ + int open_direction = 0; + int open_flags = 0; + { + const char *p = mode; + + for (; *p != '\0'; p++) + { + switch (*p) + { + case 'r': + open_direction = O_RDONLY; + continue; + case 'w': + open_direction = O_WRONLY; + open_flags |= /* not! O_CREAT | */ O_TRUNC; + continue; + case 'a': + open_direction = O_WRONLY; + open_flags |= /* not! O_CREAT | */ O_APPEND; + continue; + case 'b': + /* While it is non-standard, O_BINARY is guaranteed by + gnulib <fcntl.h>. */ + open_flags |= O_BINARY; + continue; + case '+': + open_direction = O_RDWR; + continue; + case 'x': + /* not! open_flags |= O_EXCL; */ + continue; + case 'e': + open_flags |= O_CLOEXEC; + continue; + default: + break; + } + break; + } + } + + mode_t open_mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH; + int fd = open_supersede (filename, open_direction | open_flags, open_mode, + supersede_if_exists, supersede_if_does_not_exist, + action); + if (fd < 0) + return NULL; + + FILE *stream = fdopen (fd, mode); + if (stream == NULL) + { + int saved_errno = errno; + close (fd); + close_supersede (-1, action); + errno = saved_errno; + } + return stream; +} + +int +fclose_supersede (FILE *stream, const struct supersede_final_action *action) +{ + if (stream == NULL) + return -1; + int ret; + if (action->final_rename_temp != NULL) + ret = fclose_temp (stream); + else + ret = fclose (stream); + return after_close_actions (ret, action); +} + +#if GNULIB_FWRITEERROR +int +fwriteerror_supersede (FILE *stream, const struct supersede_final_action *action) +{ + if (stream == NULL) + return -1; + int ret; + if (action->final_rename_temp != NULL) + ret = fclose_temp (stream); + else + ret = fclose (stream); + return after_close_actions (ret, action); +} +#endif diff --git a/lib/supersede.h b/lib/supersede.h new file mode 100644 index 0000000..111d15b --- /dev/null +++ b/lib/supersede.h @@ -0,0 +1,157 @@ +/* Open a file, without destroying an old file with the same name. + + Copyright (C) 2020 Free Software Foundation, Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. */ + +/* Written by Bruno Haible, 2020. */ + +#ifndef _GL_SUPERSEDE_H +#define _GL_SUPERSEDE_H + +#include <stdbool.h> +#include <stdio.h> +#include <sys/types.h> + +#ifdef __cplusplus +extern "C" { +#endif + +/* When writing a file, for some usages it is important that at any moment, + a process that opens the file will see consistent data in the file. This + can be important in two situations: + * If supersede_if_exists == true, then when the file already existed, + it is important that a process that opens the file while the new file's + contents is being written sees consistent data - namely the old file's + data. + * If supersede_if_does_not_exist == true, then when the file did not exist, + it is important that a process that opens the file while the new file's + contents is being written sees no file (as opposed to a file with + truncated contents). + + In both situations, the effect is implemented by creating a temporary file, + writing into that temporary file, and renaming the temporary file when the + temporary file's contents is complete. + + Note that opening a file with superseding may fail when it would succeed + without superseding (for example, for a writable file in an unwritable + directory). And also the other way around: Opening a file with superseding + may succeed although it would fail without superseding (for example, for + an unwritable file in a writable directory). */ + +/* This type holds everything that needs to needs to be remembered in order to + execute the final rename action. */ +struct supersede_final_action +{ + char *final_rename_temp; + char *final_rename_dest; +}; + +/* =================== open() and close() with supersede =================== */ + +/* The typical code idiom is like this: + + struct supersede_final_action action; + int fd = open_supersede (filename, O_RDWR, mode, + supersede_if_exists, supersede_if_does_not_exist, + &action); + if (fd >= 0) + { + ... write the file's contents ... + if (successful) + { + if (close_supersede (fd, &action) < 0) + error (...); + } + else + { + // Abort the operation. + close (fd); + close_supersede (-1, &action); + } + } + */ + +/* Opens a file (typically for writing) in superseding mode, depending on + supersede_if_exists and supersede_if_does_not_exist. + FLAGS should not contain O_CREAT nor O_EXCL. + MODE is used when the file does not yet exist. The umask of the process + is considered, like in open(), i.e. the effective mode is + (MODE & ~ getumask ()). + Upon success, it fills in ACTION and returns a file descriptor. + Upon failure, it returns -1 and sets errno. */ +extern int open_supersede (const char *filename, int flags, mode_t mode, + bool supersede_if_exists, + bool supersede_if_does_not_exist, + struct supersede_final_action *action); + +/* Closes a file and executes the final rename action. + FD must have been returned by open_supersede(), or -1 if you want to abort + the operation. */ +extern int close_supersede (int fd, + const struct supersede_final_action *action); + +/* ================== fopen() and fclose() with supersede ================== */ + +/* The typical code idiom is like this: + + struct supersede_final_action action; + FILE *stream = + fopen_supersede (filename, O_RDWR, mode, + supersede_if_exists, supersede_if_does_not_exist, + &action); + if (stream != NULL) + { + ... write the file's contents ... + if (successful) + { + if (fclose_supersede (stream, &action) < 0) + error (...); + } + else + { + // Abort the operation. + fclose (stream); + fclose_supersede (NULL, &action); + } + } + */ + +/* Opens a file (typically for writing) in superseding mode, depending on + supersede_if_exists and supersede_if_does_not_exist. + Upon success, it fills in ACTION and returns a file stream. + Upon failure, it returns NULL and sets errno. */ +extern FILE *fopen_supersede (const char *filename, const char *mode, + bool supersede_if_exists, + bool supersede_if_does_not_exist, + struct supersede_final_action *action); + +/* Closes a file stream and executes the final rename action. + STREAM must have been returned by fopen_supersede(), or NULL if you want to + abort the operation. */ +extern int fclose_supersede (FILE *stream, + const struct supersede_final_action *action); + +/* Closes a file stream, like with fwriteerror, and executes the final rename + action. + STREAM must have been returned by fopen_supersede(), or NULL if you want to + abort the operation. */ +extern int fwriteerror_supersede (FILE *stream, + const struct supersede_final_action *action); + +#ifdef __cplusplus +} +#endif + +#endif /* _GL_SUPERSEDE_H */ diff --git a/m4/supersede.m4 b/m4/supersede.m4 new file mode 100644 index 0000000..2d445cf --- /dev/null +++ b/m4/supersede.m4 @@ -0,0 +1,10 @@ +# supersede.m4 serial 1 +dnl Copyright (C) 2020 Free Software Foundation, Inc. +dnl This file is free software; the Free Software Foundation +dnl gives unlimited permission to copy and/or distribute it, +dnl with or without modifications, as long as this notice is preserved. + +AC_DEFUN([gl_SUPERSEDE], +[ + AC_CHECK_FUNCS([chown]) +]) diff --git a/modules/supersede b/modules/supersede new file mode 100644 index 0000000..cf6e196 --- /dev/null +++ b/modules/supersede @@ -0,0 +1,41 @@ +Description: +Open a file, without destroying an old file with the same name. + +Files: +lib/supersede.h +lib/supersede.c +m4/supersede.m4 + +Depends-on: +fcntl-h +sys_stat +clean-temp +tempname +canonicalize +open +unlink +ignore-value +stat +stat-time +utimens +acl-permissions +qcopy-acl +fdopen + +configure.ac: +gl_SUPERSEDE + +Makefile.am: +lib_SOURCES += supersede.c + +Include: +"supersede.h" + +Link: +$(LIB_GETRANDOM) + +License: +GPL + +Maintainer: +all -- 2.7.4
>From 65219bb39efb32710036c895dbfa0d1c5916d803 Mon Sep 17 00:00:00 2001 From: Bruno Haible <[email protected]> Date: Sun, 5 Jul 2020 12:27:29 +0200 Subject: [PATCH 2/2] supersede: Add tests. * tests/test-supersede.c: New file. * tests/test-supersede-open.h: New file. * tests/test-supersede-fopen.h: New file. * modules/supersede-tests: New file. --- ChangeLog | 6 + modules/supersede-tests | 21 ++++ tests/test-supersede-fopen.h | 265 +++++++++++++++++++++++++++++++++++++++++++ tests/test-supersede-open.h | 262 ++++++++++++++++++++++++++++++++++++++++++ tests/test-supersede.c | 63 ++++++++++ 5 files changed, 617 insertions(+) create mode 100644 modules/supersede-tests create mode 100644 tests/test-supersede-fopen.h create mode 100644 tests/test-supersede-open.h create mode 100644 tests/test-supersede.c diff --git a/ChangeLog b/ChangeLog index b1b82d3..40a71ad 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,11 @@ 2020-07-05 Bruno Haible <[email protected]> + supersede: Add tests. + * tests/test-supersede.c: New file. + * tests/test-supersede-open.h: New file. + * tests/test-supersede-fopen.h: New file. + * modules/supersede-tests: New file. + supersede: New module. * lib/supersede.h: New file. * lib/supersede.c: New file. diff --git a/modules/supersede-tests b/modules/supersede-tests new file mode 100644 index 0000000..811d8f6 --- /dev/null +++ b/modules/supersede-tests @@ -0,0 +1,21 @@ +Files: +tests/test-supersede.c +tests/test-supersede-open.h +tests/test-supersede-fopen.h +tests/macros.h + +Depends-on: +mkdtemp +filenamecat +write +read-file +unlink +rmdir +symlink + +configure.ac: + +Makefile.am: +TESTS += test-supersede +check_PROGRAMS += test-supersede +test_supersede_LDADD = $(LDADD) $(LIB_ACL) $(LIB_CLOCK_GETTIME) $(LIB_GETRANDOM) diff --git a/tests/test-supersede-fopen.h b/tests/test-supersede-fopen.h new file mode 100644 index 0000000..e230102 --- /dev/null +++ b/tests/test-supersede-fopen.h @@ -0,0 +1,265 @@ +/* Tests for opening a file without destroying an old file with the same name. + + Copyright (C) 2020 Free Software Foundation, Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. */ + +/* Written by Bruno Haible, 2020. */ + +static void +test_fopen_supersede (bool supersede_if_exists, bool supersede_if_does_not_exist) +{ + char xtemplate[] = "gnulibtestXXXXXX"; + char *dir = mkdtemp (xtemplate); + char *filename = file_name_concat (dir, "test.mo", NULL); + struct stat statbuf; + + /* Test the case that the file does not yet exist. */ + { + ASSERT (stat (filename, &statbuf) < 0); + + struct supersede_final_action action; + FILE *fp = + fopen_supersede (filename, "wb", + supersede_if_exists, supersede_if_does_not_exist, + &action); + ASSERT (fp != NULL); + ASSERT (fwrite ("Hello world\n", 1, 12, fp) == 12 && fflush (fp) == 0); + if (supersede_if_does_not_exist) + ASSERT (stat (filename, &statbuf) < 0); + else + ASSERT (stat (filename, &statbuf) == 0); + ASSERT (fclose_supersede (fp, &action) == 0); + + ASSERT (stat (filename, &statbuf) == 0); + + size_t file_size; + char *file_contents = read_file (filename, RF_BINARY, &file_size); + ASSERT (file_size == 12); + ASSERT (memcmp (file_contents, "Hello world\n", 12) == 0); + } + + /* Test the case that the file exists and is a regular file. */ + { + ASSERT (stat (filename, &statbuf) == 0); + dev_t orig_dev = statbuf.st_dev; + ino_t orig_ino = statbuf.st_ino; + + struct supersede_final_action action; + FILE *fp = + fopen_supersede (filename, "wb", + supersede_if_exists, supersede_if_does_not_exist, + &action); + ASSERT (fp != NULL); + ASSERT (fwrite ("Foobar\n", 1, 7, fp) == 7 && fflush (fp) == 0); + ASSERT (stat (filename, &statbuf) == 0); + { + size_t file_size; + char *file_contents = read_file (filename, RF_BINARY, &file_size); + if (supersede_if_exists) + { + ASSERT (file_size == 12); + ASSERT (memcmp (file_contents, "Hello world\n", 12) == 0); + } + else + { + ASSERT (file_size == 7); + ASSERT (memcmp (file_contents, "Foobar\n", 7) == 0); + } + } + ASSERT (fclose_supersede (fp, &action) == 0); + + ASSERT (stat (filename, &statbuf) == 0); + + size_t file_size; + char *file_contents = read_file (filename, RF_BINARY, &file_size); + ASSERT (file_size == 7); + ASSERT (memcmp (file_contents, "Foobar\n", 7) == 0); + + if (supersede_if_exists) + { + /* Verify that the file now has a different inode number, on the same + device. */ +#if !(defined _WIN32 && !defined __CYGWIN__) + ASSERT (memcmp (&orig_dev, &statbuf.st_dev, sizeof (dev_t)) == 0); + ASSERT (memcmp (&orig_ino, &statbuf.st_ino, sizeof (ino_t)) != 0); +#endif + } + } + + /* Test the case that the file exists and is a character device. */ + { + ASSERT (stat (DEV_NULL, &statbuf) == 0); + + struct supersede_final_action action; + FILE *fp = + fopen_supersede (DEV_NULL, "wb", + supersede_if_exists, supersede_if_does_not_exist, + &action); + ASSERT (fp != NULL); + ASSERT (fwrite ("Foobar\n", 1, 7, fp) == 7 && fflush (fp) == 0); + ASSERT (stat (DEV_NULL, &statbuf) == 0); + ASSERT (fclose_supersede (fp, &action) == 0); + + ASSERT (stat (DEV_NULL, &statbuf) == 0); + } + + /* Test the case that the file is a symbolic link to an existing regular + file. */ + { + const char *linkname = "link1"; + unlink (linkname); + if (symlink (filename, linkname) >= 0) + { + ASSERT (stat (linkname, &statbuf) == 0); + dev_t orig_dev = statbuf.st_dev; + ino_t orig_ino = statbuf.st_ino; + + struct supersede_final_action action; + FILE *fp = + fopen_supersede (linkname, "wb", + supersede_if_exists, supersede_if_does_not_exist, + &action); + ASSERT (fp != NULL); + ASSERT (fwrite ("New\n", 1, 4, fp) == 4 && fflush (fp) == 0); + ASSERT (stat (linkname, &statbuf) == 0); + { + size_t file_size; + char *file_contents = read_file (linkname, RF_BINARY, &file_size); + if (supersede_if_exists) + { + ASSERT (file_size == 7); + ASSERT (memcmp (file_contents, "Foobar\n", 7) == 0); + } + else + { + ASSERT (file_size == 4); + ASSERT (memcmp (file_contents, "New\n", 4) == 0); + } + } + ASSERT (fclose_supersede (fp, &action) == 0); + + ASSERT (stat (linkname, &statbuf) == 0); + + size_t file_size; + char *file_contents = read_file (linkname, RF_BINARY, &file_size); + ASSERT (file_size == 4); + ASSERT (memcmp (file_contents, "New\n", 4) == 0); + + if (supersede_if_exists) + { + /* Verify that the file now has a different inode number, on the + same device. */ +#if !(defined _WIN32 && !defined __CYGWIN__) + ASSERT (memcmp (&orig_dev, &statbuf.st_dev, sizeof (dev_t)) == 0); + ASSERT (memcmp (&orig_ino, &statbuf.st_ino, sizeof (ino_t)) != 0); +#endif + } + + /* Clean up. */ + unlink (linkname); + } + } + + /* Test the case that the file is a symbolic link to an existing character + device. */ + { + const char *linkname = "link2"; + unlink (linkname); + if (symlink (DEV_NULL, linkname) >= 0) + { + ASSERT (stat (linkname, &statbuf) == 0); + + struct supersede_final_action action; + FILE *fp = + fopen_supersede (linkname, "wb", + supersede_if_exists, supersede_if_does_not_exist, + &action); + ASSERT (fp != NULL); + ASSERT (fwrite ("New\n", 1, 4, fp) == 4 && fflush (fp) == 0); + ASSERT (stat (linkname, &statbuf) == 0); + ASSERT (fclose_supersede (fp, &action) == 0); + + ASSERT (stat (linkname, &statbuf) == 0); + + /* Clean up. */ + unlink (linkname); + } + } + + /* Clean up. */ + unlink (filename); + + /* Test the case that the file is a symbolic link to a nonexistent file in an + existing directory. */ + { + const char *linkname = "link3"; + unlink (linkname); + if (symlink (filename, linkname) >= 0) + { + ASSERT (stat (linkname, &statbuf) < 0); + + struct supersede_final_action action; + FILE *fp = + fopen_supersede (linkname, "wb", + supersede_if_exists, supersede_if_does_not_exist, + &action); + ASSERT (fp != NULL); + ASSERT (fwrite ("Hello world\n", 1, 12, fp) == 12 && fflush (fp) == 0); + if (supersede_if_does_not_exist) + ASSERT (stat (linkname, &statbuf) < 0); + else + ASSERT (stat (linkname, &statbuf) == 0); + ASSERT (fclose_supersede (fp, &action) == 0); + + ASSERT (stat (linkname, &statbuf) == 0); + + size_t file_size; + char *file_contents = read_file (linkname, RF_BINARY, &file_size); + ASSERT (file_size == 12); + ASSERT (memcmp (file_contents, "Hello world\n", 12) == 0); + + /* Clean up. */ + unlink (linkname); + } + } + + /* Test the case that the file is a symbolic link to a nonexistent file in a + nonexistent directory. */ + { + const char *linkname = "link4"; + unlink (linkname); + if (symlink ("/nonexistent/gnulibtest8237/24715863701440", linkname) >= 0) + { + ASSERT (stat (linkname, &statbuf) < 0); + + struct supersede_final_action action; + FILE *fp = + fopen_supersede (linkname, "wb", + supersede_if_exists, supersede_if_does_not_exist, + &action); + ASSERT (fp == NULL); + ASSERT (errno == ENOENT); + + ASSERT (stat (linkname, &statbuf) < 0); + + /* Clean up. */ + unlink (linkname); + } + } + + /* Clean up. */ + unlink (filename); + rmdir (dir); +} diff --git a/tests/test-supersede-open.h b/tests/test-supersede-open.h new file mode 100644 index 0000000..f3b9b15 --- /dev/null +++ b/tests/test-supersede-open.h @@ -0,0 +1,262 @@ +/* Tests for opening a file without destroying an old file with the same name. + + Copyright (C) 2020 Free Software Foundation, Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. */ + +/* Written by Bruno Haible, 2020. */ + +static void +test_open_supersede (bool supersede_if_exists, bool supersede_if_does_not_exist) +{ + char xtemplate[] = "gnulibtestXXXXXX"; + char *dir = mkdtemp (xtemplate); + char *filename = file_name_concat (dir, "test.mo", NULL); + struct stat statbuf; + + /* Test the case that the file does not yet exist. */ + { + ASSERT (stat (filename, &statbuf) < 0); + + struct supersede_final_action action; + int fd = open_supersede (filename, O_RDWR | O_TRUNC, 0666, + supersede_if_exists, supersede_if_does_not_exist, + &action); + ASSERT (fd >= 0); + ASSERT (write (fd, "Hello world\n", 12) == 12); + if (supersede_if_does_not_exist) + ASSERT (stat (filename, &statbuf) < 0); + else + ASSERT (stat (filename, &statbuf) == 0); + ASSERT (close_supersede (fd, &action) == 0); + + ASSERT (stat (filename, &statbuf) == 0); + + size_t file_size; + char *file_contents = read_file (filename, RF_BINARY, &file_size); + ASSERT (file_size == 12); + ASSERT (memcmp (file_contents, "Hello world\n", 12) == 0); + } + + /* Test the case that the file exists and is a regular file. */ + { + ASSERT (stat (filename, &statbuf) == 0); + dev_t orig_dev = statbuf.st_dev; + ino_t orig_ino = statbuf.st_ino; + + struct supersede_final_action action; + int fd = open_supersede (filename, O_RDWR | O_TRUNC, 0666, + supersede_if_exists, supersede_if_does_not_exist, + &action); + ASSERT (fd >= 0); + ASSERT (write (fd, "Foobar\n", 7) == 7); + ASSERT (stat (filename, &statbuf) == 0); + { + size_t file_size; + char *file_contents = read_file (filename, RF_BINARY, &file_size); + if (supersede_if_exists) + { + ASSERT (file_size == 12); + ASSERT (memcmp (file_contents, "Hello world\n", 12) == 0); + } + else + { + ASSERT (file_size == 7); + ASSERT (memcmp (file_contents, "Foobar\n", 7) == 0); + } + } + ASSERT (close_supersede (fd, &action) == 0); + + ASSERT (stat (filename, &statbuf) == 0); + + size_t file_size; + char *file_contents = read_file (filename, RF_BINARY, &file_size); + ASSERT (file_size == 7); + ASSERT (memcmp (file_contents, "Foobar\n", 7) == 0); + + if (supersede_if_exists) + { + /* Verify that the file now has a different inode number, on the same + device. */ +#if !(defined _WIN32 && !defined __CYGWIN__) + ASSERT (memcmp (&orig_dev, &statbuf.st_dev, sizeof (dev_t)) == 0); + ASSERT (memcmp (&orig_ino, &statbuf.st_ino, sizeof (ino_t)) != 0); +#endif + } + } + + /* Test the case that the file exists and is a character device. */ + { + ASSERT (stat (DEV_NULL, &statbuf) == 0); + + struct supersede_final_action action; + int fd = open_supersede (DEV_NULL, O_RDWR | O_TRUNC, 0666, + supersede_if_exists, supersede_if_does_not_exist, + &action); + ASSERT (fd >= 0); + ASSERT (write (fd, "Foobar\n", 7) == 7); + ASSERT (stat (DEV_NULL, &statbuf) == 0); + ASSERT (close_supersede (fd, &action) == 0); + + ASSERT (stat (DEV_NULL, &statbuf) == 0); + } + + /* Test the case that the file is a symbolic link to an existing regular + file. */ + { + const char *linkname = "link1"; + unlink (linkname); + if (symlink (filename, linkname) >= 0) + { + ASSERT (stat (linkname, &statbuf) == 0); + dev_t orig_dev = statbuf.st_dev; + ino_t orig_ino = statbuf.st_ino; + + struct supersede_final_action action; + int fd = + open_supersede (linkname, O_RDWR | O_TRUNC, 0666, + supersede_if_exists, supersede_if_does_not_exist, + &action); + ASSERT (fd >= 0); + ASSERT (write (fd, "New\n", 4) == 4); + ASSERT (stat (linkname, &statbuf) == 0); + { + size_t file_size; + char *file_contents = read_file (linkname, RF_BINARY, &file_size); + if (supersede_if_exists) + { + ASSERT (file_size == 7); + ASSERT (memcmp (file_contents, "Foobar\n", 7) == 0); + } + else + { + ASSERT (file_size == 4); + ASSERT (memcmp (file_contents, "New\n", 4) == 0); + } + } + ASSERT (close_supersede (fd, &action) == 0); + + ASSERT (stat (linkname, &statbuf) == 0); + + size_t file_size; + char *file_contents = read_file (linkname, RF_BINARY, &file_size); + ASSERT (file_size == 4); + ASSERT (memcmp (file_contents, "New\n", 4) == 0); + + if (supersede_if_exists) + { + /* Verify that the file now has a different inode number, on the + same device. */ +#if !(defined _WIN32 && !defined __CYGWIN__) + ASSERT (memcmp (&orig_dev, &statbuf.st_dev, sizeof (dev_t)) == 0); + ASSERT (memcmp (&orig_ino, &statbuf.st_ino, sizeof (ino_t)) != 0); +#endif + } + + /* Clean up. */ + unlink (linkname); + } + } + + /* Test the case that the file is a symbolic link to an existing character + device. */ + { + const char *linkname = "link2"; + unlink (linkname); + if (symlink (DEV_NULL, linkname) >= 0) + { + ASSERT (stat (linkname, &statbuf) == 0); + + struct supersede_final_action action; + int fd = + open_supersede (linkname, O_RDWR | O_TRUNC, 0666, + supersede_if_exists, supersede_if_does_not_exist, + &action); + ASSERT (fd >= 0); + ASSERT (write (fd, "New\n", 4) == 4); + ASSERT (stat (linkname, &statbuf) == 0); + ASSERT (close_supersede (fd, &action) == 0); + + ASSERT (stat (linkname, &statbuf) == 0); + + /* Clean up. */ + unlink (linkname); + } + } + + /* Clean up. */ + unlink (filename); + + /* Test the case that the file is a symbolic link to a nonexistent file in an + existing directory. */ + { + const char *linkname = "link3"; + unlink (linkname); + if (symlink (filename, linkname) >= 0) + { + ASSERT (stat (linkname, &statbuf) < 0); + + struct supersede_final_action action; + int fd = + open_supersede (linkname, O_RDWR | O_TRUNC, 0666, + supersede_if_exists, supersede_if_does_not_exist, + &action); + ASSERT (fd >= 0); + ASSERT (write (fd, "Hello world\n", 12) == 12); + if (supersede_if_does_not_exist) + ASSERT (stat (linkname, &statbuf) < 0); + else + ASSERT (stat (linkname, &statbuf) == 0); + ASSERT (close_supersede (fd, &action) == 0); + + ASSERT (stat (linkname, &statbuf) == 0); + + size_t file_size; + char *file_contents = read_file (linkname, RF_BINARY, &file_size); + ASSERT (file_size == 12); + ASSERT (memcmp (file_contents, "Hello world\n", 12) == 0); + + /* Clean up. */ + unlink (linkname); + } + } + + /* Test the case that the file is a symbolic link to a nonexistent file in a + nonexistent directory. */ + { + const char *linkname = "link4"; + unlink (linkname); + if (symlink ("/nonexistent/gnulibtest8237/24715863701440", linkname) >= 0) + { + ASSERT (stat (linkname, &statbuf) < 0); + + struct supersede_final_action action; + int fd = + open_supersede (linkname, O_RDWR | O_TRUNC, 0666, + supersede_if_exists, supersede_if_does_not_exist, + &action); + ASSERT (fd < 0); + ASSERT (errno == ENOENT); + + ASSERT (stat (linkname, &statbuf) < 0); + + /* Clean up. */ + unlink (linkname); + } + } + + /* Clean up. */ + unlink (filename); + rmdir (dir); +} diff --git a/tests/test-supersede.c b/tests/test-supersede.c new file mode 100644 index 0000000..843d891 --- /dev/null +++ b/tests/test-supersede.c @@ -0,0 +1,63 @@ +/* Tests for opening a file without destroying an old file with the same name. + + Copyright (C) 2020 Free Software Foundation, Inc. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. */ + +/* Written by Bruno Haible, 2020. */ + +#include <config.h> + +/* Specification. */ +#include "supersede.h" + +#include <errno.h> +#include <fcntl.h> +#include <stdbool.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <unistd.h> + +#include "filenamecat.h" +#include "read-file.h" +#include "macros.h" + +/* The name of the "always silent" device. */ +#if defined _WIN32 && ! defined __CYGWIN__ +/* Native Windows API. */ +# define DEV_NULL "NUL" +#else +/* Unix API. */ +# define DEV_NULL "/dev/null" +#endif + +#include "test-supersede-open.h" +#include "test-supersede-fopen.h" + +int +main (void) +{ + test_open_supersede (false, false); + test_open_supersede (false, true); + test_open_supersede (true, false); + test_open_supersede (true, true); + + test_fopen_supersede (false, false); + test_fopen_supersede (false, true); + test_fopen_supersede (true, false); + test_fopen_supersede (true, true); + + return 0; +} -- 2.7.4
