Mercurial
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 { |