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