add session save and session load functionality
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.
This commit is contained in:
parent
05ee680778
commit
cabddb26e6
16 changed files with 1947 additions and 49 deletions
854
session.c
Normal file
854
session.c
Normal file
|
|
@ -0,0 +1,854 @@
|
|||
#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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue