summaryrefslogtreecommitdiff
path: root/lib/modechange.c
blob: 991235b09cc1809cf15cea779d86e001cd3fb047 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
/* modechange.c -- file mode manipulation

   Copyright (C) 1989, 1990, 1997, 1998, 1999, 2001, 2003, 2004 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.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.  */

/* Written by David MacKenzie <djm@ai.mit.edu> */

/* The ASCII mode string is compiled into a linked list 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. */

#if HAVE_CONFIG_H
# include <config.h>
#endif

#include "modechange.h"
#include <sys/stat.h>
#include "xstrtol.h"
#include <stdbool.h>
#include <stddef.h>
#include <stdlib.h>

#if STAT_MACROS_BROKEN
# undef S_ISDIR
#endif

#if !defined(S_ISDIR) && defined(S_IFDIR)
# define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR)
#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 */

#ifndef S_ISUID
# define S_ISUID SUID
#endif
#ifndef S_ISGID
# define S_ISGID SGID
#endif
#ifndef S_ISVTX
# define S_ISVTX SVTX
#endif
#ifndef S_IRUSR
# define S_IRUSR RUSR
#endif
#ifndef S_IWUSR
# define S_IWUSR WUSR
#endif
#ifndef S_IXUSR
# define S_IXUSR XUSR
#endif
#ifndef S_IRGRP
# define S_IRGRP RGRP
#endif
#ifndef S_IWGRP
# define S_IWGRP WGRP
#endif
#ifndef S_IXGRP
# define S_IXGRP XGRP
#endif
#ifndef S_IROTH
# define S_IROTH ROTH
#endif
#ifndef S_IWOTH
# define S_IWOTH WOTH
#endif
#ifndef S_IXOTH
# define S_IXOTH XOTH
#endif
#ifndef S_IRWXU
# define S_IRWXU (S_IRUSR | S_IWUSR | S_IXUSR)
#endif
#ifndef S_IRWXG
# define S_IRWXG (S_IRGRP | S_IWGRP | S_IXGRP)
#endif
#ifndef S_IRWXO
# define S_IRWXO (S_IROTH | S_IWOTH | S_IXOTH)
#endif

/* All the mode bits that can be affected by chmod.  */
#define CHMOD_MODE_BITS \
  (S_ISUID | S_ISGID | S_ISVTX | S_IRWXU | S_IRWXG | S_IRWXO)

/* Return newly allocated memory to hold one element of type TYPE. */
#define talloc(type) ((type *) malloc (sizeof (type)))

/* Create a mode_change entry with the specified `=ddd'-style
   mode change operation, where NEW_MODE is `ddd'.  Return the
   new entry, or NULL upon failure.  */

static struct mode_change *
make_node_op_equals (mode_t new_mode)
{
  struct mode_change *p;
  p = talloc (struct mode_change);
  if (p == NULL)
    return p;
  p->next = NULL;
  p->op = '=';
  p->flags = 0;
  p->value = new_mode;
  p->affected = CHMOD_MODE_BITS;	/* Affect all permissions. */
  return p;
}

/* Append entry E to the end of the link list with the specified
   HEAD and TAIL.  */

static void
mode_append_entry (struct mode_change **head,
		   struct mode_change **tail,
		   struct mode_change *e)
{
  if (*head == NULL)
    *head = *tail = e;
  else
    {
      (*tail)->next = e;
      *tail = e;
    }
}

/* Return a linked list 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. */

struct mode_change *
mode_compile (const char *mode_string, unsigned int masked_ops)
{
  struct mode_change *head;	/* First element of the linked list. */
  struct mode_change *tail;	/* An element of the linked list. */
  unsigned long octal_value;	/* The mode value, if octal.  */
  mode_t umask_value;		/* The umask value (surprise). */

  head = NULL;
#ifdef lint
  tail = NULL;
#endif

  if (xstrtoul (mode_string, NULL, 8, &octal_value, "") == LONGINT_OK)
    {
      struct mode_change *p;
      mode_t mode;
      if (octal_value != (octal_value & ALLM))
	return MODE_INVALID;

      /* Help the compiler optimize the usual case where mode_t uses
	 the traditional octal representation.  */
      mode = ((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_value
	      : (mode_t) ((octal_value & SUID ? S_ISUID : 0)
			  | (octal_value & SGID ? S_ISGID : 0)
			  | (octal_value & SVTX ? S_ISVTX : 0)
			  | (octal_value & RUSR ? S_IRUSR : 0)
			  | (octal_value & WUSR ? S_IWUSR : 0)
			  | (octal_value & XUSR ? S_IXUSR : 0)
			  | (octal_value & RGRP ? S_IRGRP : 0)
			  | (octal_value & WGRP ? S_IWGRP : 0)
			  | (octal_value & XGRP ? S_IXGRP : 0)
			  | (octal_value & ROTH ? S_IROTH : 0)
			  | (octal_value & WOTH ? S_IWOTH : 0)
			  | (octal_value & XOTH ? S_IXOTH : 0)));

      p = make_node_op_equals (mode);
      if (p == NULL)
	return MODE_MEMORY_EXHAUSTED;
      mode_append_entry (&head, &tail, p);
      return head;
    }

  umask_value = umask (0);
  umask (umask_value);		/* Restore the old value. */

  /* One loop iteration for each "ugoa...=+-rwxXstugo...[=+-rwxXstugo...]". */
  for (;; mode_string++)
    {
      /* Which bits in the mode are operated on. */
      mode_t affected_bits = 0;
      /* `affected_bits' modified by umask. */
      mode_t affected_masked;
      /* Operators to actually use umask on. */
      unsigned int ops_to_mask = 0;

      bool who_specified_p;

      /* Turn on all the bits in `affected_bits' for each group given. */
      for (;; mode_string++)
	switch (*mode_string)
	  {
	  case 'u':
	    affected_bits |= S_ISUID | S_IRWXU;
	    break;
	  case 'g':
	    affected_bits |= S_ISGID | S_IRWXG;
	    break;
	  case 'o':
	    affected_bits |= S_ISVTX | S_IRWXO;
	    break;
	  case 'a':
	    affected_bits |= CHMOD_MODE_BITS;
	    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)
	who_specified_p = true;
      else
	{
	  who_specified_p = false;
	  affected_bits = CHMOD_MODE_BITS;
	  ops_to_mask = masked_ops;
	}

      while (*mode_string == '=' || *mode_string == '+' || *mode_string == '-')
	{
	  struct mode_change *change = talloc (struct mode_change);
	  if (change == NULL)
	    {
	      mode_free (head);
	      return MODE_MEMORY_EXHAUSTED;
	    }

	  change->next = NULL;
	  change->op = *mode_string;	/* One of "=+-". */
	  affected_masked = affected_bits;

	  /* Per the Single Unix Spec, if `who' is not specified and the
	     `=' operator is used, then clear all the bits first.  */
	  if (!who_specified_p &&
	      ops_to_mask & (*mode_string == '=' ? MODE_MASK_EQUALS : 0))
	    {
	      struct mode_change *p = make_node_op_equals (0);
	      if (p == NULL)
		return MODE_MEMORY_EXHAUSTED;
	      mode_append_entry (&head, &tail, p);
	    }

	  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;

	  /* Add the element to the tail of the list, so the operations
	     are performed in the correct order. */
	  mode_append_entry (&head, &tail, change);

	  /* Set `value' according to the bits set in `affected_masked'. */
	  for (++mode_string;; ++mode_string)
	    switch (*mode_string)
	      {
	      case 'r':
		change->value |= ((S_IRUSR | S_IRGRP | S_IROTH)
				  & affected_masked);
		break;
	      case 'w':
		change->value |= ((S_IWUSR | S_IWGRP | S_IWOTH)
				  & affected_masked);
		break;
	      case 'X':
		change->flags |= MODE_X_IF_ANY_X;
		/* Fall through. */
	      case 'x':
		change->value |= ((S_IXUSR | S_IXGRP | S_IXOTH)
				  & affected_masked);
		break;
	      case 's':
		/* Set the setuid/gid bits if `u' or `g' is selected. */
		change->value |= (S_ISUID | S_ISGID) & affected_masked;
		break;
	      case 't':
		/* Set the "save text image" bit if `o' is selected. */
		change->value |= S_ISVTX & 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 = S_IRWXU;
		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 = S_IRWXG;
		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 = S_IRWXO;
		change->flags |= MODE_COPY_EXISTING;
		break;
	      default:
		goto no_more_values;
	      }
	no_more_values:;
	}

      if (*mode_string != ',')
	break;
    }

  if (*mode_string == 0)
    return head;
invalid:
  mode_free (head);
  return MODE_INVALID;
}

/* Return a file mode change operation that sets permissions to match those
   of REF_FILE.  Return MODE_BAD_REFERENCE if REF_FILE can't be accessed.  */

struct mode_change *
mode_create_from_ref (const char *ref_file)
{
  struct mode_change *change;	/* the only change element */
  struct stat ref_stats;

  if (stat (ref_file, &ref_stats))
    return MODE_BAD_REFERENCE;

  change = talloc (struct mode_change);

  if (change == NULL)
    return MODE_MEMORY_EXHAUSTED;

  change->op = '=';
  change->flags = 0;
  change->affected = CHMOD_MODE_BITS;
  change->value = ref_stats.st_mode;
  change->next = NULL;

  return change;
}

/* 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. */

mode_t
mode_adjust (mode_t oldmode, const struct mode_change *changes)
{
  mode_t newmode;	/* The adjusted mode and one operand. */
  mode_t value;		/* The other operand. */

  newmode = oldmode & CHMOD_MODE_BITS;

  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 & S_IRWXU)
	    /* Copy `u' permissions onto `g' and `o'. */
	    value |= (  (value & S_IRUSR ? S_IRGRP | S_IROTH : 0)
		      | (value & S_IWUSR ? S_IWGRP | S_IWOTH : 0)
		      | (value & S_IXUSR ? S_IXGRP | S_IXOTH : 0));
	  else if (changes->value & S_IRWXG)
	    /* Copy `g' permissions onto `u' and `o'. */
	    value |= (  (value & S_IRGRP ? S_IRUSR | S_IROTH : 0)
		      | (value & S_IWGRP ? S_IWUSR | S_IWOTH : 0)
		      | (value & S_IXGRP ? S_IXUSR | S_IXOTH : 0));
	  else
	    /* Copy `o' permissions onto `u' and `g'. */
	    value |= (  (value & S_IROTH ? S_IRUSR | S_IRGRP : 0)
		      | (value & S_IWOTH ? S_IWUSR | S_IWGRP : 0)
		      | (value & S_IXOTH ? S_IXUSR | S_IXGRP : 0));

	  /* In order to change only `u', `g', or `o' permissions,
	     or some combination thereof, clear unselected bits.
	     This cannot 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 & (S_IXUSR | S_IXGRP | S_IXOTH)) == 0)
	    /* Clear the execute bits. */
	    value &= ~ (S_IXUSR | S_IXGRP | S_IXOTH);
	}

      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;
}

/* Free the memory used by the list of file mode change operations
   CHANGES. */

void
mode_free (register struct mode_change *changes)
{
  register struct mode_change *next;

  while (changes)
    {
      next = changes->next;
      free (changes);
      changes = next;
    }
}