diff options
| author | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:26:51 +0300 |
|---|---|---|
| committer | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:26:51 +0300 |
| commit | ec6a123cffbe492c576ec1ad545d5296321a86e1 (patch) | |
| tree | 95d04b9d34bfa1b68888ee28937016ad4677ca87 /corebinutils | |
| parent | 6a34aa4538028e0ee7d7df21b1579e38b2d19c65 (diff) | |
| parent | b972052b8d6d2339bc27f924db78cfd35df73c30 (diff) | |
| download | Project-Tick-ec6a123cffbe492c576ec1ad545d5296321a86e1.tar.gz Project-Tick-ec6a123cffbe492c576ec1ad545d5296321a86e1.zip | |
Add 'corebinutils/ln/' from commit 'b972052b8d6d2339bc27f924db78cfd35df73c30'
git-subtree-dir: corebinutils/ln
git-subtree-mainline: 6a34aa4538028e0ee7d7df21b1579e38b2d19c65
git-subtree-split: b972052b8d6d2339bc27f924db78cfd35df73c30
Diffstat (limited to 'corebinutils')
| -rw-r--r-- | corebinutils/ln/.gitignore | 25 | ||||
| -rw-r--r-- | corebinutils/ln/GNUmakefile | 40 | ||||
| -rw-r--r-- | corebinutils/ln/LICENSE | 32 | ||||
| -rw-r--r-- | corebinutils/ln/LICENSES/BSD-3-Clause.txt | 11 | ||||
| -rw-r--r-- | corebinutils/ln/README.md | 33 | ||||
| -rw-r--r-- | corebinutils/ln/ln.1 | 319 | ||||
| -rw-r--r-- | corebinutils/ln/ln.c | 630 | ||||
| -rw-r--r-- | corebinutils/ln/symlink.7 | 498 | ||||
| -rw-r--r-- | corebinutils/ln/tests/test.sh | 315 | ||||
| -rw-r--r-- | corebinutils/ln/tree.txt | 901 |
10 files changed, 2804 insertions, 0 deletions
diff --git a/corebinutils/ln/.gitignore b/corebinutils/ln/.gitignore new file mode 100644 index 0000000000..a74d30b48c --- /dev/null +++ b/corebinutils/ln/.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/ln/GNUmakefile b/corebinutils/ln/GNUmakefile new file mode 100644 index 0000000000..314a164ecc --- /dev/null +++ b/corebinutils/ln/GNUmakefile @@ -0,0 +1,40 @@ +.DEFAULT_GOAL := all + +CC ?= cc +CPPFLAGS ?= +CPPFLAGS += -D_POSIX_C_SOURCE=200809L +CFLAGS ?= -O2 +CFLAGS += -std=c17 -g -Wall -Wextra -Werror +LDFLAGS ?= +LDLIBS ?= + +OBJDIR := $(CURDIR)/build +OUTDIR := $(CURDIR)/out +TARGET := $(OUTDIR)/ln +LINK_TARGET := $(OUTDIR)/link +OBJS := $(OBJDIR)/ln.o + +.PHONY: all clean dirs status test + +all: $(TARGET) $(LINK_TARGET) + +dirs: + @mkdir -p "$(OBJDIR)" "$(OUTDIR)" + +$(TARGET): $(OBJS) | dirs + $(CC) $(LDFLAGS) -o "$@" $(OBJS) $(LDLIBS) + +$(LINK_TARGET): $(TARGET) | dirs + ln -sf "ln" "$@" + +$(OBJDIR)/ln.o: $(CURDIR)/ln.c | dirs + $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/ln.c" -o "$@" + +test: $(TARGET) $(LINK_TARGET) + LN_BIN="$(TARGET)" LINK_BIN="$(LINK_TARGET)" sh "$(CURDIR)/tests/test.sh" + +status: + @printf '%s\n' "$(TARGET)" + +clean: + @rm -rf "$(OBJDIR)" "$(OUTDIR)" diff --git a/corebinutils/ln/LICENSE b/corebinutils/ln/LICENSE new file mode 100644 index 0000000000..6b7184dfd3 --- /dev/null +++ b/corebinutils/ln/LICENSE @@ -0,0 +1,32 @@ +Copyright (c) 1987, 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. diff --git a/corebinutils/ln/LICENSES/BSD-3-Clause.txt b/corebinutils/ln/LICENSES/BSD-3-Clause.txt new file mode 100644 index 0000000000..ea890afbc7 --- /dev/null +++ b/corebinutils/ln/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/ln/README.md b/corebinutils/ln/README.md new file mode 100644 index 0000000000..5f23e4ae54 --- /dev/null +++ b/corebinutils/ln/README.md @@ -0,0 +1,33 @@ +# ln + +Standalone Linux-native port of FreeBSD `ln` 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 + +- The FreeBSD `ln` control flow was rewritten into a standalone Linux-native utility instead of preserving BSD build glue or `err(3)` helpers. +- Hard-link creation maps directly to Linux `linkat(2)` with `AT_SYMLINK_FOLLOW` for `-L` and flag `0` for `-P`. +- The default hard-link behavior follows the FreeBSD utility, not GNU coreutils: source symlinks are dereferenced by default, equivalent to `-L`. +- Symbolic-link creation maps directly to Linux `symlink(2)`. +- Existing-target replacement uses Linux `unlink(2)` for non-directories and `rmdir(2)` for `-sF` empty-directory replacement. +- Path handling is dynamic; the port does not depend on `PATH_MAX`, `strlcpy(3)`, `getprogname(3)`, or other BSD-only libc interfaces. + +## Supported / Unsupported Semantics + +- Supported: `ln source [target]`, `ln source ... target_dir`, `link source target`, `-L`, `-P`, `-s`, `-f`, `-F`, `-h`, `-n`, `-i`, `-v`, and `-w`. +- Supported: replacing a symlink that points at a directory with `-snhf` / `-snf`, and replacing an empty directory with `-sF`. +- Unsupported by design: GNU long options and GNU-specific parsing extensions. This port keeps the BSD/POSIX command-line surface strict. +- Unsupported on Linux by kernel/filesystem semantics: hard-linking directories and cross-filesystem hard links. Those fail with the native Linux error instead of being masked. diff --git a/corebinutils/ln/ln.1 b/corebinutils/ln/ln.1 new file mode 100644 index 0000000000..43522e0c1c --- /dev/null +++ b/corebinutils/ln/ln.1 @@ -0,0 +1,319 @@ +.\"- +.\" Copyright (c) 1980, 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 June 12, 2017 +.Dt LN 1 +.Os +.Sh NAME +.Nm ln , +.Nm link +.Nd link files +.Sh SYNOPSIS +.Nm +.Op Fl L | Fl P | Fl s Op Fl F +.Op Fl f | iw +.Op Fl hnv +.Ar source_file +.Op Ar target_file +.Nm +.Op Fl L | Fl P | Fl s Op Fl F +.Op Fl f | iw +.Op Fl hnv +.Ar source_file ... +.Ar target_dir +.Nm link +.Ar source_file Ar target_file +.Sh DESCRIPTION +The +.Nm +utility creates a new directory entry (linked file) for the file name +specified by +.Ar target_file . +The +.Ar target_file +will be created with the same file modes as the +.Ar source_file . +It is useful for maintaining multiple copies of a file in many places +at once without using up storage for the +.Dq copies ; +instead, a link +.Dq points +to the original copy. +There are two types of links; hard links and symbolic links. +How a link +.Dq points +to a file is one of the differences between a hard and symbolic link. +.Pp +The options are as follows: +.Bl -tag -width flag +.It Fl F +If the target file already exists and is a directory, then remove it +so that the link may occur. +The +.Fl F +option should be used with either +.Fl f +or +.Fl i +options. +If neither +.Fl f +nor +.Fl i +is specified, +.Fl f +is implied. +The +.Fl F +option is a no-op unless +.Fl s +is specified. +.It Fl L +When creating a hard link to a symbolic link, +create a hard link to the target of the symbolic link. +This is the default. +This option cancels the +.Fl P +option. +.It Fl P +When creating a hard link to a symbolic link, +create a hard link to the symbolic link itself. +This option cancels the +.Fl L +option. +.It Fl f +If the target file already exists, +then unlink it so that the link may occur. +(The +.Fl f +option overrides any previous +.Fl i +and +.Fl w +options.) +.It Fl h +If the +.Ar target_file +or +.Ar target_dir +is a symbolic link, do not follow it. +This is most useful with the +.Fl f +option, to replace a symlink which may point to a directory. +.It Fl i +Cause +.Nm +to write a prompt to standard error if the target file exists. +If the response from the standard input begins with the character +.Sq Li y +or +.Sq Li Y , +then unlink the target file so that the link may occur. +Otherwise, do not attempt the link. +(The +.Fl i +option overrides any previous +.Fl f +options.) +.It Fl n +Same as +.Fl h , +for compatibility with other +.Nm +implementations. +.It Fl s +Create a symbolic link. +.It Fl v +Cause +.Nm +to be verbose, showing files as they are processed. +.It Fl w +Warn if the source of a symbolic link does not currently exist. +.El +.Pp +By default, +.Nm +makes +.Em hard +links. +A hard link to a file is indistinguishable from the original directory entry; +any changes to a file are effectively independent of the name used to reference +the file. +Directories may not be hardlinked, and hard links may not span file systems. +.Pp +A symbolic link contains the name of the file to +which it is linked. +The referenced file is used when an +.Xr open 2 +operation is performed on the link. +A +.Xr stat 2 +on a symbolic link will return the linked-to file; an +.Xr lstat 2 +must be done to obtain information about the link. +The +.Xr readlink 2 +call may be used to read the contents of a symbolic link. +Symbolic links may span file systems and may refer to directories. +.Pp +Given one or two arguments, +.Nm +creates a link to an existing file +.Ar source_file . +If +.Ar target_file +is given, the link has that name; +.Ar target_file +may also be a directory in which to place the link; +otherwise it is placed in the current directory. +If only the directory is specified, the link will be made +to the last component of +.Ar source_file . +.Pp +Given more than two arguments, +.Nm +makes links in +.Ar target_dir +to all the named source files. +The links made will have the same name as the files being linked to. +.Pp +When the utility is called as +.Nm link , +exactly two arguments must be supplied, +neither of which may specify a directory. +No options may be supplied in this simple mode of operation, +which performs a +.Xr link 2 +operation using the two passed arguments. +.Sh EXAMPLES +Create a symbolic link named +.Pa /home/src +and point it to +.Pa /usr/src : +.Pp +.Dl # ln -s /usr/src /home/src +.Pp +Hard link +.Pa /usr/local/bin/fooprog +to file +.Pa /usr/local/bin/fooprog-1.0 : +.Pp +.Dl # ln /usr/local/bin/fooprog-1.0 /usr/local/bin/fooprog +.Pp +As an exercise, try the following commands: +.Bd -literal -offset indent +# ls -i /bin/[ +11553 /bin/[ +# ls -i /bin/test +11553 /bin/test +.Ed +.Pp +Note that both files have the same inode; that is, +.Pa /bin/[ +is essentially an alias for the +.Xr test 1 +command. +This hard link exists so +.Xr test 1 +may be invoked from shell scripts, for example, using the +.Li "if [ ]" +construct. +.Pp +In the next example, the second call to +.Nm +removes the original +.Pa foo +and creates a replacement pointing to +.Pa baz : +.Bd -literal -offset indent +# mkdir bar baz +# ln -s bar foo +# ln -shf baz foo +.Ed +.Pp +Without the +.Fl h +option, this would instead leave +.Pa foo +pointing to +.Pa bar +and inside +.Pa foo +create a new symlink +.Pa baz +pointing to itself. +This results from directory-walking. +.Pp +An easy rule to remember is that the argument order for +.Nm +is the same as for +.Xr cp 1 : +The first argument needs to exist, the second one is created. +.Sh COMPATIBILITY +The +.Fl h , +.Fl i , +.Fl n , +.Fl v +and +.Fl w +options are non-standard and their use in scripts is not recommended. +They are provided solely for compatibility with other +.Nm +implementations. +.Pp +The +.Fl F +option is a +.Fx +extension and should not be used in portable scripts. +.Sh SEE ALSO +.Xr link 2 , +.Xr lstat 2 , +.Xr readlink 2 , +.Xr stat 2 , +.Xr symlink 2 , +.Xr symlink 7 +.Sh STANDARDS +The +.Nm +utility conforms to +.St -p1003.2-92 . +.Pp +The simplified +.Nm link +command conforms to +.St -susv2 . +.Sh HISTORY +An +.Nm +command appeared in +.At v1 . diff --git a/corebinutils/ln/ln.c b/corebinutils/ln/ln.c new file mode 100644 index 0000000000..63e1d27c7e --- /dev/null +++ b/corebinutils/ln/ln.c @@ -0,0 +1,630 @@ +/*- + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 1987, 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. + */ + +#define _POSIX_C_SOURCE 200809L + +#include <errno.h> +#include <fcntl.h> +#include <stdarg.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <unistd.h> + +struct ln_options { + bool force; + bool remove_dir; + bool no_target_follow; + bool interactive; + bool follow_source_symlink; + bool symbolic; + bool verbose; + bool warn_missing; + char linkch; +}; + +static const char *progname; + +static void error_errno(const char *fmt, ...); +static void error_msg(const char *fmt, ...); +static char *join_path(const char *dir, const char *name); +static int link_usage(void); +static int ln_usage(void); +static int linkit(const struct ln_options *options, const char *source, + const char *target, bool target_is_dir); +static const char *path_basename_start(const char *path); +static size_t path_basename_len(const char *path); +static char *path_basename_dup(const char *path); +static char *path_dirname_dup(const char *path); +static const char *program_name(const char *argv0); +static int prompt_replace(const char *target); +static int remove_existing_target(const struct ln_options *options, + const char *target, const struct stat *target_sb); +static int samedirent(const char *path1, const char *path2); +static bool should_append_basename(const struct ln_options *options, + const char *target, bool target_is_dir); +static int stat_parent_dir(const char *path, struct stat *sb); +static void warn_missing_symlink_source(const char *source, + const char *target); + +static const char * +program_name(const char *argv0) +{ + const char *name; + + if (argv0 == NULL || argv0[0] == '\0') + return ("ln"); + 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(1); + } + return (ptr); +} + +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 char * +path_basename_dup(const char *path) +{ + const char *start; + size_t len; + char *name; + + start = path_basename_start(path); + len = path_basename_len(path); + name = xmalloc(len + 1); + memcpy(name, start, len); + name[len] = '\0'; + return (name); +} + +static const char * +path_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 +path_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 = path_basename_start(path); + return ((size_t)(end - start)); +} + +static char * +path_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 +samedirent(const char *path1, const char *path2) +{ + const char *base1; + const char *base2; + struct stat sb1; + struct stat sb2; + size_t base1_len; + size_t base2_len; + + if (strcmp(path1, path2) == 0) + return (1); + + base1 = path_basename_start(path1); + base2 = path_basename_start(path2); + base1_len = path_basename_len(path1); + base2_len = path_basename_len(path2); + if (base1_len != base2_len || memcmp(base1, base2, base1_len) != 0) + return (0); + + if (stat_parent_dir(path1, &sb1) != 0 || stat_parent_dir(path2, &sb2) != 0) + return (0); + + return (sb1.st_dev == sb2.st_dev && sb1.st_ino == sb2.st_ino); +} + +static bool +should_append_basename(const struct ln_options *options, const char *target, + bool target_is_dir) +{ + struct stat sb; + const char *base; + + base = strrchr(target, '/'); + base = base == NULL ? target : base + 1; + if (base[0] == '\0' || (base[0] == '.' && base[1] == '\0')) + return (true); + + if (options->remove_dir) + return (false); + if (target_is_dir) + return (true); + if (lstat(target, &sb) == 0 && S_ISDIR(sb.st_mode)) + return (true); + if (options->no_target_follow) + return (false); + if (stat(target, &sb) == 0 && S_ISDIR(sb.st_mode)) + return (true); + return (false); +} + +static int +stat_parent_dir(const char *path, struct stat *sb) +{ + char *dir; + size_t dir_len; + const char *base; + + base = path_basename_start(path); + if (base == path) + return (stat(".", sb)); + + dir_len = (size_t)(base - path); + while (dir_len > 1 && path[dir_len - 1] == '/') + dir_len--; + if (dir_len == 1 && path[0] == '/') + return (stat("/", sb)); + + dir = xmalloc(dir_len + 1); + memcpy(dir, path, dir_len); + dir[dir_len] = '\0'; + if (stat(dir, sb) != 0) { + free(dir); + return (-1); + } + free(dir); + return (0); +} + +static int +remove_existing_target(const struct ln_options *options, const char *target, + const struct stat *target_sb) +{ + if (options->remove_dir && S_ISDIR(target_sb->st_mode)) { + if (rmdir(target) != 0) { + error_errno("%s", target); + return (1); + } + return (0); + } + + if (unlink(target) != 0) { + error_errno("%s", target); + return (1); + } + return (0); +} + +static void +warn_missing_symlink_source(const char *source, const char *target) +{ + char *dir; + char *resolved; + struct stat st; + + if (source[0] == '/') { + if (stat(source, &st) != 0) + error_errno("warning: %s", source); + return; + } + + dir = path_dirname_dup(target); + resolved = join_path(dir, source); + if (stat(resolved, &st) != 0) + error_errno("warning: %s", source); + free(dir); + free(resolved); +} + +static int +prompt_replace(const char *target) +{ + char answer[16]; + bool stdin_is_tty; + int ch; + + stdin_is_tty = isatty(STDIN_FILENO) == 1; + (void)stdin_is_tty; + + (void)fflush(stdout); + (void)fprintf(stderr, "replace %s? ", target); + if (fgets(answer, sizeof(answer), stdin) == NULL) { + if (ferror(stdin)) { + error_errno("stdin"); + return (-1); + } + if (!stdin_is_tty && feof(stdin)) { + (void)fprintf(stderr, "not replaced\n"); + return (1); + } + (void)fprintf(stderr, "not replaced\n"); + return (1); + } + + if (strchr(answer, '\n') == NULL) { + while ((ch = getchar()) != '\n' && ch != EOF) + continue; + if (ferror(stdin)) { + error_errno("stdin"); + return (-1); + } + } + + if (answer[0] != 'y' && answer[0] != 'Y') { + (void)fprintf(stderr, "not replaced\n"); + return (1); + } + return (0); +} + +static int +linkit(const struct ln_options *options, const char *source, const char *target, + bool target_is_dir) +{ + struct stat source_sb; + struct stat target_sb; + char *resolved_target; + bool exists; + int flags; + int ret; + + resolved_target = NULL; + ret = 1; + if (!options->symbolic) { + if ((options->follow_source_symlink ? stat : lstat)(source, + &source_sb) != 0) { + error_errno("%s", source); + goto cleanup; + } + if (S_ISDIR(source_sb.st_mode)) { + errno = EISDIR; + error_errno("%s", source); + goto cleanup; + } + } + + if (should_append_basename(options, target, target_is_dir)) { + char *base; + + base = path_basename_dup(source); + resolved_target = join_path(target, base); + free(base); + } else { + resolved_target = xstrdup(target); + } + + if (options->symbolic && options->warn_missing) + warn_missing_symlink_source(source, resolved_target); + + exists = lstat(resolved_target, &target_sb) == 0; + if (exists && !options->symbolic && samedirent(source, resolved_target)) { + error_msg("%s and %s are the same directory entry", source, + resolved_target); + goto cleanup; + } + + if (exists && options->force) { + if (remove_existing_target(options, resolved_target, &target_sb) != 0) + goto cleanup; + } else if (exists && options->interactive) { + ret = prompt_replace(resolved_target); + if (ret != 0) + goto cleanup; + if (remove_existing_target(options, resolved_target, &target_sb) != 0) + goto cleanup; + } + + flags = options->follow_source_symlink ? AT_SYMLINK_FOLLOW : 0; + if (options->symbolic) { + if (symlink(source, resolved_target) != 0) { + error_errno("%s", resolved_target); + goto cleanup; + } + } else if (linkat(AT_FDCWD, source, AT_FDCWD, resolved_target, flags) != 0) { + error_errno("%s", resolved_target); + goto cleanup; + } + + if (options->verbose) + (void)printf("%s %c> %s\n", resolved_target, options->linkch, source); + + ret = 0; +cleanup: + free(resolved_target); + return (ret); +} + +static int +link_usage(void) +{ + (void)fprintf(stderr, "usage: link source_file target_file\n"); + return (2); +} + +static int +ln_usage(void) +{ + (void)fprintf(stderr, + "usage: ln [-s [-F] | -L | -P] [-f | -i] [-hnvw] " + "source_file [target_file]\n"); + (void)fprintf(stderr, + " ln [-s [-F] | -L | -P] [-f | -i] [-hnvw] " + "source_file ... target_dir\n"); + return (2); +} + +int +main(int argc, char *argv[]) +{ + struct ln_options options; + struct stat sb; + char *targetdir; + int ch; + int exitval; + + progname = program_name(argv[0]); + memset(&options, 0, sizeof(options)); + /* FreeBSD hard-link semantics default to -L, unlike GNU ln's -P. */ + options.follow_source_symlink = true; + + if (strcmp(progname, "link") == 0) { + opterr = 0; + while ((ch = getopt(argc, argv, "")) != -1) + return (link_usage()); + argc -= optind; + argv += optind; + if (argc != 2) + return (link_usage()); + if (lstat(argv[1], &sb) == 0) { + errno = EEXIST; + error_errno("%s", argv[1]); + return (1); + } + return (linkit(&options, argv[0], argv[1], false)); + } + + opterr = 0; + while ((ch = getopt(argc, argv, "FLPfhinsvw")) != -1) { + switch (ch) { + case 'F': + options.remove_dir = true; + break; + case 'L': + options.follow_source_symlink = true; + break; + case 'P': + options.follow_source_symlink = false; + break; + case 'f': + options.force = true; + options.interactive = false; + options.warn_missing = false; + break; + case 'h': + case 'n': + options.no_target_follow = true; + break; + case 'i': + options.interactive = true; + options.force = false; + break; + case 's': + options.symbolic = true; + break; + case 'v': + options.verbose = true; + break; + case 'w': + options.warn_missing = true; + break; + case '?': + default: + if (optopt != 0) + error_msg("unknown option -- %c", optopt); + return (ln_usage()); + } + } + + argv += optind; + argc -= optind; + + options.linkch = options.symbolic ? '-' : '='; + if (!options.symbolic) + options.remove_dir = false; + if (options.remove_dir && !options.interactive) { + options.force = true; + options.warn_missing = false; + } + + switch (argc) { + case 0: + return (ln_usage()); + case 1: + return (linkit(&options, argv[0], ".", true)); + case 2: + return (linkit(&options, argv[0], argv[1], false)); + default: + break; + } + + targetdir = argv[argc - 1]; + if (options.no_target_follow && lstat(targetdir, &sb) == 0 && + S_ISLNK(sb.st_mode)) { + errno = ENOTDIR; + error_errno("%s", targetdir); + return (1); + } + if (stat(targetdir, &sb) != 0) { + error_errno("%s", targetdir); + return (1); + } + if (!S_ISDIR(sb.st_mode)) + return (ln_usage()); + + exitval = 0; + for (int i = 0; i < argc - 1; i++) + exitval |= linkit(&options, argv[i], targetdir, true); + return (exitval); +} diff --git a/corebinutils/ln/symlink.7 b/corebinutils/ln/symlink.7 new file mode 100644 index 0000000000..db251e1d29 --- /dev/null +++ b/corebinutils/ln/symlink.7 @@ -0,0 +1,498 @@ +.\"- +.\" Copyright (c) 1992, 1993, 1994 +.\" The Regents of the University of California. 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. +.\" +.Dd August 11, 2024 +.Dt SYMLINK 7 +.Os +.Sh NAME +.Nm symlink +.Nd symbolic link handling +.Sh SYMBOLIC LINK HANDLING +Symbolic links are files that act as pointers to other files. +To understand their behavior, you must first understand how hard links +work. +A hard link to a file is indistinguishable from the original file because +it is a reference to the object underlying the original file name. +Changes to a file are independent of the name used to reference the +file. +Hard links may not refer to directories and may not reference files +on different file systems. +A symbolic link contains the name of the file to which it is linked, +i.e., it is a pointer to another name, and not to an underlying object. +For this reason, symbolic links may reference directories and may span +file systems. +.Pp +Because a symbolic link and its referenced object coexist in the file system +name space, confusion can arise in distinguishing between the link itself +and the referenced object. +Historically, commands and system calls have adopted their own link +following conventions in a somewhat ad-hoc fashion. +Rules for more a uniform approach, as they are implemented in this system, +are outlined here. +It is important that local applications conform to these rules, too, +so that the user interface can be as consistent as possible. +.Pp +Symbolic links are handled either by operating on the link itself, +or by operating on the object referenced by the link. +In the latter case, +an application or system call is said to +.Dq follow +the link. +Symbolic links may reference other symbolic links, +in which case the links are dereferenced until an object that is +not a symbolic link is found, +a symbolic link which references a file which does not exist is found, +or a loop is detected. +(Loop detection is done by placing an upper limit on the number of +links that may be followed, and an error results if this limit is +exceeded.) +.Pp +There are four separate areas that need to be discussed. +They are as follows: +.Pp +.Bl -enum -compact -offset indent +.It +Symbolic links used as file name arguments for system calls. +.It +Mount options to ignore symbolic links. +.It +Symbolic links specified as command line arguments to utilities that +are not traversing a file tree. +.It +Symbolic links encountered by utilities that are traversing a file tree +(either specified on the command line or encountered as part of the +file hierarchy walk). +.El +.Ss System calls. +The first area is symbolic links used as file name arguments for +system calls. +.Pp +Except as noted below, all system calls follow symbolic links. +For example, if there were a symbolic link +.Dq Li slink +which pointed to a file named +.Dq Li afile , +the system call +.Dq Li open("slink" ...\&) +would return a file descriptor to the file +.Dq afile . +.Pp +There are thirteen system calls that do not follow links, and which operate +on the symbolic link itself. +They are: +.Xr lchflags 2 , +.Xr lchmod 2 , +.Xr lchown 2 , +.Xr lpathconf 2 , +.Xr lstat 2 , +.Xr lutimes 2 , +.Xr readlink 2 , +.Xr readlinkat 2 , +.Xr rename 2 , +.Xr renameat 2 , +.Xr rmdir 2 , +.Xr unlink 2 , +and +.Xr unlinkat 2 . +Because +.Xr remove 3 +is an alias for +.Xr unlink 2 , +it also does not follow symbolic links. +When +.Xr rmdir 2 +or +.Xr unlinkat 2 +with the +.Dv AT_REMOVEDIR +flag +is applied to a symbolic link, it fails with the error +.Er ENOTDIR . +.Pp +The +.Xr linkat 2 +system call does not follow symbolic links +unless given the +.Dv AT_SYMLINK_FOLLOW +flag. +.Pp +The following system calls follow symbolic links +unless given the +.Dv AT_SYMLINK_NOFOLLOW +flag: +.Xr chflagsat 2 , +.Xr faccessat 2 , +.Xr fchmodat 2 , +.Xr fchownat 2 , +.Xr fstatat 2 +and +.Xr utimensat 2 . +.Pp +The owner and group of an existing symbolic link can be changed by +means of the +.Xr lchown 2 +system call. +The flags, access permissions, owner/group and modification time of +an existing symbolic link can be changed by means of the +.Xr lchflags 2 , +.Xr lchmod 2 , +.Xr lchown 2 , +and +.Xr lutimes 2 +system calls, respectively. +Of these, only the flags and ownership are used by the system; +the access permissions are ignored. +.Pp +The +.Bx 4.4 +system differs from historical +.Bx 4 +systems in that the system call +.Xr chown 2 +has been changed to follow symbolic links. +The +.Xr lchown 2 +system call was added later when the limitations of the new +.Xr chown 2 +became apparent. +.Ss Mount options +.Fx +has a +.Xr mount 8 +option nosymfollow. When this option is enabled, the kernel +does not follow symlinks on the mounted file system and return EACCES. +You can still create or remove symlinks, or read the value of a symbolic link. +.Pp +This option is intended to be used when mounting file systems from +untrusted external storage systems or public writable /tmp file systems +to prevent symlink-based privilege escalation and sandbox escape attacks. +.Pp +The mount option nosymfollow first appeared in +.Fx 3.0 +.Ss Commands not traversing a file tree. +The second area is symbolic links, specified as command line file +name arguments, to commands which are not traversing a file tree. +.Pp +Except as noted below, commands follow symbolic links named as command +line arguments. +For example, if there were a symbolic link +.Dq Li slink +which pointed to a file named +.Dq Li afile , +the command +.Dq Li cat slink +would display the contents of the file +.Dq Li afile . +.Pp +It is important to realize that this rule includes commands which may +optionally traverse file trees, e.g.\& the command +.Dq Li "chown file" +is included in this rule, while the command +.Dq Li "chown -R file" +is not. +(The latter is described in the third area, below.) +.Pp +If it is explicitly intended that the command operate on the symbolic +link instead of following the symbolic link, e.g., it is desired that +.Dq Li "chown slink" +change the ownership of the file that +.Dq Li slink +is, whether it is a symbolic link or not, the +.Fl h +option should be used. +In the above example, +.Dq Li "chown root slink" +would change the ownership of the file referenced by +.Dq Li slink , +while +.Dq Li "chown -h root slink" +would change the ownership of +.Dq Li slink +itself. +.Pp +There are five exceptions to this rule. +The +.Xr mv 1 +and +.Xr rm 1 +commands do not follow symbolic links named as arguments, +but respectively attempt to rename and delete them. +(Note, if the symbolic link references a file via a relative path, +moving it to another directory may very well cause it to stop working, +since the path may no longer be correct.) +.Pp +The +.Xr ls 1 +command is also an exception to this rule. +For compatibility with historic systems (when +.Nm ls +is not doing a tree walk, i.e., the +.Fl R +option is not specified), +the +.Nm ls +command follows symbolic links named as arguments if the +.Fl H +or +.Fl L +option is specified, +or if the +.Fl F , +.Fl d +or +.Fl l +options are not specified. +(The +.Nm ls +command is the only command where the +.Fl H +and +.Fl L +options affect its behavior even though it is not doing a walk of +a file tree.) +.Pp +The +.Xr file 1 +and +.Xr stat 1 +commands are also exceptions to this rule. +These +commands do not follow symbolic links named as argument by default, +but do follow symbolic links named as argument if the +.Fl L +option is specified. +.Pp +The +.Bx 4.4 +system differs from historical +.Bx 4 +systems in that the +.Nm chown +and +.Nm chgrp +commands follow symbolic links specified on the command line. +.Ss Commands traversing a file tree. +The following commands either optionally or always traverse file trees: +.Xr chflags 1 , +.Xr chgrp 1 , +.Xr chmod 1 , +.Xr cp 1 , +.Xr du 1 , +.Xr find 1 , +.Xr ls 1 , +.Xr pax 1 , +.Xr rm 1 , +.Xr tar 1 +and +.Xr chown 8 . +.Pp +It is important to realize that the following rules apply equally to +symbolic links encountered during the file tree traversal and symbolic +links listed as command line arguments. +.Pp +The first rule applies to symbolic links that reference files that are +not of type directory. +Operations that apply to symbolic links are performed on the links +themselves, but otherwise the links are ignored. +.Pp +The command +.Dq Li "rm -r slink directory" +will remove +.Dq Li slink , +as well as any symbolic links encountered in the tree traversal of +.Dq Li directory , +because symbolic links may be removed. +In no case will +.Nm rm +affect the file which +.Dq Li slink +references in any way. +.Pp +The second rule applies to symbolic links that reference files of type +directory. +Symbolic links which reference files of type directory are never +.Dq followed +by default. +This is often referred to as a +.Dq physical +walk, as opposed to a +.Dq logical +walk (where symbolic links referencing directories are followed). +.Pp +As consistently as possible, you can make commands doing a file tree +walk follow any symbolic links named on the command line, regardless +of the type of file they reference, by specifying the +.Fl H +(for +.Dq half\-logical ) +flag. +This flag is intended to make the command line name space look +like the logical name space. +(Note, for commands that do not always do file tree traversals, the +.Fl H +flag will be ignored if the +.Fl R +flag is not also specified.) +.Pp +For example, the command +.Dq Li "chown -HR user slink" +will traverse the file hierarchy rooted in the file pointed to by +.Dq Li slink . +Note, the +.Fl H +is not the same as the previously discussed +.Fl h +flag. +The +.Fl H +flag causes symbolic links specified on the command line to be +dereferenced both for the purposes of the action to be performed +and the tree walk, and it is as if the user had specified the +name of the file to which the symbolic link pointed. +.Pp +As consistently as possible, you can make commands doing a file tree +walk follow any symbolic links named on the command line, as well as +any symbolic links encountered during the traversal, regardless of +the type of file they reference, by specifying the +.Fl L +(for +.Dq logical ) +flag. +This flag is intended to make the entire name space look like +the logical name space. +(Note, for commands that do not always do file tree traversals, the +.Fl L +flag will be ignored if the +.Fl R +flag is not also specified.) +.Pp +For example, the command +.Dq Li "chown -LR user slink" +will change the owner of the file referenced by +.Dq Li slink . +If +.Dq Li slink +references a directory, +.Nm chown +will traverse the file hierarchy rooted in the directory that it +references. +In addition, if any symbolic links are encountered in any file tree that +.Nm chown +traverses, they will be treated in the same fashion as +.Dq Li slink . +.Pp +As consistently as possible, you can specify the default behavior by +specifying the +.Fl P +(for +.Dq physical ) +flag. +This flag is intended to make the entire name space look like the +physical name space. +.Pp +For commands that do not by default do file tree traversals, the +.Fl H , +.Fl L +and +.Fl P +flags are ignored if the +.Fl R +flag is not also specified. +In addition, you may specify the +.Fl H , +.Fl L +and +.Fl P +options more than once; the last one specified determines the +command's behavior. +This is intended to permit you to alias commands to behave one way +or the other, and then override that behavior on the command line. +.Pp +The +.Xr ls 1 +and +.Xr rm 1 +commands have exceptions to these rules. +The +.Nm rm +command operates on the symbolic link, and not the file it references, +and therefore never follows a symbolic link. +The +.Nm rm +command does not support the +.Fl H , +.Fl L +or +.Fl P +options. +.Pp +To maintain compatibility with historic systems, +the +.Nm ls +command acts a little differently. +If you do not specify the +.Fl F , +.Fl d +or +.Fl l +options, +.Nm ls +will follow symbolic links specified on the command line. +If the +.Fl L +flag is specified, +.Nm ls +follows all symbolic links, +regardless of their type, +whether specified on the command line or encountered in the tree walk. +.Sh SEE ALSO +.Xr chflags 1 , +.Xr chgrp 1 , +.Xr chmod 1 , +.Xr cp 1 , +.Xr du 1 , +.Xr find 1 , +.Xr ln 1 , +.Xr ls 1 , +.Xr mv 1 , +.Xr pax 1 , +.Xr rm 1 , +.Xr tar 1 , +.Xr lchflags 2 , +.Xr lchmod 2 , +.Xr lchown 2 , +.Xr lstat 2 , +.Xr lutimes 2 , +.Xr readlink 2 , +.Xr rename 2 , +.Xr symlink 2 , +.Xr unlink 2 , +.Xr fts 3 , +.Xr remove 3 , +.Xr chown 8 , +.Xr mount 8 diff --git a/corebinutils/ln/tests/test.sh b/corebinutils/ln/tests/test.sh new file mode 100644 index 0000000000..d85f28ee47 --- /dev/null +++ b/corebinutils/ln/tests/test.sh @@ -0,0 +1,315 @@ +#!/bin/sh +set -eu + +ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) +LN_BIN=${LN_BIN:-"$ROOT/out/ln"} +LINK_BIN=${LINK_BIN:-"$ROOT/out/link"} +TMPDIR=${TMPDIR:-/tmp} +WORKDIR=$(mktemp -d "$TMPDIR/ln-test.XXXXXX") +STDOUT_FILE="$WORKDIR/stdout" +STDERR_FILE="$WORKDIR/stderr" +LAST_STATUS=0 +LAST_STDOUT= +LAST_STDERR= +trap 'rm -rf "$WORKDIR"' EXIT INT TERM + +export LC_ALL=C + +LN_USAGE=$(cat <<'EOF' +usage: ln [-s [-F] | -L | -P] [-f | -i] [-hnvw] source_file [target_file] + ln [-s [-F] | -L | -P] [-f | -i] [-hnvw] source_file ... target_dir +EOF +) + +LINK_USAGE=$(cat <<'EOF' +usage: link source_file target_file +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 + if [ -n "$text" ]; then + printf '%s\n' "FAIL: $name" >&2 + printf '%s\n' "--- expected empty ---" >&2 + printf '%s\n' "--- actual ---" >&2 + printf '%s' "$text" >&2 + printf '\n' >&2 + exit 1 + fi +} + +assert_status() { + name=$1 + expected=$2 + actual=$3 + if [ "$expected" -ne "$actual" ]; then + printf '%s\n' "FAIL: $name" >&2 + printf '%s\n' "expected status: $expected" >&2 + printf '%s\n' "actual status: $actual" >&2 + exit 1 + fi +} + +assert_same_inode() { + path1=$1 + path2=$2 + inode1=$(stat -c '%d:%i' "$path1") + inode2=$(stat -c '%d:%i' "$path2") + [ "$inode1" = "$inode2" ] || fail "$path1 and $path2 differ" +} + +assert_symlink_target() { + expected=$1 + path=$2 + [ -L "$path" ] || fail "$path is not a symlink" + actual=$(readlink "$path") + [ "$actual" = "$expected" ] || fail "$path target expected $expected got $actual" +} + +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") +} + +[ -x "$LN_BIN" ] || fail "missing binary: $LN_BIN" +[ -x "$LINK_BIN" ] || fail "missing binary: $LINK_BIN" + +run_capture "$LN_BIN" +assert_status "ln usage status" 2 "$LAST_STATUS" +assert_empty "ln usage stdout" "$LAST_STDOUT" +assert_eq "ln usage stderr" "$LN_USAGE" "$LAST_STDERR" + +run_capture "$LINK_BIN" +assert_status "link usage status" 2 "$LAST_STATUS" +assert_empty "link usage stdout" "$LAST_STDOUT" +assert_eq "link usage stderr" "$LINK_USAGE" "$LAST_STDERR" + +mkdir "$WORKDIR/basic" +printf 'payload\n' >"$WORKDIR/basic/source" +"$LN_BIN" "$WORKDIR/basic/source" "$WORKDIR/basic/target" +assert_same_inode "$WORKDIR/basic/source" "$WORKDIR/basic/target" + +mkdir "$WORKDIR/symlink" +"$LN_BIN" -s relative-target "$WORKDIR/symlink/link" +assert_symlink_target "relative-target" "$WORKDIR/symlink/link" + +mkdir "$WORKDIR/warn" +run_capture "$LN_BIN" -sw missing "$WORKDIR/warn/link" +assert_status "warn missing status" 0 "$LAST_STATUS" +assert_empty "warn missing stdout" "$LAST_STDOUT" +assert_contains "warn missing stderr" "$LAST_STDERR" "warning: missing" +assert_symlink_target "missing" "$WORKDIR/warn/link" + +mkdir "$WORKDIR/follow" +printf 'x\n' >"$WORKDIR/follow/file" +ln -s file "$WORKDIR/follow/source-link" +"$LN_BIN" "$WORKDIR/follow/source-link" "$WORKDIR/follow/hard-default" +assert_same_inode "$WORKDIR/follow/file" "$WORKDIR/follow/hard-default" +[ ! -L "$WORKDIR/follow/hard-default" ] || fail "default hard link did not follow source symlink" +"$LN_BIN" -L "$WORKDIR/follow/source-link" "$WORKDIR/follow/hard-follow" +assert_same_inode "$WORKDIR/follow/file" "$WORKDIR/follow/hard-follow" +[ ! -L "$WORKDIR/follow/hard-follow" ] || fail "-L produced a symlink" + +mkdir "$WORKDIR/nofollow" +printf 'x\n' >"$WORKDIR/nofollow/file" +ln -s file "$WORKDIR/nofollow/source-link" +"$LN_BIN" -P "$WORKDIR/nofollow/source-link" "$WORKDIR/nofollow/hard-link-to-symlink" +assert_same_inode "$WORKDIR/nofollow/source-link" "$WORKDIR/nofollow/hard-link-to-symlink" +assert_symlink_target "file" "$WORKDIR/nofollow/hard-link-to-symlink" + +mkdir "$WORKDIR/force" +printf 'old\n' >"$WORKDIR/force/dst" +"$LN_BIN" -f "$WORKDIR/basic/source" "$WORKDIR/force/dst" +assert_same_inode "$WORKDIR/basic/source" "$WORKDIR/force/dst" + +mkdir "$WORKDIR/interactive-yes" +printf 'src\n' >"$WORKDIR/interactive-yes/src" +printf 'dst\n' >"$WORKDIR/interactive-yes/dst" +if ! printf 'y\n' | "$LN_BIN" -i "$WORKDIR/interactive-yes/src" \ + "$WORKDIR/interactive-yes/dst" >"$STDOUT_FILE" 2>"$STDERR_FILE"; then + fail "interactive yes failed" +fi +LAST_STDOUT=$(cat "$STDOUT_FILE") +LAST_STDERR=$(cat "$STDERR_FILE") +assert_empty "interactive yes stdout" "$LAST_STDOUT" +assert_contains "interactive yes prompt" "$LAST_STDERR" \ + "replace $WORKDIR/interactive-yes/dst?" +assert_same_inode "$WORKDIR/interactive-yes/src" "$WORKDIR/interactive-yes/dst" + +mkdir "$WORKDIR/interactive-no" +printf 'src\n' >"$WORKDIR/interactive-no/src" +printf 'dst\n' >"$WORKDIR/interactive-no/dst" +if printf 'n\n' | "$LN_BIN" -i "$WORKDIR/interactive-no/src" \ + "$WORKDIR/interactive-no/dst" >"$STDOUT_FILE" 2>"$STDERR_FILE"; then + fail "interactive no unexpectedly succeeded" +else + LAST_STATUS=$? +fi +LAST_STDOUT=$(cat "$STDOUT_FILE") +LAST_STDERR=$(cat "$STDERR_FILE") +assert_status "interactive no status" 1 "$LAST_STATUS" +assert_empty "interactive no stdout" "$LAST_STDOUT" +assert_contains "interactive no prompt" "$LAST_STDERR" \ + "replace $WORKDIR/interactive-no/dst?" +assert_contains "interactive no not replaced" "$LAST_STDERR" "not replaced" +[ "$(cat "$WORKDIR/interactive-no/dst")" = "dst" ] || fail "interactive no replaced target" + +mkdir "$WORKDIR/interactive-eof" +printf 'src\n' >"$WORKDIR/interactive-eof/src" +printf 'dst\n' >"$WORKDIR/interactive-eof/dst" +if : | "$LN_BIN" -i "$WORKDIR/interactive-eof/src" \ + "$WORKDIR/interactive-eof/dst" >"$STDOUT_FILE" 2>"$STDERR_FILE"; then + fail "interactive eof unexpectedly succeeded" +else + LAST_STATUS=$? +fi +LAST_STDOUT=$(cat "$STDOUT_FILE") +LAST_STDERR=$(cat "$STDERR_FILE") +assert_status "interactive eof status" 1 "$LAST_STATUS" +assert_empty "interactive eof stdout" "$LAST_STDOUT" +assert_contains "interactive eof prompt" "$LAST_STDERR" \ + "replace $WORKDIR/interactive-eof/dst?" +assert_contains "interactive eof not replaced" "$LAST_STDERR" "not replaced" +[ "$(cat "$WORKDIR/interactive-eof/dst")" = "dst" ] || fail "interactive eof replaced target" + +mkdir "$WORKDIR/replace-dir" +mkdir "$WORKDIR/replace-dir/source-dir" +mkdir "$WORKDIR/replace-dir/empty-dir" +"$LN_BIN" -sF "$WORKDIR/replace-dir/source-dir" "$WORKDIR/replace-dir/empty-dir" +assert_symlink_target "$WORKDIR/replace-dir/source-dir" "$WORKDIR/replace-dir/empty-dir" + +mkdir "$WORKDIR/nonempty" +mkdir "$WORKDIR/nonempty/source-dir" +mkdir "$WORKDIR/nonempty/dir" +printf 'keep\n' >"$WORKDIR/nonempty/dir/file" +run_capture "$LN_BIN" -sF "$WORKDIR/nonempty/source-dir" "$WORKDIR/nonempty/dir" +assert_status "non-empty dir replace status" 1 "$LAST_STATUS" +assert_empty "non-empty dir replace stdout" "$LAST_STDOUT" +assert_contains "non-empty dir replace stderr" "$LAST_STDERR" \ + "$WORKDIR/nonempty/dir" +[ -d "$WORKDIR/nonempty/dir" ] || fail "non-empty directory was removed" + +mkdir "$WORKDIR/no-follow-target" +mkdir "$WORKDIR/no-follow-target/actual-dir" +ln -s actual-dir "$WORKDIR/no-follow-target/linkdir" +"$LN_BIN" -snf replacement "$WORKDIR/no-follow-target/linkdir" +assert_symlink_target "replacement" "$WORKDIR/no-follow-target/linkdir" +[ ! -e "$WORKDIR/no-follow-target/actual-dir/replacement" ] || \ + fail "-snf followed symlink target" + +mkdir "$WORKDIR/follow-target" +mkdir "$WORKDIR/follow-target/actual-dir" +ln -s actual-dir "$WORKDIR/follow-target/linkdir" +"$LN_BIN" -s replacement "$WORKDIR/follow-target/linkdir" +assert_symlink_target "replacement" "$WORKDIR/follow-target/actual-dir/replacement" + +mkdir "$WORKDIR/force-append" +mkdir "$WORKDIR/force-append/srcdir" +mkdir "$WORKDIR/force-append/out" +"$LN_BIN" -sF "$WORKDIR/force-append/srcdir" "$WORKDIR/force-append/out/" +assert_symlink_target "$WORKDIR/force-append/srcdir" \ + "$WORKDIR/force-append/out/srcdir" + +mkdir "$WORKDIR/force-dot" +mkdir "$WORKDIR/force-dot/srcdir" +mkdir "$WORKDIR/force-dot/out" +( + cd "$WORKDIR/force-dot/out" + "$LN_BIN" -sF ../srcdir . +) +assert_symlink_target "../srcdir" "$WORKDIR/force-dot/out/srcdir" + +mkdir "$WORKDIR/no-target-follow-error" +mkdir "$WORKDIR/no-target-follow-error/actual-dir" +ln -s actual-dir "$WORKDIR/no-target-follow-error/linkdir" +run_capture "$LN_BIN" -h "$WORKDIR/basic/source" \ + "$WORKDIR/force/dst" "$WORKDIR/no-target-follow-error/linkdir" +assert_status "target symlink with -h status" 1 "$LAST_STATUS" +assert_empty "target symlink with -h stdout" "$LAST_STDOUT" +assert_contains "target symlink with -h stderr" "$LAST_STDERR" \ + "$WORKDIR/no-target-follow-error/linkdir" + +mkdir "$WORKDIR/multi" +printf 'a\n' >"$WORKDIR/multi/a" +printf 'b\n' >"$WORKDIR/multi/b" +mkdir "$WORKDIR/multi/out" +"$LN_BIN" "$WORKDIR/multi/a" "$WORKDIR/multi/b" "$WORKDIR/multi/out" +assert_same_inode "$WORKDIR/multi/a" "$WORKDIR/multi/out/a" +assert_same_inode "$WORKDIR/multi/b" "$WORKDIR/multi/out/b" + +mkdir "$WORKDIR/verbose" +printf 'v\n' >"$WORKDIR/verbose/src" +run_capture "$LN_BIN" -sv "$WORKDIR/verbose/src" "$WORKDIR/verbose/link" +assert_status "verbose symbolic status" 0 "$LAST_STATUS" +assert_contains "verbose symbolic stdout" "$LAST_STDOUT" \ + "$WORKDIR/verbose/link -> $WORKDIR/verbose/src" +assert_empty "verbose symbolic stderr" "$LAST_STDERR" + +mkdir "$WORKDIR/errors" +run_capture "$LN_BIN" "$WORKDIR/errors/missing" "$WORKDIR/errors/dst" +assert_status "missing hard source status" 1 "$LAST_STATUS" +assert_empty "missing hard source stdout" "$LAST_STDOUT" +assert_contains "missing hard source stderr" "$LAST_STDERR" \ + "$WORKDIR/errors/missing" + +mkdir "$WORKDIR/errors/dir-source" +run_capture "$LN_BIN" "$WORKDIR/errors/dir-source" "$WORKDIR/errors/dir-link" +assert_status "dir source status" 1 "$LAST_STATUS" +assert_empty "dir source stdout" "$LAST_STDOUT" +assert_contains "dir source stderr" "$LAST_STDERR" "Is a directory" + +mkdir "$WORKDIR/same-entry" +printf 'same\n' >"$WORKDIR/same-entry/file" +run_capture "$LN_BIN" "$WORKDIR/same-entry/file" "$WORKDIR/same-entry/./file" +assert_status "same entry status" 1 "$LAST_STATUS" +assert_empty "same entry stdout" "$LAST_STDOUT" +assert_contains "same entry stderr" "$LAST_STDERR" "same directory entry" + +mkdir "$WORKDIR/link-mode" +printf 'link\n' >"$WORKDIR/link-mode/source" +"$LINK_BIN" "$WORKDIR/link-mode/source" "$WORKDIR/link-mode/target" +assert_same_inode "$WORKDIR/link-mode/source" "$WORKDIR/link-mode/target" + +run_capture "$LINK_BIN" -s "$WORKDIR/link-mode/source" "$WORKDIR/link-mode/other" +assert_status "link mode rejects options" 2 "$LAST_STATUS" +assert_empty "link mode rejects options stdout" "$LAST_STDOUT" +assert_eq "link mode rejects options stderr" "$LINK_USAGE" "$LAST_STDERR" + +printf '%s\n' "PASS" diff --git a/corebinutils/ln/tree.txt b/corebinutils/ln/tree.txt new file mode 100644 index 0000000000..f44f464252 --- /dev/null +++ b/corebinutils/ln/tree.txt @@ -0,0 +1,901 @@ +diff --git a/ln.c b/ln.c +index 3055c75..63e1d27 100644 +--- a/ln.c ++++ b/ln.c +@@ -3,6 +3,8 @@ + * + * Copyright (c) 1987, 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 +@@ -29,350 +31,600 @@ + * SUCH DAMAGE. + */ + +-#include <sys/param.h> +-#include <sys/stat.h> ++#define _POSIX_C_SOURCE 200809L + +-#include <err.h> + #include <errno.h> + #include <fcntl.h> +-#include <libgen.h> +-#include <limits.h> ++#include <stdarg.h> + #include <stdbool.h> + #include <stdio.h> + #include <stdlib.h> + #include <string.h> ++#include <sys/stat.h> + #include <unistd.h> + +-static bool fflag; /* Unlink existing files. */ +-static bool Fflag; /* Remove empty directories also. */ +-static bool hflag; /* Check new name for symlink first. */ +-static bool iflag; /* Interactive mode. */ +-static bool Pflag; /* Create hard links to symlinks. */ +-static bool sflag; /* Symbolic, not hard, link. */ +-static bool vflag; /* Verbose output. */ +-static bool wflag; /* Warn if symlink target does not +- * exist, and -f is not enabled. */ +-static char linkch; +- +-static int linkit(const char *, const char *, bool); +-static void link_usage(void) __dead2; +-static void usage(void) __dead2; ++struct ln_options { ++ bool force; ++ bool remove_dir; ++ bool no_target_follow; ++ bool interactive; ++ bool follow_source_symlink; ++ bool symbolic; ++ bool verbose; ++ bool warn_missing; ++ char linkch; ++}; ++ ++static const char *progname; ++ ++static void error_errno(const char *fmt, ...); ++static void error_msg(const char *fmt, ...); ++static char *join_path(const char *dir, const char *name); ++static int link_usage(void); ++static int ln_usage(void); ++static int linkit(const struct ln_options *options, const char *source, ++ const char *target, bool target_is_dir); ++static const char *path_basename_start(const char *path); ++static size_t path_basename_len(const char *path); ++static char *path_basename_dup(const char *path); ++static char *path_dirname_dup(const char *path); ++static const char *program_name(const char *argv0); ++static int prompt_replace(const char *target); ++static int remove_existing_target(const struct ln_options *options, ++ const char *target, const struct stat *target_sb); ++static int samedirent(const char *path1, const char *path2); ++static bool should_append_basename(const struct ln_options *options, ++ const char *target, bool target_is_dir); ++static int stat_parent_dir(const char *path, struct stat *sb); ++static void warn_missing_symlink_source(const char *source, ++ const char *target); ++ ++static const char * ++program_name(const char *argv0) ++{ ++ const char *name; ++ ++ if (argv0 == NULL || argv0[0] == '\0') ++ return ("ln"); ++ 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(1); ++ } ++ return (ptr); ++} ++ ++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 char * ++path_basename_dup(const char *path) ++{ ++ const char *start; ++ size_t len; ++ char *name; ++ ++ start = path_basename_start(path); ++ len = path_basename_len(path); ++ name = xmalloc(len + 1); ++ memcpy(name, start, len); ++ name[len] = '\0'; ++ return (name); ++} ++ ++static const char * ++path_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 ++path_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 = path_basename_start(path); ++ return ((size_t)(end - start)); ++} ++ ++static char * ++path_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 ++samedirent(const char *path1, const char *path2) ++{ ++ const char *base1; ++ const char *base2; ++ struct stat sb1; ++ struct stat sb2; ++ size_t base1_len; ++ size_t base2_len; ++ ++ if (strcmp(path1, path2) == 0) ++ return (1); ++ ++ base1 = path_basename_start(path1); ++ base2 = path_basename_start(path2); ++ base1_len = path_basename_len(path1); ++ base2_len = path_basename_len(path2); ++ if (base1_len != base2_len || memcmp(base1, base2, base1_len) != 0) ++ return (0); ++ ++ if (stat_parent_dir(path1, &sb1) != 0 || stat_parent_dir(path2, &sb2) != 0) ++ return (0); ++ ++ return (sb1.st_dev == sb2.st_dev && sb1.st_ino == sb2.st_ino); ++} ++ ++static bool ++should_append_basename(const struct ln_options *options, const char *target, ++ bool target_is_dir) ++{ ++ struct stat sb; ++ const char *base; ++ ++ base = strrchr(target, '/'); ++ base = base == NULL ? target : base + 1; ++ if (base[0] == '\0' || (base[0] == '.' && base[1] == '\0')) ++ return (true); ++ ++ if (options->remove_dir) ++ return (false); ++ if (target_is_dir) ++ return (true); ++ if (lstat(target, &sb) == 0 && S_ISDIR(sb.st_mode)) ++ return (true); ++ if (options->no_target_follow) ++ return (false); ++ if (stat(target, &sb) == 0 && S_ISDIR(sb.st_mode)) ++ return (true); ++ return (false); ++} ++ ++static int ++stat_parent_dir(const char *path, struct stat *sb) ++{ ++ char *dir; ++ size_t dir_len; ++ const char *base; ++ ++ base = path_basename_start(path); ++ if (base == path) ++ return (stat(".", sb)); ++ ++ dir_len = (size_t)(base - path); ++ while (dir_len > 1 && path[dir_len - 1] == '/') ++ dir_len--; ++ if (dir_len == 1 && path[0] == '/') ++ return (stat("/", sb)); ++ ++ dir = xmalloc(dir_len + 1); ++ memcpy(dir, path, dir_len); ++ dir[dir_len] = '\0'; ++ if (stat(dir, sb) != 0) { ++ free(dir); ++ return (-1); ++ } ++ free(dir); ++ return (0); ++} ++ ++static int ++remove_existing_target(const struct ln_options *options, const char *target, ++ const struct stat *target_sb) ++{ ++ if (options->remove_dir && S_ISDIR(target_sb->st_mode)) { ++ if (rmdir(target) != 0) { ++ error_errno("%s", target); ++ return (1); ++ } ++ return (0); ++ } ++ ++ if (unlink(target) != 0) { ++ error_errno("%s", target); ++ return (1); ++ } ++ return (0); ++} ++ ++static void ++warn_missing_symlink_source(const char *source, const char *target) ++{ ++ char *dir; ++ char *resolved; ++ struct stat st; ++ ++ if (source[0] == '/') { ++ if (stat(source, &st) != 0) ++ error_errno("warning: %s", source); ++ return; ++ } ++ ++ dir = path_dirname_dup(target); ++ resolved = join_path(dir, source); ++ if (stat(resolved, &st) != 0) ++ error_errno("warning: %s", source); ++ free(dir); ++ free(resolved); ++} ++ ++static int ++prompt_replace(const char *target) ++{ ++ char answer[16]; ++ bool stdin_is_tty; ++ int ch; ++ ++ stdin_is_tty = isatty(STDIN_FILENO) == 1; ++ (void)stdin_is_tty; ++ ++ (void)fflush(stdout); ++ (void)fprintf(stderr, "replace %s? ", target); ++ if (fgets(answer, sizeof(answer), stdin) == NULL) { ++ if (ferror(stdin)) { ++ error_errno("stdin"); ++ return (-1); ++ } ++ if (!stdin_is_tty && feof(stdin)) { ++ (void)fprintf(stderr, "not replaced\n"); ++ return (1); ++ } ++ (void)fprintf(stderr, "not replaced\n"); ++ return (1); ++ } ++ ++ if (strchr(answer, '\n') == NULL) { ++ while ((ch = getchar()) != '\n' && ch != EOF) ++ continue; ++ if (ferror(stdin)) { ++ error_errno("stdin"); ++ return (-1); ++ } ++ } ++ ++ if (answer[0] != 'y' && answer[0] != 'Y') { ++ (void)fprintf(stderr, "not replaced\n"); ++ return (1); ++ } ++ return (0); ++} ++ ++static int ++linkit(const struct ln_options *options, const char *source, const char *target, ++ bool target_is_dir) ++{ ++ struct stat source_sb; ++ struct stat target_sb; ++ char *resolved_target; ++ bool exists; ++ int flags; ++ int ret; ++ ++ resolved_target = NULL; ++ ret = 1; ++ if (!options->symbolic) { ++ if ((options->follow_source_symlink ? stat : lstat)(source, ++ &source_sb) != 0) { ++ error_errno("%s", source); ++ goto cleanup; ++ } ++ if (S_ISDIR(source_sb.st_mode)) { ++ errno = EISDIR; ++ error_errno("%s", source); ++ goto cleanup; ++ } ++ } ++ ++ if (should_append_basename(options, target, target_is_dir)) { ++ char *base; ++ ++ base = path_basename_dup(source); ++ resolved_target = join_path(target, base); ++ free(base); ++ } else { ++ resolved_target = xstrdup(target); ++ } ++ ++ if (options->symbolic && options->warn_missing) ++ warn_missing_symlink_source(source, resolved_target); ++ ++ exists = lstat(resolved_target, &target_sb) == 0; ++ if (exists && !options->symbolic && samedirent(source, resolved_target)) { ++ error_msg("%s and %s are the same directory entry", source, ++ resolved_target); ++ goto cleanup; ++ } ++ ++ if (exists && options->force) { ++ if (remove_existing_target(options, resolved_target, &target_sb) != 0) ++ goto cleanup; ++ } else if (exists && options->interactive) { ++ ret = prompt_replace(resolved_target); ++ if (ret != 0) ++ goto cleanup; ++ if (remove_existing_target(options, resolved_target, &target_sb) != 0) ++ goto cleanup; ++ } ++ ++ flags = options->follow_source_symlink ? AT_SYMLINK_FOLLOW : 0; ++ if (options->symbolic) { ++ if (symlink(source, resolved_target) != 0) { ++ error_errno("%s", resolved_target); ++ goto cleanup; ++ } ++ } else if (linkat(AT_FDCWD, source, AT_FDCWD, resolved_target, flags) != 0) { ++ error_errno("%s", resolved_target); ++ goto cleanup; ++ } ++ ++ if (options->verbose) ++ (void)printf("%s %c> %s\n", resolved_target, options->linkch, source); ++ ++ ret = 0; ++cleanup: ++ free(resolved_target); ++ return (ret); ++} ++ ++static int ++link_usage(void) ++{ ++ (void)fprintf(stderr, "usage: link source_file target_file\n"); ++ return (2); ++} ++ ++static int ++ln_usage(void) ++{ ++ (void)fprintf(stderr, ++ "usage: ln [-s [-F] | -L | -P] [-f | -i] [-hnvw] " ++ "source_file [target_file]\n"); ++ (void)fprintf(stderr, ++ " ln [-s [-F] | -L | -P] [-f | -i] [-hnvw] " ++ "source_file ... target_dir\n"); ++ return (2); ++} + + int + main(int argc, char *argv[]) + { ++ struct ln_options options; + struct stat sb; + char *targetdir; +- int ch, exitval; +- +- /* +- * Test for the special case where the utility is called as +- * "link", for which the functionality provided is greatly +- * simplified. +- */ +- if (strcmp(getprogname(), "link") == 0) { +- while (getopt(argc, argv, "") != -1) +- link_usage(); ++ int ch; ++ int exitval; ++ ++ progname = program_name(argv[0]); ++ memset(&options, 0, sizeof(options)); ++ /* FreeBSD hard-link semantics default to -L, unlike GNU ln's -P. */ ++ options.follow_source_symlink = true; ++ ++ if (strcmp(progname, "link") == 0) { ++ opterr = 0; ++ while ((ch = getopt(argc, argv, "")) != -1) ++ return (link_usage()); + argc -= optind; + argv += optind; + if (argc != 2) +- link_usage(); +- if (lstat(argv[1], &sb) == 0) +- errc(1, EEXIST, "%s", argv[1]); +- /* +- * We could simply call link(2) here, but linkit() +- * performs additional checks and gives better +- * diagnostics. +- */ +- exit(linkit(argv[0], argv[1], false)); ++ return (link_usage()); ++ if (lstat(argv[1], &sb) == 0) { ++ errno = EEXIST; ++ error_errno("%s", argv[1]); ++ return (1); ++ } ++ return (linkit(&options, argv[0], argv[1], false)); + } + +- while ((ch = getopt(argc, argv, "FLPfhinsvw")) != -1) ++ opterr = 0; ++ while ((ch = getopt(argc, argv, "FLPfhinsvw")) != -1) { + switch (ch) { + case 'F': +- Fflag = true; ++ options.remove_dir = true; + break; + case 'L': +- Pflag = false; ++ options.follow_source_symlink = true; + break; + case 'P': +- Pflag = true; ++ options.follow_source_symlink = false; + break; + case 'f': +- fflag = true; +- iflag = false; +- wflag = false; ++ options.force = true; ++ options.interactive = false; ++ options.warn_missing = false; + break; + case 'h': + case 'n': +- hflag = true; ++ options.no_target_follow = true; + break; + case 'i': +- iflag = true; +- fflag = false; ++ options.interactive = true; ++ options.force = false; + break; + case 's': +- sflag = true; ++ options.symbolic = true; + break; + case 'v': +- vflag = true; ++ options.verbose = true; + break; + case 'w': +- wflag = true; ++ options.warn_missing = true; + break; + case '?': + default: +- usage(); ++ if (optopt != 0) ++ error_msg("unknown option -- %c", optopt); ++ return (ln_usage()); + } ++ } + + argv += optind; + argc -= optind; + +- linkch = sflag ? '-' : '='; +- if (!sflag) +- Fflag = false; +- if (Fflag && !iflag) { +- fflag = true; +- wflag = false; /* Implied when fflag is true */ ++ options.linkch = options.symbolic ? '-' : '='; ++ if (!options.symbolic) ++ options.remove_dir = false; ++ if (options.remove_dir && !options.interactive) { ++ options.force = true; ++ options.warn_missing = false; + } + + switch (argc) { + case 0: +- usage(); +- /* NOTREACHED */ +- case 1: /* ln source */ +- exit(linkit(argv[0], ".", true)); +- case 2: /* ln source target */ +- exit(linkit(argv[0], argv[1], false)); ++ return (ln_usage()); ++ case 1: ++ return (linkit(&options, argv[0], ".", true)); ++ case 2: ++ return (linkit(&options, argv[0], argv[1], false)); + default: +- ; ++ break; + } +- /* ln source1 source2 directory */ ++ + targetdir = argv[argc - 1]; +- if (hflag && lstat(targetdir, &sb) == 0 && S_ISLNK(sb.st_mode)) { +- /* +- * We were asked not to follow symlinks, but found one at +- * the target--simulate "not a directory" error +- */ ++ if (options.no_target_follow && lstat(targetdir, &sb) == 0 && ++ S_ISLNK(sb.st_mode)) { + errno = ENOTDIR; +- err(1, "%s", targetdir); +- } +- if (stat(targetdir, &sb)) +- err(1, "%s", targetdir); +- if (!S_ISDIR(sb.st_mode)) +- usage(); +- for (exitval = 0; *argv != targetdir; ++argv) +- exitval |= linkit(*argv, targetdir, true); +- exit(exitval); +-} +- +-/* +- * Two pathnames refer to the same directory entry if the directories match +- * and the final components' names match. +- */ +-static int +-samedirent(const char *path1, const char *path2) +-{ +- const char *file1, *file2; +- char pathbuf[PATH_MAX]; +- struct stat sb1, sb2; +- +- if (strcmp(path1, path2) == 0) +- return 1; +- file1 = strrchr(path1, '/'); +- if (file1 != NULL) +- file1++; +- else +- file1 = path1; +- file2 = strrchr(path2, '/'); +- if (file2 != NULL) +- file2++; +- else +- file2 = path2; +- if (strcmp(file1, file2) != 0) +- return 0; +- if (file1 - path1 >= PATH_MAX || file2 - path2 >= PATH_MAX) +- return 0; +- if (file1 == path1) +- memcpy(pathbuf, ".", 2); +- else { +- memcpy(pathbuf, path1, file1 - path1); +- pathbuf[file1 - path1] = '\0'; +- } +- if (stat(pathbuf, &sb1) != 0) +- return 0; +- if (file2 == path2) +- memcpy(pathbuf, ".", 2); +- else { +- memcpy(pathbuf, path2, file2 - path2); +- pathbuf[file2 - path2] = '\0'; +- } +- if (stat(pathbuf, &sb2) != 0) +- return 0; +- return sb1.st_dev == sb2.st_dev && sb1.st_ino == sb2.st_ino; +-} +- +-/* +- * Create a link to source. If target is a directory (and some additional +- * conditions apply, see comments within) the link will be created within +- * target and have the basename of source. Otherwise, the link will be +- * named target. If isdir is true, target has already been determined to +- * be a directory; otherwise, we will check, if needed. +- */ +-static int +-linkit(const char *source, const char *target, bool isdir) +-{ +- char path[PATH_MAX]; +- char wbuf[PATH_MAX]; +- char bbuf[PATH_MAX]; +- struct stat sb; +- const char *p; +- int ch, first; +- bool append, exists; +- +- if (!sflag) { +- /* If source doesn't exist, quit now. */ +- if ((Pflag ? lstat : stat)(source, &sb)) { +- warn("%s", source); +- return (1); +- } +- /* Only symbolic links to directories. */ +- if (S_ISDIR(sb.st_mode)) { +- errno = EISDIR; +- warn("%s", source); +- return (1); +- } +- } +- +- /* +- * Append a slash and the source's basename if: +- * - the target is "." or ends in "/" or "/.", or +- * - the target is a directory (and not a symlink if hflag) and +- * Fflag is not set +- */ +- if ((p = strrchr(target, '/')) == NULL) +- p = target; +- else +- p++; +- append = false; +- if (p[0] == '\0' || (p[0] == '.' && p[1] == '\0')) { +- append = true; +- } else if (!Fflag) { +- if (isdir || (lstat(target, &sb) == 0 && S_ISDIR(sb.st_mode)) || +- (!hflag && stat(target, &sb) == 0 && S_ISDIR(sb.st_mode))) { +- append = true; +- } +- } +- if (append) { +- if (strlcpy(bbuf, source, sizeof(bbuf)) >= sizeof(bbuf) || +- (p = basename(bbuf)) == NULL /* can't happen */ || +- snprintf(path, sizeof(path), "%s/%s", target, p) >= +- (ssize_t)sizeof(path)) { +- errno = ENAMETOOLONG; +- warn("%s", source); +- return (1); +- } +- target = path; +- } +- +- /* +- * If the link source doesn't exist, and a symbolic link was +- * requested, and -w was specified, give a warning. +- */ +- if (sflag && wflag) { +- if (*source == '/') { +- /* Absolute link source. */ +- if (stat(source, &sb) != 0) +- warn("warning: %s inaccessible", source); +- } else { +- /* +- * Relative symlink source. Try to construct the +- * absolute path of the source, by appending `source' +- * to the parent directory of the target. +- */ +- strlcpy(bbuf, target, sizeof(bbuf)); +- p = dirname(bbuf); +- if (p != NULL) { +- (void)snprintf(wbuf, sizeof(wbuf), "%s/%s", +- p, source); +- if (stat(wbuf, &sb) != 0) +- warn("warning: %s", source); +- } +- } +- } +- +- /* +- * If the file exists, first check it is not the same directory entry. +- */ +- exists = lstat(target, &sb) == 0; +- if (exists) { +- if (!sflag && samedirent(source, target)) { +- warnx("%s and %s are the same directory entry", +- source, target); +- return (1); +- } +- } +- /* +- * Then unlink it forcibly if -f was specified +- * and interactively if -i was specified. +- */ +- if (fflag && exists) { +- if (Fflag && S_ISDIR(sb.st_mode)) { +- if (rmdir(target)) { +- warn("%s", target); +- return (1); +- } +- } else if (unlink(target)) { +- warn("%s", target); +- return (1); +- } +- } else if (iflag && exists) { +- fflush(stdout); +- fprintf(stderr, "replace %s? ", target); +- +- first = ch = getchar(); +- while(ch != '\n' && ch != EOF) +- ch = getchar(); +- if (first != 'y' && first != 'Y') { +- fprintf(stderr, "not replaced\n"); +- return (1); +- } +- +- if (Fflag && S_ISDIR(sb.st_mode)) { +- if (rmdir(target)) { +- warn("%s", target); +- return (1); +- } +- } else if (unlink(target)) { +- warn("%s", target); +- return (1); +- } ++ error_errno("%s", targetdir); ++ return (1); + } +- +- /* Attempt the link. */ +- if (sflag ? symlink(source, target) : +- linkat(AT_FDCWD, source, AT_FDCWD, target, +- Pflag ? 0 : AT_SYMLINK_FOLLOW)) { +- warn("%s", target); ++ if (stat(targetdir, &sb) != 0) { ++ error_errno("%s", targetdir); + return (1); + } +- if (vflag) +- (void)printf("%s %c> %s\n", target, linkch, source); +- return (0); +-} +- +-static void +-link_usage(void) +-{ +- (void)fprintf(stderr, "usage: link source_file target_file\n"); +- exit(1); +-} ++ if (!S_ISDIR(sb.st_mode)) ++ return (ln_usage()); + +-static void +-usage(void) +-{ +- (void)fprintf(stderr, "%s\n%s\n", +- "usage: ln [-s [-F] | -L | -P] [-f | -i] [-hnv] source_file [target_file]", +- " ln [-s [-F] | -L | -P] [-f | -i] [-hnv] source_file ... target_dir"); +- exit(1); ++ exitval = 0; ++ for (int i = 0; i < argc - 1; i++) ++ exitval |= linkit(&options, argv[i], targetdir, true); ++ return (exitval); + } |
