Artifact 9fa23fd466a1128a89a420829e25068b9f08bddfc40de1173f4450801ce2a0d2:
- File
internal/blob/compress.go
— part of check-in
[cc42ad8829]
at
2026-07-23 09:52:23
on branch trunk
— sync: store received wire frames verbatim instead of re-encoding (#132)
* fix: store received wire frames verbatim instead of re-encoding
Clone reproduced content correctly but not bytes: the receive path decompressed every cfile frame and re-compressed it with blob.Compress's unparameterized zlib writer, so a clone's blob.content never matched its source's — about 0.5% larger on the wapp corpus, purely from re-encoding frames that were already correctly encoded on the wire.
xfer's cfile decoder now hands back StoredBlob alongside the decompressed Content: the same bytes recast into Fossil's on-disk blob format ([4-byte BE size][zlib data]) without a second zlib pass — verbatim when the wire already sent that format (Fossil's send_compressed_file case), or the received zlib stream with a length prefix bolted on when it didn't (our own encoder's plain-zlib case). storeReceivedFile/storeDeltaContent/ storeResolvedContent and blob.StoreDeltaRaw thread that through via a new blob.EncodeForStorage helper, which writes the verbatim bytes when given and only falls back to Compress when there's nothing to preserve — covering both the clone/pull and server-push receive paths.
Locally authored content (never received over the wire) always takes the Compress fallback, so #71's deltification policy is untouched. The clone BUGGIFY corruption test drops storedBlob alongside the corrupted content it's exercising, so verbatim storage can't mask the corruption it exists to catch.
Adds a corpus-level regression test cloning a real multi-revision fossil repo and asserting sum(length(content)) matches between source and clone.
* test: use RequireFossilBin instead of removed HasFossil helper
testutil.HasFossil was removed upstream in favor of RequireFossilBin; this test's skip guard needed the same update to compile against current main. (user: [email protected], size: 3087) [annotate] [blame] [check-ins using]
package blob import ( "bytes" "compress/zlib" "encoding/binary" "fmt" "io" "github.com/danmestas/go-libfossil/simio" ) // Compress produces Fossil-compatible compressed blob content: // [4-byte big-endian uncompressed size][zlib-compressed data]. // This matches Fossil's blob_compress() in src/blob.c. func Compress(data []byte) (result []byte, err error) { if data == nil { panic("blob.Compress: data must not be nil") } defer func() { if err == nil && len(result) == 0 { panic("blob.Compress: postcondition violated: result is empty with no error") } }() var buf bytes.Buffer // 4-byte big-endian uncompressed size prefix. if err := binary.Write(&buf, binary.BigEndian, uint32(len(data))); err != nil { return nil, fmt.Errorf("write size prefix: %w", err) } w := zlib.NewWriter(&buf) if _, err := w.Write(data); err != nil { return nil, fmt.Errorf("zlib compress: %w", err) } if err := w.Close(); err != nil { return nil, fmt.Errorf("zlib close: %w", err) } return buf.Bytes(), nil } // EncodeForStorage returns the bytes to write into blob.content for data. // When verbatim is non-nil, it is already data re-expressed in Fossil's // on-disk blob format (e.g. bytes received over the wire that were already // encoded that way) and is returned as-is, with no zlib pass of our own. // When verbatim is nil, data is compressed fresh via Compress. // // Centralizing the choice here -- rather than in each receive-path caller -- // means a caller with wire-verbatim bytes on hand never has to decide // whether re-encoding is safe to skip; it just offers the bytes and this // function decides. Locally authored content, which never has verbatim // bytes to offer, is unaffected: it always takes the Compress path. func EncodeForStorage(data []byte, verbatim []byte) ([]byte, error) { if data == nil { panic("blob.EncodeForStorage: data must not be nil") } if verbatim != nil { if len(verbatim) == 0 { panic("blob.EncodeForStorage: verbatim must not be empty when non-nil") } return verbatim, nil } return Compress(data) } // Decompress handles Fossil's compressed blob format: // [4-byte big-endian uncompressed size][zlib-compressed data]. // The 4-byte prefix is skipped before decompressing. func Decompress(data []byte) (result []byte, err error) { if data == nil { panic("blob.Decompress: data must not be nil") } defer func() { if err == nil && result == nil { panic("blob.Decompress: postcondition violated: result is nil with no error") } }() if len(data) < 5 { return nil, fmt.Errorf("zlib decompress: data too short (%d bytes)", len(data)) } // Skip the 4-byte size prefix. zlibData := data[4:] r, err := zlib.NewReader(bytes.NewReader(zlibData)) if err != nil { return nil, fmt.Errorf("zlib decompress: %w", err) } defer r.Close() out, err := io.ReadAll(r) if err != nil { return nil, fmt.Errorf("zlib read: %w", err) } // BUGGIFY: truncate decompressed output to exercise partial-read handling. if simio.Buggify(0.02) && len(out) > 1 { out = out[:len(out)/2] } return out, nil }