1
0
mirror of git://git.sv.gnu.org/coreutils.git synced 2026-02-15 11:52:15 +02:00

mv: when installing to dir use dir-relative names

When the destination for mv is a directory, use functions like openat
to access the destination files, when such functions are available.
This should be more efficient and should avoid some race conditions.
Likewise for 'install'.
* src/cp.c (must_be_working_directory, target_directory_operand)
(target_dirfd_valid): Move from here ...
* src/system.h: ... to here, so that install and mv can use them.
Make them inline so GCC doesn’t complain.
* src/install.c (lchown) [HAVE_LCHOWN]: Remove; no longer needed.
(need_copy, copy_file, change_attributes, change_timestamps)
(install_file_in_file, install_file_in_dir):
New args for directory-relative names.  All uses changed.
Continue to pass full names as needed, for diagnostics and for
lower-level functions that do not support directory-relative names.
(install_file_in_dir): Update *TARGET_DIRFD as needed.
(main): Handle target-directory in the new, cp-like way.
* src/mv.c (remove_trailing_slashes): Remove static var; now local.
(do_move): New args for directory-relative names.  All uses changed.
Continue to pass full names as needed, for diagnostics and for
lower-level functions that do not support directory-relative names.
(movefile): Remove; no longer needed.
(main): Handle target-directory in the new, cp-like way.
* tests/install/basic-1.sh:
* tests/mv/diag.sh: Adjust to match new diagnostic wording.
This commit is contained in:
Paul Eggert
2022-01-29 11:40:17 -08:00
parent 534cfbb448
commit 57c812cc3e
7 changed files with 202 additions and 213 deletions

2
NEWS
View File

@@ -52,7 +52,7 @@ GNU coreutils NEWS -*- outline -*-
** Improvements
cp now uses openat and similar syscalls when copying to a directory.
cp, mv, and install now use openat-like syscalls when copying to a directory.
This avoids some race conditions and should be more efficient.
On macOS, cp creates a copy-on-write clone if source and destination

View File

@@ -564,63 +564,6 @@ make_dir_parents_private (char const *const_dir, size_t src_offset,
return true;
}
/* Must F designate the working directory? */
ATTRIBUTE_PURE static bool
must_be_working_directory (char const *f)
{
/* Return true for ".", "./.", ".///./", etc. */
while (*f++ == '.')
{
if (*f != '/')
return !*f;
while (*++f == '/')
continue;
if (!*f)
return true;
}
return false;
}
/* Return a file descriptor open to FILE, for use in openat.
As an optimization, return AT_FDCWD if FILE must be the working directory.
Fail if FILE is not a directory.
On failure return a negative value; this is -1 unless AT_FDCWD == -1. */
static int
target_directory_operand (char const *file)
{
if (must_be_working_directory (file))
return AT_FDCWD;
int fd = open (file, O_PATHSEARCH | O_DIRECTORY);
if (!O_DIRECTORY && 0 <= fd)
{
/* On old systems like Solaris 10 that do not support O_DIRECTORY,
check by hand whether FILE is a directory. */
struct stat st;
int err;
if (fstat (fd, &st) != 0 ? (err = errno, true)
: !S_ISDIR (st.st_mode) && (err = ENOTDIR, true))
{
close (fd);
errno = err;
fd = -1;
}
}
return fd - (AT_FDCWD == -1 && fd < 0);
}
/* Return true if FD represents success for target_directory_operand. */
static bool
target_dirfd_valid (int fd)
{
return fd != -1 - (AT_FDCWD == -1);
}
/* Scan the arguments, and copy each by calling copy.
Return true if successful. */

View File

@@ -61,10 +61,6 @@ static bool use_default_selinux_context = true;
# define endpwent() ((void) 0)
#endif
#if ! HAVE_LCHOWN
# define lchown(name, uid, gid) chown (name, uid, gid)
#endif
/* The user name that will own the files, or NULL to make the owner
the current user ID. */
static char *owner_name;
@@ -165,9 +161,11 @@ extra_mode (mode_t input)
return !! (input & ~ mask);
}
/* Return true if copy of file SRC_NAME to file DEST_NAME is necessary. */
/* Return true if copy of file SRC_NAME to file DEST_NAME aka
DEST_DIRFD+DEST_RELNAME is necessary. */
static bool
need_copy (char const *src_name, char const *dest_name,
int dest_dirfd, char const *dest_relname,
const struct cp_options *x)
{
struct stat src_sb, dest_sb;
@@ -181,7 +179,7 @@ need_copy (char const *src_name, char const *dest_name,
if (lstat (src_name, &src_sb) != 0)
return true;
if (lstat (dest_name, &dest_sb) != 0)
if (lstatat (dest_dirfd, dest_relname, &dest_sb) != 0)
return true;
if (!S_ISREG (src_sb.st_mode) || !S_ISREG (dest_sb.st_mode)
@@ -241,7 +239,7 @@ need_copy (char const *src_name, char const *dest_name,
if (src_fd < 0)
return true;
dest_fd = open (dest_name, O_RDONLY | O_BINARY);
dest_fd = openat (dest_dirfd, dest_relname, O_RDONLY | O_BINARY);
if (dest_fd < 0)
{
close (src_fd);
@@ -353,28 +351,6 @@ setdefaultfilecon (char const *file)
freecon (scontext);
}
/* FILE is the last operand of this command. Return true if FILE is a
directory. But report an error there is a problem accessing FILE,
or if FILE does not exist but would have to refer to an existing
directory if it referred to anything at all. */
static bool
target_directory_operand (char const *file)
{
char const *b = last_component (file);
size_t blen = strlen (b);
bool looks_like_a_dir = (blen == 0 || ISSLASH (b[blen - 1]));
struct stat st;
int err = (stat (file, &st) == 0 ? 0 : errno);
bool is_a_dir = !err && S_ISDIR (st.st_mode);
if (err && err != ENOENT)
die (EXIT_FAILURE, err, _("failed to access %s"), quoteaf (file));
if (is_a_dir < looks_like_a_dir)
die (EXIT_FAILURE, err, _("target %s is not a directory"),
quoteaf (file));
return is_a_dir;
}
/* Report that directory DIR was made, if OPTIONS requests this. */
static void
announce_mkdir (char const *dir, void *options)
@@ -431,15 +407,16 @@ process_dir (char *dir, struct savewd *wd, void *options)
return ret;
}
/* Copy file FROM onto file TO, creating TO if necessary.
Return true if successful. */
/* Copy file FROM onto file TO aka TO_DIRFD+TO_RELNAME, creating TO if
necessary. Return true if successful. */
static bool
copy_file (char const *from, char const *to, const struct cp_options *x)
copy_file (char const *from, char const *to,
int to_dirfd, char const *to_relname, const struct cp_options *x)
{
bool copy_into_self;
if (copy_only_if_needed && !need_copy (from, to, x))
if (copy_only_if_needed && !need_copy (from, to, to_dirfd, to_relname, x))
return true;
/* Allow installing from non-regular files like /dev/null.
@@ -448,14 +425,14 @@ copy_file (char const *from, char const *to, const struct cp_options *x)
However, since !x->recursive, the call to "copy" will fail if FROM
is a directory. */
return copy (from, to, AT_FDCWD, to, 0, x, &copy_into_self, NULL);
return copy (from, to, to_dirfd, to_relname, 0, x, &copy_into_self, NULL);
}
/* Set the attributes of file or directory NAME.
/* Set the attributes of file or directory NAME aka DIRFD+RELNAME.
Return true if successful. */
static bool
change_attributes (char const *name)
change_attributes (char const *name, int dirfd, char const *relname)
{
bool ok = false;
/* chown must precede chmod because on some systems,
@@ -471,9 +448,9 @@ change_attributes (char const *name)
want to know. */
if (! (owner_id == (uid_t) -1 && group_id == (gid_t) -1)
&& lchown (name, owner_id, group_id) != 0)
&& lchownat (dirfd, relname, owner_id, group_id) != 0)
error (0, errno, _("cannot change ownership of %s"), quoteaf (name));
else if (chmod (name, mode) != 0)
else if (chmodat (dirfd, relname, mode) != 0)
error (0, errno, _("cannot change permissions of %s"), quoteaf (name));
else
ok = true;
@@ -484,17 +461,18 @@ change_attributes (char const *name)
return ok;
}
/* Set the timestamps of file DEST to match those of SRC_SB.
/* Set the timestamps of file DEST aka DIRFD+RELNAME to match those of SRC_SB.
Return true if successful. */
static bool
change_timestamps (struct stat const *src_sb, char const *dest)
change_timestamps (struct stat const *src_sb, char const *dest,
int dirfd, char const *relname)
{
struct timespec timespec[2];
timespec[0] = get_stat_atime (src_sb);
timespec[1] = get_stat_mtime (src_sb);
if (utimens (dest, timespec))
if (utimensat (dirfd, relname, timespec, 0))
{
error (0, errno, _("cannot set timestamps for %s"), quoteaf (dest));
return false;
@@ -653,12 +631,13 @@ In the 4th form, create all components of the given DIRECTORY(ies).\n\
exit (status);
}
/* Copy file FROM onto file TO and give TO the appropriate
attributes.
/* Copy file FROM onto file TO aka TO_DIRFD+TO_RELNAME and give TO the
appropriate attributes. X gives the command options.
Return true if successful. */
static bool
install_file_in_file (char const *from, char const *to,
int to_dirfd, char const *to_relname,
const struct cp_options *x)
{
struct stat from_sb;
@@ -667,19 +646,19 @@ install_file_in_file (char const *from, char const *to,
error (0, errno, _("cannot stat %s"), quoteaf (from));
return false;
}
if (! copy_file (from, to, x))
if (! copy_file (from, to, to_dirfd, to_relname, x))
return false;
if (strip_files)
if (! strip (to))
{
if (unlink (to) != 0) /* Cleanup. */
if (unlinkat (to_dirfd, to_relname, 0) != 0) /* Cleanup. */
die (EXIT_FAILURE, errno, _("cannot unlink %s"), quoteaf (to));
return false;
}
if (x->preserve_timestamps && (strip_files || ! S_ISREG (from_sb.st_mode))
&& ! change_timestamps (&from_sb, to))
&& ! change_timestamps (&from_sb, to, to_dirfd, to_relname))
return false;
return change_attributes (to);
return change_attributes (to, to_dirfd, to_relname);
}
/* Create any missing parent directories of TO,
@@ -731,7 +710,7 @@ install_file_in_file_parents (char const *from, char *to,
const struct cp_options *x)
{
return (mkancesdirs_safe_wd (from, to, (struct cp_options *)x, false)
&& install_file_in_file (from, to, x));
&& install_file_in_file (from, to, AT_FDCWD, to, x));
}
/* Copy file FROM into directory TO_DIR, keeping its same name,
@@ -740,16 +719,39 @@ install_file_in_file_parents (char const *from, char *to,
static bool
install_file_in_dir (char const *from, char const *to_dir,
const struct cp_options *x, bool mkdir_and_install)
const struct cp_options *x, bool mkdir_and_install,
int *target_dirfd)
{
char const *from_base = last_component (from);
char *to = file_name_concat (to_dir, from_base, NULL);
char *to_relname;
char *to = file_name_concat (to_dir, from_base, &to_relname);
bool ret = true;
if (mkdir_and_install)
ret = mkancesdirs_safe_wd (from, to, (struct cp_options *)x, true);
if (!target_dirfd_valid (*target_dirfd)
&& (ret = mkdir_and_install)
&& (ret = mkancesdirs_safe_wd (from, to, (struct cp_options *) x, true)))
{
int fd = open (to_dir, O_PATHSEARCH | O_DIRECTORY);
if (fd < 0)
{
error (0, errno, _("cannot open %s"), quoteaf (to));
ret = false;
}
else
*target_dirfd = fd;
}
if (ret)
{
int to_dirfd = *target_dirfd;
if (!target_dirfd_valid (to_dirfd))
{
to_dirfd = AT_FDCWD;
to_relname = to;
}
ret = install_file_in_file (from, to, to_dirfd, to_relname, x);
}
ret = ret && install_file_in_file (from, to, x);
free (to);
return ret;
}
@@ -899,18 +901,6 @@ main (int argc, char **argv)
die (EXIT_FAILURE, 0,
_("target directory not allowed when installing a directory"));
if (target_directory)
{
struct stat st;
bool stat_success = stat (target_directory, &st) == 0 ? true : false;
if (! mkdir_and_install && ! stat_success)
die (EXIT_FAILURE, errno, _("failed to access %s"),
quoteaf (target_directory));
if (stat_success && ! S_ISDIR (st.st_mode))
die (EXIT_FAILURE, 0, _("target %s is not a directory"),
quoteaf (target_directory));
}
x.backup_type = (make_backups
? xget_version (_("backup type"),
version_control_string)
@@ -939,6 +929,7 @@ main (int argc, char **argv)
usage (EXIT_FAILURE);
}
int target_dirfd = AT_FDCWD;
if (no_target_directory)
{
if (target_directory)
@@ -951,13 +942,26 @@ main (int argc, char **argv)
usage (EXIT_FAILURE);
}
}
else if (! (dir_arg || target_directory))
else if (target_directory)
{
if (2 <= n_files && target_directory_operand (file[n_files - 1]))
target_directory = file[--n_files];
target_dirfd = target_directory_operand (target_directory);
if (! (target_dirfd_valid (target_dirfd)
|| (mkdir_and_install && errno == ENOENT)))
die (EXIT_FAILURE, errno, _("failed to access %s"),
quoteaf (target_directory));
}
else if (!dir_arg)
{
char const *lastfile = file[n_files - 1];
int fd = target_directory_operand (lastfile);
if (target_dirfd_valid (fd))
{
target_dirfd = fd;
target_directory = lastfile;
n_files--;
}
else if (2 < n_files)
die (EXIT_FAILURE, 0, _("target %s is not a directory"),
quoteaf (file[n_files - 1]));
die (EXIT_FAILURE, errno, _("target %s"), quoteaf (lastfile));
}
if (specified_mode)
@@ -1006,7 +1010,8 @@ main (int argc, char **argv)
{
if (! (mkdir_and_install
? install_file_in_file_parents (file[0], file[1], &x)
: install_file_in_file (file[0], file[1], &x)))
: install_file_in_file (file[0], file[1], AT_FDCWD,
file[1], &x)))
exit_status = EXIT_FAILURE;
}
else
@@ -1015,7 +1020,8 @@ main (int argc, char **argv)
dest_info_init (&x);
for (i = 0; i < n_files; i++)
if (! install_file_in_dir (file[i], target_directory, &x,
i == 0 && mkdir_and_install))
i == 0 && mkdir_and_install,
&target_dirfd))
exit_status = EXIT_FAILURE;
#ifdef lint
dest_info_free (&x);

143
src/mv.c
View File

@@ -50,9 +50,6 @@ enum
STRIP_TRAILING_SLASHES_OPTION = CHAR_MAX + 1
};
/* Remove any trailing slashes from each SOURCE argument. */
static bool remove_trailing_slashes;
static struct option const long_options[] =
{
{"backup", optional_argument, NULL, 'b'},
@@ -146,31 +143,18 @@ cp_option_init (struct cp_options *x)
x->src_info = NULL;
}
/* FILE is the last operand of this command. Return true if FILE is a
directory. But report an error if there is a problem accessing FILE, other
than nonexistence (errno == ENOENT). */
static bool
target_directory_operand (char const *file)
{
struct stat st;
int err = (stat (file, &st) == 0 ? 0 : errno);
bool is_a_dir = !err && S_ISDIR (st.st_mode);
if (err && err != ENOENT)
die (EXIT_FAILURE, err, _("failed to access %s"), quoteaf (file));
return is_a_dir;
}
/* Move SOURCE onto DEST. Handles cross-file-system moves.
/* Move SOURCE onto DEST aka DEST_DIRFD+DEST_RELNAME.
Handle cross-file-system moves.
If SOURCE is a directory, DEST must not exist.
Return true if successful. */
static bool
do_move (char const *source, char const *dest, const struct cp_options *x)
do_move (char const *source, char const *dest,
int dest_dirfd, char const *dest_relname, const struct cp_options *x)
{
bool copy_into_self;
bool rename_succeeded;
bool ok = copy (source, dest, AT_FDCWD, dest, 0, x,
bool ok = copy (source, dest, dest_dirfd, dest_relname, 0, x,
&copy_into_self, &rename_succeeded);
if (ok)
@@ -246,43 +230,6 @@ do_move (char const *source, char const *dest, const struct cp_options *x)
return ok;
}
/* Move file SOURCE onto DEST. Handles the case when DEST is a directory.
Treat DEST as a directory if DEST_IS_DIR.
Return true if successful. */
static bool
movefile (char *source, char *dest, bool dest_is_dir,
const struct cp_options *x)
{
bool ok;
/* This code was introduced to handle the ambiguity in the semantics
of mv that is induced by the varying semantics of the rename function.
Some systems (e.g., GNU/Linux) have a rename function that honors a
trailing slash, while others (like Solaris 5,6,7) have a rename
function that ignores a trailing slash. I believe the GNU/Linux
rename semantics are POSIX and susv2 compliant. */
if (remove_trailing_slashes)
strip_trailing_slashes (source);
if (dest_is_dir)
{
/* Treat DEST as a directory; build the full filename. */
char const *src_basename = last_component (source);
char *new_dest = file_name_concat (dest, src_basename, NULL);
strip_trailing_slashes (new_dest);
ok = do_move (source, new_dest, x);
free (new_dest);
}
else
{
ok = do_move (source, dest, x);
}
return ok;
}
void
usage (int status)
{
@@ -343,7 +290,8 @@ main (int argc, char **argv)
char const *backup_suffix = NULL;
char *version_control_string = NULL;
struct cp_options x;
char *target_directory = NULL;
bool remove_trailing_slashes = false;
char const *target_directory = NULL;
bool no_target_directory = false;
int n_files;
char **file;
@@ -387,16 +335,6 @@ main (int argc, char **argv)
case 't':
if (target_directory)
die (EXIT_FAILURE, 0, _("multiple target directories specified"));
else
{
struct stat st;
if (stat (optarg, &st) != 0)
die (EXIT_FAILURE, errno, _("failed to access %s"),
quoteaf (optarg));
if (! S_ISDIR (st.st_mode))
die (EXIT_FAILURE, 0, _("target %s is not a directory"),
quoteaf (optarg));
}
target_directory = optarg;
break;
case 'T':
@@ -443,6 +381,7 @@ main (int argc, char **argv)
usage (EXIT_FAILURE);
}
int target_dirfd = AT_FDCWD;
if (no_target_directory)
{
if (target_directory)
@@ -455,23 +394,60 @@ main (int argc, char **argv)
usage (EXIT_FAILURE);
}
}
else if (!target_directory)
else if (target_directory)
{
assert (2 <= n_files);
target_dirfd = target_directory_operand (target_directory);
if (! target_dirfd_valid (target_dirfd))
die (EXIT_FAILURE, errno, _("target directory %s"),
quoteaf (target_directory));
}
else
{
char const *lastfile = file[n_files - 1];
if (n_files == 2)
x.rename_errno = (renameatu (AT_FDCWD, file[0], AT_FDCWD, file[1],
x.rename_errno = (renameatu (AT_FDCWD, file[0], AT_FDCWD, lastfile,
RENAME_NOREPLACE)
? errno : 0);
if (x.rename_errno != 0 && target_directory_operand (file[n_files - 1]))
if (x.rename_errno != 0)
{
x.rename_errno = -1;
target_directory = file[--n_files];
int fd = target_directory_operand (lastfile);
if (target_dirfd_valid (fd))
{
x.rename_errno = -1;
target_dirfd = fd;
target_directory = lastfile;
n_files--;
}
else
{
/* The last operand LASTFILE cannot be opened as a directory.
If there are more than two operands, report an error.
Also, report an error if LASTFILE is known to be a directory
even though it could not be opened, which can happen if
opening failed with EACCES on a platform lacking O_PATH.
In this case use stat to test whether LASTFILE is a
directory, in case opening a non-directory with (O_SEARCH
| O_DIRECTORY) failed with EACCES not ENOTDIR. */
int err = errno;
struct stat st;
if (2 < n_files
|| (O_PATHSEARCH == O_SEARCH && err == EACCES
&& stat (lastfile, &st) == 0 && S_ISDIR (st.st_mode)))
die (EXIT_FAILURE, err, _("target %s"), quoteaf (lastfile));
}
}
else if (2 < n_files)
die (EXIT_FAILURE, 0, _("target %s is not a directory"),
quoteaf (file[n_files - 1]));
}
/* Handle the ambiguity in the semantics of mv induced by the
varying semantics of the rename function. POSIX-compatible
systems (e.g., GNU/Linux) have a rename function that honors a
trailing slash in the source, while others (Solaris 9, FreeBSD
7.2) have a rename function that ignores it. */
if (remove_trailing_slashes)
for (int i = 0; i < n_files; i++)
strip_trailing_slashes (file[i]);
if (x.interactive == I_ALWAYS_NO)
x.update = false;
@@ -502,7 +478,14 @@ main (int argc, char **argv)
for (int i = 0; i < n_files; ++i)
{
x.last_file = i + 1 == n_files;
ok &= movefile (file[i], target_directory, true, &x);
char const *source = file[i];
char const *source_basename = last_component (source);
char *dest_relname;
char *dest = file_name_concat (target_directory, source_basename,
&dest_relname);
strip_trailing_slashes (dest_relname);
ok &= do_move (source, dest, target_dirfd, dest_relname, &x);
free (dest);
}
#ifdef lint
@@ -512,7 +495,7 @@ main (int argc, char **argv)
else
{
x.last_file = true;
ok = movefile (file[0], file[1], false, &x);
ok = do_move (file[0], file[1], AT_FDCWD, file[1], &x);
}
return ok ? EXIT_SUCCESS : EXIT_FAILURE;

View File

@@ -107,6 +107,63 @@ enum { O_PATHSEARCH = O_PATH };
enum { O_PATHSEARCH = O_SEARCH };
#endif
/* Must F designate the working directory? */
ATTRIBUTE_PURE static inline bool
must_be_working_directory (char const *f)
{
/* Return true for ".", "./.", ".///./", etc. */
while (*f++ == '.')
{
if (*f != '/')
return !*f;
while (*++f == '/')
continue;
if (!*f)
return true;
}
return false;
}
/* Return a file descriptor open to FILE, for use in openat.
As an optimization, return AT_FDCWD if FILE must be the working directory.
Fail if FILE is not a directory.
On failure return a negative value; this is -1 unless AT_FDCWD == -1. */
static inline int
target_directory_operand (char const *file)
{
if (must_be_working_directory (file))
return AT_FDCWD;
int fd = open (file, O_PATHSEARCH | O_DIRECTORY);
if (!O_DIRECTORY && 0 <= fd)
{
/* On old systems like Solaris 10 that do not support O_DIRECTORY,
check by hand whether FILE is a directory. */
struct stat st;
int err;
if (fstat (fd, &st) != 0 ? (err = errno, true)
: !S_ISDIR (st.st_mode) && (err = ENOTDIR, true))
{
close (fd);
errno = err;
fd = -1;
}
}
return fd - (AT_FDCWD == -1 && fd < 0);
}
/* Return true if FD represents success for target_directory_operand. */
static inline bool
target_dirfd_valid (int fd)
{
return fd != -1 - (AT_FDCWD == -1);
}
#include <dirent.h>
#ifndef _D_EXACT_NAMLEN
# define _D_EXACT_NAMLEN(dp) strlen ((dp)->d_name)

View File

@@ -131,7 +131,7 @@ EOF
touch sub4/file_exists || framework_failure_
ginstall -t sub4/file_exists -Dv file >out 2>&1 && fail=1
compare - out <<\EOF || fail=1
ginstall: target 'sub4/file_exists' is not a directory
ginstall: failed to access 'sub4/file_exists': Not a directory
EOF
# Ensure that -D with an already existing directory for -t's option argument

View File

@@ -39,8 +39,8 @@ mv: missing file operand
Try 'mv --help' for more information.
mv: missing destination file operand after 'no-file'
Try 'mv --help' for more information.
mv: target 'f1' is not a directory
mv: target 'f2' is not a directory
mv: target 'f1': Not a directory
mv: target directory 'f2': Not a directory
EOF
compare exp out || fail=1