New [key-bindings]:
- session-save: captures cwd and foreground process argv to ~/.local/share/foot/state/{name}.json
- session-save-secure: prompts for a password, encrypts the scrollback with argon2id + XChaCha20-Poly1305 (libsodium) and writes it to {name}.scrollback.enc(stores up to 1Mb scrollback buffer).
- session-load: a minimal fuzzy picker that displays saved sessions (both secure and vanilla), UI piggybacks on search bar subsurface. use arrows to navigate and delete to delete a previously saved session.
854 lines
21 KiB
C
854 lines
21 KiB
C
#include "session.h"
|
|
|
|
#include <dirent.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
|
|
#include <termios.h>
|
|
|
|
#define LOG_MODULE "session"
|
|
#define LOG_ENABLE_DBG 0
|
|
#include "log.h"
|
|
#include "xmalloc.h"
|
|
|
|
#define JSON_SUFFIX ".json"
|
|
#define JSON_SUFFIX_LEN 5
|
|
#define SCROLLBACK_SUFFIX ".scrollback.txt"
|
|
#define SCROLLBACK_ENC_SUFFIX ".scrollback.enc"
|
|
#define SCROLLBACK_MAX_BYTES (1u << 20) /* 1 MB cap */
|
|
|
|
static int
|
|
state_dir(char *buf, size_t buf_sz)
|
|
{
|
|
const char *xdg = getenv("XDG_DATA_HOME");
|
|
int n;
|
|
if (xdg != NULL && xdg[0] == '/')
|
|
n = snprintf(buf, buf_sz, "%s/foot/state", xdg);
|
|
else {
|
|
const char *home = getenv("HOME");
|
|
if (home == NULL)
|
|
return -1;
|
|
n = snprintf(buf, buf_sz, "%s/.local/share/foot/state", home);
|
|
}
|
|
if (n < 0 || (size_t)n >= buf_sz)
|
|
return -1;
|
|
return n;
|
|
}
|
|
|
|
static bool
|
|
mkdir_p(const char *path)
|
|
{
|
|
char tmp[4096];
|
|
size_t len = strlen(path);
|
|
if (len >= sizeof(tmp))
|
|
return false;
|
|
memcpy(tmp, path, len + 1);
|
|
|
|
for (size_t i = 1; i < len; i++) {
|
|
if (tmp[i] == '/') {
|
|
tmp[i] = '\0';
|
|
if (mkdir(tmp, 0700) < 0 && errno != EEXIST) {
|
|
LOG_ERRNO("mkdir(%s)", tmp);
|
|
return false;
|
|
}
|
|
tmp[i] = '/';
|
|
}
|
|
}
|
|
if (mkdir(tmp, 0700) < 0 && errno != EEXIST) {
|
|
LOG_ERRNO("mkdir(%s)", tmp);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static int
|
|
session_path(const char *name, char *buf, size_t buf_sz)
|
|
{
|
|
char dir[1024];
|
|
if (state_dir(dir, sizeof(dir)) < 0)
|
|
return -1;
|
|
int n = snprintf(buf, buf_sz, "%s/%s.json", dir, name);
|
|
if (n < 0 || (size_t)n >= buf_sz)
|
|
return -1;
|
|
return n;
|
|
}
|
|
|
|
bool
|
|
session_name_is_valid(const char *name)
|
|
{
|
|
if (name == NULL || name[0] == '\0')
|
|
return false;
|
|
for (const char *p = name; *p != '\0'; p++) {
|
|
char c = *p;
|
|
bool ok = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
|
|
(c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-';
|
|
if (!ok)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void
|
|
session_state_free(struct session_state *st)
|
|
{
|
|
if (st == NULL)
|
|
return;
|
|
free(st->cwd);
|
|
if (st->argv != NULL) {
|
|
for (int i = 0; i < st->argc; i++)
|
|
free(st->argv[i]);
|
|
free(st->argv);
|
|
}
|
|
st->cwd = NULL;
|
|
st->argv = NULL;
|
|
st->argc = 0;
|
|
}
|
|
|
|
#if defined(__linux__)
|
|
static char *
|
|
read_link(const char *path)
|
|
{
|
|
char *buf = NULL;
|
|
size_t cap = 256;
|
|
for (;;) {
|
|
buf = xrealloc(buf, cap);
|
|
ssize_t n = readlink(path, buf, cap - 1);
|
|
if (n < 0) {
|
|
free(buf);
|
|
return NULL;
|
|
}
|
|
if ((size_t)n < cap - 1) {
|
|
buf[n] = '\0';
|
|
return buf;
|
|
}
|
|
cap *= 2;
|
|
if (cap > 1 << 16) {
|
|
free(buf);
|
|
return NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool
|
|
read_cmdline(pid_t pid, int *out_argc, char ***out_argv)
|
|
{
|
|
char path[64];
|
|
snprintf(path, sizeof(path), "/proc/%d/cmdline", (int)pid);
|
|
|
|
int fd = open(path, O_RDONLY | O_CLOEXEC);
|
|
if (fd < 0)
|
|
return false;
|
|
|
|
char *buf = NULL;
|
|
size_t len = 0;
|
|
size_t cap = 0;
|
|
char chunk[1024];
|
|
for (;;) {
|
|
ssize_t n = read(fd, chunk, sizeof(chunk));
|
|
if (n < 0) {
|
|
if (errno == EINTR)
|
|
continue;
|
|
close(fd);
|
|
free(buf);
|
|
return false;
|
|
}
|
|
if (n == 0)
|
|
break;
|
|
if (len + (size_t)n + 1 > cap) {
|
|
cap = (cap == 0 ? 1024 : cap * 2);
|
|
while (len + (size_t)n + 1 > cap)
|
|
cap *= 2;
|
|
buf = xrealloc(buf, cap);
|
|
}
|
|
memcpy(buf + len, chunk, n);
|
|
len += n;
|
|
}
|
|
close(fd);
|
|
|
|
if (buf == NULL || len == 0) {
|
|
free(buf);
|
|
return false;
|
|
}
|
|
/* /proc/<pid>/cmdline is NUL-separated; ensure trailing NUL */
|
|
if (buf[len - 1] != '\0') {
|
|
buf = xrealloc(buf, len + 1);
|
|
buf[len] = '\0';
|
|
len++;
|
|
}
|
|
|
|
/* Count args */
|
|
int argc = 0;
|
|
for (size_t i = 0; i < len; i++)
|
|
if (buf[i] == '\0' && (i == 0 || buf[i - 1] != '\0'))
|
|
argc++;
|
|
|
|
if (argc <= 0) {
|
|
free(buf);
|
|
return false;
|
|
}
|
|
|
|
char **argv = xmalloc(sizeof(char *) * (argc + 1));
|
|
int idx = 0;
|
|
size_t start = 0;
|
|
for (size_t i = 0; i < len && idx < argc; i++) {
|
|
if (buf[i] == '\0') {
|
|
argv[idx++] = xstrdup(buf + start);
|
|
start = i + 1;
|
|
}
|
|
}
|
|
argv[argc] = NULL;
|
|
free(buf);
|
|
|
|
*out_argc = argc;
|
|
*out_argv = argv;
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
bool
|
|
session_capture(int ptmx, const char *fallback_cwd, struct session_state *out)
|
|
{
|
|
memset(out, 0, sizeof(*out));
|
|
|
|
#if defined(__linux__)
|
|
pid_t fg = tcgetpgrp(ptmx);
|
|
if (fg > 0) {
|
|
char path[64];
|
|
snprintf(path, sizeof(path), "/proc/%d/cwd", (int)fg);
|
|
char *cwd = read_link(path);
|
|
if (cwd != NULL)
|
|
out->cwd = cwd;
|
|
|
|
int argc = 0;
|
|
char **argv = NULL;
|
|
if (read_cmdline(fg, &argc, &argv)) {
|
|
/* If the foreground process is a shell (login or otherwise), don't
|
|
* try to resurrect it via argv — let the user's normal shell
|
|
* launch take over. We only restore non-shell processes. */
|
|
const char *base = argv[0];
|
|
const char *slash = strrchr(base, '/');
|
|
if (slash != NULL)
|
|
base = slash + 1;
|
|
const char *shells[] = {
|
|
"bash", "zsh", "fish", "sh", "dash", "ksh", "tcsh", "csh",
|
|
"-bash", "-zsh", "-fish", "-sh", "-dash"
|
|
};
|
|
bool is_shell = false;
|
|
for (size_t i = 0; i < sizeof(shells) / sizeof(shells[0]); i++) {
|
|
if (strcmp(base, shells[i]) == 0) {
|
|
is_shell = true;
|
|
break;
|
|
}
|
|
}
|
|
if (is_shell) {
|
|
for (int i = 0; i < argc; i++)
|
|
free(argv[i]);
|
|
free(argv);
|
|
} else {
|
|
out->argc = argc;
|
|
out->argv = argv;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (out->cwd == NULL && fallback_cwd != NULL)
|
|
out->cwd = xstrdup(fallback_cwd);
|
|
|
|
return out->cwd != NULL;
|
|
}
|
|
|
|
/* ------------- JSON encoding/decoding (minimal, hand-rolled) ------------- */
|
|
|
|
static void
|
|
json_escape(FILE *fp, const char *s)
|
|
{
|
|
fputc('"', fp);
|
|
for (const char *p = s; *p != '\0'; p++) {
|
|
unsigned char c = (unsigned char)*p;
|
|
switch (c) {
|
|
case '"': fputs("\\\"", fp); break;
|
|
case '\\': fputs("\\\\", fp); break;
|
|
case '\b': fputs("\\b", fp); break;
|
|
case '\f': fputs("\\f", fp); break;
|
|
case '\n': fputs("\\n", fp); break;
|
|
case '\r': fputs("\\r", fp); break;
|
|
case '\t': fputs("\\t", fp); break;
|
|
default:
|
|
if (c < 0x20)
|
|
fprintf(fp, "\\u%04x", c);
|
|
else
|
|
fputc(c, fp);
|
|
}
|
|
}
|
|
fputc('"', fp);
|
|
}
|
|
|
|
bool
|
|
session_save(const char *name, const struct session_state *st)
|
|
{
|
|
if (!session_name_is_valid(name)) {
|
|
LOG_ERR("invalid session name: %s", name == NULL ? "(null)" : name);
|
|
return false;
|
|
}
|
|
if (st == NULL || st->cwd == NULL) {
|
|
LOG_ERR("session_save: missing cwd");
|
|
return false;
|
|
}
|
|
|
|
char dir[1024];
|
|
if (state_dir(dir, sizeof(dir)) < 0) {
|
|
LOG_ERR("could not resolve state directory");
|
|
return false;
|
|
}
|
|
if (!mkdir_p(dir))
|
|
return false;
|
|
|
|
char path[1280];
|
|
if (session_path(name, path, sizeof(path)) < 0)
|
|
return false;
|
|
|
|
char tmp_path[1408];
|
|
snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", path);
|
|
|
|
FILE *fp = fopen(tmp_path, "we");
|
|
if (fp == NULL) {
|
|
LOG_ERRNO("fopen(%s)", tmp_path);
|
|
return false;
|
|
}
|
|
|
|
fputs("{\n \"cwd\": ", fp);
|
|
json_escape(fp, st->cwd);
|
|
if (st->argc > 0 && st->argv != NULL) {
|
|
fputs(",\n \"argv\": [", fp);
|
|
for (int i = 0; i < st->argc; i++) {
|
|
if (i > 0)
|
|
fputs(", ", fp);
|
|
json_escape(fp, st->argv[i]);
|
|
}
|
|
fputc(']', fp);
|
|
}
|
|
fputs("\n}\n", fp);
|
|
|
|
if (fflush(fp) != 0 || fsync(fileno(fp)) != 0) {
|
|
LOG_ERRNO("fsync(%s)", tmp_path);
|
|
fclose(fp);
|
|
unlink(tmp_path);
|
|
return false;
|
|
}
|
|
fclose(fp);
|
|
|
|
if (rename(tmp_path, path) < 0) {
|
|
LOG_ERRNO("rename(%s -> %s)", tmp_path, path);
|
|
unlink(tmp_path);
|
|
return false;
|
|
}
|
|
|
|
LOG_INFO("session saved: %s", path);
|
|
return true;
|
|
}
|
|
|
|
/* ------------- JSON parsing (just enough for our schema) ------------- */
|
|
|
|
struct parser {
|
|
const char *p;
|
|
const char *end;
|
|
};
|
|
|
|
static void
|
|
skip_ws(struct parser *pa)
|
|
{
|
|
while (pa->p < pa->end) {
|
|
char c = *pa->p;
|
|
if (c == ' ' || c == '\t' || c == '\n' || c == '\r')
|
|
pa->p++;
|
|
else
|
|
break;
|
|
}
|
|
}
|
|
|
|
static bool
|
|
parse_string(struct parser *pa, char **out)
|
|
{
|
|
skip_ws(pa);
|
|
if (pa->p >= pa->end || *pa->p != '"')
|
|
return false;
|
|
pa->p++;
|
|
|
|
size_t cap = 64;
|
|
size_t len = 0;
|
|
char *buf = xmalloc(cap);
|
|
|
|
while (pa->p < pa->end && *pa->p != '"') {
|
|
char c = *pa->p++;
|
|
char decoded;
|
|
if (c == '\\') {
|
|
if (pa->p >= pa->end) { free(buf); return false; }
|
|
char esc = *pa->p++;
|
|
switch (esc) {
|
|
case '"': decoded = '"'; break;
|
|
case '\\': decoded = '\\'; break;
|
|
case '/': decoded = '/'; break;
|
|
case 'b': decoded = '\b'; break;
|
|
case 'f': decoded = '\f'; break;
|
|
case 'n': decoded = '\n'; break;
|
|
case 'r': decoded = '\r'; break;
|
|
case 't': decoded = '\t'; break;
|
|
case 'u': {
|
|
if (pa->end - pa->p < 4) { free(buf); return false; }
|
|
unsigned val = 0;
|
|
for (int i = 0; i < 4; i++) {
|
|
char h = pa->p[i];
|
|
val <<= 4;
|
|
if (h >= '0' && h <= '9') val |= h - '0';
|
|
else if (h >= 'a' && h <= 'f') val |= h - 'a' + 10;
|
|
else if (h >= 'A' && h <= 'F') val |= h - 'A' + 10;
|
|
else { free(buf); return false; }
|
|
}
|
|
pa->p += 4;
|
|
if (val < 0x80) {
|
|
decoded = (char)val;
|
|
} else {
|
|
/* UTF-8 encode codepoint (no surrogate-pair handling) */
|
|
char enc[4];
|
|
int n;
|
|
if (val < 0x800) {
|
|
enc[0] = (char)(0xc0 | (val >> 6));
|
|
enc[1] = (char)(0x80 | (val & 0x3f));
|
|
n = 2;
|
|
} else {
|
|
enc[0] = (char)(0xe0 | (val >> 12));
|
|
enc[1] = (char)(0x80 | ((val >> 6) & 0x3f));
|
|
enc[2] = (char)(0x80 | (val & 0x3f));
|
|
n = 3;
|
|
}
|
|
if (len + (size_t)n + 1 > cap) {
|
|
while (len + (size_t)n + 1 > cap) cap *= 2;
|
|
buf = xrealloc(buf, cap);
|
|
}
|
|
memcpy(buf + len, enc, n);
|
|
len += n;
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
default: free(buf); return false;
|
|
}
|
|
} else {
|
|
decoded = c;
|
|
}
|
|
if (len + 2 > cap) {
|
|
cap *= 2;
|
|
buf = xrealloc(buf, cap);
|
|
}
|
|
buf[len++] = decoded;
|
|
}
|
|
if (pa->p >= pa->end) { free(buf); return false; }
|
|
pa->p++; /* consume closing quote */
|
|
buf[len] = '\0';
|
|
*out = buf;
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
expect_char(struct parser *pa, char c)
|
|
{
|
|
skip_ws(pa);
|
|
if (pa->p >= pa->end || *pa->p != c)
|
|
return false;
|
|
pa->p++;
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
session_load(const char *name, struct session_state *out)
|
|
{
|
|
memset(out, 0, sizeof(*out));
|
|
|
|
if (!session_name_is_valid(name)) {
|
|
LOG_ERR("invalid session name: %s", name == NULL ? "(null)" : name);
|
|
return false;
|
|
}
|
|
|
|
char path[1280];
|
|
if (session_path(name, path, sizeof(path)) < 0)
|
|
return false;
|
|
|
|
int fd = open(path, O_RDONLY | O_CLOEXEC);
|
|
if (fd < 0) {
|
|
LOG_ERRNO("open(%s)", path);
|
|
return false;
|
|
}
|
|
struct stat sb;
|
|
if (fstat(fd, &sb) < 0 || sb.st_size <= 0 || sb.st_size > (off_t)(1 << 20)) {
|
|
LOG_ERR("session file %s has unexpected size", path);
|
|
close(fd);
|
|
return false;
|
|
}
|
|
char *data = xmalloc(sb.st_size + 1);
|
|
ssize_t got = 0;
|
|
while (got < sb.st_size) {
|
|
ssize_t n = read(fd, data + got, sb.st_size - got);
|
|
if (n < 0) {
|
|
if (errno == EINTR) continue;
|
|
free(data);
|
|
close(fd);
|
|
return false;
|
|
}
|
|
if (n == 0) break;
|
|
got += n;
|
|
}
|
|
close(fd);
|
|
data[got] = '\0';
|
|
|
|
struct parser pa = { .p = data, .end = data + got };
|
|
bool ok = false;
|
|
|
|
if (!expect_char(&pa, '{')) goto done;
|
|
|
|
for (;;) {
|
|
skip_ws(&pa);
|
|
if (pa.p < pa.end && *pa.p == '}') { pa.p++; break; }
|
|
|
|
char *key = NULL;
|
|
if (!parse_string(&pa, &key)) goto done;
|
|
if (!expect_char(&pa, ':')) { free(key); goto done; }
|
|
|
|
if (strcmp(key, "cwd") == 0) {
|
|
free(key);
|
|
if (!parse_string(&pa, &out->cwd)) goto done;
|
|
} else if (strcmp(key, "argv") == 0) {
|
|
free(key);
|
|
if (!expect_char(&pa, '[')) goto done;
|
|
int cap = 4;
|
|
out->argv = xmalloc(sizeof(char *) * cap);
|
|
out->argc = 0;
|
|
for (;;) {
|
|
skip_ws(&pa);
|
|
if (pa.p < pa.end && *pa.p == ']') { pa.p++; break; }
|
|
if (out->argc + 1 >= cap) {
|
|
cap *= 2;
|
|
out->argv = xrealloc(out->argv, sizeof(char *) * cap);
|
|
}
|
|
if (!parse_string(&pa, &out->argv[out->argc])) goto done;
|
|
out->argc++;
|
|
skip_ws(&pa);
|
|
if (pa.p < pa.end && *pa.p == ',') { pa.p++; continue; }
|
|
}
|
|
out->argv[out->argc] = NULL;
|
|
} else {
|
|
free(key);
|
|
/* unknown key: skip a string value */
|
|
char *junk = NULL;
|
|
if (!parse_string(&pa, &junk)) goto done;
|
|
free(junk);
|
|
}
|
|
|
|
skip_ws(&pa);
|
|
if (pa.p < pa.end && *pa.p == ',') { pa.p++; continue; }
|
|
}
|
|
|
|
ok = out->cwd != NULL;
|
|
|
|
done:
|
|
free(data);
|
|
if (!ok)
|
|
session_state_free(out);
|
|
return ok;
|
|
}
|
|
|
|
/* ------------- listing ------------- */
|
|
|
|
static int
|
|
cmp_names(const void *a, const void *b)
|
|
{
|
|
return strcmp(*(const char *const *)a, *(const char *const *)b);
|
|
}
|
|
|
|
char **
|
|
session_list(size_t *out_count)
|
|
{
|
|
*out_count = 0;
|
|
|
|
char dir[1024];
|
|
if (state_dir(dir, sizeof(dir)) < 0)
|
|
return NULL;
|
|
|
|
DIR *d = opendir(dir);
|
|
if (d == NULL)
|
|
return NULL;
|
|
|
|
size_t cap = 8;
|
|
size_t count = 0;
|
|
char **names = xmalloc(sizeof(char *) * cap);
|
|
|
|
struct dirent *de;
|
|
while ((de = readdir(d)) != NULL) {
|
|
const char *n = de->d_name;
|
|
size_t nlen = strlen(n);
|
|
if (nlen <= JSON_SUFFIX_LEN)
|
|
continue;
|
|
if (strcmp(n + nlen - JSON_SUFFIX_LEN, JSON_SUFFIX) != 0)
|
|
continue;
|
|
char *stripped = xmalloc(nlen - JSON_SUFFIX_LEN + 1);
|
|
memcpy(stripped, n, nlen - JSON_SUFFIX_LEN);
|
|
stripped[nlen - JSON_SUFFIX_LEN] = '\0';
|
|
if (!session_name_is_valid(stripped)) {
|
|
free(stripped);
|
|
continue;
|
|
}
|
|
if (count >= cap) {
|
|
cap *= 2;
|
|
names = xrealloc(names, sizeof(char *) * cap);
|
|
}
|
|
names[count++] = stripped;
|
|
}
|
|
closedir(d);
|
|
|
|
qsort(names, count, sizeof(char *), cmp_names);
|
|
|
|
*out_count = count;
|
|
return names;
|
|
}
|
|
|
|
bool
|
|
session_exists(const char *name)
|
|
{
|
|
if (!session_name_is_valid(name))
|
|
return false;
|
|
char path[1280];
|
|
if (session_path(name, path, sizeof(path)) < 0)
|
|
return false;
|
|
return access(path, F_OK) == 0;
|
|
}
|
|
|
|
bool
|
|
session_delete(const char *name)
|
|
{
|
|
if (!session_name_is_valid(name))
|
|
return false;
|
|
char path[1280];
|
|
if (session_path(name, path, sizeof(path)) < 0)
|
|
return false;
|
|
if (unlink(path) < 0) {
|
|
LOG_ERRNO("unlink(%s)", path);
|
|
return false;
|
|
}
|
|
session_delete_scrollback(name);
|
|
session_delete_enc_scrollback(name);
|
|
LOG_INFO("session deleted: %s", path);
|
|
return true;
|
|
}
|
|
|
|
static int
|
|
scrollback_path(const char *name, char *buf, size_t buf_sz)
|
|
{
|
|
char dir[1024];
|
|
if (state_dir(dir, sizeof(dir)) < 0)
|
|
return -1;
|
|
int n = snprintf(buf, buf_sz, "%s/%s%s", dir, name, SCROLLBACK_SUFFIX);
|
|
if (n < 0 || (size_t)n >= buf_sz)
|
|
return -1;
|
|
return n;
|
|
}
|
|
|
|
bool
|
|
session_load_scrollback(const char *name, char **out_text, size_t *out_len)
|
|
{
|
|
*out_text = NULL;
|
|
*out_len = 0;
|
|
if (!session_name_is_valid(name))
|
|
return false;
|
|
|
|
char path[1280];
|
|
if (scrollback_path(name, path, sizeof(path)) < 0)
|
|
return false;
|
|
|
|
int fd = open(path, O_RDONLY | O_CLOEXEC);
|
|
if (fd < 0)
|
|
return false;
|
|
|
|
struct stat sb;
|
|
if (fstat(fd, &sb) < 0 || sb.st_size < 0 ||
|
|
(size_t)sb.st_size > SCROLLBACK_MAX_BYTES * 4 /* generous on load */) {
|
|
close(fd);
|
|
return false;
|
|
}
|
|
if (sb.st_size == 0) {
|
|
close(fd);
|
|
return false;
|
|
}
|
|
|
|
char *data = xmalloc(sb.st_size + 1);
|
|
ssize_t got = 0;
|
|
while (got < sb.st_size) {
|
|
ssize_t n = read(fd, data + got, sb.st_size - got);
|
|
if (n < 0) {
|
|
if (errno == EINTR) continue;
|
|
free(data);
|
|
close(fd);
|
|
return false;
|
|
}
|
|
if (n == 0) break;
|
|
got += n;
|
|
}
|
|
close(fd);
|
|
data[got] = '\0';
|
|
|
|
*out_text = data;
|
|
*out_len = (size_t)got;
|
|
return true;
|
|
}
|
|
|
|
void
|
|
session_delete_scrollback(const char *name)
|
|
{
|
|
if (!session_name_is_valid(name))
|
|
return;
|
|
char path[1280];
|
|
if (scrollback_path(name, path, sizeof(path)) < 0)
|
|
return;
|
|
unlink(path);
|
|
}
|
|
|
|
static int
|
|
enc_path(const char *name, char *buf, size_t buf_sz)
|
|
{
|
|
char dir[1024];
|
|
if (state_dir(dir, sizeof(dir)) < 0)
|
|
return -1;
|
|
int n = snprintf(buf, buf_sz, "%s/%s%s", dir, name, SCROLLBACK_ENC_SUFFIX);
|
|
if (n < 0 || (size_t)n >= buf_sz)
|
|
return -1;
|
|
return n;
|
|
}
|
|
|
|
bool
|
|
session_has_enc_scrollback(const char *name)
|
|
{
|
|
if (!session_name_is_valid(name))
|
|
return false;
|
|
char path[1280];
|
|
if (enc_path(name, path, sizeof(path)) < 0)
|
|
return false;
|
|
return access(path, F_OK) == 0;
|
|
}
|
|
|
|
void
|
|
session_delete_enc_scrollback(const char *name)
|
|
{
|
|
if (!session_name_is_valid(name))
|
|
return;
|
|
char path[1280];
|
|
if (enc_path(name, path, sizeof(path)) < 0)
|
|
return;
|
|
unlink(path);
|
|
}
|
|
|
|
bool
|
|
session_write_enc_blob(const char *name, const unsigned char *blob, size_t len)
|
|
{
|
|
if (!session_name_is_valid(name))
|
|
return false;
|
|
char dir[1024];
|
|
if (state_dir(dir, sizeof(dir)) < 0)
|
|
return false;
|
|
if (!mkdir_p(dir))
|
|
return false;
|
|
char path[1280];
|
|
if (enc_path(name, path, sizeof(path)) < 0)
|
|
return false;
|
|
char tmp_path[1408];
|
|
snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", path);
|
|
|
|
/* 0600 — encrypted but still reduces accidental exposure. */
|
|
int fd = open(tmp_path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0600);
|
|
if (fd < 0) {
|
|
LOG_ERRNO("open(%s)", tmp_path);
|
|
return false;
|
|
}
|
|
size_t written = 0;
|
|
while (written < len) {
|
|
ssize_t n = write(fd, blob + written, len - written);
|
|
if (n < 0) {
|
|
if (errno == EINTR) continue;
|
|
LOG_ERRNO("write(%s)", tmp_path);
|
|
close(fd);
|
|
unlink(tmp_path);
|
|
return false;
|
|
}
|
|
written += n;
|
|
}
|
|
if (fsync(fd) < 0) {
|
|
LOG_ERRNO("fsync(%s)", tmp_path);
|
|
close(fd);
|
|
unlink(tmp_path);
|
|
return false;
|
|
}
|
|
close(fd);
|
|
if (rename(tmp_path, path) < 0) {
|
|
LOG_ERRNO("rename(%s -> %s)", tmp_path, path);
|
|
unlink(tmp_path);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
session_read_enc_blob(const char *name, unsigned char **out, size_t *out_len)
|
|
{
|
|
*out = NULL;
|
|
*out_len = 0;
|
|
if (!session_name_is_valid(name))
|
|
return false;
|
|
char path[1280];
|
|
if (enc_path(name, path, sizeof(path)) < 0)
|
|
return false;
|
|
int fd = open(path, O_RDONLY | O_CLOEXEC);
|
|
if (fd < 0)
|
|
return false;
|
|
struct stat sb;
|
|
if (fstat(fd, &sb) < 0 || sb.st_size <= 0 ||
|
|
(size_t)sb.st_size > SCROLLBACK_MAX_BYTES * 4) {
|
|
close(fd);
|
|
return false;
|
|
}
|
|
unsigned char *buf = xmalloc(sb.st_size);
|
|
ssize_t got = 0;
|
|
while (got < sb.st_size) {
|
|
ssize_t n = read(fd, buf + got, sb.st_size - got);
|
|
if (n < 0) {
|
|
if (errno == EINTR) continue;
|
|
free(buf);
|
|
close(fd);
|
|
return false;
|
|
}
|
|
if (n == 0) break;
|
|
got += n;
|
|
}
|
|
close(fd);
|
|
if (got != sb.st_size) {
|
|
free(buf);
|
|
return false;
|
|
}
|
|
*out = buf;
|
|
*out_len = (size_t)got;
|
|
return true;
|
|
}
|
|
|
|
void
|
|
session_free_names(char **names, size_t count)
|
|
{
|
|
if (names == NULL)
|
|
return;
|
|
for (size_t i = 0; i < count; i++)
|
|
free(names[i]);
|
|
free(names);
|
|
}
|