#include "config.h"
#include <sys/queue.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <sys/file.h>
#include <bitstring.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <paths.h>
#include <pwd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include "common.h"
#define VI_FHEADER "X-vi-recover-file: "
#define VI_PHEADER "X-vi-recover-path: "
static int rcv_copy(SCR *, int, char *);
static void rcv_email(SCR *, int);
static char *rcv_gets(char *, size_t, int);
static int rcv_mailfile(SCR *, int, char *);
static int rcv_mktemp(SCR *, char *, char *, int);
static int rcv_openat(SCR *, int, const char *, int *);
int
rcv_tmp(SCR *sp, EXF *ep, char *name)
{
struct stat sb;
static int warned = 0;
int fd;
char *dp, *p, path[PATH_MAX];
if (opts_empty(sp, O_RECDIR, 0))
goto err;
dp = O_STR(sp, O_RECDIR);
if (stat(dp, &sb)) {
if (!warned) {
warned = 1;
msgq(sp, M_SYSERR, "%s", dp);
goto err;
}
return 1;
}
for (p = name; *p; ++p)
if (*p == '\n') {
msgq(sp, M_ERR,
"Files with newlines in the name are unrecoverable");
goto err;
}
(void)snprintf(path, sizeof(path), "%s/vi.XXXXXXXXXX", dp);
if ((fd = rcv_mktemp(sp, path, dp, S_IRWXU)) == -1)
goto err;
(void)close(fd);
if ((ep->rcv_path = strdup(path)) == NULL) {
msgq(sp, M_SYSERR, NULL);
(void)unlink(path);
err: msgq(sp, M_ERR,
"Modifications not recoverable if the session fails");
return (1);
}
F_SET(ep, F_RCV_ON);
return (0);
}
int
rcv_init(SCR *sp)
{
EXF *ep;
recno_t lno;
ep = sp->ep;
F_CLR(ep, F_FIRSTMODIFY);
if (!F_ISSET(ep, F_RCV_ON))
return (0);
F_CLR(ep, F_RCV_ON);
if (ep->rcv_mpath == NULL) {
if (rcv_mailfile(sp, 0, NULL))
goto err;
if (db_last(sp, &lno))
goto err;
sp->gp->scr_busy(sp,
"Copying file for recovery...", BUSY_ON);
if (ep->db->sync(ep->db, R_RECNOSYNC)) {
msgq_str(sp, M_SYSERR, ep->rcv_path,
"Preservation failed: %s");
sp->gp->scr_busy(sp, NULL, BUSY_OFF);
goto err;
}
sp->gp->scr_busy(sp, NULL, BUSY_OFF);
}
(void)chmod(ep->rcv_path, S_IRUSR | S_IWUSR);
F_SET(ep, F_RCV_ON);
return (0);
err: msgq(sp, M_ERR,
"Modifications not recoverable if the session fails");
return (1);
}
int
rcv_sync(SCR *sp, u_int flags)
{
EXF *ep;
int fd, rval;
char *dp, buf[1024];
ep = sp->ep;
if (ep == NULL || !F_ISSET(ep, F_RCV_ON))
return (0);
if (F_ISSET(ep, F_MODIFIED)) {
F_CLR(ep, F_RCV_SYNC);
if (ep->db->sync(ep->db, R_RECNOSYNC)) {
F_CLR(ep, F_RCV_ON | F_RCV_NORM);
msgq_str(sp, M_SYSERR,
ep->rcv_path, "File backup failed: %s");
return (1);
}
if (LF_ISSET(RCV_PRESERVE))
F_SET(ep, F_RCV_NORM);
if (LF_ISSET(RCV_EMAIL))
rcv_email(sp, ep->rcv_fd);
}
rval = 0;
if (LF_ISSET(RCV_SNAPSHOT)) {
if (opts_empty(sp, O_RECDIR, 0))
goto err;
dp = O_STR(sp, O_RECDIR);
(void)snprintf(buf, sizeof(buf), "%s/vi.XXXXXXXXXX", dp);
if ((fd = rcv_mktemp(sp, buf, dp, S_IRUSR | S_IWUSR)) == -1)
goto err;
sp->gp->scr_busy(sp,
"Copying file for recovery...", BUSY_ON);
if (rcv_copy(sp, fd, ep->rcv_path) ||
close(fd) || rcv_mailfile(sp, 1, buf)) {
(void)unlink(buf);
(void)close(fd);
rval = 1;
}
sp->gp->scr_busy(sp, NULL, BUSY_OFF);
}
if (0) {
err: rval = 1;
}
if (LF_ISSET(RCV_ENDSESSION))
F_SET(sp, SC_EXIT_FORCE);
return (rval);
}
static int
rcv_mailfile(SCR *sp, int issync, char *cp_path)
{
EXF *ep;
GS *gp;
struct passwd *pw;
size_t len;
time_t now;
uid_t uid;
int fd;
char *dp, *p, *t, buf[4096], mpath[PATH_MAX];
char *t1, *t2, *t3;
char host[HOST_NAME_MAX+1];
gp = sp->gp;
if ((pw = getpwuid(uid = getuid())) == NULL) {
msgq(sp, M_ERR,
"Information on user id %u not found", uid);
return (1);
}
if (opts_empty(sp, O_RECDIR, 0))
return (1);
dp = O_STR(sp, O_RECDIR);
(void)snprintf(mpath, sizeof(mpath), "%s/recover.XXXXXXXXXX", dp);
if ((fd = rcv_mktemp(sp, mpath, dp, S_IRUSR | S_IWUSR)) == -1)
return (1);
ep = sp->ep;
if (file_lock(sp, NULL, NULL, fd, 1) != LOCK_SUCCESS)
msgq(sp, M_SYSERR, "Unable to lock recovery file");
if (!issync) {
ep->rcv_fd = fd;
if ((ep->rcv_mpath = strdup(mpath)) == NULL) {
msgq(sp, M_SYSERR, NULL);
goto err;
}
cp_path = ep->rcv_path;
}
t = sp->frp->name;
if ((p = strrchr(t, '/')) == NULL)
p = t;
else
++p;
(void)time(&now);
(void)gethostname(host, sizeof(host));
len = snprintf(buf, sizeof(buf),
"%s%s\n%s%s\n%s\n%s\n%s%s\n%s%s\n%s\n%s\n\n",
VI_FHEADER, t,
VI_PHEADER, cp_path,
"Reply-To: root",
"From: root (Nvi recovery program)",
"To: ", pw->pw_name,
"Subject: Nvi saved the file ", p,
"Precedence: bulk",
"Auto-Submitted: auto-generated");
if (len > sizeof(buf) - 1)
goto lerr;
if (write(fd, buf, len) != len)
goto werr;
len = snprintf(buf, sizeof(buf),
"%s%.24s%s%s%s%s%s%s%s%s%s%s%s%s%s%s\n\n",
"On ", ctime(&now), ", the user ", pw->pw_name,
" was editing a file named ", t, " on the machine ",
host, ", when it was saved for recovery. ",
"You can recover most, if not all, of the changes ",
"to this file using the -r option to ", getprogname(), ":\n\n\t",
getprogname(), " -r ", t);
if (len > sizeof(buf) - 1) {
lerr: msgq(sp, M_ERR, "Recovery file buffer overrun");
goto err;
}
#define FMTCOLS 60
for (t1 = buf; len > 0; len -= t2 - t1, t1 = t2) {
if (len <= FMTCOLS) {
t2 = t1 + (len - 1);
goto wout;
}
t2 = strchr(t1, '\n');
if (t2 - t1 <= FMTCOLS)
goto wout;
for (t3 = t2; t2 > t1; --t2)
if (*t2 == ' ') {
if (t2 - t1 <= FMTCOLS)
goto wout;
t3 = t2;
}
t2 = t3;
wout: *t2++ = '\n';
if (write(fd, t1, t2 - t1) != t2 - t1)
goto werr;
}
if (issync) {
rcv_email(sp, fd);
if (close(fd)) {
werr: msgq(sp, M_SYSERR, "Recovery file");
goto err;
}
}
return (0);
err: if (!issync)
ep->rcv_fd = -1;
if (fd != -1)
(void)close(fd);
return (1);
}
static int
rcv_openat(SCR *sp, int dfd, const char *name, int *locked)
{
struct stat sb;
int fd, dummy;
fd = openat(dfd, name, O_RDONLY|O_NOFOLLOW|O_NONBLOCK);
if (fd == -1)
goto bad;
if (fstat(fd, &sb) == -1 || !S_ISREG(sb.st_mode) ||
(sb.st_mode & ALLPERMS) != (S_IRUSR | S_IWUSR))
goto bad;
if (locked == NULL)
locked = &dummy;
switch ((*locked = file_lock(sp, NULL, NULL, fd, 0))) {
case LOCK_FAILED:
break;
case LOCK_SUCCESS:
break;
case LOCK_UNAVAIL:
goto bad;
}
return fd;
bad:
if (fd != -1)
close(fd);
return -1;
}
int
rcv_list(SCR *sp)
{
struct dirent *dp;
struct stat sb;
DIR *dirp;
int fd;
FILE *fp;
int found;
char *p, *t, file[PATH_MAX], path[PATH_MAX];
if (opts_empty(sp, O_RECDIR, 0))
return (1);
p = O_STR(sp, O_RECDIR);
if ((dirp = opendir(p)) == NULL) {
msgq_str(sp, M_SYSERR, p, "recdir: %s");
return (1);
}
for (found = 0; (dp = readdir(dirp)) != NULL;) {
if (strncmp(dp->d_name, "recover.", 8))
continue;
if ((fd = rcv_openat(sp, dirfd(dirp), dp->d_name, NULL)) == -1)
continue;
if ((fp = fdopen(fd, "r")) == NULL) {
close(fd);
continue;
}
if (fgets(file, sizeof(file), fp) == NULL ||
strncmp(file, VI_FHEADER, sizeof(VI_FHEADER) - 1) ||
(p = strchr(file, '\n')) == NULL ||
fgets(path, sizeof(path), fp) == NULL ||
strncmp(path, VI_PHEADER, sizeof(VI_PHEADER) - 1) ||
(t = strchr(path, '\n')) == NULL) {
msgq_str(sp, M_ERR, dp->d_name,
"%s: malformed recovery file");
goto next;
}
*p = *t = '\0';
errno = 0;
if (stat(path + sizeof(VI_PHEADER) - 1, &sb) &&
errno == ENOENT) {
(void)unlinkat(dirfd(dirp), dp->d_name, 0);
goto next;
}
(void)fstat(fd, &sb);
(void)printf("%.24s: %s\n",
ctime(&sb.st_mtime), file + sizeof(VI_FHEADER) - 1);
found = 1;
next: (void)fclose(fp);
}
if (found == 0)
(void)printf("%s: No files to recover\n", getprogname());
(void)closedir(dirp);
return (0);
}
int
rcv_read(SCR *sp, FREF *frp)
{
struct dirent *dp;
struct stat sb;
DIR *dirp;
EXF *ep;
struct timespec rec_mtim;
int fd, found, lck, requested, sv_fd;
char *name, *p, *t, *rp, *recp, *pathp;
char file[PATH_MAX], path[PATH_MAX], recpath[PATH_MAX];
if (opts_empty(sp, O_RECDIR, 0))
return (1);
rp = O_STR(sp, O_RECDIR);
if ((dirp = opendir(rp)) == NULL) {
msgq_str(sp, M_SYSERR, rp, "%s");
return (1);
}
name = frp->name;
sv_fd = -1;
rec_mtim.tv_sec = rec_mtim.tv_nsec = 0;
recp = pathp = NULL;
for (found = requested = 0; (dp = readdir(dirp)) != NULL;) {
if (strncmp(dp->d_name, "recover.", 8))
continue;
if ((size_t)snprintf(recpath, sizeof(recpath), "%s/%s",
rp, dp->d_name) >= sizeof(recpath))
continue;
if ((fd = rcv_openat(sp, dirfd(dirp), dp->d_name, &lck)) == -1)
continue;
if (rcv_gets(file, sizeof(file), fd) == NULL ||
strncmp(file, VI_FHEADER, sizeof(VI_FHEADER) - 1) ||
(p = strchr(file, '\n')) == NULL ||
rcv_gets(path, sizeof(path), fd) == NULL ||
strncmp(path, VI_PHEADER, sizeof(VI_PHEADER) - 1) ||
(t = strchr(path, '\n')) == NULL) {
msgq_str(sp, M_ERR, recpath,
"%s: malformed recovery file");
goto next;
}
*p = *t = '\0';
++found;
errno = 0;
if (stat(path + sizeof(VI_PHEADER) - 1, &sb) &&
errno == ENOENT) {
(void)unlink(dp->d_name);
goto next;
}
if (strcmp(file + sizeof(VI_FHEADER) - 1, name))
goto next;
++requested;
(void)fstat(fd, &sb);
if (recp == NULL ||
timespeccmp(&rec_mtim, &sb.st_mtim, <)) {
p = recp;
t = pathp;
if ((recp = strdup(recpath)) == NULL) {
msgq(sp, M_SYSERR, NULL);
recp = p;
goto next;
}
if ((pathp = strdup(path)) == NULL) {
msgq(sp, M_SYSERR, NULL);
free(recp);
recp = p;
pathp = t;
goto next;
}
if (p != NULL) {
free(p);
free(t);
}
rec_mtim = sb.st_mtim;
if (sv_fd != -1)
(void)close(sv_fd);
sv_fd = fd;
} else
next: (void)close(fd);
}
(void)closedir(dirp);
if (recp == NULL) {
msgq_str(sp, M_INFO, name,
"No files named %s, readable by you, to recover");
return (1);
}
if (found) {
if (requested > 1)
msgq(sp, M_INFO,
"There are older versions of this file for you to recover");
if (found > requested)
msgq(sp, M_INFO,
"There are other files for you to recover");
}
if (file_init(sp, frp, pathp + sizeof(VI_PHEADER) - 1, 0)) {
free(recp);
free(pathp);
(void)close(sv_fd);
return (1);
}
ep = sp->ep;
ep->rcv_mpath = recp;
ep->rcv_fd = sv_fd;
if (lck != LOCK_SUCCESS)
F_SET(frp, FR_UNLOCKED);
F_SET(ep, F_RCV_ON);
return (0);
}
static int
rcv_copy(SCR *sp, int wfd, char *fname)
{
int nr, nw, off, rfd;
char buf[8 * 1024];
if ((rfd = open(fname, O_RDONLY)) == -1)
goto err;
while ((nr = read(rfd, buf, sizeof(buf))) > 0)
for (off = 0; nr; nr -= nw, off += nw)
if ((nw = write(wfd, buf + off, nr)) < 0)
goto err;
if (nr == 0)
return (0);
err: msgq_str(sp, M_SYSERR, fname, "%s");
return (1);
}
static char *
rcv_gets(char *buf, size_t len, int fd)
{
int nr;
char *p;
if ((nr = read(fd, buf, len - 1)) == -1)
return (NULL);
buf[nr] = '\0';
if ((p = strchr(buf, '\n')) == NULL)
return (NULL);
(void)lseek(fd, (off_t)((p - buf) + 1), SEEK_SET);
return (buf);
}
static int
rcv_mktemp(SCR *sp, char *path, char *dname, int perms)
{
int fd;
if ((fd = mkstemp(path)) == -1 || fchmod(fd, perms) == -1) {
msgq_str(sp, M_SYSERR, dname, "%s");
if (fd != -1) {
close(fd);
unlink(path);
fd = -1;
}
}
return (fd);
}
static void
rcv_email(SCR *sp, int fd)
{
struct stat sb;
pid_t pid;
if (O_ISSET(sp, O_SECURE))
return;
if (_PATH_SENDMAIL[0] != '/' || stat(_PATH_SENDMAIL, &sb) == -1)
msgq_str(sp, M_SYSERR,
_PATH_SENDMAIL, "not sending email: %s");
else {
switch (pid = fork()) {
case -1:
msgq(sp, M_SYSERR, "fork");
break;
case 0:
if (lseek(fd, 0, SEEK_SET) == -1) {
msgq(sp, M_SYSERR, "lseek");
_exit(127);
}
if (fd != STDIN_FILENO) {
if (dup2(fd, STDIN_FILENO) == -1) {
msgq(sp, M_SYSERR, "dup2");
_exit(127);
}
close(fd);
}
execl(_PATH_SENDMAIL, "sendmail", "-t", (char *)NULL);
msgq(sp, M_SYSERR, _PATH_SENDMAIL);
_exit(127);
default:
while (waitpid(pid, NULL, 0) == -1 && errno == EINTR)
continue;
break;
}
}
}