xref: /freebsd/contrib/nvi/common/recover.c (revision 6be3386466ab79a84b48429ae66244f21526d3df)
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 		    timespeccmp(&rec_mtim, &sb.st_mtim, <)) {
705 			p = recp;
706 			t = pathp;
707 			recp = recpath;
708 			pathp = path;
709 			if (p != NULL) {
710 				free(p);
711 				free(t);
712 			}
713 			rec_mtim = sb.st_mtim;
714 			if (sv_fd != -1)
715 				(void)close(sv_fd);
716 			sv_fd = dup(fileno(fp));
717 		} else {
718 next:			free(recpath);
719 			free(path);
720 		}
721 		(void)fclose(fp);
722 		free(file);
723 	}
724 	(void)closedir(dirp);
725 
726 	if (recp == NULL) {
727 		msgq_str(sp, M_INFO, name,
728 		    "068|No files named %s, readable by you, to recover");
729 		return (1);
730 	}
731 	if (found) {
732 		if (requested > 1)
733 			msgq(sp, M_INFO,
734 	    "069|There are older versions of this file for you to recover");
735 		if (found > requested)
736 			msgq(sp, M_INFO,
737 			    "070|There are other files for you to recover");
738 	}
739 
740 	/*
741 	 * Create the FREF structure, start the btree file.
742 	 *
743 	 * XXX
744 	 * file_init() is going to set ep->rcv_path.
745 	 */
746 	if (file_init(sp, frp, pathp, 0)) {
747 		free(recp);
748 		free(pathp);
749 		(void)close(sv_fd);
750 		return (1);
751 	}
752 	free(pathp);
753 
754 	/*
755 	 * We keep an open lock on the file so that the recover option can
756 	 * distinguish between files that are live and those that need to
757 	 * be recovered.  The lock is already acquired, just copy it.
758 	 */
759 	ep = sp->ep;
760 	ep->rcv_mpath = recp;
761 	ep->rcv_fd = sv_fd;
762 	if (!locked)
763 		F_SET(frp, FR_UNLOCKED);
764 
765 	/* We believe the file is recoverable. */
766 	F_SET(ep, F_RCV_ON);
767 	return (0);
768 }
769 
770 /*
771  * rcv_copy --
772  *	Copy a recovery file.
773  */
774 static int
775 rcv_copy(SCR *sp, int wfd, char *fname)
776 {
777 	int nr, nw, off, rfd;
778 	char buf[8 * 1024];
779 
780 	if ((rfd = open(fname, O_RDONLY, 0)) == -1)
781 		goto err;
782 	while ((nr = read(rfd, buf, sizeof(buf))) > 0)
783 		for (off = 0; nr; nr -= nw, off += nw)
784 			if ((nw = write(wfd, buf + off, nr)) < 0)
785 				goto err;
786 	if (nr == 0)
787 		return (0);
788 
789 err:	msgq_str(sp, M_SYSERR, fname, "%s");
790 	return (1);
791 }
792 
793 /*
794  * rcv_mktemp --
795  *	Paranoid make temporary file routine.
796  */
797 static int
798 rcv_mktemp(SCR *sp, char *path, char *dname)
799 {
800 	int fd;
801 
802 	if ((fd = mkstemp(path)) == -1)
803 		msgq_str(sp, M_SYSERR, dname, "%s");
804 	return (fd);
805 }
806 
807 /*
808  * rcv_email --
809  *	Send email.
810  */
811 static void
812 rcv_email(SCR *sp, char *fname)
813 {
814 	char *buf;
815 
816 	if (asprintf(&buf, _PATH_SENDMAIL " -odb -t < %s", fname) == -1) {
817 		msgq_str(sp, M_ERR, strerror(errno),
818 		    "071|not sending email: %s");
819 		return;
820 	}
821 	(void)system(buf);
822 	free(buf);
823 }
824 
825 /*
826  * rcv_dlnwrite --
827  *	Encode a string into an X-vi-data line and write it.
828  */
829 static int
830 rcv_dlnwrite(SCR *sp, const char *dtype, const char *src, FILE *fp)
831 {
832 	char *bp = NULL, *p;
833 	size_t blen = 0;
834 	size_t dlen, len;
835 	int plen, xlen;
836 
837 	len = strlen(src);
838 	dlen = strlen(dtype);
839 	GET_SPACE_GOTOC(sp, bp, blen, (len + 2) / 3 * 4 + dlen + 2);
840 	(void)memcpy(bp, dtype, dlen);
841 	bp[dlen] = ';';
842 	if ((xlen = b64_ntop((u_char *)src,
843 	    len, bp + dlen + 1, blen)) == -1)
844 		goto err;
845 	xlen += dlen + 1;
846 
847 	/* Output as an MIME folding header. */
848 	if ((plen = fprintf(fp, VI_DHEADER " %.*s\n",
849 	    FMTCOLS - (int)sizeof(VI_DHEADER), bp)) < 0)
850 		goto err;
851 	plen -= (int)sizeof(VI_DHEADER) + 1;
852 	for (p = bp, xlen -= plen; xlen > 0; xlen -= plen) {
853 		p += plen;
854 		if ((plen = fprintf(fp, " %.*s\n", FMTCOLS - 1, p)) < 0)
855 			goto err;
856 		plen -= 2;
857 	}
858 	FREE_SPACE(sp, bp, blen);
859 	return (0);
860 
861 err:	FREE_SPACE(sp, bp, blen);
862 	return (1);
863 alloc_err:
864 	msgq(sp, M_SYSERR, NULL);
865 	return (-1);
866 }
867 
868 /*
869  * rcv_dlnread --
870  *	Read an X-vi-data line and decode it.
871  */
872 static int
873 rcv_dlnread(SCR *sp, char **dtypep,
874 	char **datap,		/* free *datap if != NULL after use. */
875 	FILE *fp)
876 {
877 	int ch;
878 	char buf[1024];
879 	char *bp = NULL, *p, *src;
880 	size_t blen = 0;
881 	size_t len, off, dlen;
882 	char *dtype, *data;
883 	int xlen;
884 
885 	if (fgets(buf, sizeof(buf), fp) == NULL)
886 		return (1);
887 	if (strncmp(buf, VI_DHEADER, sizeof(VI_DHEADER) - 1)) {
888 		*dtypep = NULL;
889 		*datap = NULL;
890 		return (0);
891 	}
892 
893 	/* Fetch an MIME folding header. */
894 	len = strlen(buf) - sizeof(VI_DHEADER) + 1;
895 	GET_SPACE_GOTOC(sp, bp, blen, len);
896 	(void)memcpy(bp, buf + sizeof(VI_DHEADER) - 1, len);
897 	p = bp + len;
898 	while ((ch = fgetc(fp)) == ' ') {
899 		if (fgets(buf, sizeof(buf), fp) == NULL)
900 			goto err;
901 		off = strlen(buf);
902 		len += off;
903 		ADD_SPACE_GOTOC(sp, bp, blen, len);
904 		p = bp + len - off;
905 		(void)memcpy(p, buf, off);
906 	}
907 	bp[len] = '\0';
908 	(void)ungetc(ch, fp);
909 
910 	for (p = bp; *p == ' ' || *p == '\n'; p++);
911 	if ((src = strchr(p, ';')) == NULL)
912 		goto err;
913 	dlen = src - p;
914 	src += 1;
915 	len -= src - bp;
916 
917 	/* Memory looks like: "<data>\0<dtype>\0". */
918 	MALLOC(sp, data, dlen + len / 4 * 3 + 2);
919 	if (data == NULL)
920 		goto err;
921 	if ((xlen = (b64_pton(p + dlen + 1,
922 	    (u_char *)data, len / 4 * 3 + 1))) == -1) {
923 		free(data);
924 		goto err;
925 	}
926 	data[xlen] = '\0';
927 	dtype = data + xlen + 1;
928 	(void)memcpy(dtype, p, dlen);
929 	dtype[dlen] = '\0';
930 	FREE_SPACE(sp, bp, blen);
931 	*dtypep = dtype;
932 	*datap = data;
933 	return (0);
934 
935 err: 	FREE_SPACE(sp, bp, blen);
936 	return (1);
937 alloc_err:
938 	msgq(sp, M_SYSERR, NULL);
939 	return (-1);
940 }
941