1 /***********************************************************************
2 *                                                                      *
3 *               This software is part of the ast package               *
4 *           Copyright (c) 1992-2007 AT&T Knowledge Ventures            *
5 *                      and is licensed under the                       *
6 *                  Common Public License, Version 1.0                  *
7 *                      by AT&T Knowledge Ventures                      *
8 *                                                                      *
9 *                A copy of the License is available at                 *
10 *            http://www.opensource.org/licenses/cpl1.0.txt             *
11 *         (with md5 checksum 059e8cd6165cb4c31e351f2b69388fd9)         *
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  * Glenn Fowler
24  * AT&T Research
25  *
26  * sum -- list file checksum and size
27  */
28 
29 static const char usage[] =
30 "[-?\n@(#)$Id: sum (AT&T Research) 2007-02-07 $\n]"
31 USAGE_LICENSE
32 "[+NAME?cksum,md5sum,sum - print file checksum and block count]"
33 "[+DESCRIPTION?\bsum\b lists the checksum, and for most methods the block"
34 "	count, for each file argument. The standard input is read if there are"
35 "	no \afile\a arguments. \bgetconf UNIVERSE\b determines the default"
36 "	\bsum\b method: \batt\b for the \batt\b universe, \bbsd\b otherwise."
37 "	The default for the other commands is the command name itself. The"
38 "	\batt\b method is a true sum, all others are order dependent.]"
39 "[+?Method names consist of a leading identifier and 0 or more options"
40 "	separated by -.]"
41 "[+?\bgetconf PATH_RESOLVE\b determines how symbolic links are handled. This"
42 "	can be explicitly overridden by the \b--logical\b, \b--metaphysical\b,"
43 "	and \b--physical\b options below. \bPATH_RESOLVE\b can be one of:]{"
44 "		[+logical?Follow all symbolic links.]"
45 "		[+metaphysical?Follow command argument symbolic links,"
46 "			otherwise don't follow.]"
47 "		[+physical?Don't follow symbolic links.]"
48 "}"
49 
50 "[a:all?List the checksum for all files. Use with \b--total\b to list both"
51 "	individual and total checksums and block counts.]"
52 "[b:binary?Read files in binary mode. This is the default.]"
53 "[c:check?Each \afile\a is interpreted as the output from a previous \bsum\b."
54 "	If \b--header\b or \b--permissions\b was specified in the previous"
55 "	\bsum\b then the checksum method is automatically determined,"
56 "	otherwise \b--method\b must be specified. The listed checksum is"
57 "	compared with the current value and a warning is issued for each file"
58 "	that does not match. If \afile\a was generated by \b--permissions\b"
59 "	then the file mode, user and group are also checked. Empty lines,"
60 "	lines starting with \b#<space>\b, or the line \b#\b are ignored. Lines"
61 "	containing no blanks are interpreted as [no]]\aname\a[=\avalue\a]]"
62 "	options:]{"
63 "		[+method=name?Checksum method to apply to subsequent lines.]"
64 "		[+permissions?Subsequent lines were generated with"
65 "			\b--permissions\b.]"
66 "}"
67 "[h:header?Print the checksum method as the first output line. Used with"
68 "	\b--check\b and \b--permissions\b.]"
69 "[l:list?Each \afile\a is interpreted as a list of files, one per line,"
70 "	that is checksummed.]"
71 "[p:permissions?If \b--check\b is not specified then list the file"
72 "	mode, user and group between the checksum and path. User and group"
73 "	matching the caller are output as \b-\b. If \b--check\b is"
74 "	specified then the mode, user and group for each path in \afile\a"
75 "	are updated if necessary to match those in \afile\a. A warning is"
76 "	printed on the standard error for each changed file.]"
77 "[r:recursive?Recursively checksum the contents of directories.]"
78 "[s:silent|status?No output for \b--check\b; 0 exit status means all sums"
79 "	matched, non-0 means at least one sum failed to match. Ignored for"
80 "	\b--permissions\b.]"
81 "[t:total?List only the total checksum and block count of all files."
82 "	\b--all\b \b--total\b lists each checksum and the total. The"
83 "	total checksum and block count may be different from the checksum"
84 "	and block count of the catenation of all files due to partial"
85 "	blocks that may occur when the files are treated separately.]"
86 "[T:text?Read files in text mode (i.e., treat \b\\r\\n\b as \b\\n\b).]"
87 "[w!:warn?Warn about invalid \b--check\b lines.]"
88 "[x:method|algorithm?Specifies the checksum \amethod\a to"
89 "	apply. Parenthesized method options are readonly implementation"
90 "	details.]:[method]{\fmethods\f}"
91 "[L:logical|follow?Follow symbolic links when traversing directories. The"
92 "	default is determined by \bgetconf PATH_RESOLVE\b.]"
93 "[H:metaphysical?Follow command argument symbolic links, otherwise don't"
94 "	follow symbolic links when traversing directories. The default is"
95 "	determined by \bgetconf PATH_RESOLVE\b.]"
96 "[P:physical?Don't follow symbolic links when traversing directories. The"
97 "	default is determined by \bgetconf PATH_RESOLVE\b.]"
98 
99 "\n"
100 "\n[ file ... ]\n"
101 "\n"
102 
103 "[+SEE ALSO?\bgetconf\b(1), \btw\b(1), \buuencode\b(1)]"
104 ;
105 
106 #include <cmd.h>
107 #include <sum.h>
108 #include <ls.h>
109 #include <fts.h>
110 #include <error.h>
111 
112 typedef struct State_s			/* program state		*/
113 {
114 	int		all;		/* list all items		*/
115 	Sfio_t*		check;		/* check previous output	*/
116 	gid_t		gid;		/* caller gid			*/
117 	int		header;		/* list method on output	*/
118 	int		list;		/* list file name too		*/
119 	Sum_t*		oldsum;		/* previous sum method		*/
120 	int		permissions;	/* include mode,uer,group	*/
121 	int		haveperm;	/* permissions in the input	*/
122 	int		recursive;	/* recursively descend dirs	*/
123 	unsigned long	size;		/* combined size of all files	*/
124 	int		silent;		/* silent check, 0 exit if ok	*/
125 	int		(*sort)(FTSENT* const*, FTSENT* const*);
126 	Sum_t*		sum;		/* sum method			*/
127 	int		text;		/* \r\n == \n			*/
128 	int		total;		/* list totals only		*/
129 	uid_t		uid;		/* caller uid			*/
130 	int		warn;		/* invalid check line warnings	*/
131 } State_t;
132 
133 static void	verify(State_t*, char*, char*, Sfio_t*);
134 
135 /*
136  * open path for read mode
137  */
138 
139 static Sfio_t*
140 openfile(const char* path, const char* mode)
141 {
142 	Sfio_t*		sp;
143 
144 	if (!path || streq(path, "-") || streq(path, "/dev/stdin") || streq(path, "/dev/fd/0"))
145 	{
146 		sp = sfstdin;
147 		sfopen(sp, NiL, mode);
148 	}
149 	else if (!(sp = sfopen(NiL, path, mode)))
150 		error(ERROR_SYSTEM|2, "%s: cannot read", path);
151 	return sp;
152 }
153 
154 /*
155  * close an openfile() stream
156  */
157 
158 static int
159 closefile(Sfio_t* sp)
160 {
161 	return sp == sfstdin ? 0 : sfclose(sp);
162 }
163 
164 /*
165  * compute and print sum on an open file
166  */
167 
168 static void
169 pr(State_t* state, Sfio_t* op, Sfio_t* ip, char* file, int perm, struct stat* st, Sfio_t* check)
170 {
171 	register char*	p;
172 	register char*	r;
173 	register char*	e;
174 	register int	peek;
175 	struct stat	ss;
176 
177 	if (check)
178 	{
179 		state->oldsum = state->sum;
180 		while (p = sfgetr(ip, '\n', 1))
181 			verify(state, p, file, check);
182 		state->sum = state->oldsum;
183 		if (state->warn && !sfeof(ip))
184 			error(2, "%s: last line incomplete", file);
185 		return;
186 	}
187 	suminit(state->sum);
188 	if (state->text)
189 	{
190 		peek = 0;
191 		while (p = sfreserve(ip, SF_UNBOUND, 0))
192 		{
193 			e = p + sfvalue(ip);
194 			if (peek)
195 			{
196 				peek = 0;
197 				if (*p != '\n')
198 					sumblock(state->sum, "\r", 1);
199 			}
200 			while (r = memchr(p, '\r', e - p))
201 			{
202 				if (++r >= e)
203 				{
204 					e--;
205 					peek = 1;
206 					break;
207 				}
208 				sumblock(state->sum, p, r - p - (*r == '\n'));
209 				p = r;
210 			}
211 			sumblock(state->sum, p, e - p);
212 		}
213 		if (peek)
214 			sumblock(state->sum, "\r", 1);
215 	}
216 	else
217 		while (p = sfreserve(ip, SF_UNBOUND, 0))
218 			sumblock(state->sum, p, sfvalue(ip));
219 	if (sfvalue(ip))
220 		error(ERROR_SYSTEM|2, "%s: read error", file);
221 	sumdone(state->sum);
222 	if (!state->total || state->all)
223 	{
224 		sumprint(state->sum, op, SUM_SIZE|SUM_SCALE);
225 		if (perm >= 0)
226 		{
227 			if (perm)
228 			{
229 				if (!st && fstat(sffileno(ip), st = &ss))
230 					error(ERROR_SYSTEM|2, "%s: cannot stat", file);
231 				else
232 					sfprintf(sfstdout, " %04o %s %s",
233 						modex(st->st_mode & S_IPERM),
234 						(st->st_uid != state->uid && ((st->st_mode & S_ISUID) || (st->st_mode & S_IRUSR) && !(st->st_mode & (S_IRGRP|S_IROTH)) || (st->st_mode & S_IXUSR) && !(st->st_mode & (S_IXGRP|S_IXOTH)))) ? fmtuid(st->st_uid) : "-",
235 						(st->st_gid != state->gid && ((st->st_mode & S_ISGID) || (st->st_mode & S_IRGRP) && !(st->st_mode & S_IROTH) || (st->st_mode & S_IXGRP) && !(st->st_mode & S_IXOTH))) ? fmtgid(st->st_gid) : "-");
236 			}
237 			if (ip != sfstdin)
238 				sfprintf(op, " %s", file);
239 			sfputc(op, '\n');
240 		}
241 	}
242 }
243 
244 /*
245  * verify previous sum output
246  */
247 
248 static void
249 verify(State_t* state, register char* s, char* check, Sfio_t* rp)
250 {
251 	register char*	t;
252 	char*		e;
253 	char*		file;
254 	int		attr;
255 	int		mode;
256 	int		uid;
257 	int		gid;
258 	Sfio_t*		sp;
259 	struct stat	st;
260 
261 	if (!*s || *s == '#' && (!*(s + 1) || *(s + 1) == ' ' || *(s + 1) == '\t'))
262 		return;
263 	if (t = strchr(s, ' '))
264 	{
265 		if ((t - s) > 10 || !(file = strchr(t + 1, ' ')))
266 			file = t;
267 		*file++ = 0;
268 		attr = 0;
269 		if ((mode = strtol(file, &e, 8)) && *e == ' ' && (e - file) == 4)
270 		{
271 			mode = modei(mode);
272 			if (t = strchr(++e, ' '))
273 			{
274 				if (*e == '-' && (t - e) == 1)
275 					uid = -1;
276 				else
277 				{
278 					*t = 0;
279 					uid = struid(e);
280 					*t = ' ';
281 				}
282 				if (e = strchr(++t, ' '))
283 				{
284 					if (*t == '-' && (e - t) == 1)
285 						gid = -1;
286 					else
287 					{
288 						*e = 0;
289 						gid = struid(t);
290 						*e = ' ';
291 					}
292 					file = e + 1;
293 					attr = 1;
294 				}
295 			}
296 		}
297 		if (sp = openfile(file, "rb"))
298 		{
299 			pr(state, rp, sp, file, -1, NiL, NiL);
300 			if (!(t = sfstruse(rp)))
301 				error(ERROR_SYSTEM|3, "out of space");
302 			if (!streq(s, t))
303 			{
304 				if (state->silent)
305 					error_info.errors++;
306 				else
307 					error(2, "%s: checksum changed", file);
308 			}
309 			else if (attr)
310 			{
311 				if (fstat(sffileno(sp), &st))
312 				{
313 					if (state->silent)
314 						error_info.errors++;
315 					else
316 						error(ERROR_SYSTEM|2, "%s: cannot stat", file);
317 				}
318 				else
319 				{
320 					if (uid < 0 || uid == st.st_uid)
321 						uid = -1;
322 					else if (!state->permissions)
323 					{
324 						if (state->silent)
325 							error_info.errors++;
326 						else
327 							error(2, "%s: uid should be %s", file, fmtuid(uid));
328 					}
329 					if (gid < 0 || gid == st.st_gid)
330 						gid = -1;
331 					else if (!state->permissions)
332 					{
333 						if (state->silent)
334 							error_info.errors++;
335 						else
336 							error(2, "%s: gid should be %s", file, fmtgid(gid));
337 					}
338 					if (state->permissions && (uid >= 0 || gid >= 0))
339 					{
340 						if (chown(file, uid, gid) < 0)
341 						{
342 							if (uid < 0)
343 								error(ERROR_SYSTEM|2, "%s: cannot change group to %s", file, fmtgid(gid));
344 							else if (gid < 0)
345 								error(ERROR_SYSTEM|2, "%s: cannot change user to %s", file, fmtuid(uid));
346 							else
347 								error(ERROR_SYSTEM|2, "%s: cannot change user to %s and group to %s", file, fmtuid(uid), fmtgid(gid));
348 						}
349 						else
350 						{
351 							if (uid < 0)
352 								error(1, "%s: changed group to %s", file, fmtgid(gid));
353 							else if (gid < 0)
354 								error(1, "%s: changed user to %s", file, fmtuid(uid));
355 							else
356 								error(1, "%s: changed user to %s and group to %s", file, fmtuid(uid), fmtgid(gid));
357 						}
358 					}
359 					if ((st.st_mode & S_IPERM) ^ mode)
360 					{
361 						if (state->permissions)
362 						{
363 							if (chmod(file, mode) < 0)
364 								error(ERROR_SYSTEM|2, "%s: cannot change mode to %s", file, fmtmode(mode, 0));
365 							else
366 								error(ERROR_SYSTEM|1, "%s: changed mode to %s", file, fmtmode(mode, 0));
367 						}
368 						else if (state->silent)
369 							error_info.errors++;
370 						else
371 							error(2, "%s: mode should be %s", file, fmtmode(mode, 0));
372 					}
373 				}
374 			}
375 			closefile(sp);
376 		}
377 	}
378 	else if (strneq(s, "method=", 7))
379 	{
380 		s += 7;
381 		if (state->sum != state->oldsum)
382 			sumclose(state->sum);
383 		if (!(state->sum = sumopen(s)))
384 			error(3, "%s: %s: unknown checksum method", check, s);
385 	}
386 	else if (streq(s, "permissions"))
387 		state->haveperm = 1;
388 	else
389 		error(1, "%s: %s: unknown option", check, s);
390 }
391 
392 /*
393  * sum the list of files in lp
394  */
395 
396 static void
397 list(State_t* state, register Sfio_t* lp)
398 {
399 	register char*		file;
400 	register Sfio_t*	sp;
401 
402 	while (file = sfgetr(lp, '\n', 1))
403 		if (sp = openfile(file, state->check ? "rt" : "rb"))
404 		{
405 			pr(state, sfstdout, sp, file, state->permissions, NiL, state->check);
406 			closefile(sp);
407 		}
408 }
409 
410 /*
411  * order child entries
412  */
413 
414 static int
415 order(FTSENT* const* f1, FTSENT* const* f2)
416 {
417 	return strcoll((*f1)->fts_name, (*f2)->fts_name);
418 }
419 
420 /*
421  * optget() info discipline function
422  */
423 
424 static int
425 optinfo(Opt_t* op, Sfio_t* sp, const char* s, Optdisc_t* dp)
426 {
427 	if (streq(s, "methods"))
428 		return sumusage(sp);
429 	return 0;
430 }
431 
432 int
433 b_cksum(int argc, register char** argv, void* context)
434 {
435 	register int	flags;
436 	register char*	s;
437 	char*		file;
438 	Sfio_t*		sp;
439 	FTS*		fts;
440 	FTSENT*		ent;
441 	Optdisc_t	optdisc;
442 	State_t		state;
443 
444 	cmdinit(argc, argv, context, ERROR_CATALOG, ERROR_NOTIFY);
445 	memset(&state, 0, sizeof(state));
446 	setlocale(LC_ALL, "");
447 	flags = fts_flags() | FTS_TOP | FTS_NOPOSTORDER | FTS_NOSEEDOTDIR;
448 	state.warn = 1;
449 	optinit(&optdisc, optinfo);
450 	for (;;)
451 	{
452 		switch (optget(argv, usage))
453 		{
454 		case 'a':
455 			state.all = 1;
456 			continue;
457 		case 'b':
458 			state.text = 0;
459 			continue;
460 		case 'c':
461 			if (!(state.check = sfstropen()))
462 				error(3, "out of space [check]");
463 			continue;
464 		case 'h':
465 			state.header = 1;
466 			continue;
467 		case 'l':
468 			state.list = 1;
469 			continue;
470 		case 'p':
471 			state.permissions = 1;
472 			continue;
473 		case 'r':
474 			flags &= ~FTS_TOP;
475 			state.recursive = 1;
476 			state.sort = order;
477 			continue;
478 		case 's':
479 			state.silent = opt_info.num;
480 			continue;
481 		case 't':
482 			state.total = 1;
483 			continue;
484 		case 'w':
485 			state.warn = opt_info.num;
486 			continue;
487 		case 'x':
488 			if (!(state.sum = sumopen(opt_info.arg)))
489 				error(3, "%s: unknown checksum method", opt_info.arg);
490 			continue;
491 		case 'H':
492 			flags |= FTS_META|FTS_PHYSICAL;
493 			continue;
494 		case 'L':
495 			flags &= ~(FTS_META|FTS_PHYSICAL);
496 			continue;
497 		case 'P':
498 			flags &= ~FTS_META;
499 			flags |= FTS_PHYSICAL;
500 			continue;
501 		case 'T':
502 			state.text = 1;
503 			continue;
504 		case '?':
505 			error(ERROR_USAGE|4, "%s", opt_info.arg);
506 			break;
507 		case ':':
508 			error(2, "%s", opt_info.arg);
509 			break;
510 		}
511 		break;
512 	}
513 	argv += opt_info.index;
514 	if (error_info.errors)
515 		error(ERROR_USAGE|4, "%s", optusage(NiL));
516 
517 	/*
518 	 * check the method
519 	 */
520 
521 	if (!state.sum && !(state.sum = sumopen(error_info.id)) && !(state.sum = sumopen(astconf("UNIVERSE", NiL, NiL))))
522 		state.sum = sumopen(NiL);
523 
524 	/*
525 	 * do it
526 	 */
527 
528 	if (state.permissions)
529 	{
530 		state.uid = geteuid();
531 		state.gid = getegid();
532 		state.silent = 0;
533 	}
534 	if (!state.check && (state.header || state.permissions))
535 	{
536 		sfprintf(sfstdout, "method=%s\n", state.sum->name);
537 		if (state.permissions)
538 			sfprintf(sfstdout, "permissions\n");
539 	}
540 	if (state.list)
541 	{
542 		if (*argv)
543 		{
544 			while (file = *argv++)
545 				if (sp = openfile(file, "rt"))
546 				{
547 					list(&state, sp);
548 					closefile(sp);
549 				}
550 		}
551 		else if (sp = openfile(NiL, "rt"))
552 		{
553 			list(&state, sp);
554 			closefile(sp);
555 		}
556 	}
557 	else if (!*argv && !state.recursive)
558 		pr(&state, sfstdout, sfstdin, "/dev/stdin", state.permissions, NiL, state.check);
559 	else if (!(fts = fts_open(argv, flags, state.sort)))
560 		error(ERROR_system(1), "%s: not found", *argv);
561 	else
562 	{
563 		while (!cmdquit() && (ent = fts_read(fts)))
564 			switch (ent->fts_info)
565 			{
566 			case FTS_SL:
567 				if (!(flags & FTS_PHYSICAL) || (flags & FTS_META) && ent->fts_level == 1)
568 					fts_set(NiL, ent, FTS_FOLLOW);
569 				break;
570 			case FTS_F:
571 				if (sp = openfile(ent->fts_accpath, "rb"))
572 				{
573 					pr(&state, sfstdout, sp, ent->fts_path, state.permissions, ent->fts_statp, state.check);
574 					closefile(sp);
575 				}
576 				break;
577 			case FTS_DC:
578 				error(ERROR_warn(0), "%s: directory causes cycle", ent->fts_accpath);
579 				break;
580 			case FTS_DNR:
581 				error(ERROR_system(0), "%s: cannot read directory", ent->fts_accpath);
582 				break;
583 			case FTS_DNX:
584 				error(ERROR_system(0), "%s: cannot search directory", ent->fts_accpath);
585 				break;
586 			case FTS_NS:
587 				error(ERROR_system(0), "%s: not found", ent->fts_accpath);
588 				break;
589 			}
590 		fts_close(fts);
591 	}
592 	if (state.total)
593 	{
594 		sumprint(state.sum, sfstdout, SUM_TOTAL|SUM_SIZE|SUM_SCALE);
595 		sfputc(sfstdout, '\n');
596 	}
597 	sumclose(state.sum);
598 	return error_info.errors != 0;
599 }
600