* Makefile, NEWS, zic.8: Mention this.
* private.h (_): Parenthesize result, for compatibility with N_.
(N_): New macro, for strings translated later.
* zic.c (creat_perms): New static var.
(HAVE_PWD_H): Provide a default, typically 1.
[HAVE_PWD_H]: Include pwd.h and grp.h.
(gid_t, uid_t, struct group, struct passwd, getgrnam, getpwnam, fchown):
New fallback types and macros if !HAVE_PWD_H.
(no_gid, no_uid): New static constants, so that we can avoid casts.
(output_group, output_owner): New static vars.
(GID_T_MAX, UID_T_MAX): New macros.
(chmetadata): fchown output if requested.
(usage): Mention -u.
(group_option, owner_option, use_safe_permissions):
New static functions.
(main): Support -u (and -g, but this is undocumented).
(open_outfile): Use creat_perms instead of CREAT_PERMS.
---
 Makefile |   3 ++
 NEWS     |   2 +-
 zic.8    |  13 +++++
 zic.c    | 155 +++++++++++++++++++++++++++++++++++++++++++++++++++++--
 4 files changed, 168 insertions(+), 5 deletions(-)

diff --git a/Makefile b/Makefile
index 224f250a..b3ceb997 100644
--- a/Makefile
+++ b/Makefile
@@ -267,6 +267,9 @@ LDLIBS=
 #  -DHAVE_MEMPCPY=1 if your system has mempcpy, 0 if not (default is guessed)
 #  -DHAVE_POSIX_DECLS=0 if your system's include files do not declare
 #      variables like 'tzname' required by POSIX
+#  -DHAVE_PWD_H=0 if your system lacks pwd.h, grp.h and corresponding functions
+#      The following additional options may be needed:
+#      -Dgid_t=T, -Duid_t=T to define gid_t, uid_t to be type T (default int)
 #  -DHAVE_SETENV=0 if your system lacks the setenv function
 #  -DHAVE_SETMODE=[01] if your system lacks or has the setmode and getmode
 #      functions (default is guessed)
diff --git a/NEWS b/NEWS
index 8f48a7bd..9764cf0c 100644
--- a/NEWS
+++ b/NEWS
@@ -105,7 +105,7 @@ Unreleased, experimental changes
     exceedingly long TZ strings no longer fail merely because they
     exceed an arbitrary file name length limit imposed by tzcode.
 
-    zic has new options -D and -m, inspired by FreeBSD.
+    zic has new options -D, -m, and -u, inspired by FreeBSD.
 
     zic now uses the fdopen function, which was standardized by
     POSIX.1-1988 and is now safe to use in portable code.
diff --git a/zic.8 b/zic.8
index 761b204c..75ca288e 100644
--- a/zic.8
+++ b/zic.8
@@ -203,6 +203,19 @@ it increases the size of the altered output files.
 When creating local time information, put the configuration link in
 the named file rather than in the standard location.
 .TP
+.BI "\-u " owner\fR[:\fPgroup\fR]\fP
+Change the output regular files' owner and group to those specified.
+The
+.I owner
+is either a user name, or an unsigned decimal integer user ID,
+or an empty string meaning no change to the owner.
+The
+.I group
+is similar for group names and IDs.
+This option does not affect directories or hard or symbolic links.
+It typically needs special privileges to change ownership,
+and is ignored on platforms that lack the notions of owners and groups.
+.TP
 .B \-v
 Be more verbose, and complain about the following situations:
 .RS
diff --git a/zic.c b/zic.c
index fc03c32d..d6bf8b60 100644
--- a/zic.c
+++ b/zic.c
@@ -101,7 +101,68 @@ enum { FORMAT_LEN_GROWTH_BOUND = 5 };
 /* File permission bits for making regular files.
    The umask modifies these bits.  */
 #define CREAT_PERMS (MKDIR_PERMS & ~(S_IXUSR | S_IXGRP | S_IXOTH))
+static mode_t creat_perms = CREAT_PERMS;
 
+#ifndef HAVE_PWD_H
+# ifdef __has_include
+#  if __has_include(<pwd.h>) && __has_include(<grp.h>)
+#   define HAVE_PWD_H 1
+#  else
+#   define HAVE_PWD_H 0
+#  endif
+# endif
+#endif
+#ifndef HAVE_PWD_H
+# define HAVE_PWD_H 1
+#endif
+#if HAVE_PWD_H
+# include <grp.h>
+# include <pwd.h>
+#else
+# ifndef gid_t
+#  define gid_t int
+# endif
+# ifndef uid_t
+#  define uid_t int
+# endif
+struct group { gid_t gr_gid; };
+struct passwd { uid_t pw_uid; };
+# define getgrnam(arg) NULL
+# define getpwnam(arg) NULL
+# define fchown(fd, owner, group) ((fd) < 0 ? -1 : 0)
+#endif
+static gid_t const no_gid = -1;
+static uid_t const no_uid = -1;
+static gid_t output_group = -1;
+static uid_t output_owner = -1;
+#ifndef GID_T_MAX
+# define GID_T_MAX_NO_PADDING MAXVAL(gid_t, TYPE_BIT(gid_t))
+# if HAVE__GENERIC
+#  define GID_T_MAX \
+    (TYPE_SIGNED(gid_t) \
+     ? _Generic((gid_t) 0, \
+               signed char: SCHAR_MAX, short: SHRT_MAX, \
+               int: INT_MAX, long: LONG_MAX, long long: LLONG_MAX, \
+               default: GID_T_MAX_NO_PADDING) \
+     : (gid_t) -1)
+# else
+#  define GID_T_MAX GID_T_MAX_NO_PADDING
+# endif
+#endif
+#ifndef UID_T_MAX
+# define UID_T_MAX_NO_PADDING MAXVAL(uid_t, TYPE_BIT(uid_t))
+# if HAVE__GENERIC
+#  define UID_T_MAX \
+    (TYPE_SIGNED(uid_t) \
+     ? _Generic((uid_t) 0, \
+               signed char: SCHAR_MAX, short: SHRT_MAX, \
+               int: INT_MAX, long: LONG_MAX, long long: LLONG_MAX, \
+               default: UID_T_MAX_NO_PADDING) \
+     : (uid_t) -1)
+# else
+#  define UID_T_MAX UID_T_MAX_NO_PADDING
+# endif
+#endif
 
 /* The minimum alignment of a type, for pre-C23 platforms.
    The __SUNPRO_C test is because Oracle Developer Studio 12.6 lacks
@@ -738,12 +799,17 @@ mode_option(char const *arg)
   }
 #endif
   return arg2num(arg, 8, min(MODE_T_MAX, ULONG_MAX),
-                "%s: -m '%s': invalid mode\n");
+                N_("%s: -m '%s': invalid mode\n"));
 }
 
 static int
 chmetadata(FILE *stream)
 {
+  if (output_owner != no_uid || output_group != no_gid) {
+    int r = fchown(fileno(stream), output_owner, output_group);
+    if (r < 0)
+      return r;
+  }
   return output_mode == no_mode ? 0 : fchmod(fileno(stream), output_mode);
 }
 
@@ -789,7 +855,7 @@ usage(FILE *stream, int status)
            "\t[ -b {slim|fat} ] [ -d directory ] [ -D ] \\\n"
            "\t[ -l localtime ] [ -L leapseconds ] [ -m mode ] \\\n"
            "\t[ -p posixrules ] [ -r '[@lo][/@hi]' ] [ -R @hi ] \\\n"
-           "\t[ -t localtime-link ] \\\n"
+           "\t[ -t localtime-link ] [ -u 'owner[:group]' ] \\\n"
            "\t[ filename ... ]\n\n"
            "Report bugs to %s.\n"),
          progname, progname, REPORT_BUGS_TO);
@@ -798,6 +864,71 @@ usage(FILE *stream, int status)
   exit(status);
 }
 
+static void
+group_option(char const *arg)
+{
+  if (*arg) {
+    if (output_group != no_gid) {
+      fprintf(stderr, _("multiple groups specified"));
+      exit(EXIT_FAILURE);
+    } else {
+      struct group *gr = getgrnam(arg);
+      output_group = (gr ? gr->gr_gid
+                     : arg2num(arg, 10, min(GID_T_MAX, ULONG_MAX),
+                               N_("%s: invalid group: %s\n")));
+    }
+  }
+}
+
+static void
+owner_option(char const *arg)
+{
+  if (*arg) {
+    if (output_owner != no_uid) {
+      fprintf(stderr, _("multiple owners specified"));
+      exit(EXIT_FAILURE);
+    } else {
+      struct passwd *pw = getpwnam(arg);
+      output_owner = (pw ? pw->pw_uid
+                     : arg2num(arg, 10, min(UID_T_MAX, ULONG_MAX),
+                               N_("%s: invalid owner: %s\n")));
+    }
+  }
+}
+
+/* If setting owner or group, use temp file permissions that avoid
+   security races before the fchmod at the end.  */
+static void
+use_safe_temp_permissions(void)
+{
+  if (output_owner != no_uid || output_group != no_gid) {
+
+    /* The mode when done with the file.  */
+    mode_t omode;
+    if (output_mode == no_mode) {
+      mode_t cmask = umask(0);
+      umask(cmask);
+      omode = CREAT_PERMS & ~cmask;
+    } else
+      omode = output_mode;
+
+    /* The mode passed to open+O_CREAT.  Do not bother with executable
+       permissions, as they should not be used and this mode is merely
+       a nicety (even a mode of 0 still work).  */
+    creat_perms = ((((omode & (S_IRUSR | S_IRGRP | S_IROTH))
+                    == (S_IRUSR | S_IRGRP | S_IROTH))
+                   ? S_IRUSR | S_IRGRP | S_IROTH : 0)
+                  | (((omode & (S_IWUSR | S_IWGRP | S_IWOTH))
+                      == (S_IWUSR | S_IWGRP | S_IWOTH))
+                     ? S_IWUSR | S_IWGRP | S_IWOTH : 0));
+
+    /* If creat_perms is not the final mode, arrange to run
+       fchmod later, even if -m was not used.  */
+    if (creat_perms != omode)
+      output_mode = omode;
+  }
+}
+
 /* Change the working directory to DIR, possibly creating DIR and its
    ancestors.  After this is done, all files are accessed with names
    relative to DIR.  */
@@ -1119,7 +1250,7 @@ main(int argc, char **argv)
                } else if (strcmp(argv[k], "--help") == 0) {
                        usage(stdout, EXIT_SUCCESS);
                }
-       while ((c = getopt(argc, argv, "b:d:Dl:L:m:p:r:R:st:vy:")) != -1)
+       while ((c = getopt(argc, argv, "b:d:Dg:l:L:m:p:r:R:st:u:vy:")) != -1)
                switch (c) {
                        default:
                                usage(stderr, EXIT_FAILURE);
@@ -1143,6 +1274,11 @@ main(int argc, char **argv)
                        case 'D':
                                skip_mkdir = true;
                                break;
+                       case 'g':
+                               /* This undocumented option is present for
+                                  compatibility with FreeBSD 14.  */
+                               group_option(optarg);
+                               break;
                        case 'l':
                                if (lcltime)
                                  duplicate_options("-l");
@@ -1163,6 +1299,16 @@ main(int argc, char **argv)
                                  duplicate_options("-t");
                                tzdefault = optarg;
                                break;
+                       case 'u':
+                               {
+                                 char *colon = strchr(optarg, ':');
+                                 if (colon)
+                                   *colon = '\0';
+                                 owner_option(optarg);
+                                 if (colon)
+                                   group_option(colon + 1);
+                               }
+                               break;
                        case 'y':
                                warning(_("-y ignored"));
                                break;
@@ -1228,6 +1374,7 @@ main(int argc, char **argv)
        if (errors)
                return EXIT_FAILURE;
        associate();
+       use_safe_temp_permissions();
        change_directory(directory);
        directory_ends_in_slash = directory[strlen(directory) - 1] == '/';
        catch_signals();
@@ -1460,7 +1607,7 @@ open_outfile(char const **outname, char **tempname)
 
   while (true) {
     int oflags = O_WRONLY | O_BINARY | O_CREAT | O_EXCL;
-    int fd = open(*outname, oflags, CREAT_PERMS);
+    int fd = open(*outname, oflags, creat_perms);
     int err;
     if (fd < 0)
       err = errno;
-- 
2.48.1

Reply via email to