xref: /illumos-gate/usr/src/contrib/ast/src/lib/libcmd/chgrp.c (revision 3aa6c13072f3d4792a18693e916aed260a496c1f)
1 /***********************************************************************
2 *                                                                      *
3 *               This software is part of the ast package               *
4 *          Copyright (c) 1992-2012 AT&T Intellectual Property          *
5 *                      and is licensed under the                       *
6 *                 Eclipse Public License, Version 1.0                  *
7 *                    by AT&T Intellectual Property                     *
8 *                                                                      *
9 *                A copy of the License is available at                 *
10 *          http://www.eclipse.org/org/documents/epl-v10.html           *
11 *         (with md5 checksum b35adb5213ca9657e911e9befb180842)         *
12 *                                                                      *
13 *              Information and Software Systems Research               *
14 *                            AT&T Research                             *
15 *                           Florham Park NJ                            *
16 *                                                                      *
17 *                 Glenn Fowler <gsf@research.att.com>                  *
18 *                  David Korn <dgk@research.att.com>                   *
19 *                                                                      *
20 ***********************************************************************/
21 #pragma prototyped
22 /*
23  * David Korn
24  * Glenn Fowler
25  * AT&T Research
26  *
27  * chgrp+chown
28  */
29 
30 static const char usage_1[] =
31 "[-?@(#)$Id: chgrp (AT&T Research) 2012-04-20 $\n]"
32 USAGE_LICENSE
33 ;
34 
35 static const char usage_grp_1[] =
36 "[+NAME?chgrp - change the group ownership of files]"
37 "[+DESCRIPTION?\bchgrp\b changes the group ownership of each file"
38 "	to \agroup\a, which can be either a group name or a numeric"
39 "	group id. The user ownership of each file may also be changed to"
40 "	\auser\a by prepending \auser\a\b:\b to the group name.]"
41 ;
42 
43 static const char usage_own_1[] =
44 "[+NAME?chown - change the ownership of files]"
45 "[+DESCRIPTION?\bchown\b changes the ownership of each file"
46 "	to \auser\a, which can be either a user name or a numeric"
47 "	user id. The group ownership of each file may also be changed to"
48 "	\auser\a by appending \b:\b\agroup\a to the user name.]"
49 ;
50 
51 static const char usage_2[] =
52 "[b:before?Only change files with \bctime\b before (less than) the "
53     "\bmtime\b of \afile\a.]:[file]"
54 "[c:changes?Describe only files whose ownership actually changes.]"
55 "[f:quiet|silent?Do not report files whose ownership fails to change.]"
56 "[h|l:symlink?Change the ownership of symbolic links on systems that "
57     "support \blchown\b(2). Implies \b--physical\b.]"
58 "[m:map?The first operand is interpreted as a file that contains a map "
59     "of space separated \afrom_uid:from_gid to_uid:to_gid\a pairs. The "
60     "\auid\a or \agid\a part of each pair may be omitted to mean any \auid\a "
61     "or \agid\a. Ownership of files matching the \afrom\a part of any pair "
62     "is changed to the corresponding \ato\a part of the pair. The matching "
63     "for each file operand is in the order \auid\a:\agid\a, \auid\a:, "
64     ":\agid\a. For a given file, once a \auid\a or \agid\a mapping is "
65     "determined it is not overridden by any subsequent match. Unmatched "
66     "files are silently ignored.]"
67 "[n:show?Show actions but don't execute.]"
68 "[N:numeric?By default numeric user and group id operands are first "
69     "interpreted as names; if no name exists then they are interpreted as "
70     "explicit numeric ids. \b--numeric\b interprets numeric id operands as "
71     "numeric ids.]"
72 "[r:reference?Omit the explicit ownership operand and use the ownership "
73     "of \afile\a instead.]:[file]"
74 "[u:unmapped?Print a diagnostic for each file for which either the "
75     "\auid\a or \agid\a or both were not mapped.]"
76 "[v:verbose?Describe changed permissions of all files.]"
77 "[H:metaphysical?Follow symbolic links for command arguments; otherwise "
78     "don't follow symbolic links when traversing directories.]"
79 "[L:logical|follow?Follow symbolic links when traversing directories.]"
80 "[P:physical|nofollow?Don't follow symbolic links when traversing "
81     "directories.]"
82 "[R:recursive?Recursively change ownership of directories and their "
83     "contents.]"
84 "[X:test?Canonicalize output for testing.]"
85 
86 "\n"
87 "\n"
88 ;
89 
90 static const char usage_3[] =
91 " file ...\n"
92 "\n"
93 "[+EXIT STATUS?]{"
94 	"[+0?All files changed successfully.]"
95 	"[+>0?Unable to change ownership of one or more files.]"
96 "}"
97 "[+SEE ALSO?\bchmod\b(1), \bchown\b(2), \btw\b(1), \bgetconf\b(1), \bls\b(1)]"
98 ;
99 
100 #if defined(__STDPP__directive) && defined(__STDPP__hide)
101 __STDPP__directive pragma pp:hide lchown
102 #else
103 #define lchown		______lchown
104 #endif
105 
106 #include <cmd.h>
107 #include <cdt.h>
108 #include <ls.h>
109 #include <ctype.h>
110 #include <fts_fix.h>
111 
112 #ifndef ENOSYS
113 #define ENOSYS	EINVAL
114 #endif
115 
116 #include "FEATURE/symlink"
117 
118 #if defined(__STDPP__directive) && defined(__STDPP__hide)
119 __STDPP__directive pragma pp:nohide lchown
120 #else
121 #undef	lchown
122 #endif
123 
124 typedef struct Key_s			/* uid/gid key			*/
125 {
126 	int		uid;		/* uid				*/
127 	int		gid;		/* gid				*/
128 } Key_t;
129 
130 typedef struct Map_s			/* uid/gid map			*/
131 {
132 	Dtlink_t	link;		/* dictionary link		*/
133 	Key_t		key;		/* key				*/
134 	Key_t		to;		/* map to these			*/
135 } Map_t;
136 
137 #define NOID		(-1)
138 
139 #define OPT_CHOWN	0x0001		/* chown			*/
140 #define OPT_FORCE	0x0002		/* ignore errors		*/
141 #define OPT_GID		0x0004		/* have gid			*/
142 #define OPT_LCHOWN	0x0008		/* lchown			*/
143 #define OPT_NUMERIC	0x0010		/* favor numeric ids		*/
144 #define OPT_SHOW	0x0020		/* show but don't do		*/
145 #define OPT_TEST	0x0040		/* canonicalize output		*/
146 #define OPT_UID		0x0080		/* have uid			*/
147 #define OPT_UNMAPPED	0x0100		/* unmapped file diagnostic	*/
148 #define OPT_VERBOSE	0x0200		/* have uid			*/
149 
150 extern int	lchown(const char*, uid_t, gid_t);
151 
152 /*
153  * parse uid and gid from s
154  */
155 
156 static void
157 getids(register char* s, char** e, Key_t* key, int options)
158 {
159 	register char*	t;
160 	register int	n;
161 	register int	m;
162 	char*		z;
163 	char		buf[64];
164 
165 	key->uid = key->gid = NOID;
166 	while (isspace(*s))
167 		s++;
168 	for (t = s; (n = *t) && n != ':' && n != '.' && !isspace(n); t++);
169 	if (n)
170 	{
171 		options |= OPT_CHOWN;
172 		if ((n = t++ - s) >= sizeof(buf))
173 			n = sizeof(buf) - 1;
174 		*((s = (char*)memcpy(buf, s, n)) + n) = 0;
175 	}
176 	if (options & OPT_CHOWN)
177 	{
178 		if (*s)
179 		{
180 			n = (int)strtol(s, &z, 0);
181 			if (*z || !(options & OPT_NUMERIC))
182 			{
183 				if ((m = struid(s)) != NOID)
184 					n = m;
185 				else if (*z)
186 					error(ERROR_exit(1), "%s: unknown user", s);
187 			}
188 			key->uid = n;
189 		}
190 		for (s = t; (n = *t) && !isspace(n); t++);
191 		if (n)
192 		{
193 			if ((n = t++ - s) >= sizeof(buf))
194 				n = sizeof(buf) - 1;
195 			*((s = (char*)memcpy(buf, s, n)) + n) = 0;
196 		}
197 	}
198 	if (*s)
199 	{
200 		n = (int)strtol(s, &z, 0);
201 		if (*z || !(options & OPT_NUMERIC))
202 		{
203 			if ((m = strgid(s)) != NOID)
204 				n = m;
205 			else if (*z)
206 				error(ERROR_exit(1), "%s: unknown group", s);
207 		}
208 		key->gid = n;
209 	}
210 	if (e)
211 		*e = t;
212 }
213 
214 /*
215  * NOTE: we only use the native lchown() on symlinks just in case
216  *	 the implementation is a feckless stub
217  */
218 
219 int
220 b_chgrp(int argc, char** argv, Shbltin_t* context)
221 {
222 	register int	options = 0;
223 	register char*	s;
224 	register Map_t*	m;
225 	register FTS*	fts;
226 	register FTSENT*ent;
227 	register int	i;
228 	Dt_t*		map = 0;
229 	int		logical = 1;
230 	int		flags;
231 	int		uid;
232 	int		gid;
233 	char*		op;
234 	char*		usage;
235 	char*		t;
236 	Sfio_t*		sp;
237 	unsigned long	before;
238 	Dtdisc_t	mapdisc;
239 	Key_t		keys[3];
240 	Key_t		key;
241 	struct stat	st;
242 	int		(*chownf)(const char*, uid_t, gid_t);
243 
244 	cmdinit(argc, argv, context, ERROR_CATALOG, ERROR_NOTIFY);
245 	flags = fts_flags() | FTS_META | FTS_TOP | FTS_NOPOSTORDER | FTS_NOSEEDOTDIR;
246 	before = ~0;
247 	if (!(sp = sfstropen()))
248 		error(ERROR_SYSTEM|3, "out of space");
249 	sfputr(sp, usage_1, -1);
250 	if (error_info.id[2] == 'g')
251 		sfputr(sp, usage_grp_1, -1);
252 	else
253 	{
254 		sfputr(sp, usage_own_1, -1);
255 		options |= OPT_CHOWN;
256 	}
257 	sfputr(sp, usage_2, -1);
258 	if (options & OPT_CHOWN)
259 		sfputr(sp, ERROR_translate(0, 0, 0, "[owner[:group]]"), -1);
260 	else
261 		sfputr(sp, ERROR_translate(0, 0, 0, "[[owner:]group]"), -1);
262 	sfputr(sp, usage_3, -1);
263 	if (!(usage = sfstruse(sp)))
264 		error(ERROR_SYSTEM|3, "out of space");
265 	for (;;)
266 	{
267 		switch (optget(argv, usage))
268 		{
269 		case 'b':
270 			if (stat(opt_info.arg, &st))
271 				error(ERROR_exit(1), "%s: cannot stat", opt_info.arg);
272 			before = st.st_mtime;
273 			continue;
274 		case 'c':
275 		case 'v':
276 			options |= OPT_VERBOSE;
277 			continue;
278 		case 'f':
279 			options |= OPT_FORCE;
280 			continue;
281 		case 'h':
282 			options |= OPT_LCHOWN;
283 			continue;
284 		case 'm':
285 			memset(&mapdisc, 0, sizeof(mapdisc));
286 			mapdisc.key = offsetof(Map_t, key);
287 			mapdisc.size = sizeof(Key_t);
288 			if (!(map = dtopen(&mapdisc, Dtset)))
289 				error(ERROR_exit(1), "out of space [id map]");
290 			continue;
291 		case 'n':
292 			options |= OPT_SHOW;
293 			continue;
294 		case 'N':
295 			options |= OPT_NUMERIC;
296 			continue;
297 		case 'r':
298 			if (stat(opt_info.arg, &st))
299 				error(ERROR_exit(1), "%s: cannot stat", opt_info.arg);
300 			uid = st.st_uid;
301 			gid = st.st_gid;
302 			options |= OPT_UID|OPT_GID;
303 			continue;
304 		case 'u':
305 			options |= OPT_UNMAPPED;
306 			continue;
307 		case 'H':
308 			flags |= FTS_META|FTS_PHYSICAL;
309 			logical = 0;
310 			continue;
311 		case 'L':
312 			flags &= ~(FTS_META|FTS_PHYSICAL);
313 			logical = 0;
314 			continue;
315 		case 'P':
316 			flags &= ~FTS_META;
317 			flags |= FTS_PHYSICAL;
318 			logical = 0;
319 			continue;
320 		case 'R':
321 			flags &= ~FTS_TOP;
322 			logical = 0;
323 			continue;
324 		case 'X':
325 			options |= OPT_TEST;
326 			continue;
327 		case ':':
328 			error(2, "%s", opt_info.arg);
329 			continue;
330 		case '?':
331 			error(ERROR_usage(2), "%s", opt_info.arg);
332 			break;
333 		}
334 		break;
335 	}
336 	argv += opt_info.index;
337 	argc -= opt_info.index;
338 	if (error_info.errors || argc < 2)
339 		error(ERROR_usage(2), "%s", optusage(NiL));
340 	s = *argv;
341 	if (options & OPT_LCHOWN)
342 	{
343 		flags &= ~FTS_META;
344 		flags |= FTS_PHYSICAL;
345 		logical = 0;
346 	}
347 	if (logical)
348 		flags &= ~(FTS_META|FTS_PHYSICAL);
349 	if (map)
350 	{
351 		if (streq(s, "-"))
352 			sp = sfstdin;
353 		else if (!(sp = sfopen(NiL, s, "r")))
354 			error(ERROR_exit(1), "%s: cannot read", s);
355 		while (s = sfgetr(sp, '\n', 1))
356 		{
357 			getids(s, &t, &key, options);
358 			if (!(m = (Map_t*)dtmatch(map, &key)))
359 			{
360 				if (!(m = (Map_t*)stakalloc(sizeof(Map_t))))
361 					error(ERROR_exit(1), "out of space [id dictionary]");
362 				m->key = key;
363 				m->to.uid = m->to.gid = NOID;
364 				dtinsert(map, m);
365 			}
366 			getids(t, NiL, &m->to, options);
367 		}
368 		if (sp != sfstdin)
369 			sfclose(sp);
370 		keys[1].gid = keys[2].uid = NOID;
371 	}
372 	else if (!(options & (OPT_UID|OPT_GID)))
373 	{
374 		getids(s, NiL, &key, options);
375 		if ((uid = key.uid) != NOID)
376 			options |= OPT_UID;
377 		if ((gid = key.gid) != NOID)
378 			options |= OPT_GID;
379 	}
380 	switch (options & (OPT_UID|OPT_GID))
381 	{
382 	case OPT_UID:
383 		s = ERROR_translate(0, 0, 0, " owner");
384 		break;
385 	case OPT_GID:
386 		s = ERROR_translate(0, 0, 0, " group");
387 		break;
388 	case OPT_UID|OPT_GID:
389 		s = ERROR_translate(0, 0, 0, " owner and group");
390 		break;
391 	default:
392 		s = "";
393 		break;
394 	}
395 	if (!(fts = fts_open(argv + 1, flags, NiL)))
396 		error(ERROR_system(1), "%s: not found", argv[1]);
397 	while (!sh_checksig(context) && (ent = fts_read(fts)))
398 		switch (ent->fts_info)
399 		{
400 		case FTS_SL:
401 		case FTS_SLNONE:
402 			if (options & OPT_LCHOWN)
403 			{
404 #if _lib_lchown
405 				chownf = lchown;
406 				op = "lchown";
407 				goto commit;
408 #else
409 				if (!(options & OPT_FORCE))
410 				{
411 					errno = ENOSYS;
412 					error(ERROR_system(0), "%s: cannot change symlink owner/group", ent->fts_path);
413 				}
414 #endif
415 			}
416 			break;
417 		case FTS_F:
418 		case FTS_D:
419 		anyway:
420 			chownf = chown;
421 			op = "chown";
422 		commit:
423 			if ((unsigned long)ent->fts_statp->st_ctime >= before)
424 				break;
425 			if (map)
426 			{
427 				options &= ~(OPT_UID|OPT_GID);
428 				uid = gid = NOID;
429 				keys[0].uid = keys[1].uid = ent->fts_statp->st_uid;
430 				keys[0].gid = keys[2].gid = ent->fts_statp->st_gid;
431 				i = 0;
432 				do
433 				{
434 					if (m = (Map_t*)dtmatch(map, &keys[i]))
435 					{
436 						if (uid == NOID && m->to.uid != NOID)
437 						{
438 							uid = m->to.uid;
439 							options |= OPT_UID;
440 						}
441 						if (gid == NOID && m->to.gid != NOID)
442 						{
443 							gid = m->to.gid;
444 							options |= OPT_GID;
445 						}
446 					}
447 				} while (++i < elementsof(keys) && (uid == NOID || gid == NOID));
448 			}
449 			else
450 			{
451 				if (!(options & OPT_UID))
452 					uid = ent->fts_statp->st_uid;
453 				if (!(options & OPT_GID))
454 					gid = ent->fts_statp->st_gid;
455 			}
456 			if ((options & OPT_UNMAPPED) && (uid == NOID || gid == NOID))
457 			{
458 				if (uid == NOID && gid == NOID)
459 					error(ERROR_warn(0), "%s: uid and gid not mapped", ent->fts_path);
460 				else if (uid == NOID)
461 					error(ERROR_warn(0), "%s: uid not mapped", ent->fts_path);
462 				else
463 					error(ERROR_warn(0), "%s: gid not mapped", ent->fts_path);
464 			}
465 			if (uid != ent->fts_statp->st_uid && uid != NOID || gid != ent->fts_statp->st_gid && gid != NOID)
466 			{
467 				if (options & (OPT_SHOW|OPT_VERBOSE))
468 				{
469 					if (options & OPT_TEST)
470 					{
471 						ent->fts_statp->st_uid = 0;
472 						ent->fts_statp->st_gid = 0;
473 					}
474 					sfprintf(sfstdout, "%s uid:%05d->%05d gid:%05d->%05d %s\n", op, ent->fts_statp->st_uid, uid, ent->fts_statp->st_gid, gid, ent->fts_path);
475 				}
476 				if (!(options & OPT_SHOW) && (*chownf)(ent->fts_accpath, uid, gid) && !(options & OPT_FORCE))
477 					error(ERROR_system(0), "%s: cannot change%s", ent->fts_path, s);
478 			}
479 			break;
480 		case FTS_DC:
481 			if (!(options & OPT_FORCE))
482 				error(ERROR_warn(0), "%s: directory causes cycle", ent->fts_path);
483 			break;
484 		case FTS_DNR:
485 			if (!(options & OPT_FORCE))
486 				error(ERROR_system(0), "%s: cannot read directory", ent->fts_path);
487 			goto anyway;
488 		case FTS_DNX:
489 			if (!(options & OPT_FORCE))
490 				error(ERROR_system(0), "%s: cannot search directory", ent->fts_path);
491 			goto anyway;
492 		case FTS_NS:
493 			if (!(options & OPT_FORCE))
494 				error(ERROR_system(0), "%s: not found", ent->fts_path);
495 			break;
496 		}
497 	fts_close(fts);
498 	if (map)
499 		dtclose(map);
500 	return error_info.errors != 0;
501 }
502