Mercurial
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); }