Artifact d8b80c226061073c8c00e8903b3182459ce0098780c1d9e279c7ffefd31d25d6:
- File
internal/xfer/message_contenttype_test.go
— part of check-in
[f1236bd031]
at
2026-07-23 13:54:08
on branch trunk
— xfer: select framing by Content-Type; bound clone rounds by client decode limit (#133)
* fix: select xfer framing by Content-Type (#106); bound clone rounds by the client's decode limit (#109)
seeing which parsed, which made a decompression failure indistinguishable from "wrong framing" (the root of #104). Select framing from the §4 Content-Type instead: application/x-fossil is the §4.1 compressed container, every other type (including application/x-fossil-uncompressed, used by clone-v3 replies) is plain card text — the same rule canonical fossil applies. Verified against fossil 2.28: pull replies arrive as the prefixed container, clone replies as plain text, and no reply is ever an unprefixed raw zlib stream. The former unprefixed-zlib framing, checked first on the unverified belief that fossil emits it, was removed along with the length-prefix/zlib-header aliasing guard that only existed to disambiguate the two compressed framings trial-dispatch conflated. A compressed body that fails to inflate is now reported as the fault it is, never re-fed to the card parser. xfer.Decode takes the Content-Type; byte-oriented callers with no header pass the compressed type explicitly.
crossing one whole, so a round carried budget + one whole artifact. When sub-budget filler preceded a large artifact, that sum exceeded what the client could decode (xfer.MaxDecompressedBytes) and the clone failed with a spurious "declared container length exceeds" error whose threshold depended on incidental filler size. The server now flushes the round and sends an over-bound artifact alone when adding it would cross the client's decode bound, so clonability is a property of the artifact alone. The 2*budget compile-time guard now certifies what it appears to.
* fix: reject unrecognized xfer Content-Type with 415 (#106)
A POST whose Content-Type was absent or unrecognized fell through to the plaintext card decoder, which is exactly the #104 failure shape: possibly- compressed binary fed to the card parser. Request bodies come from untrusted peers, so a body carrying no framing signal is rejected rather than guessed at. Real fossil always sets the header; no conforming peer is affected.
xfer.KnownContentType shares the media-type parsing with the framing check, so the server and Decode agree on what a type means.
Also runs the #109 filler subtests in parallel — serially they pushed internal/sync past the 120s per-package timeout CI uses. (user: [email protected], size: 4539) [annotate] [blame] [check-ins using]
package xfer import ( "strings" "testing" ) // TestContentTypeIsCompressed pins the §4 media-type-to-framing rule: only // application/x-fossil selects the compressed container; every other type, // including application/x-fossil-uncompressed and an absent header, selects // plain card text. Method: map each media type and assert the boolean, which // is the whole of the dispatch decision. Case and trailing parameters are // ignored because §4 signals framing by the bare media type. func TestContentTypeIsCompressed(t *testing.T) { cases := []struct { contentType string compressed bool }{ {ContentTypeCompressed, true}, {"application/x-fossil", true}, {"APPLICATION/X-FOSSIL", true}, {"application/x-fossil; charset=utf-8", true}, {" application/x-fossil ", true}, {ContentTypeUncompressed, false}, {"application/x-fossil-uncompressed", false}, {"application/x-fossil-uncompressed; charset=utf-8", false}, {"text/plain", false}, {"", false}, } for _, c := range cases { if got := contentTypeIsCompressed(c.contentType); got != c.compressed { t.Errorf("contentTypeIsCompressed(%q) = %v, want %v", c.contentType, got, c.compressed) } } } // TestDecodeDispatchesOnContentType pins that framing follows the Content-Type, // not the bytes: a compressed container decodes to its real cards when labelled // compressed, and plain card text decodes when labelled uncompressed but is // refused when labelled compressed because it is not a zlib stream. Method: // build a container of known card count and plain card text, decode each under // its own label, and confirm the plain text is refused under the compressed // label rather than mis-decompressed. // // The reverse pairing -- a compressed body under the uncompressed label -- is // deliberately not asserted: DecodeUncompressed reads whatever lines it can and // does not reliably fault on binary input, so its output is unspecified. What // matters for §4 is that the label chooses the path, which the compressed-label // cases establish. func TestDecodeDispatchesOnContentType(t *testing.T) { compressed := compressedContainer(t, 4096) plaintext := []byte("igot " + strings.Repeat("a", 40) + "\nclone_seqno 3\n") if _, err := Decode(compressed, ContentTypeCompressed); err != nil { t.Errorf("compressed body under compressed type: %v", err) } msg, err := Decode(plaintext, ContentTypeUncompressed) if err != nil { t.Errorf("plain cards under uncompressed type: %v", err) } else if len(msg.Cards) != 2 { t.Errorf("plain cards under uncompressed type: %d cards, want 2", len(msg.Cards)) } if _, err := Decode(plaintext, ContentTypeCompressed); err == nil { t.Error("plain cards under compressed type decoded; they are not a zlib stream") } } // TestDecodeCompressedFailureNamesDecompression pins acceptance criterion 2 of // issue #106: a body whose Content-Type says compressed and which fails to // decompress reports the decompression fault, and is never retried as card // text. Method: build a body with a small in-bound declared length followed by // bytes that are not a zlib stream, so the failure occurs at inflation rather // than at the length check, and assert the error names the zlib failure and not // a card-parse failure. func TestDecodeCompressedFailureNamesDecompression(t *testing.T) { // Declared length 100 (well under the bound) so the length guard passes and // control reaches inflation; the payload after the prefix is not zlib. body := append([]byte{0x00, 0x00, 0x00, 0x64}, []byte("this is not a zlib stream at all")...) _, err := Decode(body, ContentTypeCompressed) if err == nil { t.Fatal("Decode accepted a non-zlib body under the compressed Content-Type") } if !strings.Contains(err.Error(), "zlib") { t.Errorf("error does not name the decompression fault: %v", err) } if strings.Contains(err.Error(), "decode card") { t.Errorf("compressed body was retried as cards: %v", err) } } // TestDecodeEmptyBodyIsEmptyMessage pins that an empty body is an empty message // under either framing, since a zero-length reply carries no cards. Method: // decode a nil body under each Content-Type and assert no error and no cards. func TestDecodeEmptyBodyIsEmptyMessage(t *testing.T) { for _, ct := range []string{ContentTypeCompressed, ContentTypeUncompressed, ""} { msg, err := Decode(nil, ct) if err != nil { t.Fatalf("Decode(nil, %q): %v", ct, err) } if len(msg.Cards) != 0 { t.Errorf("Decode(nil, %q) returned %d cards, want 0", ct, len(msg.Cards)) } } }