| /* Open a file, without destroying an old file with the same name. | |
| Copyright (C) 2020-2026 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); | |
| if (temp_filename == NULL) | |
| return -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) | |
| { | |
| /* Extra flags for existing devices. */ | |
| int extra_flags = | |
| #if defined __sun || (defined _WIN32 && !defined __CYGWIN__) | |
| /* open ("/dev/null", O_TRUNC | O_WRONLY) fails on Solaris zones: | |
| - with error EINVAL on Illumos, see | |
| <https://www.illumos.org/issues/13035>, | |
| - with error EACCES on Solaris 11.3. | |
| Likewise, open ("NUL", O_TRUNC | O_RDWR) fails with error EINVAL on | |
| native Windows. | |
| As a workaround, add the O_CREAT flag, although it ought not to be | |
| necessary. */ | |
| O_CREAT; | |
| #else | |
| 0; | |
| #endif | |
| #if defined _WIN32 && ! defined __CYGWIN__ | |
| if (streq (filename, "/dev/null")) | |
| filename = "NUL"; | |
| #endif | |
| 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 | extra_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) | |
| free (canon_filename); | |
| } | |
| } | |
| } | |
| 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) | |
| free (canon_filename); | |
| } | |
| else | |
| { | |
| /* It is possibly a character device, socket, or | |
| something like that. */ | |
| fd = open (canon_filename, flags | extra_flags, mode); | |
| free (canon_filename); | |
| if (fd >= 0) | |
| { | |
| action->final_rename_temp = NULL; | |
| action->final_rename_dest = NULL; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| 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; | |
| } | |
| #if defined __sun || (defined _WIN32 && !defined __CYGWIN__) | |
| /* See the comment regarding extra_flags, above. */ | |
| else if (errno == EINVAL || errno == EACCES) | |
| { | |
| 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. As a workaround, add the O_CREAT | |
| flag, although it ought not to be necessary.*/ | |
| fd = open (filename, flags | extra_flags, mode); | |
| if (fd >= 0) | |
| { | |
| /* The file exists. */ | |
| action->final_rename_temp = NULL; | |
| action->final_rename_dest = NULL; | |
| } | |
| } | |
| } | |
| #endif | |
| 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) | |
| free (canon_filename); | |
| } | |
| } | |
| } | |
| 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) | |
| { | |
| free (action->final_rename_temp); | |
| free (action->final_rename_dest); | |
| 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 |