summaryrefslogtreecommitdiff
path: root/corebinutils
diff options
context:
space:
mode:
authorMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:23:44 +0300
committerMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:23:44 +0300
commitbd39aec8766c747a5a85a90ec5755a96b8a23cf1 (patch)
tree753e241b84b3b3550ea4771f1e3a8c4a22e238af /corebinutils
parent729788b336bbcf127179ac655ccf3b3e366f4ca6 (diff)
parent07152ace4e0e484f31e08030227a3d91bc803335 (diff)
downloadProject-Tick-bd39aec8766c747a5a85a90ec5755a96b8a23cf1.tar.gz
Project-Tick-bd39aec8766c747a5a85a90ec5755a96b8a23cf1.zip
Add 'corebinutils/cp/' from commit '07152ace4e0e484f31e08030227a3d91bc803335'
git-subtree-dir: corebinutils/cp git-subtree-mainline: 729788b336bbcf127179ac655ccf3b3e366f4ca6 git-subtree-split: 07152ace4e0e484f31e08030227a3d91bc803335
Diffstat (limited to 'corebinutils')
-rw-r--r--corebinutils/cp/.gitignore25
-rw-r--r--corebinutils/cp/GNUmakefile41
-rw-r--r--corebinutils/cp/LICENSE32
-rw-r--r--corebinutils/cp/LICENSES/BSD-2-Clause.txt9
-rw-r--r--corebinutils/cp/LICENSES/BSD-3-Clause.txt11
-rw-r--r--corebinutils/cp/README.md23
-rw-r--r--corebinutils/cp/cp.1346
-rw-r--r--corebinutils/cp/cp.c758
-rw-r--r--corebinutils/cp/extern.h46
-rw-r--r--corebinutils/cp/fts.c465
-rw-r--r--corebinutils/cp/fts.h94
-rw-r--r--corebinutils/cp/tests/test.sh71
-rw-r--r--corebinutils/cp/utils.c527
13 files changed, 2448 insertions, 0 deletions
diff --git a/corebinutils/cp/.gitignore b/corebinutils/cp/.gitignore
new file mode 100644
index 0000000000..a74d30b48c
--- /dev/null
+++ b/corebinutils/cp/.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/cp/GNUmakefile b/corebinutils/cp/GNUmakefile
new file mode 100644
index 0000000000..10a00bc36c
--- /dev/null
+++ b/corebinutils/cp/GNUmakefile
@@ -0,0 +1,41 @@
+.DEFAULT_GOAL := all
+
+CC ?= cc
+CPPFLAGS += -D_GNU_SOURCE -I$(CURDIR)
+CFLAGS ?= -O2
+CFLAGS += -g -Wall -Wextra -Wno-unused-parameter
+LDFLAGS ?=
+LDLIBS ?=
+
+OBJDIR := $(CURDIR)/build
+OUTDIR := $(CURDIR)/out
+TARGET := $(OUTDIR)/cp
+OBJS := $(OBJDIR)/cp.o $(OBJDIR)/utils.o $(OBJDIR)/fts.o
+
+.PHONY: all clean dirs test status
+
+all: $(TARGET)
+
+dirs:
+ @mkdir -p "$(OBJDIR)" "$(OUTDIR)"
+
+$(TARGET): $(OBJS) | dirs
+ $(CC) $(LDFLAGS) -o "$@" $(OBJS) $(LDLIBS)
+
+$(OBJDIR)/cp.o: $(CURDIR)/cp.c $(CURDIR)/extern.h $(CURDIR)/fts.h | dirs
+ $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/cp.c" -o "$@"
+
+$(OBJDIR)/utils.o: $(CURDIR)/utils.c $(CURDIR)/extern.h $(CURDIR)/fts.h | dirs
+ $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/utils.c" -o "$@"
+
+$(OBJDIR)/fts.o: $(CURDIR)/fts.c $(CURDIR)/fts.h | dirs
+ $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/fts.c" -o "$@"
+
+test: $(TARGET)
+ CP_BIN="$(TARGET)" sh "$(CURDIR)/tests/test.sh"
+
+status:
+ @printf '%s\n' "$(TARGET)"
+
+clean:
+ @rm -rf "$(CURDIR)/build" "$(CURDIR)/out"
diff --git a/corebinutils/cp/LICENSE b/corebinutils/cp/LICENSE
new file mode 100644
index 0000000000..36b035840f
--- /dev/null
+++ b/corebinutils/cp/LICENSE
@@ -0,0 +1,32 @@
+Copyright (c) 1988, 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
+Kevin Fall.
+
+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. \ No newline at end of file
diff --git a/corebinutils/cp/LICENSES/BSD-2-Clause.txt b/corebinutils/cp/LICENSES/BSD-2-Clause.txt
new file mode 100644
index 0000000000..5f662b354c
--- /dev/null
+++ b/corebinutils/cp/LICENSES/BSD-2-Clause.txt
@@ -0,0 +1,9 @@
+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.
+
+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/cp/LICENSES/BSD-3-Clause.txt b/corebinutils/cp/LICENSES/BSD-3-Clause.txt
new file mode 100644
index 0000000000..ea890afbc7
--- /dev/null
+++ b/corebinutils/cp/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/cp/README.md b/corebinutils/cp/README.md
new file mode 100644
index 0000000000..4808f470db
--- /dev/null
+++ b/corebinutils/cp/README.md
@@ -0,0 +1,23 @@
+# cp
+
+Standalone musl-libc-based Linux port of FreeBSD `cp` 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
+```
+
+## Notes
+
+- Port strategy is minimum-diff: the original `cp.c` / `utils.c` flow is kept as much as possible.
+- `fts(3)` is provided locally in this project because musl does not ship `<fts.h>`.
+- FreeBSD-only ACL and file flag preservation paths are disabled on non-FreeBSD builds.
diff --git a/corebinutils/cp/cp.1 b/corebinutils/cp/cp.1
new file mode 100644
index 0000000000..6938820fb1
--- /dev/null
+++ b/corebinutils/cp/cp.1
@@ -0,0 +1,346 @@
+.\"-
+.\" Copyright (c) 1989, 1990, 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
+.\" 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 July 9, 2025
+.Dt CP 1
+.Os
+.Sh NAME
+.Nm cp
+.Nd copy files
+.Sh SYNOPSIS
+.Nm
+.Oo
+.Fl R
+.Op Fl H | Fl L | Fl P
+.Oc
+.Op Fl f | i | n
+.Op Fl alNpsvx
+.Ar source_file target_file
+.Nm
+.Oo
+.Fl R
+.Op Fl H | Fl L | Fl P
+.Oc
+.Op Fl f | i | n
+.Op Fl alNpsvx
+.Ar source_file ... target_directory
+.Nm
+.Op Fl f | i | n
+.Op Fl alNPpsvx
+.Ar source_file target_file
+.Nm
+.Op Fl f | i | n
+.Op Fl alNPpsvx
+.Ar source_file ... target_directory
+.Sh DESCRIPTION
+In the first synopsis form, the
+.Nm
+utility copies the contents of the
+.Ar source_file
+to the
+.Ar target_file .
+In the second synopsis form,
+the contents of each named
+.Ar source_file
+is copied to the destination
+.Ar target_directory .
+The names of the files themselves are not changed.
+If
+.Nm
+detects an attempt to copy a file to itself, the copy will fail.
+.Pp
+The following options are available:
+.Bl -tag -width flag
+.It Fl H
+If the
+.Fl R
+option is specified, symbolic links on the command line are followed.
+(Symbolic links encountered in the tree traversal are not followed.)
+.It Fl L , Fl -dereference
+If the
+.Fl R
+option is specified, all symbolic links are followed.
+.It Fl P , Fl -no-dereference
+No symbolic links are followed.
+This is the default if the
+.Fl R
+option is specified.
+.It Fl R , Fl -recursive
+If
+.Ar source_file
+designates a directory,
+.Nm
+copies the directory and the entire subtree connected at that point.
+If the
+.Ar source_file
+ends in a
+.Pa / ,
+the contents of the directory are copied rather than the
+directory itself.
+This option also causes symbolic links to be copied, rather than
+indirected through, and for
+.Nm
+to create special files rather than copying them as normal files.
+Created directories have the same mode as the corresponding source
+directory, unmodified by the process' umask.
+.Pp
+Note that
+.Nm
+copies hard linked files as separate files.
+If you need to preserve hard links, consider using
+.Xr tar 1 ,
+.Xr cpio 1 ,
+or
+.Xr pax 1
+instead.
+.It Fl a , Fl -archive
+Archive mode.
+Same as
+.Fl RpP .
+.It Fl f , Fl -force
+For each existing destination pathname, remove it and
+create a new file, without prompting for confirmation
+regardless of its permissions.
+(The
+.Fl f
+option overrides any previous
+.Fl i
+or
+.Fl n
+options.)
+.It Fl i , Fl -interactive
+Write a prompt to the standard error output before copying a file
+that would overwrite an existing file.
+If the response from the standard input begins with the character
+.Sq Li y
+or
+.Sq Li Y ,
+the file copy is attempted.
+(The
+.Fl i
+option overrides any previous
+.Fl f
+or
+.Fl n
+options.)
+.It Fl l , Fl -link
+Create hard links to regular files in a hierarchy instead of copying.
+.It Fl N
+When used with
+.Fl p ,
+suppress copying file flags.
+.It Fl n , Fl -no-clobber
+Do not overwrite an existing file.
+(The
+.Fl n
+option overrides any previous
+.Fl f
+or
+.Fl i
+options.)
+.It Fl p
+Preserve the following attributes of each source
+file in the copy: modification time, access time,
+file flags, file mode, ACL, user ID, and group ID, as allowed by permissions.
+.Pp
+If the user ID and group ID cannot be preserved, no error message
+is displayed and the exit value is not altered.
+.Pp
+If the source file has its set-user-ID bit on and the user ID cannot
+be preserved, the set-user-ID bit is not preserved
+in the copy's permissions.
+If the source file has its set-group-ID bit on and the group ID cannot
+be preserved, the set-group-ID bit is not preserved
+in the copy's permissions.
+If the source file has both its set-user-ID and set-group-ID bits on,
+and either the user ID or group ID cannot be preserved, neither
+the set-user-ID nor set-group-ID bits are preserved in the copy's
+permissions.
+.It Fl -sort
+Visit and traverse sources in (non-localized) lexicographical order.
+Normally,
+.Nm
+visits the sources in the order they were listed on the command line,
+and if recursing, traverses their contents in whichever order they
+were returned in by the kernel, which may be the order in which they
+were created, lexicographical order, or something else entirely.
+With
+.Fl -sort ,
+the sources are both visited and traversed in lexicographical order.
+This is mostly useful for testing.
+.It Fl s , Fl -symbolic-link
+Create symbolic links to regular files in a hierarchy instead of copying.
+.It Fl v , Fl -verbose
+Be verbose, showing both the source and destination path of each file
+as is copied.
+.It Fl x , Fl -one-file-system
+Do not traverse file system mount points.
+.El
+.Pp
+For each destination file that already exists, its contents are
+overwritten if permissions allow.
+Its mode, user ID, and group
+ID are unchanged unless the
+.Fl p
+option was specified.
+.Pp
+In the second synopsis form,
+.Ar target_directory
+must exist unless there is only one named
+.Ar source_file
+which is a directory and the
+.Fl R
+flag is specified.
+.Pp
+If the destination file does not exist, the mode of the source file is
+used as modified by the file mode creation mask
+.Pf ( Ic umask ,
+see
+.Xr csh 1 ) .
+If the source file has its set-user-ID bit on, that bit is removed
+unless both the source file and the destination file are owned by the
+same user.
+If the source file has its set-group-ID bit on, that bit is removed
+unless both the source file and the destination file are in the same
+group and the user is a member of that group.
+If both the set-user-ID and set-group-ID bits are set, all of the above
+conditions must be fulfilled or both bits are removed.
+.Pp
+Appropriate permissions are required for file creation or overwriting.
+.Pp
+Symbolic links are always followed unless the
+.Fl R
+flag is set, in which case symbolic links are not followed, by default.
+The
+.Fl H
+or
+.Fl L
+flags (in conjunction with the
+.Fl R
+flag) cause symbolic links to be followed as described above.
+The
+.Fl H ,
+.Fl L
+and
+.Fl P
+options are ignored unless the
+.Fl R
+option is specified.
+In addition, these options override each other and the
+command's actions are determined by the last one specified.
+.Pp
+If
+.Nm
+receives a
+.Dv SIGINFO
+(see the
+.Cm status
+argument for
+.Xr stty 1 )
+signal, the current input and output file and the percentage complete
+will be written to the standard output.
+.Sh EXIT STATUS
+.Ex -std
+.Sh EXAMPLES
+Make a copy of file
+.Pa foo
+named
+.Pa bar :
+.Pp
+.Dl $ cp foo bar
+.Pp
+Copy a group of files to the
+.Pa /tmp
+directory:
+.Pp
+.Dl $ cp *.txt /tmp
+.Pp
+Copy the directory
+.Pa junk
+and all of its contents (including any subdirectories) to the
+.Pa /tmp
+directory:
+.Pp
+.Dl $ cp -R junk /tmp
+.Sh COMPATIBILITY
+Historic versions of the
+.Nm
+utility had a
+.Fl r
+option.
+This implementation supports that option, however, its behavior
+is different from historical
+.Fx
+behavior.
+Use of this option
+is strongly discouraged as the behavior is
+implementation-dependent.
+In
+.Fx ,
+.Fl r
+is a synonym for
+.Fl RL
+and works the same unless modified by other flags.
+Historical implementations
+of
+.Fl r
+differ as they copy special files as normal
+files while recreating a hierarchy.
+.Pp
+The
+.Fl a ,
+.Fl l ,
+.Fl N ,
+.Fl n ,
+.Fl s ,
+.Fl v ,
+and
+.Fl x
+options are non-standard and their use in scripts is not recommended.
+.Sh SEE ALSO
+.Xr mv 1 ,
+.Xr umask 2 ,
+.Xr fts 3 ,
+.Xr symlink 7
+.Sh STANDARDS
+The
+.Nm
+command is expected to be
+.St -p1003.2
+compatible.
+.Sh HISTORY
+A
+.Nm
+command appeared in
+.At v1 .
diff --git a/corebinutils/cp/cp.c b/corebinutils/cp/cp.c
new file mode 100644
index 0000000000..b790818bfc
--- /dev/null
+++ b/corebinutils/cp/cp.c
@@ -0,0 +1,758 @@
+/*-
+ * SPDX-License-Identifier: BSD-3-Clause
+ *
+ * Copyright (c) 1988, 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
+ * David Hitz of Auspex Systems 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.
+ */
+
+/*
+ * Cp copies source files to target files.
+ *
+ * The global PATH_T structure "to" always contains the path to the
+ * current target file. Since fts(3) does not change directories,
+ * this path can be either absolute or dot-relative.
+ *
+ * The basic algorithm is to initialize "to" and use fts(3) to traverse
+ * the file hierarchy rooted in the argument list. A trivial case is the
+ * case of 'cp file1 file2'. The more interesting case is the case of
+ * 'cp file1 file2 ... fileN dir' where the hierarchy is traversed and the
+ * path (relative to the root of the traversal) is appended to dir (stored
+ * in "to") to form the final target path.
+ */
+
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#include <assert.h>
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <limits.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "extern.h"
+
+#ifndef SIGINFO
+#define SIGINFO SIGUSR1
+#endif
+
+#ifndef O_SEARCH
+#define O_SEARCH 0
+#endif
+
+#ifndef AT_RESOLVE_BENEATH
+#define AT_RESOLVE_BENEATH 0
+#endif
+
+#ifndef __unused
+#define __unused __attribute__((unused))
+#endif
+
+#ifndef HAVE_ERRC
+#define warnc(code, fmt, ...) do { errno = (code); warn((fmt), ##__VA_ARGS__); } while (0)
+#define errc(eval, code, fmt, ...) do { errno = (code); err((eval), (fmt), ##__VA_ARGS__); } while (0)
+#endif
+
+static size_t
+local_strlcpy(char *dst, const char *src, size_t size)
+{
+ size_t src_len;
+ size_t copy_len;
+
+ src_len = strlen(src);
+ if (size == 0)
+ return (src_len);
+ copy_len = src_len >= size ? size - 1 : src_len;
+ memcpy(dst, src, copy_len);
+ dst[copy_len] = '\0';
+ return (src_len);
+}
+
+static int
+local_asprintf(char **ret, const char *fmt, const char *a, const char *b)
+{
+ int len;
+ char *buf;
+
+ len = snprintf(NULL, 0, fmt, a, b);
+ if (len < 0)
+ return (-1);
+ buf = malloc((size_t)len + 1);
+ if (buf == NULL)
+ return (-1);
+ if (snprintf(buf, (size_t)len + 1, fmt, a, b) != len) {
+ free(buf);
+ return (-1);
+ }
+ *ret = buf;
+ return (len);
+}
+
+static char dot[] = ".";
+
+#define END(buf) (buf + sizeof(buf))
+PATH_T to = { .dir = -1, .end = to.path };
+bool Nflag, fflag, iflag, lflag, nflag, pflag, sflag, vflag;
+static bool Hflag, Lflag, Pflag, Rflag, rflag, Sflag;
+volatile sig_atomic_t info;
+
+enum op { FILE_TO_FILE, FILE_TO_DIR, DIR_TO_DNE };
+
+static int copy(char *[], enum op, int, struct stat *);
+static void siginfo(int __unused);
+
+enum {
+ SORT_OPT = CHAR_MAX,
+};
+
+static const struct option long_opts[] =
+{
+ { "archive", no_argument, NULL, 'a' },
+ { "force", no_argument, NULL, 'f' },
+ { "interactive", no_argument, NULL, 'i' },
+ { "dereference", no_argument, NULL, 'L' },
+ { "link", no_argument, NULL, 'l' },
+ { "no-clobber", no_argument, NULL, 'n' },
+ { "no-dereference", no_argument, NULL, 'P' },
+ { "recursive", no_argument, NULL, 'R' },
+ { "symbolic-link", no_argument, NULL, 's' },
+ { "verbose", no_argument, NULL, 'v' },
+ { "one-file-system", no_argument, NULL, 'x' },
+ { "sort", no_argument, NULL, SORT_OPT },
+ { 0 }
+};
+
+int
+main(int argc, char *argv[])
+{
+ struct stat to_stat, tmp_stat;
+ enum op type;
+ int ch, fts_options, r;
+ char *sep, *target;
+ bool have_trailing_slash = false;
+
+ fts_options = FTS_NOCHDIR | FTS_PHYSICAL;
+ while ((ch = getopt_long(argc, argv, "+HLPRafilNnprsvx", long_opts,
+ NULL)) != -1)
+ switch (ch) {
+ case 'H':
+ Hflag = true;
+ Lflag = Pflag = false;
+ break;
+ case 'L':
+ Lflag = true;
+ Hflag = Pflag = false;
+ break;
+ case 'P':
+ Pflag = true;
+ Hflag = Lflag = false;
+ break;
+ case 'R':
+ Rflag = true;
+ break;
+ case 'a':
+ pflag = true;
+ Rflag = true;
+ Pflag = true;
+ Hflag = Lflag = false;
+ break;
+ case 'f':
+ fflag = true;
+ iflag = nflag = false;
+ break;
+ case 'i':
+ iflag = true;
+ fflag = nflag = false;
+ break;
+ case 'l':
+ lflag = true;
+ break;
+ case 'N':
+ Nflag = true;
+ break;
+ case 'n':
+ nflag = true;
+ fflag = iflag = false;
+ break;
+ case 'p':
+ pflag = true;
+ break;
+ case 'r':
+ rflag = Lflag = true;
+ Hflag = Pflag = false;
+ break;
+ case 's':
+ sflag = true;
+ break;
+ case 'v':
+ vflag = true;
+ break;
+ case 'x':
+ fts_options |= FTS_XDEV;
+ break;
+ case SORT_OPT:
+ Sflag = true;
+ break;
+ default:
+ usage();
+ }
+ argc -= optind;
+ argv += optind;
+
+ if (argc < 2)
+ usage();
+
+ if (Rflag && rflag)
+ errx(1, "the -R and -r options may not be specified together");
+ if (lflag && sflag)
+ errx(1, "the -l and -s options may not be specified together");
+ if (rflag)
+ Rflag = true;
+ if (Rflag) {
+ if (Hflag)
+ fts_options |= FTS_COMFOLLOW;
+ if (Lflag) {
+ fts_options &= ~FTS_PHYSICAL;
+ fts_options |= FTS_LOGICAL;
+ }
+ } else if (!Pflag) {
+ fts_options &= ~FTS_PHYSICAL;
+ fts_options |= FTS_LOGICAL | FTS_COMFOLLOW;
+ }
+ (void)signal(SIGINFO, siginfo);
+
+ /* Save the target base in "to". */
+ target = argv[--argc];
+ if (*target == '\0') {
+ target = dot;
+ } else if ((sep = strrchr(target, '/')) != NULL && sep[1] == '\0') {
+ have_trailing_slash = true;
+ while (sep > target && *sep == '/')
+ sep--;
+ sep[1] = '\0';
+ }
+ /*
+ * Copy target into to.base, leaving room for a possible separator
+ * which will be appended later in the non-FILE_TO_FILE cases.
+ */
+ if (local_strlcpy(to.base, target, sizeof(to.base) - 1) >=
+ sizeof(to.base) - 1)
+ errc(1, ENAMETOOLONG, "%s", target);
+
+ /* Set end of argument list for fts(3). */
+ argv[argc] = NULL;
+
+ /*
+ * Cp has two distinct cases:
+ *
+ * cp [-R] source target
+ * cp [-R] source1 ... sourceN directory
+ *
+ * In both cases, source can be either a file or a directory.
+ *
+ * In (1), the target becomes a copy of the source. That is, if the
+ * source is a file, the target will be a file, and likewise for
+ * directories.
+ *
+ * In (2), the real target is not directory, but "directory/source".
+ */
+ r = stat(to.base, &to_stat);
+ if (r == -1 && errno != ENOENT)
+ err(1, "%s", target);
+ if (r == -1 || !S_ISDIR(to_stat.st_mode)) {
+ /*
+ * Case (1). Target is not a directory.
+ */
+ if (argc > 1)
+ errc(1, ENOTDIR, "%s", target);
+
+ /*
+ * Need to detect the case:
+ * cp -R dir foo
+ * Where dir is a directory and foo does not exist, where
+ * we want pathname concatenations turned on but not for
+ * the initial mkdir().
+ */
+ if (r == -1) {
+ if (Rflag && (Lflag || Hflag))
+ stat(*argv, &tmp_stat);
+ else
+ lstat(*argv, &tmp_stat);
+
+ if (S_ISDIR(tmp_stat.st_mode) && Rflag)
+ type = DIR_TO_DNE;
+ else
+ type = FILE_TO_FILE;
+ } else
+ type = FILE_TO_FILE;
+
+ if (have_trailing_slash && type == FILE_TO_FILE) {
+ if (r == -1)
+ errc(1, ENOENT, "%s", target);
+ else
+ errc(1, ENOTDIR, "%s", target);
+ }
+ } else {
+ /*
+ * Case (2). Target is a directory.
+ */
+ type = FILE_TO_DIR;
+ }
+
+ /*
+ * For DIR_TO_DNE, we could provide copy() with the to_stat we've
+ * already allocated on the stack here that isn't being used for
+ * anything. Not doing so, though, simplifies later logic a little bit
+ * as we need to skip checking root_stat on the first iteration and
+ * ensure that we set it with the first mkdir().
+ */
+ exit (copy(argv, type, fts_options, (type == DIR_TO_DNE ? NULL :
+ &to_stat)));
+}
+
+static int
+ftscmp(const FTSENT * const *a, const FTSENT * const *b)
+{
+ return (strcmp((*a)->fts_name, (*b)->fts_name));
+}
+
+static int
+copy(char *argv[], enum op type, int fts_options, struct stat *root_stat)
+{
+ char rootname[NAME_MAX];
+ struct stat created_root_stat, to_stat, *curr_stat;
+ FTS *ftsp;
+ FTSENT *curr;
+ char *recpath = NULL, *sep;
+ int atflags, dne, badcp, len, level, rval;
+ mode_t mask, mode;
+ bool beneath = Rflag && type != FILE_TO_FILE;
+
+ /*
+ * Keep an inverted copy of the umask, for use in correcting
+ * permissions on created directories when not using -p.
+ */
+ mask = ~umask(0777);
+ umask(~mask);
+
+ if (type == FILE_TO_FILE) {
+ to.dir = AT_FDCWD;
+ to.end = to.path + local_strlcpy(to.path, to.base, sizeof(to.path));
+ to.base[0] = '\0';
+ } else if (type == FILE_TO_DIR) {
+ to.dir = open(to.base, O_DIRECTORY | O_SEARCH);
+ if (to.dir < 0)
+ err(1, "%s", to.base);
+ /*
+ * We have previously made sure there is room for this.
+ */
+ if (strcmp(to.base, "/") != 0) {
+ sep = strchr(to.base, '\0');
+ sep[0] = '/';
+ sep[1] = '\0';
+ }
+ } else {
+ /*
+ * We will create the destination directory imminently.
+ */
+ to.dir = -1;
+ }
+
+ level = FTS_ROOTLEVEL;
+ if ((ftsp = fts_open(argv, fts_options, Sflag ? ftscmp : NULL)) == NULL)
+ err(1, "fts_open");
+ for (badcp = rval = 0;
+ (curr = fts_read(ftsp)) != NULL;
+ badcp = 0, *to.end = '\0') {
+ curr_stat = curr->fts_statp;
+ switch (curr->fts_info) {
+ case FTS_NS:
+ case FTS_DNR:
+ case FTS_ERR:
+ if (level > curr->fts_level) {
+ /* leaving a directory; remove its name from to.path */
+ if (type == DIR_TO_DNE &&
+ curr->fts_level == FTS_ROOTLEVEL) {
+ /* this is actually our created root */
+ } else {
+ while (to.end > to.path && *to.end != '/')
+ to.end--;
+ assert(strcmp(to.end + (*to.end == '/'),
+ curr->fts_name) == 0);
+ *to.end = '\0';
+ }
+ level--;
+ }
+ warnc(curr->fts_errno, "%s", curr->fts_path);
+ badcp = rval = 1;
+ continue;
+ case FTS_DC: /* Warn, continue. */
+ warnx("%s: directory causes a cycle", curr->fts_path);
+ badcp = rval = 1;
+ continue;
+ case FTS_D:
+ /*
+ * Stash the root basename off for detecting
+ * recursion later.
+ *
+ * This will be essential if the root is a symlink
+ * and we're rolling with -L or -H. The later
+ * bits will need this bit in particular.
+ */
+ if (curr->fts_level == FTS_ROOTLEVEL) {
+ local_strlcpy(rootname, curr->fts_name,
+ sizeof(rootname));
+ }
+ /* we must have a destination! */
+ if (type == DIR_TO_DNE &&
+ curr->fts_level == FTS_ROOTLEVEL) {
+ assert(to.dir < 0);
+ assert(root_stat == NULL);
+ mode = curr_stat->st_mode | S_IRWXU;
+ /*
+ * Will our umask prevent us from entering
+ * the directory after we create it?
+ */
+ if (~mask & S_IRWXU)
+ umask(~mask & ~S_IRWXU);
+ if (mkdir(to.base, mode) != 0) {
+ warn("%s", to.base);
+ fts_set(ftsp, curr, FTS_SKIP);
+ badcp = rval = 1;
+ if (~mask & S_IRWXU)
+ umask(~mask);
+ continue;
+ }
+ to.dir = open(to.base, O_DIRECTORY | O_SEARCH);
+ if (to.dir < 0) {
+ warn("%s", to.base);
+ (void)rmdir(to.base);
+ fts_set(ftsp, curr, FTS_SKIP);
+ badcp = rval = 1;
+ if (~mask & S_IRWXU)
+ umask(~mask);
+ continue;
+ }
+ if (fstat(to.dir, &created_root_stat) != 0) {
+ warn("%s", to.base);
+ (void)close(to.dir);
+ (void)rmdir(to.base);
+ fts_set(ftsp, curr, FTS_SKIP);
+ to.dir = -1;
+ badcp = rval = 1;
+ if (~mask & S_IRWXU)
+ umask(~mask);
+ continue;
+ }
+ if (~mask & S_IRWXU)
+ umask(~mask);
+ root_stat = &created_root_stat;
+ curr->fts_number = 1;
+ /*
+ * We have previously made sure there is
+ * room for this.
+ */
+ sep = strchr(to.base, '\0');
+ sep[0] = '/';
+ sep[1] = '\0';
+ } else if (strcmp(curr->fts_name, "/") == 0) {
+ /* special case when source is the root directory */
+ } else {
+ /* entering a directory; append its name to to.path */
+ len = snprintf(to.end, END(to.path) - to.end, "%s%s",
+ to.end > to.path ? "/" : "", curr->fts_name);
+ if (to.end + len >= END(to.path)) {
+ *to.end = '\0';
+ warnc(ENAMETOOLONG, "%s%s%s%s", to.base,
+ to.path, to.end > to.path ? "/" : "",
+ curr->fts_name);
+ fts_set(ftsp, curr, FTS_SKIP);
+ badcp = rval = 1;
+ continue;
+ }
+ to.end += len;
+ }
+ level++;
+ /*
+ * We're on the verge of recursing on ourselves.
+ * Either we need to stop right here (we knowingly
+ * just created it), or we will in an immediate
+ * descendant. Record the path of the immediate
+ * descendant to make our lives a little less
+ * complicated looking.
+ */
+ if (type != FILE_TO_FILE &&
+ root_stat->st_dev == curr_stat->st_dev &&
+ root_stat->st_ino == curr_stat->st_ino) {
+ assert(recpath == NULL);
+ if (root_stat == &created_root_stat) {
+ /*
+ * This directory didn't exist
+ * when we started, we created it
+ * as part of traversal. Stop
+ * right here before we do
+ * something silly.
+ */
+ fts_set(ftsp, curr, FTS_SKIP);
+ continue;
+ }
+ if (local_asprintf(&recpath, "%s/%s", to.path,
+ rootname) < 0) {
+ warnc(ENOMEM, NULL);
+ fts_set(ftsp, curr, FTS_SKIP);
+ badcp = rval = 1;
+ continue;
+ }
+ }
+ if (recpath != NULL &&
+ strcmp(recpath, to.path) == 0) {
+ fts_set(ftsp, curr, FTS_SKIP);
+ continue;
+ }
+ break;
+ case FTS_DP:
+ /*
+ * We are nearly finished with this directory. If we
+ * didn't actually copy it, or otherwise don't need to
+ * change its attributes, then we are done.
+ *
+ * If -p is in effect, set all the attributes.
+ * Otherwise, set the correct permissions, limited
+ * by the umask. Optimise by avoiding a chmod()
+ * if possible (which is usually the case if we
+ * made the directory). Note that mkdir() does not
+ * honour setuid, setgid and sticky bits, but we
+ * normally want to preserve them on directories.
+ */
+ if (curr->fts_number && pflag) {
+ int fd = *to.path ? -1 : to.dir;
+ if (setfile(curr_stat, fd, true))
+ rval = 1;
+ if (preserve_dir_acls(curr->fts_accpath,
+ to.path) != 0)
+ rval = 1;
+ } else if (curr->fts_number) {
+ const char *path = *to.path ? to.path : dot;
+ mode = curr_stat->st_mode;
+ if (fchmodat(to.dir, path, mode & mask, 0) != 0) {
+ warn("chmod: %s%s", to.base, to.path);
+ rval = 1;
+ }
+ }
+ if (level > curr->fts_level) {
+ /* leaving a directory; remove its name from to.path */
+ if (type == DIR_TO_DNE &&
+ curr->fts_level == FTS_ROOTLEVEL) {
+ /* this is actually our created root */
+ } else if (strcmp(curr->fts_name, "/") == 0) {
+ /* special case when source is the root directory */
+ } else {
+ while (to.end > to.path && *to.end != '/')
+ to.end--;
+ assert(strcmp(to.end + (*to.end == '/'),
+ curr->fts_name) == 0);
+ *to.end = '\0';
+ }
+ level--;
+ }
+ continue;
+ default:
+ /* something else: append its name to to.path */
+ if (type == FILE_TO_FILE)
+ break;
+ len = snprintf(to.end, END(to.path) - to.end, "%s%s",
+ to.end > to.path ? "/" : "", curr->fts_name);
+ if (to.end + len >= END(to.path)) {
+ *to.end = '\0';
+ warnc(ENAMETOOLONG, "%s%s%s%s", to.base,
+ to.path, to.end > to.path ? "/" : "",
+ curr->fts_name);
+ badcp = rval = 1;
+ continue;
+ }
+ /* intentionally do not update to.end */
+ break;
+ }
+
+ /* Not an error but need to remember it happened. */
+ if (to.path[0] == '\0') {
+ /*
+ * This can happen in three cases:
+ * - The source path is the root directory.
+ * - DIR_TO_DNE; we created the directory and
+ * populated root_stat earlier.
+ * - FILE_TO_DIR if a source has a trailing slash;
+ * the caller populated root_stat.
+ */
+ dne = false;
+ to_stat = *root_stat;
+ } else {
+ atflags = beneath ? AT_RESOLVE_BENEATH : 0;
+ if (curr->fts_info == FTS_D || curr->fts_info == FTS_SL)
+ atflags |= AT_SYMLINK_NOFOLLOW;
+ dne = fstatat(to.dir, to.path, &to_stat, atflags) != 0;
+ }
+
+ /* Check if source and destination are identical. */
+ if (!dne &&
+ to_stat.st_dev == curr_stat->st_dev &&
+ to_stat.st_ino == curr_stat->st_ino) {
+ warnx("%s%s and %s are identical (not copied).",
+ to.base, to.path, curr->fts_path);
+ badcp = rval = 1;
+ if (S_ISDIR(curr_stat->st_mode))
+ fts_set(ftsp, curr, FTS_SKIP);
+ continue;
+ }
+
+ switch (curr_stat->st_mode & S_IFMT) {
+ case S_IFLNK:
+ if ((fts_options & FTS_LOGICAL) ||
+ ((fts_options & FTS_COMFOLLOW) &&
+ curr->fts_level == 0)) {
+ /*
+ * We asked FTS to follow links but got
+ * here anyway, which means the target is
+ * nonexistent or inaccessible. Let
+ * copy_file() deal with the error.
+ */
+ if (copy_file(curr, dne, beneath))
+ badcp = rval = 1;
+ } else {
+ /* Copy the link. */
+ if (copy_link(curr, dne, beneath))
+ badcp = rval = 1;
+ }
+ break;
+ case S_IFDIR:
+ if (!Rflag) {
+ warnx("%s is a directory (not copied).",
+ curr->fts_path);
+ fts_set(ftsp, curr, FTS_SKIP);
+ badcp = rval = 1;
+ break;
+ }
+ /*
+ * If the directory doesn't exist, create the new
+ * one with the from file mode plus owner RWX bits,
+ * modified by the umask. Trade-off between being
+ * able to write the directory (if from directory is
+ * 555) and not causing a permissions race. If the
+ * umask blocks owner writes, we fail.
+ */
+ if (dne) {
+ mode = curr_stat->st_mode | S_IRWXU;
+ /*
+ * Will our umask prevent us from entering
+ * the directory after we create it?
+ */
+ if (~mask & S_IRWXU)
+ umask(~mask & ~S_IRWXU);
+ if (mkdirat(to.dir, to.path, mode) != 0) {
+ warn("%s%s", to.base, to.path);
+ fts_set(ftsp, curr, FTS_SKIP);
+ badcp = rval = 1;
+ if (~mask & S_IRWXU)
+ umask(~mask);
+ break;
+ }
+ if (~mask & S_IRWXU)
+ umask(~mask);
+ } else if (!S_ISDIR(to_stat.st_mode)) {
+ warnc(ENOTDIR, "%s%s", to.base, to.path);
+ fts_set(ftsp, curr, FTS_SKIP);
+ badcp = rval = 1;
+ break;
+ }
+ /*
+ * Arrange to correct directory attributes later
+ * (in the post-order phase) if this is a new
+ * directory, or if the -p flag is in effect.
+ * Note that fts_number may already be set if this
+ * is the newly created destination directory.
+ */
+ curr->fts_number |= pflag || dne;
+ break;
+ case S_IFBLK:
+ case S_IFCHR:
+ if (Rflag && !sflag) {
+ if (copy_special(curr_stat, dne, beneath))
+ badcp = rval = 1;
+ } else {
+ if (copy_file(curr, dne, beneath))
+ badcp = rval = 1;
+ }
+ break;
+ case S_IFSOCK:
+ warnx("%s is a socket (not copied).",
+ curr->fts_path);
+ break;
+ case S_IFIFO:
+ if (Rflag && !sflag) {
+ if (copy_fifo(curr_stat, dne, beneath))
+ badcp = rval = 1;
+ } else {
+ if (copy_file(curr, dne, beneath))
+ badcp = rval = 1;
+ }
+ break;
+ default:
+ if (copy_file(curr, dne, beneath))
+ badcp = rval = 1;
+ break;
+ }
+ if (vflag && !badcp)
+ (void)printf("%s -> %s%s\n", curr->fts_path, to.base, to.path);
+ }
+ assert(level == FTS_ROOTLEVEL);
+ if (errno)
+ err(1, "fts_read");
+ (void)fts_close(ftsp);
+ if (to.dir != AT_FDCWD && to.dir >= 0)
+ (void)close(to.dir);
+ free(recpath);
+ return (rval);
+}
+
+static void
+siginfo(int sig __unused)
+{
+
+ info = 1;
+}
diff --git a/corebinutils/cp/extern.h b/corebinutils/cp/extern.h
new file mode 100644
index 0000000000..efc0e7fe06
--- /dev/null
+++ b/corebinutils/cp/extern.h
@@ -0,0 +1,46 @@
+/*-
+ * SPDX-License-Identifier: BSD-3-Clause
+ *
+ * Copyright (c) 1991, 1993, 1994
+ * The Regents of the University of California. All rights reserved.
+ */
+
+#pragma once
+
+#include <limits.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <sys/stat.h>
+
+#include "fts.h"
+
+#ifndef __unused
+#define __unused __attribute__((unused))
+#endif
+
+typedef struct {
+ int dir;
+ char base[PATH_MAX + 1];
+ char *end;
+ char path[PATH_MAX];
+} PATH_T;
+
+extern PATH_T to;
+extern bool Nflag, fflag, iflag, lflag, nflag, pflag, sflag, vflag;
+extern volatile sig_atomic_t info;
+
+int copy_fifo(struct stat *, bool, bool);
+int copy_file(const FTSENT *, bool, bool);
+int copy_link(const FTSENT *, bool, bool);
+int copy_special(struct stat *, bool, bool);
+int setfile(struct stat *, int, bool);
+int preserve_dir_acls(const char *, const char *);
+int preserve_fd_acls(int, int);
+void usage(void) __attribute__((noreturn));
+
+#ifdef ENOTCAPABLE
+#define warn(...) \
+ warnc(errno == ENOTCAPABLE ? EACCES : errno, __VA_ARGS__)
+#define err(rv, ...) \
+ errc(rv, errno == ENOTCAPABLE ? EACCES : errno, __VA_ARGS__)
+#endif
diff --git a/corebinutils/cp/fts.c b/corebinutils/cp/fts.c
new file mode 100644
index 0000000000..c254034136
--- /dev/null
+++ b/corebinutils/cp/fts.c
@@ -0,0 +1,465 @@
+/*-
+ * SPDX-License-Identifier: BSD-3-Clause
+ *
+ * Copyright (c) 2026
+ * Project Tick. All rights reserved.
+ *
+ * This code is derived from software contributed to Berkeley by
+ * David Hitz of Auspex Systems 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.
+ */
+
+#include "fts.h"
+
+#include <dirent.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+
+enum {
+ FTS_STATE_PRE = 0,
+ FTS_STATE_CHILDREN,
+ FTS_STATE_DNR,
+ FTS_STATE_POST,
+ FTS_STATE_DONE,
+};
+
+static FTSENT *fts_make_entry(FTS *ftsp, FTSENT *parent, const char *path,
+ int level, int is_root);
+static int fts_prepare_children(FTS *ftsp, FTSENT *parent);
+static FTSENT *fts_advance(FTS *ftsp, FTSENT *ent);
+static void fts_free_entry(FTSENT *ent);
+static const char *fts_basename(const char *path);
+static char *fts_join_path(const char *parent, const char *name);
+static int fts_should_follow(const FTS *ftsp, int is_root);
+static int fts_is_cycle(const FTSENT *parent, const struct stat *st);
+static const FTSENT *fts_root(const FTSENT *ent);
+
+static const char *
+fts_basename(const char *path)
+{
+ const char *end;
+ const char *base;
+
+ end = path + strlen(path);
+ while (end > path + 1 && end[-1] == '/')
+ end--;
+ base = end;
+ while (base > path && base[-1] != '/')
+ base--;
+ if (base == end)
+ return ("/");
+ return (base);
+}
+
+static char *
+fts_join_path(const char *parent, const char *name)
+{
+ size_t parent_len, name_len;
+ int need_sep;
+ char *path;
+
+ parent_len = strlen(parent);
+ name_len = strlen(name);
+ need_sep = parent_len > 0 && parent[parent_len - 1] != '/';
+ path = malloc(parent_len + need_sep + name_len + 1);
+ if (path == NULL)
+ return (NULL);
+ memcpy(path, parent, parent_len);
+ if (need_sep)
+ path[parent_len++] = '/';
+ memcpy(path + parent_len, name, name_len + 1);
+ return (path);
+}
+
+static int
+fts_should_follow(const FTS *ftsp, int is_root)
+{
+ if (ftsp->fts_options & FTS_LOGICAL)
+ return (1);
+ if (is_root && (ftsp->fts_options & FTS_COMFOLLOW))
+ return (1);
+ return (0);
+}
+
+static const FTSENT *
+fts_root(const FTSENT *ent)
+{
+ while (ent != NULL && ent->fts_parent != NULL)
+ ent = ent->fts_parent;
+ return (ent);
+}
+
+static int
+fts_is_cycle(const FTSENT *parent, const struct stat *st)
+{
+ const FTSENT *cur;
+
+ for (cur = parent; cur != NULL; cur = cur->fts_parent) {
+ if (cur->fts_statp != NULL &&
+ cur->fts_statp->st_dev == st->st_dev &&
+ cur->fts_statp->st_ino == st->st_ino) {
+ return (1);
+ }
+ }
+ return (0);
+}
+
+static FTSENT *
+fts_make_entry(FTS *ftsp, FTSENT *parent, const char *path, int level, int is_root)
+{
+ FTSENT *ent;
+ struct stat st;
+ int follow;
+
+ ent = calloc(1, sizeof(*ent));
+ if (ent == NULL)
+ return (NULL);
+
+ ent->fts_path = strdup(path);
+ ent->fts_accpath = ent->fts_path;
+ ent->fts_name = strdup(fts_basename(path));
+ ent->fts_parent = parent;
+ ent->fts_level = level;
+ ent->fts_statp = malloc(sizeof(*ent->fts_statp));
+ if (ent->fts_path == NULL || ent->fts_name == NULL || ent->fts_statp == NULL) {
+ fts_free_entry(ent);
+ return (NULL);
+ }
+
+ follow = fts_should_follow(ftsp, is_root);
+ if ((follow ? stat(path, &st) : lstat(path, &st)) != 0) {
+ if (lstat(path, &st) == 0 && S_ISLNK(st.st_mode)) {
+ ent->fts_info = FTS_SL;
+ *ent->fts_statp = st;
+ } else {
+ ent->fts_info = FTS_NS;
+ ent->fts_errno = errno;
+ memset(ent->fts_statp, 0, sizeof(*ent->fts_statp));
+ }
+ return (ent);
+ }
+
+ *ent->fts_statp = st;
+ if (S_ISDIR(st.st_mode)) {
+ if (fts_is_cycle(parent, &st)) {
+ ent->fts_info = FTS_DC;
+ } else {
+ const FTSENT *root;
+
+ ent->fts_info = FTS_D;
+ root = fts_root(ent);
+ if ((ftsp->fts_options & FTS_XDEV) != 0 && root != NULL &&
+ root->fts_statp != NULL && root != ent &&
+ root->fts_statp->st_dev != st.st_dev) {
+ ent->fts_instr = FTS_SKIP;
+ }
+ }
+ } else if (S_ISLNK(st.st_mode) && !follow) {
+ ent->fts_info = FTS_SL;
+ } else {
+ ent->fts_info = FTS_F;
+ }
+
+ return (ent);
+}
+
+static int
+fts_prepare_children(FTS *ftsp, FTSENT *parent)
+{
+ struct dirent *dp;
+ DIR *dirp;
+ char **names;
+ char *child_path;
+ FTSENT *child;
+ size_t cap, count, i;
+
+ dirp = opendir(parent->fts_accpath);
+ if (dirp == NULL) {
+ parent->fts_errno = errno;
+ parent->fts_info = FTS_DNR;
+ parent->_state = FTS_STATE_DNR;
+ return (-1);
+ }
+
+ names = NULL;
+ cap = count = 0;
+ while ((dp = readdir(dirp)) != NULL) {
+ char *name;
+
+ if (strcmp(dp->d_name, ".") == 0 || strcmp(dp->d_name, "..") == 0)
+ continue;
+ if (count == cap) {
+ size_t new_cap;
+ char **new_names;
+
+ new_cap = cap == 0 ? 16 : cap * 2;
+ new_names = realloc(names, new_cap * sizeof(*names));
+ if (new_names == NULL)
+ goto fail;
+ names = new_names;
+ cap = new_cap;
+ }
+ name = strdup(dp->d_name);
+ if (name == NULL)
+ goto fail;
+ names[count++] = name;
+ }
+ closedir(dirp);
+
+ if (count == 0) {
+ free(names);
+ parent->_children = NULL;
+ parent->_child_count = 0;
+ parent->_child_index = 0;
+ return (0);
+ }
+
+ if (ftsp->fts_compar != NULL) {
+ FTSENT **cmp_entries;
+
+ cmp_entries = calloc(count, sizeof(*cmp_entries));
+ if (cmp_entries == NULL)
+ goto fail_names;
+ for (i = 0; i < count; i++) {
+ child_path = fts_join_path(parent->fts_path, names[i]);
+ if (child_path == NULL) {
+ while (i > 0)
+ fts_free_entry(cmp_entries[--i]);
+ free(cmp_entries);
+ goto fail_names;
+ }
+ cmp_entries[i] = fts_make_entry(ftsp, parent, child_path,
+ parent->fts_level + 1, 0);
+ free(child_path);
+ if (cmp_entries[i] == NULL) {
+ while (i > 0)
+ fts_free_entry(cmp_entries[--i]);
+ free(cmp_entries);
+ goto fail_names;
+ }
+ }
+ qsort(cmp_entries, count, sizeof(*cmp_entries),
+ (int (*)(const void *, const void *))ftsp->fts_compar);
+ parent->_children = cmp_entries;
+ parent->_child_count = count;
+ parent->_child_index = 0;
+ for (i = 0; i < count; i++)
+ free(names[i]);
+ free(names);
+ return (0);
+ }
+
+ parent->_children = calloc(count, sizeof(*parent->_children));
+ if (parent->_children == NULL)
+ goto fail_names;
+ for (i = 0; i < count; i++) {
+ child_path = fts_join_path(parent->fts_path, names[i]);
+ if (child_path == NULL)
+ goto fail_children;
+ child = fts_make_entry(ftsp, parent, child_path, parent->fts_level + 1, 0);
+ free(child_path);
+ if (child == NULL)
+ goto fail_children;
+ parent->_children[i] = child;
+ }
+ parent->_child_count = count;
+ parent->_child_index = 0;
+ for (i = 0; i < count; i++)
+ free(names[i]);
+ free(names);
+ return (0);
+
+fail_children:
+ while (i > 0)
+ fts_free_entry(parent->_children[--i]);
+ free(parent->_children);
+ parent->_children = NULL;
+fail_names:
+ for (i = 0; i < count; i++)
+ free(names[i]);
+ free(names);
+ parent->fts_errno = ENOMEM;
+ parent->fts_info = FTS_ERR;
+ parent->_state = FTS_STATE_DONE;
+ return (-1);
+fail:
+ closedir(dirp);
+ free(names);
+ parent->fts_errno = ENOMEM;
+ parent->fts_info = FTS_ERR;
+ parent->_state = FTS_STATE_DONE;
+ return (-1);
+}
+
+static FTSENT *
+fts_advance(FTS *ftsp, FTSENT *ent)
+{
+ FTSENT *parent;
+
+ for (;;) {
+ if (ent == NULL)
+ break;
+
+ if (ent->fts_info == FTS_D && ent->_state == FTS_STATE_PRE) {
+ if (ent->fts_instr == FTS_SKIP || ent->fts_info == FTS_DC) {
+ ent->fts_info = FTS_DP;
+ ent->_state = FTS_STATE_POST;
+ return (ent);
+ }
+ if (fts_prepare_children(ftsp, ent) != 0) {
+ if (ent->fts_info == FTS_DNR || ent->fts_info == FTS_ERR)
+ return (ent);
+ }
+ ent->_state = FTS_STATE_CHILDREN;
+ if (ent->_child_count > 0)
+ return (ent->_children[ent->_child_index++]);
+ ent->fts_info = FTS_DP;
+ ent->_state = FTS_STATE_POST;
+ return (ent);
+ }
+
+ if (ent->_state == FTS_STATE_DNR) {
+ ent->fts_info = FTS_DP;
+ ent->_state = FTS_STATE_POST;
+ return (ent);
+ }
+
+ if (ent->_state == FTS_STATE_CHILDREN) {
+ if (ent->_child_index < ent->_child_count)
+ return (ent->_children[ent->_child_index++]);
+ ent->fts_info = FTS_DP;
+ ent->_state = FTS_STATE_POST;
+ return (ent);
+ }
+
+ ent->_state = FTS_STATE_DONE;
+ parent = ent->fts_parent;
+ if (parent != NULL) {
+ ent = parent;
+ continue;
+ }
+
+ if (++ftsp->_root_index < ftsp->_root_count)
+ return (ftsp->_roots[ftsp->_root_index]);
+ break;
+ }
+
+ return (NULL);
+}
+
+FTS *
+fts_open(char * const *paths, int options,
+ int (*compar)(const FTSENT * const *, const FTSENT * const *))
+{
+ FTS *ftsp;
+ size_t count, i;
+
+ count = 0;
+ while (paths[count] != NULL)
+ count++;
+
+ ftsp = calloc(1, sizeof(*ftsp));
+ if (ftsp == NULL)
+ return (NULL);
+ ftsp->_roots = calloc(count, sizeof(*ftsp->_roots));
+ if (ftsp->_roots == NULL) {
+ free(ftsp);
+ return (NULL);
+ }
+ ftsp->_root_count = count;
+ ftsp->fts_options = options;
+ ftsp->fts_compar = compar;
+
+ for (i = 0; i < count; i++) {
+ ftsp->_roots[i] = fts_make_entry(ftsp, NULL, paths[i], FTS_ROOTLEVEL, 1);
+ if (ftsp->_roots[i] == NULL) {
+ while (i > 0)
+ fts_free_entry(ftsp->_roots[--i]);
+ free(ftsp->_roots);
+ free(ftsp);
+ return (NULL);
+ }
+ }
+
+ return (ftsp);
+}
+
+FTSENT *
+fts_read(FTS *ftsp)
+{
+ if (ftsp == NULL || ftsp->_root_count == 0)
+ return (NULL);
+ if (ftsp->_current == NULL) {
+ ftsp->_root_index = 0;
+ ftsp->_current = ftsp->_roots[0];
+ return (ftsp->_current);
+ }
+ ftsp->_current = fts_advance(ftsp, ftsp->_current);
+ if (ftsp->_current == NULL)
+ errno = 0;
+ return (ftsp->_current);
+}
+
+int
+fts_set(FTS *ftsp, FTSENT *f, int instr)
+{
+ (void)ftsp;
+ if (f == NULL)
+ return (-1);
+ f->fts_instr = instr;
+ return (0);
+}
+
+static void
+fts_free_entry(FTSENT *ent)
+{
+ size_t i;
+
+ if (ent == NULL)
+ return;
+ for (i = 0; i < ent->_child_count; i++)
+ fts_free_entry(ent->_children[i]);
+ free(ent->_children);
+ free(ent->fts_statp);
+ free(ent->fts_name);
+ free(ent->fts_path);
+ free(ent);
+}
+
+int
+fts_close(FTS *ftsp)
+{
+ size_t i;
+
+ if (ftsp == NULL)
+ return (0);
+ for (i = 0; i < ftsp->_root_count; i++)
+ fts_free_entry(ftsp->_roots[i]);
+ free(ftsp->_roots);
+ free(ftsp);
+ return (0);
+}
diff --git a/corebinutils/cp/fts.h b/corebinutils/cp/fts.h
new file mode 100644
index 0000000000..0b573e3085
--- /dev/null
+++ b/corebinutils/cp/fts.h
@@ -0,0 +1,94 @@
+/*-
+ * SPDX-License-Identifier: BSD-3-Clause
+ *
+ * Copyright (c) 2026
+ * Project Tick. All rights reserved.
+ *
+ * This code is derived from software contributed to Berkeley by
+ * David Hitz of Auspex Systems 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.
+ */
+
+#pragma once
+
+#include <sys/stat.h>
+
+#include <stddef.h>
+
+typedef struct _ftsent FTSENT;
+typedef struct _fts FTS;
+
+struct _ftsent {
+ FTSENT *fts_parent;
+ char *fts_path;
+ char *fts_accpath;
+ char *fts_name;
+ int fts_errno;
+ int fts_info;
+ int fts_instr;
+ int fts_level;
+ long fts_number;
+ struct stat *fts_statp;
+
+ FTSENT **_children;
+ size_t _child_count;
+ size_t _child_index;
+ int _state;
+};
+
+struct _fts {
+ FTSENT **_roots;
+ size_t _root_count;
+ size_t _root_index;
+ int fts_options;
+ int (*fts_compar)(const FTSENT * const *, const FTSENT * const *);
+ FTSENT *_current;
+};
+
+#define FTS_COMFOLLOW 0x0001
+#define FTS_LOGICAL 0x0002
+#define FTS_NOCHDIR 0x0004
+#define FTS_PHYSICAL 0x0008
+#define FTS_XDEV 0x0010
+
+#define FTS_ROOTLEVEL 0
+
+#define FTS_D 1
+#define FTS_DC 2
+#define FTS_DNR 3
+#define FTS_DP 4
+#define FTS_ERR 5
+#define FTS_F 6
+#define FTS_NS 7
+#define FTS_SL 8
+
+#define FTS_SKIP 1
+
+FTS *fts_open(char * const *paths, int options,
+ int (*compar)(const FTSENT * const *, const FTSENT * const *));
+FTSENT *fts_read(FTS *ftsp);
+int fts_set(FTS *ftsp, FTSENT *f, int instr);
+int fts_close(FTS *ftsp);
diff --git a/corebinutils/cp/tests/test.sh b/corebinutils/cp/tests/test.sh
new file mode 100644
index 0000000000..1ddd3a787c
--- /dev/null
+++ b/corebinutils/cp/tests/test.sh
@@ -0,0 +1,71 @@
+#!/bin/sh
+#
+# Copyright (c) 2026
+# Project Tick. All rights reserved.
+#
+# 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.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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.
+#
+
+set -eu
+
+ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
+CP_BIN=${CP_BIN:-"$ROOT/out/cp"}
+TMPDIR=${TMPDIR:-/tmp}
+WORKDIR=$(mktemp -d "$TMPDIR/cp-test.XXXXXX")
+trap 'rm -rf "$WORKDIR"' EXIT INT TERM
+
+fail() {
+ printf '%s\n' "FAIL: $1" >&2
+ exit 1
+}
+
+[ -x "$CP_BIN" ] || fail "missing binary: $CP_BIN"
+
+printf '%s\n' "hello" > "$WORKDIR/src.txt"
+"$CP_BIN" "$WORKDIR/src.txt" "$WORKDIR/dst.txt"
+cmp "$WORKDIR/src.txt" "$WORKDIR/dst.txt" >/dev/null 2>&1 || fail "file copy mismatch"
+
+mkdir -p "$WORKDIR/outdir"
+"$CP_BIN" "$WORKDIR/src.txt" "$WORKDIR/outdir"
+cmp "$WORKDIR/src.txt" "$WORKDIR/outdir/src.txt" >/dev/null 2>&1 || fail "file to dir mismatch"
+
+mkdir -p "$WORKDIR/tree/sub"
+printf '%s\n' "nested" > "$WORKDIR/tree/sub/file.txt"
+"$CP_BIN" -R "$WORKDIR/tree" "$WORKDIR/tree-copy"
+cmp "$WORKDIR/tree/sub/file.txt" "$WORKDIR/tree-copy/sub/file.txt" >/dev/null 2>&1 || fail "recursive copy mismatch"
+
+ln -s "$WORKDIR/src.txt" "$WORKDIR/src-link"
+"$CP_BIN" -P "$WORKDIR/src-link" "$WORKDIR/dst-link"
+[ -L "$WORKDIR/dst-link" ] || fail "symlink not preserved under -P"
+
+printf '%s\n' "old" > "$WORKDIR/existing.txt"
+printf '%s\n' "new" > "$WORKDIR/new.txt"
+"$CP_BIN" -n "$WORKDIR/new.txt" "$WORKDIR/existing.txt" || true
+[ "$(cat "$WORKDIR/existing.txt")" = "old" ] || fail "no-clobber failed"
+
+usage_output=$("$CP_BIN" 2>&1 || true)
+case $usage_output in
+ *"usage: cp "*) ;;
+ *) fail "usage output missing" ;;
+esac
+
+printf '%s\n' "PASS"
diff --git a/corebinutils/cp/utils.c b/corebinutils/cp/utils.c
new file mode 100644
index 0000000000..20e3b79784
--- /dev/null
+++ b/corebinutils/cp/utils.c
@@ -0,0 +1,527 @@
+/*-
+ * SPDX-License-Identifier: BSD-3-Clause
+ *
+ * Copyright (c) 1991, 1993, 1994
+ * The Regents of the University of California. All rights reserved.
+ *
+ * Copyright (c) 2026
+ * Project Tick. All rights reserved.
+ *
+ * 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.
+ */
+
+#include <sys/param.h>
+#include <sys/stat.h>
+
+#ifdef __FreeBSD__
+#include <sys/acl.h>
+#endif
+
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sysexits.h>
+#include <unistd.h>
+
+#include "extern.h"
+
+#ifndef MAXPHYS
+#define MAXPHYS (128 * 1024)
+#endif
+
+#ifndef O_RESOLVE_BENEATH
+#define O_RESOLVE_BENEATH 0
+#endif
+
+#ifndef AT_RESOLVE_BENEATH
+#define AT_RESOLVE_BENEATH 0
+#endif
+
+#define cp_pct(x, y) ((y == 0) ? 0 : (int)(100.0 * (x) / (y)))
+
+/*
+ * Memory strategy threshold, in pages: if physmem is larger then this, use a
+ * large buffer.
+ */
+#define PHYSPAGES_THRESHOLD (32*1024)
+
+/* Maximum buffer size in bytes - do not allow it to grow larger than this. */
+#define BUFSIZE_MAX (2*1024*1024)
+
+/*
+ * Small (default) buffer size in bytes. It's inefficient for this to be
+ * smaller than MAXPHYS.
+ */
+#define BUFSIZE_SMALL (MAXPHYS)
+
+/*
+ * Prompt used in -i case.
+ */
+#define YESNO "(y/n [n]) "
+
+static ssize_t
+copy_fallback(int from_fd, int to_fd)
+{
+ static char *buf = NULL;
+ static size_t bufsize;
+ ssize_t rcount, wresid, wcount = 0;
+ char *bufp;
+
+ if (buf == NULL) {
+ if (sysconf(_SC_PHYS_PAGES) > PHYSPAGES_THRESHOLD)
+ bufsize = MIN(BUFSIZE_MAX, MAXPHYS * 8);
+ else
+ bufsize = BUFSIZE_SMALL;
+ buf = malloc(bufsize);
+ if (buf == NULL)
+ err(1, "Not enough memory");
+ }
+ rcount = read(from_fd, buf, bufsize);
+ if (rcount <= 0)
+ return (rcount);
+ for (bufp = buf, wresid = rcount; ; bufp += wcount, wresid -= wcount) {
+ wcount = write(to_fd, bufp, wresid);
+ if (wcount <= 0)
+ break;
+ if (wcount >= wresid)
+ break;
+ }
+ return (wcount < 0 ? wcount : rcount);
+}
+
+int
+copy_file(const FTSENT *entp, bool dne, bool beneath)
+{
+ struct stat sb, *fs;
+ ssize_t wcount;
+ off_t wtotal;
+ int ch, checkch, from_fd, rval, to_fd;
+ bool use_copy_file_range = true;
+
+ fs = entp->fts_statp;
+ from_fd = to_fd = -1;
+ if (!lflag && !sflag) {
+ if ((from_fd = open(entp->fts_path, O_RDONLY, 0)) < 0 ||
+ fstat(from_fd, &sb) != 0) {
+ warn("%s", entp->fts_path);
+ if (from_fd >= 0)
+ (void)close(from_fd);
+ return (1);
+ }
+ /*
+ * Check that the file hasn't been replaced with one of a
+ * different type. This can happen if we've been asked to
+ * copy something which is actively being modified and
+ * lost the race, or if we've been asked to copy something
+ * like /proc/X/fd/Y which stat(2) reports as S_IFREG but
+ * is actually something else once you open it.
+ */
+ if ((sb.st_mode & S_IFMT) != (fs->st_mode & S_IFMT)) {
+ warnx("%s: File changed", entp->fts_path);
+ (void)close(from_fd);
+ return (1);
+ }
+ }
+
+ /*
+ * If the file exists and we're interactive, verify with the user.
+ * If the file DNE, set the mode to be the from file, minus setuid
+ * bits, modified by the umask; arguably wrong, but it makes copying
+ * executables work right and it's been that way forever. (The
+ * other choice is 666 or'ed with the execute bits on the from file
+ * modified by the umask.)
+ */
+ if (!dne) {
+ if (nflag) {
+ if (vflag)
+ printf("%s%s not overwritten\n",
+ to.base, to.path);
+ rval = 1;
+ goto done;
+ } else if (iflag) {
+ (void)fprintf(stderr, "overwrite %s%s? %s",
+ to.base, to.path, YESNO);
+ checkch = ch = getchar();
+ while (ch != '\n' && ch != EOF)
+ ch = getchar();
+ if (checkch != 'y' && checkch != 'Y') {
+ (void)fprintf(stderr, "not overwritten\n");
+ rval = 1;
+ goto done;
+ }
+ }
+
+ if (fflag) {
+ /* remove existing destination file */
+ (void)unlinkat(to.dir, to.path,
+ beneath ? AT_RESOLVE_BENEATH : 0);
+ dne = 1;
+ }
+ }
+
+ rval = 0;
+
+ if (lflag) {
+ if (linkat(AT_FDCWD, entp->fts_path, to.dir, to.path, 0) != 0) {
+ warn("%s%s", to.base, to.path);
+ rval = 1;
+ }
+ goto done;
+ }
+
+ if (sflag) {
+ if (symlinkat(entp->fts_path, to.dir, to.path) != 0) {
+ warn("%s%s", to.base, to.path);
+ rval = 1;
+ }
+ goto done;
+ }
+
+ if (!dne) {
+ /* overwrite existing destination file */
+ to_fd = openat(to.dir, to.path,
+ O_WRONLY | O_TRUNC | (beneath ? O_RESOLVE_BENEATH : 0), 0);
+ } else {
+ /* create new destination file */
+ to_fd = openat(to.dir, to.path,
+ O_WRONLY | O_TRUNC | O_CREAT |
+ (beneath ? O_RESOLVE_BENEATH : 0),
+ fs->st_mode & ~(S_ISUID | S_ISGID));
+ }
+ if (to_fd == -1) {
+ warn("%s%s", to.base, to.path);
+ rval = 1;
+ goto done;
+ }
+
+ wtotal = 0;
+ do {
+ if (use_copy_file_range) {
+ wcount = copy_file_range(from_fd, NULL,
+ to_fd, NULL, SSIZE_MAX, 0);
+ if (wcount < 0 && errno == EINVAL) {
+ /* probably a non-seekable descriptor */
+ use_copy_file_range = false;
+ }
+ }
+ if (!use_copy_file_range) {
+ wcount = copy_fallback(from_fd, to_fd);
+ }
+ if (wcount >= 0)
+ wtotal += wcount;
+ else if (errno != EINTR)
+ break;
+ if (info) {
+ info = 0;
+ (void)fprintf(stderr,
+ "%s -> %s%s %3d%%\n",
+ entp->fts_path, to.base, to.path,
+ cp_pct(wtotal, fs->st_size));
+ }
+ } while (wcount != 0);
+ if (wcount < 0) {
+ warn("%s", entp->fts_path);
+ rval = 1;
+ }
+
+ /*
+ * Don't remove the target even after an error. The target might
+ * not be a regular file, or its attributes might be important,
+ * or its contents might be irreplaceable. It would only be safe
+ * to remove it if we created it and its length is 0.
+ */
+ if (pflag && setfile(fs, to_fd, beneath))
+ rval = 1;
+ if (pflag && preserve_fd_acls(from_fd, to_fd) != 0)
+ rval = 1;
+ if (close(to_fd)) {
+ warn("%s%s", to.base, to.path);
+ rval = 1;
+ }
+
+done:
+ if (from_fd != -1)
+ (void)close(from_fd);
+ return (rval);
+}
+
+int
+copy_link(const FTSENT *p, bool dne, bool beneath)
+{
+ ssize_t len;
+ int atflags = beneath ? AT_RESOLVE_BENEATH : 0;
+ char llink[PATH_MAX];
+
+ if (!dne && nflag) {
+ if (vflag)
+ printf("%s%s not overwritten\n", to.base, to.path);
+ return (1);
+ }
+ if ((len = readlink(p->fts_path, llink, sizeof(llink) - 1)) == -1) {
+ warn("readlink: %s", p->fts_path);
+ return (1);
+ }
+ llink[len] = '\0';
+ if (!dne && unlinkat(to.dir, to.path, atflags) != 0) {
+ warn("unlink: %s%s", to.base, to.path);
+ return (1);
+ }
+ if (symlinkat(llink, to.dir, to.path) != 0) {
+ warn("symlink: %s", llink);
+ return (1);
+ }
+ return (pflag ? setfile(p->fts_statp, -1, beneath) : 0);
+}
+
+int
+copy_fifo(struct stat *from_stat, bool dne, bool beneath)
+{
+ int atflags = beneath ? AT_RESOLVE_BENEATH : 0;
+
+ if (!dne && nflag) {
+ if (vflag)
+ printf("%s%s not overwritten\n", to.base, to.path);
+ return (1);
+ }
+ if (!dne && unlinkat(to.dir, to.path, atflags) != 0) {
+ warn("unlink: %s%s", to.base, to.path);
+ return (1);
+ }
+ if (mkfifoat(to.dir, to.path, from_stat->st_mode) != 0) {
+ warn("mkfifo: %s%s", to.base, to.path);
+ return (1);
+ }
+ return (pflag ? setfile(from_stat, -1, beneath) : 0);
+}
+
+int
+copy_special(struct stat *from_stat, bool dne, bool beneath)
+{
+ int atflags = beneath ? AT_RESOLVE_BENEATH : 0;
+
+ if (!dne && nflag) {
+ if (vflag)
+ printf("%s%s not overwritten\n", to.base, to.path);
+ return (1);
+ }
+ if (!dne && unlinkat(to.dir, to.path, atflags) != 0) {
+ warn("unlink: %s%s", to.base, to.path);
+ return (1);
+ }
+ if (mknodat(to.dir, to.path, from_stat->st_mode, from_stat->st_rdev) != 0) {
+ warn("mknod: %s%s", to.base, to.path);
+ return (1);
+ }
+ return (pflag ? setfile(from_stat, -1, beneath) : 0);
+}
+
+int
+setfile(struct stat *fs, int fd, bool beneath)
+{
+ static struct timespec tspec[2];
+ struct stat ts;
+ int atflags = beneath ? AT_RESOLVE_BENEATH : 0;
+ int rval, gotstat, islink, fdval;
+
+ rval = 0;
+ fdval = fd != -1;
+ islink = !fdval && S_ISLNK(fs->st_mode);
+ if (islink)
+ atflags |= AT_SYMLINK_NOFOLLOW;
+ fs->st_mode &= S_ISUID | S_ISGID | S_ISVTX |
+ S_IRWXU | S_IRWXG | S_IRWXO;
+
+ tspec[0] = fs->st_atim;
+ tspec[1] = fs->st_mtim;
+ if (fdval ? futimens(fd, tspec) :
+ utimensat(to.dir, to.path, tspec, atflags)) {
+ warn("utimensat: %s%s", to.base, to.path);
+ rval = 1;
+ }
+ if (fdval ? fstat(fd, &ts) :
+ fstatat(to.dir, to.path, &ts, atflags)) {
+ gotstat = 0;
+ } else {
+ gotstat = 1;
+ ts.st_mode &= S_ISUID | S_ISGID | S_ISVTX |
+ S_IRWXU | S_IRWXG | S_IRWXO;
+ }
+ /*
+ * Changing the ownership probably won't succeed, unless we're root
+ * or POSIX_CHOWN_RESTRICTED is not set. Set uid/gid before setting
+ * the mode; current BSD behavior is to remove all setuid bits on
+ * chown. If chown fails, lose setuid/setgid bits.
+ */
+ if (!gotstat || fs->st_uid != ts.st_uid || fs->st_gid != ts.st_gid) {
+ if (fdval ? fchown(fd, fs->st_uid, fs->st_gid) :
+ fchownat(to.dir, to.path, fs->st_uid, fs->st_gid, atflags)) {
+ if (errno != EPERM) {
+ warn("chown: %s%s", to.base, to.path);
+ rval = 1;
+ }
+ fs->st_mode &= ~(S_ISUID | S_ISGID);
+ }
+ }
+
+ if (!gotstat || fs->st_mode != ts.st_mode) {
+ if (fdval ? fchmod(fd, fs->st_mode) :
+ fchmodat(to.dir, to.path, fs->st_mode, atflags)) {
+ warn("chmod: %s%s", to.base, to.path);
+ rval = 1;
+ }
+ }
+
+#ifdef __FreeBSD__
+ if (!Nflag && (!gotstat || fs->st_flags != ts.st_flags)) {
+ if (fdval ? fchflags(fd, fs->st_flags) :
+ chflagsat(to.dir, to.path, fs->st_flags, atflags)) {
+ /*
+ * NFS doesn't support chflags; ignore errors unless
+ * there's reason to believe we're losing bits. (Note,
+ * this still won't be right if the server supports
+ * flags and we were trying to *remove* flags on a file
+ * that we copied, i.e., that we didn't create.)
+ */
+ if (errno != EOPNOTSUPP || fs->st_flags != 0) {
+ warn("chflags: %s%s", to.base, to.path);
+ rval = 1;
+ }
+ }
+ }
+#endif
+
+ return (rval);
+}
+
+int
+preserve_fd_acls(int source_fd, int dest_fd)
+{
+#ifndef __FreeBSD__
+ (void)source_fd;
+ (void)dest_fd;
+ return (0);
+#else
+ acl_t acl;
+ acl_type_t acl_type;
+ int acl_supported = 0, ret, trivial;
+
+ ret = fpathconf(source_fd, _PC_ACL_NFS4);
+ if (ret > 0 ) {
+ acl_supported = 1;
+ acl_type = ACL_TYPE_NFS4;
+ } else if (ret < 0 && errno != EINVAL) {
+ warn("fpathconf(..., _PC_ACL_NFS4) failed for %s%s",
+ to.base, to.path);
+ return (-1);
+ }
+ if (acl_supported == 0) {
+ ret = fpathconf(source_fd, _PC_ACL_EXTENDED);
+ if (ret > 0 ) {
+ acl_supported = 1;
+ acl_type = ACL_TYPE_ACCESS;
+ } else if (ret < 0 && errno != EINVAL) {
+ warn("fpathconf(..., _PC_ACL_EXTENDED) failed for %s%s",
+ to.base, to.path);
+ return (-1);
+ }
+ }
+ if (acl_supported == 0)
+ return (0);
+
+ acl = acl_get_fd_np(source_fd, acl_type);
+ if (acl == NULL) {
+ warn("failed to get acl entries while setting %s%s",
+ to.base, to.path);
+ return (-1);
+ }
+ if (acl_is_trivial_np(acl, &trivial)) {
+ warn("acl_is_trivial() failed for %s%s",
+ to.base, to.path);
+ acl_free(acl);
+ return (-1);
+ }
+ if (trivial) {
+ acl_free(acl);
+ return (0);
+ }
+ if (acl_set_fd_np(dest_fd, acl, acl_type) < 0) {
+ warn("failed to set acl entries for %s%s",
+ to.base, to.path);
+ acl_free(acl);
+ return (-1);
+ }
+ acl_free(acl);
+ return (0);
+#endif
+}
+
+int
+preserve_dir_acls(const char *source_dir, const char *dest_dir)
+{
+#ifndef __FreeBSD__
+ (void)source_dir;
+ (void)dest_dir;
+ return (0);
+#else
+ int source_fd = -1, dest_fd = -1, ret;
+
+ if ((source_fd = open(source_dir, O_DIRECTORY | O_RDONLY)) < 0) {
+ warn("%s: failed to copy ACLs", source_dir);
+ return (-1);
+ }
+ dest_fd = (*dest_dir == '\0') ? to.dir :
+ openat(to.dir, dest_dir, O_DIRECTORY, AT_RESOLVE_BENEATH);
+ if (dest_fd < 0) {
+ warn("%s: failed to copy ACLs to %s%s", source_dir,
+ to.base, dest_dir);
+ close(source_fd);
+ return (-1);
+ }
+ if ((ret = preserve_fd_acls(source_fd, dest_fd)) != 0) {
+ /* preserve_fd_acls() already printed a message */
+ }
+ if (dest_fd != to.dir)
+ close(dest_fd);
+ close(source_fd);
+ return (ret);
+#endif
+}
+
+void
+usage(void)
+{
+
+ (void)fprintf(stderr, "%s\n%s\n",
+ "usage: cp [-R [-H | -L | -P]] [-f | -i | -n] [-alpsvx] "
+ "source_file target_file",
+ " cp [-R [-H | -L | -P]] [-f | -i | -n] [-alpsvx] "
+ "source_file ... "
+ "target_directory");
+ exit(EX_USAGE);
+}