--- /dev/null
+/* 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
--- /dev/null
+/* 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 */