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 -#include +#define _POSIX_C_SOURCE 200809L -#include #include #include -#include -#include +#include #include #include #include #include +#include #include -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); }