xref: /openbsd-src/usr.bin/vi/common/recover.c (revision b2ea75c1b17e1a9a339660e7ed45cd24946b230e)
1 /*	$OpenBSD: recover.c,v 1.7 2001/06/18 21:39:26 millert 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 __P((SCR *, int, char *));
115 static void	 rcv_email __P((SCR *, char *));
116 static char	*rcv_gets __P((char *, size_t, int));
117 static int	 rcv_mailfile __P((SCR *, int, char *));
118 static int	 rcv_mktemp __P((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 __P((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.XXXXXX", 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 __P((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 __P((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.XXXXXX", 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.XXXXXX", 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\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 	if (len > sizeof(buf) - 1)
415 		goto lerr;
416 	if (write(fd, buf, len) != len)
417 		goto werr;
418 
419 	len = snprintf(buf, sizeof(buf),
420 	    "%s%.24s%s%s%s%s%s%s%s%s%s%s%s%s%s%s\n\n",
421 	    "On ", ctime(&now), ", the user ", pw->pw_name,
422 	    " was editing a file named ", t, " on the machine ",
423 	    host, ", when it was saved for recovery. ",
424 	    "You can recover most, if not all, of the changes ",
425 	    "to this file using the -r option to ", gp->progname, ":\n\n\t",
426 	    gp->progname, " -r ", t);
427 	if (len > sizeof(buf) - 1) {
428 lerr:		msgq(sp, M_ERR, "064|Recovery file buffer overrun");
429 		goto err;
430 	}
431 
432 	/*
433 	 * Format the message.  (Yes, I know it's silly.)
434 	 * Requires that the message end in a <newline>.
435 	 */
436 #define	FMTCOLS	60
437 	for (t1 = buf; len > 0; len -= t2 - t1, t1 = t2) {
438 		/* Check for a short length. */
439 		if (len <= FMTCOLS) {
440 			t2 = t1 + (len - 1);
441 			goto wout;
442 		}
443 
444 		/* Check for a required <newline>. */
445 		t2 = strchr(t1, '\n');
446 		if (t2 - t1 <= FMTCOLS)
447 			goto wout;
448 
449 		/* Find the closest space, if any. */
450 		for (t3 = t2; t2 > t1; --t2)
451 			if (*t2 == ' ') {
452 				if (t2 - t1 <= FMTCOLS)
453 					goto wout;
454 				t3 = t2;
455 			}
456 		t2 = t3;
457 
458 		/* t2 points to the last character to display. */
459 wout:		*t2++ = '\n';
460 
461 		/* t2 points one after the last character to display. */
462 		if (write(fd, t1, t2 - t1) != t2 - t1)
463 			goto werr;
464 	}
465 
466 	if (issync) {
467 		rcv_email(sp, mpath);
468 		if (close(fd)) {
469 werr:			msgq(sp, M_SYSERR, "065|Recovery file");
470 			goto err;
471 		}
472 	}
473 	return (0);
474 
475 err:	if (!issync)
476 		ep->rcv_fd = -1;
477 	if (fd != -1)
478 		(void)close(fd);
479 	return (1);
480 }
481 
482 /*
483  *	people making love
484  *	never exactly the same
485  *	just like a snowflake
486  *
487  * rcv_list --
488  *	List the files that can be recovered by this user.
489  *
490  * PUBLIC: int rcv_list __P((SCR *));
491  */
492 int
493 rcv_list(sp)
494 	SCR *sp;
495 {
496 	struct dirent *dp;
497 	struct stat sb;
498 	DIR *dirp;
499 	FILE *fp;
500 	int found;
501 	char *p, *t, file[MAXPATHLEN], path[MAXPATHLEN];
502 
503 	/* Open the recovery directory for reading. */
504 	if (opts_empty(sp, O_RECDIR, 0))
505 		return (1);
506 	p = O_STR(sp, O_RECDIR);
507 	if (chdir(p) || (dirp = opendir(".")) == NULL) {
508 		msgq_str(sp, M_SYSERR, p, "recdir: %s");
509 		return (1);
510 	}
511 
512 	/* Read the directory. */
513 	for (found = 0; (dp = readdir(dirp)) != NULL;) {
514 		if (strncmp(dp->d_name, "recover.", 8))
515 			continue;
516 
517 		/*
518 		 * If it's readable, it's recoverable.
519 		 *
520 		 * XXX
521 		 * Should be "r", we don't want to write the file.  However,
522 		 * if we're using fcntl(2), there's no way to lock a file
523 		 * descriptor that's not open for writing.
524 		 */
525 		if ((fp = fopen(dp->d_name, "r+")) == NULL)
526 			continue;
527 
528 		switch (file_lock(sp, NULL, NULL, fileno(fp), 1)) {
529 		case LOCK_FAILED:
530 			/*
531 			 * XXX
532 			 * Assume that a lock can't be acquired, but that we
533 			 * should permit recovery anyway.  If this is wrong,
534 			 * and someone else is using the file, we're going to
535 			 * die horribly.
536 			 */
537 			break;
538 		case LOCK_SUCCESS:
539 			break;
540 		case LOCK_UNAVAIL:
541 			/* If it's locked, it's live. */
542 			(void)fclose(fp);
543 			continue;
544 		}
545 
546 		/* Check the headers. */
547 		if (fgets(file, sizeof(file), fp) == NULL ||
548 		    strncmp(file, VI_FHEADER, sizeof(VI_FHEADER) - 1) ||
549 		    (p = strchr(file, '\n')) == NULL ||
550 		    fgets(path, sizeof(path), fp) == NULL ||
551 		    strncmp(path, VI_PHEADER, sizeof(VI_PHEADER) - 1) ||
552 		    (t = strchr(path, '\n')) == NULL) {
553 			msgq_str(sp, M_ERR, dp->d_name,
554 			    "066|%s: malformed recovery file");
555 			goto next;
556 		}
557 		*p = *t = '\0';
558 
559 		/*
560 		 * If the file doesn't exist, it's an orphaned recovery file,
561 		 * toss it.
562 		 *
563 		 * XXX
564 		 * This can occur if the backup file was deleted and we crashed
565 		 * before deleting the email file.
566 		 */
567 		errno = 0;
568 		if (stat(path + sizeof(VI_PHEADER) - 1, &sb) &&
569 		    errno == ENOENT) {
570 			(void)unlink(dp->d_name);
571 			goto next;
572 		}
573 
574 		/* Get the last modification time and display. */
575 		(void)fstat(fileno(fp), &sb);
576 		(void)printf("%.24s: %s\n",
577 		    ctime(&sb.st_mtime), file + sizeof(VI_FHEADER) - 1);
578 		found = 1;
579 
580 		/* Close, discarding lock. */
581 next:		(void)fclose(fp);
582 	}
583 	if (found == 0)
584 		(void)printf("vi: no files to recover.\n");
585 	(void)closedir(dirp);
586 	return (0);
587 }
588 
589 /*
590  * rcv_read --
591  *	Start a recovered file as the file to edit.
592  *
593  * PUBLIC: int rcv_read __P((SCR *, FREF *));
594  */
595 int
596 rcv_read(sp, frp)
597 	SCR *sp;
598 	FREF *frp;
599 {
600 	struct dirent *dp;
601 	struct stat sb;
602 	DIR *dirp;
603 	EXF *ep;
604 	time_t rec_mtime;
605 	int fd, found, locked, requested, sv_fd;
606 	char *name, *p, *t, *rp, *recp, *pathp;
607 	char file[MAXPATHLEN], path[MAXPATHLEN], recpath[MAXPATHLEN];
608 
609 	if (opts_empty(sp, O_RECDIR, 0))
610 		return (1);
611 	rp = O_STR(sp, O_RECDIR);
612 	if ((dirp = opendir(rp)) == NULL) {
613 		msgq_str(sp, M_SYSERR, rp, "%s");
614 		return (1);
615 	}
616 
617 	name = frp->name;
618 	sv_fd = -1;
619 	rec_mtime = 0;
620 	recp = pathp = NULL;
621 	for (found = requested = 0; (dp = readdir(dirp)) != NULL;) {
622 		if (strncmp(dp->d_name, "recover.", 8))
623 			continue;
624 		(void)snprintf(recpath,
625 		    sizeof(recpath), "%s/%s", rp, dp->d_name);
626 
627 		/*
628 		 * If it's readable, it's recoverable.  It would be very
629 		 * nice to use stdio(3), but, we can't because that would
630 		 * require closing and then reopening the file so that we
631 		 * could have a lock and still close the FP.  Another tip
632 		 * of the hat to fcntl(2).
633 		 *
634 		 * XXX
635 		 * Should be O_RDONLY, we don't want to write it.  However,
636 		 * if we're using fcntl(2), there's no way to lock a file
637 		 * descriptor that's not open for writing.
638 		 */
639 		if ((fd = open(recpath, O_RDWR, 0)) == -1)
640 			continue;
641 
642 		switch (file_lock(sp, NULL, NULL, fd, 1)) {
643 		case LOCK_FAILED:
644 			/*
645 			 * XXX
646 			 * Assume that a lock can't be acquired, but that we
647 			 * should permit recovery anyway.  If this is wrong,
648 			 * and someone else is using the file, we're going to
649 			 * die horribly.
650 			 */
651 			locked = 0;
652 			break;
653 		case LOCK_SUCCESS:
654 			locked = 1;
655 			break;
656 		case LOCK_UNAVAIL:
657 			/* If it's locked, it's live. */
658 			(void)close(fd);
659 			continue;
660 		}
661 
662 		/* Check the headers. */
663 		if (rcv_gets(file, sizeof(file), fd) == NULL ||
664 		    strncmp(file, VI_FHEADER, sizeof(VI_FHEADER) - 1) ||
665 		    (p = strchr(file, '\n')) == NULL ||
666 		    rcv_gets(path, sizeof(path), fd) == NULL ||
667 		    strncmp(path, VI_PHEADER, sizeof(VI_PHEADER) - 1) ||
668 		    (t = strchr(path, '\n')) == NULL) {
669 			msgq_str(sp, M_ERR, recpath,
670 			    "067|%s: malformed recovery file");
671 			goto next;
672 		}
673 		*p = *t = '\0';
674 		++found;
675 
676 		/*
677 		 * If the file doesn't exist, it's an orphaned recovery file,
678 		 * toss it.
679 		 *
680 		 * XXX
681 		 * This can occur if the backup file was deleted and we crashed
682 		 * before deleting the email file.
683 		 */
684 		errno = 0;
685 		if (stat(path + sizeof(VI_PHEADER) - 1, &sb) &&
686 		    errno == ENOENT) {
687 			(void)unlink(dp->d_name);
688 			goto next;
689 		}
690 
691 		/* Check the file name. */
692 		if (strcmp(file + sizeof(VI_FHEADER) - 1, name))
693 			goto next;
694 
695 		++requested;
696 
697 		/*
698 		 * If we've found more than one, take the most recent.
699 		 *
700 		 * XXX
701 		 * Since we're using st_mtime, for portability reasons,
702 		 * we only get a single second granularity, instead of
703 		 * getting it right.
704 		 */
705 		(void)fstat(fd, &sb);
706 		if (recp == NULL || rec_mtime < sb.st_mtime) {
707 			p = recp;
708 			t = pathp;
709 			if ((recp = strdup(recpath)) == NULL) {
710 				msgq(sp, M_SYSERR, NULL);
711 				recp = p;
712 				goto next;
713 			}
714 			if ((pathp = strdup(path)) == NULL) {
715 				msgq(sp, M_SYSERR, NULL);
716 				free(recp);
717 				recp = p;
718 				pathp = t;
719 				goto next;
720 			}
721 			if (p != NULL) {
722 				free(p);
723 				free(t);
724 			}
725 			rec_mtime = sb.st_mtime;
726 			if (sv_fd != -1)
727 				(void)close(sv_fd);
728 			sv_fd = fd;
729 		} else
730 next:			(void)close(fd);
731 	}
732 	(void)closedir(dirp);
733 
734 	if (recp == NULL) {
735 		msgq_str(sp, M_INFO, name,
736 		    "068|No files named %s, readable by you, to recover");
737 		return (1);
738 	}
739 	if (found) {
740 		if (requested > 1)
741 			msgq(sp, M_INFO,
742 	    "069|There are older versions of this file for you to recover");
743 		if (found > requested)
744 			msgq(sp, M_INFO,
745 			    "070|There are other files for you to recover");
746 	}
747 
748 	/*
749 	 * Create the FREF structure, start the btree file.
750 	 *
751 	 * XXX
752 	 * file_init() is going to set ep->rcv_path.
753 	 */
754 	if (file_init(sp, frp, pathp + sizeof(VI_PHEADER) - 1, 0)) {
755 		free(recp);
756 		free(pathp);
757 		(void)close(sv_fd);
758 		return (1);
759 	}
760 
761 	/*
762 	 * We keep an open lock on the file so that the recover option can
763 	 * distinguish between files that are live and those that need to
764 	 * be recovered.  The lock is already acquired, just copy it.
765 	 */
766 	ep = sp->ep;
767 	ep->rcv_mpath = recp;
768 	ep->rcv_fd = sv_fd;
769 	if (!locked)
770 		F_SET(frp, FR_UNLOCKED);
771 
772 	/* We believe the file is recoverable. */
773 	F_SET(ep, F_RCV_ON);
774 	return (0);
775 }
776 
777 /*
778  * rcv_copy --
779  *	Copy a recovery file.
780  */
781 static int
782 rcv_copy(sp, wfd, fname)
783 	SCR *sp;
784 	int wfd;
785 	char *fname;
786 {
787 	int nr, nw, off, rfd;
788 	char buf[8 * 1024];
789 
790 	if ((rfd = open(fname, O_RDONLY, 0)) == -1)
791 		goto err;
792 	while ((nr = read(rfd, buf, sizeof(buf))) > 0)
793 		for (off = 0; nr; nr -= nw, off += nw)
794 			if ((nw = write(wfd, buf + off, nr)) < 0)
795 				goto err;
796 	if (nr == 0)
797 		return (0);
798 
799 err:	msgq_str(sp, M_SYSERR, fname, "%s");
800 	return (1);
801 }
802 
803 /*
804  * rcv_gets --
805  *	Fgets(3) for a file descriptor.
806  */
807 static char *
808 rcv_gets(buf, len, fd)
809 	char *buf;
810 	size_t len;
811 	int fd;
812 {
813 	int nr;
814 	char *p;
815 
816 	if ((nr = read(fd, buf, len - 1)) == -1)
817 		return (NULL);
818 	if ((p = strchr(buf, '\n')) == NULL)
819 		return (NULL);
820 	(void)lseek(fd, (off_t)((p - buf) + 1), SEEK_SET);
821 	return (buf);
822 }
823 
824 /*
825  * rcv_mktemp --
826  *	Paranoid make temporary file routine.
827  */
828 static int
829 rcv_mktemp(sp, path, dname, perms)
830 	SCR *sp;
831 	char *path, *dname;
832 	int perms;
833 {
834 	int fd;
835 
836 	/*
837 	 * !!!
838 	 * We expect mkstemp(3) to set the permissions correctly.  On
839 	 * historic System V systems, mkstemp didn't.  Do it here, on
840 	 * GP's.  This also protects us from users with stupid umasks.
841 	 *
842 	 * XXX
843 	 * The variable perms should really be a mode_t.
844 	 */
845 	if ((fd = mkstemp(path)) == -1 || fchmod(fd, perms) == -1) {
846 		msgq_str(sp, M_SYSERR, dname, "%s");
847 		if (fd != -1) {
848 			close(fd);
849 			unlink(path);
850 			fd = -1;
851 		}
852 	}
853 	return (fd);
854 }
855 
856 /*
857  * rcv_email --
858  *	Send email.
859  */
860 static void
861 rcv_email(sp, fname)
862 	SCR *sp;
863 	char *fname;
864 {
865 	struct stat sb;
866 	char buf[MAXPATHLEN * 2 + 20];
867 
868 	if (_PATH_SENDMAIL[0] != '/' || stat(_PATH_SENDMAIL, &sb))
869 		msgq_str(sp, M_SYSERR,
870 		    _PATH_SENDMAIL, "071|not sending email: %s");
871 	else {
872 		/*
873 		 * !!!
874 		 * If you need to port this to a system that doesn't have
875 		 * sendmail, the -t flag causes sendmail to read the message
876 		 * for the recipients instead of specifying them some other
877 		 * way.
878 		 */
879 		(void)snprintf(buf, sizeof(buf),
880 		    "%s -t < %s", _PATH_SENDMAIL, fname);
881 		(void)system(buf);
882 	}
883 }
884