diff mrjunejune/main.c @ 244:b8aa08503378

[tools] Add sandboxed online LaTeX editor Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Mon, 03 Aug 2026 16:56:25 -0700
parents 543df0fe7168
children b8b6e726964a
line wrap: on
line diff
--- 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 <time.h>
 #include <sys/stat.h>
 #include <stdarg.h>
+#include <stdatomic.h>
 #include <pthread.h>
 
 // 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);
 }