logo

utils-std

Collection of commonly available Unix tools
commit: 3c38e0ef8a875dfb0449c77be9a05f6ff464cb11
parent a34efc85f4fd027385e0a1842c00883f116a0b6e
Author: Haelwenn (lanodan) Monnier <contact@hacktivis.me>
Date:   Sun,  5 May 2024 00:38:03 +0200

cmd/ln: Add support for -n option

Which required a restructuring to follow a TOCTOU-appropriate approach.

Diffstat:

Mcmd/ln.111+++++++++--
Mcmd/ln.c88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Mtest-cmd/ln.t12++++++++++++
3 files changed, 87 insertions(+), 24 deletions(-)

diff --git a/cmd/ln.1 b/cmd/ln.1 @@ -9,13 +9,13 @@ .Nd create hard links and symbolic links .Sh SYNOPSIS .Nm -.Op Fl f +.Op Fl fn .Op Fl L Ns | Ns Fl P .Ar source... .Ar target .Nm .Fl s -.Op Fl f +.Op Fl fn .Ar reference... .Ar target .Sh DESCRIPTION @@ -49,6 +49,10 @@ Forcefully create links by removing existing entries. If .Ar source is a symbolic link, dereference it. +.It Fl n +Prevent descending into +.Ar target +as a directory if it is a symbolic link. .It Fl P If .Ar source @@ -64,5 +68,8 @@ Create symbolic links instead of hard links. should be compliant with the .St -p1003.1-2008 specification. +The +.Fl n +option is an extension. .Sh AUTHORS .An Haelwenn (lanodan) Monnier Aq Mt contact+utils@hacktivis.me diff --git a/cmd/ln.c b/cmd/ln.c @@ -22,58 +22,99 @@ #include <unistd.h> // getopt, symlink, link static bool opt_s = false, force = false; -static int link_flags = 0; +static int link_flags = 0; +static int open_dir_flags = O_RDONLY | O_DIRECTORY; static int do_link(char *src, char *dest) { assert(errno == 0); - struct stat dest_stat; - if(lstat(dest, &dest_stat) < 0) + if(opt_s) { - if(errno != ENOENT) + if(symlink(src, dest) == 0) return 0; + + if(errno != EEXIST) { - fprintf(stderr, "ln: Failed getting status for target '%s': %s\n", dest, strerror(errno)); + fprintf(stderr, "ln: Failed creating symlink '%s': %s\n", dest, strerror(errno)); return -1; } - errno = 0; } - else if(force) + else { - if(unlink(dest) < 0) + if(linkat(AT_FDCWD, src, AT_FDCWD, dest, link_flags) == 0) return 0; + + if(errno != EEXIST) { - fprintf(stderr, "ln: Unlinking '%s' before replacing failed: %s\n", dest, strerror(errno)); + fprintf(stderr, + "ln: Failed creating hard link from '%s' to '%s': %s\n", + src, + dest, + strerror(errno)); return -1; } } - else + + // Fallback + assert(errno == EEXIST); + errno = 0; + + int dirfd = open(dest, open_dir_flags); + if(dirfd < 0 && errno != ENOTDIR) { - fprintf(stderr, "ln: Error: Destination '%s' already exists\n", dest); + fprintf( + stderr, "ln: Failed opening destination as directory '%s': %s\n", dest, strerror(errno)); return -1; } - if(opt_s) + if(errno == ENOTDIR) { - if(symlink(src, dest) < 0) + if(!force) { - fprintf(stderr, "ln: Failed creating symlink '%s': %s\n", dest, strerror(errno)); + fprintf(stderr, "ln: Error: Destination '%s' already exists\n", dest); return -1; } - } - else - { - if(linkat(AT_FDCWD, src, AT_FDCWD, dest, link_flags) < 0) + + errno = 0; + + dirfd = AT_FDCWD; + + if(unlink(dest) < 0) { fprintf(stderr, - "ln: Failed creating hard link from '%s' to '%s': %s\n", - src, + "ln: Failed removing already existing destination '%s': %s\n", dest, strerror(errno)); return -1; } } - assert(errno == 0); + if(opt_s) + { + if(symlinkat(src, dirfd, dest) == 0) goto cleanup; + + fprintf(stderr, "ln: Failed creating symlink '%s': %s\n", dest, strerror(errno)); + return -1; + } + else + { + if(linkat(AT_FDCWD, src, dirfd, dest, link_flags) == 0) goto cleanup; + + fprintf(stderr, + "ln: Failed creating hard link from '%s' to '%s': %s\n", + src, + dest, + strerror(errno)); + return -1; + } + +cleanup: + if(dirfd == AT_FDCWD) return 0; + + if(close(dirfd) != 0) + { + fprintf(stderr, "ln: Failed closing directory '%s': %s\n", dest, strerror(errno)); + return -1; + } return 0; } @@ -91,13 +132,16 @@ int main(int argc, char *argv[]) { int c = -1; - while((c = getopt(argc, argv, ":fsLP")) != -1) + while((c = getopt(argc, argv, ":fnsLP")) != -1) { switch(c) { case 'f': force = true; break; + case 'n': + FIELD_SET(open_dir_flags, O_NOFOLLOW); + break; case 's': opt_s = true; break; diff --git a/test-cmd/ln.t b/test-cmd/ln.t @@ -62,5 +62,17 @@ $ test -L force_symlink $ rm force_symlink + $ mkdir n_directory + $ ln -s n_directory n_dir_symlink + $ ln -sn //example.org n_dir_symlink + ln: Error: Destination 'n_dir_symlink' already exists + [1] + $ readlink n_dir_symlink + n_directory + $ ln -snf //example.org n_dir_symlink + $ readlink n_dir_symlink + //example.org + $ rm -r n_directory n_dir_symlink + $ find . .