xref: /openbsd/sbin/ncheck_ffs/ncheck_ffs.c (revision 17df1aa7)
1 /*	$OpenBSD: ncheck_ffs.c,v 1.35 2009/10/27 23:59:33 deraadt Exp $	*/
2 
3 /*-
4  * Copyright (c) 1995, 1996 SigmaSoft, Th. Lockert <tholo@sigmasoft.com>
5  * All rights reserved.
6  *
7  * Redistribution and use in source and binary forms, with or without
8  * modification, are permitted provided that the following conditions
9  * are met:
10  * 1. Redistributions of source code must retain the above copyright
11  *    notice, this list of conditions and the following disclaimer.
12  * 2. Redistributions in binary form must reproduce the above copyright
13  *    notice, this list of conditions and the following disclaimer in the
14  *    documentation and/or other materials provided with the distribution.
15  *
16  * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
17  * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
18  * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
19  * THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
20  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
21  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
22  * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
23  * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
24  * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
25  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26  */
27 
28 /*-
29  * Copyright (c) 1980, 1988, 1991, 1993
30  *	The Regents of the University of California.  All rights reserved.
31  *
32  * Redistribution and use in source and binary forms, with or without
33  * modification, are permitted provided that the following conditions
34  * are met:
35  * 1. Redistributions of source code must retain the above copyright
36  *    notice, this list of conditions and the following disclaimer.
37  * 2. Redistributions in binary form must reproduce the above copyright
38  *    notice, this list of conditions and the following disclaimer in the
39  *    documentation and/or other materials provided with the distribution.
40  * 3. Neither the name of the University nor the names of its contributors
41  *    may be used to endorse or promote products derived from this software
42  *    without specific prior written permission.
43  *
44  * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
45  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
46  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
47  * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
48  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
49  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
50  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
51  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
52  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
53  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
54  * SUCH DAMAGE.
55  */
56 
57 #include <sys/param.h>
58 #include <sys/time.h>
59 #include <sys/stat.h>
60 #include <ufs/ffs/fs.h>
61 #include <ufs/ufs/dir.h>
62 #include <ufs/ufs/dinode.h>
63 
64 #include <stdio.h>
65 #include <stdlib.h>
66 #include <fcntl.h>
67 #include <string.h>
68 #include <ctype.h>
69 #include <unistd.h>
70 #include <fstab.h>
71 #include <errno.h>
72 #include <err.h>
73 
74 #define DIP(dp, field) \
75     ((sblock->fs_magic == FS_UFS1_MAGIC) ? \
76     ((struct ufs1_dinode *)(dp))->field : \
77     ((struct ufs2_dinode *)(dp))->field)
78 
79 char	*disk;		/* name of the disk file */
80 char	rdisk[PATH_MAX];/* resolved name of the disk file */
81 int	diskfd;		/* disk file descriptor */
82 struct	fs *sblock;	/* the file system super block */
83 char	sblock_buf[MAXBSIZE];
84 int	sblock_try[] = SBLOCKSEARCH; /* possible superblock locations */
85 long	dev_bsize;	/* block size of underlying disk device */
86 int	dev_bshift;	/* log2(dev_bsize) */
87 ino_t	*ilist;		/* list of inodes to check */
88 int	ninodes;	/* number of inodes in list */
89 int	sflag;		/* only suid and special files */
90 int	aflag;		/* print the . and .. entries too */
91 int	mflag;		/* verbose output */
92 int	iflag;		/* specific inode */
93 char	*format;	/* output format */
94 
95 struct icache_s {
96 	ino_t		ino;
97 	union {
98 		struct ufs1_dinode dp1;
99 		struct ufs2_dinode dp2;
100 	} di;
101 } *icache;
102 int	nicache;
103 
104 void addinode(ino_t inum);
105 void *getino(ino_t inum);
106 void findinodes(ino_t);
107 void bread(daddr64_t, char *, int);
108 __dead void usage(void);
109 void scanonedir(ino_t, const char *);
110 void dirindir(ino_t, daddr64_t, int, off_t, const char *);
111 void searchdir(ino_t, daddr64_t, long, off_t, const char *);
112 int matchino(const void *, const void *);
113 int matchcache(const void *, const void *);
114 void cacheino(ino_t, void *);
115 void *cached(ino_t);
116 int main(int, char *[]);
117 char *rawname(char *);
118 void format_entry(const char *, struct direct *);
119 
120 /*
121  * Check to see if the indicated inodes are the same
122  */
123 int
124 matchino(const void *key, const void *val)
125 {
126 	ino_t k = *(ino_t *)key;
127 	ino_t v = *(ino_t *)val;
128 
129 	if (k < v)
130 		return -1;
131 	else if (k > v)
132 		return 1;
133 	return 0;
134 }
135 
136 /*
137  * Check if the indicated inode match the entry in the cache
138  */
139 int
140 matchcache(const void *key, const void *val)
141 {
142 	ino_t		ino = *(ino_t *)key;
143 	struct icache_s	*ic = (struct icache_s *)val;
144 
145 	if (ino < ic->ino)
146 		return -1;
147 	else if (ino > ic->ino)
148 		return 1;
149 	return 0;
150 }
151 
152 /*
153  * Add an inode to the cached entries
154  */
155 void
156 cacheino(ino_t ino, void *dp)
157 {
158 	struct icache_s *newicache;
159 
160 	newicache = realloc(icache, (nicache + 1) * sizeof(struct icache_s));
161 	if (newicache == NULL) {
162 		if (icache)
163 			free(icache);
164 		icache = NULL;
165 		errx(1, "malloc");
166 	}
167 	icache = newicache;
168 	icache[nicache].ino = ino;
169 	if (sblock->fs_magic == FS_UFS1_MAGIC)
170 		icache[nicache++].di.dp1 = *(struct ufs1_dinode *)dp;
171 	else
172 		icache[nicache++].di.dp2 = *(struct ufs2_dinode *)dp;
173 }
174 
175 /*
176  * Get a cached inode
177  */
178 void *
179 cached(ino_t ino)
180 {
181 	struct icache_s *ic;
182 	void *dp = NULL;
183 
184 	ic = (struct icache_s *)bsearch(&ino, icache, nicache,
185 	    sizeof(struct icache_s), matchcache);
186 	if (ic != NULL) {
187 		if (sblock->fs_magic == FS_UFS1_MAGIC)
188 			dp = &ic->di.dp1;
189 		else
190 			dp = &ic->di.dp2;
191 	}
192 	return (dp);
193 }
194 
195 /*
196  * Walk the inode list for a filesystem to find all allocated inodes
197  * Remember inodes we want to give information about and cache all
198  * inodes pointing to directories
199  */
200 void
201 findinodes(ino_t maxino)
202 {
203 	ino_t ino;
204 	void *dp;
205 	mode_t mode;
206 
207 	for (ino = ROOTINO; ino < maxino; ino++) {
208 		dp = getino(ino);
209 		mode = DIP(dp, di_mode) & IFMT;
210 		if (!mode)
211 			continue;
212 		if (mode == IFDIR)
213 			cacheino(ino, dp);
214 		if (iflag ||
215 		    (sflag && (mode == IFDIR ||
216 		     ((DIP(dp, di_mode) & (ISGID | ISUID)) == 0 &&
217 		      (mode == IFREG || mode == IFLNK)))))
218 			continue;
219 		addinode(ino);
220 	}
221 }
222 
223 /*
224  * Get a specified inode from disk.  Attempt to minimize reads to once
225  * per cylinder group
226  */
227 void *
228 getino(ino_t inum)
229 {
230 	static char *itab = NULL;
231 	static daddr64_t iblk = -1;
232 	void *dp;
233 	size_t dsize;
234 
235 	if (inum < ROOTINO || inum >= sblock->fs_ncg * sblock->fs_ipg)
236 		return NULL;
237 	if ((dp = cached(inum)) != NULL)
238 		return dp;
239 	if (sblock->fs_magic == FS_UFS1_MAGIC)
240 		dsize = sizeof(struct ufs1_dinode);
241 	else
242 		dsize = sizeof(struct ufs2_dinode);
243 	if ((inum / sblock->fs_ipg) != iblk || itab == NULL) {
244 		iblk = inum / sblock->fs_ipg;
245 		if (itab == NULL &&
246 		    (itab = calloc(sblock->fs_ipg, dsize)) == NULL)
247 			errx(1, "no memory for inodes");
248 		bread(fsbtodb(sblock, cgimin(sblock, iblk)), itab,
249 		      sblock->fs_ipg * dsize);
250 	}
251 	return itab + (inum % sblock->fs_ipg) * dsize;
252 }
253 
254 /*
255  * Read a chunk of data from the disk.
256  * Try to recover from hard errors by reading in sector sized pieces.
257  * Error recovery is attempted at most BREADEMAX times before seeking
258  * consent from the operator to continue.
259  */
260 int	breaderrors = 0;
261 #define	BREADEMAX 32
262 
263 void
264 bread(daddr64_t blkno, char *buf, int size)
265 {
266 	int cnt, i;
267 
268 loop:
269 	if (lseek(diskfd, ((off_t)blkno << dev_bshift), SEEK_SET) < 0)
270 		warnx("bread: lseek fails");
271 	if ((cnt = read(diskfd, buf, size)) == size)
272 		return;
273 	if (blkno + (size / dev_bsize) > fsbtodb(sblock, sblock->fs_ffs1_size)) {
274 		/*
275 		 * Trying to read the final fragment.
276 		 *
277 		 * NB - dump only works in TP_BSIZE blocks, hence
278 		 * rounds `dev_bsize' fragments up to TP_BSIZE pieces.
279 		 * It should be smarter about not actually trying to
280 		 * read more than it can get, but for the time being
281 		 * we punt and scale back the read only when it gets
282 		 * us into trouble. (mkm 9/25/83)
283 		 */
284 		size -= dev_bsize;
285 		goto loop;
286 	}
287 	if (cnt == -1)
288 		warnx("read error from %s: %s: [block %lld]: count=%d",
289 		    disk, strerror(errno), (long long)blkno, size);
290 	else
291 		warnx("short read error from %s: [block %lld]: count=%d, got=%d",
292 		    disk, (long long)blkno, size, cnt);
293 	if (++breaderrors > BREADEMAX)
294 		errx(1, "More than %d block read errors from %s", BREADEMAX, disk);
295 	/*
296 	 * Zero buffer, then try to read each sector of buffer separately.
297 	 */
298 	memset(buf, 0, size);
299 	for (i = 0; i < size; i += dev_bsize, buf += dev_bsize, blkno++) {
300 		if (lseek(diskfd, ((off_t)blkno << dev_bshift), SEEK_SET) < 0)
301 			warnx("bread: lseek2 fails!");
302 		if ((cnt = read(diskfd, buf, (int)dev_bsize)) == dev_bsize)
303 			continue;
304 		if (cnt == -1) {
305 			warnx("read error from %s: %s: [sector %lld]: count=%ld",
306 			    disk, strerror(errno), (long long)blkno, dev_bsize);
307 			continue;
308 		}
309 		warnx("short read error from %s: [sector %lld]: count=%ld, got=%d",
310 		    disk, (long long)blkno, dev_bsize, cnt);
311 	}
312 }
313 
314 /*
315  * Add an inode to the in-memory list of inodes to dump
316  */
317 void
318 addinode(ino_t ino)
319 {
320 	ino_t *newilist;
321 
322 	newilist = realloc(ilist, sizeof(ino_t) * (ninodes + 1));
323 	if (newilist == NULL) {
324 		if (ilist)
325 			free(ilist);
326 		ilist = NULL;
327 		errx(4, "not enough memory to allocate tables");
328 	}
329 	ilist = newilist;
330 	ilist[ninodes] = ino;
331 	ninodes++;
332 }
333 
334 /*
335  * Scan the directory pointer at by ino
336  */
337 void
338 scanonedir(ino_t ino, const char *path)
339 {
340 	void *dp;
341 	off_t filesize;
342 	int i;
343 
344 	if ((dp = cached(ino)) == NULL)
345 		return;
346 	filesize = (off_t)DIP(dp, di_size);
347 	for (i = 0; filesize > 0 && i < NDADDR; i++) {
348 		if (DIP(dp, di_db[i]) != 0) {
349 			searchdir(ino, DIP(dp, di_db[i]),
350 			    sblksize(sblock, DIP(dp, di_size), i),
351 			    filesize, path);
352 		}
353 		filesize -= sblock->fs_bsize;
354 	}
355 	for (i = 0; filesize > 0 && i < NIADDR; i++) {
356 		if (DIP(dp, di_ib[i]))
357 			dirindir(ino, DIP(dp, di_ib[i]), i, filesize, path);
358 	}
359 }
360 
361 /*
362  * Read indirect blocks, and pass the data blocks to be searched
363  * as directories. Quit as soon as any entry is found that will
364  * require the directory to be dumped.
365  */
366 void
367 dirindir(ino_t ino, daddr64_t blkno, int ind_level, off_t filesize,
368     const char *path)
369 {
370 	int i;
371 	static void *idblk;
372 
373 	if (idblk == NULL && (idblk = malloc(sblock->fs_bsize)) == NULL)
374 		errx(1, "dirindir: cannot allocate indirect memory.\n");
375 	bread(fsbtodb(sblock, blkno), idblk, (int)sblock->fs_bsize);
376 	if (ind_level <= 0) {
377 		for (i = 0; filesize > 0 && i < NINDIR(sblock); i++) {
378 			if (sblock->fs_magic == FS_UFS1_MAGIC)
379 				blkno = ((int32_t *)idblk)[i];
380 			else
381 				blkno = ((int64_t *)idblk)[i];
382 			if (blkno != 0)
383 				searchdir(ino, blkno, sblock->fs_bsize,
384 				    filesize, path);
385 		}
386 		return;
387 	}
388 	ind_level--;
389 	for (i = 0; filesize > 0 && i < NINDIR(sblock); i++) {
390 		if (sblock->fs_magic == FS_UFS1_MAGIC)
391 			blkno = ((int32_t *)idblk)[i];
392 		else
393 			blkno = ((int64_t *)idblk)[i];
394 		if (blkno != 0)
395 			dirindir(ino, blkno, ind_level, filesize, path);
396 	}
397 }
398 
399 /*
400  * Scan a disk block containing directory information looking to see if
401  * any of the entries are on the dump list and to see if the directory
402  * contains any subdirectories.
403  */
404 void
405 searchdir(ino_t ino, daddr64_t blkno, long size, off_t filesize,
406     const char *path)
407 {
408 	char *dblk;
409 	struct direct *dp;
410 	void *di;
411 	mode_t mode;
412 	char *npath;
413 	long loc;
414 
415 	if ((dblk = malloc(sblock->fs_bsize)) == NULL)
416 		errx(1, "searchdir: cannot allocate indirect memory.");
417 	bread(fsbtodb(sblock, blkno), dblk, (int)size);
418 	if (filesize < size)
419 		size = filesize;
420 	for (loc = 0; loc < size; ) {
421 		dp = (struct direct *)(dblk + loc);
422 		if (dp->d_reclen == 0) {
423 			warnx("corrupted directory, inode %u", ino);
424 			break;
425 		}
426 		loc += dp->d_reclen;
427 		if (dp->d_ino == 0)
428 			continue;
429 		if (dp->d_name[0] == '.') {
430 			if (!aflag && (dp->d_name[1] == '\0' ||
431 			    (dp->d_name[1] == '.' && dp->d_name[2] == '\0')))
432 				continue;
433 		}
434 		di = getino(dp->d_ino);
435 		mode = DIP(di, di_mode) & IFMT;
436 		if (bsearch(&dp->d_ino, ilist, ninodes, sizeof(*ilist), matchino)) {
437 			if (format) {
438 				format_entry(path, dp);
439 			} else {
440 				if (mflag)
441 					printf("mode %-6o uid %-5u gid %-5u ino ",
442 					    DIP(di, di_mode), DIP(di, di_uid),
443 					    DIP(di, di_gid));
444 				printf("%-7u %s/%s%s\n", dp->d_ino, path,
445 				    dp->d_name, mode == IFDIR ? "/." : "");
446 			}
447 		}
448 		if (mode == IFDIR) {
449 			if (dp->d_name[0] == '.') {
450 				if (dp->d_name[1] == '\0' ||
451 				    (dp->d_name[1] == '.' && dp->d_name[2] == '\0'))
452 				continue;
453 			}
454 			if (asprintf(&npath, "%s/%s", path, dp->d_name) == -1)
455 				errx(1, "malloc");
456 			scanonedir(dp->d_ino, npath);
457 			free(npath);
458 		}
459 	}
460 }
461 
462 char *
463 rawname(char *name)
464 {
465 	static char newname[MAXPATHLEN];
466 	char *p;
467 
468 	if ((p = strrchr(name, '/')) == NULL)
469 		return name;
470 	*p = '\0';
471 	strlcpy(newname, name, sizeof newname - 2);
472 	*p++ = '/';
473 	strlcat(newname, "/r", sizeof newname);
474 	strlcat(newname, p, sizeof newname);
475 	return(newname);
476 }
477 
478 __dead void
479 usage(void)
480 {
481 	extern char *__progname;
482 
483 	fprintf(stderr,
484 	    "usage: %s [-ams] [-f format] [-i number ...] filesystem\n",
485 	    __progname);
486 	exit(3);
487 }
488 
489 int
490 main(int argc, char *argv[])
491 {
492 	struct stat stblock;
493 	struct fstab *fsp;
494 	unsigned long ulval;
495 	ssize_t n;
496 	char *ep, *odisk;
497 	int c, i;
498 
499 	while ((c = getopt(argc, argv, "af:i:ms")) != -1)
500 		switch (c) {
501 		case 'a':
502 			aflag++;
503 			break;
504 		case 'i':
505 			iflag++;
506 
507 			errno = 0;
508 			ulval = strtoul(optarg, &ep, 10);
509 			if (optarg[0] == '\0' || *ep != '\0')
510 				errx(1, "%s is not a number",
511 				    optarg);
512 			if (errno == ERANGE && ulval == ULONG_MAX)
513 				errx(1, "%s is out or range",
514 				    optarg);
515 			addinode((ino_t)ulval);
516 
517 			while (optind < argc) {
518 				errno = 0;
519 				ulval = strtoul(argv[optind], &ep, 10);
520 				if (argv[optind][0] == '\0' || *ep != '\0')
521 					break;
522 				if (errno == ERANGE && ulval == ULONG_MAX)
523 					errx(1, "%s is out or range",
524 					    argv[optind]);
525 				addinode((ino_t)ulval);
526 				optind++;
527 			}
528 			break;
529 		case 'f':
530 			format = optarg;
531 			break;
532 		case 'm':
533 			mflag++;
534 			break;
535 		case 's':
536 			sflag++;
537 			break;
538 		default:
539 			usage();
540 			exit(2);
541 		}
542 	if (optind != argc - 1 || (mflag && format))
543 		usage();
544 
545 	odisk = argv[optind];
546 	if (realpath(odisk, rdisk) == NULL)
547 		err(1, "cannot find real path for %s", odisk);
548 	disk = rdisk;
549 
550 	if (stat(disk, &stblock) < 0)
551 		err(1, "cannot stat %s", disk);
552 
553         if (S_ISBLK(stblock.st_mode)) {
554 		disk = rawname(disk);
555 	} else if (!S_ISCHR(stblock.st_mode)) {
556 		if ((fsp = getfsfile(disk)) == NULL)
557 			err(1, "could not find file system %s", disk);
558                 disk = rawname(fsp->fs_spec);
559         }
560 
561 	if ((diskfd = open(disk, O_RDONLY)) < 0)
562 		err(1, "cannot open %s", disk);
563 	sblock = (struct fs *)sblock_buf;
564 	for (i = 0; sblock_try[i] != -1; i++) {
565 		n = pread(diskfd, sblock, SBLOCKSIZE, (off_t)sblock_try[i]);
566 		if (n == SBLOCKSIZE && (sblock->fs_magic == FS_UFS1_MAGIC ||
567 		     (sblock->fs_magic == FS_UFS2_MAGIC &&
568 		      sblock->fs_sblockloc == sblock_try[i])) &&
569 		    sblock->fs_bsize <= MAXBSIZE &&
570 		    sblock->fs_bsize >= sizeof(struct fs))
571 			break;
572 	}
573 	if (sblock_try[i] == -1)
574 		errx(1, "cannot find filesystem superblock");
575 
576 	dev_bsize = sblock->fs_fsize / fsbtodb(sblock, 1);
577 	dev_bshift = ffs(dev_bsize) - 1;
578 	if (dev_bsize != (1 << dev_bshift))
579 		errx(2, "blocksize (%ld) not a power of 2", dev_bsize);
580 	findinodes(sblock->fs_ipg * sblock->fs_ncg);
581 	if (!format)
582 		printf("%s:\n", disk);
583 	scanonedir(ROOTINO, "");
584 	close(diskfd);
585 	exit (0);
586 }
587 
588 void
589 format_entry(const char *path, struct direct *dp)
590 {
591 	static size_t size;
592 	static char *buf;
593 	size_t nsize;
594 	char *src, *dst, *newbuf;
595 	int len;
596 
597 	if (buf == NULL) {
598 		if ((buf = malloc(LINE_MAX)) == NULL)
599 			err(1, "malloc");
600 		size = LINE_MAX;
601 	}
602 
603 	for (src = format, dst = buf; *src; src++) {
604 		/* Need room for at least one character in buf. */
605 		if (size <= dst - buf) {
606 		    expand_buf:
607 			nsize = size << 1;
608 
609 			if ((newbuf = realloc(buf, nsize)) == NULL)
610 				err(1, "realloc");
611 			buf = newbuf;
612 			size = nsize;
613 		}
614 		if (src[0] =='\\') {
615 			switch (src[1]) {
616 			case 'I':
617 				len = snprintf(dst, size - (dst - buf), "%u",
618 				    dp->d_ino);
619 				if (len == -1 || len >= size - (dst - buf))
620 					goto expand_buf;
621 				dst += len;
622 				break;
623 			case 'P':
624 				len = snprintf(dst, size - (dst - buf), "%s/%s",
625 				    path, dp->d_name);
626 				if (len == -1 || len >= size - (dst - buf))
627 					goto expand_buf;
628 				dst += len;
629 				break;
630 			case '\\':
631 				*dst++ = '\\';
632 				break;
633 			case '0':
634 				/* XXX - support other octal numbers? */
635 				*dst++ = '\0';
636 				break;
637 			case 'a':
638 				*dst++ = '\a';
639 				break;
640 			case 'b':
641 				*dst++ = '\b';
642 				break;
643 			case 'e':
644 				*dst++ = '\e';
645 				break;
646 			case 'f':
647 				*dst++ = '\f';
648 				break;
649 			case 'n':
650 				*dst++ = '\n';
651 				break;
652 			case 'r':
653 				*dst++ = '\r';
654 				break;
655 			case 't':
656 				*dst++ = '\t';
657 				break;
658 			case 'v':
659 				*dst++ = '\v';
660 				break;
661 			default:
662 				*dst++ = src[1];
663 				break;
664 			}
665 			src++;
666 		} else
667 			*dst++ = *src;
668 	}
669 	fwrite(buf, dst - buf, 1, stdout);
670 }
671