summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--corebinutils/mv/.gitignore25
-rw-r--r--corebinutils/mv/GNUmakefile36
-rw-r--r--corebinutils/mv/LICENSE32
-rw-r--r--corebinutils/mv/LICENSES/BSD-3-Clause.txt11
-rw-r--r--corebinutils/mv/README.md47
-rw-r--r--corebinutils/mv/mv.1186
-rw-r--r--corebinutils/mv/mv.c1263
-rw-r--r--corebinutils/mv/tests/test.sh414
8 files changed, 2014 insertions, 0 deletions
diff --git a/corebinutils/mv/.gitignore b/corebinutils/mv/.gitignore
new file mode 100644
index 0000000000..a74d30b48c
--- /dev/null
+++ b/corebinutils/mv/.gitignore
@@ -0,0 +1,25 @@
+*.a
+*.core
+*.lo
+*.nossppico
+*.o
+*.orig
+*.pico
+*.pieo
+*.po
+*.rej
+*.so
+*.so.[0-9]*
+*.sw[nop]
+*~
+.*DS_Store
+.cache
+.clangd
+.ccls-cache
+.depend*
+compile_commands.json
+compile_commands.events.json
+tags
+build/
+out/
+.linux-obj/
diff --git a/corebinutils/mv/GNUmakefile b/corebinutils/mv/GNUmakefile
new file mode 100644
index 0000000000..371a8cd5d2
--- /dev/null
+++ b/corebinutils/mv/GNUmakefile
@@ -0,0 +1,36 @@
+.DEFAULT_GOAL := all
+
+CC ?= cc
+CCACHE_DISABLE ?= 1
+CPPFLAGS += -D_POSIX_C_SOURCE=200809L -D_FILE_OFFSET_BITS=64
+CFLAGS ?= -O2
+CFLAGS += -std=c17 -g -Wall -Wextra -Werror
+LDFLAGS ?=
+LDLIBS ?=
+
+OBJDIR := $(CURDIR)/build
+OUTDIR := $(CURDIR)/out
+TARGET := $(OUTDIR)/mv
+OBJS := $(OBJDIR)/mv.o
+
+.PHONY: all clean dirs status test
+
+all: $(TARGET)
+
+dirs:
+ @mkdir -p "$(OBJDIR)" "$(OUTDIR)"
+
+$(TARGET): $(OBJS) | dirs
+ env CCACHE_DISABLE="$(CCACHE_DISABLE)" $(CC) $(LDFLAGS) -o "$@" $(OBJS) $(LDLIBS)
+
+$(OBJDIR)/mv.o: $(CURDIR)/mv.c | dirs
+ env CCACHE_DISABLE="$(CCACHE_DISABLE)" $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/mv.c" -o "$@"
+
+test: $(TARGET)
+ MV_BIN="$(TARGET)" sh "$(CURDIR)/tests/test.sh"
+
+status:
+ @printf '%s\n' "$(TARGET)"
+
+clean:
+ @rm -rf "$(OBJDIR)" "$(OUTDIR)"
diff --git a/corebinutils/mv/LICENSE b/corebinutils/mv/LICENSE
new file mode 100644
index 0000000000..8e21339bcd
--- /dev/null
+++ b/corebinutils/mv/LICENSE
@@ -0,0 +1,32 @@
+Copyright (c) 1989, 1993, 1994
+ The Regents of the University of California. All rights reserved.
+
+Copyright (c) 2026
+ Project Tick. All rights reserved.
+
+This code is derived from software contributed to Berkeley by
+Ken Smith of The State University of New York at Buffalo.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+3. Neither the name of the University nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGE.
diff --git a/corebinutils/mv/LICENSES/BSD-3-Clause.txt b/corebinutils/mv/LICENSES/BSD-3-Clause.txt
new file mode 100644
index 0000000000..086d3992cb
--- /dev/null
+++ b/corebinutils/mv/LICENSES/BSD-3-Clause.txt
@@ -0,0 +1,11 @@
+Copyright (c) <year> <owner>.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/corebinutils/mv/README.md b/corebinutils/mv/README.md
new file mode 100644
index 0000000000..f758a5b886
--- /dev/null
+++ b/corebinutils/mv/README.md
@@ -0,0 +1,47 @@
+# mv
+
+Standalone Linux-native port of FreeBSD `mv` for Project Tick BSD/Linux Distribution.
+
+## Build
+
+```sh
+gmake -f GNUmakefile
+gmake -f GNUmakefile CC=musl-gcc
+```
+
+## Test
+
+```sh
+gmake -f GNUmakefile test
+gmake -f GNUmakefile test CC=musl-gcc
+```
+
+## Port Strategy
+
+- Port structure follows the standalone sibling ports such as `bin/ln`: local `GNUmakefile`, local README, and self-contained shell tests.
+- The FreeBSD `mv.c` implementation was rewritten into a Linux-native utility instead of carrying BSD libc helpers such as `err(3)`, `strmode(3)`, `statfs(2)`, `PATH_MAX`-bounded path assembly, or `cp`/`rm` subprocess fallback.
+- Fast-path moves map directly to Linux `rename(2)`.
+- Cross-filesystem moves map to native recursive copy/remove logic:
+ - regular files: `open(2)` + `read(2)` + `write(2)`
+ - directories: `mkdir(2)` + `opendir(3)`/`readdir(3)` recursion
+ - symlinks: `readlink(2)` + `symlink(2)`
+ - FIFOs: `mkfifo(2)`
+ - device nodes: `mknod(2)`
+ - metadata: `fchown(2)`/`fchmod(2)`/`utimensat(2)`/`futimens(2)`
+ - Linux xattrs and ACL xattrs: `listxattr(2)`/`getxattr(2)`/`setxattr(2)` and their `l*`/`f*` variants
+- Target path construction is dynamic; the port does not depend on `PATH_MAX`, `realpath(3)` canonicalization, or BSD-only libc APIs.
+
+## Supported / Unsupported Semantics
+
+- Supported: `mv [-f | -i | -n] [-hv] source target`
+- Supported: `mv [-f | -i | -n] [-v] source ... directory`
+- Supported: same-filesystem rename, cross-filesystem moves for regular files, directories, symlinks, FIFOs, and device nodes.
+- Supported: `-h` semantics from `mv.1` for replacing a symbolic link to a directory in the two-operand form.
+- Supported: Linux xattr-preserving cross-filesystem moves when both source and destination filesystems support the relevant xattrs.
+- Supported and tested: partial regular-file copy failures clean up the destination file instead of leaving a truncated target behind.
+- Unsupported by design: GNU long options and GNU option permutation.
+- Unsupported on Linux and rejected explicitly during cross-filesystem fallback: socket files.
+- Unsupported as a compatibility shim: FreeBSD file flags / `chflags(2)` semantics. Linux has no direct equivalent here.
+- Unsupported and rejected explicitly during cross-filesystem fallback: moving a mount point by copy/remove. The port refuses that case instead of partially copying mounted content and failing later.
+- Cross-filesystem fallback does not preserve hardlink graphs; linked inputs become distinct copies at the destination. That matches the current recursive copy strategy and is covered by tests.
+- Not atomic across filesystems: the implementation follows `mv.1`'s documented copy-then-remove semantics, so a failed cross-filesystem move can leave the destination partially created or the source still present.
diff --git a/corebinutils/mv/mv.1 b/corebinutils/mv/mv.1
new file mode 100644
index 0000000000..138ce99f58
--- /dev/null
+++ b/corebinutils/mv/mv.1
@@ -0,0 +1,186 @@
+.\"-
+.\" Copyright (c) 1989, 1990, 1993
+.\" The Regents of the University of California. All rights reserved.
+.\"
+.\" This code is derived from software contributed to Berkeley by
+.\" the Institute of Electrical and Electronics Engineers, Inc.
+.\"
+.\" Redistribution and use in source and binary forms, with or without
+.\" modification, are permitted provided that the following conditions
+.\" are met:
+.\" 1. Redistributions of source code must retain the above copyright
+.\" notice, this list of conditions and the following disclaimer.
+.\" 2. Redistributions in binary form must reproduce the above copyright
+.\" notice, this list of conditions and the following disclaimer in the
+.\" documentation and/or other materials provided with the distribution.
+.\" 3. Neither the name of the University nor the names of its contributors
+.\" may be used to endorse or promote products derived from this software
+.\" without specific prior written permission.
+.\"
+.\" THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+.\" ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+.\" SUCH DAMAGE.
+.\"
+.Dd March 15, 2013
+.Dt MV 1
+.Os
+.Sh NAME
+.Nm mv
+.Nd move files
+.Sh SYNOPSIS
+.Nm
+.Op Fl f | i | n
+.Op Fl hv
+.Ar source target
+.Nm
+.Op Fl f | i | n
+.Op Fl v
+.Ar source ... directory
+.Sh DESCRIPTION
+In its first form, the
+.Nm
+utility renames the file named by the
+.Ar source
+operand to the destination path named by the
+.Ar target
+operand.
+This form is assumed when the last operand does not name an already
+existing directory.
+.Pp
+In its second form,
+.Nm
+moves each file named by a
+.Ar source
+operand to a destination file in the existing directory named by the
+.Ar directory
+operand.
+The destination path for each operand is the pathname produced by the
+concatenation of the last operand, a slash, and the final pathname
+component of the named file.
+.Pp
+The following options are available:
+.Bl -tag -width indent
+.It Fl f
+Do not prompt for confirmation before overwriting the destination
+path.
+(The
+.Fl f
+option overrides any previous
+.Fl i
+or
+.Fl n
+options.)
+.It Fl h
+If the
+.Ar target
+operand is a symbolic link to a directory,
+do not follow it.
+This causes the
+.Nm
+utility to rename the file
+.Ar source
+to the destination path
+.Ar target
+rather than moving
+.Ar source
+into the directory referenced by
+.Ar target .
+.It Fl i
+Cause
+.Nm
+to write a prompt to standard error before moving a file that would
+overwrite an existing file.
+If the response from the standard input begins with the character
+.Ql y
+or
+.Ql Y ,
+the move is attempted.
+(The
+.Fl i
+option overrides any previous
+.Fl f
+or
+.Fl n
+options.)
+.It Fl n
+Do not overwrite an existing file.
+(The
+.Fl n
+option overrides any previous
+.Fl f
+or
+.Fl i
+options.)
+.It Fl v
+Cause
+.Nm
+to be verbose, showing files after they are moved.
+.El
+.Pp
+It is an error for the
+.Ar source
+operand to specify a directory if the target exists and is not a directory.
+.Pp
+If the destination path does not have a mode which permits writing,
+.Nm
+prompts the user for confirmation as specified for the
+.Fl i
+option.
+.Pp
+As the
+.Xr rename 2
+call does not work across file systems,
+.Nm
+uses
+.Xr cp 1
+and
+.Xr rm 1
+to accomplish the move.
+The effect is equivalent to:
+.Bd -literal -offset indent
+rm -f destination_path && \e
+cp -pRP source_file destination && \e
+rm -rf source_file
+.Ed
+.Sh EXIT STATUS
+.Ex -std
+.Sh EXAMPLES
+Rename file
+.Pa foo
+to
+.Pa bar ,
+overwriting
+.Pa bar
+if it already exists:
+.Pp
+.Dl $ mv -f foo bar
+.Sh COMPATIBILITY
+The
+.Fl h ,
+.Fl n ,
+and
+.Fl v
+options are non-standard and their use in scripts is not recommended.
+.Sh SEE ALSO
+.Xr cp 1 ,
+.Xr rm 1 ,
+.Xr symlink 7
+.Sh STANDARDS
+The
+.Nm
+utility is expected to be
+.St -p1003.2
+compatible.
+.Sh HISTORY
+A
+.Nm
+command appeared in
+.At v1 .
diff --git a/corebinutils/mv/mv.c b/corebinutils/mv/mv.c
new file mode 100644
index 0000000000..6a903c7132
--- /dev/null
+++ b/corebinutils/mv/mv.c
@@ -0,0 +1,1263 @@
+/*-
+ * SPDX-License-Identifier: BSD-3-Clause
+ *
+ * Copyright (c) 1989, 1993, 1994
+ * The Regents of the University of California. All rights reserved.
+ * Copyright (c) 2026
+ * Project Tick. All rights reserved.
+ *
+ * This code is derived from software contributed to Berkeley by
+ * Ken Smith of The State University of New York at Buffalo.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the University nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#define _POSIX_C_SOURCE 200809L
+
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/xattr.h>
+
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#ifndef O_CLOEXEC
+#define O_CLOEXEC 0
+#endif
+
+#ifndef AT_NO_AUTOMOUNT
+#define AT_NO_AUTOMOUNT 0
+#endif
+
+#define MV_EXIT_ERROR 1
+#define MV_EXIT_USAGE 2
+#define COPY_BUFFER_MIN (128U * 1024U)
+#define COPY_BUFFER_MAX (2U * 1024U * 1024U)
+
+struct mv_options {
+ bool force;
+ bool interactive;
+ bool no_clobber;
+ bool no_target_dir_follow;
+ bool verbose;
+};
+
+struct move_target {
+ bool treat_as_directory;
+ char *path;
+};
+
+static const char *progname;
+
+static int append_child_basename(const char *directory, const char *source,
+ char **result);
+static int apply_existing_target_policy(const struct mv_options *options,
+ const char *target, bool *skip_move);
+static int apply_path_metadata(const char *target, const struct stat *source_sb,
+ bool nofollow, const char *source);
+static size_t basename_len(const char *path);
+static const char *basename_start(const char *path);
+static int cleanup_failed_target(const char *target);
+static int copy_directory_tree(const char *source, const char *target,
+ const struct stat *source_sb);
+static int copy_file_data(int from_fd, int to_fd, const char *source,
+ const char *target);
+static int copy_file_xattrs(int source_fd, int dest_fd, const char *source,
+ const char *target);
+static int copy_move_fallback(const struct mv_options *options,
+ const char *source, const char *target,
+ const struct stat *source_sb, const struct stat *target_sb);
+static int copy_node(const char *source, const char *target,
+ const struct stat *source_sb);
+static int copy_non_regular(const char *source, const char *target,
+ const struct stat *source_sb);
+static int copy_path_xattrs(const char *source, const char *target,
+ bool nofollow);
+static int copy_regular_file(const char *source, const char *target,
+ const struct stat *source_sb);
+static int determine_target_mode(const struct mv_options *options, int argc,
+ char *const argv[], struct move_target *target);
+static char *dirname_dup(const char *path);
+static void error_errno(const char *fmt, ...);
+static void error_msg(const char *fmt, ...);
+static int file_list_xattrs(int fd, const char *source, char **names_buf,
+ ssize_t *names_len);
+static int handle_single_move(const struct mv_options *options,
+ const char *source, const char *target);
+static int is_dir_symlink(const char *path);
+static int is_mount_point(const char *path, const struct stat *source_sb,
+ bool *mount_point);
+static char *join_path(const char *dir, const char *name);
+static int path_list_xattrs(const char *source, bool nofollow,
+ char **names_buf, ssize_t *names_len);
+static int preserve_timestamps_fd(int fd, const struct stat *source_sb,
+ const char *target);
+static int preserve_timestamps_path(const char *target,
+ const struct stat *source_sb, bool nofollow);
+static const char *program_name(const char *argv0);
+static int prompt_overwrite(const char *target);
+static int readlink_alloc(const char *path, char **targetp);
+static int remove_source_tree(const char *path, const struct stat *sb);
+static int remove_target_path(const char *target, const struct stat *target_sb);
+static int set_fd_ownership_and_mode(int fd, const struct stat *source_sb,
+ const char *target);
+static int set_path_ownership_and_mode(const char *target,
+ const struct stat *source_sb, bool nofollow);
+static int set_xattr_loop(const char *source, const char *target, char *names,
+ ssize_t names_len, bool nofollow, int source_fd, int dest_fd);
+static int should_prompt_for_permissions(const char *target);
+static void usage(void);
+static void *xmalloc(size_t size);
+static void *xrealloc(void *ptr, size_t size);
+static char *xstrdup(const char *text);
+extern int mknod(const char *path, mode_t mode, dev_t dev);
+
+static const char *
+program_name(const char *argv0)
+{
+ const char *name;
+
+ if (argv0 == NULL || argv0[0] == '\0')
+ return ("mv");
+ name = strrchr(argv0, '/');
+ return (name == NULL ? argv0 : name + 1);
+}
+
+static void
+verror_message(bool with_errno, const char *fmt, va_list ap)
+{
+ int saved_errno;
+
+ saved_errno = errno;
+ (void)fprintf(stderr, "%s: ", progname);
+ (void)vfprintf(stderr, fmt, ap);
+ if (with_errno)
+ (void)fprintf(stderr, ": %s", strerror(saved_errno));
+ (void)fputc('\n', stderr);
+}
+
+static void
+error_errno(const char *fmt, ...)
+{
+ va_list ap;
+
+ va_start(ap, fmt);
+ verror_message(true, fmt, ap);
+ va_end(ap);
+}
+
+static void
+error_msg(const char *fmt, ...)
+{
+ va_list ap;
+
+ va_start(ap, fmt);
+ verror_message(false, fmt, ap);
+ va_end(ap);
+}
+
+static void *
+xmalloc(size_t size)
+{
+ void *ptr;
+
+ ptr = malloc(size);
+ if (ptr == NULL) {
+ error_msg("out of memory");
+ exit(MV_EXIT_ERROR);
+ }
+ return (ptr);
+}
+
+static void *
+xrealloc(void *ptr, size_t size)
+{
+ void *newptr;
+
+ newptr = realloc(ptr, size);
+ if (newptr == NULL) {
+ error_msg("out of memory");
+ exit(MV_EXIT_ERROR);
+ }
+ return (newptr);
+}
+
+static char *
+xstrdup(const char *text)
+{
+ size_t len;
+ char *copy;
+
+ len = strlen(text) + 1;
+ copy = xmalloc(len);
+ memcpy(copy, text, len);
+ return (copy);
+}
+
+static const char *
+basename_start(const char *path)
+{
+ const char *end;
+ const char *start;
+
+ if (path[0] == '\0')
+ return (path);
+
+ end = path + strlen(path);
+ while (end > path && end[-1] == '/')
+ end--;
+ if (end == path)
+ return (path);
+
+ start = end;
+ while (start > path && start[-1] != '/')
+ start--;
+ return (start);
+}
+
+static size_t
+basename_len(const char *path)
+{
+ const char *end;
+ const char *start;
+
+ if (path[0] == '\0')
+ return (1);
+
+ end = path + strlen(path);
+ while (end > path && end[-1] == '/')
+ end--;
+ if (end == path)
+ return (1);
+
+ start = basename_start(path);
+ return ((size_t)(end - start));
+}
+
+static char *
+dirname_dup(const char *path)
+{
+ const char *end;
+ const char *slash;
+ size_t len;
+ char *dir;
+
+ if (path[0] == '\0')
+ return (xstrdup("."));
+
+ end = path + strlen(path);
+ while (end > path && end[-1] == '/')
+ end--;
+ if (end == path)
+ return (xstrdup("/"));
+
+ slash = end;
+ while (slash > path && slash[-1] != '/')
+ slash--;
+ if (slash == path)
+ return (xstrdup("."));
+
+ while (slash > path && slash[-1] == '/')
+ slash--;
+ if (slash == path)
+ return (xstrdup("/"));
+
+ len = (size_t)(slash - path);
+ dir = xmalloc(len + 1);
+ memcpy(dir, path, len);
+ dir[len] = '\0';
+ return (dir);
+}
+
+static char *
+join_path(const char *dir, const char *name)
+{
+ size_t dir_len;
+ size_t name_len;
+ bool need_sep;
+ char *path;
+
+ dir_len = strlen(dir);
+ name_len = strlen(name);
+ need_sep = dir_len != 0 && dir[dir_len - 1] != '/';
+
+ path = xmalloc(dir_len + (need_sep ? 1U : 0U) + name_len + 1U);
+ memcpy(path, dir, dir_len);
+ if (need_sep)
+ path[dir_len++] = '/';
+ memcpy(path + dir_len, name, name_len);
+ path[dir_len + name_len] = '\0';
+ return (path);
+}
+
+static int
+append_child_basename(const char *directory, const char *source, char **result)
+{
+ const char *base;
+ size_t base_len;
+ char *name;
+
+ base = basename_start(source);
+ base_len = basename_len(source);
+ name = xmalloc(base_len + 1);
+ memcpy(name, base, base_len);
+ name[base_len] = '\0';
+ *result = join_path(directory, name);
+ free(name);
+ return (0);
+}
+
+static void
+usage(void)
+{
+ (void)fprintf(stderr, "%s\n%s\n",
+ "usage: mv [-f | -i | -n] [-hv] source target",
+ " mv [-f | -i | -n] [-v] source ... directory");
+ exit(MV_EXIT_USAGE);
+}
+
+static int
+prompt_overwrite(const char *target)
+{
+ int ch;
+ int first;
+
+ (void)fprintf(stderr, "overwrite %s? (y/n [n]) ", target);
+ first = getchar();
+ ch = first;
+ while (ch != '\n' && ch != EOF)
+ ch = getchar();
+ return (first == 'y' || first == 'Y');
+}
+
+static int
+should_prompt_for_permissions(const char *target)
+{
+ struct stat sb;
+
+ if (!isatty(STDIN_FILENO))
+ return (0);
+ if (access(target, W_OK) == 0)
+ return (0);
+ if (stat(target, &sb) != 0)
+ return (0);
+ return (1);
+}
+
+static int
+apply_existing_target_policy(const struct mv_options *options, const char *target,
+ bool *skip_move)
+{
+ int prompt;
+
+ *skip_move = false;
+ if (options->force)
+ return (0);
+ if (options->no_clobber) {
+ if (options->verbose)
+ (void)printf("%s not overwritten\n", target);
+ *skip_move = true;
+ return (0);
+ }
+
+ prompt = options->interactive || should_prompt_for_permissions(target);
+ if (!prompt)
+ return (0);
+
+ if (!prompt_overwrite(target)) {
+ (void)fprintf(stderr, "not overwritten\n");
+ *skip_move = true;
+ }
+ return (0);
+}
+
+static int
+readlink_alloc(const char *path, char **targetp)
+{
+ size_t size;
+ ssize_t len;
+ char *buf;
+
+ size = 128;
+ buf = xmalloc(size);
+ for (;;) {
+ len = readlink(path, buf, size);
+ if (len < 0) {
+ free(buf);
+ return (-1);
+ }
+ if ((size_t)len < size) {
+ buf[len] = '\0';
+ *targetp = buf;
+ return (0);
+ }
+ size *= 2;
+ buf = xrealloc(buf, size);
+ }
+}
+
+static int
+file_list_xattrs(int fd, const char *source, char **names_buf, ssize_t *names_len)
+{
+ ssize_t len;
+ char *buf;
+
+ len = flistxattr(fd, NULL, 0);
+ if (len < 0) {
+ if (errno == ENOTSUP || errno == EOPNOTSUPP) {
+ *names_buf = NULL;
+ *names_len = 0;
+ return (0);
+ }
+ error_errno("list xattrs for %s", source);
+ return (-1);
+ }
+ if (len == 0) {
+ *names_buf = NULL;
+ *names_len = 0;
+ return (0);
+ }
+
+ buf = xmalloc((size_t)len);
+ if (flistxattr(fd, buf, (size_t)len) != len) {
+ error_errno("list xattrs for %s", source);
+ free(buf);
+ return (-1);
+ }
+ *names_buf = buf;
+ *names_len = len;
+ return (0);
+}
+
+static int
+path_list_xattrs(const char *source, bool nofollow, char **names_buf, ssize_t *names_len)
+{
+ ssize_t len;
+ char *buf;
+
+ if (nofollow)
+ len = llistxattr(source, NULL, 0);
+ else
+ len = listxattr(source, NULL, 0);
+ if (len < 0) {
+ if (errno == ENOTSUP || errno == EOPNOTSUPP) {
+ *names_buf = NULL;
+ *names_len = 0;
+ return (0);
+ }
+ error_errno("list xattrs for %s", source);
+ return (-1);
+ }
+ if (len == 0) {
+ *names_buf = NULL;
+ *names_len = 0;
+ return (0);
+ }
+
+ buf = xmalloc((size_t)len);
+ if ((nofollow ? llistxattr(source, buf, (size_t)len) :
+ listxattr(source, buf, (size_t)len)) != len) {
+ error_errno("list xattrs for %s", source);
+ free(buf);
+ return (-1);
+ }
+ *names_buf = buf;
+ *names_len = len;
+ return (0);
+}
+
+static int
+set_xattr_loop(const char *source, const char *target, char *names,
+ ssize_t names_len, bool nofollow, int source_fd, int dest_fd)
+{
+ char *name;
+ char *limit;
+
+ limit = names + names_len;
+ for (name = names; name < limit; name += strlen(name) + 1) {
+ ssize_t value_len;
+ void *value;
+ int set_result;
+
+ if (source_fd >= 0)
+ value_len = fgetxattr(source_fd, name, NULL, 0);
+ else if (nofollow)
+ value_len = lgetxattr(source, name, NULL, 0);
+ else
+ value_len = getxattr(source, name, NULL, 0);
+ if (value_len < 0) {
+ error_errno("read xattr '%s' from %s", name, source);
+ return (-1);
+ }
+
+ value = xmalloc(value_len == 0 ? 1U : (size_t)value_len);
+ if (value_len > 0) {
+ if (source_fd >= 0)
+ value_len = fgetxattr(source_fd, name, value,
+ (size_t)value_len);
+ else if (nofollow)
+ value_len = lgetxattr(source, name, value,
+ (size_t)value_len);
+ else
+ value_len = getxattr(source, name, value,
+ (size_t)value_len);
+ if (value_len < 0) {
+ error_errno("read xattr '%s' from %s", name, source);
+ free(value);
+ return (-1);
+ }
+ }
+
+ if (dest_fd >= 0)
+ set_result = fsetxattr(dest_fd, name, value,
+ (size_t)value_len, 0);
+ else if (nofollow)
+ set_result = lsetxattr(target, name, value,
+ (size_t)value_len, 0);
+ else
+ set_result = setxattr(target, name, value,
+ (size_t)value_len, 0);
+ if (set_result != 0) {
+ error_errno("set xattr '%s' on %s", name, target);
+ free(value);
+ return (-1);
+ }
+ free(value);
+ }
+ return (0);
+}
+
+static int
+copy_file_xattrs(int source_fd, int dest_fd, const char *source, const char *target)
+{
+ char *names;
+ ssize_t names_len;
+ int rc;
+
+ names = NULL;
+ names_len = 0;
+ rc = file_list_xattrs(source_fd, source, &names, &names_len);
+ if (rc != 0)
+ return (rc);
+ if (names_len == 0)
+ return (0);
+
+ rc = set_xattr_loop(source, target, names, names_len, false, source_fd,
+ dest_fd);
+ free(names);
+ return (rc);
+}
+
+static int
+copy_path_xattrs(const char *source, const char *target, bool nofollow)
+{
+ char *names;
+ ssize_t names_len;
+ int rc;
+
+ names = NULL;
+ names_len = 0;
+ rc = path_list_xattrs(source, nofollow, &names, &names_len);
+ if (rc != 0)
+ return (rc);
+ if (names_len == 0)
+ return (0);
+
+ rc = set_xattr_loop(source, target, names, names_len, nofollow, -1, -1);
+ free(names);
+ return (rc);
+}
+
+static int
+preserve_timestamps_fd(int fd, const struct stat *source_sb, const char *target)
+{
+ struct timespec times[2];
+
+ times[0] = source_sb->st_atim;
+ times[1] = source_sb->st_mtim;
+ if (futimens(fd, times) != 0) {
+ error_errno("set times on %s", target);
+ return (-1);
+ }
+ return (0);
+}
+
+static int
+preserve_timestamps_path(const char *target, const struct stat *source_sb,
+ bool nofollow)
+{
+ struct timespec times[2];
+ int flags;
+
+ times[0] = source_sb->st_atim;
+ times[1] = source_sb->st_mtim;
+ flags = nofollow ? AT_SYMLINK_NOFOLLOW : 0;
+ if (utimensat(AT_FDCWD, target, times, flags) != 0) {
+ error_errno("set times on %s", target);
+ return (-1);
+ }
+ return (0);
+}
+
+static int
+set_fd_ownership_and_mode(int fd, const struct stat *source_sb, const char *target)
+{
+ struct stat current_sb;
+ mode_t mode;
+
+ if (fstat(fd, &current_sb) != 0) {
+ error_errno("stat %s", target);
+ return (-1);
+ }
+
+ mode = source_sb->st_mode & 07777;
+ if (current_sb.st_uid != source_sb->st_uid ||
+ current_sb.st_gid != source_sb->st_gid) {
+ if (fchown(fd, source_sb->st_uid, source_sb->st_gid) != 0) {
+ if (errno != EPERM) {
+ error_errno("set owner on %s", target);
+ return (-1);
+ }
+ mode &= ~(S_ISUID | S_ISGID);
+ }
+ }
+
+ if ((current_sb.st_mode & 07777) != mode && fchmod(fd, mode) != 0) {
+ error_errno("set mode on %s", target);
+ return (-1);
+ }
+
+ return (0);
+}
+
+static int
+set_path_ownership_and_mode(const char *target, const struct stat *source_sb,
+ bool nofollow)
+{
+ struct stat current_sb;
+ mode_t mode;
+ int flags;
+
+ flags = nofollow ? AT_SYMLINK_NOFOLLOW : 0;
+ if ((nofollow ? lstat(target, &current_sb) : stat(target, &current_sb)) != 0) {
+ error_errno("stat %s", target);
+ return (-1);
+ }
+
+ mode = source_sb->st_mode & 07777;
+ if (current_sb.st_uid != source_sb->st_uid ||
+ current_sb.st_gid != source_sb->st_gid) {
+ if (fchownat(AT_FDCWD, target, source_sb->st_uid, source_sb->st_gid,
+ flags) != 0) {
+ if (errno != EPERM) {
+ error_errno("set owner on %s", target);
+ return (-1);
+ }
+ mode &= ~(S_ISUID | S_ISGID);
+ }
+ }
+
+ if (!nofollow && (current_sb.st_mode & 07777) != mode &&
+ fchmodat(AT_FDCWD, target, mode, 0) != 0) {
+ error_errno("set mode on %s", target);
+ return (-1);
+ }
+
+return (0);
+}
+
+static int
+apply_path_metadata(const char *target, const struct stat *source_sb,
+ bool nofollow, const char *source)
+{
+ if (set_path_ownership_and_mode(target, source_sb, nofollow) != 0)
+ return (-1);
+ if (copy_path_xattrs(source, target, nofollow) != 0)
+ return (-1);
+ return (preserve_timestamps_path(target, source_sb, nofollow));
+}
+
+static size_t
+copy_buffer_size(void)
+{
+ long pages;
+ long pagesize;
+ size_t size;
+
+ pages = sysconf(_SC_PHYS_PAGES);
+ pagesize = sysconf(_SC_PAGESIZE);
+ if (pages > 0 && pagesize > 0) {
+ uint64_t total;
+
+ total = (uint64_t)pages * (uint64_t)pagesize;
+ if (total >= (uint64_t)(512U * 1024U * 1024U))
+ return (COPY_BUFFER_MAX);
+ }
+ size = COPY_BUFFER_MIN;
+ return (size);
+}
+
+static int
+copy_file_data(int from_fd, int to_fd, const char *source, const char *target)
+{
+ char *buffer;
+ size_t buffer_size;
+
+ buffer_size = copy_buffer_size();
+ buffer = xmalloc(buffer_size);
+ for (;;) {
+ ssize_t read_count;
+ char *outp;
+
+ read_count = read(from_fd, buffer, buffer_size);
+ if (read_count == 0)
+ break;
+ if (read_count < 0) {
+ if (errno == EINTR)
+ continue;
+ error_errno("read %s", source);
+ free(buffer);
+ return (-1);
+ }
+
+ outp = buffer;
+ while (read_count > 0) {
+ ssize_t write_count;
+
+ write_count = write(to_fd, outp, (size_t)read_count);
+ if (write_count < 0) {
+ if (errno == EINTR)
+ continue;
+ error_errno("write %s", target);
+ free(buffer);
+ return (-1);
+ }
+ outp += write_count;
+ read_count -= write_count;
+ }
+ }
+ free(buffer);
+ return (0);
+}
+
+static int
+copy_regular_file(const char *source, const char *target, const struct stat *source_sb)
+{
+ int source_fd;
+ int target_fd;
+ int saved_errno;
+ int rc;
+
+ source_fd = open(source, O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
+ if (source_fd < 0) {
+ error_errno("%s", source);
+ return (-1);
+ }
+
+ target_fd = open(target, O_WRONLY | O_CREAT | O_EXCL | O_TRUNC | O_CLOEXEC,
+ 0600);
+ if (target_fd < 0) {
+ error_errno("%s", target);
+ (void)close(source_fd);
+ return (-1);
+ }
+
+ rc = copy_file_data(source_fd, target_fd, source, target);
+ if (rc == 0 && set_fd_ownership_and_mode(target_fd, source_sb, target) != 0)
+ rc = -1;
+ if (rc == 0 && copy_file_xattrs(source_fd, target_fd, source, target) != 0)
+ rc = -1;
+ if (rc == 0 && preserve_timestamps_fd(target_fd, source_sb, target) != 0)
+ rc = -1;
+
+ saved_errno = errno;
+ if (close(target_fd) != 0 && rc == 0) {
+ error_errno("%s", target);
+ rc = -1;
+ saved_errno = errno;
+ }
+ (void)close(source_fd);
+
+ if (rc != 0) {
+ errno = saved_errno;
+ (void)unlink(target);
+ return (-1);
+ }
+
+ return (0);
+}
+
+static int
+copy_non_regular(const char *source, const char *target, const struct stat *source_sb)
+{
+ char *link_target;
+
+ if (S_ISLNK(source_sb->st_mode)) {
+ link_target = NULL;
+ if (readlink_alloc(source, &link_target) != 0) {
+ error_errno("readlink %s", source);
+ return (-1);
+ }
+ if (symlink(link_target, target) != 0) {
+ error_errno("symlink %s", target);
+ free(link_target);
+ return (-1);
+ }
+ free(link_target);
+ if (apply_path_metadata(target, source_sb, true, source) != 0) {
+ (void)unlink(target);
+ return (-1);
+ }
+ return (0);
+ }
+
+ if (S_ISFIFO(source_sb->st_mode)) {
+ if (mkfifo(target, source_sb->st_mode & 07777) != 0) {
+ error_errno("mkfifo %s", target);
+ return (-1);
+ }
+ if (apply_path_metadata(target, source_sb, false, source) != 0) {
+ (void)unlink(target);
+ return (-1);
+ }
+ return (0);
+ }
+
+ if (S_ISCHR(source_sb->st_mode) || S_ISBLK(source_sb->st_mode)) {
+ if (mknod(target, source_sb->st_mode, source_sb->st_rdev) != 0) {
+ error_errno("mknod %s", target);
+ return (-1);
+ }
+ if (apply_path_metadata(target, source_sb, false, source) != 0) {
+ (void)unlink(target);
+ return (-1);
+ }
+ return (0);
+ }
+
+ if (S_ISSOCK(source_sb->st_mode))
+ error_msg("cannot move socket across filesystems: %s", source);
+ else
+ error_msg("unsupported file type for cross-filesystem move: %s",
+ source);
+ return (-1);
+}
+
+static int
+copy_directory_tree(const char *source, const char *target, const struct stat *source_sb)
+{
+ DIR *dirp;
+ struct dirent *entry;
+ mode_t mode;
+ int rc;
+
+ mode = (source_sb->st_mode & 07777) | S_IRWXU;
+ if (mkdir(target, mode) != 0) {
+ error_errno("mkdir %s", target);
+ return (-1);
+ }
+
+ dirp = opendir(source);
+ if (dirp == NULL) {
+ error_errno("opendir %s", source);
+ (void)rmdir(target);
+ return (-1);
+ }
+
+ rc = 0;
+ while ((entry = readdir(dirp)) != NULL) {
+ struct stat child_sb;
+ char *child_source;
+ char *child_target;
+
+ if ((entry->d_name[0] == '.' && entry->d_name[1] == '\0') ||
+ (entry->d_name[0] == '.' && entry->d_name[1] == '.' &&
+ entry->d_name[2] == '\0'))
+ continue;
+
+ child_source = join_path(source, entry->d_name);
+ child_target = join_path(target, entry->d_name);
+ if (lstat(child_source, &child_sb) != 0) {
+ error_errno("%s", child_source);
+ free(child_source);
+ free(child_target);
+ rc = -1;
+ break;
+ }
+ if (copy_node(child_source, child_target, &child_sb) != 0) {
+ free(child_source);
+ free(child_target);
+ rc = -1;
+ break;
+ }
+ free(child_source);
+ free(child_target);
+ }
+
+ if (closedir(dirp) != 0 && rc == 0) {
+ error_errno("closedir %s", source);
+ rc = -1;
+ }
+
+ if (rc != 0)
+ return (-1);
+
+ if (apply_path_metadata(target, source_sb, false, source) != 0)
+ return (-1);
+
+ return (0);
+}
+
+static int
+copy_node(const char *source, const char *target, const struct stat *source_sb)
+{
+ if (S_ISREG(source_sb->st_mode))
+ return (copy_regular_file(source, target, source_sb));
+ if (S_ISDIR(source_sb->st_mode))
+ return (copy_directory_tree(source, target, source_sb));
+ return (copy_non_regular(source, target, source_sb));
+}
+
+static int
+remove_source_tree(const char *path, const struct stat *sb)
+{
+ DIR *dirp;
+ struct dirent *entry;
+ int rc;
+
+ if (!S_ISDIR(sb->st_mode)) {
+ if (unlink(path) != 0) {
+ error_errno("remove %s", path);
+ return (-1);
+ }
+ return (0);
+ }
+
+ dirp = opendir(path);
+ if (dirp == NULL) {
+ error_errno("opendir %s", path);
+ return (-1);
+ }
+
+ rc = 0;
+ while ((entry = readdir(dirp)) != NULL) {
+ struct stat child_sb;
+ char *child_path;
+
+ if ((entry->d_name[0] == '.' && entry->d_name[1] == '\0') ||
+ (entry->d_name[0] == '.' && entry->d_name[1] == '.' &&
+ entry->d_name[2] == '\0'))
+ continue;
+
+ child_path = join_path(path, entry->d_name);
+ if (lstat(child_path, &child_sb) != 0) {
+ error_errno("%s", child_path);
+ free(child_path);
+ rc = -1;
+ break;
+ }
+ if (remove_source_tree(child_path, &child_sb) != 0) {
+ free(child_path);
+ rc = -1;
+ break;
+ }
+ free(child_path);
+ }
+
+ if (closedir(dirp) != 0 && rc == 0) {
+ error_errno("closedir %s", path);
+ rc = -1;
+ }
+ if (rc != 0)
+ return (-1);
+
+ if (rmdir(path) != 0) {
+ error_errno("remove %s", path);
+ return (-1);
+ }
+ return (0);
+}
+
+static int
+cleanup_failed_target(const char *target)
+{
+ struct stat sb;
+
+ if (lstat(target, &sb) != 0) {
+ if (errno == ENOENT)
+ return (0);
+ error_errno("%s", target);
+ return (-1);
+ }
+ return (remove_source_tree(target, &sb));
+}
+
+static int
+remove_target_path(const char *target, const struct stat *target_sb)
+{
+ if (S_ISDIR(target_sb->st_mode)) {
+ if (rmdir(target) != 0) {
+ error_errno("remove %s", target);
+ return (-1);
+ }
+ return (0);
+ }
+ if (unlink(target) != 0) {
+ error_errno("remove %s", target);
+ return (-1);
+ }
+ return (0);
+}
+
+static int
+is_mount_point(const char *path, const struct stat *source_sb, bool *mount_point)
+{
+ char *parent;
+ struct stat parent_sb;
+
+ *mount_point = false;
+ parent = dirname_dup(path);
+ if (lstat(parent, &parent_sb) != 0) {
+ error_errno("%s", parent);
+ free(parent);
+ return (-1);
+ }
+ if (source_sb->st_dev != parent_sb.st_dev)
+ *mount_point = true;
+
+ if (!*mount_point && lstat(parent, &parent_sb) == 0 &&
+ source_sb->st_dev == parent_sb.st_dev &&
+ source_sb->st_ino == parent_sb.st_ino)
+ *mount_point = true;
+
+ free(parent);
+ return (0);
+}
+
+static int
+copy_move_fallback(const struct mv_options *options, const char *source,
+ const char *target, const struct stat *source_sb, const struct stat *target_sb)
+{
+ bool mount_point;
+
+ if (S_ISDIR(source_sb->st_mode)) {
+ if (is_mount_point(source, source_sb, &mount_point) != 0)
+ return (-1);
+ if (mount_point) {
+ error_msg("cannot move mount point across filesystems: %s",
+ source);
+ return (-1);
+ }
+ }
+
+ if (target_sb != NULL && remove_target_path(target, target_sb) != 0)
+ return (-1);
+
+ if (copy_node(source, target, source_sb) != 0) {
+ (void)cleanup_failed_target(target);
+ return (-1);
+ }
+
+ if (remove_source_tree(source, source_sb) != 0)
+ return (-1);
+
+ if (options->verbose)
+ (void)printf("%s -> %s\n", source, target);
+ return (0);
+}
+
+static int
+handle_single_move(const struct mv_options *options, const char *source,
+ const char *target)
+{
+ struct stat source_sb;
+ struct stat target_sb;
+ bool target_exists;
+ bool skip_move;
+
+ if (lstat(source, &source_sb) != 0) {
+ error_errno("%s", source);
+ return (MV_EXIT_ERROR);
+ }
+
+ target_exists = false;
+ if (lstat(target, &target_sb) == 0)
+ target_exists = true;
+ else if (errno != ENOENT) {
+ error_errno("%s", target);
+ return (MV_EXIT_ERROR);
+ }
+
+ if (target_exists) {
+ if (apply_existing_target_policy(options, target, &skip_move) != 0)
+ return (MV_EXIT_ERROR);
+ if (skip_move)
+ return (0);
+
+ if (S_ISDIR(source_sb.st_mode) && !S_ISDIR(target_sb.st_mode)) {
+ errno = ENOTDIR;
+ error_errno("%s", target);
+ return (MV_EXIT_ERROR);
+ }
+ if (!S_ISDIR(source_sb.st_mode) && S_ISDIR(target_sb.st_mode)) {
+ errno = EISDIR;
+ error_errno("%s", target);
+ return (MV_EXIT_ERROR);
+ }
+ }
+
+ if (rename(source, target) == 0) {
+ if (options->verbose)
+ (void)printf("%s -> %s\n", source, target);
+ return (0);
+ }
+
+ if (errno != EXDEV) {
+ error_errno("rename %s to %s", source, target);
+ return (MV_EXIT_ERROR);
+ }
+
+ if (copy_move_fallback(options, source, target, &source_sb,
+ target_exists ? &target_sb : NULL) != 0)
+ return (MV_EXIT_ERROR);
+
+ return (0);
+}
+
+static int
+is_dir_symlink(const char *path)
+{
+ struct stat lst;
+ struct stat st;
+
+ if (lstat(path, &lst) != 0)
+ return (0);
+ if (!S_ISLNK(lst.st_mode))
+ return (0);
+ if (stat(path, &st) != 0)
+ return (0);
+ return (S_ISDIR(st.st_mode) ? 1 : 0);
+}
+
+static int
+determine_target_mode(const struct mv_options *options, int argc, char *const argv[],
+ struct move_target *target)
+{
+ struct stat sb;
+ int stat_rc;
+ bool treat_as_directory;
+
+ treat_as_directory = false;
+ stat_rc = stat(argv[argc - 1], &sb);
+ if (!(options->no_target_dir_follow && argc == 2 &&
+ is_dir_symlink(argv[argc - 1])) &&
+ stat_rc == 0 && S_ISDIR(sb.st_mode))
+ treat_as_directory = true;
+
+ if (!treat_as_directory && argc > 2) {
+ if (stat_rc != 0 && errno != ENOENT)
+ error_errno("%s", argv[argc - 1]);
+ else
+ error_msg("%s is not a directory", argv[argc - 1]);
+ return (-1);
+ }
+
+ target->treat_as_directory = treat_as_directory;
+ target->path = argv[argc - 1];
+ return (0);
+}
+
+int
+main(int argc, char *argv[])
+{
+ struct move_target target;
+ struct mv_options options;
+ int ch;
+ int rval;
+
+ progname = program_name(argv[0]);
+ memset(&options, 0, sizeof(options));
+
+ while ((ch = getopt(argc, argv, "+fhinv")) != -1) {
+ switch (ch) {
+ case 'f':
+ options.force = true;
+ options.interactive = false;
+ options.no_clobber = false;
+ break;
+ case 'h':
+ options.no_target_dir_follow = true;
+ break;
+ case 'i':
+ options.interactive = true;
+ options.force = false;
+ options.no_clobber = false;
+ break;
+ case 'n':
+ options.no_clobber = true;
+ options.force = false;
+ options.interactive = false;
+ break;
+ case 'v':
+ options.verbose = true;
+ break;
+ default:
+ usage();
+ }
+ }
+
+ argc -= optind;
+ argv += optind;
+ if (argc < 2)
+ usage();
+ if (options.no_target_dir_follow && argc != 2)
+ usage();
+ if (determine_target_mode(&options, argc, argv, &target) != 0)
+ return (MV_EXIT_ERROR);
+
+ if (!target.treat_as_directory)
+ return (handle_single_move(&options, argv[0], target.path));
+
+ rval = 0;
+ for (int i = 0; i < argc - 1; i++) {
+ char *child_target;
+
+ append_child_basename(target.path, argv[i], &child_target);
+ if (handle_single_move(&options, argv[i], child_target) != 0)
+ rval = MV_EXIT_ERROR;
+ free(child_target);
+ }
+ return (rval);
+}
diff --git a/corebinutils/mv/tests/test.sh b/corebinutils/mv/tests/test.sh
new file mode 100644
index 0000000000..c2cd08c8a8
--- /dev/null
+++ b/corebinutils/mv/tests/test.sh
@@ -0,0 +1,414 @@
+#!/bin/sh
+set -eu
+
+ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
+MV_BIN=${MV_BIN:-"$ROOT/out/mv"}
+TMPDIR=${TMPDIR:-/tmp}
+WORKDIR=$(mktemp -d "$TMPDIR/mv-test.XXXXXX")
+STDOUT_FILE="$WORKDIR/stdout"
+STDERR_FILE="$WORKDIR/stderr"
+LAST_STATUS=0
+LAST_STDOUT=
+LAST_STDERR=
+ALTROOT=
+
+cleanup() {
+ rm -rf "$WORKDIR"
+ if [ -n "${ALTROOT:-}" ] && [ -d "$ALTROOT" ]; then
+ rm -rf "$ALTROOT"
+ fi
+}
+trap cleanup EXIT INT TERM
+
+export LC_ALL=C
+
+MV_USAGE=$(cat <<'EOF'
+usage: mv [-f | -i | -n] [-hv] source target
+ mv [-f | -i | -n] [-v] source ... directory
+EOF
+)
+
+fail() {
+ printf '%s\n' "FAIL: $1" >&2
+ exit 1
+}
+
+assert_eq() {
+ name=$1
+ expected=$2
+ actual=$3
+ if [ "$expected" != "$actual" ]; then
+ printf '%s\n' "FAIL: $name" >&2
+ printf '%s\n' "--- expected ---" >&2
+ printf '%s' "$expected" >&2
+ printf '\n%s\n' "--- actual ---" >&2
+ printf '%s' "$actual" >&2
+ printf '\n' >&2
+ exit 1
+ fi
+}
+
+assert_contains() {
+ name=$1
+ text=$2
+ pattern=$3
+ case $text in
+ *"$pattern"*) ;;
+ *) fail "$name" ;;
+ esac
+}
+
+assert_empty() {
+ name=$1
+ text=$2
+ [ -z "$text" ] || fail "$name"
+}
+
+assert_exists() {
+ path=$1
+ [ -e "$path" ] || fail "missing path: $path"
+}
+
+assert_not_exists() {
+ path=$1
+ [ ! -e "$path" ] || fail "unexpected path: $path"
+}
+
+assert_status() {
+ name=$1
+ expected=$2
+ actual=$3
+ [ "$expected" -eq "$actual" ] || fail "$name"
+}
+
+assert_file_text() {
+ expected=$1
+ path=$2
+ actual=$(cat "$path")
+ [ "$actual" = "$expected" ] || fail "content mismatch: $path"
+}
+
+assert_mode() {
+ expected=$1
+ path=$2
+ actual=$(stat -c '%a' "$path")
+ [ "$actual" = "$expected" ] || fail "mode mismatch: $path"
+}
+
+assert_symlink_target() {
+ expected=$1
+ path=$2
+ [ -L "$path" ] || fail "not a symlink: $path"
+ actual=$(readlink "$path")
+ [ "$actual" = "$expected" ] || fail "symlink mismatch: $path"
+}
+
+inode_key() {
+ stat -c '%d:%i' "$1"
+}
+
+assert_same_inode() {
+ path1=$1
+ path2=$2
+ [ "$(inode_key "$path1")" = "$(inode_key "$path2")" ] || \
+ fail "inode mismatch: $path1 $path2"
+}
+
+assert_different_inode() {
+ path1=$1
+ path2=$2
+ [ "$(inode_key "$path1")" != "$(inode_key "$path2")" ] || \
+ fail "unexpected shared inode: $path1 $path2"
+}
+
+run_capture() {
+ if "$@" >"$STDOUT_FILE" 2>"$STDERR_FILE"; then
+ LAST_STATUS=0
+ else
+ LAST_STATUS=$?
+ fi
+ LAST_STDOUT=$(cat "$STDOUT_FILE")
+ LAST_STDERR=$(cat "$STDERR_FILE")
+}
+
+find_alt_parent() {
+ base_dev=$(stat -c '%d' "$WORKDIR")
+ uid=$(id -u)
+
+ for candidate in /dev/shm "$TMPDIR" /tmp /var/tmp "/run/user/$uid"; do
+ [ -d "$candidate" ] || continue
+ [ -w "$candidate" ] || continue
+ probe=$(mktemp -d "$candidate/mv-test-probe.XXXXXX" 2>/dev/null) || continue
+ probe_dev=$(stat -c '%d' "$probe" 2>/dev/null || true)
+ rm -rf "$probe"
+ [ -n "$probe_dev" ] || continue
+ if [ "$probe_dev" != "$base_dev" ]; then
+ printf '%s\n' "$candidate"
+ return 0
+ fi
+ done
+ return 1
+}
+
+setup_cross_device_root() {
+ alt_parent=$(find_alt_parent) || return 1
+ ALTROOT=$(mktemp -d "$alt_parent/mv-test-fs.XXXXXX")
+ return 0
+}
+
+[ -x "$MV_BIN" ] || fail "missing binary: $MV_BIN"
+
+run_capture "$MV_BIN"
+assert_status "usage status" 2 "$LAST_STATUS"
+assert_empty "usage stdout" "$LAST_STDOUT"
+assert_eq "usage stderr" "$MV_USAGE" "$LAST_STDERR"
+
+run_capture "$MV_BIN" -h a b c
+assert_status "invalid -h usage status" 2 "$LAST_STATUS"
+assert_empty "invalid -h usage stdout" "$LAST_STDOUT"
+assert_eq "invalid -h usage stderr" "$MV_USAGE" "$LAST_STDERR"
+
+mkdir "$WORKDIR/basic"
+printf 'hello\n' >"$WORKDIR/basic/src"
+run_capture "$MV_BIN" "$WORKDIR/basic/src" "$WORKDIR/basic/dst"
+assert_status "basic rename status" 0 "$LAST_STATUS"
+assert_empty "basic rename stdout" "$LAST_STDOUT"
+assert_empty "basic rename stderr" "$LAST_STDERR"
+assert_file_text "hello" "$WORKDIR/basic/dst"
+assert_not_exists "$WORKDIR/basic/src"
+
+mkdir "$WORKDIR/into-dir"
+printf 'alpha\n' >"$WORKDIR/into-dir/src"
+mkdir "$WORKDIR/into-dir/out"
+run_capture "$MV_BIN" "$WORKDIR/into-dir/src" "$WORKDIR/into-dir/out"
+assert_status "move into dir status" 0 "$LAST_STATUS"
+assert_file_text "alpha" "$WORKDIR/into-dir/out/src"
+assert_not_exists "$WORKDIR/into-dir/src"
+
+mkdir "$WORKDIR/hflag"
+mkdir "$WORKDIR/hflag/actual"
+ln -s actual "$WORKDIR/hflag/linkdir"
+printf 'first\n' >"$WORKDIR/hflag/src1"
+run_capture "$MV_BIN" "$WORKDIR/hflag/src1" "$WORKDIR/hflag/linkdir"
+assert_status "default symlink-dir target status" 0 "$LAST_STATUS"
+assert_file_text "first" "$WORKDIR/hflag/actual/src1"
+printf 'second\n' >"$WORKDIR/hflag/src2"
+run_capture "$MV_BIN" -h "$WORKDIR/hflag/src2" "$WORKDIR/hflag/linkdir"
+assert_status "h replace symlink status" 0 "$LAST_STATUS"
+assert_file_text "second" "$WORKDIR/hflag/linkdir"
+[ ! -L "$WORKDIR/hflag/linkdir" ] || fail "-h did not replace symlink target"
+
+mkdir "$WORKDIR/nflag"
+printf 'source\n' >"$WORKDIR/nflag/src"
+printf 'target\n' >"$WORKDIR/nflag/dst"
+run_capture "$MV_BIN" -n "$WORKDIR/nflag/src" "$WORKDIR/nflag/dst"
+assert_status "n status" 0 "$LAST_STATUS"
+assert_empty "n stdout" "$LAST_STDOUT"
+assert_empty "n stderr" "$LAST_STDERR"
+assert_file_text "source" "$WORKDIR/nflag/src"
+assert_file_text "target" "$WORKDIR/nflag/dst"
+
+printf 'source\n' >"$WORKDIR/nflag/src2"
+printf 'target\n' >"$WORKDIR/nflag/dst2"
+run_capture "$MV_BIN" -vn "$WORKDIR/nflag/src2" "$WORKDIR/nflag/dst2"
+assert_status "vn status" 0 "$LAST_STATUS"
+assert_eq "vn stdout" "$WORKDIR/nflag/dst2 not overwritten" "$LAST_STDOUT"
+assert_empty "vn stderr" "$LAST_STDERR"
+assert_file_text "source" "$WORKDIR/nflag/src2"
+assert_file_text "target" "$WORKDIR/nflag/dst2"
+
+mkdir "$WORKDIR/interactive-no"
+printf 'source\n' >"$WORKDIR/interactive-no/src"
+printf 'target\n' >"$WORKDIR/interactive-no/dst"
+if printf 'n\n' | "$MV_BIN" -i "$WORKDIR/interactive-no/src" \
+ "$WORKDIR/interactive-no/dst" >"$STDOUT_FILE" 2>"$STDERR_FILE"; then
+ LAST_STATUS=0
+else
+ LAST_STATUS=$?
+fi
+LAST_STDOUT=$(cat "$STDOUT_FILE")
+LAST_STDERR=$(cat "$STDERR_FILE")
+assert_status "interactive no status" 0 "$LAST_STATUS"
+assert_empty "interactive no stdout" "$LAST_STDOUT"
+assert_contains "interactive no prompt" "$LAST_STDERR" \
+ "overwrite $WORKDIR/interactive-no/dst? (y/n [n]) "
+assert_contains "interactive no rejection" "$LAST_STDERR" "not overwritten"
+assert_file_text "source" "$WORKDIR/interactive-no/src"
+assert_file_text "target" "$WORKDIR/interactive-no/dst"
+
+mkdir "$WORKDIR/interactive-yes"
+printf 'source\n' >"$WORKDIR/interactive-yes/src"
+printf 'target\n' >"$WORKDIR/interactive-yes/dst"
+if printf 'y\n' | "$MV_BIN" -i "$WORKDIR/interactive-yes/src" \
+ "$WORKDIR/interactive-yes/dst" >"$STDOUT_FILE" 2>"$STDERR_FILE"; then
+ LAST_STATUS=0
+else
+ LAST_STATUS=$?
+fi
+LAST_STDOUT=$(cat "$STDOUT_FILE")
+LAST_STDERR=$(cat "$STDERR_FILE")
+assert_status "interactive yes status" 0 "$LAST_STATUS"
+assert_empty "interactive yes stdout" "$LAST_STDOUT"
+assert_contains "interactive yes prompt" "$LAST_STDERR" \
+ "overwrite $WORKDIR/interactive-yes/dst? (y/n [n]) "
+assert_not_exists "$WORKDIR/interactive-yes/src"
+assert_file_text "source" "$WORKDIR/interactive-yes/dst"
+
+mkdir "$WORKDIR/f-precedence"
+printf 'source\n' >"$WORKDIR/f-precedence/src"
+printf 'target\n' >"$WORKDIR/f-precedence/dst"
+run_capture "$MV_BIN" -i -n -f "$WORKDIR/f-precedence/src" "$WORKDIR/f-precedence/dst"
+assert_status "force precedence status" 0 "$LAST_STATUS"
+assert_empty "force precedence stderr" "$LAST_STDERR"
+assert_file_text "source" "$WORKDIR/f-precedence/dst"
+assert_not_exists "$WORKDIR/f-precedence/src"
+
+mkdir "$WORKDIR/dir-to-file"
+mkdir "$WORKDIR/dir-to-file/src"
+printf 'payload\n' >"$WORKDIR/dir-to-file/src/file"
+printf 'target\n' >"$WORKDIR/dir-to-file/dst"
+run_capture "$MV_BIN" "$WORKDIR/dir-to-file/src" "$WORKDIR/dir-to-file/dst"
+assert_status "dir-to-file status" 1 "$LAST_STATUS"
+assert_contains "dir-to-file stderr" "$LAST_STDERR" "Not a directory"
+assert_exists "$WORKDIR/dir-to-file/src/file"
+
+mkdir "$WORKDIR/file-to-dir"
+printf 'payload\n' >"$WORKDIR/file-to-dir/src"
+mkdir -p "$WORKDIR/file-to-dir/out/src"
+run_capture "$MV_BIN" "$WORKDIR/file-to-dir/src" "$WORKDIR/file-to-dir/out"
+assert_status "file-to-dir final dir status" 1 "$LAST_STATUS"
+assert_contains "file-to-dir stderr" "$LAST_STDERR" "Is a directory"
+assert_exists "$WORKDIR/file-to-dir/src"
+
+mkdir "$WORKDIR/verbose"
+printf 'payload\n' >"$WORKDIR/verbose/src"
+mkdir "$WORKDIR/verbose/out"
+run_capture "$MV_BIN" -v "$WORKDIR/verbose/src" "$WORKDIR/verbose/out"
+assert_status "verbose status" 0 "$LAST_STATUS"
+assert_eq "verbose stdout" \
+ "$WORKDIR/verbose/src -> $WORKDIR/verbose/out/src" "$LAST_STDOUT"
+assert_empty "verbose stderr" "$LAST_STDERR"
+
+mkdir "$WORKDIR/fifo"
+mkfifo "$WORKDIR/fifo/src"
+run_capture "$MV_BIN" "$WORKDIR/fifo/src" "$WORKDIR/fifo/dst"
+assert_status "fifo rename status" 0 "$LAST_STATUS"
+[ -p "$WORKDIR/fifo/dst" ] || fail "fifo move lost fifo type"
+assert_not_exists "$WORKDIR/fifo/src"
+
+if setup_cross_device_root; then
+ mkdir "$WORKDIR/cross-file"
+ printf 'cross-data\n' >"$WORKDIR/cross-file/src"
+ chmod 754 "$WORKDIR/cross-file/src"
+ touch -t 202001020304.05 "$WORKDIR/cross-file/src"
+ src_mtime=$(stat -c '%Y' "$WORKDIR/cross-file/src")
+ run_capture "$MV_BIN" "$WORKDIR/cross-file/src" "$ALTROOT/regular"
+ assert_status "cross regular status" 0 "$LAST_STATUS"
+ assert_file_text "cross-data" "$ALTROOT/regular"
+ assert_mode "754" "$ALTROOT/regular"
+ [ "$(stat -c '%Y' "$ALTROOT/regular")" = "$src_mtime" ] || fail "mtime mismatch after cross-device regular move"
+ assert_not_exists "$WORKDIR/cross-file/src"
+
+ mkdir "$WORKDIR/cross-link"
+ printf 'target\n' >"$WORKDIR/cross-link/data"
+ ln -s data "$WORKDIR/cross-link/src"
+ run_capture "$MV_BIN" "$WORKDIR/cross-link/src" "$ALTROOT/link"
+ assert_status "cross symlink status" 0 "$LAST_STATUS"
+ assert_symlink_target "data" "$ALTROOT/link"
+ assert_not_exists "$WORKDIR/cross-link/src"
+
+ mkdir "$WORKDIR/cross-tree"
+ mkdir -p "$WORKDIR/cross-tree/src/sub"
+ printf 'nested\n' >"$WORKDIR/cross-tree/src/sub/file"
+ ln -s sub/file "$WORKDIR/cross-tree/src/link"
+ mkfifo "$WORKDIR/cross-tree/src/pipe"
+ chmod 711 "$WORKDIR/cross-tree/src/sub"
+ run_capture "$MV_BIN" "$WORKDIR/cross-tree/src" "$ALTROOT"
+ assert_status "cross tree status" 0 "$LAST_STATUS"
+ assert_file_text "nested" "$ALTROOT/src/sub/file"
+ assert_symlink_target "sub/file" "$ALTROOT/src/link"
+ [ -p "$ALTROOT/src/pipe" ] || fail "cross tree fifo missing"
+ assert_mode "711" "$ALTROOT/src/sub"
+ assert_not_exists "$WORKDIR/cross-tree/src"
+
+ mkdir "$WORKDIR/cross-empty"
+ mkdir -p "$WORKDIR/cross-empty/src/child"
+ printf 'replacement\n' >"$WORKDIR/cross-empty/src/child/file"
+ mkdir -p "$ALTROOT/replace-empty/src"
+ run_capture "$MV_BIN" "$WORKDIR/cross-empty/src" "$ALTROOT/replace-empty"
+ assert_status "cross replace empty dir status" 0 "$LAST_STATUS"
+ assert_file_text "replacement" "$ALTROOT/replace-empty/src/child/file"
+ assert_not_exists "$WORKDIR/cross-empty/src"
+
+ mkdir "$WORKDIR/cross-nonempty"
+ mkdir -p "$WORKDIR/cross-nonempty/src/child"
+ printf 'source\n' >"$WORKDIR/cross-nonempty/src/child/file"
+ mkdir -p "$ALTROOT/replace-nonempty/src"
+ printf 'keep\n' >"$ALTROOT/replace-nonempty/src/existing"
+ run_capture "$MV_BIN" "$WORKDIR/cross-nonempty/src" "$ALTROOT/replace-nonempty"
+ assert_status "cross replace nonempty dir status" 1 "$LAST_STATUS"
+ assert_contains "cross replace nonempty dir stderr" "$LAST_STDERR" \
+ "Directory not empty"
+ assert_exists "$WORKDIR/cross-nonempty/src/child/file"
+ assert_file_text "keep" "$ALTROOT/replace-nonempty/src/existing"
+
+ mkdir "$WORKDIR/cross-fifo"
+ mkfifo "$WORKDIR/cross-fifo/src"
+ run_capture "$MV_BIN" "$WORKDIR/cross-fifo/src" "$ALTROOT/fifo"
+ assert_status "cross fifo status" 0 "$LAST_STATUS"
+ [ -p "$ALTROOT/fifo" ] || fail "cross-device fifo missing"
+ assert_not_exists "$WORKDIR/cross-fifo/src"
+
+ mkdir "$WORKDIR/cross-hardlinks"
+ mkdir "$WORKDIR/cross-hardlinks/hardlinks-src"
+ printf 'shared\n' >"$WORKDIR/cross-hardlinks/hardlinks-src/a"
+ ln "$WORKDIR/cross-hardlinks/hardlinks-src/a" \
+ "$WORKDIR/cross-hardlinks/hardlinks-src/b"
+ assert_same_inode "$WORKDIR/cross-hardlinks/hardlinks-src/a" \
+ "$WORKDIR/cross-hardlinks/hardlinks-src/b"
+ run_capture "$MV_BIN" "$WORKDIR/cross-hardlinks/hardlinks-src" "$ALTROOT"
+ assert_status "cross hardlink tree status" 0 "$LAST_STATUS"
+ assert_file_text "shared" "$ALTROOT/hardlinks-src/a"
+ assert_file_text "shared" "$ALTROOT/hardlinks-src/b"
+ assert_different_inode "$ALTROOT/hardlinks-src/a" \
+ "$ALTROOT/hardlinks-src/b"
+ assert_not_exists "$WORKDIR/cross-hardlinks/hardlinks-src"
+
+ alt_avail_kb=$(df -Pk "$ALTROOT" | awk 'NR==2 { print $4 }')
+ case $alt_avail_kb in
+ ''|*[!0-9]*) alt_avail_kb=0 ;;
+ esac
+ if [ "$alt_avail_kb" -ge 4096 ] && [ "$alt_avail_kb" -le 131072 ]; then
+ mkdir "$WORKDIR/cross-partial"
+ dd if=/dev/zero of="$WORKDIR/cross-partial/src" bs=1024 count=2048 \
+ >"$STDOUT_FILE" 2>"$STDERR_FILE" || fail "partial source creation failed"
+ fill_kb=$((alt_avail_kb - 1024))
+ if [ "$fill_kb" -gt 0 ] && dd if=/dev/zero of="$ALTROOT/.fill-enospc" \
+ bs=1024 count="$fill_kb" >"$STDOUT_FILE" 2>"$STDERR_FILE"; then
+ run_capture "$MV_BIN" "$WORKDIR/cross-partial/src" \
+ "$ALTROOT/partial-target"
+ [ "$LAST_STATUS" -ne 0 ] || fail "cross partial failure status"
+ assert_contains "cross partial failure stderr" "$LAST_STDERR" \
+ "No space left on device"
+ assert_exists "$WORKDIR/cross-partial/src"
+ assert_not_exists "$ALTROOT/partial-target"
+ fi
+ fi
+
+ if command -v setfattr >/dev/null 2>&1 && command -v getfattr >/dev/null 2>&1; then
+ printf 'probe\n' >"$WORKDIR/xattr-probe"
+ printf 'probe\n' >"$ALTROOT/xattr-probe"
+ if setfattr -n user.project_tick -v probe "$WORKDIR/xattr-probe" 2>/dev/null &&
+ setfattr -n user.project_tick -v probe "$ALTROOT/xattr-probe" 2>/dev/null; then
+ printf 'payload\n' >"$WORKDIR/xattr-src"
+ setfattr -n user.project_tick -v value "$WORKDIR/xattr-src"
+ run_capture "$MV_BIN" "$WORKDIR/xattr-src" "$ALTROOT/xattr-dst"
+ assert_status "cross xattr status" 0 "$LAST_STATUS"
+ xattr_value=$(getfattr --only-values -n user.project_tick "$ALTROOT/xattr-dst" 2>/dev/null)
+ assert_eq "cross xattr value" "value" "$xattr_value"
+ assert_not_exists "$WORKDIR/xattr-src"
+ fi
+ fi
+fi
+
+printf '%s\n' "PASS"