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