commit: bb298210abf592c18230160143f35d70c18958b8
parent a2a8cecb071ee55df5c328938cbbfd92915bc356
Author: Haelwenn (lanodan) Monnier <contact@hacktivis.me>
Date: Sat, 27 Apr 2024 10:28:56 +0200
cmd/chown: new
Diffstat:
M | Makefile | 1 | + |
A | cmd/chown.1 | 86 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | cmd/chown.c | 349 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
M | makeless.sh | 1 | + |
A | test-cmd/chown.t | 62 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
5 files changed, 499 insertions(+), 0 deletions(-)
diff --git a/Makefile b/Makefile
@@ -47,6 +47,7 @@ install: all
mkdir -p ${DESTDIR}${BINDIR}/
cp -p ${EXE} ${SCRIPTS} ${DESTDIR}${BINDIR}/
ln -s test ${DESTDIR}${BINDIR}/'['
+ ln -s chown ${DESTDIR}${BINDIR}/chgrp
mkdir -p ${DESTDIR}${MANDIR}/man1
cp -p ${MAN1} ${DESTDIR}${MANDIR}/man1
diff --git a/cmd/chown.1 b/cmd/chown.1
@@ -0,0 +1,86 @@
+.\" utils-std: Collection of commonly available Unix tools
+.\" Copyright 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me>
+.\" SPDX-License-Identifier: MPL-2.0
+.Dd 2024-04-27
+.Dt CHOWN 1
+.Os
+.Sh NAME
+.Nm chown , chgrp
+.Nd Change files ownership
+.Sh SYNOPSIS
+.Nm chown
+.Op Fl v
+.Op Fl h | Fl R Op Fl HLP
+.Ar owner Ns Op : Ns Ar group
+.Ar file ...
+.Nm chgrp
+.Op Fl v
+.Op Fl h | Fl R Op Fl HLP
+.Ar group
+.Ar file ...
+.Sh DESCRIPTION
+.Nm chown
+sets the user ID given by
+.Ar owner ,
+and when given by
+.Ar group ,
+the group ID on each given
+.Ar file .
+.Pp
+.Nm chgrp
+sets the group ID given by
+.Ar group
+on each given
+.Ar file .
+.Pp
+The
+.Ar owner
+and
+.Ar group
+arguments can be either numeric IDs, or names.
+Ownership refers to both user and group.
+.Sh OPTIONS
+.Bl -tag -width Ds
+.It Fl h
+If
+.Ar file
+is a symbolic link, set it's ownership.
+.It Fl H
+If
+.Fl R
+is also specified,
+and
+.Ar file
+refers to a symbolic link itself refering to a directory,
+change ownership of the directory and it's childs.
+.It Fl L
+If
+.Fl R
+is also specified,
+and symbolic links refering to directories are found,
+change ownership of said directories and their childs.
+.It Fl P
+If
+.Fl R
+is also specified,
+change ownership of symbolic links without dereferencing.
+.It Fl R
+Recurse into directories.
+.It Fl v
+Verbose, print a message about each processed file, whether a change was made or not.
+.El
+.Sh EXIT STATUS
+.Ex -std
+.Sh SEE ALSO
+.Xr stat 1
+.Sh STANDARDS
+.Nm
+should be compliant with the
+.St -p1003.1-2008
+specification.
+.Pp
+The
+.Fl v
+option is an extension, also present in GNU coreutils.
+.Sh AUTHORS
+.An Haelwenn (lanodan) Monnier Aq Mt contact+utils@hacktivis.me
diff --git a/cmd/chown.c b/cmd/chown.c
@@ -0,0 +1,349 @@
+// utils-std: Collection of commonly available Unix tools
+// SPDX-FileCopyrightText: 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me>
+// SPDX-License-Identifier: MPL-2.0
+
+#define _POSIX_C_SOURCE 200809L
+
+// NetBSD <10 hides fdopendir behind _NETBSD_SOURCE
+#if __NetBSD_Version__ < 1000000000
+#define _NETBSD_SOURCE
+#endif
+
+#include <assert.h>
+#include <dirent.h> // fdopendir, readdir, closedir
+#include <errno.h>
+#include <fcntl.h> // AT_FDCWD, fchownat
+#include <grp.h> // getgrnam
+#include <limits.h> // PATH_MAX
+#include <pwd.h> // getpwnam
+#include <stdbool.h>
+#include <stdio.h> // fprintf
+#include <stdlib.h> // abort, exit
+#include <string.h> // strerror, strcmp, strrchr
+#include <sys/stat.h> // fstatat, S_ISDIR
+#include <unistd.h> // getopt
+
+static char *argv0 = NULL;
+
+static uid_t user = (uid_t)-1;
+static uid_t group = (uid_t)-1;
+
+static bool opt_v = false, opt_R = false;
+
+enum chown_follow_symlinks
+{
+ CHOWN_FOLLOW_UNK_SYMLINKS,
+ CHOWN_FOLLOW_ALL_SYMLINKS,
+ CHOWN_FOLLOW_NO_SYMLINK,
+ CHOWN_FOLLOW_ONE_SYMLINK,
+};
+
+static int
+parse_user(char *str)
+{
+ if(str == NULL) return -1;
+ if(str[0] == 0) return 0;
+
+ assert(errno == 0);
+ char *endptr = NULL;
+ unsigned int id = strtoul(str, &endptr, 0);
+ if(errno == 0)
+ {
+ user = id;
+ return 0;
+ }
+
+ errno = 0;
+
+ struct passwd *pw = getpwnam(str);
+ if(pw == NULL)
+ {
+ char *e = strerror(errno);
+ if(errno == 0) e = "Entry Not Found";
+
+ fprintf(stderr, "%s: Error: Failed to get entry for username '%s': %s\n", argv0, str, e);
+
+ return -1;
+ }
+
+ user = pw->pw_uid;
+
+ return 0;
+}
+
+static int
+parse_group(char *str)
+{
+ if(str == NULL) return -1;
+ if(str[0] == 0) return 0;
+
+ assert(errno == 0);
+ char *endptr = NULL;
+ unsigned int id = strtoul(str, &endptr, 0);
+ if(errno == 0)
+ {
+ group = id;
+ return 0;
+ }
+
+ errno = 0;
+
+ struct group *gr = getgrnam(str);
+ if(gr == NULL)
+ {
+ char *e = strerror(errno);
+ if(errno == 0) e = "Entry Not Found";
+
+ fprintf(stderr, "%s: Error: Failed to get entry for group '%s': %s\n", argv0, str, e);
+
+ return -1;
+ }
+
+ group = gr->gr_gid;
+
+ return 0;
+}
+
+static char *
+static_basename(char *path)
+{
+ char *sep = strrchr(path, '/');
+
+ return (sep == NULL) ? path : sep + 1;
+}
+
+static int
+do_fchownat(int fd, char *name, char *acc_path, enum chown_follow_symlinks follow_symlinks)
+{
+ struct stat stats;
+ int err = 0;
+
+ int stat_opts = (follow_symlinks == CHOWN_FOLLOW_NO_SYMLINK) ? AT_SYMLINK_NOFOLLOW : 0;
+ int chown_opts = (follow_symlinks == CHOWN_FOLLOW_NO_SYMLINK) ? AT_SYMLINK_NOFOLLOW : 0;
+
+ assert(errno == 0);
+ if(fstatat(fd, name, &stats, stat_opts) != 0)
+ {
+ fprintf(stderr, "%s: Failed getting status for '%s': %s\n", argv0, acc_path, strerror(errno));
+ errno = 0;
+ return 1;
+ }
+
+ if(stats.st_uid != user && stats.st_gid != group)
+ {
+ assert(errno == 0);
+ if(fchownat(fd, name, user, group, chown_opts) != 0)
+ {
+ fprintf(stderr,
+ "%s: Failed setting ownership to '%d:%d' for '%s': %s\n",
+ argv0,
+ user,
+ group,
+ acc_path,
+ strerror(errno));
+ errno = 0;
+ return 1;
+ }
+
+ if(opt_v)
+ printf("%s: Ownership changed from %d:%d to %d:%d for '%s'\n",
+ argv0,
+ stats.st_uid,
+ stats.st_gid,
+ user,
+ group,
+ acc_path);
+ }
+ else
+ {
+ if(opt_v)
+ printf("%s: Ownership already set to %d:%d for '%s'\n",
+ argv0,
+ stats.st_uid,
+ stats.st_gid,
+ acc_path);
+ }
+
+ if(opt_R && S_ISDIR(stats.st_mode))
+ {
+ assert(errno == 0);
+ int dir = openat(fd, name, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ if(dir == -1)
+ {
+ fprintf(
+ stderr, "%s: Couldn't open '%s' as directory: %s\n", argv0, acc_path, strerror(errno));
+ errno = 0;
+ return 1;
+ }
+
+ assert(errno == 0);
+ DIR *dirp = fdopendir(dir);
+ if(dirp == NULL)
+ {
+ fprintf(stderr,
+ "%s: Couldn't get DIR entry for opened '%s': %s\n",
+ argv0,
+ acc_path,
+ strerror(errno));
+ errno = 0;
+ return 1;
+ }
+
+ while(true)
+ {
+ assert(errno == 0);
+ struct dirent *dp = readdir(dirp);
+ if(dp == NULL)
+ {
+ if(errno == 0) break;
+
+ fprintf(
+ stderr, "%s: Failed reading directory '%s': %s\n", argv0, acc_path, strerror(errno));
+ closedir(dirp); // FIXME: unhandled error
+ errno = 0;
+ return 1;
+ }
+
+ if(strcmp(dp->d_name, ".") == 0) continue;
+ if(strcmp(dp->d_name, "..") == 0) continue;
+
+ assert(errno == 0);
+ char new_path[PATH_MAX] = "";
+ if(snprintf(new_path, PATH_MAX, "%s/%s", acc_path, dp->d_name) < 0)
+ {
+ fprintf(stderr,
+ "%s: Couldn't concatenate '%s' into parent '%s', skipping to next entry: %s",
+ argv0,
+ name,
+ acc_path,
+ strerror(errno));
+ err++;
+ errno = 0;
+ continue;
+ }
+
+ enum chown_follow_symlinks child_follow_symlinks =
+ (follow_symlinks == CHOWN_FOLLOW_ONE_SYMLINK) ? CHOWN_FOLLOW_NO_SYMLINK : follow_symlinks;
+
+ // No depth counter for now, unlikely to be a problem as symlinks aren't followed
+ int ret = do_fchownat(dir, dp->d_name, new_path, child_follow_symlinks);
+ if(ret != 0) return ret;
+ }
+
+ // fdopendir allocates memory for DIR, needs closedir
+ assert(errno == 0);
+ if(closedir(dirp) != 0)
+ {
+ fprintf(stderr,
+ "%s: Deallocating directory entry for '%s' failed: %s\n",
+ argv0,
+ acc_path,
+ strerror(errno));
+ errno = 0;
+ return 1;
+ }
+ }
+
+ return err;
+}
+
+static void
+usage()
+{
+ if(strcmp(argv0, "chown") == 0)
+ fprintf(stderr, "Usage: %s [-h | -R [-H|-L|-P]] owner[:group] file...\n", argv0);
+ else if(strcmp(argv0, "chgrp") == 0)
+ fprintf(stderr, "Usage: %s [-h | -R [-H|-L|-P]] group file...\n", argv0);
+ else
+ {
+ fprintf(stderr, "%s: Unknown utility '%s' expected either chown or chgrp\n", argv0, argv0);
+ exit(1);
+ }
+}
+
+int
+main(int argc, char *argv[])
+{
+ argv0 = static_basename(argv[0]);
+
+ enum chown_follow_symlinks follow_symlinks = CHOWN_FOLLOW_UNK_SYMLINKS;
+
+ int c = -1;
+ while((c = getopt(argc, argv, ":hRHLPv")) != -1)
+ {
+ switch(c)
+ {
+ case 'h':
+ follow_symlinks = CHOWN_FOLLOW_NO_SYMLINK;
+ break;
+ case 'R':
+ opt_R = true;
+ break;
+ case 'H':
+ follow_symlinks = CHOWN_FOLLOW_ONE_SYMLINK;
+ break;
+ case 'L':
+ follow_symlinks = CHOWN_FOLLOW_ALL_SYMLINKS;
+ break;
+ case 'P':
+ follow_symlinks = CHOWN_FOLLOW_NO_SYMLINK;
+ break;
+ case 'v':
+ opt_v = true;
+ break;
+ case ':':
+ fprintf(stderr, "%s: Error: Missing operand for option: '-%c'\n", argv0, optopt);
+ usage();
+ return 1;
+ case '?':
+ fprintf(stderr, "%s: Error: Unrecognised option: '-%c'\n", argv0, optopt);
+ usage();
+ return 1;
+ default:
+ abort();
+ }
+ }
+
+ argc -= optind;
+ argv += optind;
+
+ if(follow_symlinks == CHOWN_FOLLOW_UNK_SYMLINKS)
+ follow_symlinks = opt_R ? CHOWN_FOLLOW_NO_SYMLINK : CHOWN_FOLLOW_ALL_SYMLINKS;
+
+ if(argc < 2)
+ {
+ fprintf(stderr, "%s: Expects >=2 arguments, %d given\n", argv0, argc);
+ usage();
+ return 1;
+ }
+
+ if(strcmp(argv0, "chown") == 0)
+ {
+ char *gname = strchr(argv[0], ':');
+ if(gname != NULL)
+ {
+ gname[0] = 0;
+ gname++;
+
+ if(parse_group(gname) < 0) return 1;
+ }
+ if(parse_user(argv[0]) < 0) return 1;
+ }
+ else if(strcmp(argv0, "chgrp") == 0)
+ {
+ if(parse_group(argv[0]) < 0) return 1;
+ }
+ else
+ {
+ fprintf(stderr, "%s: Unknown utility '%s' expected either chown or chgrp\n", argv0, argv0);
+ return 1;
+ }
+
+ for(int i = 1; i < argc; i++)
+ {
+ int ret = do_fchownat(AT_FDCWD, argv[i], argv[i], follow_symlinks);
+ if(ret != 0) return ret;
+ }
+
+ return 0;
+}
diff --git a/makeless.sh b/makeless.sh
@@ -15,6 +15,7 @@ $CC -std=c99 $CFLAGS -o cmd/base64 cmd/base64.c $LDFLAGS $LDSTATIC
$CC -std=c99 $CFLAGS -o cmd/basename cmd/basename.c $LDFLAGS $LDSTATIC
$CC -std=c99 $CFLAGS -o cmd/cat cmd/cat.c $LDFLAGS $LDSTATIC
$CC -std=c99 $CFLAGS -o cmd/chmod cmd/chmod.c lib/mode.c lib/symbolize_mode.c $LDFLAGS $LDSTATIC
+$CC -std=c99 $CFLAGS -o cmd/chown cmd/chown.c $LDFLAGS $LDSTATIC
$CC -std=c99 $CFLAGS -o cmd/chroot cmd/chroot.c $LDFLAGS $LDSTATIC
$CC -std=c99 $CFLAGS -o cmd/date cmd/date.c lib/iso_parse.c $LDFLAGS $LDSTATIC
$CC -std=c99 $CFLAGS -o cmd/df cmd/df.c lib/humanize.c $LDFLAGS $LDSTATIC
diff --git a/test-cmd/chown.t b/test-cmd/chown.t
@@ -0,0 +1,62 @@
+#!/usr/bin/env cram
+# SPDX-FileCopyrightText: 2017 Haelwenn (lanodan) Monnier <contact+utils@hacktivis.me>
+# SPDX-License-Identifier: MPL-2.0
+
+ $ export PATH="$TESTDIR/../cmd:$PATH"
+
+ $ test "$(command -v chown)" = "$TESTDIR/../cmd/chown"
+
+ $ test ! -f enoent
+ $ chown nobody:nogroup enoent
+ chown: Failed getting status for 'enoent': No such file or directory
+ [1]
+
+ $ touch id_unchanged
+ $ chown -v $(id -u):$(id -g) id_unchanged
+ chown: Ownership already set to \d+:\d+ for 'id_unchanged' (re)
+ $ rm id_unchanged
+
+ $ touch uid_unchanged
+ $ chown -v $(id -u) uid_unchanged
+ chown: Ownership already set to \d+:\d+ for 'uid_unchanged' (re)
+ $ rm uid_unchanged
+
+ $ touch name_unchanged
+ $ chown -v $(id -un):$(id -gn) name_unchanged
+ chown: Ownership already set to \d+:\d+ for 'name_unchanged' (re)
+ $ rm name_unchanged
+
+ $ touch uname_unchanged
+ $ chown -v $(id -un) uname_unchanged
+ chown: Ownership already set to \d+:\d+ for 'uname_unchanged' (re)
+ $ rm uname_unchanged
+
+ $ mkdir loop.d
+ $ ln -s ../loop.d/ loop.d/loop
+ $ chown -v $(id -u) loop.d
+ chown: Ownership already set to \d+:\d+ for 'loop.d' (re)
+ $ chown -v $(id -u) loop.d/loop
+ chown: Ownership already set to \d+:\d+ for 'loop.d/loop' (re)
+ $ chown -v -h $(id -u) loop.d/loop
+ chown: Ownership already set to \d+:\d+ for 'loop.d/loop' (re)
+ $ chown -v -R $(id -u) loop.d
+ chown: Ownership already set to \d+:\d+ for 'loop.d' (re)
+ chown: Ownership already set to \d+:\d+ for 'loop.d/loop' (re)
+ $ chown -v -R $(id -u) loop.d/loop
+ chown: Ownership already set to \d+:\d+ for 'loop.d/loop' (re)
+ $ chown -v -R -P $(id -u) loop.d/loop
+ chown: Ownership already set to \d+:\d+ for 'loop.d/loop' (re)
+ $ chown -v -R -H $(id -u) loop.d/loop
+ chown: Ownership already set to \d+:\d+ for 'loop.d/loop' (re)
+ chown: Ownership already set to \d+:\d+ for 'loop.d/loop/loop' (re)
+ $ chown -v -R -L $(id -u) loop.d/loop >/dev/null 2>/dev/null
+ [1]
+ $ rm -r loop.d
+
+ $ touch grp_unchanged
+ $ chown -v :$(id -gn) grp_unchanged
+ chown: Ownership already set to \d+:\d+ for 'grp_unchanged' (re)
+ $ rm grp_unchanged
+
+ $ find .
+ .