xref: /illumos-gate/usr/src/cmd/bart/compare.c (revision 8a2b682e57a046b828f37bcde1776f131ef4629f)
1 /*
2  * CDDL HEADER START
3  *
4  * The contents of this file are subject to the terms of the
5  * Common Development and Distribution License, Version 1.0 only
6  * (the "License").  You may not use this file except in compliance
7  * with the License.
8  *
9  * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
10  * or http://www.opensolaris.org/os/licensing.
11  * See the License for the specific language governing permissions
12  * and limitations under the License.
13  *
14  * When distributing Covered Code, include this CDDL HEADER in each
15  * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
16  * If applicable, add the following below this CDDL HEADER, with the
17  * fields enclosed by brackets "[]" replaced with your own identifying
18  * information: Portions Copyright [yyyy] [name of copyright owner]
19  *
20  * CDDL HEADER END
21  */
22 /*
23  * Copyright 2003 Sun Microsystems, Inc.  All rights reserved.
24  * Use is subject to license terms.
25  */
26 
27 #include <unistd.h>
28 #include "bart.h"
29 
30 static int compare_manifests(FILE *rulesfile, char *control, char *test,
31     boolean_t prog_fmt, uint_t flags);
32 static void extract_fname_ftype(char *line, char *fname, char *type);
33 static int report_add(char *fname, char *type);
34 static int report_delete(char *fname, char *type);
35 static int evaluate_differences(char *control_line, char *test_line,
36     boolean_t prog_fmt, int flags);
37 static void report_error(char *fname, char *type, char *ctrl_val,
38     char *test_val, boolean_t prog_fmt);
39 static int read_manifest_line(FILE *fd, char *buf, int buf_size, int start_pos,
40     char **line, char *fname);
41 static void parse_line(char *line, char *fname, char *type, char *size,
42     char *mode, char *acl, char *mtime, char *uid, char *gid, char *contents,
43     char *devnode, char *dest);
44 static void init_default_flags(uint_t *flags);
45 static void get_token(char *line, int *curr_pos, int line_len, char *buf,
46     int buf_size);
47 
48 int
49 bart_compare(int argc, char **argv)
50 {
51 	char			*control_fname, *test_fname;
52 	int			c;
53 	FILE			*rules_fd = NULL;
54 	uint_t			glob_flags;
55 	boolean_t		prog_fmt = B_FALSE;
56 
57 	init_default_flags(&glob_flags);
58 
59 	while ((c = getopt(argc, argv, "pr:i:")) != EOF) {
60 		switch (c) {
61 		case 'p':
62 			prog_fmt = B_TRUE;
63 			break;
64 
65 		case 'r':
66 			if (optarg == NULL)
67 				usage();
68 
69 			if (strcmp(optarg, "-") == 0)
70 				rules_fd = stdin;
71 			else
72 				rules_fd = fopen(optarg, "r");
73 			if (rules_fd == NULL) {
74 				perror(optarg);
75 				usage();
76 			}
77 			break;
78 
79 		case 'i':
80 			process_glob_ignores(optarg, &glob_flags);
81 			break;
82 
83 		case '?':
84 		default:
85 			usage();
86 		}
87 	}
88 
89 	/* Make sure we have the right number of args */
90 	if ((optind + 2) != argc)
91 		usage();
92 	argv += optind;
93 	control_fname = argv[0];
94 	test_fname = argv[1];
95 	/* At this point, the filenames are sane, so do the comparison */
96 	return (compare_manifests(rules_fd, control_fname, test_fname,
97 	    prog_fmt, glob_flags));
98 }
99 
100 static int
101 compare_manifests(FILE *rulesfile, char *control, char *test,
102     boolean_t prog_fmt, uint_t flags)
103 {
104 	FILE	*control_fd, *test_fd;
105 	char	*control_line, *test_line, control_buf[BUF_SIZE],
106 	    test_buf[BUF_SIZE], control_fname[PATH_MAX],
107 	    control_type[TYPE_SIZE], test_fname[PATH_MAX],
108 	    test_type[TYPE_SIZE];
109 	int	control_pos, test_pos, ret, fname_cmp, return_status;
110 
111 	return_status = EXIT;
112 
113 	return_status = read_rules(rulesfile, "", flags, 0);
114 
115 	control_fd = fopen(control, "r");
116 	if (control_fd == NULL) {
117 		perror(control);
118 		return (FATAL_EXIT);
119 	}
120 
121 	test_fd = fopen(test, "r");
122 	if (test_fd == NULL) {
123 		perror(test);
124 		return (FATAL_EXIT);
125 	}
126 
127 	control_pos = read_manifest_line(control_fd, control_buf,
128 	    BUF_SIZE, 0, &control_line, control);
129 	test_pos = read_manifest_line(test_fd, test_buf, BUF_SIZE, 0,
130 	    &test_line, test);
131 
132 	while ((control_pos != -1) && (test_pos != -1)) {
133 		ret = strcmp(control_line, test_line);
134 		if (ret == 0) {
135 			/* Lines compare OK, just read the next lines.... */
136 			control_pos = read_manifest_line(control_fd,
137 			    control_buf, BUF_SIZE, control_pos, &control_line,
138 			    control);
139 			test_pos = read_manifest_line(test_fd, test_buf,
140 			    BUF_SIZE, test_pos, &test_line, test);
141 			continue;
142 		}
143 
144 		/*
145 		 * Something didn't compare properly.
146 		 */
147 		extract_fname_ftype(control_line, control_fname, control_type);
148 		extract_fname_ftype(test_line, test_fname, test_type);
149 		fname_cmp = strcmp(control_fname, test_fname);
150 
151 		if (fname_cmp == 0) {
152 			/*
153 			 * Filenames were the same, see what was
154 			 * different and continue.
155 			 */
156 			if (evaluate_differences(control_line, test_line,
157 			    prog_fmt, flags) != 0)
158 				return_status = WARNING_EXIT;
159 
160 			control_pos = read_manifest_line(control_fd,
161 			    control_buf, BUF_SIZE, control_pos, &control_line,
162 			    control);
163 			test_pos = read_manifest_line(test_fd, test_buf,
164 			    BUF_SIZE, test_pos, &test_line, test);
165 		} else if (fname_cmp > 0) {
166 			/* Filenames were different, a files was ADDED */
167 			if (report_add(test_fname, test_type)) {
168 				report_error(test_fname, ADD_KEYWORD, NULL,
169 				    NULL, prog_fmt);
170 				return_status = WARNING_EXIT;
171 			}
172 			test_pos = read_manifest_line(test_fd, test_buf,
173 			    BUF_SIZE, test_pos, &test_line, test);
174 		} else if (fname_cmp < 0) {
175 			/* Filenames were different, a files was DELETED */
176 			if (report_delete(control_fname, control_type)) {
177 				report_error(control_fname, DELETE_KEYWORD,
178 				    NULL, NULL, prog_fmt);
179 				return_status = WARNING_EXIT;
180 			}
181 			control_pos = read_manifest_line(control_fd,
182 			    control_buf, BUF_SIZE, control_pos, &control_line,
183 			    control);
184 		}
185 	}
186 
187 	/*
188 	 * Entering this while loop means files were DELETED from the test
189 	 * manifest.
190 	 */
191 	while (control_pos != -1) {
192 		(void) sscanf(control_line, "%1023s", control_fname);
193 		if (report_delete(control_fname, control_type)) {
194 			report_error(control_fname, DELETE_KEYWORD, NULL,
195 			    NULL, prog_fmt);
196 			return_status = WARNING_EXIT;
197 		}
198 		control_pos = read_manifest_line(control_fd, control_buf,
199 		    BUF_SIZE, control_pos, &control_line, control);
200 	}
201 
202 	/*
203 	 * Entering this while loop means files were ADDED to the test
204 	 * manifest.
205 	 */
206 	while (test_pos != -1) {
207 		(void) sscanf(test_line, "%1023s", test_fname);
208 		if (report_add(test_fname, test_type)) {
209 			report_error(test_fname, ADD_KEYWORD, NULL,
210 			    NULL, prog_fmt);
211 			return_status = WARNING_EXIT;
212 		}
213 		test_pos = read_manifest_line(test_fd, test_buf,
214 		    BUF_SIZE, test_pos, &test_line, test);
215 	}
216 
217 	(void) fclose(control_fd);
218 	(void) fclose(test_fd);
219 
220 	/* For programmatic mode, add a newline for cosmetic reasons */
221 	if (prog_fmt && (return_status != 0))
222 		(void) printf("\n");
223 
224 	return (return_status);
225 }
226 
227 static void
228 parse_line(char *line, char *fname, char *type, char *size, char *mode,
229     char *acl, char *mtime, char *uid, char *gid, char *contents, char *devnode,
230     char *dest)
231 {
232 	int		pos, line_len;
233 
234 	line_len = strlen(line);
235 	pos = 0;
236 
237 	get_token(line, &pos, line_len, fname, PATH_MAX);
238 	get_token(line, &pos, line_len, type, TYPE_SIZE);
239 	get_token(line, &pos, line_len, size, MISC_SIZE);
240 	get_token(line, &pos, line_len, mode, MISC_SIZE);
241 	get_token(line, &pos, line_len, acl, ACL_SIZE);
242 	get_token(line, &pos, line_len, mtime, MISC_SIZE);
243 	get_token(line, &pos, line_len, uid, MISC_SIZE);
244 	get_token(line, &pos, line_len, gid, MISC_SIZE);
245 
246 	/* Reset these fields... */
247 
248 	*contents = '\0';
249 	*devnode = '\0';
250 	*dest = '\0';
251 
252 	/* Handle filetypes which have a last field..... */
253 	if (type[0] == 'F')
254 		get_token(line, &pos, line_len, contents, PATH_MAX);
255 	else if ((type[0] == 'B') || (type[0] == 'C'))
256 		get_token(line, &pos, line_len, devnode, PATH_MAX);
257 	else if (type[0] == 'L')
258 		get_token(line, &pos, line_len, dest, PATH_MAX);
259 }
260 
261 static void
262 get_token(char *line, int *curr_pos, int line_len, char *buf, int buf_size)
263 {
264 	int	cnt = 0;
265 
266 	while (isspace(line[*curr_pos]) && (*curr_pos < line_len))
267 		(*curr_pos)++;
268 
269 	while (!isspace(line[*curr_pos]) &&
270 	    (*curr_pos < line_len) && (cnt < (buf_size-1))) {
271 		buf[cnt] = line[*curr_pos];
272 		(*curr_pos)++;
273 		cnt++;
274 	}
275 	buf[cnt] = '\0';
276 }
277 
278 /*
279  * Utility function: extract fname and type from this line
280  */
281 static void
282 extract_fname_ftype(char *line, char *fname, char *type)
283 {
284 	int		line_len, pos;
285 
286 	pos = 0;
287 	line_len = strlen(line);
288 
289 	get_token(line, &pos, line_len, fname, PATH_MAX);
290 	get_token(line, &pos, line_len, type, TYPE_SIZE);
291 }
292 
293 /*
294  * Utility function: tells us whether or not this addition should be reported
295  *
296  * Returns 0 if the discrepancy is ignored, non-zero if the discrepancy is
297  * reported.
298  */
299 static int
300 report_add(char *fname, char *type)
301 {
302 	struct rule	*rule_ptr;
303 
304 	rule_ptr = check_rules(fname, type[0]);
305 	if ((rule_ptr != NULL) && (rule_ptr->attr_list & ATTR_ADD))
306 		return (1);
307 	else
308 		return (0);
309 }
310 
311 /*
312  * Utility function: tells us whether or not this deletion should be reported
313  *
314  * Returns 0 if the discrepancy is ignored, non-zero if the discrepancy is
315  * reported.
316  */
317 static int
318 report_delete(char *fname, char *type)
319 {
320 	struct rule	*rule_ptr;
321 
322 	rule_ptr = check_rules(fname, type[0]);
323 
324 	if ((rule_ptr != NULL) && (rule_ptr->attr_list & ATTR_DELETE))
325 		return (1);
326 	else
327 		return (0);
328 }
329 
330 /*
331  * This function takes in the two entries, which have been flagged as
332  * different, breaks them up and reports discrepancies.  Note, discrepancies
333  * are affected by the 'CHECK' and 'IGNORE' stanzas which may apply to
334  * these entries.
335  *
336  * Returns the number of discrepancies reported.
337  */
338 static int
339 evaluate_differences(char *control_line, char *test_line,
340     boolean_t prog_fmt, int flags)
341 {
342 	char		ctrl_fname[PATH_MAX], test_fname[PATH_MAX],
343 	    ctrl_type[TYPE_SIZE], test_type[TYPE_SIZE],
344 	    ctrl_size[MISC_SIZE], ctrl_mode[MISC_SIZE],
345 	    ctrl_acl[ACL_SIZE], ctrl_mtime[MISC_SIZE],
346 	    ctrl_uid[MISC_SIZE], ctrl_gid[MISC_SIZE],
347 	    ctrl_dest[PATH_MAX], ctrl_contents[PATH_MAX],
348 	    ctrl_devnode[PATH_MAX], test_size[MISC_SIZE],
349 	    test_mode[MISC_SIZE], test_acl[ACL_SIZE],
350 	    test_mtime[MISC_SIZE], test_uid[MISC_SIZE],
351 	    test_gid[MISC_SIZE], test_dest[PATH_MAX],
352 	    test_contents[PATH_MAX], test_devnode[PATH_MAX],
353 	    *tag;
354 	int		ret_val;
355 	struct rule	*rule_ptr;
356 
357 	ret_val = 0;
358 
359 	parse_line(control_line, ctrl_fname, ctrl_type, ctrl_size, ctrl_mode,
360 	    ctrl_acl, ctrl_mtime, ctrl_uid, ctrl_gid, ctrl_contents,
361 	    ctrl_devnode, ctrl_dest);
362 
363 	/*
364 	 * Now we know the fname and type, let's get the rule that matches this
365 	 * manifest entry.  If there is a match, make sure to setup the
366 	 * correct reporting flags.
367 	 */
368 	rule_ptr = check_rules(ctrl_fname, ctrl_type[0]);
369 	if (rule_ptr != NULL)
370 		flags = rule_ptr->attr_list;
371 
372 	parse_line(test_line, test_fname, test_type, test_size, test_mode,
373 	    test_acl, test_mtime, test_uid, test_gid, test_contents,
374 	    test_devnode, test_dest);
375 
376 	/*
377 	 * Report the errors based upon which keywords have been set by
378 	 * the user.
379 	 */
380 	if ((flags & ATTR_TYPE) && (ctrl_type[0] != test_type[0])) {
381 		report_error(ctrl_fname, TYPE_KEYWORD, ctrl_type,
382 		    test_type, prog_fmt);
383 		ret_val++;
384 	}
385 
386 	if ((flags & ATTR_SIZE) && (strcmp(ctrl_size, test_size) != 0)) {
387 		report_error(ctrl_fname, SIZE_KEYWORD, ctrl_size,
388 		    test_size, prog_fmt);
389 		ret_val++;
390 	}
391 
392 	if ((flags & ATTR_MODE) && (strcmp(ctrl_mode, test_mode) != 0)) {
393 		report_error(ctrl_fname, MODE_KEYWORD, ctrl_mode,
394 		    test_mode, prog_fmt);
395 		ret_val++;
396 	}
397 
398 	if ((flags & ATTR_ACL) && (strcmp(ctrl_acl, test_acl) != 0)) {
399 		report_error(ctrl_fname, ACL_KEYWORD, ctrl_acl,
400 		    test_acl, prog_fmt);
401 		ret_val++;
402 	}
403 
404 	if ((flags & ATTR_MTIME) && (ctrl_type[0] == test_type[0])) {
405 		if (strcmp(ctrl_mtime, test_mtime) != 0) {
406 			switch (ctrl_type[0]) {
407 			case 'D':
408 				tag = "dirmtime";
409 				break;
410 			case 'L':
411 				tag = "lnmtime";
412 				break;
413 			default:
414 				tag = "mtime";
415 				break;
416 			}
417 			if (flags == 0) {
418 				report_error(ctrl_fname, tag, ctrl_mtime,
419 				    test_mtime, prog_fmt);
420 			ret_val++;
421 		}
422 	}
423 
424 	if ((ctrl_type[0] == 'F') && (flags & ATTR_MTIME) &&
425 	    (strcmp(ctrl_mtime, test_mtime) != 0)) {
426 		report_error(ctrl_fname, MTIME_KEYWORD, ctrl_mtime, test_mtime,
427 		    prog_fmt);
428 		ret_val++;
429 	}
430 
431 	if ((ctrl_type[0] == 'D') && (flags & ATTR_DIRMTIME) &&
432 	    (strcmp(ctrl_mtime, test_mtime) != 0)) {
433 		report_error(ctrl_fname, DIRMTIME_KEYWORD, ctrl_mtime,
434 		    test_mtime, prog_fmt);
435 		ret_val++;
436 	}
437 
438 	if ((ctrl_type[0] == 'L') && (flags & ATTR_LNMTIME) &&
439 	    (strcmp(ctrl_mtime, test_mtime) != 0)) {
440 		report_error(ctrl_fname, LNMTIME_KEYWORD, ctrl_mtime,
441 		    test_mtime, prog_fmt);
442 		ret_val++;
443 	}
444 	} else if ((flags & ATTR_MTIME) &&
445 	    (strcmp(ctrl_mtime, test_mtime) != 0)) {
446 		report_error(ctrl_fname, MTIME_KEYWORD, ctrl_mtime,
447 		    test_mtime, prog_fmt);
448 		ret_val++;
449 	}
450 
451 	if ((flags & ATTR_UID) && (strcmp(ctrl_uid, test_uid) != 0)) {
452 		report_error(ctrl_fname, UID_KEYWORD, ctrl_uid,
453 		    test_uid, prog_fmt);
454 		ret_val++;
455 	}
456 
457 	if ((flags & ATTR_GID) && (strcmp(ctrl_gid, test_gid) != 0)) {
458 		report_error(ctrl_fname, GID_KEYWORD, ctrl_gid,
459 		    test_gid, prog_fmt);
460 		ret_val++;
461 	}
462 
463 	if ((flags & ATTR_DEVNODE) &&
464 	    (strcmp(ctrl_devnode, test_devnode) != 0)) {
465 		report_error(ctrl_fname, DEVNODE_KEYWORD, ctrl_devnode,
466 		    test_devnode, prog_fmt);
467 		ret_val++;
468 	}
469 
470 	if ((flags & ATTR_DEST) && (strcmp(ctrl_dest, test_dest) != 0)) {
471 		report_error(ctrl_fname, DEST_KEYWORD, ctrl_dest,
472 		    test_dest, prog_fmt);
473 		ret_val++;
474 	}
475 
476 	if ((flags & ATTR_CONTENTS) &&
477 	    (strcmp(ctrl_contents, test_contents)) != 0) {
478 		report_error(ctrl_fname, CONTENTS_KEYWORD, ctrl_contents,
479 		    test_contents, prog_fmt);
480 		ret_val++;
481 	}
482 
483 	return (ret_val);
484 }
485 
486 /*
487  * Function responsible for reporting errors.
488  */
489 static void
490 report_error(char *fname, char *type, char *ctrl_val, char *test_val,
491     boolean_t prog_fmt)
492 {
493 	static char	last_fname[PATH_MAX] = "";
494 
495 	if (!prog_fmt) {
496 		/* Verbose mode */
497 		if (strcmp(fname, last_fname) != 0) {
498 			(void) printf("%s:\n", fname);
499 			(void) strlcpy(last_fname, fname, sizeof (last_fname));
500 		}
501 
502 		if (strcmp(type, ADD_KEYWORD) == 0 ||
503 		    strcmp(type, DELETE_KEYWORD) == 0)
504 			(void) printf("  %s\n", type);
505 		else
506 			(void) printf("  %s  control:%s  test:%s\n", type,
507 			    ctrl_val, test_val);
508 	} else {
509 		/* Programmatic mode */
510 		if (strcmp(fname, last_fname) != 0) {
511 			/* Ensure a line is not printed for the initial case */
512 			if (strlen(last_fname) != 0)
513 				(void) printf("\n");
514 			(void) strlcpy(last_fname, fname, sizeof (last_fname));
515 			(void) printf("%s ", fname);
516 		}
517 
518 		(void) printf("%s ", type);
519 		if (strcmp(type, ADD_KEYWORD) != 0 &&
520 		    strcmp(type, DELETE_KEYWORD) != 0) {
521 			(void) printf("%s ", ctrl_val);
522 			(void) printf("%s ", test_val);
523 		}
524 	}
525 }
526 
527 /*
528  * Function responsible for reading in a line from the manifest.
529  * Takes in the file ptr and a buffer, parses the buffer  and sets the 'line'
530  * ptr correctly.  In the case when the buffer is fully parsed, this function
531  * reads more data from the file ptr and refills the buffer.
532  */
533 static int
534 read_manifest_line(FILE *fd, char *buf, int buf_size, int start_pos,
535     char **line, char *fname)
536 {
537 	int	end_pos, len, iscomment = 0, filepos;
538 
539 	/*
540 	 * Initialization case: make sure the manifest version is OK
541 	 */
542 	if (start_pos == 0) {
543 		end_pos = 0;
544 		buf[0] = '\0';
545 		filepos = ftell(fd);
546 		(void) fread((void *) buf, (size_t)buf_size, (size_t)1, fd);
547 
548 		*line = buf;
549 
550 		if (filepos == 0) {
551 			if (strncmp(buf, MANIFEST_VER,
552 			    strlen(MANIFEST_VER)) != 0)
553 				(void) fprintf(stderr, MISSING_VER, fname);
554 			if ((*line[0] == '!') || (*line[0] == '#'))
555 				iscomment++;
556 
557 			while (iscomment) {
558 				while ((buf[end_pos] != '\n') &&
559 				    (buf[end_pos] != '\0') &&
560 				    (end_pos < buf_size))
561 					end_pos++;
562 
563 				if (end_pos >= buf_size)
564 					return (-1);
565 
566 				end_pos++;
567 				*line = &(buf[end_pos]);
568 				iscomment = 0;
569 				if ((*line[0] == '!') || (*line[0] == '#'))
570 					iscomment++;
571 			}
572 		}
573 
574 		while ((buf[end_pos] != '\n') && (buf[end_pos] != '\0') &&
575 		    (end_pos < buf_size))
576 			end_pos++;
577 
578 		if (end_pos < buf_size) {
579 			if (buf[end_pos] == '\n') {
580 				buf[end_pos] = '\0';
581 				return (end_pos);
582 			}
583 
584 			if (buf[end_pos] == '\0')
585 				return (-1);
586 		}
587 
588 		(void) fprintf(stderr, MANIFEST_ERR);
589 		exit(FATAL_EXIT);
590 	}
591 
592 	end_pos = (start_pos+1);
593 	*line = &(buf[end_pos]);
594 
595 	/* Read the buffer until EOL or the buffer is empty */
596 	while ((buf[end_pos] != '\n') && (buf[end_pos] != '\0') &&
597 	    (end_pos < buf_size))
598 		end_pos++;
599 
600 	if (end_pos < buf_size) {
601 		/* Found the end of the line, normal exit */
602 		if (buf[end_pos] == '\n') {
603 			buf[end_pos] = '\0';
604 			return (end_pos);
605 		}
606 
607 		/* No more input to read */
608 		if (buf[end_pos] == '\0')
609 			return (-1);
610 	}
611 
612 	/*
613 	 * The following code takes the remainder of the buffer and
614 	 * puts it at the beginning.  The space after the remainder, which
615 	 * is now at the beginning, is blanked.
616 	 * At this point, read in more data and continue to find the EOL....
617 	 */
618 	len = end_pos - (start_pos + 1);
619 	(void) memcpy(buf, &(buf[start_pos+1]), (size_t)len);
620 	(void) memset(&buf[len], '\0', (buf_size - len));
621 	(void) fread((void *) &buf[len], (size_t)(buf_size-len), (size_t)1, fd);
622 	*line = buf;
623 	end_pos = len;
624 
625 	/* Read the buffer until EOL or the buffer is empty */
626 	while ((buf[end_pos] != '\n') && (buf[end_pos] != '\0') &&
627 	    (end_pos < buf_size))
628 		end_pos++;
629 
630 	if (end_pos < buf_size) {
631 		/* Found the end of the line, normal exit */
632 		if (buf[end_pos] == '\n') {
633 			buf[end_pos] = '\0';
634 			return (end_pos);
635 		}
636 
637 		/* No more input to read */
638 		if (buf[end_pos] == '\0')
639 			return (-1);
640 	}
641 
642 	(void) fprintf(stderr, MANIFEST_ERR);
643 	exit(FATAL_EXIT);
644 
645 	/* NOTREACHED */
646 }
647 
648 static void
649 init_default_flags(uint_t *flags)
650 {
651 	/* Default behavior: everything is checked *except* dirmtime */
652 	*flags = ATTR_ALL & ~(ATTR_DIRMTIME);
653 }
654