/* Open a file, without destroying an old file with the same name.
Copyright (C) 2020-2024 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);
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)
{
int fd;
/* 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
,
- 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 (strcmp (filename, "/dev/null") == 0)
filename = "NUL";
#endif
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
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)
{
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 . */
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