>From 6dd23517fef58efe208ebcffbe55058c5c0091fe Mon Sep 17 00:00:00 2001 From: Bruno Haible 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 + 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 + 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 . */ + +/* Written by Bruno Haible, 2020. */ + +#include + +/* Specification. */ +#include "supersede.h" + +#include +#include +#include +#include +#include + +#if defined _WIN32 && !defined __CYGWIN__ +/* A native Windows platform. */ +# define WIN32_LEAN_AND_MEAN /* avoid including junk */ +# include +# include +#else +# include +#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 + + 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 + . */ + 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 . */ + 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 . */ + +/* Written by Bruno Haible, 2020. */ + +#ifndef _GL_SUPERSEDE_H +#define _GL_SUPERSEDE_H + +#include +#include +#include + +#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