xref: /freebsd/contrib/nvi/common/recover.c (revision 6ae1554a5d9b318f8ad53ccc39fa5a961403da73)
1 /*-
2  * Copyright (c) 1993, 1994
3  *	The Regents of the University of California.  All rights reserved.
4  * Copyright (c) 1993, 1994, 1995, 1996
5  *	Keith Bostic.  All rights reserved.
6  *
7  * See the LICENSE file for redistribution information.
8  */
9 
10 #include "config.h"
11 
12 #ifndef lint
13 static const char sccsid[] = "$Id: recover.c,v 11.3 2015/04/04 03:50:42 zy Exp $";
14 #endif /* not lint */
15 
16 #include <sys/types.h>
17 #include <sys/queue.h>
18 #include <sys/stat.h>
19 
20 /*
21  * We include <sys/file.h>, because the open #defines were found there
22  * on historical systems.  We also include <fcntl.h> because the open(2)
23  * #defines are found there on newer systems.
24  */
25 #include <sys/file.h>
26 
27 #include <bitstring.h>
28 #include <dirent.h>
29 #include <errno.h>
30 #include <fcntl.h>
31 #include <limits.h>
32 #include <pwd.h>
33 #include <netinet/in.h>		/* Required by resolv.h. */
34 #include <resolv.h>
35 #include <stdio.h>
36 #include <stdlib.h>
37 #include <string.h>
38 #include <time.h>
39 #include <unistd.h>
40 
41 #include "../ex/version.h"
42 #include "common.h"
43 #include "pathnames.h"
44 
45 /*
46  * Recovery code.
47  *
48  * The basic scheme is as follows.  In the EXF structure, we maintain full
49  * paths of a b+tree file and a mail recovery file.  The former is the file
50  * used as backing store by the DB package.  The latter is the file that
51  * contains an email message to be sent to the user if we crash.  The two
52  * simple states of recovery are:
53  *
54  *	+ first starting the edit session:
55  *		the b+tree file exists and is mode 700, the mail recovery
56  *		file doesn't exist.
57  *	+ after the file has been modified:
58  *		the b+tree file exists and is mode 600, the mail recovery
59  *		file exists, and is exclusively locked.
60  *
61  * In the EXF structure we maintain a file descriptor that is the locked
62  * file descriptor for the mail recovery file.
63  *
64  * To find out if a recovery file/backing file pair are in use, try to get
65  * a lock on the recovery file.
66  *
67  * To find out if a backing file can be deleted at boot time, check for an
68  * owner execute bit.  (Yes, I know it's ugly, but it's either that or put
69  * special stuff into the backing file itself, or correlate the files at
70  * boot time, neither of which looks like fun.)  Note also that there's a
71  * window between when the file is created and the X bit is set.  It's small,
72  * but it's there.  To fix the window, check for 0 length files as well.
73  *
74  * To find out if a file can be recovered, check the F_RCV_ON bit.  Note,
75  * this DOES NOT mean that any initialization has been done, only that we
76  * haven't yet failed at setting up or doing recovery.
77  *
78  * To preserve a recovery file/backing file pair, set the F_RCV_NORM bit.
79  * If that bit is not set when ending a file session:
80  *	If the EXF structure paths (rcv_path and rcv_mpath) are not NULL,
81  *	they are unlink(2)'d, and free(3)'d.
82  *	If the EXF file descriptor (rcv_fd) is not -1, it is closed.
83  *
84  * The backing b+tree file is set up when a file is first edited, so that
85  * the DB package can use it for on-disk caching and/or to snapshot the
86  * file.  When the file is first modified, the mail recovery file is created,
87  * the backing file permissions are updated, the file is sync(2)'d to disk,
88  * and the timer is started.  Then, at RCV_PERIOD second intervals, the
89  * b+tree file is synced to disk.  RCV_PERIOD is measured using SIGALRM, which
90  * means that the data structures (SCR, EXF, the underlying tree structures)
91  * must be consistent when the signal arrives.
92  *
93  * The recovery mail file contains normal mail headers, with two additional
94  *
95  *	X-vi-data: <file|path>;<base64 encoded path>
96  *
97  * MIME headers; the folding character is limited to ' '.
98  *
99  * Btree files are named "vi.XXXXXX" and recovery files are named
100  * "recover.XXXXXX".
101  */
102 
103 #define	VI_DHEADER	"X-vi-data:"
104 
105 static int	 rcv_copy(SCR *, int, char *);
106 static void	 rcv_email(SCR *, char *);
107 static int	 rcv_mailfile(SCR *, int, char *);
108 static int	 rcv_mktemp(SCR *, char *, char *);
109 static int	 rcv_dlnwrite(SCR *, const char *, const char *, FILE *);
110 static int	 rcv_dlnread(SCR *, char **, char **, FILE *);
111 
112 /*
113  * rcv_tmp --
114  *	Build a file name that will be used as the recovery file.
115  *
116  * PUBLIC: int rcv_tmp(SCR *, EXF *, char *);
117  */
118 int
119 rcv_tmp(
120 	SCR *sp,
121 	EXF *ep,
122 	char *name)
123 {
124 	struct stat sb;
125 	int fd;
126 	char *dp, *path;
127 
128 	/*
129 	 * !!!
130 	 * ep MAY NOT BE THE SAME AS sp->ep, DON'T USE THE LATTER.
131 	 *
132 	 *
133 	 * If the recovery directory doesn't exist, try and create it.  As
134 	 * the recovery files are themselves protected from reading/writing
135 	 * by other than the owner, the worst that can happen is that a user
136 	 * would have permission to remove other user's recovery files.  If
137 	 * the sticky bit has the BSD semantics, that too will be impossible.
138 	 */
139 	if (opts_empty(sp, O_RECDIR, 0))
140 		goto err;
141 	dp = O_STR(sp, O_RECDIR);
142 	if (stat(dp, &sb)) {
143 		if (errno != ENOENT || mkdir(dp, 0)) {
144 			msgq(sp, M_SYSERR, "%s", dp);
145 			goto err;
146 		}
147 		(void)chmod(dp, S_IRWXU | S_IRWXG | S_IRWXO | S_ISVTX);
148 	}
149 
150 	if ((path = join(dp, "vi.XXXXXX")) == NULL)
151 		goto err;
152 	if ((fd = rcv_mktemp(sp, path, dp)) == -1) {
153 		free(path);
154 		goto err;
155 	}
156 	(void)fchmod(fd, S_IRWXU);
157 	(void)close(fd);
158 
159 	ep->rcv_path = path;
160 	if (0) {
161 err:		msgq(sp, M_ERR,
162 		    "056|Modifications not recoverable if the session fails");
163 		return (1);
164 	}
165 
166 	/* We believe the file is recoverable. */
167 	F_SET(ep, F_RCV_ON);
168 	return (0);
169 }
170 
171 /*
172  * rcv_init --
173  *	Force the file to be snapshotted for recovery.
174  *
175  * PUBLIC: int rcv_init(SCR *);
176  */
177 int
178 rcv_init(SCR *sp)
179 {
180 	EXF *ep;
181 	recno_t lno;
182 
183 	ep = sp->ep;
184 
185 	/* Only do this once. */
186 	F_CLR(ep, F_FIRSTMODIFY);
187 
188 	/* If we already know the file isn't recoverable, we're done. */
189 	if (!F_ISSET(ep, F_RCV_ON))
190 		return (0);
191 
192 	/* Turn off recoverability until we figure out if this will work. */
193 	F_CLR(ep, F_RCV_ON);
194 
195 	/* Test if we're recovering a file, not editing one. */
196 	if (ep->rcv_mpath == NULL) {
197 		/* Build a file to mail to the user. */
198 		if (rcv_mailfile(sp, 0, NULL))
199 			goto err;
200 
201 		/* Force a read of the entire file. */
202 		if (db_last(sp, &lno))
203 			goto err;
204 
205 		/* Turn on a busy message, and sync it to backing store. */
206 		sp->gp->scr_busy(sp,
207 		    "057|Copying file for recovery...", BUSY_ON);
208 		if (ep->db->sync(ep->db, R_RECNOSYNC)) {
209 			msgq_str(sp, M_SYSERR, ep->rcv_path,
210 			    "058|Preservation failed: %s");
211 			sp->gp->scr_busy(sp, NULL, BUSY_OFF);
212 			goto err;
213 		}
214 		sp->gp->scr_busy(sp, NULL, BUSY_OFF);
215 	}
216 
217 	/* Turn off the owner execute bit. */
218 	(void)chmod(ep->rcv_path, S_IRUSR | S_IWUSR);
219 
220 	/* We believe the file is recoverable. */
221 	F_SET(ep, F_RCV_ON);
222 	return (0);
223 
224 err:	msgq(sp, M_ERR,
225 	    "059|Modifications not recoverable if the session fails");
226 	return (1);
227 }
228 
229 /*
230  * rcv_sync --
231  *	Sync the file, optionally:
232  *		flagging the backup file to be preserved
233  *		snapshotting the backup file and send email to the user
234  *		sending email to the user if the file was modified
235  *		ending the file session
236  *
237  * PUBLIC: int rcv_sync(SCR *, u_int);
238  */
239 int
240 rcv_sync(
241 	SCR *sp,
242 	u_int flags)
243 {
244 	EXF *ep;
245 	int fd, rval;
246 	char *dp, *buf;
247 
248 	/* Make sure that there's something to recover/sync. */
249 	ep = sp->ep;
250 	if (ep == NULL || !F_ISSET(ep, F_RCV_ON))
251 		return (0);
252 
253 	/* Sync the file if it's been modified. */
254 	if (F_ISSET(ep, F_MODIFIED)) {
255 		if (ep->db->sync(ep->db, R_RECNOSYNC)) {
256 			F_CLR(ep, F_RCV_ON | F_RCV_NORM);
257 			msgq_str(sp, M_SYSERR,
258 			    ep->rcv_path, "060|File backup failed: %s");
259 			return (1);
260 		}
261 
262 		/* REQUEST: don't remove backing file on exit. */
263 		if (LF_ISSET(RCV_PRESERVE))
264 			F_SET(ep, F_RCV_NORM);
265 
266 		/* REQUEST: send email. */
267 		if (LF_ISSET(RCV_EMAIL))
268 			rcv_email(sp, ep->rcv_mpath);
269 	}
270 
271 	/*
272 	 * !!!
273 	 * Each time the user exec's :preserve, we have to snapshot all of
274 	 * the recovery information, i.e. it's like the user re-edited the
275 	 * file.  We copy the DB(3) backing file, and then create a new mail
276 	 * recovery file, it's simpler than exiting and reopening all of the
277 	 * underlying files.
278 	 *
279 	 * REQUEST: snapshot the file.
280 	 */
281 	rval = 0;
282 	if (LF_ISSET(RCV_SNAPSHOT)) {
283 		if (opts_empty(sp, O_RECDIR, 0))
284 			goto err;
285 		dp = O_STR(sp, O_RECDIR);
286 		if ((buf = join(dp, "vi.XXXXXX")) == NULL) {
287 			msgq(sp, M_SYSERR, NULL);
288 			goto err;
289 		}
290 		if ((fd = rcv_mktemp(sp, buf, dp)) == -1) {
291 			free(buf);
292 			goto err;
293 		}
294 		sp->gp->scr_busy(sp,
295 		    "061|Copying file for recovery...", BUSY_ON);
296 		if (rcv_copy(sp, fd, ep->rcv_path) ||
297 		    close(fd) || rcv_mailfile(sp, 1, buf)) {
298 			(void)unlink(buf);
299 			(void)close(fd);
300 			rval = 1;
301 		}
302 		free(buf);
303 		sp->gp->scr_busy(sp, NULL, BUSY_OFF);
304 	}
305 	if (0) {
306 err:		rval = 1;
307 	}
308 
309 	/* REQUEST: end the file session. */
310 	if (LF_ISSET(RCV_ENDSESSION) && file_end(sp, NULL, 1))
311 		rval = 1;
312 
313 	return (rval);
314 }
315 
316 /*
317  * rcv_mailfile --
318  *	Build the file to mail to the user.
319  */
320 static int
321 rcv_mailfile(
322 	SCR *sp,
323 	int issync,
324 	char *cp_path)
325 {
326 	EXF *ep;
327 	GS *gp;
328 	struct passwd *pw;
329 	int len;
330 	time_t now;
331 	uid_t uid;
332 	int fd;
333 	FILE *fp;
334 	char *dp, *p, *t, *qt, *buf, *mpath;
335 	char *t1, *t2, *t3;
336 	int st;
337 
338 	/*
339 	 * XXX
340 	 * MAXHOSTNAMELEN/HOST_NAME_MAX are deprecated. We try sysconf(3)
341 	 * first, then fallback to _POSIX_HOST_NAME_MAX.
342 	 */
343 	char *host;
344 	long hostmax = sysconf(_SC_HOST_NAME_MAX);
345 	if (hostmax < 0)
346 		hostmax = _POSIX_HOST_NAME_MAX;
347 
348 	gp = sp->gp;
349 	if ((pw = getpwuid(uid = getuid())) == NULL) {
350 		msgq(sp, M_ERR,
351 		    "062|Information on user id %u not found", uid);
352 		return (1);
353 	}
354 
355 	if (opts_empty(sp, O_RECDIR, 0))
356 		return (1);
357 	dp = O_STR(sp, O_RECDIR);
358 	if ((mpath = join(dp, "recover.XXXXXX")) == NULL) {
359 		msgq(sp, M_SYSERR, NULL);
360 		return (1);
361 	}
362 	if ((fd = rcv_mktemp(sp, mpath, dp)) == -1) {
363 		free(mpath);
364 		return (1);
365 	}
366 	if ((fp = fdopen(fd, "w")) == NULL) {
367 		free(mpath);
368 		close(fd);
369 		return (1);
370 	}
371 
372 	/*
373 	 * XXX
374 	 * We keep an open lock on the file so that the recover option can
375 	 * distinguish between files that are live and those that need to
376 	 * be recovered.  There's an obvious window between the mkstemp call
377 	 * and the lock, but it's pretty small.
378 	 */
379 	ep = sp->ep;
380 	if (file_lock(sp, NULL, fd, 1) != LOCK_SUCCESS)
381 		msgq(sp, M_SYSERR, "063|Unable to lock recovery file");
382 	if (!issync) {
383 		/* Save the recover file descriptor, and mail path. */
384 		ep->rcv_fd = dup(fd);
385 		ep->rcv_mpath = mpath;
386 		cp_path = ep->rcv_path;
387 	}
388 
389 	t = sp->frp->name;
390 	if ((p = strrchr(t, '/')) == NULL)
391 		p = t;
392 	else
393 		++p;
394 	(void)time(&now);
395 
396 	if ((st = rcv_dlnwrite(sp, "file", t, fp))) {
397 		if (st == 1)
398 			goto werr;
399 		goto err;
400 	}
401 	if ((st = rcv_dlnwrite(sp, "path", cp_path, fp))) {
402 		if (st == 1)
403 			goto werr;
404 		goto err;
405 	}
406 
407 	MALLOC(sp, host, char *, hostmax + 1);
408 	if (host == NULL)
409 		goto err;
410 	(void)gethostname(host, hostmax + 1);
411 
412 	len = fprintf(fp, "%s%s%s\n%s%s%s%s\n%s%.40s\n%s\n\n",
413 	    "From: root@", host, " (Nvi recovery program)",
414 	    "To: ", pw->pw_name, "@", host,
415 	    "Subject: Nvi saved the file ", p,
416 	    "Precedence: bulk");		/* For vacation(1). */
417 	if (len < 0) {
418 		free(host);
419 		goto werr;
420 	}
421 
422 	if ((qt = quote(t)) == NULL) {
423 		free(host);
424 		msgq(sp, M_SYSERR, NULL);
425 		goto err;
426 	}
427 	len = asprintf(&buf, "%s%.24s%s%s%s%s%s%s%s%s%s%s%s%s%s%s\n\n",
428 	    "On ", ctime(&now), ", the user ", pw->pw_name,
429 	    " was editing a file named ", t, " on the machine ",
430 	    host, ", when it was saved for recovery. ",
431 	    "You can recover most, if not all, of the changes ",
432 	    "to this file using the -r option to ", gp->progname, ":\n\n\t",
433 	    gp->progname, " -r ", qt);
434 	free(qt);
435 	free(host);
436 	if (buf == NULL) {
437 		msgq(sp, M_SYSERR, NULL);
438 		goto err;
439 	}
440 
441 	/*
442 	 * Format the message.  (Yes, I know it's silly.)
443 	 * Requires that the message end in a <newline>.
444 	 */
445 #define	FMTCOLS	60
446 	for (t1 = buf; len > 0; len -= t2 - t1, t1 = t2) {
447 		/* Check for a short length. */
448 		if (len <= FMTCOLS) {
449 			t2 = t1 + (len - 1);
450 			goto wout;
451 		}
452 
453 		/* Check for a required <newline>. */
454 		t2 = strchr(t1, '\n');
455 		if (t2 - t1 <= FMTCOLS)
456 			goto wout;
457 
458 		/* Find the closest space, if any. */
459 		for (t3 = t2; t2 > t1; --t2)
460 			if (*t2 == ' ') {
461 				if (t2 - t1 <= FMTCOLS)
462 					goto wout;
463 				t3 = t2;
464 			}
465 		t2 = t3;
466 
467 		/* t2 points to the last character to display. */
468 wout:		*t2++ = '\n';
469 
470 		/* t2 points one after the last character to display. */
471 		if (fwrite(t1, 1, t2 - t1, fp) != t2 - t1) {
472 			free(buf);
473 			goto werr;
474 		}
475 	}
476 
477 	if (issync) {
478 		fflush(fp);
479 		rcv_email(sp, mpath);
480 		free(mpath);
481 	}
482 	if (fclose(fp)) {
483 		free(buf);
484 werr:		msgq(sp, M_SYSERR, "065|Recovery file");
485 		goto err;
486 	}
487 	free(buf);
488 	return (0);
489 
490 err:	if (!issync)
491 		ep->rcv_fd = -1;
492 	if (fp != NULL)
493 		(void)fclose(fp);
494 	return (1);
495 }
496 
497 /*
498  *	people making love
499  *	never exactly the same
500  *	just like a snowflake
501  *
502  * rcv_list --
503  *	List the files that can be recovered by this user.
504  *
505  * PUBLIC: int rcv_list(SCR *);
506  */
507 int
508 rcv_list(SCR *sp)
509 {
510 	struct dirent *dp;
511 	struct stat sb;
512 	DIR *dirp;
513 	FILE *fp;
514 	int found;
515 	char *p, *file, *path;
516 	char *dtype, *data;
517 	int st;
518 
519 	/* Open the recovery directory for reading. */
520 	if (opts_empty(sp, O_RECDIR, 0))
521 		return (1);
522 	p = O_STR(sp, O_RECDIR);
523 	if (chdir(p) || (dirp = opendir(".")) == NULL) {
524 		msgq_str(sp, M_SYSERR, p, "recdir: %s");
525 		return (1);
526 	}
527 
528 	/* Read the directory. */
529 	for (found = 0; (dp = readdir(dirp)) != NULL;) {
530 		if (strncmp(dp->d_name, "recover.", 8))
531 			continue;
532 
533 		/* If it's readable, it's recoverable. */
534 		if ((fp = fopen(dp->d_name, "r")) == NULL)
535 			continue;
536 
537 		switch (file_lock(sp, NULL, fileno(fp), 1)) {
538 		case LOCK_FAILED:
539 			/*
540 			 * XXX
541 			 * Assume that a lock can't be acquired, but that we
542 			 * should permit recovery anyway.  If this is wrong,
543 			 * and someone else is using the file, we're going to
544 			 * die horribly.
545 			 */
546 			break;
547 		case LOCK_SUCCESS:
548 			break;
549 		case LOCK_UNAVAIL:
550 			/* If it's locked, it's live. */
551 			(void)fclose(fp);
552 			continue;
553 		}
554 
555 		/* Check the headers. */
556 		for (file = NULL, path = NULL;
557 		    file == NULL || path == NULL;) {
558 			if ((st = rcv_dlnread(sp, &dtype, &data, fp))) {
559 				if (st == 1)
560 					msgq_str(sp, M_ERR, dp->d_name,
561 					    "066|%s: malformed recovery file");
562 				goto next;
563 			}
564 			if (dtype == NULL)
565 				continue;
566 			if (!strcmp(dtype, "file"))
567 				file = data;
568 			else if (!strcmp(dtype, "path"))
569 				path = data;
570 			else
571 				free(data);
572 		}
573 
574 		/*
575 		 * If the file doesn't exist, it's an orphaned recovery file,
576 		 * toss it.
577 		 *
578 		 * XXX
579 		 * This can occur if the backup file was deleted and we crashed
580 		 * before deleting the email file.
581 		 */
582 		errno = 0;
583 		if (stat(path, &sb) &&
584 		    errno == ENOENT) {
585 			(void)unlink(dp->d_name);
586 			goto next;
587 		}
588 
589 		/* Get the last modification time and display. */
590 		(void)fstat(fileno(fp), &sb);
591 		(void)printf("%.24s: %s\n",
592 		    ctime(&sb.st_mtime), file);
593 		found = 1;
594 
595 		/* Close, discarding lock. */
596 next:		(void)fclose(fp);
597 		if (file != NULL)
598 			free(file);
599 		if (path != NULL)
600 			free(path);
601 	}
602 	if (found == 0)
603 		(void)printf("%s: No files to recover\n", sp->gp->progname);
604 	(void)closedir(dirp);
605 	return (0);
606 }
607 
608 /*
609  * rcv_read --
610  *	Start a recovered file as the file to edit.
611  *
612  * PUBLIC: int rcv_read(SCR *, FREF *);
613  */
614 int
615 rcv_read(
616 	SCR *sp,
617 	FREF *frp)
618 {
619 	struct dirent *dp;
620 	struct stat sb;
621 	DIR *dirp;
622 	FILE *fp;
623 	EXF *ep;
624 	struct timespec rec_mtim = { 0, 0 };
625 	int found, locked = 0, requested, sv_fd;
626 	char *name, *p, *t, *rp, *recp, *pathp;
627 	char *file, *path, *recpath;
628 	char *dtype, *data;
629 	int st;
630 
631 	if (opts_empty(sp, O_RECDIR, 0))
632 		return (1);
633 	rp = O_STR(sp, O_RECDIR);
634 	if ((dirp = opendir(rp)) == NULL) {
635 		msgq_str(sp, M_SYSERR, rp, "%s");
636 		return (1);
637 	}
638 
639 	name = frp->name;
640 	sv_fd = -1;
641 	recp = pathp = NULL;
642 	for (found = requested = 0; (dp = readdir(dirp)) != NULL;) {
643 		if (strncmp(dp->d_name, "recover.", 8))
644 			continue;
645 		if ((recpath = join(rp, dp->d_name)) == NULL) {
646 			msgq(sp, M_SYSERR, NULL);
647 			continue;
648 		}
649 
650 		/* If it's readable, it's recoverable. */
651 		if ((fp = fopen(recpath, "r")) == NULL) {
652 			free(recpath);
653 			continue;
654 		}
655 
656 		switch (file_lock(sp, NULL, fileno(fp), 1)) {
657 		case LOCK_FAILED:
658 			/*
659 			 * XXX
660 			 * Assume that a lock can't be acquired, but that we
661 			 * should permit recovery anyway.  If this is wrong,
662 			 * and someone else is using the file, we're going to
663 			 * die horribly.
664 			 */
665 			locked = 0;
666 			break;
667 		case LOCK_SUCCESS:
668 			locked = 1;
669 			break;
670 		case LOCK_UNAVAIL:
671 			/* If it's locked, it's live. */
672 			(void)fclose(fp);
673 			continue;
674 		}
675 
676 		/* Check the headers. */
677 		for (file = NULL, path = NULL;
678 		    file == NULL || path == NULL;) {
679 			if ((st = rcv_dlnread(sp, &dtype, &data, fp))) {
680 				if (st == 1)
681 					msgq_str(sp, M_ERR, dp->d_name,
682 					    "067|%s: malformed recovery file");
683 				goto next;
684 			}
685 			if (dtype == NULL)
686 				continue;
687 			if (!strcmp(dtype, "file"))
688 				file = data;
689 			else if (!strcmp(dtype, "path"))
690 				path = data;
691 			else
692 				free(data);
693 		}
694 		++found;
695 
696 		/*
697 		 * If the file doesn't exist, it's an orphaned recovery file,
698 		 * toss it.
699 		 *
700 		 * XXX
701 		 * This can occur if the backup file was deleted and we crashed
702 		 * before deleting the email file.
703 		 */
704 		errno = 0;
705 		if (stat(path, &sb) &&
706 		    errno == ENOENT) {
707 			(void)unlink(dp->d_name);
708 			goto next;
709 		}
710 
711 		/* Check the file name. */
712 		if (strcmp(file, name))
713 			goto next;
714 
715 		++requested;
716 
717 		/* If we've found more than one, take the most recent. */
718 		(void)fstat(fileno(fp), &sb);
719 		if (recp == NULL ||
720 		    timespeccmp(&rec_mtim, &sb.st_mtimespec, <)) {
721 			p = recp;
722 			t = pathp;
723 			recp = recpath;
724 			pathp = path;
725 			if (p != NULL) {
726 				free(p);
727 				free(t);
728 			}
729 			rec_mtim = sb.st_mtimespec;
730 			if (sv_fd != -1)
731 				(void)close(sv_fd);
732 			sv_fd = dup(fileno(fp));
733 		} else {
734 next:			free(recpath);
735 			if (path != NULL)
736 				free(path);
737 		}
738 		(void)fclose(fp);
739 		if (file != NULL)
740 			free(file);
741 	}
742 	(void)closedir(dirp);
743 
744 	if (recp == NULL) {
745 		msgq_str(sp, M_INFO, name,
746 		    "068|No files named %s, readable by you, to recover");
747 		return (1);
748 	}
749 	if (found) {
750 		if (requested > 1)
751 			msgq(sp, M_INFO,
752 	    "069|There are older versions of this file for you to recover");
753 		if (found > requested)
754 			msgq(sp, M_INFO,
755 			    "070|There are other files for you to recover");
756 	}
757 
758 	/*
759 	 * Create the FREF structure, start the btree file.
760 	 *
761 	 * XXX
762 	 * file_init() is going to set ep->rcv_path.
763 	 */
764 	if (file_init(sp, frp, pathp, 0)) {
765 		free(recp);
766 		free(pathp);
767 		(void)close(sv_fd);
768 		return (1);
769 	}
770 	free(pathp);
771 
772 	/*
773 	 * We keep an open lock on the file so that the recover option can
774 	 * distinguish between files that are live and those that need to
775 	 * be recovered.  The lock is already acquired, just copy it.
776 	 */
777 	ep = sp->ep;
778 	ep->rcv_mpath = recp;
779 	ep->rcv_fd = sv_fd;
780 	if (!locked)
781 		F_SET(frp, FR_UNLOCKED);
782 
783 	/* We believe the file is recoverable. */
784 	F_SET(ep, F_RCV_ON);
785 	return (0);
786 }
787 
788 /*
789  * rcv_copy --
790  *	Copy a recovery file.
791  */
792 static int
793 rcv_copy(
794 	SCR *sp,
795 	int wfd,
796 	char *fname)
797 {
798 	int nr, nw, off, rfd;
799 	char buf[8 * 1024];
800 
801 	if ((rfd = open(fname, O_RDONLY, 0)) == -1)
802 		goto err;
803 	while ((nr = read(rfd, buf, sizeof(buf))) > 0)
804 		for (off = 0; nr; nr -= nw, off += nw)
805 			if ((nw = write(wfd, buf + off, nr)) < 0)
806 				goto err;
807 	if (nr == 0)
808 		return (0);
809 
810 err:	msgq_str(sp, M_SYSERR, fname, "%s");
811 	return (1);
812 }
813 
814 /*
815  * rcv_mktemp --
816  *	Paranoid make temporary file routine.
817  */
818 static int
819 rcv_mktemp(
820 	SCR *sp,
821 	char *path,
822 	char *dname)
823 {
824 	int fd;
825 
826 	if ((fd = mkstemp(path)) == -1)
827 		msgq_str(sp, M_SYSERR, dname, "%s");
828 	return (fd);
829 }
830 
831 /*
832  * rcv_email --
833  *	Send email.
834  */
835 static void
836 rcv_email(
837 	SCR *sp,
838 	char *fname)
839 {
840 	char *buf;
841 
842 	(void)asprintf(&buf, _PATH_SENDMAIL " -odb -t < %s", fname);
843 	if (buf == NULL) {
844 		msgq_str(sp, M_ERR, strerror(errno),
845 		    "071|not sending email: %s");
846 		return;
847 	}
848 	(void)system(buf);
849 	free(buf);
850 }
851 
852 /*
853  * rcv_dlnwrite --
854  *	Encode a string into an X-vi-data line and write it.
855  */
856 static int
857 rcv_dlnwrite(
858 	SCR *sp,
859 	const char *dtype,
860 	const char *src,
861 	FILE *fp)
862 {
863 	char *bp = NULL, *p;
864 	size_t blen = 0;
865 	size_t dlen, len;
866 	int plen, xlen;
867 
868 	len = strlen(src);
869 	dlen = strlen(dtype);
870 	GET_SPACE_GOTOC(sp, bp, blen, (len + 2) / 3 * 4 + dlen + 2);
871 	(void)memcpy(bp, dtype, dlen);
872 	bp[dlen] = ';';
873 	if ((xlen = b64_ntop((u_char *)src,
874 	    len, bp + dlen + 1, blen)) == -1)
875 		goto err;
876 	xlen += dlen + 1;
877 
878 	/* Output as an MIME folding header. */
879 	if ((plen = fprintf(fp, VI_DHEADER " %.*s\n",
880 	    FMTCOLS - (int)sizeof(VI_DHEADER), bp)) < 0)
881 		goto err;
882 	plen -= (int)sizeof(VI_DHEADER) + 1;
883 	for (p = bp, xlen -= plen; xlen > 0; xlen -= plen) {
884 		p += plen;
885 		if ((plen = fprintf(fp, " %.*s\n", FMTCOLS - 1, p)) < 0)
886 			goto err;
887 		plen -= 2;
888 	}
889 	FREE_SPACE(sp, bp, blen);
890 	return (0);
891 
892 err:	FREE_SPACE(sp, bp, blen);
893 	return (1);
894 alloc_err:
895 	msgq(sp, M_SYSERR, NULL);
896 	return (-1);
897 }
898 
899 /*
900  * rcv_dlnread --
901  *	Read an X-vi-data line and decode it.
902  */
903 static int
904 rcv_dlnread(
905 	SCR *sp,
906 	char **dtypep,
907 	char **datap,		/* free *datap if != NULL after use. */
908 	FILE *fp)
909 {
910 	int ch;
911 	char buf[1024];
912 	char *bp = NULL, *p, *src;
913 	size_t blen = 0;
914 	size_t len, off, dlen;
915 	char *dtype, *data;
916 	int xlen;
917 
918 	if (fgets(buf, sizeof(buf), fp) == NULL)
919 		return (1);
920 	if (strncmp(buf, VI_DHEADER, sizeof(VI_DHEADER) - 1)) {
921 		*dtypep = NULL;
922 		*datap = NULL;
923 		return (0);
924 	}
925 
926 	/* Fetch an MIME folding header. */
927 	len = strlen(buf) - sizeof(VI_DHEADER) + 1;
928 	GET_SPACE_GOTOC(sp, bp, blen, len);
929 	(void)memcpy(bp, buf + sizeof(VI_DHEADER) - 1, len);
930 	p = bp + len;
931 	while ((ch = fgetc(fp)) == ' ') {
932 		if (fgets(buf, sizeof(buf), fp) == NULL)
933 			goto err;
934 		off = strlen(buf);
935 		len += off;
936 		ADD_SPACE_GOTOC(sp, bp, blen, len);
937 		p = bp + len - off;
938 		(void)memcpy(p, buf, off);
939 	}
940 	bp[len] = '\0';
941 	(void)ungetc(ch, fp);
942 
943 	for (p = bp; *p == ' ' || *p == '\n'; p++);
944 	if ((src = strchr(p, ';')) == NULL)
945 		goto err;
946 	dlen = src - p;
947 	src += 1;
948 	len -= src - bp;
949 
950 	/* Memory looks like: "<data>\0<dtype>\0". */
951 	MALLOC(sp, data, char *, dlen + len / 4 * 3 + 2);
952 	if (data == NULL)
953 		goto err;
954 	if ((xlen = (b64_pton(p + dlen + 1,
955 	    (u_char *)data, len / 4 * 3 + 1))) == -1) {
956 		free(data);
957 		goto err;
958 	}
959 	data[xlen] = '\0';
960 	dtype = data + xlen + 1;
961 	(void)memcpy(dtype, p, dlen);
962 	dtype[dlen] = '\0';
963 	FREE_SPACE(sp, bp, blen);
964 	*dtypep = dtype;
965 	*datap = data;
966 	return (0);
967 
968 err: 	FREE_SPACE(sp, bp, blen);
969 	return (1);
970 alloc_err:
971 	msgq(sp, M_SYSERR, NULL);
972 	return (-1);
973 }
974