diff hg-web/main.c @ 224:3007ef5fc0ed hg-web

[hg-web] Simplify visuals and preview static files
author MrJuneJune <me@mrjunejune.com>
date Sun, 02 Aug 2026 13:51:11 -0700
parents 0e7b9464248d
children c5129452493e
line wrap: on
line diff
--- a/hg-web/main.c	Sun Aug 02 10:07:40 2026 -0700
+++ b/hg-web/main.c	Sun Aug 02 13:51:11 2026 -0700
@@ -198,6 +198,77 @@
          strchr(value, '\n') == NULL;
 }
 
+static boolean extension_is(const char *extension, const char *expected)
+{
+  return extension && strcasecmp(extension, expected) == 0;
+}
+
+static const char *repository_file_content_type(
+    const char *path,
+    boolean *inline_preview,
+    boolean *sandbox_content)
+{
+  const char *extension = strrchr(path, '.');
+  *inline_preview = TRUE;
+  *sandbox_content = FALSE;
+
+  if (extension_is(extension, ".png")) return "image/png";
+  if (extension_is(extension, ".jpg") ||
+      extension_is(extension, ".jpeg")) return "image/jpeg";
+  if (extension_is(extension, ".gif")) return "image/gif";
+  if (extension_is(extension, ".webp")) return "image/webp";
+  if (extension_is(extension, ".avif")) return "image/avif";
+  if (extension_is(extension, ".bmp")) return "image/bmp";
+  if (extension_is(extension, ".ico")) return "image/x-icon";
+  if (extension_is(extension, ".svg"))
+  {
+    *sandbox_content = TRUE;
+    return "image/svg+xml";
+  }
+  if (extension_is(extension, ".mp4") ||
+      extension_is(extension, ".m4v")) return "video/mp4";
+  if (extension_is(extension, ".webm")) return "video/webm";
+  if (extension_is(extension, ".mov")) return "video/quicktime";
+  if (extension_is(extension, ".ogv")) return "video/ogg";
+  if (extension_is(extension, ".mp3")) return "audio/mpeg";
+  if (extension_is(extension, ".wav")) return "audio/wav";
+  if (extension_is(extension, ".ogg") ||
+      extension_is(extension, ".oga")) return "audio/ogg";
+  if (extension_is(extension, ".flac")) return "audio/flac";
+  if (extension_is(extension, ".m4a")) return "audio/mp4";
+  if (extension_is(extension, ".aac")) return "audio/aac";
+  if (extension_is(extension, ".pdf")) return "application/pdf";
+  if (extension_is(extension, ".wasm")) return "application/wasm";
+
+  *inline_preview = FALSE;
+  if (extension_is(extension, ".md") ||
+      extension_is(extension, ".markdown")) return "text/markdown; charset=utf-8";
+  if (extension_is(extension, ".txt") ||
+      extension_is(extension, ".log") ||
+      extension_is(extension, ".c") ||
+      extension_is(extension, ".h") ||
+      extension_is(extension, ".cc") ||
+      extension_is(extension, ".cpp") ||
+      extension_is(extension, ".js") ||
+      extension_is(extension, ".jsx") ||
+      extension_is(extension, ".ts") ||
+      extension_is(extension, ".tsx") ||
+      extension_is(extension, ".css") ||
+      extension_is(extension, ".html") ||
+      extension_is(extension, ".htm") ||
+      extension_is(extension, ".xml") ||
+      extension_is(extension, ".json") ||
+      extension_is(extension, ".yaml") ||
+      extension_is(extension, ".yml") ||
+      extension_is(extension, ".toml") ||
+      extension_is(extension, ".sh") ||
+      extension_is(extension, ".py") ||
+      extension_is(extension, ".rs") ||
+      extension_is(extension, ".go"))
+    return "text/plain; charset=utf-8";
+  return "application/octet-stream";
+}
+
 static Seobeo_Client_Response *hg_proxy_request(
     const char *method,
     const char *path,
@@ -243,6 +314,7 @@
 static Seobeo_Request_Entry *forward_hg_response(
     Seobeo_Client_Response *hg_response,
     const char *default_content_type,
+    const char *override_content_type,
     Dowa_Arena *arena)
 {
   if (!hg_response)
@@ -252,7 +324,9 @@
   const char *upstream_content_type =
       map_value_case_insensitive(hg_response->headers, "Content-Type");
   const char *upstream_or_default_content_type =
-      upstream_content_type ? upstream_content_type : default_content_type;
+      override_content_type
+          ? override_content_type
+          : upstream_content_type ? upstream_content_type : default_content_type;
   if (!upstream_or_default_content_type)
     upstream_or_default_content_type = "application/octet-stream";
   char *content_type = arena_string(arena, upstream_or_default_content_type);
@@ -297,6 +371,7 @@
   return forward_hg_response(
       hg_proxy_request("GET", hg_path, NULL, 0, NULL, "application/json"),
       "application/json",
+      NULL,
       arena);
 }
 
@@ -314,10 +389,28 @@
   if (length < 0 || (size_t)length >= sizeof(hg_path))
     return text_response(arena, "400", "text/plain", "File path is too long");
 
-  return forward_hg_response(
-      hg_proxy_request("GET", hg_path, NULL, 0, NULL, "application/octet-stream"),
-      "application/octet-stream",
+  Seobeo_Client_Response *hg_response =
+      hg_proxy_request("GET", hg_path, NULL, 0, NULL, "application/octet-stream");
+  if (!hg_response)
+    return forward_hg_response(NULL, "application/json", NULL, arena);
+
+  boolean inline_preview = FALSE;
+  boolean sandbox_content = FALSE;
+  const char *content_type =
+      repository_file_content_type(path, &inline_preview, &sandbox_content);
+  Seobeo_Request_Entry *response = forward_hg_response(
+      hg_response, "application/octet-stream", content_type, arena);
+  Dowa_HashMap_Push_Arena(
+      response,
+      "Content-Disposition",
+      inline_preview ? "inline" : "attachment",
       arena);
+  Dowa_HashMap_Push_Arena(
+      response, "X-Content-Type-Options", "nosniff", arena);
+  if (sandbox_content)
+    Dowa_HashMap_Push_Arena(
+        response, "Content-Security-Policy", "sandbox", arena);
+  return response;
 }
 
 Seobeo_Request_Entry *ApiGetReadme(Seobeo_Request_Entry *request, Dowa_Arena *arena)
@@ -351,7 +444,8 @@
     Seobeo_Client_Response_Destroy(hg_response);
     return text_response(arena, "204", "text/markdown", "");
   }
-  return forward_hg_response(hg_response, "text/markdown", arena);
+  return forward_hg_response(
+      hg_response, "text/markdown", "text/markdown; charset=utf-8", arena);
 }
 
 Seobeo_Request_Entry *ApiGetGraph(Seobeo_Request_Entry *request, Dowa_Arena *arena)
@@ -385,6 +479,7 @@
   return forward_hg_response(
       hg_proxy_request("GET", hg_path, NULL, 0, NULL, "application/json"),
       "application/json",
+      NULL,
       arena);
 }
 
@@ -402,6 +497,7 @@
   return forward_hg_response(
       hg_proxy_request("GET", hg_path, NULL, 0, NULL, "application/json"),
       "application/json",
+      NULL,
       arena);
 }