# HG changeset patch
# User MrJuneJune
# Date 1785801385 25200
# Node ID b8aa0850337832acc7edde23027f7cd1aea73cc1
# Parent 823f2a8b16c8ca9d62226216d8c84d62431af146
[tools] Add sandboxed online LaTeX editor
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
diff -r 823f2a8b16c8 -r b8aa08503378 mrjunejune/BUILD
--- a/mrjunejune/BUILD Mon Aug 03 15:26:44 2026 -0700
+++ b/mrjunejune/BUILD Mon Aug 03 16:56:25 2026 -0700
@@ -171,6 +171,14 @@
visibility = ["//mrjunejune/test:__pkg__"],
)
+cc_library(
+ name = "latex_renderer",
+ srcs = ["latex_renderer.c"],
+ hdrs = ["latex_renderer.h"],
+ copts = ["-D_GNU_SOURCE"],
+ visibility = ["//mrjunejune/test:__pkg__"],
+)
+
# Server binary
cc_binary(
name = "mrjunejune_server",
@@ -180,6 +188,7 @@
"//markdown_converter:markdown_to_html_c",
"//s3:s3",
"//deita:deita",
+ ":latex_renderer",
],
copts = ["-D_GNU_SOURCE"],
linkopts = ["-lpthread"],
@@ -200,6 +209,7 @@
"//markdown_converter:markdown_to_html_c",
"//s3:s3",
"//deita:deita",
+ ":latex_renderer",
],
copts = ["-D_GNU_SOURCE"],
linkopts = ["-lpthread"],
diff -r 823f2a8b16c8 -r b8aa08503378 mrjunejune/README.md
--- a/mrjunejune/README.md Mon Aug 03 15:26:44 2026 -0700
+++ b/mrjunejune/README.md Mon Aug 03 16:56:25 2026 -0700
@@ -39,6 +39,25 @@
bazel test //mrjunejune/test:theme_and_webp_test
```
+## LaTeX editor
+
+`/tools/latex_editor` sends source to the C server, which compiles it with the
+host's `/usr/bin/pdflatex` and returns the PDF directly to the browser. Install
+TeX Live on production hosts before deploying:
+
+```bash
+sudo apt-get install texlive-latex-base texlive-latex-recommended
+```
+
+The compiler child uses restricted TeX file access, Linux Landlock, a seccomp
+network filter, and CPU, memory, file-size, process, and wall-clock limits.
+Compilation fails closed when the Linux sandbox cannot be applied.
+
+```bash
+bazel test //mrjunejune/test:latex_renderer_test
+bazel test //mrjunejune/test:latex_editor_test
+```
+
## TODO
- Add caching layer
diff -r 823f2a8b16c8 -r b8aa08503378 mrjunejune/latex_renderer.c
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/latex_renderer.c Mon Aug 03 16:56:25 2026 -0700
@@ -0,0 +1,718 @@
+#include "mrjunejune/latex_renderer.h"
+
+#ifdef __linux__
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#ifndef LANDLOCK_ACCESS_FS_REFER
+#define LANDLOCK_ACCESS_FS_REFER (1ULL << 13)
+#endif
+#ifndef LANDLOCK_ACCESS_FS_TRUNCATE
+#define LANDLOCK_ACCESS_FS_TRUNCATE (1ULL << 14)
+#endif
+
+#if defined(__x86_64__)
+#define LATEX_AUDIT_ARCH AUDIT_ARCH_X86_64
+#elif defined(__aarch64__)
+#define LATEX_AUDIT_ARCH AUDIT_ARCH_AARCH64
+#else
+#define LATEX_AUDIT_ARCH 0
+#endif
+
+#define LATEX_DIAGNOSTICS_MAX_BYTES (16 * 1024)
+#define LATEX_CPU_TIMEOUT_SECONDS 5
+#define LATEX_WALL_TIMEOUT_SECONDS 8
+
+static char *duplicate_message(const char *message)
+{
+ size_t length = strlen(message);
+ char *copy = malloc(length + 1);
+ if (copy)
+ memcpy(copy, message, length + 1);
+ return copy;
+}
+
+static Latex_Render_Result result_with_message(
+ Latex_Render_Status status,
+ const char *message)
+{
+ return (Latex_Render_Result){
+ .status = status,
+ .diagnostics = duplicate_message(message),
+ };
+}
+
+static bool write_all(int fd, const uint8_t *data, size_t size)
+{
+ while (size > 0)
+ {
+ ssize_t written = write(fd, data, size);
+ if (written < 0)
+ {
+ if (errno == EINTR)
+ continue;
+ return false;
+ }
+ data += written;
+ size -= (size_t)written;
+ }
+ return true;
+}
+
+static int remove_tree(const char *path, unsigned depth)
+{
+ if (depth > 16)
+ return -1;
+
+ struct stat status;
+ if (lstat(path, &status) != 0)
+ return errno == ENOENT ? 0 : -1;
+ if (!S_ISDIR(status.st_mode))
+ return unlink(path);
+
+ DIR *directory = opendir(path);
+ if (!directory)
+ return -1;
+
+ int result = 0;
+ struct dirent *entry;
+ while ((entry = readdir(directory)) != NULL)
+ {
+ if (strcmp(entry->d_name, ".") == 0 ||
+ strcmp(entry->d_name, "..") == 0)
+ continue;
+
+ char child[1024];
+ int length = snprintf(
+ child,
+ sizeof(child),
+ "%s/%s",
+ path,
+ entry->d_name);
+ if (length < 0 ||
+ (size_t)length >= sizeof(child) ||
+ remove_tree(child, depth + 1) != 0)
+ {
+ result = -1;
+ break;
+ }
+ }
+ closedir(directory);
+ return result == 0 ? rmdir(path) : result;
+}
+
+static bool set_limit(int resource, rlim_t value)
+{
+ struct rlimit limit = {
+ .rlim_cur = value,
+ .rlim_max = value,
+ };
+ return setrlimit(resource, &limit) == 0;
+}
+
+static bool add_landlock_path_rule(
+ int ruleset_fd,
+ const char *path,
+ uint64_t access,
+ bool required)
+{
+ int path_fd = open(path, O_PATH | O_CLOEXEC);
+ if (path_fd < 0)
+ return !required && errno == ENOENT;
+
+ struct stat status;
+ bool ok = fstat(path_fd, &status) == 0;
+ if (ok && !S_ISDIR(status.st_mode))
+ {
+ access &= LANDLOCK_ACCESS_FS_EXECUTE |
+ LANDLOCK_ACCESS_FS_READ_FILE |
+ LANDLOCK_ACCESS_FS_WRITE_FILE;
+ }
+
+ struct landlock_path_beneath_attr rule = {
+ .allowed_access = access,
+ .parent_fd = path_fd,
+ };
+ if (ok)
+ ok = syscall(
+ SYS_landlock_add_rule,
+ ruleset_fd,
+ LANDLOCK_RULE_PATH_BENEATH,
+ &rule,
+ 0) == 0;
+ close(path_fd);
+ return ok;
+}
+
+static bool apply_filesystem_sandbox(const char *job_directory)
+{
+ int abi = syscall(
+ SYS_landlock_create_ruleset,
+ NULL,
+ 0,
+ LANDLOCK_CREATE_RULESET_VERSION);
+ if (abi < 1)
+ return false;
+
+ uint64_t read_access =
+ LANDLOCK_ACCESS_FS_EXECUTE |
+ LANDLOCK_ACCESS_FS_READ_FILE |
+ LANDLOCK_ACCESS_FS_READ_DIR;
+ uint64_t write_access =
+ LANDLOCK_ACCESS_FS_WRITE_FILE |
+ LANDLOCK_ACCESS_FS_REMOVE_DIR |
+ LANDLOCK_ACCESS_FS_REMOVE_FILE |
+ LANDLOCK_ACCESS_FS_MAKE_CHAR |
+ LANDLOCK_ACCESS_FS_MAKE_DIR |
+ LANDLOCK_ACCESS_FS_MAKE_REG |
+ LANDLOCK_ACCESS_FS_MAKE_SOCK |
+ LANDLOCK_ACCESS_FS_MAKE_FIFO |
+ LANDLOCK_ACCESS_FS_MAKE_BLOCK |
+ LANDLOCK_ACCESS_FS_MAKE_SYM;
+ if (abi >= 2)
+ write_access |= LANDLOCK_ACCESS_FS_REFER;
+ if (abi >= 3)
+ write_access |= LANDLOCK_ACCESS_FS_TRUNCATE;
+
+ struct landlock_ruleset_attr ruleset = {
+ .handled_access_fs = read_access | write_access,
+ };
+ int ruleset_fd = syscall(
+ SYS_landlock_create_ruleset,
+ &ruleset,
+ sizeof(ruleset),
+ 0);
+ if (ruleset_fd < 0)
+ return false;
+
+ const char *read_only_directories[] = {
+ "/usr",
+ "/lib",
+ "/lib64",
+ "/bin",
+ "/var/lib/texmf",
+ "/etc/texmf",
+ "/etc/fonts",
+ };
+ bool ok = true;
+ for (size_t i = 0;
+ ok && i < sizeof(read_only_directories) / sizeof(read_only_directories[0]);
+ i++)
+ {
+ ok = add_landlock_path_rule(
+ ruleset_fd,
+ read_only_directories[i],
+ read_access,
+ false);
+ }
+ if (ok)
+ ok = add_landlock_path_rule(
+ ruleset_fd,
+ "/etc/ld.so.cache",
+ LANDLOCK_ACCESS_FS_READ_FILE,
+ false);
+ if (ok)
+ ok = add_landlock_path_rule(
+ ruleset_fd,
+ "/dev/null",
+ LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_WRITE_FILE,
+ true);
+ if (ok)
+ ok = add_landlock_path_rule(
+ ruleset_fd,
+ "/dev/urandom",
+ LANDLOCK_ACCESS_FS_READ_FILE,
+ false);
+ if (ok)
+ ok = add_landlock_path_rule(
+ ruleset_fd,
+ job_directory,
+ read_access | write_access,
+ true);
+ if (ok)
+ ok = prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == 0;
+ if (ok)
+ ok = syscall(
+ SYS_landlock_restrict_self,
+ ruleset_fd,
+ 0) == 0;
+ close(ruleset_fd);
+ return ok;
+}
+
+static bool apply_network_sandbox(void)
+{
+ if (LATEX_AUDIT_ARCH == 0)
+ return false;
+
+ struct sock_filter filters[64];
+ size_t count = 0;
+#define ADD_FILTER(value) filters[count++] = (struct sock_filter)value
+#define DENY_SYSCALL(number) \
+ do { \
+ ADD_FILTER(BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, number, 0, 1)); \
+ ADD_FILTER(BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM)); \
+ } while (0)
+
+ ADD_FILTER(BPF_STMT(
+ BPF_LD | BPF_W | BPF_ABS,
+ offsetof(struct seccomp_data, arch)));
+ ADD_FILTER(BPF_JUMP(
+ BPF_JMP | BPF_JEQ | BPF_K,
+ LATEX_AUDIT_ARCH,
+ 1,
+ 0));
+ ADD_FILTER(BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS));
+ ADD_FILTER(BPF_STMT(
+ BPF_LD | BPF_W | BPF_ABS,
+ offsetof(struct seccomp_data, nr)));
+ DENY_SYSCALL(__NR_socket);
+ DENY_SYSCALL(__NR_socketpair);
+ DENY_SYSCALL(__NR_connect);
+ DENY_SYSCALL(__NR_accept);
+ DENY_SYSCALL(__NR_accept4);
+ DENY_SYSCALL(__NR_bind);
+ DENY_SYSCALL(__NR_listen);
+ DENY_SYSCALL(__NR_sendto);
+ DENY_SYSCALL(__NR_recvfrom);
+ DENY_SYSCALL(__NR_sendmsg);
+ DENY_SYSCALL(__NR_recvmsg);
+ ADD_FILTER(BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW));
+
+ struct sock_fprog program = {
+ .len = (unsigned short)count,
+ .filter = filters,
+ };
+#undef DENY_SYSCALL
+#undef ADD_FILTER
+ return prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &program) == 0;
+}
+
+static void configure_child_environment(const char *job_directory)
+{
+ clearenv();
+ setenv("HOME", job_directory, 1);
+ setenv("TMPDIR", job_directory, 1);
+ setenv("TEXMFVAR", job_directory, 1);
+ setenv("TEXMFCONFIG", job_directory, 1);
+ setenv("openin_any", "p", 1);
+ setenv("openout_any", "p", 1);
+ setenv("shell_escape", "0", 1);
+ setenv("SOURCE_DATE_EPOCH", "0", 1);
+ setenv("LANG", "C.UTF-8", 1);
+}
+
+static void close_inherited_descriptors(void)
+{
+#ifdef SYS_close_range
+ if (syscall(SYS_close_range, 3U, UINT_MAX, 0) == 0)
+ return;
+#endif
+ struct rlimit limit;
+ rlim_t maximum = 65536;
+ if (getrlimit(RLIMIT_NOFILE, &limit) == 0 &&
+ limit.rlim_cur != RLIM_INFINITY &&
+ limit.rlim_cur < maximum)
+ maximum = limit.rlim_cur;
+ for (int fd = 3; fd < (int)maximum; fd++)
+ close(fd);
+}
+
+static void run_compiler_child(
+ const char *compiler,
+ const char *job_directory,
+ int output_fd)
+{
+ if (dup2(output_fd, STDOUT_FILENO) < 0 ||
+ dup2(output_fd, STDERR_FILENO) < 0)
+ _exit(120);
+ if (output_fd != STDOUT_FILENO && output_fd != STDERR_FILENO)
+ close(output_fd);
+ close_inherited_descriptors();
+ if (setpgid(0, 0) != 0 ||
+ prctl(PR_SET_PDEATHSIG, SIGKILL) != 0 ||
+ getppid() == 1)
+ _exit(119);
+ if (chdir(job_directory) != 0)
+ _exit(121);
+
+ configure_child_environment(job_directory);
+ if (!set_limit(RLIMIT_CPU, LATEX_CPU_TIMEOUT_SECONDS) ||
+ !set_limit(RLIMIT_AS, 768 * 1024 * 1024) ||
+ !set_limit(RLIMIT_FSIZE, LATEX_PDF_MAX_BYTES) ||
+ !set_limit(RLIMIT_NOFILE, 64) ||
+ !set_limit(RLIMIT_NPROC, 1))
+ _exit(122);
+ if (!apply_filesystem_sandbox(job_directory) ||
+ !apply_network_sandbox())
+ _exit(123);
+
+ char *const arguments[] = {
+ (char *)compiler,
+ "-interaction=nonstopmode",
+ "-halt-on-error",
+ "-no-shell-escape",
+ "-file-line-error",
+ "-output-directory=.",
+ "document.tex",
+ NULL,
+ };
+ execv(compiler, arguments);
+ _exit(errno == ENOENT ? 127 : 126);
+}
+
+static double elapsed_seconds(
+ const struct timespec *start,
+ const struct timespec *now)
+{
+ return (double)(now->tv_sec - start->tv_sec) +
+ (double)(now->tv_nsec - start->tv_nsec) / 1000000000.0;
+}
+
+static int wait_for_compiler(
+ pid_t child,
+ int output_fd,
+ char *diagnostics,
+ size_t diagnostics_size,
+ bool *timed_out)
+{
+ int flags = fcntl(output_fd, F_GETFL, 0);
+ if (flags >= 0)
+ fcntl(output_fd, F_SETFL, flags | O_NONBLOCK);
+
+ size_t used = 0;
+ int child_status = 0;
+ bool child_done = false;
+ struct timespec start;
+ clock_gettime(CLOCK_MONOTONIC, &start);
+
+ while (!child_done)
+ {
+ char buffer[2048];
+ ssize_t count;
+ while ((count = read(output_fd, buffer, sizeof(buffer))) > 0)
+ {
+ size_t available = diagnostics_size - used - 1;
+ size_t copy_size = (size_t)count < available ? (size_t)count : available;
+ if (copy_size > 0)
+ {
+ memcpy(diagnostics + used, buffer, copy_size);
+ used += copy_size;
+ }
+ }
+
+ pid_t waited = waitpid(child, &child_status, WNOHANG);
+ if (waited == child)
+ {
+ child_done = true;
+ break;
+ }
+ if (waited < 0 && errno != EINTR)
+ break;
+
+ struct timespec now;
+ clock_gettime(CLOCK_MONOTONIC, &now);
+ if (elapsed_seconds(&start, &now) >= LATEX_WALL_TIMEOUT_SECONDS)
+ {
+ *timed_out = true;
+ kill(-child, SIGKILL);
+ while (waitpid(child, &child_status, 0) < 0 && errno == EINTR)
+ {
+ }
+ child_done = true;
+ break;
+ }
+
+ struct pollfd poll_fd = {
+ .fd = output_fd,
+ .events = POLLIN,
+ };
+ poll(&poll_fd, 1, 50);
+ }
+
+ char buffer[2048];
+ ssize_t count;
+ while ((count = read(output_fd, buffer, sizeof(buffer))) > 0)
+ {
+ size_t available = diagnostics_size - used - 1;
+ size_t copy_size = (size_t)count < available ? (size_t)count : available;
+ if (copy_size > 0)
+ {
+ memcpy(diagnostics + used, buffer, copy_size);
+ used += copy_size;
+ }
+ }
+ diagnostics[used] = '\0';
+ kill(-child, SIGKILL);
+ return child_status;
+}
+
+static bool read_pdf(
+ const char *path,
+ uint8_t **data,
+ size_t *size)
+{
+ int fd = open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
+ if (fd < 0)
+ return false;
+
+ struct stat status;
+ bool ok = fstat(fd, &status) == 0 &&
+ S_ISREG(status.st_mode) &&
+ status.st_size > 4 &&
+ status.st_size <= LATEX_PDF_MAX_BYTES;
+ uint8_t *content = NULL;
+ if (ok)
+ {
+ content = malloc((size_t)status.st_size);
+ ok = content != NULL;
+ }
+ size_t offset = 0;
+ while (ok && offset < (size_t)status.st_size)
+ {
+ ssize_t count = read(
+ fd,
+ content + offset,
+ (size_t)status.st_size - offset);
+ if (count < 0 && errno == EINTR)
+ continue;
+ if (count <= 0)
+ {
+ ok = false;
+ break;
+ }
+ offset += (size_t)count;
+ }
+ close(fd);
+
+ if (!ok ||
+ memcmp(content, "%PDF-", 5) != 0)
+ {
+ free(content);
+ return false;
+ }
+ *data = content;
+ *size = offset;
+ return true;
+}
+
+Latex_Render_Result Latex_Render(const uint8_t *source, size_t source_size)
+{
+ if (!source || source_size == 0)
+ return result_with_message(
+ LATEX_RENDER_INVALID_INPUT,
+ "LaTeX source is required.");
+ if (source_size > LATEX_SOURCE_MAX_BYTES)
+ return result_with_message(
+ LATEX_RENDER_INVALID_INPUT,
+ "LaTeX source exceeds the 64 KiB limit.");
+ if (memchr(source, '\0', source_size))
+ return result_with_message(
+ LATEX_RENDER_INVALID_INPUT,
+ "LaTeX source cannot contain NUL bytes.");
+
+ const char *compiler = "/usr/bin/pdflatex";
+ if (access(compiler, X_OK) != 0)
+ return result_with_message(
+ LATEX_RENDER_COMPILER_UNAVAILABLE,
+ "The server LaTeX compiler is unavailable.");
+
+ char job_directory[] = "/tmp/mrjunejune-latex-XXXXXX";
+ if (!mkdtemp(job_directory))
+ return result_with_message(
+ LATEX_RENDER_INTERNAL_ERROR,
+ "Unable to create the LaTeX workspace.");
+
+ char source_path[512];
+ snprintf(
+ source_path,
+ sizeof(source_path),
+ "%s/document.tex",
+ job_directory);
+ int source_fd = open(
+ source_path,
+ O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC | O_NOFOLLOW,
+ 0600);
+ if (source_fd < 0 || !write_all(source_fd, source, source_size))
+ {
+ if (source_fd >= 0)
+ close(source_fd);
+ remove_tree(job_directory, 0);
+ return result_with_message(
+ LATEX_RENDER_INTERNAL_ERROR,
+ "Unable to write the LaTeX source.");
+ }
+ close(source_fd);
+
+ int output_pipe[2];
+ if (pipe2(output_pipe, O_CLOEXEC) != 0)
+ {
+ remove_tree(job_directory, 0);
+ return result_with_message(
+ LATEX_RENDER_INTERNAL_ERROR,
+ "Unable to capture compiler output.");
+ }
+
+ pid_t child = fork();
+ if (child < 0)
+ {
+ close(output_pipe[0]);
+ close(output_pipe[1]);
+ remove_tree(job_directory, 0);
+ return result_with_message(
+ LATEX_RENDER_INTERNAL_ERROR,
+ "Unable to start the LaTeX compiler.");
+ }
+ if (child == 0)
+ {
+ close(output_pipe[0]);
+ run_compiler_child(compiler, job_directory, output_pipe[1]);
+ }
+ setpgid(child, child);
+
+ close(output_pipe[1]);
+ char *diagnostics = calloc(1, LATEX_DIAGNOSTICS_MAX_BYTES + 1);
+ if (!diagnostics)
+ {
+ kill(-child, SIGKILL);
+ waitpid(child, NULL, 0);
+ close(output_pipe[0]);
+ remove_tree(job_directory, 0);
+ return result_with_message(
+ LATEX_RENDER_INTERNAL_ERROR,
+ "Unable to allocate compiler diagnostics.");
+ }
+
+ bool timed_out = false;
+ int child_status = wait_for_compiler(
+ child,
+ output_pipe[0],
+ diagnostics,
+ LATEX_DIAGNOSTICS_MAX_BYTES + 1,
+ &timed_out);
+ close(output_pipe[0]);
+
+ Latex_Render_Result result = {
+ .status = LATEX_RENDER_INTERNAL_ERROR,
+ .diagnostics = diagnostics,
+ };
+ if (timed_out)
+ {
+ result.status = LATEX_RENDER_TIMEOUT;
+ free(result.diagnostics);
+ result.diagnostics = duplicate_message(
+ "Compilation exceeded the 8 second limit.");
+ }
+ else if (WIFEXITED(child_status) && WEXITSTATUS(child_status) == 127)
+ {
+ result.status = LATEX_RENDER_COMPILER_UNAVAILABLE;
+ }
+ else if (WIFEXITED(child_status) && WEXITSTATUS(child_status) == 123)
+ {
+ result.status = LATEX_RENDER_SANDBOX_ERROR;
+ free(result.diagnostics);
+ result.diagnostics = duplicate_message(
+ "The server could not apply the LaTeX security sandbox.");
+ }
+ else if (WIFSIGNALED(child_status) &&
+ (WTERMSIG(child_status) == SIGXCPU ||
+ WTERMSIG(child_status) == SIGKILL))
+ {
+ result.status = LATEX_RENDER_TIMEOUT;
+ free(result.diagnostics);
+ result.diagnostics = duplicate_message(
+ "Compilation exceeded its CPU or memory limit.");
+ }
+ else if (!WIFEXITED(child_status) || WEXITSTATUS(child_status) != 0)
+ {
+ result.status = LATEX_RENDER_COMPILE_ERROR;
+ if (!result.diagnostics[0])
+ {
+ free(result.diagnostics);
+ result.diagnostics = duplicate_message("LaTeX compilation failed.");
+ }
+ }
+ else
+ {
+ char pdf_path[512];
+ snprintf(pdf_path, sizeof(pdf_path), "%s/document.pdf", job_directory);
+ if (read_pdf(pdf_path, &result.pdf_data, &result.pdf_size))
+ {
+ result.status = LATEX_RENDER_OK;
+ free(result.diagnostics);
+ result.diagnostics = NULL;
+ }
+ else
+ {
+ result.status = LATEX_RENDER_INTERNAL_ERROR;
+ free(result.diagnostics);
+ result.diagnostics = duplicate_message(
+ "The compiler did not produce a valid PDF.");
+ }
+ }
+
+ remove_tree(job_directory, 0);
+ return result;
+}
+
+void Latex_Render_Result_Destroy(Latex_Render_Result *result)
+{
+ if (!result)
+ return;
+ free(result->pdf_data);
+ free(result->diagnostics);
+ memset(result, 0, sizeof(*result));
+}
+
+#else
+
+#include
+#include
+
+Latex_Render_Result Latex_Render(const uint8_t *source, size_t source_size)
+{
+ (void)source;
+ (void)source_size;
+ const char *message =
+ "The LaTeX security sandbox is only available on Linux.";
+ Latex_Render_Result result = {
+ .status = LATEX_RENDER_SANDBOX_ERROR,
+ .diagnostics = malloc(strlen(message) + 1),
+ };
+ if (result.diagnostics)
+ strcpy(result.diagnostics, message);
+ return result;
+}
+
+void Latex_Render_Result_Destroy(Latex_Render_Result *result)
+{
+ if (!result)
+ return;
+ free(result->pdf_data);
+ free(result->diagnostics);
+ memset(result, 0, sizeof(*result));
+}
+
+#endif
diff -r 823f2a8b16c8 -r b8aa08503378 mrjunejune/latex_renderer.h
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/latex_renderer.h Mon Aug 03 16:56:25 2026 -0700
@@ -0,0 +1,30 @@
+#ifndef MRJUNEJUNE_LATEX_RENDERER_H
+#define MRJUNEJUNE_LATEX_RENDERER_H
+
+#include
+#include
+
+#define LATEX_SOURCE_MAX_BYTES (64 * 1024)
+#define LATEX_PDF_MAX_BYTES (4 * 1024 * 1024)
+
+typedef enum {
+ LATEX_RENDER_OK = 0,
+ LATEX_RENDER_INVALID_INPUT,
+ LATEX_RENDER_COMPILE_ERROR,
+ LATEX_RENDER_TIMEOUT,
+ LATEX_RENDER_COMPILER_UNAVAILABLE,
+ LATEX_RENDER_SANDBOX_ERROR,
+ LATEX_RENDER_INTERNAL_ERROR,
+} Latex_Render_Status;
+
+typedef struct {
+ Latex_Render_Status status;
+ uint8_t *pdf_data;
+ size_t pdf_size;
+ char *diagnostics;
+} Latex_Render_Result;
+
+Latex_Render_Result Latex_Render(const uint8_t *source, size_t source_size);
+void Latex_Render_Result_Destroy(Latex_Render_Result *result);
+
+#endif
diff -r 823f2a8b16c8 -r b8aa08503378 mrjunejune/main.c
--- a/mrjunejune/main.c Mon Aug 03 15:26:44 2026 -0700
+++ b/mrjunejune/main.c Mon Aug 03 16:56:25 2026 -0700
@@ -2,9 +2,11 @@
#include "markdown_converter/markdown_to_html.h"
#include "s3/s3_uploader.h"
#include "deita/deita.h"
+#include "mrjunejune/latex_renderer.h"
#include
#include
#include
+#include
#include
// UUID + /tmp/ + format (max 4)
@@ -13,6 +15,7 @@
volatile sig_atomic_t stop_server = 0;
static _Atomic uint32_t counter = 0;
+static _Atomic boolean g_latex_rendering = FALSE;
// Media Processing Context for background threads
typedef struct {
@@ -304,6 +307,106 @@
return resp;
}
+Seobeo_Request_Entry* GetLatexEditor(Seobeo_Request_Entry *req, Dowa_Arena *arena)
+{
+ Seobeo_Request_Entry *resp = NULL;
+ char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024);
+ Seobeo_Render_Html_FilePath(final_body, "/tools/latex_editor/index.html", arena);
+ Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
+ return resp;
+}
+
+static Seobeo_Request_Entry *LatexErrorResponse(
+ Dowa_Arena *arena,
+ int status,
+ const char *message)
+{
+ Seobeo_Request_Entry *resp = NULL;
+ char *status_value = Dowa_Arena_Allocate(arena, 16);
+ snprintf(status_value, 16, "%d", status);
+ char *body = Dowa_Arena_Allocate(arena, strlen(message) + 1);
+ strcpy(body, message);
+ Dowa_HashMap_Push_Arena(resp, "status", status_value, arena);
+ Dowa_HashMap_Push_Arena(resp, "content-type", "text/plain; charset=utf-8", arena);
+ Dowa_HashMap_Push_Arena(resp, "cache-control", "no-store", arena);
+ Dowa_HashMap_Push_Arena(resp, "body", body, arena);
+ return resp;
+}
+
+Seobeo_Request_Entry *RenderLatexPdf(Seobeo_Request_Entry *req, Dowa_Arena *arena)
+{
+ void *body_kv = Dowa_HashMap_Get_Ptr(req, "Body");
+ void *length_kv = Dowa_HashMap_Get_Ptr(req, "Content-Length");
+ if (!body_kv || !length_kv)
+ return LatexErrorResponse(arena, 400, "LaTeX source and Content-Length are required.");
+
+ const char *length_value = ((Seobeo_Request_Entry *)length_kv)->value;
+ char *length_end = NULL;
+ errno = 0;
+ unsigned long long parsed_length = strtoull(length_value, &length_end, 10);
+ if (errno != 0 ||
+ length_end == length_value ||
+ *length_end != '\0' ||
+ parsed_length == 0)
+ return LatexErrorResponse(arena, 400, "Content-Length is invalid.");
+ if (parsed_length > LATEX_SOURCE_MAX_BYTES)
+ return LatexErrorResponse(arena, 413, "LaTeX source exceeds the 64 KiB limit.");
+
+ boolean expected = FALSE;
+ if (!atomic_compare_exchange_strong(
+ &g_latex_rendering,
+ &expected,
+ TRUE))
+ return LatexErrorResponse(arena, 429, "The LaTeX compiler is busy. Try again shortly.");
+
+ Latex_Render_Result render = Latex_Render(
+ (const uint8_t *)((Seobeo_Request_Entry *)body_kv)->value,
+ (size_t)parsed_length);
+ atomic_store(&g_latex_rendering, FALSE);
+ if (render.status != LATEX_RENDER_OK)
+ {
+ int status = 500;
+ if (render.status == LATEX_RENDER_INVALID_INPUT)
+ status = 400;
+ else if (render.status == LATEX_RENDER_COMPILE_ERROR)
+ status = 422;
+ else if (render.status == LATEX_RENDER_TIMEOUT)
+ status = 504;
+ else if (render.status == LATEX_RENDER_COMPILER_UNAVAILABLE)
+ status = 503;
+
+ const char *diagnostics = render.diagnostics
+ ? render.diagnostics
+ : "LaTeX rendering failed.";
+ char *body = Dowa_Arena_Allocate(arena, strlen(diagnostics) + 1);
+ strcpy(body, diagnostics);
+ Latex_Render_Result_Destroy(&render);
+ return LatexErrorResponse(arena, status, body);
+ }
+
+ char *pdf = Dowa_Arena_Allocate(arena, render.pdf_size + 1);
+ if (!pdf)
+ {
+ Latex_Render_Result_Destroy(&render);
+ return LatexErrorResponse(arena, 500, "The generated PDF is too large to return.");
+ }
+ memcpy(pdf, render.pdf_data, render.pdf_size);
+ pdf[render.pdf_size] = '\0';
+ char *content_length = Dowa_Arena_Allocate(arena, 32);
+ snprintf(content_length, 32, "%zu", render.pdf_size);
+ Latex_Render_Result_Destroy(&render);
+
+ Seobeo_Request_Entry *resp = NULL;
+ Dowa_HashMap_Push_Arena(resp, "status", "200", arena);
+ Dowa_HashMap_Push_Arena(resp, "content-type", "application/pdf", arena);
+ Dowa_HashMap_Push_Arena(resp, "content-length", content_length, arena);
+ Dowa_HashMap_Push_Arena(resp, "content-disposition", "inline; filename=\"document.pdf\"", arena);
+ Dowa_HashMap_Push_Arena(resp, "cache-control", "no-store", arena);
+ Dowa_HashMap_Push_Arena(resp, "x-content-type-options", "nosniff", arena);
+ Dowa_HashMap_Push_Arena(resp, "body", pdf, arena);
+ return resp;
+}
+
// Background thread function for media processing
void *Simple_WebpConverter_Background(void *arg)
{
@@ -746,6 +849,7 @@
CREATE_REDIRECT_HANDLER(MarkDownToHtml, "/tools/markdown_to_html")
CREATE_REDIRECT_HANDLER(FileConverter, "/tools/file_converter")
CREATE_REDIRECT_HANDLER(HlsPlayer, "/tools/hls_player")
+CREATE_REDIRECT_HANDLER(LatexEditor, "/tools/latex_editor")
CREATE_REDIRECT_HANDLER(Talk, "/talk")
CREATE_REDIRECT_HANDLER(Editor, "/editor")
@@ -1254,28 +1358,26 @@
// Insert into database
const char *insert_query =
"INSERT INTO media_uploads (access_token, original_filename, content_type, s3_key_original, s3_key_processed, status) "
- "VALUES (?, ?, ?, ?, ?, 'pending')";
+ "VALUES (?, ?, ?, ?, ?, 'pending') RETURNING id";
const char *params[] = { token, filename, content_type, s3_key_original, s3_key_processed };
- int32 result = Deita_Query_Execute_Update_Prepared(g_db_connection, insert_query, 5, params);
-
- if (result < 0)
+ Deita_Result_Set *id_result = Deita_Query_Execute_Prepared(
+ g_db_connection,
+ insert_query,
+ 5,
+ params,
+ arena);
+ if (!id_result || !Deita_Result_Set_Next(id_result))
{
+ if (id_result) Deita_Result_Set_Free(id_result);
Dowa_HashMap_Push_Arena(resp, "status", "500", arena);
Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
Dowa_HashMap_Push_Arena(resp, "body", "{\"error\":\"Failed to create media record\"}", arena);
return resp;
}
- // Get the inserted media_id using last_insert_rowid()
- const char *last_id_query = "SELECT last_insert_rowid()";
- Deita_Result_Set *id_result = Deita_Query_Execute(g_db_connection, last_id_query, arena);
- int64 media_id = 0;
- if (id_result && Deita_Result_Set_Next(id_result))
- {
- media_id = Deita_Result_Set_Get_Integer(id_result, 0);
- }
- if (id_result) Deita_Result_Set_Free(id_result);
+ int64 media_id = Deita_Result_Set_Get_Integer(id_result, 0);
+ Deita_Result_Set_Free(id_result);
// Generate presigned PUT URL
S3_Presigned_URL presigned = S3_Presign_Put(&g_s3_config, s3_key_original, content_type, g_s3_url_expires);
@@ -1868,11 +1970,14 @@
Seobeo_Router_Register("GET", "/tools/file_converter/index.html", GetRedirectFileConverter);
Seobeo_Router_Register("GET", "/tools/hls_player", GetHlsPlayer);
Seobeo_Router_Register("GET", "/tools/hls_player/index.html", GetRedirectHlsPlayer);
+ Seobeo_Router_Register("GET", "/tools/latex_editor", GetLatexEditor);
+ Seobeo_Router_Register("GET", "/tools/latex_editor/index.html", GetRedirectLatexEditor);
// -- File converter --/
Seobeo_Router_Register("POST", "/api/convert/image-to-webp", ConvertImageToWebP);
Seobeo_Router_Register("POST", "/api/convert/video-to-mp4", ConvertVideoToMP4);
Seobeo_Router_Register("GET", "/api/download/:filename", DownloadConvertedFile);
+ Seobeo_Router_Register("POST", "/api/latex/render", RenderLatexPdf);
// -- S3 Upload --/
Seobeo_Router_Register("POST", "/api/s3/upload-url", GetS3UploadUrl);
@@ -1909,5 +2014,5 @@
const char *server_port = getenv("MRJUNEJUNE_PORT");
if (!server_port || server_port[0] == '\0')
server_port = "6969";
- Seobeo_Web_Server_Start("mrjunejune/src", server_port, SEOBEO_MODE_EDGE, 1);
+ Seobeo_Web_Server_Start("mrjunejune/src", server_port, SEOBEO_MODE_EDGE, 4);
}
diff -r 823f2a8b16c8 -r b8aa08503378 mrjunejune/src/tools/index.html
--- a/mrjunejune/src/tools/index.html Mon Aug 03 15:26:44 2026 -0700
+++ b/mrjunejune/src/tools/index.html Mon Aug 03 16:56:25 2026 -0700
@@ -12,12 +12,9 @@
MarkDown to HTML
Images to Webp / Video to Mp4
Online HLS Player
+ Online LaTeX Editor
Personal Notes
- TODOs
-
- - - Simple online LaTex editor.
-
{{/parts/footer.html}}
+ {{/parts/header.html}}
+
+
+
+
Online LaTeX Editor
+
Write LaTeX here. My server compiles it inside a locked-down sandbox and sends the PDF back to this preview.
+
+
+
+
+
+
Download
+
+
+
+
+ Preparing your first PDF...
+
+
+
+
+
+
document.tex
+ 0 / 65,536 bytes
+
+
+
+
+
+
+
PDF preview
+ Server rendered
+
+
+
+
+
+
+
+ Limits
+ Source is capped at 64 KiB and compilation at 8 seconds. Shell commands, network access, and reads outside the isolated TeX workspace are blocked.
+
+
+ {{/parts/footer.html}}
+
diff -r 823f2a8b16c8 -r b8aa08503378 mrjunejune/src/tools/latex_editor/index.css
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/src/tools/latex_editor/index.css Mon Aug 03 16:56:25 2026 -0700
@@ -0,0 +1,195 @@
+.latex-page {
+ width: min(1500px, calc(100% - 2rem));
+ max-width: none;
+ margin: 1.5rem auto 3rem;
+}
+
+.latex-heading {
+ display: flex;
+ align-items: flex-end;
+ justify-content: space-between;
+ gap: 1.5rem;
+ margin-bottom: 1rem;
+}
+
+.latex-heading h1,
+.latex-heading p {
+ margin-bottom: 0.4rem;
+}
+
+.latex-actions {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ flex-wrap: wrap;
+ gap: 0.65rem;
+}
+
+.latex-actions button,
+.button-link {
+ min-height: 42px;
+ padding: 0.65rem 1rem;
+ border: 2px solid rgb(var(--black));
+ border-radius: 8px;
+ background: var(--awesome);
+ color: rgb(var(--black));
+ font: inherit;
+ font-weight: 700;
+ text-decoration: none;
+ cursor: pointer;
+}
+
+.latex-actions button.secondary {
+ background: transparent;
+}
+
+.latex-actions button:disabled,
+.button-link.disabled {
+ opacity: 0.45;
+ cursor: not-allowed;
+ pointer-events: none;
+}
+
+.auto-compile {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.4rem;
+ white-space: nowrap;
+}
+
+.latex-status {
+ min-height: 1.5em;
+ margin: 0.5rem 0 1rem;
+ font-weight: 700;
+}
+
+.latex-status[data-state="working"] {
+ color: var(--orange);
+}
+
+.latex-status[data-state="ready"] {
+ color: var(--blue);
+}
+
+.latex-status[data-state="error"] {
+ color: var(--red);
+}
+
+.latex-workspace {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
+ gap: 1rem;
+ min-height: 72vh;
+}
+
+.latex-panel {
+ min-width: 0;
+ overflow: hidden;
+ border: 2px solid rgb(var(--black));
+ border-radius: 12px;
+ background: var(--white);
+ box-shadow: 6px 6px 0 rgba(var(--black), 0.12);
+}
+
+.panel-heading {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ min-height: 48px;
+ padding: 0.55rem 0.8rem;
+ border-bottom: 2px solid rgb(var(--black));
+ background: var(--gray-gradient);
+}
+
+.panel-heading h2,
+.panel-heading span {
+ margin: 0;
+ font-size: 0.95rem;
+}
+
+#latexSource {
+ display: block;
+ width: 100%;
+ height: calc(72vh - 50px);
+ min-height: 540px;
+ resize: vertical;
+ box-sizing: border-box;
+ padding: 1rem;
+ border: 0;
+ outline: 0;
+ background: rgb(var(--black));
+ color: var(--white);
+ font: 15px/1.55 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+ tab-size: 2;
+}
+
+#pdfPreview {
+ display: block;
+ width: 100%;
+ height: calc(72vh - 50px);
+ min-height: 540px;
+ border: 0;
+ background: #525659;
+}
+
+.latex-diagnostics {
+ width: 100%;
+ height: calc(72vh - 50px);
+ min-height: 540px;
+ box-sizing: border-box;
+ overflow: auto;
+ margin: 0;
+ padding: 1rem;
+ white-space: pre-wrap;
+ background: #1e1e1e;
+ color: #ffb4ab;
+ font: 14px/1.5 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+}
+
+.latex-notes {
+ margin-top: 1.5rem;
+}
+
+.latex-notes h2,
+.latex-notes p {
+ margin-bottom: 0.4rem;
+}
+
+@media (max-width: 900px) {
+ .latex-heading {
+ align-items: stretch;
+ flex-direction: column;
+ }
+
+ .latex-actions {
+ justify-content: flex-start;
+ }
+
+ .latex-workspace {
+ grid-template-columns: 1fr;
+ }
+
+ #latexSource,
+ #pdfPreview,
+ .latex-diagnostics {
+ height: 65vh;
+ min-height: 480px;
+ }
+}
+
+@media (max-width: 520px) {
+ .latex-page {
+ width: min(100% - 1rem, 1500px);
+ }
+
+ .latex-actions {
+ align-items: stretch;
+ }
+
+ .latex-actions button,
+ .button-link {
+ flex: 1 1 45%;
+ text-align: center;
+ }
+}
diff -r 823f2a8b16c8 -r b8aa08503378 mrjunejune/src/tools/latex_editor/index.html
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mrjunejune/src/tools/latex_editor/index.html Mon Aug 03 16:56:25 2026 -0700
@@ -0,0 +1,83 @@
+
+
+