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