comparison 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
comparison
equal deleted inserted replaced
223:0e7b9464248d 224:3007ef5fc0ed
196 return length <= maximum_length && 196 return length <= maximum_length &&
197 strchr(value, '\r') == NULL && 197 strchr(value, '\r') == NULL &&
198 strchr(value, '\n') == NULL; 198 strchr(value, '\n') == NULL;
199 } 199 }
200 200
201 static boolean extension_is(const char *extension, const char *expected)
202 {
203 return extension && strcasecmp(extension, expected) == 0;
204 }
205
206 static const char *repository_file_content_type(
207 const char *path,
208 boolean *inline_preview,
209 boolean *sandbox_content)
210 {
211 const char *extension = strrchr(path, '.');
212 *inline_preview = TRUE;
213 *sandbox_content = FALSE;
214
215 if (extension_is(extension, ".png")) return "image/png";
216 if (extension_is(extension, ".jpg") ||
217 extension_is(extension, ".jpeg")) return "image/jpeg";
218 if (extension_is(extension, ".gif")) return "image/gif";
219 if (extension_is(extension, ".webp")) return "image/webp";
220 if (extension_is(extension, ".avif")) return "image/avif";
221 if (extension_is(extension, ".bmp")) return "image/bmp";
222 if (extension_is(extension, ".ico")) return "image/x-icon";
223 if (extension_is(extension, ".svg"))
224 {
225 *sandbox_content = TRUE;
226 return "image/svg+xml";
227 }
228 if (extension_is(extension, ".mp4") ||
229 extension_is(extension, ".m4v")) return "video/mp4";
230 if (extension_is(extension, ".webm")) return "video/webm";
231 if (extension_is(extension, ".mov")) return "video/quicktime";
232 if (extension_is(extension, ".ogv")) return "video/ogg";
233 if (extension_is(extension, ".mp3")) return "audio/mpeg";
234 if (extension_is(extension, ".wav")) return "audio/wav";
235 if (extension_is(extension, ".ogg") ||
236 extension_is(extension, ".oga")) return "audio/ogg";
237 if (extension_is(extension, ".flac")) return "audio/flac";
238 if (extension_is(extension, ".m4a")) return "audio/mp4";
239 if (extension_is(extension, ".aac")) return "audio/aac";
240 if (extension_is(extension, ".pdf")) return "application/pdf";
241 if (extension_is(extension, ".wasm")) return "application/wasm";
242
243 *inline_preview = FALSE;
244 if (extension_is(extension, ".md") ||
245 extension_is(extension, ".markdown")) return "text/markdown; charset=utf-8";
246 if (extension_is(extension, ".txt") ||
247 extension_is(extension, ".log") ||
248 extension_is(extension, ".c") ||
249 extension_is(extension, ".h") ||
250 extension_is(extension, ".cc") ||
251 extension_is(extension, ".cpp") ||
252 extension_is(extension, ".js") ||
253 extension_is(extension, ".jsx") ||
254 extension_is(extension, ".ts") ||
255 extension_is(extension, ".tsx") ||
256 extension_is(extension, ".css") ||
257 extension_is(extension, ".html") ||
258 extension_is(extension, ".htm") ||
259 extension_is(extension, ".xml") ||
260 extension_is(extension, ".json") ||
261 extension_is(extension, ".yaml") ||
262 extension_is(extension, ".yml") ||
263 extension_is(extension, ".toml") ||
264 extension_is(extension, ".sh") ||
265 extension_is(extension, ".py") ||
266 extension_is(extension, ".rs") ||
267 extension_is(extension, ".go"))
268 return "text/plain; charset=utf-8";
269 return "application/octet-stream";
270 }
271
201 static Seobeo_Client_Response *hg_proxy_request( 272 static Seobeo_Client_Response *hg_proxy_request(
202 const char *method, 273 const char *method,
203 const char *path, 274 const char *path,
204 const char *request_body, 275 const char *request_body,
205 size_t request_body_length, 276 size_t request_body_length,
241 } 312 }
242 313
243 static Seobeo_Request_Entry *forward_hg_response( 314 static Seobeo_Request_Entry *forward_hg_response(
244 Seobeo_Client_Response *hg_response, 315 Seobeo_Client_Response *hg_response,
245 const char *default_content_type, 316 const char *default_content_type,
317 const char *override_content_type,
246 Dowa_Arena *arena) 318 Dowa_Arena *arena)
247 { 319 {
248 if (!hg_response) 320 if (!hg_response)
249 return text_response( 321 return text_response(
250 arena, "502", "application/json", "{\"error\":\"Mercurial backend unavailable\"}"); 322 arena, "502", "application/json", "{\"error\":\"Mercurial backend unavailable\"}");
251 323
252 const char *upstream_content_type = 324 const char *upstream_content_type =
253 map_value_case_insensitive(hg_response->headers, "Content-Type"); 325 map_value_case_insensitive(hg_response->headers, "Content-Type");
254 const char *upstream_or_default_content_type = 326 const char *upstream_or_default_content_type =
255 upstream_content_type ? upstream_content_type : default_content_type; 327 override_content_type
328 ? override_content_type
329 : upstream_content_type ? upstream_content_type : default_content_type;
256 if (!upstream_or_default_content_type) 330 if (!upstream_or_default_content_type)
257 upstream_or_default_content_type = "application/octet-stream"; 331 upstream_or_default_content_type = "application/octet-stream";
258 char *content_type = arena_string(arena, upstream_or_default_content_type); 332 char *content_type = arena_string(arena, upstream_or_default_content_type);
259 333
260 char *status = Dowa_Arena_Allocate(arena, 8); 334 char *status = Dowa_Arena_Allocate(arena, 8);
295 return text_response(arena, "400", "application/json", "{\"error\":\"Repository path is too long\"}"); 369 return text_response(arena, "400", "application/json", "{\"error\":\"Repository path is too long\"}");
296 370
297 return forward_hg_response( 371 return forward_hg_response(
298 hg_proxy_request("GET", hg_path, NULL, 0, NULL, "application/json"), 372 hg_proxy_request("GET", hg_path, NULL, 0, NULL, "application/json"),
299 "application/json", 373 "application/json",
374 NULL,
300 arena); 375 arena);
301 } 376 }
302 377
303 Seobeo_Request_Entry *ApiGetFile(Seobeo_Request_Entry *request, Dowa_Arena *arena) 378 Seobeo_Request_Entry *ApiGetFile(Seobeo_Request_Entry *request, Dowa_Arena *arena)
304 { 379 {
312 char hg_path[MAX_PATH_LENGTH]; 387 char hg_path[MAX_PATH_LENGTH];
313 int length = snprintf(hg_path, sizeof(hg_path), "/raw-file/tip/%s", encoded); 388 int length = snprintf(hg_path, sizeof(hg_path), "/raw-file/tip/%s", encoded);
314 if (length < 0 || (size_t)length >= sizeof(hg_path)) 389 if (length < 0 || (size_t)length >= sizeof(hg_path))
315 return text_response(arena, "400", "text/plain", "File path is too long"); 390 return text_response(arena, "400", "text/plain", "File path is too long");
316 391
317 return forward_hg_response( 392 Seobeo_Client_Response *hg_response =
318 hg_proxy_request("GET", hg_path, NULL, 0, NULL, "application/octet-stream"), 393 hg_proxy_request("GET", hg_path, NULL, 0, NULL, "application/octet-stream");
319 "application/octet-stream", 394 if (!hg_response)
395 return forward_hg_response(NULL, "application/json", NULL, arena);
396
397 boolean inline_preview = FALSE;
398 boolean sandbox_content = FALSE;
399 const char *content_type =
400 repository_file_content_type(path, &inline_preview, &sandbox_content);
401 Seobeo_Request_Entry *response = forward_hg_response(
402 hg_response, "application/octet-stream", content_type, arena);
403 Dowa_HashMap_Push_Arena(
404 response,
405 "Content-Disposition",
406 inline_preview ? "inline" : "attachment",
320 arena); 407 arena);
408 Dowa_HashMap_Push_Arena(
409 response, "X-Content-Type-Options", "nosniff", arena);
410 if (sandbox_content)
411 Dowa_HashMap_Push_Arena(
412 response, "Content-Security-Policy", "sandbox", arena);
413 return response;
321 } 414 }
322 415
323 Seobeo_Request_Entry *ApiGetReadme(Seobeo_Request_Entry *request, Dowa_Arena *arena) 416 Seobeo_Request_Entry *ApiGetReadme(Seobeo_Request_Entry *request, Dowa_Arena *arena)
324 { 417 {
325 const char *encoded_path = map_value_case_insensitive(request, "query_path"); 418 const char *encoded_path = map_value_case_insensitive(request, "query_path");
349 if (hg_response && hg_response->status_code == HTTP_NOT_FOUND) 442 if (hg_response && hg_response->status_code == HTTP_NOT_FOUND)
350 { 443 {
351 Seobeo_Client_Response_Destroy(hg_response); 444 Seobeo_Client_Response_Destroy(hg_response);
352 return text_response(arena, "204", "text/markdown", ""); 445 return text_response(arena, "204", "text/markdown", "");
353 } 446 }
354 return forward_hg_response(hg_response, "text/markdown", arena); 447 return forward_hg_response(
448 hg_response, "text/markdown", "text/markdown; charset=utf-8", arena);
355 } 449 }
356 450
357 Seobeo_Request_Entry *ApiGetGraph(Seobeo_Request_Entry *request, Dowa_Arena *arena) 451 Seobeo_Request_Entry *ApiGetGraph(Seobeo_Request_Entry *request, Dowa_Arena *arena)
358 { 452 {
359 const char *graph_id = map_value_case_insensitive(request, ":graph_id"); 453 const char *graph_id = map_value_case_insensitive(request, ":graph_id");
383 return text_response(arena, "400", "application/json", "{\"error\":\"Graph request is too long\"}"); 477 return text_response(arena, "400", "application/json", "{\"error\":\"Graph request is too long\"}");
384 478
385 return forward_hg_response( 479 return forward_hg_response(
386 hg_proxy_request("GET", hg_path, NULL, 0, NULL, "application/json"), 480 hg_proxy_request("GET", hg_path, NULL, 0, NULL, "application/json"),
387 "application/json", 481 "application/json",
482 NULL,
388 arena); 483 arena);
389 } 484 }
390 485
391 Seobeo_Request_Entry *ApiGetChangeset(Seobeo_Request_Entry *request, Dowa_Arena *arena) 486 Seobeo_Request_Entry *ApiGetChangeset(Seobeo_Request_Entry *request, Dowa_Arena *arena)
392 { 487 {
400 return text_response(arena, "400", "application/json", "{\"error\":\"Changeset request is too long\"}"); 495 return text_response(arena, "400", "application/json", "{\"error\":\"Changeset request is too long\"}");
401 496
402 return forward_hg_response( 497 return forward_hg_response(
403 hg_proxy_request("GET", hg_path, NULL, 0, NULL, "application/json"), 498 hg_proxy_request("GET", hg_path, NULL, 0, NULL, "application/json"),
404 "application/json", 499 "application/json",
500 NULL,
405 arena); 501 arena);
406 } 502 }
407 503
408 static int64_t monotonic_milliseconds(void) 504 static int64_t monotonic_milliseconds(void)
409 { 505 {