maint: update copyright
[gnulib.git] / lib / modechange.c
index 8eb8dc5..8ac1879 100644 (file)
@@ -1,10 +1,12 @@
 /* modechange.c -- file mode manipulation
-   Copyright (C) 1989, 1990 Free Software Foundation, Inc.
 
-   This program is free software; you can redistribute it and/or modify
+   Copyright (C) 1989-1990, 1997-1999, 2001, 2003-2006, 2009-2014 Free Software
+   Foundation, Inc.
+
+   This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
-   the Free Software Foundation; either version 2, or (at your option)
-   any later version.
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
 
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    GNU General Public License for more details.
 
    You should have received a copy of the GNU General Public License
-   along with this program; if not, write to the Free Software
-   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.  */
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
 
 /* Written by David MacKenzie <djm@ai.mit.edu> */
 
-/* The ASCII mode string is compiled into a linked list of `struct
+/* The ASCII mode string is compiled into an array of 'struct
    modechange', which can then be applied to each file to be changed.
    We do this instead of re-parsing the ASCII string for each file
    because the compiled form requires less computation to use; when
    changing the mode of many files, this probably results in a
-   performance gain. */
+   performance gain.  */
 
-#ifdef HAVE_CONFIG_H
 #include <config.h>
-#endif
 
-#include <sys/types.h>
-#include <sys/stat.h>
 #include "modechange.h"
-
-#ifdef STDC_HEADERS
+#include <sys/stat.h>
+#include "stat-macros.h"
+#include "xalloc.h"
 #include <stdlib.h>
-#else
-char *malloc ();
-#endif
 
-#ifndef NULL
-#define NULL 0
-#endif
+/* The traditional octal values corresponding to each mode bit.  */
+#define SUID 04000
+#define SGID 02000
+#define SVTX 01000
+#define RUSR 00400
+#define WUSR 00200
+#define XUSR 00100
+#define RGRP 00040
+#define WGRP 00020
+#define XGRP 00010
+#define ROTH 00004
+#define WOTH 00002
+#define XOTH 00001
+#define ALLM 07777 /* all octal mode bits */
+
+/* Convert OCTAL, which uses one of the traditional octal values, to
+   an internal mode_t value.  */
+static mode_t
+octal_to_mode (unsigned int octal)
+{
+  /* Help the compiler optimize the usual case where mode_t uses
+     the traditional octal representation.  */
+  return ((S_ISUID == SUID && S_ISGID == SGID && S_ISVTX == SVTX
+           && S_IRUSR == RUSR && S_IWUSR == WUSR && S_IXUSR == XUSR
+           && S_IRGRP == RGRP && S_IWGRP == WGRP && S_IXGRP == XGRP
+           && S_IROTH == ROTH && S_IWOTH == WOTH && S_IXOTH == XOTH)
+          ? octal
+          : (mode_t) ((octal & SUID ? S_ISUID : 0)
+                      | (octal & SGID ? S_ISGID : 0)
+                      | (octal & SVTX ? S_ISVTX : 0)
+                      | (octal & RUSR ? S_IRUSR : 0)
+                      | (octal & WUSR ? S_IWUSR : 0)
+                      | (octal & XUSR ? S_IXUSR : 0)
+                      | (octal & RGRP ? S_IRGRP : 0)
+                      | (octal & WGRP ? S_IWGRP : 0)
+                      | (octal & XGRP ? S_IXGRP : 0)
+                      | (octal & ROTH ? S_IROTH : 0)
+                      | (octal & WOTH ? S_IWOTH : 0)
+                      | (octal & XOTH ? S_IXOTH : 0)));
+}
 
-#ifdef STAT_MACROS_BROKEN
-#undef S_ISDIR
-#endif /* STAT_MACROS_BROKEN.  */
+/* Special operations flags.  */
+enum
+  {
+    /* For the sentinel at the end of the mode changes array.  */
+    MODE_DONE,
 
-#if !defined(S_ISDIR) && defined(S_IFDIR)
-#define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR)
-#endif
+    /* The typical case.  */
+    MODE_ORDINARY_CHANGE,
 
-/* Return newly allocated memory to hold one element of type TYPE. */
-#define talloc(type) ((type *) malloc (sizeof (type)))
+    /* In addition to the typical case, affect the execute bits if at
+       least one execute bit is set already, or if the file is a
+       directory.  */
+    MODE_X_IF_ANY_X,
 
-#define isodigit(c) ((c) >= '0' && (c) <= '7')
+    /* Instead of the typical case, copy some existing permissions for
+       u, g, or o onto the other two.  Which of u, g, or o is copied
+       is determined by which bits are set in the 'value' field.  */
+    MODE_COPY_EXISTING
+  };
 
-static int oatoi ();
+/* Description of a mode change.  */
+struct mode_change
+{
+  char op;                      /* One of "=+-".  */
+  char flag;                    /* Special operations flag.  */
+  mode_t affected;              /* Set for u, g, o, or a.  */
+  mode_t value;                 /* Bits to add/remove.  */
+  mode_t mentioned;             /* Bits explicitly mentioned.  */
+};
+
+/* Return a mode_change array with the specified "=ddd"-style
+   mode change operation, where NEW_MODE is "ddd" and MENTIONED
+   contains the bits explicitly mentioned in the mode are MENTIONED.  */
+
+static struct mode_change *
+make_node_op_equals (mode_t new_mode, mode_t mentioned)
+{
+  struct mode_change *p = xmalloc (2 * sizeof *p);
+  p->op = '=';
+  p->flag = MODE_ORDINARY_CHANGE;
+  p->affected = CHMOD_MODE_BITS;
+  p->value = new_mode;
+  p->mentioned = mentioned;
+  p[1].flag = MODE_DONE;
+  return p;
+}
 
-/* Return a linked list of file mode change operations created from
+/* Return a pointer to an array of file mode change operations created from
    MODE_STRING, an ASCII string that contains either an octal number
    specifying an absolute mode, or symbolic mode change operations with
    the form:
    [ugoa...][[+-=][rwxXstugo...]...][,...]
-   MASKED_OPS is a bitmask indicating which symbolic mode operators (=+-)
-   should not affect bits set in the umask when no users are given.
-   Operators not selected in MASKED_OPS ignore the umask.
 
-   Return MODE_INVALID if `mode_string' does not contain a valid
-   representation of file mode change operations;
-   return MODE_MEMORY_EXHAUSTED if there is insufficient memory. */
+   Return NULL if 'mode_string' does not contain a valid
+   representation of file mode change operations.  */
 
 struct mode_change *
-mode_compile (mode_string, masked_ops)
-     register char *mode_string;
-     unsigned masked_ops;
+mode_compile (char const *mode_string)
 {
-  struct mode_change *head;    /* First element of the linked list. */
-  struct mode_change *change;  /* An element of the linked list. */
-  int i;                       /* General purpose temporary. */
-  int umask_value;             /* The umask value (surprise). */
-  unsigned short affected_bits;        /* Which bits in the mode are operated on. */
-  unsigned short affected_masked; /* `affected_bits' modified by umask. */
-  unsigned ops_to_mask;                /* Operators to actually use umask on. */
-
-  i = oatoi (mode_string);
-  if (i >= 0)
+  /* The array of mode-change directives to be returned.  */
+  struct mode_change *mc;
+  size_t used = 0;
+  char const *p;
+
+  if ('0' <= *mode_string && *mode_string < '8')
     {
-      if (i > 07777)
-       return MODE_INVALID;
-      head = talloc (struct mode_change);
-      if (head == NULL)
-       return MODE_MEMORY_EXHAUSTED;
-      head->next = NULL;
-      head->op = '=';
-      head->flags = 0;
-      head->value = i;
-      head->affected = 07777;  /* Affect all permissions. */
-      return head;
+      unsigned int octal_mode = 0;
+      mode_t mode;
+      mode_t mentioned;
+
+      p = mode_string;
+      do
+        {
+          octal_mode = 8 * octal_mode + *p++ - '0';
+          if (ALLM < octal_mode)
+            return NULL;
+        }
+      while ('0' <= *p && *p < '8');
+
+      if (*p)
+        return NULL;
+
+      mode = octal_to_mode (octal_mode);
+      mentioned = (p - mode_string < 5
+                   ? (mode & (S_ISUID | S_ISGID)) | S_ISVTX | S_IRWXUGO
+                   : CHMOD_MODE_BITS);
+      return make_node_op_equals (mode, mentioned);
     }
 
-  umask_value = umask (0);
-  umask (umask_value);         /* Restore the old value. */
-
-  head = NULL;
-#ifdef lint
-  change = NULL;
-#endif
-  --mode_string;
+  /* Allocate enough space to hold the result.  */
+  {
+    size_t needed = 1;
+    for (p = mode_string; *p; p++)
+      needed += (*p == '=' || *p == '+' || *p == '-');
+    mc = xnmalloc (needed, sizeof *mc);
+  }
+
+  /* One loop iteration for each
+     '[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=][0-7]+'.  */
+  for (p = mode_string; ; p++)
+    {
+      /* Which bits in the mode are operated on.  */
+      mode_t affected = 0;
+
+      /* Turn on all the bits in 'affected' for each group given.  */
+      for (;; p++)
+        switch (*p)
+          {
+          default:
+            goto invalid;
+          case 'u':
+            affected |= S_ISUID | S_IRWXU;
+            break;
+          case 'g':
+            affected |= S_ISGID | S_IRWXG;
+            break;
+          case 'o':
+            affected |= S_ISVTX | S_IRWXO;
+            break;
+          case 'a':
+            affected |= CHMOD_MODE_BITS;
+            break;
+          case '=': case '+': case '-':
+            goto no_more_affected;
+          }
+    no_more_affected:;
+
+      do
+        {
+          char op = *p++;
+          mode_t value;
+          mode_t mentioned = 0;
+          char flag = MODE_COPY_EXISTING;
+          struct mode_change *change;
+
+          switch (*p)
+            {
+            case '0': case '1': case '2': case '3':
+            case '4': case '5': case '6': case '7':
+              {
+                unsigned int octal_mode = 0;
+
+                do
+                  {
+                    octal_mode = 8 * octal_mode + *p++ - '0';
+                    if (ALLM < octal_mode)
+                      return NULL;
+                  }
+                while ('0' <= *p && *p < '8');
+
+                if (affected || (*p && *p != ','))
+                  return NULL;
+                affected = mentioned = CHMOD_MODE_BITS;
+                value = octal_to_mode (octal_mode);
+                flag = MODE_ORDINARY_CHANGE;
+                break;
+              }
+
+            case 'u':
+              /* Set the affected bits to the value of the "u" bits
+                 on the same file.  */
+              value = S_IRWXU;
+              p++;
+              break;
+            case 'g':
+              /* Set the affected bits to the value of the "g" bits
+                 on the same file.  */
+              value = S_IRWXG;
+              p++;
+              break;
+            case 'o':
+              /* Set the affected bits to the value of the "o" bits
+                 on the same file.  */
+              value = S_IRWXO;
+              p++;
+              break;
+
+            default:
+              value = 0;
+              flag = MODE_ORDINARY_CHANGE;
+
+              for (;; p++)
+                switch (*p)
+                  {
+                  case 'r':
+                    value |= S_IRUSR | S_IRGRP | S_IROTH;
+                    break;
+                  case 'w':
+                    value |= S_IWUSR | S_IWGRP | S_IWOTH;
+                    break;
+                  case 'x':
+                    value |= S_IXUSR | S_IXGRP | S_IXOTH;
+                    break;
+                  case 'X':
+                    flag = MODE_X_IF_ANY_X;
+                    break;
+                  case 's':
+                    /* Set the setuid/gid bits if 'u' or 'g' is selected.  */
+                    value |= S_ISUID | S_ISGID;
+                    break;
+                  case 't':
+                    /* Set the "save text image" bit if 'o' is selected.  */
+                    value |= S_ISVTX;
+                    break;
+                  default:
+                    goto no_more_values;
+                  }
+            no_more_values:;
+            }
+
+          change = &mc[used++];
+          change->op = op;
+          change->flag = flag;
+          change->affected = affected;
+          change->value = value;
+          change->mentioned =
+            (mentioned ? mentioned : affected ? affected & value : value);
+        }
+      while (*p == '=' || *p == '+' || *p == '-');
+
+      if (*p != ',')
+        break;
+    }
 
-  /* One loop iteration for each "ugoa...=+-rwxXstugo...[=+-rwxXstugo...]". */
-  do
+  if (*p == 0)
     {
-      affected_bits = 0;
-      ops_to_mask = 0;
-      /* Turn on all the bits in `affected_bits' for each group given. */
-      for (++mode_string;; ++mode_string)
-       switch (*mode_string)
-         {
-         case 'u':
-           affected_bits |= 04700;
-           break;
-         case 'g':
-           affected_bits |= 02070;
-           break;
-         case 'o':
-           affected_bits |= 01007;
-           break;
-         case 'a':
-           affected_bits |= 07777;
-           break;
-         default:
-           goto no_more_affected;
-         }
-
-    no_more_affected:
-      /* If none specified, affect all bits, except perhaps those
-        set in the umask. */
-      if (affected_bits == 0)
-       {
-         affected_bits = 07777;
-         ops_to_mask = masked_ops;
-       }
-
-      while (*mode_string == '=' || *mode_string == '+' || *mode_string == '-')
-       {
-         /* Add the element to the tail of the list, so the operations
-            are performed in the correct order. */
-         if (head == NULL)
-           {
-             head = talloc (struct mode_change);
-             if (head == NULL)
-               return MODE_MEMORY_EXHAUSTED;
-             change = head;
-           }
-         else
-           {
-             change->next = talloc (struct mode_change);
-             if (change->next == NULL)
-               {
-                 mode_free (change);
-                 return MODE_MEMORY_EXHAUSTED;
-               }
-             change = change->next;
-           }
-
-         change->next = NULL;
-         change->op = *mode_string;    /* One of "=+-". */
-         affected_masked = affected_bits;
-         if (ops_to_mask & (*mode_string == '=' ? MODE_MASK_EQUALS
-                            : *mode_string == '+' ? MODE_MASK_PLUS
-                            : MODE_MASK_MINUS))
-           affected_masked &= ~umask_value;
-         change->affected = affected_masked;
-         change->value = 0;
-         change->flags = 0;
-
-         /* Set `value' according to the bits set in `affected_masked'. */
-         for (++mode_string;; ++mode_string)
-           switch (*mode_string)
-             {
-             case 'r':
-               change->value |= 00444 & affected_masked;
-               break;
-             case 'w':
-               change->value |= 00222 & affected_masked;
-               break;
-             case 'X':
-               change->flags |= MODE_X_IF_ANY_X;
-               /* Fall through. */
-             case 'x':
-               change->value |= 00111 & affected_masked;
-               break;
-             case 's':
-               /* Set the setuid/gid bits if `u' or `g' is selected. */
-               change->value |= 06000 & affected_masked;
-               break;
-             case 't':
-               /* Set the "save text image" bit if `o' is selected. */
-               change->value |= 01000 & affected_masked;
-               break;
-             case 'u':
-               /* Set the affected bits to the value of the `u' bits
-                  on the same file.  */
-               if (change->value)
-                 goto invalid;
-               change->value = 00700;
-               change->flags |= MODE_COPY_EXISTING;
-               break;
-             case 'g':
-               /* Set the affected bits to the value of the `g' bits
-                  on the same file.  */
-               if (change->value)
-                 goto invalid;
-               change->value = 00070;
-               change->flags |= MODE_COPY_EXISTING;
-               break;
-             case 'o':
-               /* Set the affected bits to the value of the `o' bits
-                  on the same file.  */
-               if (change->value)
-                 goto invalid;
-               change->value = 00007;
-               change->flags |= MODE_COPY_EXISTING;
-               break;
-             default:
-               goto no_more_values;
-             }
-       no_more_values:;
-       }
-  } while (*mode_string == ',');
-  if (*mode_string == 0)
-    return head;
+      mc[used].flag = MODE_DONE;
+      return mc;
+    }
+
 invalid:
-  mode_free (head);
-  return MODE_INVALID;
+  free (mc);
+  return NULL;
 }
 
-/* Return file mode OLDMODE, adjusted as indicated by the list of change
-   operations CHANGES.  If OLDMODE is a directory, the type `X'
-   change affects it even if no execute bits were set in OLDMODE.
-   The returned value has the S_IFMT bits cleared. */
+/* Return a file mode change operation that sets permissions to match those
+   of REF_FILE.  Return NULL (setting errno) if REF_FILE can't be accessed.  */
 
-unsigned short
-mode_adjust (oldmode, changes)
-     unsigned oldmode;
-     register struct mode_change *changes;
+struct mode_change *
+mode_create_from_ref (const char *ref_file)
 {
-  unsigned short newmode;      /* The adjusted mode and one operand. */
-  unsigned short value;                /* The other operand. */
-
-  newmode = oldmode & 07777;
-
-  for (; changes; changes = changes->next)
-    {
-      if (changes->flags & MODE_COPY_EXISTING)
-       {
-         /* Isolate in `value' the bits in `newmode' to copy, given in
-            the mask `changes->value'. */
-         value = newmode & changes->value;
-
-         if (changes->value & 00700)
-           /* Copy `u' permissions onto `g' and `o'. */
-           value |= (value >> 3) | (value >> 6);
-         else if (changes->value & 00070)
-           /* Copy `g' permissions onto `u' and `o'. */
-           value |= (value << 3) | (value >> 3);
-         else
-           /* Copy `o' permissions onto `u' and `g'. */
-           value |= (value << 3) | (value << 6);
-
-         /* In order to change only `u', `g', or `o' permissions,
-            or some combination thereof, clear unselected bits.
-            This can not be done in mode_compile because the value
-            to which the `changes->affected' mask is applied depends
-            on the old mode of each file. */
-         value &= changes->affected;
-       }
-      else
-       {
-         value = changes->value;
-         /* If `X', do not affect the execute bits if the file is not a
-            directory and no execute bits are already set. */
-         if ((changes->flags & MODE_X_IF_ANY_X)
-             && !S_ISDIR (oldmode)
-             && (newmode & 00111) == 0)
-           value &= ~00111;    /* Clear the execute bits. */
-       }
+  struct stat ref_stats;
 
-      switch (changes->op)
-       {
-       case '=':
-         /* Preserve the previous values in `newmode' of bits that are
-            not affected by this change operation. */
-         newmode = (newmode & ~changes->affected) | value;
-         break;
-       case '+':
-         newmode |= value;
-         break;
-       case '-':
-         newmode &= ~value;
-         break;
-       }
-    }
-  return newmode;
+  if (stat (ref_file, &ref_stats) != 0)
+    return NULL;
+  return make_node_op_equals (ref_stats.st_mode, CHMOD_MODE_BITS);
 }
 
-/* Free the memory used by the list of file mode change operations
-   CHANGES. */
-
-void
-mode_free (changes)
-     register struct mode_change *changes;
+/* Return the file mode bits of OLDMODE (which is the mode of a
+   directory if DIR), assuming the umask is UMASK_VALUE, adjusted as
+   indicated by the list of change operations CHANGES.  If DIR, the
+   type 'X' change affects the returned value even if no execute bits
+   were set in OLDMODE, and set user and group ID bits are preserved
+   unless CHANGES mentioned them.  If PMODE_BITS is not null, store into
+   *PMODE_BITS a mask denoting file mode bits that are affected by
+   CHANGES.
+
+   The returned value and *PMODE_BITS contain only file mode bits.
+   For example, they have the S_IFMT bits cleared on a standard
+   Unix-like host.  */
+
+mode_t
+mode_adjust (mode_t oldmode, bool dir, mode_t umask_value,
+             struct mode_change const *changes, mode_t *pmode_bits)
 {
-  register struct mode_change *next;
+  /* The adjusted mode.  */
+  mode_t newmode = oldmode & CHMOD_MODE_BITS;
 
-  while (changes)
+  /* File mode bits that CHANGES cares about.  */
+  mode_t mode_bits = 0;
+
+  for (; changes->flag != MODE_DONE; changes++)
     {
-      next = changes->next;
-      free (changes);
-      changes = next;
-    }
-}
+      mode_t affected = changes->affected;
+      mode_t omit_change =
+        (dir ? S_ISUID | S_ISGID : 0) & ~ changes->mentioned;
+      mode_t value = changes->value;
+
+      switch (changes->flag)
+        {
+        case MODE_ORDINARY_CHANGE:
+          break;
+
+        case MODE_COPY_EXISTING:
+          /* Isolate in 'value' the bits in 'newmode' to copy.  */
+          value &= newmode;
+
+          /* Copy the isolated bits to the other two parts.  */
+          value |= ((value & (S_IRUSR | S_IRGRP | S_IROTH)
+                     ? S_IRUSR | S_IRGRP | S_IROTH : 0)
+                    | (value & (S_IWUSR | S_IWGRP | S_IWOTH)
+                       ? S_IWUSR | S_IWGRP | S_IWOTH : 0)
+                    | (value & (S_IXUSR | S_IXGRP | S_IXOTH)
+                       ? S_IXUSR | S_IXGRP | S_IXOTH : 0));
+          break;
+
+        case MODE_X_IF_ANY_X:
+          /* Affect the execute bits if execute bits are already set
+             or if the file is a directory.  */
+          if ((newmode & (S_IXUSR | S_IXGRP | S_IXOTH)) | dir)
+            value |= S_IXUSR | S_IXGRP | S_IXOTH;
+          break;
+        }
+
+      /* If WHO was specified, limit the change to the affected bits.
+         Otherwise, apply the umask.  Either way, omit changes as
+         requested.  */
+      value &= (affected ? affected : ~umask_value) & ~ omit_change;
 
-/* Return a positive integer containing the value of the ASCII
-   octal number S.  If S is not an octal number, return -1.  */
+      switch (changes->op)
+        {
+        case '=':
+          /* If WHO was specified, preserve the previous values of
+             bits that are not affected by this change operation.
+             Otherwise, clear all the bits.  */
+          {
+            mode_t preserved = (affected ? ~affected : 0) | omit_change;
+            mode_bits |= CHMOD_MODE_BITS & ~preserved;
+            newmode = (newmode & preserved) | value;
+            break;
+          }
+
+        case '+':
+          mode_bits |= value;
+          newmode |= value;
+          break;
+
+        case '-':
+          mode_bits |= value;
+          newmode &= ~value;
+          break;
+        }
+    }
 
-static int
-oatoi (s)
-     char *s;
-{
-  register int i;
-
-  if (*s == 0)
-    return -1;
-  for (i = 0; isodigit (*s); ++s)
-    i = i * 8 + *s - '0';
-  if (*s)
-    return -1;
-  return i;
+  if (pmode_bits)
+    *pmode_bits = mode_bits;
+  return newmode;
 }