234 lines
6.5 KiB
C
234 lines
6.5 KiB
C
|
/* Rename a file relative to open directories.
|
||
|
Copyright (C) 2009-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 Eric Blake and Paul Eggert */
|
||
|
|
||
|
#include <config.h>
|
||
|
|
||
|
#include "renameatu.h"
|
||
|
|
||
|
#include <errno.h>
|
||
|
#include <stdio.h>
|
||
|
#include <sys/stat.h>
|
||
|
#include <unistd.h>
|
||
|
|
||
|
#ifdef __linux__
|
||
|
# include <sys/syscall.h>
|
||
|
#endif
|
||
|
|
||
|
static int
|
||
|
errno_fail (int e)
|
||
|
{
|
||
|
errno = e;
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
#if HAVE_RENAMEAT
|
||
|
|
||
|
# include <stdbool.h>
|
||
|
# include <stdlib.h>
|
||
|
# include <string.h>
|
||
|
|
||
|
# include "dirname.h"
|
||
|
# include "openat.h"
|
||
|
|
||
|
#else
|
||
|
# include "openat-priv.h"
|
||
|
|
||
|
static int
|
||
|
rename_noreplace (char const *src, char const *dst)
|
||
|
{
|
||
|
/* This has a race between the call to lstat and the call to rename. */
|
||
|
struct stat st;
|
||
|
return (lstat (dst, &st) == 0 || errno == EOVERFLOW ? errno_fail (EEXIST)
|
||
|
: errno == ENOENT ? rename (src, dst)
|
||
|
: -1);
|
||
|
}
|
||
|
#endif
|
||
|
|
||
|
#undef renameat
|
||
|
|
||
|
/* Rename FILE1, in the directory open on descriptor FD1, to FILE2, in
|
||
|
the directory open on descriptor FD2. If possible, do it without
|
||
|
changing the working directory. Otherwise, resort to using
|
||
|
save_cwd/fchdir, then rename/restore_cwd. If either the save_cwd or
|
||
|
the restore_cwd fails, then give a diagnostic and exit nonzero.
|
||
|
|
||
|
Obey FLAGS when doing the renaming. If FLAGS is zero, this
|
||
|
function is equivalent to renameat (FD1, SRC, FD2, DST).
|
||
|
Otherwise, attempt to implement FLAGS even if the implementation is
|
||
|
not atomic; this differs from the GNU/Linux native renameat2,
|
||
|
which fails if it cannot guarantee atomicity. */
|
||
|
|
||
|
int
|
||
|
renameatu (int fd1, char const *src, int fd2, char const *dst,
|
||
|
unsigned int flags)
|
||
|
{
|
||
|
int ret_val = -1;
|
||
|
int err = EINVAL;
|
||
|
|
||
|
#ifdef HAVE_RENAMEAT2
|
||
|
ret_val = renameat2 (fd1, src, fd2, dst, flags);
|
||
|
err = errno;
|
||
|
#elif defined SYS_renameat2
|
||
|
ret_val = syscall (SYS_renameat2, fd1, src, fd2, dst, flags);
|
||
|
err = errno;
|
||
|
#elif defined RENAME_EXCL
|
||
|
if (! (flags & ~(RENAME_EXCHANGE | RENAME_NOREPLACE)))
|
||
|
{
|
||
|
ret_val = renameatx_np (fd1, src, fd2, dst,
|
||
|
((flags & RENAME_EXCHANGE ? RENAME_SWAP : 0)
|
||
|
| (flags & RENAME_NOREPLACE ? RENAME_EXCL : 0)));
|
||
|
err = errno;
|
||
|
}
|
||
|
#endif
|
||
|
|
||
|
if (! (ret_val < 0 && (err == EINVAL || err == ENOSYS || err == ENOTSUP)))
|
||
|
return ret_val;
|
||
|
|
||
|
#if HAVE_RENAMEAT
|
||
|
{
|
||
|
size_t src_len;
|
||
|
size_t dst_len;
|
||
|
char *src_temp = (char *) src;
|
||
|
char *dst_temp = (char *) dst;
|
||
|
bool src_slash;
|
||
|
bool dst_slash;
|
||
|
int rename_errno = ENOTDIR;
|
||
|
struct stat src_st;
|
||
|
struct stat dst_st;
|
||
|
bool dst_found_nonexistent = false;
|
||
|
|
||
|
if (flags != 0)
|
||
|
{
|
||
|
/* RENAME_NOREPLACE is the only flag currently supported. */
|
||
|
if (flags & ~RENAME_NOREPLACE)
|
||
|
return errno_fail (ENOTSUP);
|
||
|
else
|
||
|
{
|
||
|
/* This has a race between the call to lstatat and the calls to
|
||
|
renameat below. */
|
||
|
if (lstatat (fd2, dst, &dst_st) == 0 || errno == EOVERFLOW)
|
||
|
return errno_fail (EEXIST);
|
||
|
if (errno != ENOENT)
|
||
|
return -1;
|
||
|
dst_found_nonexistent = true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/* Let strace see any ENOENT failure. */
|
||
|
src_len = strlen (src);
|
||
|
dst_len = strlen (dst);
|
||
|
if (!src_len || !dst_len)
|
||
|
return renameat (fd1, src, fd2, dst);
|
||
|
|
||
|
src_slash = src[src_len - 1] == '/';
|
||
|
dst_slash = dst[dst_len - 1] == '/';
|
||
|
if (!src_slash && !dst_slash)
|
||
|
return renameat (fd1, src, fd2, dst);
|
||
|
|
||
|
/* Presence of a trailing slash requires directory semantics. If
|
||
|
the source does not exist, or if the destination cannot be turned
|
||
|
into a directory, give up now. Otherwise, strip trailing slashes
|
||
|
before calling rename. */
|
||
|
if (lstatat (fd1, src, &src_st))
|
||
|
return -1;
|
||
|
if (dst_found_nonexistent)
|
||
|
{
|
||
|
if (!S_ISDIR (src_st.st_mode))
|
||
|
return errno_fail (ENOENT);
|
||
|
}
|
||
|
else if (lstatat (fd2, dst, &dst_st))
|
||
|
{
|
||
|
if (errno != ENOENT || !S_ISDIR (src_st.st_mode))
|
||
|
return -1;
|
||
|
}
|
||
|
else if (!S_ISDIR (dst_st.st_mode))
|
||
|
return errno_fail (ENOTDIR);
|
||
|
else if (!S_ISDIR (src_st.st_mode))
|
||
|
return errno_fail (EISDIR);
|
||
|
|
||
|
# if RENAME_TRAILING_SLASH_SOURCE_BUG
|
||
|
/* See the lengthy comment in rename.c why Solaris 9 is forced to
|
||
|
GNU behavior, while Solaris 10 is left with POSIX behavior,
|
||
|
regarding symlinks with trailing slash. */
|
||
|
ret_val = -1;
|
||
|
if (src_slash)
|
||
|
{
|
||
|
src_temp = strdup (src);
|
||
|
if (!src_temp)
|
||
|
{
|
||
|
/* Rather than rely on strdup-posix, we set errno ourselves. */
|
||
|
rename_errno = ENOMEM;
|
||
|
goto out;
|
||
|
}
|
||
|
strip_trailing_slashes (src_temp);
|
||
|
if (lstatat (fd1, src_temp, &src_st))
|
||
|
{
|
||
|
rename_errno = errno;
|
||
|
goto out;
|
||
|
}
|
||
|
if (S_ISLNK (src_st.st_mode))
|
||
|
goto out;
|
||
|
}
|
||
|
if (dst_slash)
|
||
|
{
|
||
|
dst_temp = strdup (dst);
|
||
|
if (!dst_temp)
|
||
|
{
|
||
|
rename_errno = ENOMEM;
|
||
|
goto out;
|
||
|
}
|
||
|
strip_trailing_slashes (dst_temp);
|
||
|
if (lstatat (fd2, dst_temp, &dst_st))
|
||
|
{
|
||
|
if (errno != ENOENT)
|
||
|
{
|
||
|
rename_errno = errno;
|
||
|
goto out;
|
||
|
}
|
||
|
}
|
||
|
else if (S_ISLNK (dst_st.st_mode))
|
||
|
goto out;
|
||
|
}
|
||
|
# endif /* RENAME_TRAILING_SLASH_SOURCE_BUG */
|
||
|
|
||
|
/* renameat does not honor trailing / on Solaris 10. Solve it in a
|
||
|
similar manner to rename. No need to worry about bugs not present
|
||
|
on Solaris, since all other systems either lack renameat or honor
|
||
|
trailing slash correctly. */
|
||
|
|
||
|
ret_val = renameat (fd1, src_temp, fd2, dst_temp);
|
||
|
rename_errno = errno;
|
||
|
goto out;
|
||
|
out:
|
||
|
if (src_temp != src)
|
||
|
free (src_temp);
|
||
|
if (dst_temp != dst)
|
||
|
free (dst_temp);
|
||
|
errno = rename_errno;
|
||
|
return ret_val;
|
||
|
}
|
||
|
#else /* !HAVE_RENAMEAT */
|
||
|
|
||
|
/* RENAME_NOREPLACE is the only flag currently supported. */
|
||
|
if (flags & ~RENAME_NOREPLACE)
|
||
|
return errno_fail (ENOTSUP);
|
||
|
return at_func2 (fd1, src, fd2, dst, flags ? rename_noreplace : rename);
|
||
|
|
||
|
#endif /* !HAVE_RENAMEAT */
|
||
|
}
|