Following up on my recent 'cp' changes, I installed the attached so that
'mv' and 'install' are consistent with 'cp'. (I did a similar thing to
'ln' back in October 2018.)From 57c812cc3e17ecf5df887029221fe3f2d0cd7ea0 Mon Sep 17 00:00:00 2001
From: Paul Eggert <egg...@cs.ucla.edu>
Date: Sat, 29 Jan 2022 11:40:17 -0800
Subject: [PATCH] mv: when installing to dir use dir-relative names
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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.
---
NEWS | 2 +-
src/cp.c | 57 ---------------
src/install.c | 150 ++++++++++++++++++++-------------------
src/mv.c | 143 ++++++++++++++++---------------------
src/system.h | 57 +++++++++++++++
tests/install/basic-1.sh | 2 +-
tests/mv/diag.sh | 4 +-
7 files changed, 202 insertions(+), 213 deletions(-)
diff --git a/NEWS b/NEWS
index ebcd5cb2f..711f7811a 100644
--- a/NEWS
+++ b/NEWS
@@ -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
diff --git a/src/cp.c b/src/cp.c
index 3bcc0d681..d680eb01d 100644
--- a/src/cp.c
+++ b/src/cp.c
@@ -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. */
diff --git a/src/install.c b/src/install.c
index b157f59d2..a27a5343b 100644
--- a/src/install.c
+++ b/src/install.c
@@ -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, ©_into_self, NULL);
+ return copy (from, to, to_dirfd, to_relname, 0, x, ©_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);
diff --git a/src/mv.c b/src/mv.c
index b5eb169f3..fcf32cd43 100644
--- a/src/mv.c
+++ b/src/mv.c
@@ -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,
©_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;
diff --git a/src/system.h b/src/system.h
index 6f9ebbc7c..9f10579dc 100644
--- a/src/system.h
+++ b/src/system.h
@@ -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)
diff --git a/tests/install/basic-1.sh b/tests/install/basic-1.sh
index 83bec639b..690d591e5 100755
--- a/tests/install/basic-1.sh
+++ b/tests/install/basic-1.sh
@@ -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
diff --git a/tests/mv/diag.sh b/tests/mv/diag.sh
index 92410699f..c0a558548 100755
--- a/tests/mv/diag.sh
+++ b/tests/mv/diag.sh
@@ -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
--
2.32.0