esc
Externally Scriptable Editor
git clone git://mccd.space/esc
| Log | Files | Refs | README |
fuse_ipc.c (21367B)
1 #define FUSE_USE_VERSION 26
2 #include <fuse.h>
3
4 #include "editor.h"
5 #include "fuse_ipc.h"
6 #include "strbuf.h"
7 #include <SDL3/SDL.h>
8 #include <errno.h>
9 #include <stdio.h>
10 #include <stdlib.h>
11 #include <string.h>
12 #include <sys/stat.h>
13 #include <sys/types.h>
14 #include <unistd.h>
15
16 /* Per-open write accumulator (stored in fi->fh cast to/from uintptr_t) */
17 typedef struct {
18 char *data;
19 size_t len;
20 size_t cap;
21 } WriteBuffer;
22
23 /*
24 * In-memory temp file slot. Used for the create→write→rename workflow
25 * that tools like `sed -i` perform. Slots are identified by FUSE path;
26 * an empty path[0] means the slot is free.
27 */
28 #define MAX_TEMP_FILES 8
29 typedef struct {
30 char path[256];
31 char *data;
32 size_t len;
33 } TempFile;
34
35 /* FUSE private data passed as private_data to fuse_new */
36 typedef struct {
37 Editor *ed;
38 TempFile temps[MAX_TEMP_FILES];
39 } FuseCtx;
40
41 struct FuseIPC {
42 struct fuse *fuse;
43 struct fuse_chan *chan;
44 SDL_Thread *thread;
45 char mountpoint[256];
46 FuseCtx *ctx;
47 };
48
49 static FuseCtx *get_ctx(void) {
50 return (FuseCtx *)fuse_get_context()->private_data;
51 }
52
53 /* ---- temp-file helpers ------------------------------------------------ */
54
55 static TempFile *find_temp(FuseCtx *ctx, const char *path) {
56 for (int i = 0; i < MAX_TEMP_FILES; i++) {
57 if (ctx->temps[i].path[0] &&
58 strcmp(ctx->temps[i].path, path) == 0)
59 return &ctx->temps[i];
60 }
61 return NULL;
62 }
63
64 static TempFile *alloc_temp(FuseCtx *ctx, const char *path) {
65 for (int i = 0; i < MAX_TEMP_FILES; i++) {
66 if (!ctx->temps[i].path[0]) {
67 strncpy(ctx->temps[i].path, path,
68 sizeof(ctx->temps[i].path) - 1);
69 ctx->temps[i].data = NULL;
70 ctx->temps[i].len = 0;
71 return &ctx->temps[i];
72 }
73 }
74 return NULL;
75 }
76
77 static void free_temp(TempFile *tf) {
78 free(tf->data);
79 tf->data = NULL;
80 tf->len = 0;
81 tf->path[0] = '\0';
82 }
83
84 /* Push a wakeup event so SDL_WaitEvent unblocks and the frame is redrawn. */
85 static void wake_render(void) {
86 SDL_Event ev;
87 memset(&ev, 0, sizeof(ev));
88 ev.type = SDL_EVENT_USER;
89 SDL_PushEvent(&ev);
90 }
91
92 /* ---- path helpers ---------------------------------------------------- */
93
94 /*
95 * Parse /buffer/N/sub paths.
96 * Returns slot index (>=0) and sets *sub to the sub-file name.
97 * Returns -2 for /buffer itself.
98 * Returns -3 for /buffer/N (slot directory).
99 * Returns -1 if not a /buffer/... path.
100 */
101 static int parse_buffer_path(const char *path, const char **sub) {
102 if (strcmp(path, "/buffer") == 0) {
103 if (sub) *sub = NULL;
104 return -2;
105 }
106 if (strncmp(path, "/buffer/", 8) != 0)
107 return -1;
108 const char *rest = path + 8;
109 /* rest should be N or N/sub */
110 char *slash = strchr(rest, '/');
111 int idx = atoi(rest);
112 if (idx < 0)
113 return -1;
114 /* Validate that 'rest' is really a number */
115 const char *p = rest;
116 if (*p == '-') p++;
117 if (*p < '0' || *p > '9')
118 return -1;
119 if (!slash) {
120 if (sub) *sub = NULL;
121 return -3; /* slot directory */
122 }
123 if (sub) *sub = slash + 1;
124 return idx;
125 }
126
127 static int is_dir(const char *path) {
128 if (strcmp(path, "/") == 0 || strcmp(path, "/buffer") == 0)
129 return 1;
130 const char *sub = NULL;
131 int r = parse_buffer_path(path, &sub);
132 return r == -3; /* /buffer/N with no sub-file */
133 }
134
135 static int is_symlink(const char *path) { return strcmp(path, "/cwd") == 0; }
136
137 static int is_file(const char *path) {
138 if (strcmp(path, "/cursor") == 0)
139 return 1;
140 if (strcmp(path, "/buffer/count") == 0)
141 return 1;
142 const char *sub = NULL;
143 int idx = parse_buffer_path(path, &sub);
144 if (idx < 0 || !sub)
145 return 0;
146 return strcmp(sub, "body") == 0 || strcmp(sub, "path") == 0 ||
147 strcmp(sub, "ranges") == 0 || strcmp(sub, "range") == 0;
148 }
149
150 static int is_writable(const char *path) {
151 if (strcmp(path, "/cursor") == 0)
152 return 1;
153 const char *sub = NULL;
154 int idx = parse_buffer_path(path, &sub);
155 if (idx < 0 || !sub)
156 return 0;
157 return strcmp(sub, "body") == 0 || strcmp(sub, "path") == 0 ||
158 strcmp(sub, "range") == 0;
159 }
160
161 /* ---- ranges serialisation -------------------------------------------- */
162
163 /*
164 * Build a malloc'd text representation of ed->ranges filtered to
165 * [slot_start, slot_end). Pass slot_start=0, slot_end=INT_MAX for all.
166 * Offsets in output are relative to slot_start.
167 * Caller must hold editor lock.
168 */
169 static char *build_ranges_filtered(Editor *ed, int slot_start, int slot_end,
170 size_t *out_len) {
171 size_t total = 0;
172 char tmp[128];
173
174 for (size_t i = 0; i < ed->ranges_count; i++) {
175 EditorRange *r = &ed->ranges[i];
176 if (r->end_byte <= slot_start || r->start_byte >= slot_end)
177 continue;
178 int s = r->start_byte - slot_start;
179 int e = r->end_byte - slot_start;
180 if (r->type == RANGE_FORMAT)
181 total += snprintf(tmp, sizeof(tmp),
182 "format %d %d %d %d\n", s, e,
183 r->data.format.bold ? 1 : 0,
184 r->data.format.italic ? 1 : 0);
185 else
186 total += snprintf(tmp, sizeof(tmp),
187 "replacement %d %d %d\n", s, e,
188 r->data.replacement.visual_cols);
189 }
190
191 char *buf = malloc(total + 1);
192 if (!buf)
193 return NULL;
194
195 size_t pos = 0;
196 for (size_t i = 0; i < ed->ranges_count; i++) {
197 EditorRange *r = &ed->ranges[i];
198 if (r->end_byte <= slot_start || r->start_byte >= slot_end)
199 continue;
200 int s = r->start_byte - slot_start;
201 int e = r->end_byte - slot_start;
202 if (r->type == RANGE_FORMAT)
203 pos += sprintf(buf + pos, "format %d %d %d %d\n", s, e,
204 r->data.format.bold ? 1 : 0,
205 r->data.format.italic ? 1 : 0);
206 else
207 pos += sprintf(buf + pos, "replacement %d %d %d\n",
208 s, e,
209 r->data.replacement.visual_cols);
210 }
211 buf[pos] = '\0';
212 *out_len = pos;
213 return buf;
214 }
215
216 static char *build_ranges(Editor *ed, size_t *out_len) {
217 return build_ranges_filtered(ed, 0, (int)ed->text.len + 1, out_len);
218 }
219
220 /* ---- FUSE callbacks -------------------------------------------------- */
221
222 static int op_getattr(const char *path, struct stat *st) {
223 memset(st, 0, sizeof(*st));
224 FuseCtx *ctx = get_ctx();
225 Editor *ed = ctx->ed;
226
227 if (is_dir(path)) {
228 st->st_mode = S_IFDIR | 0755;
229 st->st_nlink = 2;
230 return 0;
231 }
232
233 if (is_symlink(path)) {
234 char tmp[64];
235 st->st_mode = S_IFLNK | 0777;
236 st->st_nlink = 1;
237 st->st_size =
238 snprintf(tmp, sizeof(tmp), "/proc/%d/cwd", (int)getpid());
239 return 0;
240 }
241
242 if (is_file(path)) {
243 st->st_mode = S_IFREG | 0644;
244 st->st_nlink = 1;
245
246 editor_lock(ed);
247
248 if (strcmp(path, "/cursor") == 0) {
249 char tmp[64];
250 st->st_size = snprintf(tmp, sizeof(tmp), "%d %d\n",
251 ed->cursor_idx,
252 ed->selection_anchor);
253 } else if (strcmp(path, "/buffer/count") == 0) {
254 char tmp[32];
255 st->st_size =
256 snprintf(tmp, sizeof(tmp), "%d\n", ed->files_count);
257 } else {
258 const char *sub = NULL;
259 int idx = parse_buffer_path(path, &sub);
260 if (idx >= 0 && idx < ed->files_count && sub) {
261 FileSlot *s = &ed->files[idx];
262 if (strcmp(sub, "body") == 0) {
263 st->st_size =
264 (off_t)(s->buf_end - s->buf_start);
265 } else if (strcmp(sub, "path") == 0) {
266 st->st_size = s->path
267 ? (off_t)strlen(s->path)
268 : 0;
269 } else if (strcmp(sub, "ranges") == 0) {
270 size_t sz;
271 char *tmp = build_ranges_filtered(
272 ed, s->buf_start, s->buf_end, &sz);
273 st->st_size = (off_t)sz;
274 free(tmp);
275 } else if (strcmp(sub, "range") == 0) {
276 if (!s->has_range) {
277 editor_unlock(ed);
278 return -ENOENT;
279 }
280 char tmp[64];
281 st->st_size = snprintf(
282 tmp, sizeof(tmp), "%d %d\n",
283 s->range_start, s->range_end);
284 }
285 }
286 }
287
288 editor_unlock(ed);
289 return 0;
290 }
291
292 /* In-flight or committed temp files */
293 TempFile *tf = find_temp(ctx, path);
294 if (tf) {
295 st->st_mode = S_IFREG | 0644;
296 st->st_nlink = 1;
297 st->st_size = (off_t)tf->len;
298 return 0;
299 }
300
301 return -ENOENT;
302 }
303
304 static int op_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
305 off_t offset, struct fuse_file_info *fi) {
306 (void)offset;
307 (void)fi;
308 FuseCtx *ctx = get_ctx();
309 Editor *ed = ctx->ed;
310
311 filler(buf, ".", NULL, 0);
312 filler(buf, "..", NULL, 0);
313
314 if (strcmp(path, "/") == 0) {
315 filler(buf, "cursor", NULL, 0);
316 filler(buf, "cwd", NULL, 0);
317 filler(buf, "buffer", NULL, 0);
318 return 0;
319 }
320 if (strcmp(path, "/buffer") == 0) {
321 filler(buf, "count", NULL, 0);
322 editor_lock(ed);
323 int n = ed->files_count;
324 editor_unlock(ed);
325 char tmp[32];
326 for (int i = 0; i < n; i++) {
327 snprintf(tmp, sizeof(tmp), "%d", i);
328 filler(buf, tmp, NULL, 0);
329 }
330 return 0;
331 }
332 /* /buffer/N */
333 const char *sub = NULL;
334 int idx = parse_buffer_path(path, &sub);
335 if (idx == -3) {
336 filler(buf, "body", NULL, 0);
337 filler(buf, "path", NULL, 0);
338 filler(buf, "ranges", NULL, 0);
339 /* range only shown when has_range */
340 editor_lock(ed);
341 int slot_idx = -parse_buffer_path(path, NULL) - 3;
342 /* re-parse to get numeric index */
343 const char *rest = path + 8;
344 int sidx = atoi(rest);
345 bool hr = (sidx >= 0 && sidx < ed->files_count &&
346 ed->files[sidx].has_range);
347 editor_unlock(ed);
348 if (hr)
349 filler(buf, "range", NULL, 0);
350 (void)slot_idx;
351 return 0;
352 }
353 return -ENOENT;
354 }
355
356 static int op_readlink(const char *path, char *buf, size_t size) {
357 if (!is_symlink(path))
358 return -ENOENT;
359 snprintf(buf, size, "/proc/%d/cwd", (int)getpid());
360 return 0;
361 }
362
363 static int op_open(const char *path, struct fuse_file_info *fi) {
364 fi->fh = 0;
365
366 if (!is_file(path)) {
367 /* Allow opening an existing temp file for reading */
368 FuseCtx *ctx = get_ctx();
369 if (find_temp(ctx, path))
370 return 0;
371 return -ENOENT;
372 }
373
374 if (!is_writable(path)) {
375 if ((fi->flags & O_ACCMODE) != O_RDONLY)
376 return -EACCES;
377 return 0;
378 }
379
380 /* Read-only open on a writable file: no write buffer needed */
381 if ((fi->flags & O_ACCMODE) == O_RDONLY)
382 return 0;
383
384 WriteBuffer *wb = calloc(1, sizeof(WriteBuffer));
385 if (!wb)
386 return -ENOMEM;
387 fi->fh = (uint64_t)(uintptr_t)wb;
388 return 0;
389 }
390
391 /*
392 * op_create — called when a new file is created (e.g. sed -i's temp file).
393 * Allow creation inside any /buffer/N/ prefix.
394 */
395 static int op_create(const char *path, mode_t mode, struct fuse_file_info *fi) {
396 (void)mode;
397 FuseCtx *ctx = get_ctx();
398
399 if (is_file(path) || is_dir(path))
400 return -EEXIST;
401
402 /* Must be under /buffer/N/ */
403 const char *sub = NULL;
404 int idx = parse_buffer_path(path, &sub);
405 if (idx < 0 || !sub)
406 return -EACCES;
407
408 TempFile *tf = find_temp(ctx, path);
409 if (!tf) {
410 tf = alloc_temp(ctx, path);
411 if (!tf)
412 return -ENOSPC;
413 }
414
415 WriteBuffer *wb = calloc(1, sizeof(WriteBuffer));
416 if (!wb)
417 return -ENOMEM;
418
419 fi->fh = (uint64_t)(uintptr_t)wb;
420 return 0;
421 }
422
423 static int op_read(const char *path, char *buf, size_t size, off_t offset,
424 struct fuse_file_info *fi) {
425 (void)fi;
426 FuseCtx *ctx = get_ctx();
427 Editor *ed = ctx->ed;
428
429 if (strcmp(path, "/cursor") == 0) {
430 editor_lock(ed);
431 char tmp[64];
432 int n = snprintf(tmp, sizeof(tmp), "%d %d\n", ed->cursor_idx,
433 ed->selection_anchor);
434 editor_unlock(ed);
435
436 if (offset >= (off_t)n)
437 return 0;
438 size_t to_copy = (size_t)(n - offset);
439 if (to_copy > size)
440 to_copy = size;
441 memcpy(buf, tmp + offset, to_copy);
442 return (int)to_copy;
443 }
444
445 if (strcmp(path, "/buffer/count") == 0) {
446 editor_lock(ed);
447 char tmp[32];
448 int n = snprintf(tmp, sizeof(tmp), "%d\n", ed->files_count);
449 editor_unlock(ed);
450 if (offset >= (off_t)n)
451 return 0;
452 size_t to_copy = (size_t)(n - offset);
453 if (to_copy > size) to_copy = size;
454 memcpy(buf, tmp + offset, to_copy);
455 return (int)to_copy;
456 }
457
458 /* /buffer/N/... */
459 const char *sub = NULL;
460 int idx = parse_buffer_path(path, &sub);
461 if (idx >= 0 && sub) {
462 editor_lock(ed);
463 if (idx >= ed->files_count) {
464 editor_unlock(ed);
465 return -ENOENT;
466 }
467 FileSlot *s = &ed->files[idx];
468
469 if (strcmp(sub, "body") == 0) {
470 int slot_len = s->buf_end - s->buf_start;
471 if (offset >= (off_t)slot_len) {
472 editor_unlock(ed);
473 return 0;
474 }
475 size_t to_copy = (size_t)(slot_len - offset);
476 if (to_copy > size) to_copy = size;
477 memcpy(buf, ed->text.data + s->buf_start + offset, to_copy);
478 editor_unlock(ed);
479 return (int)to_copy;
480 }
481
482 if (strcmp(sub, "path") == 0) {
483 size_t flen = s->path ? strlen(s->path) : 0;
484 if (offset >= (off_t)flen) {
485 editor_unlock(ed);
486 return 0;
487 }
488 size_t to_copy = flen - (size_t)offset;
489 if (to_copy > size) to_copy = size;
490 memcpy(buf, s->path + offset, to_copy);
491 editor_unlock(ed);
492 return (int)to_copy;
493 }
494
495 if (strcmp(sub, "ranges") == 0) {
496 size_t rlen;
497 char *rbuf = build_ranges_filtered(ed, s->buf_start,
498 s->buf_end, &rlen);
499 editor_unlock(ed);
500 if (!rbuf) return -ENOMEM;
501 if (offset >= (off_t)rlen) { free(rbuf); return 0; }
502 size_t to_copy = rlen - (size_t)offset;
503 if (to_copy > size) to_copy = size;
504 memcpy(buf, rbuf + offset, to_copy);
505 free(rbuf);
506 return (int)to_copy;
507 }
508
509 if (strcmp(sub, "range") == 0) {
510 if (!s->has_range) {
511 editor_unlock(ed);
512 return -ENOENT;
513 }
514 char tmp[64];
515 int n = snprintf(tmp, sizeof(tmp), "%d %d\n",
516 s->range_start, s->range_end);
517 editor_unlock(ed);
518 if (offset >= (off_t)n) return 0;
519 size_t to_copy = (size_t)(n - offset);
520 if (to_copy > size) to_copy = size;
521 memcpy(buf, tmp + offset, to_copy);
522 return (int)to_copy;
523 }
524
525 editor_unlock(ed);
526 return -ENOENT;
527 }
528
529 /* temp files */
530 TempFile *tf = find_temp(ctx, path);
531 if (tf) {
532 if (offset >= (off_t)tf->len) return 0;
533 size_t to_copy = tf->len - (size_t)offset;
534 if (to_copy > size) to_copy = size;
535 memcpy(buf, tf->data + offset, to_copy);
536 return (int)to_copy;
537 }
538
539 return -ENOENT;
540 }
541
542 static int op_write(const char *path, const char *buf, size_t size,
543 off_t offset, struct fuse_file_info *fi) {
544 (void)path;
545 if (!fi->fh)
546 return -EACCES;
547
548 WriteBuffer *wb = (WriteBuffer *)(uintptr_t)fi->fh;
549 size_t end = (size_t)offset + size;
550
551 if (end > wb->cap) {
552 size_t new_cap = end < 128 ? 128 : end * 2;
553 char *nd = realloc(wb->data, new_cap);
554 if (!nd)
555 return -ENOMEM;
556 wb->data = nd;
557 wb->cap = new_cap;
558 }
559
560 memcpy(wb->data + offset, buf, size);
561 if (end > wb->len)
562 wb->len = end;
563 return (int)size;
564 }
565
566 static int op_truncate(const char *path, off_t size) {
567 (void)size;
568 /* Accept truncate on writable files; we reset on open anyway */
569 if (is_writable(path))
570 return 0;
571 /* Also accept on temp files */
572 FuseCtx *ctx = get_ctx();
573 if (find_temp(ctx, path))
574 return 0;
575 return -EACCES;
576 }
577
578 static int op_ftruncate(const char *path, off_t size,
579 struct fuse_file_info *fi) {
580 (void)path;
581 if (!fi->fh)
582 return 0;
583 WriteBuffer *wb = (WriteBuffer *)(uintptr_t)fi->fh;
584 if ((size_t)size < wb->len)
585 wb->len = (size_t)size;
586 return 0;
587 }
588
589 /* Apply content to the editor body (slot 0, legacy) and wake render loop. */
590 static void apply_body(Editor *ed, const char *data, size_t len) {
591 editor_replace_body(ed, data, len);
592 wake_render();
593 }
594
595 /* Apply content to a specific slot by index. Creates slot if needed. */
596 static void apply_slot_body(Editor *ed, int idx, const char *data, size_t len) {
597 editor_lock(ed);
598 if (idx == ed->files_count) {
599 /* Create a new slot */
600 editor_unlock(ed);
601 editor_add_file_slot(ed, NULL, data, (int)len);
602 } else if (idx < ed->files_count) {
603 /* Replace existing slot content */
604 FileSlot *s = &ed->files[idx];
605 int old_len = s->buf_end - s->buf_start;
606 /* Delete old content */
607 strbuf_delete(&ed->text, s->buf_start, old_len);
608 /* Adjust all slot boundaries as if a delete happened */
609 for (int i = 0; i < ed->files_count; i++) {
610 if (i == idx) continue;
611 if (ed->files[i].buf_start >= s->buf_end)
612 ed->files[i].buf_start -= old_len;
613 if (ed->files[i].buf_end > s->buf_start)
614 ed->files[i].buf_end -= old_len;
615 }
616 s->buf_end = s->buf_start;
617 /* Insert new content */
618 if (data && len > 0) {
619 strbuf_insert(&ed->text, s->buf_start, data, len);
620 /* Adjust boundaries after insert */
621 s->buf_end = s->buf_start + (int)len;
622 for (int i = 0; i < ed->files_count; i++) {
623 if (i == idx) continue;
624 if (ed->files[i].buf_start >= s->buf_start)
625 ed->files[i].buf_start += (int)len;
626 if (ed->files[i].buf_end > s->buf_start)
627 ed->files[i].buf_end += (int)len;
628 }
629 }
630 s->dirty = true;
631 editor_unlock(ed);
632 } else {
633 editor_unlock(ed);
634 }
635 wake_render();
636 }
637
638 static int op_release(const char *path, struct fuse_file_info *fi) {
639 FuseCtx *ctx = get_ctx();
640 Editor *ed = ctx->ed;
641
642 if (!fi->fh)
643 return 0;
644
645 WriteBuffer *wb = (WriteBuffer *)(uintptr_t)fi->fh;
646 fi->fh = 0;
647
648 /*
649 * Temp file: save the accumulated data into the TempFile slot so
650 * op_rename can pick it up. Do NOT apply to the editor yet.
651 */
652 TempFile *tf = find_temp(ctx, path);
653 if (tf) {
654 free(tf->data);
655 tf->data = wb->data; /* steal */
656 tf->len = wb->len;
657 wb->data = NULL;
658 free(wb);
659 return 0;
660 }
661
662 if (strcmp(path, "/cursor") == 0 && wb->data && wb->len > 0) {
663 char *tmp = malloc(wb->len + 1);
664 int cidx = 0, sidx = 0;
665 if (tmp) {
666 memcpy(tmp, wb->data, wb->len);
667 tmp[wb->len] = '\0';
668 sscanf(tmp, "%d %d", &cidx, &sidx);
669 free(tmp);
670 }
671 editor_lock(ed);
672 ed->cursor_idx = cidx;
673 ed->selection_anchor = sidx;
674 editor_unlock(ed);
675 wake_render();
676 } else {
677 const char *sub = NULL;
678 int idx = parse_buffer_path(path, &sub);
679 if (idx >= 0 && sub) {
680 if (strcmp(sub, "body") == 0) {
681 apply_slot_body(ed, idx, wb->data, wb->len);
682 } else if (strcmp(sub, "path") == 0 && wb->data) {
683 char *p = malloc(wb->len + 1);
684 if (p) {
685 memcpy(p, wb->data, wb->len);
686 p[wb->len] = '\0';
687 editor_lock(ed);
688 if (idx < ed->files_count) {
689 free(ed->files[idx].path);
690 ed->files[idx].path = p;
691 } else {
692 free(p);
693 }
694 editor_unlock(ed);
695 }
696 } else if (strcmp(sub, "range") == 0 && wb->data) {
697 char *tmp = malloc(wb->len + 1);
698 if (tmp) {
699 memcpy(tmp, wb->data, wb->len);
700 tmp[wb->len] = '\0';
701 int rs = 0, re = 0;
702 sscanf(tmp, "%d %d", &rs, &re);
703 free(tmp);
704 editor_set_file_slot_range(ed, idx, rs, re);
705 }
706 }
707 }
708 }
709
710 free(wb->data);
711 free(wb);
712 return 0;
713 }
714
715 static int op_rename(const char *from, const char *to) {
716 FuseCtx *ctx = get_ctx();
717 Editor *ed = ctx->ed;
718
719 TempFile *tf = find_temp(ctx, from);
720 if (!tf)
721 return -ENOENT;
722
723 const char *sub = NULL;
724 int idx = parse_buffer_path(to, &sub);
725 if (idx >= 0 && sub && strcmp(sub, "body") == 0)
726 apply_slot_body(ed, idx, tf->data, tf->len);
727
728 free_temp(tf);
729 return 0;
730 }
731
732 static int op_unlink(const char *path) {
733 FuseCtx *ctx = get_ctx();
734 TempFile *tf = find_temp(ctx, path);
735 if (!tf)
736 return -ENOENT;
737 free_temp(tf);
738 return 0;
739 }
740
741 /* ---- FUSE thread ----------------------------------------------------- */
742
743 static int fuse_thread_fn(void *arg) {
744 fuse_loop((struct fuse *)arg);
745 return 0;
746 }
747
748 /* ---- operations table ------------------------------------------------ */
749
750 static const struct fuse_operations ops = {
751 .getattr = op_getattr,
752 .readdir = op_readdir,
753 .readlink = op_readlink,
754 .open = op_open,
755 .create = op_create,
756 .read = op_read,
757 .write = op_write,
758 .truncate = op_truncate,
759 .ftruncate = op_ftruncate,
760 .release = op_release,
761 .rename = op_rename,
762 .unlink = op_unlink,
763 };
764
765 /* ---- Public API ------------------------------------------------------ */
766
767 FuseIPC *fuse_ipc_start(Editor *ed) {
768 FuseIPC *ipc = calloc(1, sizeof(FuseIPC));
769 if (!ipc)
770 return NULL;
771
772 FuseCtx *ctx = calloc(1, sizeof(FuseCtx));
773 if (!ctx) {
774 free(ipc);
775 return NULL;
776 }
777 ctx->ed = ed;
778 ipc->ctx = ctx;
779
780 const char *xdg = getenv("XDG_RUNTIME_DIR");
781 const char *base = xdg ? xdg : "/tmp";
782
783 char esc_dir[240];
784 snprintf(esc_dir, sizeof(esc_dir), "%s/esc", base);
785 mkdir(esc_dir, 0700); /* ignore EEXIST */
786
787 snprintf(ipc->mountpoint, sizeof(ipc->mountpoint), "%s/esc/%d", base,
788 (int)getpid());
789
790 if (mkdir(ipc->mountpoint, 0700) != 0) {
791 if (errno == EEXIST) {
792 /*
793 * Stale mount from a previous crash — detach it so
794 * we can reuse the directory.
795 */
796 char cmd[512];
797 snprintf(cmd, sizeof(cmd),
798 "fusermount -u -- %s 2>/dev/null",
799 ipc->mountpoint);
800 system(cmd); /* ignore errors */
801 } else {
802 SDL_Log("fuse_ipc: mkdir %s failed: %s",
803 ipc->mountpoint, strerror(errno));
804 free(ctx);
805 free(ipc);
806 return NULL;
807 }
808 }
809
810 struct fuse_args args = FUSE_ARGS_INIT(0, NULL);
811
812 ipc->chan = fuse_mount(ipc->mountpoint, &args);
813 if (!ipc->chan) {
814 SDL_Log("fuse_ipc: fuse_mount failed");
815 rmdir(ipc->mountpoint);
816 free(ctx);
817 free(ipc);
818 return NULL;
819 }
820
821 ipc->fuse = fuse_new(ipc->chan, &args, &ops, sizeof(ops), ctx);
822 if (!ipc->fuse) {
823 SDL_Log("fuse_ipc: fuse_new failed");
824 fuse_unmount(ipc->mountpoint, ipc->chan);
825 rmdir(ipc->mountpoint);
826 free(ctx);
827 free(ipc);
828 return NULL;
829 }
830
831 ipc->thread = SDL_CreateThread(fuse_thread_fn, "FuseIPC", ipc->fuse);
832 if (!ipc->thread) {
833 SDL_Log("fuse_ipc: SDL_CreateThread failed");
834 fuse_destroy(ipc->fuse);
835 fuse_unmount(ipc->mountpoint, ipc->chan);
836 rmdir(ipc->mountpoint);
837 free(ctx);
838 free(ipc);
839 return NULL;
840 }
841
842 SDL_Log("fuse_ipc: mounted at %s", ipc->mountpoint);
843 return ipc;
844 }
845
846 void fuse_ipc_stop(FuseIPC *ipc) {
847 if (!ipc)
848 return;
849 /* Unmount first: tears down the kernel-side channel, causing
850 * fuse_loop's blocking read to return an error and the thread
851 * to exit. Calling fuse_exit alone is not enough because
852 * fuse_loop only checks the exit flag after receiving a request. */
853 fuse_unmount(ipc->mountpoint, ipc->chan);
854 SDL_WaitThread(ipc->thread, NULL);
855 fuse_destroy(ipc->fuse);
856 rmdir(ipc->mountpoint);
857 free(ipc->ctx);
858 free(ipc);
859 }