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:
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 .
.