Artifact 74a946854dd27b139410545ac5cf3d3be6b6af45bf957f3a4243580ae1e72791:
- File
internal/xfer/message.go
— part of check-in
[f5744359d3]
at
2026-04-19 16:30:54
on branch trunk
— Initial open source release
libfossil is a pure-Go implementation of the Fossil SCM, reading and writing the same .fossil SQLite repository format as upstream Fossil.
This is the first public release of code that previously lived in a private repository. The module path is github.com/danmestas/libfossil and the binary is named libfossil to avoid $PATH collisions with upstream fossil.
Included: - Repo lifecycle (create, clone), checkout/checkin, timeline, merge, diff, annotate, shun, tag, branch, bisect, stash, undo/redo - Sync-over-HTTP protocol (client and handler) - Two SQLite drivers: modernc (pure Go, default) and ncruces (wasm-capable) - Observer interfaces (SyncObserver, CheckoutObserver) with an optional OpenTelemetry implementation in observer/otel (separate submodule) - Deterministic simulation testing harness with BUGGIFY fault injection - Kong-based embeddable CLI under cli/ and a standalone binary under cmd/libfossil/ (user: [email protected], size: 3388) [annotate] [blame] [check-ins using]
package xfer import ( "bufio" "bytes" "compress/zlib" "encoding/binary" "fmt" "io" ) // Message is a sequence of cards forming one xfer request or response. type Message struct { Cards []Card } // maxDecompressedBytes caps the decompressed payload size to prevent // a small compressed payload from expanding to exhaust memory. const maxDecompressedBytes = 50 * 1024 * 1024 // 50 MB // Encode serializes all cards and zlib-compresses the result. // Uses Fossil's compression format: 4-byte big-endian uncompressed size prefix // followed by standard zlib data. func (m *Message) Encode() ([]byte, error) { if m == nil { panic("xfer.Message.Encode: m must not be nil") } raw, err := m.EncodeUncompressed() if err != nil { return nil, err } var zbuf bytes.Buffer // 4-byte big-endian uncompressed size prefix (Fossil's blob_compress format). var sizePrefix [4]byte binary.BigEndian.PutUint32(sizePrefix[:], uint32(len(raw))) zbuf.Write(sizePrefix[:]) zw := zlib.NewWriter(&zbuf) if _, err := zw.Write(raw); err != nil { return nil, fmt.Errorf("xfer: message zlib write: %w", err) } if err := zw.Close(); err != nil { return nil, fmt.Errorf("xfer: message zlib close: %w", err) } return zbuf.Bytes(), nil } // EncodeUncompressed serializes all cards without zlib compression. func (m *Message) EncodeUncompressed() ([]byte, error) { var buf bytes.Buffer for i, c := range m.Cards { if err := EncodeCard(&buf, c); err != nil { return nil, fmt.Errorf("xfer: encode card %d (%T): %w", i, c, err) } } return buf.Bytes(), nil } // Decode decodes an xfer message. It tries three formats in order: // 1. Raw zlib (Fossil HTTP sync wire format — no size prefix). // 2. 4-byte BE size prefix + zlib (Fossil blob compression / our Encode format). // 3. Uncompressed card data (clone protocol v3, x-fossil-uncompressed). // // The first format that successfully decompresses wins. func Decode(data []byte) (*Message, error) { if len(data) == 0 { return &Message{}, nil } // Format 1: raw zlib (Fossil HTTP /xfer wire format). if raw, err := decompressBounded(data); err == nil { return DecodeUncompressed(raw) } // Format 2: 4-byte size prefix + zlib (our Encode format). if len(data) >= 4 { if raw, err := decompressBounded(data[4:]); err == nil { return DecodeUncompressed(raw) } } // Format 3: uncompressed card data. return DecodeUncompressed(data) } // decompressBounded decompresses zlib data with a size cap. // Returns an error if decompression fails or exceeds maxDecompressedBytes. func decompressBounded(data []byte) ([]byte, error) { zr, err := zlib.NewReader(bytes.NewReader(data)) if err != nil { return nil, err } defer zr.Close() lr := io.LimitReader(zr, maxDecompressedBytes+1) raw, err := io.ReadAll(lr) if err != nil { return nil, err } if len(raw) > maxDecompressedBytes { return nil, fmt.Errorf("xfer: decompressed payload exceeds %d bytes", maxDecompressedBytes) } return raw, nil } // DecodeUncompressed decodes cards from uncompressed data. func DecodeUncompressed(data []byte) (*Message, error) { r := bufio.NewReader(bytes.NewReader(data)) msg := &Message{} for { card, err := DecodeCard(r) if err == io.EOF { return msg, nil } if err != nil { return nil, fmt.Errorf("xfer: decode card %d: %w", len(msg.Cards), err) } msg.Cards = append(msg.Cards, card) } }