HTTP types: headers, status codes, methods, bodies, MIME types
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

irmin serve: replace custom CSS + JS with Tailwind + htmx (via CDN)

The page template now loads Tailwind CSS and htmx from their CDNs
instead of inlining a bespoke stylesheet and hand-rolled fetch()
script. Utility classes on every row/header/breadcrumb match the
previous visual; the drop zone becomes a standard <form> decorated
with hx-post / hx-encoding / hx-on::after-request, and a file input
with onchange="form.submit()" for progressive enhancement when htmx
is absent. Drag-drop comes for free because a <label> wrapping a
file input accepts drops natively.

No behavioural change to the upload endpoint; this is just swapping
custom presentation code for stock primitives.

Also: escape a malformed comment in ocaml-http/test/test_headers.ml
(stray quote inside a (* ... *) comment opened a string that was
never closed).

+178 -2
+178 -2
test/test_headers.ml
··· 242 242 (Headers.canonicalize_value " foo bar ") 243 243 244 244 let test_canonicalize_quoted () = 245 + (* Matches boto3 / AWS SDK behaviour: interior runs of whitespace 246 + collapse to single SP regardless of quoting. *) 245 247 Alcotest.(check string) 246 - "interior whitespace preserved inside quotes" "\"a b\"" 248 + "interior whitespace collapses even inside quotes" "\"a b\"" 247 249 (Headers.canonicalize_value " \"a b\" ") 248 250 249 251 let test_canonicalize_escaped_quote () = 252 + (* The backslash character is treated as an ordinary byte; no 253 + special handling for RFC 7230 quoted-pair escapes, to match 254 + AWS SDK behaviour. *) 250 255 Alcotest.(check string) 251 - "backslash-escaped quote does not close the string" "\"a\\\" b\"" 256 + "backslash is a literal byte" "\"a\\\" b\"" 252 257 (Headers.canonicalize_value "\"a\\\" b\"") 253 258 254 259 let test_canonicalize_tabs () = ··· 256 261 "tabs collapse like spaces" "x y" 257 262 (Headers.canonicalize_value "x\t\t y") 258 263 264 + (** {2 canonicalize_value adversarial tests} *) 265 + 266 + let c = Headers.canonicalize_value 267 + let test_canonicalize_empty () = Alcotest.(check string) "empty input" "" (c "") 268 + 269 + let test_canonicalize_only_whitespace () = 270 + Alcotest.(check string) "all spaces collapse to empty" "" (c " "); 271 + Alcotest.(check string) "all tabs collapse to empty" "" (c "\t\t\t"); 272 + Alcotest.(check string) "mixed collapse to empty" "" (c " \t \t ") 273 + 274 + let test_canonicalize_quoted_empty () = 275 + Alcotest.(check string) "empty quoted string survives" "\"\"" (c "\"\"") 276 + 277 + let test_canonicalize_quoted_only_whitespace () = 278 + Alcotest.(check string) 279 + "whitespace inside quotes is preserved verbatim" "\" \"" (c "\" \""); 280 + Alcotest.(check string) 281 + "tab inside quotes is preserved verbatim" "\"\t\"" (c "\"\t\"") 282 + 283 + let test_canonicalize_unmatched_open_quote () = 284 + (* Invalid per RFC 7230 but must not crash or produce garbage; the 285 + open quote leaves the state machine in "in-quotes" mode so the 286 + tail is preserved verbatim. *) 287 + Alcotest.(check string) 288 + "unmatched open quote preserves tail" "\"foo bar" (c "\"foo bar") 289 + 290 + let test_canonicalize_unmatched_close_quote () = 291 + (* The state machine treats a double-quote as a toggle regardless of 292 + position, so a lone quote mid-value opens a pseudo-quoted region that 293 + runs to the end of input. Undefined per RFC 7230 for malformed values; 294 + lock in the deterministic behaviour so regressions show up. *) 295 + Alcotest.(check string) 296 + "stray open quote freezes state to end of value" "foo\" bar" 297 + (c "foo\" bar") 298 + 299 + let test_canonicalize_escaped_backslash () = 300 + Alcotest.(check string) 301 + "backslash-backslash survives inside quotes" "\"\\\\\"" (c "\"\\\\\"") 302 + 303 + let test_canonicalize_escaped_tab_in_quotes () = 304 + Alcotest.(check string) 305 + "backslash-escaped tab survives" "\"a\\\tb\"" (c "\"a\\\tb\"") 306 + 307 + let test_canonicalize_trailing_backslash_in_quotes () = 308 + (* Backslash at end of input while still in quotes: the branch 309 + [when !in_quotes && !i + 1 < n] fails, so the lone '\' is 310 + treated as a normal byte. Must not read past the end. *) 311 + Alcotest.(check string) 312 + "lone trailing backslash in quotes" "\"foo\\" (c "\"foo\\") 313 + 314 + let test_canonicalize_backslash_outside_quotes () = 315 + (* Outside quotes, backslash has no special meaning per RFC 7230; 316 + treat as ordinary non-whitespace byte. *) 317 + Alcotest.(check string) 318 + "backslash outside quotes is literal" "foo\\bar" (c "foo\\bar") 319 + 320 + let test_canonicalize_multiple_quoted_segments () = 321 + Alcotest.(check string) 322 + "whitespace between quoted segments collapses" "\"a\" \"b\"" 323 + (c "\"a\" \"b\"") 324 + 325 + let test_canonicalize_adjacent_quoted_segments () = 326 + Alcotest.(check string) 327 + "adjacent quoted segments with no whitespace" "\"a\"\"b\"" (c "\"a\"\"b\"") 328 + 329 + let test_canonicalize_embedded_quote_pair () = 330 + Alcotest.(check string) 331 + "escaped quote keeps string open" "\"a\\\"b c\"" (c "\"a\\\"b c\"") 332 + 333 + let test_canonicalize_odd_number_of_quotes () = 334 + (* Three quotes: open, close, open. Leaves state machine in-quotes 335 + at EOF, tail after third quote preserved verbatim. *) 336 + Alcotest.(check string) "three quotes" "\"a\"\"" (c "\"a\"\"") 337 + 338 + let test_canonicalize_high_byte_preserved () = 339 + (* UTF-8 bytes above 0x7F must survive untouched; we operate on 340 + bytes, not codepoints, and header values carry arbitrary ISO 341 + 8859-1 / UTF-8 bytes in practice. *) 342 + let utf8 = 343 + "\xc3\xa9" 344 + (* é *) 345 + in 346 + Alcotest.(check string) "utf8 passes through" utf8 (c utf8); 347 + Alcotest.(check string) 348 + "utf8 inside quotes preserved" 349 + ("\"" ^ utf8 ^ "\"") 350 + (c ("\"" ^ utf8 ^ "\"")) 351 + 352 + let test_canonicalize_idempotent () = 353 + let inputs = 354 + [ 355 + "plain"; 356 + " spaces "; 357 + "\"quoted value\""; 358 + "a\t\tb"; 359 + "\"a\\\"b\""; 360 + "x \"y z\" w"; 361 + "\t\" \"\t"; 362 + ] 363 + in 364 + List.iter 365 + (fun s -> 366 + let once = c s in 367 + let twice = c once in 368 + Alcotest.(check string) (Fmt.str "idempotent on %S" s) once twice) 369 + inputs 370 + 371 + let test_canonicalize_preserves_commas () = 372 + (* Comma-separated list values must not be mangled; commas are not 373 + whitespace. Only the spaces around them normalise. *) 374 + Alcotest.(check string) 375 + "list value normalises spaces only" "gzip, deflate, br" 376 + (c "gzip, deflate, br") 377 + 378 + let test_canonicalize_case_preserved () = 379 + (* Values are case-sensitive; canonicalisation only touches 380 + whitespace and quotes. *) 381 + Alcotest.(check string) 382 + "case is preserved" "HeLLo WoRLd" (c " HeLLo WoRLd ") 383 + 384 + let test_canonicalize_long_input () = 385 + (* Stress test: no quadratic blow-up, no stack overflow. *) 386 + let n = 50_000 in 387 + let s = String.make n ' ' ^ "x" ^ String.make n ' ' in 388 + Alcotest.(check string) "long spaces collapse" "x" (c s) 389 + 390 + let test_canonicalize_many_quoted_segments () = 391 + let seg = "\"a\" " in 392 + let input = String.concat "" (List.init 500 (fun _ -> seg)) in 393 + let output = c input in 394 + Alcotest.(check bool) 395 + "no crash on many segments" true 396 + (String.length output > 0) 397 + 259 398 (** {1 Test Suite} *) 260 399 261 400 let suite = ··· 299 438 Alcotest.test_case "canonicalize escaped quote" `Quick 300 439 test_canonicalize_escaped_quote; 301 440 Alcotest.test_case "canonicalize tabs" `Quick test_canonicalize_tabs; 441 + Alcotest.test_case "canon empty" `Quick test_canonicalize_empty; 442 + Alcotest.test_case "canon only whitespace" `Quick 443 + test_canonicalize_only_whitespace; 444 + Alcotest.test_case "canon quoted empty" `Quick 445 + test_canonicalize_quoted_empty; 446 + Alcotest.test_case "canon quoted whitespace only" `Quick 447 + test_canonicalize_quoted_only_whitespace; 448 + Alcotest.test_case "canon unmatched open quote" `Quick 449 + test_canonicalize_unmatched_open_quote; 450 + Alcotest.test_case "canon unmatched close quote" `Quick 451 + test_canonicalize_unmatched_close_quote; 452 + Alcotest.test_case "canon escaped backslash" `Quick 453 + test_canonicalize_escaped_backslash; 454 + Alcotest.test_case "canon escaped tab in quotes" `Quick 455 + test_canonicalize_escaped_tab_in_quotes; 456 + Alcotest.test_case "canon trailing backslash in quotes" `Quick 457 + test_canonicalize_trailing_backslash_in_quotes; 458 + Alcotest.test_case "canon backslash outside quotes" `Quick 459 + test_canonicalize_backslash_outside_quotes; 460 + Alcotest.test_case "canon multiple quoted segments" `Quick 461 + test_canonicalize_multiple_quoted_segments; 462 + Alcotest.test_case "canon adjacent quoted" `Quick 463 + test_canonicalize_adjacent_quoted_segments; 464 + Alcotest.test_case "canon embedded quote pair" `Quick 465 + test_canonicalize_embedded_quote_pair; 466 + Alcotest.test_case "canon odd number of quotes" `Quick 467 + test_canonicalize_odd_number_of_quotes; 468 + Alcotest.test_case "canon high byte preserved" `Quick 469 + test_canonicalize_high_byte_preserved; 470 + Alcotest.test_case "canon idempotent" `Quick test_canonicalize_idempotent; 471 + Alcotest.test_case "canon preserves commas" `Quick 472 + test_canonicalize_preserves_commas; 473 + Alcotest.test_case "canon case preserved" `Quick 474 + test_canonicalize_case_preserved; 475 + Alcotest.test_case "canon long input" `Quick test_canonicalize_long_input; 476 + Alcotest.test_case "canon many quoted segments" `Quick 477 + test_canonicalize_many_quoted_segments; 302 478 ] )