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.
7 * See the LICENSE file for redistribution information.
12 #include <sys/types.h>
13 #include <sys/queue.h>
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.
23 #include <bitstring.h>
29 #include <netinet/in.h> /* Required by resolv.h. */
37 #include "../ex/version.h"
39 #include "pathnames.h"
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:
50 * + first starting the edit session:
51 * the b+tree file exists and is mode 700, the mail recovery
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.
57 * In the EXF structure we maintain a file descriptor that is the locked
58 * file descriptor for the mail recovery file.
60 * To find out if a recovery file/backing file pair are in use, try to get
61 * a lock on the recovery file.
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.
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.
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.
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.
89 * The recovery mail file contains normal mail headers, with two additional
91 * X-vi-data: <file|path>;<base64 encoded path>
93 * MIME headers; the folding character is limited to ' '.
95 * Btree files are named "vi.XXXXXX" and recovery files are named
99 #define VI_DHEADER "X-vi-data:"
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 *);
110 * Build a file name that will be used as the recovery file.
112 * PUBLIC: int rcv_tmp(SCR *, EXF *, char *);
115 rcv_tmp(SCR *sp, EXF *ep, char *name)
123 * ep MAY NOT BE THE SAME AS sp->ep, DON'T USE THE LATTER.
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.
132 if (opts_empty(sp, O_RECDIR, 0))
134 dp = O_STR(sp, O_RECDIR);
136 if (errno != ENOENT || mkdir(dp, 0)) {
137 msgq(sp, M_SYSERR, "%s", dp);
140 (void)chmod(dp, S_IRWXU | S_IRWXG | S_IRWXO | S_ISVTX);
143 if ((path = join(dp, "vi.XXXXXX")) == NULL)
145 if ((fd = rcv_mktemp(sp, path, dp)) == -1) {
149 (void)fchmod(fd, S_IRWXU);
155 "056|Modifications not recoverable if the session fails");
159 /* We believe the file is recoverable. */
166 * Force the file to be snapshotted for recovery.
168 * PUBLIC: int rcv_init(SCR *);
178 /* Only do this once. */
179 F_CLR(ep, F_FIRSTMODIFY);
181 /* If we already know the file isn't recoverable, we're done. */
182 if (!F_ISSET(ep, F_RCV_ON))
185 /* Turn off recoverability until we figure out if this will work. */
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))
194 /* Force a read of the entire file. */
195 if (db_last(sp, &lno))
198 /* Turn on a busy message, and sync it to backing store. */
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);
207 sp->gp->scr_busy(sp, NULL, BUSY_OFF);
210 /* Turn off the owner execute bit. */
211 (void)chmod(ep->rcv_path, S_IRUSR | S_IWUSR);
213 /* We believe the file is recoverable. */
218 "059|Modifications not recoverable if the session fails");
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
230 * PUBLIC: int rcv_sync(SCR *, u_int);
233 rcv_sync(SCR *sp, u_int flags)
239 /* Make sure that there's something to recover/sync. */
241 if (ep == NULL || !F_ISSET(ep, F_RCV_ON))
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");
253 /* REQUEST: don't remove backing file on exit. */
254 if (LF_ISSET(RCV_PRESERVE))
255 F_SET(ep, F_RCV_NORM);
257 /* REQUEST: send email. */
258 if (LF_ISSET(RCV_EMAIL))
259 rcv_email(sp, ep->rcv_mpath);
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
270 * REQUEST: snapshot the file.
273 if (LF_ISSET(RCV_SNAPSHOT)) {
274 if (opts_empty(sp, O_RECDIR, 0))
276 dp = O_STR(sp, O_RECDIR);
277 if ((buf = join(dp, "vi.XXXXXX")) == NULL) {
278 msgq(sp, M_SYSERR, NULL);
281 if ((fd = rcv_mktemp(sp, buf, dp)) == -1) {
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)) {
294 sp->gp->scr_busy(sp, NULL, BUSY_OFF);
300 /* REQUEST: end the file session. */
301 if (LF_ISSET(RCV_ENDSESSION) && file_end(sp, NULL, 1))
309 * Build the file to mail to the user.
312 rcv_mailfile(SCR *sp, int issync, char *cp_path)
322 char *dp, *p, *t, *qt, *buf, *mpath;
328 * MAXHOSTNAMELEN/HOST_NAME_MAX are deprecated. We try sysconf(3)
329 * first, then fallback to _POSIX_HOST_NAME_MAX.
332 long hostmax = sysconf(_SC_HOST_NAME_MAX);
334 hostmax = _POSIX_HOST_NAME_MAX;
337 if ((pw = getpwuid(uid = getuid())) == NULL) {
339 "062|Information on user id %u not found", uid);
343 if (opts_empty(sp, O_RECDIR, 0))
345 dp = O_STR(sp, O_RECDIR);
346 if ((mpath = join(dp, "recover.XXXXXX")) == NULL) {
347 msgq(sp, M_SYSERR, NULL);
350 if ((fd = rcv_mktemp(sp, mpath, dp)) == -1) {
354 if ((fp = fdopen(fd, "w")) == NULL) {
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.
368 if (file_lock(sp, NULL, fd, 1) != LOCK_SUCCESS)
369 msgq(sp, M_SYSERR, "063|Unable to lock recovery file");
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;
378 if ((p = strrchr(t, '/')) == NULL)
384 if ((st = rcv_dlnwrite(sp, "file", t, fp))) {
389 if ((st = rcv_dlnwrite(sp, "path", cp_path, fp))) {
395 MALLOC(sp, host, hostmax + 1);
398 (void)gethostname(host, hostmax + 1);
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). */
410 if ((qt = quote(t)) == NULL) {
412 msgq(sp, M_SYSERR, NULL);
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);
425 msgq(sp, M_SYSERR, NULL);
430 * Format the message. (Yes, I know it's silly.)
431 * Requires that the message end in a <newline>.
434 for (t1 = buf; len > 0; len -= t2 - t1, t1 = t2) {
435 /* Check for a short length. */
436 if (len <= FMTCOLS) {
441 /* Check for a required <newline>. */
442 t2 = strchr(t1, '\n');
443 if (t2 - t1 <= FMTCOLS)
446 /* Find the closest space, if any. */
447 for (t3 = t2; t2 > t1; --t2)
449 if (t2 - t1 <= FMTCOLS)
455 /* t2 points to the last character to display. */
458 /* t2 points one after the last character to display. */
459 if (fwrite(t1, 1, t2 - t1, fp) != t2 - t1) {
467 rcv_email(sp, mpath);
472 werr: msgq(sp, M_SYSERR, "065|Recovery file");
487 * never exactly the same
488 * just like a snowflake
491 * List the files that can be recovered by this user.
493 * PUBLIC: int rcv_list(SCR *);
503 char *p, *file, *path;
507 /* Open the recovery directory for reading. */
508 if (opts_empty(sp, O_RECDIR, 0))
510 p = O_STR(sp, O_RECDIR);
511 if (chdir(p) || (dirp = opendir(".")) == NULL) {
512 msgq_str(sp, M_SYSERR, p, "recdir: %s");
516 /* Read the directory. */
517 for (found = 0; (dp = readdir(dirp)) != NULL;) {
518 if (strncmp(dp->d_name, "recover.", 8))
521 /* If it's readable, it's recoverable. */
522 if ((fp = fopen(dp->d_name, "r")) == NULL)
525 switch (file_lock(sp, NULL, fileno(fp), 1)) {
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
538 /* If it's locked, it's live. */
543 /* Check the headers. */
544 for (file = NULL, path = NULL;
545 file == NULL || path == NULL;) {
546 if ((st = rcv_dlnread(sp, &dtype, &data, fp))) {
548 msgq_str(sp, M_ERR, dp->d_name,
549 "066|%s: malformed recovery file");
554 if (!strcmp(dtype, "file"))
556 else if (!strcmp(dtype, "path"))
563 * If the file doesn't exist, it's an orphaned recovery file,
567 * This can occur if the backup file was deleted and we crashed
568 * before deleting the email file.
571 if (stat(path, &sb) &&
573 (void)unlink(dp->d_name);
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);
583 /* Close, discarding lock. */
584 next: (void)fclose(fp);
589 (void)printf("%s: No files to recover\n", getprogname());
590 (void)closedir(dirp);
596 * Start a recovered file as the file to edit.
598 * PUBLIC: int rcv_read(SCR *, FREF *);
601 rcv_read(SCR *sp, FREF *frp)
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;
615 if (opts_empty(sp, O_RECDIR, 0))
617 rp = O_STR(sp, O_RECDIR);
618 if ((dirp = opendir(rp)) == NULL) {
619 msgq_str(sp, M_SYSERR, rp, "%s");
626 for (found = requested = 0; (dp = readdir(dirp)) != NULL;) {
627 if (strncmp(dp->d_name, "recover.", 8))
629 if ((recpath = join(rp, dp->d_name)) == NULL) {
630 msgq(sp, M_SYSERR, NULL);
634 /* If it's readable, it's recoverable. */
635 if ((fp = fopen(recpath, "r")) == NULL) {
640 switch (file_lock(sp, NULL, fileno(fp), 1)) {
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
655 /* If it's locked, it's live. */
660 /* Check the headers. */
661 for (file = NULL, path = NULL;
662 file == NULL || path == NULL;) {
663 if ((st = rcv_dlnread(sp, &dtype, &data, fp))) {
665 msgq_str(sp, M_ERR, dp->d_name,
666 "067|%s: malformed recovery file");
671 if (!strcmp(dtype, "file"))
673 else if (!strcmp(dtype, "path"))
681 * If the file doesn't exist, it's an orphaned recovery file,
685 * This can occur if the backup file was deleted and we crashed
686 * before deleting the email file.
689 if (stat(path, &sb) &&
691 (void)unlink(dp->d_name);
695 /* Check the file name. */
696 if (strcmp(file, name))
701 /* If we've found more than one, take the most recent. */
702 (void)fstat(fileno(fp), &sb);
704 timespeccmp(&rec_mtim, &sb.st_mtimespec, <)) {
713 rec_mtim = sb.st_mtimespec;
716 sv_fd = dup(fileno(fp));
724 (void)closedir(dirp);
727 msgq_str(sp, M_INFO, name,
728 "068|No files named %s, readable by you, to recover");
734 "069|There are older versions of this file for you to recover");
735 if (found > requested)
737 "070|There are other files for you to recover");
741 * Create the FREF structure, start the btree file.
744 * file_init() is going to set ep->rcv_path.
746 if (file_init(sp, frp, pathp, 0)) {
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.
760 ep->rcv_mpath = recp;
763 F_SET(frp, FR_UNLOCKED);
765 /* We believe the file is recoverable. */
772 * Copy a recovery file.
775 rcv_copy(SCR *sp, int wfd, char *fname)
777 int nr, nw, off, rfd;
780 if ((rfd = open(fname, O_RDONLY, 0)) == -1)
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)
789 err: msgq_str(sp, M_SYSERR, fname, "%s");
795 * Paranoid make temporary file routine.
798 rcv_mktemp(SCR *sp, char *path, char *dname)
802 if ((fd = mkstemp(path)) == -1)
803 msgq_str(sp, M_SYSERR, dname, "%s");
812 rcv_email(SCR *sp, char *fname)
816 (void)asprintf(&buf, _PATH_SENDMAIL " -odb -t < %s", fname);
818 msgq_str(sp, M_ERR, strerror(errno),
819 "071|not sending email: %s");
828 * Encode a string into an X-vi-data line and write it.
831 rcv_dlnwrite(SCR *sp, const char *dtype, const char *src, FILE *fp)
839 dlen = strlen(dtype);
840 GET_SPACE_GOTOC(sp, bp, blen, (len + 2) / 3 * 4 + dlen + 2);
841 (void)memcpy(bp, dtype, dlen);
843 if ((xlen = b64_ntop((u_char *)src,
844 len, bp + dlen + 1, blen)) == -1)
848 /* Output as an MIME folding header. */
849 if ((plen = fprintf(fp, VI_DHEADER " %.*s\n",
850 FMTCOLS - (int)sizeof(VI_DHEADER), bp)) < 0)
852 plen -= (int)sizeof(VI_DHEADER) + 1;
853 for (p = bp, xlen -= plen; xlen > 0; xlen -= plen) {
855 if ((plen = fprintf(fp, " %.*s\n", FMTCOLS - 1, p)) < 0)
859 FREE_SPACE(sp, bp, blen);
862 err: FREE_SPACE(sp, bp, blen);
865 msgq(sp, M_SYSERR, NULL);
871 * Read an X-vi-data line and decode it.
874 rcv_dlnread(SCR *sp, char **dtypep,
875 char **datap, /* free *datap if != NULL after use. */
880 char *bp = NULL, *p, *src;
882 size_t len, off, dlen;
886 if (fgets(buf, sizeof(buf), fp) == NULL)
888 if (strncmp(buf, VI_DHEADER, sizeof(VI_DHEADER) - 1)) {
894 /* Fetch an MIME folding header. */
895 len = strlen(buf) - sizeof(VI_DHEADER) + 1;
896 GET_SPACE_GOTOC(sp, bp, blen, len);
897 (void)memcpy(bp, buf + sizeof(VI_DHEADER) - 1, len);
899 while ((ch = fgetc(fp)) == ' ') {
900 if (fgets(buf, sizeof(buf), fp) == NULL)
904 ADD_SPACE_GOTOC(sp, bp, blen, len);
906 (void)memcpy(p, buf, off);
909 (void)ungetc(ch, fp);
911 for (p = bp; *p == ' ' || *p == '\n'; p++);
912 if ((src = strchr(p, ';')) == NULL)
918 /* Memory looks like: "<data>\0<dtype>\0". */
919 MALLOC(sp, data, dlen + len / 4 * 3 + 2);
922 if ((xlen = (b64_pton(p + dlen + 1,
923 (u_char *)data, len / 4 * 3 + 1))) == -1) {
928 dtype = data + xlen + 1;
929 (void)memcpy(dtype, p, dlen);
931 FREE_SPACE(sp, bp, blen);
936 err: FREE_SPACE(sp, bp, blen);
939 msgq(sp, M_SYSERR, NULL);