logo

utils-std

Collection of commonly available Unix tools
commit: bb298210abf592c18230160143f35d70c18958b8
parent a2a8cecb071ee55df5c328938cbbfd92915bc356
Author: Haelwenn (lanodan) Monnier <contact@hacktivis.me>
Date:   Sat, 27 Apr 2024 10:28:56 +0200

cmd/chown: new

Diffstat:

MMakefile1+
Acmd/chown.186+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acmd/chown.c349+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmakeless.sh1+
Atest-cmd/chown.t62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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 . + .