sbase

suckless unix tools
git clone git://git.suckless.org/sbase
Log | Files | Refs | README | LICENSE

tar.c (14905B)


      1 /* See LICENSE file for copyright and license details. */
      2 #include <sys/stat.h>
      3 #include <sys/time.h>
      4 #include <sys/types.h>
      5 #ifndef major
      6 #include <sys/sysmacros.h>
      7 #endif
      8 
      9 #include <assert.h>
     10 #include <errno.h>
     11 #include <fcntl.h>
     12 #include <grp.h>
     13 #include <libgen.h>
     14 #include <pwd.h>
     15 #include <stdio.h>
     16 #include <stdlib.h>
     17 #include <string.h>
     18 #include <unistd.h>
     19 
     20 #include "fs.h"
     21 #include "util.h"
     22 
     23 #define BLKSIZ (sizeof (struct header)) /* must equal 512 bytes */
     24 
     25 enum Type {
     26 	REG       = '0',
     27 	AREG      = '\0',
     28 	HARDLINK  = '1',
     29 	SYMLINK   = '2',
     30 	CHARDEV   = '3',
     31 	BLOCKDEV  = '4',
     32 	DIRECTORY = '5',
     33 	FIFO      = '6',
     34 	RESERVED  = '7'
     35 };
     36 
     37 struct header {
     38 	char name[100];
     39 	char mode[8];
     40 	char uid[8];
     41 	char gid[8];
     42 	char size[12];
     43 	char mtime[12];
     44 	char chksum[8];
     45 	char type;
     46 	char linkname[100];
     47 	char magic[6];
     48 	char version[2];
     49 	char uname[32];
     50 	char gname[32];
     51 	char major[8];
     52 	char minor[8];
     53 	char prefix[155];
     54 	char padding[12];
     55 };
     56 
     57 static struct dirtime {
     58 	char *name;
     59 	time_t mtime;
     60 } *dirtimes;
     61 
     62 static size_t dirtimeslen;
     63 
     64 static int tarfd;
     65 static ino_t tarinode;
     66 static dev_t tardev;
     67 
     68 static int mflag, vflag;
     69 static int filtermode;
     70 static const char *filtertool;
     71 
     72 static const char *filtertools[] = {
     73 	['J'] = "xz",
     74 	['Z'] = "compress",
     75 	['a'] = "lzma",
     76 	['j'] = "bzip2",
     77 	['z'] = "gzip",
     78 };
     79 
     80 static void
     81 pushdirtime(char *name, time_t mtime)
     82 {
     83 	dirtimes = ereallocarray(dirtimes, dirtimeslen + 1, sizeof(*dirtimes));
     84 	dirtimes[dirtimeslen].name = estrdup(name);
     85 	dirtimes[dirtimeslen].mtime = mtime;
     86 	dirtimeslen++;
     87 }
     88 
     89 static struct dirtime *
     90 popdirtime(void)
     91 {
     92 	if (dirtimeslen) {
     93 		dirtimeslen--;
     94 		return &dirtimes[dirtimeslen];
     95 	}
     96 	return NULL;
     97 }
     98 
     99 static int
    100 comp(int fd, const char *tool, const char *flags)
    101 {
    102 	int fds[2];
    103 
    104 	if (pipe(fds) < 0)
    105 		eprintf("pipe:");
    106 
    107 	switch (fork()) {
    108 	case -1:
    109 		eprintf("fork:");
    110 	case 0:
    111 		dup2(fd, 1);
    112 		dup2(fds[0], 0);
    113 		close(fds[0]);
    114 		close(fds[1]);
    115 
    116 		execlp(tool, tool, flags, NULL);
    117 		weprintf("execlp %s:", tool);
    118 		_exit(1);
    119 	}
    120 	close(fds[0]);
    121 	return fds[1];
    122 }
    123 
    124 static int
    125 decomp(int fd, const char *tool, const char *flags)
    126 {
    127 	int fds[2];
    128 
    129 	if (pipe(fds) < 0)
    130 		eprintf("pipe:");
    131 
    132 	switch (fork()) {
    133 	case -1:
    134 		eprintf("fork:");
    135 	case 0:
    136 		dup2(fd, 0);
    137 		dup2(fds[1], 1);
    138 		close(fds[0]);
    139 		close(fds[1]);
    140 
    141 		execlp(tool, tool, flags, NULL);
    142 		weprintf("execlp %s:", tool);
    143 		_exit(1);
    144 	}
    145 	close(fds[1]);
    146 	return fds[0];
    147 }
    148 
    149 static ssize_t
    150 eread(int fd, void *buf, size_t n)
    151 {
    152 	ssize_t r;
    153 
    154 again:
    155 	r = read(fd, buf, n);
    156 	if (r < 0) {
    157 		if (errno == EINTR)
    158 			goto again;
    159 		eprintf("read:");
    160 	}
    161 	return r;
    162 }
    163 
    164 static ssize_t
    165 ewrite(int fd, const void *buf, size_t n)
    166 {
    167 	ssize_t r;
    168 
    169 	if ((r = write(fd, buf, n)) != n)
    170 		eprintf("write:");
    171 	return r;
    172 }
    173 
    174 static unsigned
    175 chksum(struct header *h)
    176 {
    177 	unsigned sum, i;
    178 
    179 	memset(h->chksum, ' ', sizeof(h->chksum));
    180 	for (i = 0, sum = 0, assert(BLKSIZ == 512); i < BLKSIZ; i++)
    181 		sum += *((unsigned char *)h + i);
    182 	return sum;
    183 }
    184 
    185 static void
    186 putoctal(char *dst, unsigned num, int size)
    187 {
    188 	if (snprintf(dst, size, "%.*o", size - 1, num) >= size)
    189 		eprintf("putoctal: input number '%o' too large\n", num);
    190 }
    191 
    192 static int
    193 archive(const char *path)
    194 {
    195 	static const struct header blank = {
    196 		"././@LongLink", "0000600", "0000000", "0000000", "00000000000",
    197 		"00000000000"  , "       ",  AREG    , ""       , "ustar", "00",
    198 	};
    199 	char   b[BLKSIZ + BLKSIZ], *p;
    200 	struct header *h = (struct header *)b;
    201 	struct group  *gr;
    202 	struct passwd *pw;
    203 	struct stat st;
    204 	ssize_t l, n, r;
    205 	int fd = -1;
    206 
    207 	if (lstat(path, &st) < 0) {
    208 		weprintf("lstat %s:", path);
    209 		return 0;
    210 	} else if (st.st_ino == tarinode && st.st_dev == tardev) {
    211 		weprintf("ignoring %s\n", path);
    212 		return 0;
    213 	}
    214 	pw = getpwuid(st.st_uid);
    215 	gr = getgrgid(st.st_gid);
    216 
    217 	*h = blank;
    218 	n  = strlcpy(h->name, path, sizeof(h->name));
    219 	if (n >= sizeof(h->name)) {
    220 		*++h = blank;
    221 		h->type = 'L';
    222 		putoctal(h->size,   n,         sizeof(h->size));
    223 		putoctal(h->chksum, chksum(h), sizeof(h->chksum));
    224 		ewrite(tarfd, (char *)h, BLKSIZ);
    225 
    226 		for (p = (char *)path; n > 0; n -= BLKSIZ, p += BLKSIZ) {
    227 			if (n < BLKSIZ) {
    228 				p = memcpy(h--, p, n);
    229 				memset(p + n, 0, BLKSIZ - n);
    230 			}
    231 			ewrite(tarfd, p, BLKSIZ);
    232 		}
    233 	}
    234 
    235 	putoctal(h->mode,    (unsigned)st.st_mode & 0777, sizeof(h->mode));
    236 	putoctal(h->uid,     (unsigned)st.st_uid,         sizeof(h->uid));
    237 	putoctal(h->gid,     (unsigned)st.st_gid,         sizeof(h->gid));
    238 	putoctal(h->mtime,   (unsigned)st.st_mtime,       sizeof(h->mtime));
    239 	estrlcpy(h->uname,   pw ? pw->pw_name : "",       sizeof(h->uname));
    240 	estrlcpy(h->gname,   gr ? gr->gr_name : "",       sizeof(h->gname));
    241 
    242 	if (S_ISREG(st.st_mode)) {
    243 		h->type = REG;
    244 		putoctal(h->size, st.st_size,  sizeof(h->size));
    245 		fd = open(path, O_RDONLY);
    246 		if (fd < 0)
    247 			eprintf("open %s:", path);
    248 	} else if (S_ISDIR(st.st_mode)) {
    249 		h->type = DIRECTORY;
    250 	} else if (S_ISLNK(st.st_mode)) {
    251 		h->type = SYMLINK;
    252 		if ((r = readlink(path, h->linkname, sizeof(h->linkname) - 1)) < 0)
    253 			eprintf("readlink %s:", path);
    254 		h->linkname[r] = '\0';
    255 	} else if (S_ISCHR(st.st_mode) || S_ISBLK(st.st_mode)) {
    256 		h->type = S_ISCHR(st.st_mode) ? CHARDEV : BLOCKDEV;
    257 		putoctal(h->major, (unsigned)major(st.st_dev), sizeof(h->major));
    258 		putoctal(h->minor, (unsigned)minor(st.st_dev), sizeof(h->minor));
    259 	} else if (S_ISFIFO(st.st_mode)) {
    260 		h->type = FIFO;
    261 	}
    262 
    263 	putoctal(h->chksum, chksum(h), sizeof(h->chksum));
    264 	ewrite(tarfd, b, BLKSIZ);
    265 
    266 	if (fd != -1) {
    267 		while ((l = eread(fd, b, BLKSIZ)) > 0) {
    268 			if (l < BLKSIZ)
    269 				memset(b + l, 0, BLKSIZ - l);
    270 			ewrite(tarfd, b, BLKSIZ);
    271 		}
    272 		close(fd);
    273 	}
    274 
    275 	return 0;
    276 }
    277 
    278 static int
    279 unarchive(char *fname, ssize_t l, char b[BLKSIZ])
    280 {
    281 	struct header *h = (struct header *)b;
    282 	struct timespec times[2];
    283 	char lname[101], *tmp, *p;
    284 	long mode, major, minor, type, mtime, uid, gid;
    285 	int  fd = -1, lnk = h->type == SYMLINK;
    286 
    287 	if (!mflag && ((mtime = strtol(h->mtime, &p, 8)) < 0 || *p != '\0'))
    288 		eprintf("strtol %s: invalid mtime\n", h->mtime);
    289 	if (strcmp(fname, ".") && strcmp(fname, "./") && remove(fname) < 0)
    290 		if (errno != ENOENT) weprintf("remove %s:", fname);
    291 
    292 	tmp = estrdup(fname);
    293 	mkdirp(dirname(tmp), 0777, 0777);
    294 	free(tmp);
    295 
    296 	switch (h->type) {
    297 	case REG:
    298 	case AREG:
    299 	case RESERVED:
    300 		if ((mode = strtol(h->mode, &p, 8)) < 0 || *p != '\0')
    301 			eprintf("strtol %s: invalid mode\n", h->mode);
    302 		fd = open(fname, O_WRONLY | O_TRUNC | O_CREAT, 0600);
    303 		if (fd < 0)
    304 			eprintf("open %s:", fname);
    305 		break;
    306 	case HARDLINK:
    307 	case SYMLINK:
    308 		snprintf(lname, sizeof(lname), "%.*s", (int)sizeof(h->linkname),
    309 		         h->linkname);
    310 		if ((lnk ? symlink:link)(lname, fname) < 0)
    311 			eprintf("%s %s -> %s:", lnk ? "symlink":"link", fname, lname);
    312 		lnk++;
    313 		break;
    314 	case DIRECTORY:
    315 		if ((mode = strtol(h->mode, &p, 8)) < 0 || *p != '\0')
    316 			eprintf("strtol %s: invalid mode\n", h->mode);
    317 		if (mkdir(fname, (mode_t)mode) < 0 && errno != EEXIST)
    318 			eprintf("mkdir %s:", fname);
    319 		pushdirtime(fname, mtime);
    320 		break;
    321 	case CHARDEV:
    322 	case BLOCKDEV:
    323 		if ((mode = strtol(h->mode, &p, 8)) < 0 || *p != '\0')
    324 			eprintf("strtol %s: invalid mode\n", h->mode);
    325 		if ((major = strtol(h->major, &p, 8)) < 0 || *p != '\0')
    326 			eprintf("strtol %s: invalid major device\n", h->major);
    327 		if ((minor = strtol(h->minor, &p, 8)) < 0 || *p != '\0')
    328 			eprintf("strtol %s: invalid minor device\n", h->minor);
    329 		type = (h->type == CHARDEV) ? S_IFCHR : S_IFBLK;
    330 		if (mknod(fname, type | mode, makedev(major, minor)) < 0)
    331 			eprintf("mknod %s:", fname);
    332 		break;
    333 	case FIFO:
    334 		if ((mode = strtol(h->mode, &p, 8)) < 0 || *p != '\0')
    335 			eprintf("strtol %s: invalid mode\n", h->mode);
    336 		if (mknod(fname, S_IFIFO | mode, 0) < 0)
    337 			eprintf("mknod %s:", fname);
    338 		break;
    339 	default:
    340 		eprintf("unsupported tar-filetype %c\n", h->type);
    341 	}
    342 
    343 	if ((uid = strtol(h->uid, &p, 8)) < 0 || *p != '\0')
    344 		eprintf("strtol %s: invalid uid\n", h->uid);
    345 	if ((gid = strtol(h->gid, &p, 8)) < 0 || *p != '\0')
    346 		eprintf("strtol %s: invalid gid\n", h->gid);
    347 
    348 	if (fd != -1) {
    349 		for (; l > 0; l -= BLKSIZ)
    350 			if (eread(tarfd, b, BLKSIZ) > 0)
    351 				ewrite(fd, b, MIN(l, BLKSIZ));
    352 		close(fd);
    353 	}
    354 
    355 	if (lnk == 1)
    356 		return 0;
    357 
    358 	times[0].tv_sec = times[1].tv_sec = mtime;
    359 	times[0].tv_nsec = times[1].tv_nsec = 0;
    360 	if (!mflag && utimensat(AT_FDCWD, fname, times, AT_SYMLINK_NOFOLLOW) < 0)
    361 		weprintf("utimensat %s:", fname);
    362 	if (lnk) {
    363 		if (!getuid() && lchown(fname, uid, gid))
    364 			weprintf("lchown %s:", fname);
    365 	} else {
    366 		if (!getuid() && chown(fname, uid, gid))
    367 			weprintf("chown %s:", fname);
    368 		if (chmod(fname, mode) < 0)
    369 			eprintf("fchmod %s:", fname);
    370 	}
    371 
    372 	return 0;
    373 }
    374 
    375 static void
    376 skipblk(ssize_t l)
    377 {
    378 	char b[BLKSIZ];
    379 
    380 	for (; l > 0; l -= BLKSIZ)
    381 		if (!eread(tarfd, b, BLKSIZ))
    382 			break;
    383 }
    384 
    385 static int
    386 print(char *fname, ssize_t l, char b[BLKSIZ])
    387 {
    388 	puts(fname);
    389 	skipblk(l);
    390 	return 0;
    391 }
    392 
    393 static void
    394 c(int dirfd, const char *name, struct stat *st, void *data, struct recursor *r)
    395 {
    396 	archive(r->path);
    397 	if (vflag)
    398 		puts(r->path);
    399 
    400 	if (S_ISDIR(st->st_mode))
    401 		recurse(dirfd, name, NULL, r);
    402 }
    403 
    404 static void
    405 sanitize(struct header *h)
    406 {
    407 	size_t i, j, l;
    408 	struct {
    409 		char  *f;
    410 		size_t l;
    411 	} fields[] = {
    412 		{ h->mode,   sizeof(h->mode)   },
    413 		{ h->uid,    sizeof(h->uid)    },
    414 		{ h->gid,    sizeof(h->gid)    },
    415 		{ h->size,   sizeof(h->size)   },
    416 		{ h->mtime,  sizeof(h->mtime)  },
    417 		{ h->chksum, sizeof(h->chksum) },
    418 		{ h->major,  sizeof(h->major)  },
    419 		{ h->minor,  sizeof(h->minor)  }
    420 	};
    421 
    422 	/* Numeric fields can be terminated with spaces instead of
    423 	 * NULs as per the ustar specification.  Patch all of them to
    424 	 * use NULs so we can perform string operations on them. */
    425 	for (i = 0; i < LEN(fields); i++){
    426 		j = 0, l = fields[i].l - 1;
    427 		for (; j < l && fields[i].f[j] == ' '; j++);
    428 		for (; j <= l; j++)
    429 			if (fields[i].f[j] == ' ')
    430 				fields[i].f[j] = '\0';
    431 		if (fields[i].f[l])
    432 			eprintf("numeric field #%d (%.*s) is not null or space terminated\n",
    433 			        i, l+1, fields[i].f);
    434 	}
    435 }
    436 
    437 static void
    438 chktar(struct header *h)
    439 {
    440 	const char *reason;
    441 	char tmp[sizeof h->chksum], *err;
    442 	long sum, i;
    443 
    444 	if (h->prefix[0] == '\0' && h->name[0] == '\0') {
    445 		reason = "empty filename";
    446 		goto bad;
    447 	}
    448 	if (h->magic[0] && strncmp("ustar", h->magic, 5)) {
    449 		reason = "not ustar format";
    450 		goto bad;
    451 	}
    452 	memcpy(tmp, h->chksum, sizeof(tmp));
    453 	for (i = sizeof(tmp)-1; i > 0 && tmp[i] == ' '; i--) {
    454 		tmp[i] = '\0';
    455 	}
    456 	sum = strtol(tmp, &err, 8);
    457 	if (sum < 0 || sum >= BLKSIZ*256 || *err != '\0') {
    458 		reason = "invalid checksum";
    459 		goto bad;
    460 	}
    461 	if (sum != chksum(h)) {
    462 		reason = "incorrect checksum";
    463 		goto bad;
    464 	}
    465 	memcpy(h->chksum, tmp, sizeof(tmp));
    466 	return;
    467 bad:
    468 	eprintf("malformed tar archive: %s\n", reason);
    469 }
    470 
    471 static void
    472 xt(int argc, char *argv[], int mode)
    473 {
    474 	long size, l;
    475 	char b[BLKSIZ], fname[l = PATH_MAX + 1], *p, *q = NULL;
    476 	int i, m, n;
    477 	int (*fn)(char *, ssize_t, char[BLKSIZ]) = (mode == 'x') ? unarchive : print;
    478 	struct timespec times[2];
    479 	struct header *h = (struct header *)b;
    480 	struct dirtime *dirtime;
    481 
    482 	while (eread(tarfd, b, BLKSIZ) > 0 && (h->name[0] || h->prefix[0])) {
    483 		chktar(h);
    484 		sanitize(h);
    485 
    486 		if ((size = strtol(h->size, &p, 8)) < 0 || *p != '\0')
    487 			eprintf("strtol %s: invalid size\n", h->size);
    488 
    489 		/* Long file path is read directly into fname*/
    490 		if (h->type == 'L' || h->type == 'x' || h->type == 'g') {
    491 
    492 			/* Read header only up to size of fname buffer */
    493 			for (q = fname; q < fname+size; q += BLKSIZ) {
    494 				if (q + BLKSIZ >= fname + l)
    495 					eprintf("name exceeds buffer: %.*s\n", q-fname, fname);
    496 				eread(tarfd, q, BLKSIZ);
    497 			}
    498 
    499 			/* Convert pax x header with 'path=' field into L header */
    500 			if (h->type == 'x') for (q = fname; q < fname+size-16; q += n) {
    501 				if ((n = strtol(q, &p, 10)) < 0 || *p != ' ')
    502 					eprintf("strtol %.*s: invalid number\n", p+1-q, q);
    503 				if (n && strncmp(p+1, "path=", 5) == 0) {
    504 					memmove(fname, p+6, size = q+n - p-6 - 1);
    505 					h->type = 'L';
    506 					break;
    507 				}
    508 			}
    509 			fname[size] = '\0';
    510 
    511 			/* Non L-like header (eg. pax 'g') is skipped by setting q=null */
    512 			if (h->type != 'L')
    513 				q = NULL;
    514 			continue;
    515 		}
    516 
    517 		/* Ustar path is copied into fname if no L header (ie: q is NULL) */
    518 		if (!q) {
    519 			m = sizeof h->prefix, n = sizeof h->name;
    520 			p = "/" + !h->prefix[0];
    521 			snprintf(fname, l, "%.*s%s%.*s", m, h->prefix, p, n, h->name);
    522 		}
    523 		q = NULL;
    524 
    525 		/* If argc > 0 then only extract the given files/dirs */
    526 		if (argc) {
    527 			for (i = 0; i < argc; i++) {
    528 				if (strncmp(argv[i], fname, n = strlen(argv[i])) == 0)
    529 					if (strchr("/", fname[n]) || argv[i][n-1] == '/')
    530 						break;
    531 			}
    532 			if (i == argc) {
    533 				skipblk(size);
    534 				continue;
    535 			}
    536 		}
    537 
    538 		fn(fname, size, b);
    539 		if (vflag && mode != 't')
    540 			puts(fname);
    541 	}
    542 
    543 	if (mode == 'x' && !mflag) {
    544 		while ((dirtime = popdirtime())) {
    545 			times[0].tv_sec = times[1].tv_sec = dirtime->mtime;
    546 			times[0].tv_nsec = times[1].tv_nsec = 0;
    547 			if (utimensat(AT_FDCWD, dirtime->name, times, 0) < 0)
    548 				eprintf("utimensat %s:", fname);
    549 			free(dirtime->name);
    550 		}
    551 		free(dirtimes);
    552 		dirtimes = NULL;
    553 	}
    554 }
    555 
    556 char **args;
    557 int argn;
    558 
    559 static void
    560 usage(void)
    561 {
    562 	eprintf("usage: %s [x | t | -x | -t] [-C dir] [-J | -Z | -a | -j | -z] [-m] [-p] "
    563 	        "[-f file] [file ...]\n"
    564 	        "       %s [c | -c] [-C dir] [-J | -Z | -a | -j | -z] [-h] path ... "
    565 	        "[-f file]\n", argv0, argv0);
    566 }
    567 
    568 int
    569 main(int argc, char *argv[])
    570 {
    571 	struct recursor r = { .fn = c, .follow = 'P', .flags = DIRFIRST };
    572 	struct stat st;
    573 	char *file = NULL, *dir = ".", mode = '\0';
    574 	int fd;
    575 
    576 	argv0 = argv[0];
    577 	if (argc > 1 && strchr("cxt", mode = *argv[1]))
    578 		*(argv[1]+1) ? *argv[1] = '-' : (*++argv = argv0, --argc);
    579 
    580 	ARGBEGIN {
    581 	case 'x':
    582 	case 'c':
    583 	case 't':
    584 		mode = ARGC();
    585 		break;
    586 	case 'C':
    587 		dir = EARGF(usage());
    588 		break;
    589 	case 'f':
    590 		file = EARGF(usage());
    591 		break;
    592 	case 'm':
    593 		mflag = 1;
    594 		break;
    595 	case 'J':
    596 	case 'Z':
    597 	case 'a':
    598 	case 'j':
    599 	case 'z':
    600 		filtermode = ARGC();
    601 		filtertool = filtertools[filtermode];
    602 		break;
    603 	case 'h':
    604 		r.follow = 'L';
    605 		break;
    606 	case 'v':
    607 		vflag = 1;
    608 		break;
    609 	case 'p':
    610 		break;  /* Do nothing as already default behaviour */
    611 	default:
    612 		usage();
    613 	} ARGEND
    614 
    615 	switch (mode) {
    616 	case 'c':
    617 		if (!argc)
    618 			usage();
    619 		tarfd = 1;
    620 		if (file && *file != '-') {
    621 			tarfd = open(file, O_WRONLY | O_TRUNC | O_CREAT, 0644);
    622 			if (tarfd < 0)
    623 				eprintf("open %s:", file);
    624 			if (lstat(file, &st) < 0)
    625 				eprintf("lstat %s:", file);
    626 			tarinode = st.st_ino;
    627 			tardev = st.st_dev;
    628 		}
    629 
    630 		if (filtertool)
    631 			tarfd = comp(tarfd, filtertool, "-cf");
    632 
    633 		if (chdir(dir) < 0)
    634 			eprintf("chdir %s:", dir);
    635 		for (; *argv; argc--, argv++)
    636 			recurse(AT_FDCWD, *argv, NULL, &r);
    637 		break;
    638 	case 't':
    639 	case 'x':
    640 		tarfd = 0;
    641 		if (file && *file != '-') {
    642 			tarfd = open(file, O_RDONLY);
    643 			if (tarfd < 0)
    644 				eprintf("open %s:", file);
    645 		}
    646 
    647 		if (filtertool) {
    648 			fd = tarfd;
    649 			tarfd = decomp(tarfd, filtertool, "-cdf");
    650 			close(fd);
    651 		}
    652 
    653 		if (chdir(dir) < 0)
    654 			eprintf("chdir %s:", dir);
    655 		xt(argc, argv, mode);
    656 		break;
    657 	default:
    658 		usage();
    659 	}
    660 
    661 	return recurse_status;
    662 }