shithub: furgit

Download patch

ref: af08c84539f9353718604988ba27ae3c466860fc
parent: 27ef9a7e1f2589d1a0eeee4cd6d36d1926989cf2
author: Runxi Yu <me@runxiyu.org>
date: Tue Mar 10 09:28:59 EDT 2026

*: Move sideband64k and pktline to protocol/

--- a/cmd/receivepack9418/errpkt.go
+++ b/cmd/receivepack9418/errpkt.go
@@ -3,7 +3,7 @@
 import (
 	"io"
 
-	"codeberg.org/lindenii/furgit/format/pktline"
+	"codeberg.org/lindenii/furgit/protocol/pktline"
 )
 
 func writeErrPkt(w io.Writer, message string) {
--- a/cmd/receivepack9418/gitproto.go
+++ b/cmd/receivepack9418/gitproto.go
@@ -4,7 +4,7 @@
 	"fmt"
 	"io"
 
-	"codeberg.org/lindenii/furgit/format/pktline"
+	"codeberg.org/lindenii/furgit/protocol/pktline"
 )
 
 func readGitProtoRequest(r io.Reader) (gitProtoRequest, error) {
--- a/format/pktline/append.go
+++ /dev/null
@@ -1,39 +1,0 @@
-package pktline
-
-import "fmt"
-
-// AppendData appends one data frame to dst.
-//
-// Empty payload is encoded as 0004.
-func AppendData(dst, payload []byte) ([]byte, error) {
-	if len(payload) > LargePacketDataMax {
-		return dst, fmt.Errorf("%w: %d > %d", ErrTooLarge, len(payload), LargePacketDataMax)
-	}
-
-	var hdr [4]byte
-
-	err := EncodeLengthHeader(&hdr, len(payload)+4)
-	if err != nil {
-		return dst, err
-	}
-
-	dst = append(dst, hdr[:]...)
-	dst = append(dst, payload...)
-
-	return dst, nil
-}
-
-// AppendFlushPkt appends control frame 0000 (flush-pkt).
-func AppendFlushPkt(dst []byte) []byte {
-	return append(dst, '0', '0', '0', '0')
-}
-
-// AppendDelimPkt appends control frame 0001 (delim-pkt).
-func AppendDelimPkt(dst []byte) []byte {
-	return append(dst, '0', '0', '0', '1')
-}
-
-// AppendResponseEndPkt appends control frame 0002 (response-end-pkt).
-func AppendResponseEndPkt(dst []byte) []byte {
-	return append(dst, '0', '0', '0', '2')
-}
--- a/format/pktline/append_data_preserves_dst_on_error_test.go
+++ /dev/null
@@ -1,25 +1,0 @@
-package pktline_test
-
-import (
-	"bytes"
-	"errors"
-	"testing"
-
-	"codeberg.org/lindenii/furgit/format/pktline"
-)
-
-func TestAppendDataPreservesDstOnError(t *testing.T) {
-	t.Parallel()
-
-	orig := []byte("seed")
-	dst := append([]byte(nil), orig...)
-
-	out, err := pktline.AppendData(dst, bytes.Repeat([]byte{'x'}, pktline.LargePacketDataMax+1))
-	if !errors.Is(err, pktline.ErrTooLarge) {
-		t.Fatalf("got err %v, want ErrTooLarge", err)
-	}
-
-	if !bytes.Equal(out, orig) {
-		t.Fatalf("got %q, want %q", string(out), string(orig))
-	}
-}
--- a/format/pktline/append_helpers_test.go
+++ /dev/null
@@ -1,24 +1,0 @@
-package pktline_test
-
-import (
-	"testing"
-
-	"codeberg.org/lindenii/furgit/format/pktline"
-)
-
-func TestAppendHelpers(t *testing.T) {
-	t.Parallel()
-
-	out, err := pktline.AppendData(nil, []byte("ok"))
-	if err != nil {
-		t.Fatalf("AppendData: %v", err)
-	}
-
-	out = pktline.AppendFlushPkt(out)
-	out = pktline.AppendDelimPkt(out)
-	out = pktline.AppendResponseEndPkt(out)
-
-	if got, want := string(out), "0006ok000000010002"; got != want {
-		t.Fatalf("got %q, want %q", got, want)
-	}
-}
--- a/format/pktline/chunk_writer.go
+++ /dev/null
@@ -1,65 +1,0 @@
-package pktline
-
-import "io"
-
-// ChunkWriter packetizes arbitrary stream bytes into data pkt-lines.
-// It never writes control packets automatically.
-type ChunkWriter struct {
-	enc *Encoder
-}
-
-// NewChunkWriter creates a chunking adapter over enc.
-func NewChunkWriter(enc *Encoder) *ChunkWriter {
-	return &ChunkWriter{enc: enc}
-}
-
-// Write splits p into data frames not larger than enc's maxData.
-//
-// It implements io.Writer.
-func (cw *ChunkWriter) Write(p []byte) (int, error) {
-	total := 0
-	maxData := cw.enc.effectiveMaxData()
-
-	for len(p) > 0 {
-		n := min(len(p), maxData)
-
-		err := cw.enc.WriteData(p[:n])
-		if err != nil {
-			return total, err
-		}
-
-		total += n
-		p = p[n:]
-	}
-
-	return total, nil
-}
-
-// ReadFrom reads from r and writes pkt-line data frames to the encoder.
-//
-// It implements io.ReaderFrom.
-func (cw *ChunkWriter) ReadFrom(r io.Reader) (int64, error) {
-	buf := make([]byte, cw.enc.effectiveMaxData())
-
-	var total int64
-
-	for {
-		n, err := r.Read(buf)
-		if n > 0 {
-			werr := cw.enc.WriteData(buf[:n])
-			if werr != nil {
-				return total, werr
-			}
-
-			total += int64(n)
-		}
-
-		if err != nil {
-			if err == io.EOF {
-				return total, nil
-			}
-
-			return total, err
-		}
-	}
-}
--- a/format/pktline/chunk_writer_write_and_read_from_test.go
+++ /dev/null
@@ -1,60 +1,0 @@
-package pktline_test
-
-import (
-	"bufio"
-	"bytes"
-	"strings"
-	"testing"
-
-	"codeberg.org/lindenii/furgit/format/pktline"
-)
-
-func TestChunkWriterWriteAndReadFrom(t *testing.T) {
-	t.Parallel()
-
-	var out bytes.Buffer
-
-	bw := bufio.NewWriter(&out)
-
-	enc := pktline.NewEncoder(bw)
-	enc.SetMaxData(3)
-	cw := pktline.NewChunkWriter(enc)
-
-	n, err := cw.Write([]byte("abcdefg"))
-	if err != nil {
-		t.Fatalf("Write: %v", err)
-	}
-
-	if n != 7 {
-		t.Fatalf("Write n=%d, want 7", n)
-	}
-
-	err = enc.FlushIO()
-	if err != nil {
-		t.Fatalf("FlushIO: %v", err)
-	}
-
-	if got, want := out.String(), "0007abc0007def0005g"; got != want {
-		t.Fatalf("got %q, want %q", got, want)
-	}
-
-	out.Reset()
-
-	rn, err := cw.ReadFrom(strings.NewReader("wxyz"))
-	if err != nil {
-		t.Fatalf("ReadFrom: %v", err)
-	}
-
-	if rn != 4 {
-		t.Fatalf("ReadFrom n=%d, want 4", rn)
-	}
-
-	err = enc.FlushIO()
-	if err != nil {
-		t.Fatalf("FlushIO: %v", err)
-	}
-
-	if got, want := out.String(), "0007wxy0005z"; got != want {
-		t.Fatalf("got %q, want %q", got, want)
-	}
-}
--- a/format/pktline/constants.go
+++ /dev/null
@@ -1,12 +1,0 @@
-package pktline
-
-const (
-	// DefaultPacketMax is a conservative packet size commonly used by
-	// line-oriented protocol messages.
-	DefaultPacketMax = 1000
-	// LargePacketMax is the maximum on-wire packet size including the
-	// 4-byte hexadecimal length header.
-	LargePacketMax = 65520
-	// LargePacketDataMax is the maximum payload size in one packet.
-	LargePacketDataMax = LargePacketMax - 4
-)
--- a/format/pktline/decoder.go
+++ /dev/null
@@ -1,187 +1,0 @@
-package pktline
-
-import (
-	"errors"
-	"fmt"
-	"io"
-)
-
-// ReadOptions controls decoding behavior.
-type ReadOptions struct {
-	// ChompLF removes one trailing '\n' from PacketData payloads.
-	ChompLF bool
-}
-
-// Decoder reads pkt-line frames from an io.Reader.
-//
-// It is advisable to supply a buffered reader.
-//
-// It preserves frame boundaries and supports one-frame lookahead via PeekFrame.
-type Decoder struct {
-	r       io.Reader
-	maxData int
-	opts    ReadOptions
-
-	peeked  bool
-	peek    Frame
-	peekErr error
-}
-
-// NewDecoder creates a decoder over r.
-func NewDecoder(r io.Reader, opts ReadOptions) *Decoder {
-	return &Decoder{
-		r:       r,
-		maxData: LargePacketDataMax,
-		opts:    opts,
-	}
-}
-
-// SetMaxData sets maximum payload size accepted for one data packet.
-//
-// Non-positive n resets to LargePacketDataMax.
-func (d *Decoder) SetMaxData(n int) {
-	if n <= 0 {
-		d.maxData = LargePacketDataMax
-
-		return
-	}
-
-	d.maxData = n
-}
-
-func cloneFrame(f Frame) Frame {
-	if f.Type != PacketData {
-		return Frame{Type: f.Type}
-	}
-
-	out := Frame{Type: f.Type}
-	if f.Payload != nil {
-		out.Payload = append([]byte(nil), f.Payload...)
-	}
-
-	return out
-}
-
-// ReadFrame reads one frame.
-//
-// 0000 is a PacketFlush
-// 0001 is a PacketDelim
-// 0002 is a PacketResponseEnd
-// 0004 is a PacketData with empty payload
-//
-// 0003 and malformed headers return *ProtocolError.
-func (d *Decoder) ReadFrame() (Frame, error) {
-	if d.peeked {
-		d.peeked = false
-
-		return cloneFrame(d.peek), d.peekErr
-	}
-
-	return d.readFrame()
-}
-
-// PeekFrame returns the next frame without consuming it.
-//
-// A subsequent ReadFrame returns the same frame.
-func (d *Decoder) PeekFrame() (Frame, error) {
-	if !d.peeked {
-		d.peek, d.peekErr = d.readFrame()
-		d.peeked = true
-	}
-
-	return cloneFrame(d.peek), d.peekErr
-}
-
-func (d *Decoder) readFrame() (Frame, error) {
-	var hdr [4]byte
-
-	_, err := io.ReadFull(d.r, hdr[:])
-	if err != nil {
-		if errors.Is(err, io.EOF) {
-			return Frame{}, io.EOF
-		}
-
-		if errors.Is(err, io.ErrUnexpectedEOF) {
-			return Frame{}, io.ErrUnexpectedEOF
-		}
-
-		return Frame{}, err
-	}
-
-	n, err := ParseLengthHeader(hdr)
-	if err != nil {
-		return Frame{}, &ProtocolError{Header: hdr, Reason: err.Error()}
-	}
-
-	switch n {
-	case 0:
-		return Frame{Type: PacketFlush}, nil
-	case 1:
-		return Frame{Type: PacketDelim}, nil
-	case 2:
-		return Frame{Type: PacketResponseEnd}, nil
-	case 3:
-		return Frame{}, &ProtocolError{Header: hdr, Reason: "invalid pkt-line length 3"}
-	}
-
-	if n < 4 {
-		return Frame{}, &ProtocolError{Header: hdr, Reason: fmt.Sprintf("invalid pkt-line length %d", n)}
-	}
-
-	if n > LargePacketMax {
-		perr := &ProtocolError{Header: hdr, Reason: fmt.Sprintf("pkt-line length %d exceeds max %d", n, LargePacketMax)}
-
-		err := d.discardPayload(n - 4)
-		if err != nil {
-			return Frame{}, errors.Join(perr, err)
-		}
-
-		return Frame{}, perr
-	}
-
-	payloadLen := n - 4
-	if payloadLen > d.maxData {
-		serr := fmt.Errorf("%w: %d > %d", ErrTooLarge, payloadLen, d.maxData)
-
-		err := d.discardPayload(payloadLen)
-		if err != nil {
-			return Frame{}, errors.Join(serr, err)
-		}
-
-		return Frame{}, serr
-	}
-
-	payload := make([]byte, payloadLen)
-
-	_, err = io.ReadFull(d.r, payload)
-	if err != nil {
-		if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
-			return Frame{}, io.ErrUnexpectedEOF
-		}
-
-		return Frame{}, err
-	}
-
-	if d.opts.ChompLF && len(payload) > 0 && payload[len(payload)-1] == '\n' {
-		payload = payload[:len(payload)-1]
-	}
-
-	return Frame{Type: PacketData, Payload: payload}, nil
-}
-
-func (d *Decoder) discardPayload(n int) error {
-	if n <= 0 {
-		return nil
-	}
-
-	_, err := io.CopyN(io.Discard, d.r, int64(n))
-	if err == nil {
-		return nil
-	}
-
-	if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
-		return io.ErrUnexpectedEOF
-	}
-
-	return err
-}
--- a/format/pktline/decoder_data_control_and_0004_test.go
+++ /dev/null
@@ -1,60 +1,0 @@
-package pktline_test
-
-import (
-	"strings"
-	"testing"
-
-	"codeberg.org/lindenii/furgit/format/pktline"
-)
-
-func TestDecoderDataControlAnd0004(t *testing.T) {
-	t.Parallel()
-
-	input := "0006a\n0004000100020000"
-	dec := pktline.NewDecoder(strings.NewReader(input), pktline.ReadOptions{ChompLF: true})
-
-	f, err := dec.ReadFrame()
-	if err != nil {
-		t.Fatalf("ReadFrame #1: %v", err)
-	}
-
-	if f.Type != pktline.PacketData || string(f.Payload) != "a" {
-		t.Fatalf("frame #1 = %#v", f)
-	}
-
-	f, err = dec.ReadFrame()
-	if err != nil {
-		t.Fatalf("ReadFrame #2: %v", err)
-	}
-
-	if f.Type != pktline.PacketData || len(f.Payload) != 0 {
-		t.Fatalf("frame #2 = %#v, want empty data", f)
-	}
-
-	f, err = dec.ReadFrame()
-	if err != nil {
-		t.Fatalf("ReadFrame #3: %v", err)
-	}
-
-	if f.Type != pktline.PacketDelim {
-		t.Fatalf("frame #3 type = %v, want PacketDelim", f.Type)
-	}
-
-	f, err = dec.ReadFrame()
-	if err != nil {
-		t.Fatalf("ReadFrame #4: %v", err)
-	}
-
-	if f.Type != pktline.PacketResponseEnd {
-		t.Fatalf("frame #4 type = %v, want PacketResponseEnd", f.Type)
-	}
-
-	f, err = dec.ReadFrame()
-	if err != nil {
-		t.Fatalf("ReadFrame #5: %v", err)
-	}
-
-	if f.Type != pktline.PacketFlush {
-		t.Fatalf("frame #5 type = %v, want PacketFlush", f.Type)
-	}
-}
--- a/format/pktline/decoder_invalid_0003_test.go
+++ /dev/null
@@ -1,20 +1,0 @@
-package pktline_test
-
-import (
-	"errors"
-	"strings"
-	"testing"
-
-	"codeberg.org/lindenii/furgit/format/pktline"
-)
-
-func TestDecoderInvalid0003(t *testing.T) {
-	t.Parallel()
-
-	dec := pktline.NewDecoder(strings.NewReader("0003"), pktline.ReadOptions{})
-	_, err := dec.ReadFrame()
-
-	if _, ok := errors.AsType[*pktline.ProtocolError](err); !ok {
-		t.Fatalf("got err %v, want ProtocolError", err)
-	}
-}
--- a/format/pktline/decoder_peek_test.go
+++ /dev/null
@@ -1,32 +1,0 @@
-package pktline_test
-
-import (
-	"strings"
-	"testing"
-
-	"codeberg.org/lindenii/furgit/format/pktline"
-)
-
-func TestDecoderPeek(t *testing.T) {
-	t.Parallel()
-
-	dec := pktline.NewDecoder(strings.NewReader("0005x0000"), pktline.ReadOptions{})
-
-	f, err := dec.PeekFrame()
-	if err != nil {
-		t.Fatalf("PeekFrame: %v", err)
-	}
-
-	if f.Type != pktline.PacketData || string(f.Payload) != "x" {
-		t.Fatalf("peek frame = %#v", f)
-	}
-
-	f, err = dec.ReadFrame()
-	if err != nil {
-		t.Fatalf("ReadFrame: %v", err)
-	}
-
-	if f.Type != pktline.PacketData || string(f.Payload) != "x" {
-		t.Fatalf("read frame = %#v", f)
-	}
-}
--- a/format/pktline/decoder_rejects_over_maximum_length_test.go
+++ /dev/null
@@ -1,22 +1,0 @@
-package pktline_test
-
-import (
-	"errors"
-	"strings"
-	"testing"
-
-	"codeberg.org/lindenii/furgit/format/pktline"
-)
-
-func TestDecoderRejectsOverMaximumLength(t *testing.T) {
-	t.Parallel()
-
-	dec := pktline.NewDecoder(strings.NewReader("fffe"), pktline.ReadOptions{})
-	dec.SetMaxData(70000)
-
-	_, err := dec.ReadFrame()
-
-	if _, ok := errors.AsType[*pktline.ProtocolError](err); !ok {
-		t.Fatalf("got err %v, want ProtocolError", err)
-	}
-}
--- a/format/pktline/decoder_resync_after_over_max_data_test.go
+++ /dev/null
@@ -1,51 +1,0 @@
-package pktline_test
-
-import (
-	"bufio"
-	"bytes"
-	"errors"
-	"testing"
-
-	"codeberg.org/lindenii/furgit/format/pktline"
-)
-
-func TestDecoderResyncAfterOverMaxData(t *testing.T) {
-	t.Parallel()
-
-	var b bytes.Buffer
-
-	bw := bufio.NewWriter(&b)
-	enc := pktline.NewEncoder(bw)
-
-	err := enc.WriteData([]byte("abcd"))
-	if err != nil {
-		t.Fatalf("WriteData #1: %v", err)
-	}
-
-	err = enc.WriteData([]byte("z"))
-	if err != nil {
-		t.Fatalf("WriteData #2: %v", err)
-	}
-
-	err = enc.FlushIO()
-	if err != nil {
-		t.Fatalf("FlushIO: %v", err)
-	}
-
-	dec := pktline.NewDecoder(bytes.NewReader(b.Bytes()), pktline.ReadOptions{})
-	dec.SetMaxData(1)
-
-	_, err = dec.ReadFrame()
-	if !errors.Is(err, pktline.ErrTooLarge) {
-		t.Fatalf("got err %v, want ErrTooLarge", err)
-	}
-
-	f, err := dec.ReadFrame()
-	if err != nil {
-		t.Fatalf("ReadFrame #2: %v", err)
-	}
-
-	if f.Type != pktline.PacketData || string(f.Payload) != "z" {
-		t.Fatalf("got frame %#v, want data z", f)
-	}
-}
--- a/format/pktline/decoder_resync_after_over_wire_max_test.go
+++ /dev/null
@@ -1,37 +1,0 @@
-package pktline_test
-
-import (
-	"bytes"
-	"errors"
-	"testing"
-
-	"codeberg.org/lindenii/furgit/format/pktline"
-)
-
-func TestDecoderResyncAfterOverWireMax(t *testing.T) {
-	t.Parallel()
-
-	var b bytes.Buffer
-
-	_, _ = b.WriteString("ffff")
-	_, _ = b.Write(bytes.Repeat([]byte{'a'}, 65531))
-	_, _ = b.WriteString("0005z")
-
-	dec := pktline.NewDecoder(bytes.NewReader(b.Bytes()), pktline.ReadOptions{})
-	dec.SetMaxData(70000)
-
-	_, err := dec.ReadFrame()
-
-	if _, ok := errors.AsType[*pktline.ProtocolError](err); !ok {
-		t.Fatalf("got err %v, want ProtocolError", err)
-	}
-
-	f, err := dec.ReadFrame()
-	if err != nil {
-		t.Fatalf("ReadFrame #2: %v", err)
-	}
-
-	if f.Type != pktline.PacketData || string(f.Payload) != "z" {
-		t.Fatalf("got frame %#v, want data z", f)
-	}
-}
--- a/format/pktline/decoder_unexpected_eof_test.go
+++ /dev/null
@@ -1,21 +1,0 @@
-package pktline_test
-
-import (
-	"errors"
-	"io"
-	"strings"
-	"testing"
-
-	"codeberg.org/lindenii/furgit/format/pktline"
-)
-
-func TestDecoderUnexpectedEOF(t *testing.T) {
-	t.Parallel()
-
-	dec := pktline.NewDecoder(strings.NewReader("0006a"), pktline.ReadOptions{})
-
-	_, err := dec.ReadFrame()
-	if !errors.Is(err, io.ErrUnexpectedEOF) {
-		t.Fatalf("got err %v, want io.ErrUnexpectedEOF", err)
-	}
-}
--- a/format/pktline/doc.go
+++ /dev/null
@@ -1,2 +1,0 @@
-// Package pktline implements the pkt-line format specified in gitprotocol-common(5).
-package pktline
--- a/format/pktline/encode_length_header_test.go
+++ /dev/null
@@ -1,28 +1,0 @@
-package pktline_test
-
-import (
-	"errors"
-	"testing"
-
-	"codeberg.org/lindenii/furgit/format/pktline"
-)
-
-func TestEncodeLengthHeader(t *testing.T) {
-	t.Parallel()
-
-	var hdr [4]byte
-
-	err := pktline.EncodeLengthHeader(&hdr, 4)
-	if err != nil {
-		t.Fatalf("unexpected error: %v", err)
-	}
-
-	if got := string(hdr[:]); got != "0004" {
-		t.Fatalf("got %q, want %q", got, "0004")
-	}
-
-	err = pktline.EncodeLengthHeader(&hdr, pktline.LargePacketMax+1)
-	if !errors.Is(err, pktline.ErrInvalidLength) {
-		t.Fatalf("got err %v, want ErrInvalidLength", err)
-	}
-}
--- a/format/pktline/encoder.go
+++ /dev/null
@@ -1,145 +1,0 @@
-package pktline
-
-import (
-	"fmt"
-	"io"
-)
-
-// WriteFlusher is the output transport contract required by Encoder.
-//
-// Write emits framed bytes and Flush pushes buffered transport state.
-type WriteFlusher interface {
-	io.Writer
-	Flush() error
-}
-
-// Encoder writes pkt-line frames to a flush-capable output transport.
-//
-// It writes exactly one frame per method call and does not auto-chunk data.
-type Encoder struct {
-	w       WriteFlusher
-	maxData int
-}
-
-// NewEncoder creates an encoder over w.
-func NewEncoder(w WriteFlusher) *Encoder {
-	return &Encoder{
-		w:       w,
-		maxData: LargePacketDataMax,
-	}
-}
-
-// SetMaxData sets the maximum payload size accepted by WriteData.
-//
-// Non-positive n resets to LargePacketDataMax.
-func (e *Encoder) SetMaxData(n int) {
-	if n <= 0 {
-		e.maxData = LargePacketDataMax
-
-		return
-	}
-
-	e.maxData = n
-}
-
-func writeAll(w io.Writer, b []byte) error {
-	for len(b) > 0 {
-		n, err := w.Write(b)
-		if err != nil {
-			return err
-		}
-
-		if n <= 0 {
-			return io.ErrShortWrite
-		}
-
-		b = b[n:]
-	}
-
-	return nil
-}
-
-// WriteData writes one data frame.
-//
-// Empty payload is encoded as 0004.
-func (e *Encoder) WriteData(p []byte) error {
-	maxData := e.effectiveMaxData()
-	if len(p) > maxData {
-		return fmt.Errorf("%w: %d > %d", ErrTooLarge, len(p), maxData)
-	}
-
-	var hdr [4]byte
-
-	err := EncodeLengthHeader(&hdr, len(p)+4)
-	if err != nil {
-		return err
-	}
-
-	err = writeAll(e.w, hdr[:])
-	if err != nil {
-		return err
-	}
-
-	return writeAll(e.w, p)
-}
-
-// WriteString writes one data frame containing s and returns len(s) on success.
-func (e *Encoder) WriteString(s string) (int, error) {
-	err := e.WriteData([]byte(s))
-	if err != nil {
-		return 0, err
-	}
-
-	return len(s), nil
-}
-
-// WriteFlush writes control frame 0000 (flush-pkt).
-func (e *Encoder) WriteFlush() error {
-	return e.writeControl(0)
-}
-
-// WriteDelim writes control frame 0001 (delim-pkt).
-func (e *Encoder) WriteDelim() error {
-	return e.writeControl(1)
-}
-
-// WriteResponseEnd writes control frame 0002 (response-end-pkt).
-func (e *Encoder) WriteResponseEnd() error {
-	return e.writeControl(2)
-}
-
-// FlushIO flushes buffered output in the underlying transport.
-//
-// FlushIO does not emit any pkt-line control frame.
-func (e *Encoder) FlushIO() error {
-	return e.w.Flush()
-}
-
-// WriteFlushAndFlushIO writes a flush-pkt (0000) then flushes transport I/O.
-func (e *Encoder) WriteFlushAndFlushIO() error {
-	err := e.WriteFlush()
-	if err != nil {
-		return err
-	}
-
-	return e.FlushIO()
-}
-
-func (e *Encoder) writeControl(n int) error {
-	var hdr [4]byte
-
-	err := EncodeLengthHeader(&hdr, n)
-	if err != nil {
-		return err
-	}
-
-	return writeAll(e.w, hdr[:])
-}
-
-func (e *Encoder) effectiveMaxData() int {
-	if e.maxData <= 0 || e.maxData > LargePacketDataMax {
-		return LargePacketDataMax
-	}
-
-	return e.maxData
-}
--- a/format/pktline/encoder_buffered_flush_and_f_flush_test.go
+++ /dev/null
@@ -1,50 +1,0 @@
-package pktline_test
-
-import (
-	"bufio"
-	"bytes"
-	"testing"
-
-	"codeberg.org/lindenii/furgit/format/pktline"
-)
-
-func TestEncoderBufferedFlushAndFFlush(t *testing.T) {
-	t.Parallel()
-
-	var out bytes.Buffer
-
-	bw := bufio.NewWriter(&out)
-	enc := pktline.NewEncoder(bw)
-
-	err := enc.WriteData([]byte("x"))
-	if err != nil {
-		t.Fatalf("WriteData: %v", err)
-	}
-
-	if out.Len() != 0 {
-		t.Fatalf("unexpected immediate output: %q", out.String())
-	}
-
-	err = enc.FlushIO()
-	if err != nil {
-		t.Fatalf("FlushIO: %v", err)
-	}
-
-	if out.String() != "0005x" {
-		t.Fatalf("got %q, want %q", out.String(), "0005x")
-	}
-
-	out.Reset()
-	bw = bufio.NewWriter(&out)
-
-	enc = pktline.NewEncoder(bw)
-
-	err = enc.WriteFlushAndFlushIO()
-	if err != nil {
-		t.Fatalf("WriteFlushAndFlushIO: %v", err)
-	}
-
-	if out.String() != "0000" {
-		t.Fatalf("got %q, want %q", out.String(), "0000")
-	}
-}
--- a/format/pktline/encoder_buffered_flush_behavior_test.go
+++ /dev/null
@@ -1,86 +1,0 @@
-package pktline_test
-
-import (
-	"bufio"
-	"bytes"
-	"testing"
-
-	"codeberg.org/lindenii/furgit/format/pktline"
-)
-
-func TestEncoderBufferedFlushBehavior(t *testing.T) {
-	t.Parallel()
-
-	var out bytes.Buffer
-
-	bw := bufio.NewWriter(&out)
-	enc := pktline.NewEncoder(bw)
-
-	err := enc.WriteData([]byte("hello"))
-	if err != nil {
-		t.Fatalf("WriteData: %v", err)
-	}
-
-	err = enc.WriteFlush()
-	if err != nil {
-		t.Fatalf("WriteFlush: %v", err)
-	}
-
-	if out.Len() != 0 {
-		t.Fatalf("WriteFlush should not flush I/O, got %q", out.String())
-	}
-
-	err = enc.FlushIO()
-	if err != nil {
-		t.Fatalf("FlushIO: %v", err)
-	}
-
-	if got, want := out.String(), "0009hello0000"; got != want {
-		t.Fatalf("got %q, want %q", got, want)
-	}
-
-	out.Reset()
-	bw = bufio.NewWriter(&out)
-	enc = pktline.NewEncoder(bw)
-
-	err = enc.WriteData([]byte("ok"))
-	if err != nil {
-		t.Fatalf("WriteData: %v", err)
-	}
-
-	err = enc.WriteFlush()
-	if err != nil {
-		t.Fatalf("WriteFlush: %v", err)
-	}
-
-	if out.Len() != 0 {
-		t.Fatalf("WriteFlush should not flush I/O, got %q", out.String())
-	}
-
-	err = enc.FlushIO()
-	if err != nil {
-		t.Fatalf("FlushIO: %v", err)
-	}
-
-	if got, want := out.String(), "0006ok0000"; got != want {
-		t.Fatalf("got %q, want %q", got, want)
-	}
-
-	out.Reset()
-	bw = bufio.NewWriter(&out)
-	enc = pktline.NewEncoder(bw)
-
-	err = enc.WriteData([]byte("yo"))
-	if err != nil {
-		t.Fatalf("WriteData: %v", err)
-	}
-
-	err = enc.WriteFlushAndFlushIO()
-	if err != nil {
-		t.Fatalf("WriteFlushAndFlushIO: %v", err)
-	}
-
-	if got, want := out.String(), "0006yo0000"; got != want {
-		t.Fatalf("got %q, want %q", got, want)
-	}
-}
--- a/format/pktline/encoder_set_max_data_cannot_exceed_wire_limit_test.go
+++ /dev/null
@@ -1,26 +1,0 @@
-package pktline_test
-
-import (
-	"bufio"
-	"bytes"
-	"errors"
-	"testing"
-
-	"codeberg.org/lindenii/furgit/format/pktline"
-)
-
-func TestEncoderSetMaxDataCannotExceedWireLimit(t *testing.T) {
-	t.Parallel()
-
-	var out bytes.Buffer
-
-	bw := bufio.NewWriter(&out)
-
-	enc := pktline.NewEncoder(bw)
-	enc.SetMaxData(pktline.LargePacketDataMax + 100)
-
-	err := enc.WriteData(bytes.Repeat([]byte{'x'}, pktline.LargePacketDataMax+1))
-	if !errors.Is(err, pktline.ErrTooLarge) {
-		t.Fatalf("got err %v, want ErrTooLarge", err)
-	}
-}
--- a/format/pktline/encoder_writes_frames_test.go
+++ /dev/null
@@ -1,51 +1,0 @@
-package pktline_test
-
-import (
-	"bufio"
-	"bytes"
-	"testing"
-
-	"codeberg.org/lindenii/furgit/format/pktline"
-)
-
-func TestEncoderWritesFrames(t *testing.T) {
-	t.Parallel()
-
-	var b bytes.Buffer
-
-	bw := bufio.NewWriter(&b)
-
-	enc := pktline.NewEncoder(bw)
-
-	err := enc.WriteData([]byte("hi"))
-	if err != nil {
-		t.Fatalf("WriteData: %v", err)
-	}
-
-	err = enc.WriteFlush()
-	if err != nil {
-		t.Fatalf("WriteFlush: %v", err)
-	}
-
-	err = enc.WriteDelim()
-	if err != nil {
-		t.Fatalf("WriteDelim: %v", err)
-	}
-
-	err = enc.WriteResponseEnd()
-	if err != nil {
-		t.Fatalf("WriteResponseEnd: %v", err)
-	}
-
-	err = enc.FlushIO()
-	if err != nil {
-		t.Fatalf("FlushIO: %v", err)
-	}
-
-	got := b.String()
-
-	want := "0006hi000000010002"
-	if got != want {
-		t.Fatalf("got %q, want %q", got, want)
-	}
-}
--- a/format/pktline/errors.go
+++ /dev/null
@@ -1,31 +1,0 @@
-package pktline
-
-import "errors"
-
-var (
-	// ErrInvalidLength indicates a malformed 4-byte hexadecimal length header.
-	ErrInvalidLength = errors.New("pktline: invalid length header")
-	// ErrTooLarge indicates a payload exceeds configured packet data limits.
-	ErrTooLarge = errors.New("pktline: payload too large")
-)
-
-// ProtocolError reports invalid pkt-line framing.
-//
-// It is returned for protocol violations such as invalid control values
-// (for example 0003) or non-hex length headers.
-type ProtocolError struct {
-	Header [4]byte
-	Reason string
-}
-
-func (e *ProtocolError) Error() string {
-	if e == nil {
-		return "<nil>"
-	}
-
-	if e.Reason == "" {
-		return "pktline: protocol error"
-	}
-
-	return "pktline: protocol error: " + e.Reason
-}
--- a/format/pktline/frame.go
+++ /dev/null
@@ -1,10 +1,0 @@
-package pktline
-
-// Frame is one decoded pkt-line frame.
-//
-// For PacketData, Payload holds frame bytes (possibly empty for 0004).
-// For control frames, Payload is nil.
-type Frame struct {
-	Type    PacketType
-	Payload []byte
-}
--- a/format/pktline/header.go
+++ /dev/null
@@ -1,57 +1,0 @@
-package pktline
-
-import "fmt"
-
-func hexval(b byte) int {
-	switch {
-	case b >= '0' && b <= '9':
-		return int(b - '0')
-	case b >= 'a' && b <= 'f':
-		return int(b-'a') + 10
-	case b >= 'A' && b <= 'F':
-		return int(b-'A') + 10
-	default:
-		return -1
-	}
-}
-
-// ParseLengthHeader parses a 4-byte hexadecimal pkt-line length header.
-//
-// The returned value is the full on-wire packet size, including the 4-byte
-// header. Semantic interpretation (data/control/error) is done by Decoder.
-//
-// The 4-byte header is only an actual length when above or equal to 4.
-// Otherwise, it indicates some control packet.
-func ParseLengthHeader(h [4]byte) (int, error) {
-	a := hexval(h[0])
-	b := hexval(h[1])
-	c := hexval(h[2])
-	d := hexval(h[3])
-
-	if a < 0 || b < 0 || c < 0 || d < 0 {
-		return 0, fmt.Errorf("%w: %q", ErrInvalidLength, string(h[:]))
-	}
-
-	return (a << 12) | (b << 8) | (c << 4) | d, nil
-}
-
-// EncodeLengthHeader encodes n as a 4-byte hexadecimal pkt-line header.
-//
-// n is the full on-wire packet size including the 4-byte header.
-//
-// The 4-byte header is only an actual length when above or equal to 4.
-// Otherwise, it indicates some control packet.
-func EncodeLengthHeader(dst *[4]byte, n int) error {
-	if n < 0 || n > LargePacketMax {
-		return fmt.Errorf("%w: %d", ErrInvalidLength, n)
-	}
-
-	const hex = "0123456789abcdef"
-
-	dst[0] = hex[(n>>12)&0xf]
-	dst[1] = hex[(n>>8)&0xf]
-	dst[2] = hex[(n>>4)&0xf]
-	dst[3] = hex[n&0xf]
-
-	return nil
-}
--- a/format/pktline/parse_length_header_test.go
+++ /dev/null
@@ -1,26 +1,0 @@
-package pktline_test
-
-import (
-	"errors"
-	"testing"
-
-	"codeberg.org/lindenii/furgit/format/pktline"
-)
-
-func TestParseLengthHeader(t *testing.T) {
-	t.Parallel()
-
-	n, err := pktline.ParseLengthHeader([4]byte{'0', '0', '0', '4'})
-	if err != nil {
-		t.Fatalf("unexpected error: %v", err)
-	}
-
-	if n != 4 {
-		t.Fatalf("got %d, want 4", n)
-	}
-
-	_, err = pktline.ParseLengthHeader([4]byte{'0', '0', '0', 'x'})
-	if !errors.Is(err, pktline.ErrInvalidLength) {
-		t.Fatalf("got err %v, want ErrInvalidLength", err)
-	}
-}
--- a/format/pktline/type.go
+++ /dev/null
@@ -1,15 +1,0 @@
-package pktline
-
-// PacketType identifies the kind of pkt-line frame.
-type PacketType uint8
-
-const (
-	// PacketData is a regular data frame whose payload is application-defined.
-	PacketData PacketType = iota
-	// PacketFlush is control frame 0000 and marks end of a message.
-	PacketFlush
-	// PacketDelim is control frame 0001 and separates sections in protocol v2.
-	PacketDelim
-	// PacketResponseEnd is control frame 0002 and marks response end on stateless v2 transports.
-	PacketResponseEnd
-)
--- a/format/sideband64k/append.go
+++ /dev/null
@@ -1,40 +1,0 @@
-package sideband64k
-
-import (
-	"fmt"
-
-	"codeberg.org/lindenii/furgit/format/pktline"
-)
-
-// AppendBand appends one side-band-64k data frame to dst.
-func AppendBand(dst []byte, band Band, payload []byte) ([]byte, error) {
-	if !validBand(band) {
-		return dst, fmt.Errorf("%w: %d", ErrInvalidBand, band)
-	}
-
-	maxData := effectiveMaxData(DataMax)
-	if len(payload) > maxData {
-		return dst, fmt.Errorf("%w: %d > %d", ErrTooLarge, len(payload), maxData)
-	}
-
-	framed := make([]byte, len(payload)+1)
-	framed[0] = byte(band)
-	copy(framed[1:], payload)
-
-	return pktline.AppendData(dst, framed)
-}
-
-// AppendData appends one band-1 data frame to dst.
-func AppendData(dst, payload []byte) ([]byte, error) {
-	return AppendBand(dst, BandData, payload)
-}
-
-// AppendProgress appends one band-2 progress frame to dst.
-func AppendProgress(dst, payload []byte) ([]byte, error) {
-	return AppendBand(dst, BandProgress, payload)
-}
-
-// AppendError appends one band-3 error frame to dst.
-func AppendError(dst, payload []byte) ([]byte, error) {
-	return AppendBand(dst, BandError, payload)
-}
--- a/format/sideband64k/append_helpers_test.go
+++ /dev/null
@@ -1,30 +1,0 @@
-package sideband64k_test
-
-import (
-	"testing"
-
-	"codeberg.org/lindenii/furgit/format/sideband64k"
-)
-
-func TestAppendHelpers(t *testing.T) {
-	t.Parallel()
-
-	out, err := sideband64k.AppendData(nil, []byte("a"))
-	if err != nil {
-		t.Fatalf("AppendData: %v", err)
-	}
-
-	out, err = sideband64k.AppendProgress(out, []byte("b"))
-	if err != nil {
-		t.Fatalf("AppendProgress: %v", err)
-	}
-
-	out, err = sideband64k.AppendError(out, []byte("c"))
-	if err != nil {
-		t.Fatalf("AppendError: %v", err)
-	}
-
-	if got, want := string(out), "0006\x01a0006\x02b0006\x03c"; got != want {
-		t.Fatalf("got %q, want %q", got, want)
-	}
-}
--- a/format/sideband64k/append_preserves_dst_on_error_test.go
+++ /dev/null
@@ -1,34 +1,0 @@
-package sideband64k_test
-
-import (
-	"bytes"
-	"errors"
-	"testing"
-
-	"codeberg.org/lindenii/furgit/format/sideband64k"
-)
-
-func TestAppendBandPreservesDstOnError(t *testing.T) {
-	t.Parallel()
-
-	orig := []byte("seed")
-	dst := append([]byte(nil), orig...)
-
-	out, err := sideband64k.AppendBand(dst, 4, []byte("x"))
-	if !errors.Is(err, sideband64k.ErrInvalidBand) {
-		t.Fatalf("got err %v, want ErrInvalidBand", err)
-	}
-
-	if !bytes.Equal(out, orig) {
-		t.Fatalf("got %q, want %q", string(out), string(orig))
-	}
-
-	out, err = sideband64k.AppendData(dst, bytes.Repeat([]byte{'x'}, sideband64k.DataMax+1))
-	if !errors.Is(err, sideband64k.ErrTooLarge) {
-		t.Fatalf("got err %v, want ErrTooLarge", err)
-	}
-
-	if !bytes.Equal(out, orig) {
-		t.Fatalf("got %q, want %q", string(out), string(orig))
-	}
-}
--- a/format/sideband64k/band.go
+++ /dev/null
@@ -1,13 +1,0 @@
-package sideband64k
-
-// Band identifies the sideband stream within a pkt-line data frame.
-type Band uint8
-
-const (
-	// BandData carries primary payload bytes.
-	BandData Band = 1
-	// BandProgress carries progress or informational messages.
-	BandProgress Band = 2
-	// BandError carries fatal error messages.
-	BandError Band = 3
-)
--- a/format/sideband64k/chunk_writer.go
+++ /dev/null
@@ -1,64 +1,0 @@
-package sideband64k
-
-import "io"
-
-// ChunkWriter packetizes arbitrary stream bytes into side-band-64k data frames
-// for one fixed band.
-//
-// It never writes control packets automatically.
-type ChunkWriter struct {
-	enc  *Encoder
-	band Band
-}
-
-// NewChunkWriter creates a chunking adapter over enc for one band.
-func NewChunkWriter(enc *Encoder, band Band) *ChunkWriter {
-	return &ChunkWriter{enc: enc, band: band}
-}
-
-// Write splits p into sideband frames not larger than enc's maxData.
-func (cw *ChunkWriter) Write(p []byte) (int, error) {
-	total := 0
-	maxData := cw.enc.effectiveMaxData()
-
-	for len(p) > 0 {
-		n := min(len(p), maxData)
-
-		err := cw.enc.WriteBand(cw.band, p[:n])
-		if err != nil {
-			return total, err
-		}
-
-		total += n
-		p = p[n:]
-	}
-
-	return total, nil
-}
-
-// ReadFrom reads from r and writes sideband frames to the encoder.
-func (cw *ChunkWriter) ReadFrom(r io.Reader) (int64, error) {
-	buf := make([]byte, cw.enc.effectiveMaxData())
-
-	var total int64
-
-	for {
-		n, err := r.Read(buf)
-		if n > 0 {
-			werr := cw.enc.WriteBand(cw.band, buf[:n])
-			if werr != nil {
-				return total, werr
-			}
-
-			total += int64(n)
-		}
-
-		if err != nil {
-			if err == io.EOF {
-				return total, nil
-			}
-
-			return total, err
-		}
-	}
-}
--- a/format/sideband64k/chunk_writer_write_and_read_from_test.go
+++ /dev/null
@@ -1,60 +1,0 @@
-package sideband64k_test
-
-import (
-	"bufio"
-	"bytes"
-	"strings"
-	"testing"
-
-	"codeberg.org/lindenii/furgit/format/sideband64k"
-)
-
-func TestChunkWriterWriteAndReadFrom(t *testing.T) {
-	t.Parallel()
-
-	var out bytes.Buffer
-
-	bw := bufio.NewWriter(&out)
-	enc := sideband64k.NewEncoder(bw)
-	enc.SetMaxData(3)
-
-	cw := sideband64k.NewChunkWriter(enc, sideband64k.BandProgress)
-
-	n, err := cw.Write([]byte("abcdefg"))
-	if err != nil {
-		t.Fatalf("Write: %v", err)
-	}
-
-	if n != 7 {
-		t.Fatalf("Write n=%d, want 7", n)
-	}
-
-	err = enc.FlushIO()
-	if err != nil {
-		t.Fatalf("FlushIO: %v", err)
-	}
-
-	if got, want := out.String(), "0008\x02abc0008\x02def0006\x02g"; got != want {
-		t.Fatalf("got %q, want %q", got, want)
-	}
-
-	out.Reset()
-
-	rn, err := cw.ReadFrom(strings.NewReader("wxyz"))
-	if err != nil {
-		t.Fatalf("ReadFrom: %v", err)
-	}
-
-	if rn != 4 {
-		t.Fatalf("ReadFrom n=%d, want 4", rn)
-	}
-
-	err = enc.FlushIO()
-	if err != nil {
-		t.Fatalf("FlushIO: %v", err)
-	}
-
-	if got, want := out.String(), "0008\x02wxy0006\x02z"; got != want {
-		t.Fatalf("got %q, want %q", got, want)
-	}
-}
--- a/format/sideband64k/constants.go
+++ /dev/null
@@ -1,10 +1,0 @@
-package sideband64k
-
-import "codeberg.org/lindenii/furgit/format/pktline"
-
-const (
-	// PacketMax is the maximum on-wire pkt-line size used by side-band-64k.
-	PacketMax = pktline.LargePacketMax
-	// DataMax is the maximum sideband payload size excluding the 1-byte band designator.
-	DataMax = pktline.LargePacketDataMax - 1
-)
--- a/format/sideband64k/decoder.go
+++ /dev/null
@@ -1,158 +1,0 @@
-package sideband64k
-
-import (
-	"fmt"
-	"io"
-
-	"codeberg.org/lindenii/furgit/format/pktline"
-)
-
-// ReadOptions controls sideband decoding behavior.
-type ReadOptions struct {
-	// ChompLF removes one trailing '\n' from FrameData payloads only.
-	ChompLF bool
-}
-
-// Decoder reads side-band-64k frames from an io.Reader.
-//
-// It preserves frame boundaries and supports one-frame lookahead via
-// PeekFrame.
-type Decoder struct {
-	dec     *pktline.Decoder
-	maxData int
-	opts    ReadOptions
-
-	peeked  bool
-	peek    Frame
-	peekErr error
-}
-
-// NewDecoder creates a decoder over r.
-func NewDecoder(r io.Reader, opts ReadOptions) *Decoder {
-	d := &Decoder{
-		dec:     pktline.NewDecoder(r, pktline.ReadOptions{}),
-		maxData: DataMax,
-		opts:    opts,
-	}
-	d.dec.SetMaxData(pktline.LargePacketDataMax)
-
-	return d
-}
-
-// SetMaxData sets maximum payload size accepted for one sideband data packet.
-//
-// Non-positive n resets to DataMax.
-func (d *Decoder) SetMaxData(n int) {
-	if n <= 0 {
-		d.maxData = DataMax
-
-		return
-	}
-
-	d.maxData = n
-}
-
-// ReadFrame reads one frame.
-func (d *Decoder) ReadFrame() (Frame, error) {
-	if d.peeked {
-		d.peeked = false
-
-		return cloneFrame(d.peek), d.peekErr
-	}
-
-	return d.readFrame()
-}
-
-// PeekFrame returns the next frame without consuming it.
-func (d *Decoder) PeekFrame() (Frame, error) {
-	if !d.peeked {
-		d.peek, d.peekErr = d.readFrame()
-		d.peeked = true
-	}
-
-	return cloneFrame(d.peek), d.peekErr
-}
-
-func (d *Decoder) readFrame() (Frame, error) {
-	f, err := d.dec.ReadFrame()
-	if err != nil {
-		return Frame{}, err
-	}
-
-	switch f.Type {
-	case pktline.PacketFlush:
-		return Frame{Type: FrameFlush}, nil
-	case pktline.PacketDelim:
-		return Frame{Type: FrameDelim}, nil
-	case pktline.PacketResponseEnd:
-		return Frame{Type: FrameResponseEnd}, nil
-	case pktline.PacketData:
-		if len(f.Payload) == 0 {
-			return Frame{}, &ProtocolError{Reason: "missing sideband designator"}
-		}
-
-		payload := f.Payload[1:]
-		if len(payload) > d.effectiveMaxData() {
-			return Frame{}, fmt.Errorf("%w: %d > %d", ErrTooLarge, len(payload), d.effectiveMaxData())
-		}
-
-		band := Band(f.Payload[0])
-		if !validBand(band) {
-			return Frame{}, &ProtocolError{Reason: fmt.Sprintf("%v: %d", ErrInvalidBand, band)}
-		}
-
-		payload = append([]byte(nil), payload...)
-		if d.opts.ChompLF && band == BandData && len(payload) > 0 && payload[len(payload)-1] == '\n' {
-			payload = payload[:len(payload)-1]
-		}
-
-		return Frame{
-			Type:    frameTypeForBand(band),
-			Payload: payload,
-		}, nil
-	default:
-		return Frame{}, &ProtocolError{Reason: "unknown pkt-line frame type"}
-	}
-}
-
-func (d *Decoder) effectiveMaxData() int {
-	return effectiveMaxData(d.maxData)
-}
-
-func cloneFrame(f Frame) Frame {
-	if f.Type == FrameFlush || f.Type == FrameDelim || f.Type == FrameResponseEnd {
-		return Frame{Type: f.Type}
-	}
-
-	out := Frame{Type: f.Type}
-	if f.Payload != nil {
-		out.Payload = append([]byte(nil), f.Payload...)
-	}
-
-	return out
-}
-
-func validBand(band Band) bool {
-	return band == BandData || band == BandProgress || band == BandError
-}
-
-func frameTypeForBand(band Band) FrameType {
-	switch band {
-	case BandData:
-		return FrameData
-	case BandProgress:
-		return FrameProgress
-	case BandError:
-		return FrameError
-	default:
-		panic("invalid sideband64k band")
-	}
-}
-
-func effectiveMaxData(n int) int {
-	if n <= 0 || n > DataMax {
-		return DataMax
-	}
-
-	return n
-}
--- a/format/sideband64k/decoder_data_control_and_keepalive_test.go
+++ /dev/null
@@ -1,78 +1,0 @@
-package sideband64k_test
-
-import (
-	"strings"
-	"testing"
-
-	"codeberg.org/lindenii/furgit/format/sideband64k"
-)
-
-func TestDecoderDataControlAndKeepalive(t *testing.T) {
-	t.Parallel()
-
-	input := "0007\x01a\n0005\x010007\x02p\n0007\x03e\n000100020000"
-	dec := sideband64k.NewDecoder(strings.NewReader(input), sideband64k.ReadOptions{ChompLF: true})
-
-	f, err := dec.ReadFrame()
-	if err != nil {
-		t.Fatalf("ReadFrame #1: %v", err)
-	}
-
-	if f.Type != sideband64k.FrameData || string(f.Payload) != "a" {
-		t.Fatalf("frame #1 = %#v", f)
-	}
-
-	f, err = dec.ReadFrame()
-	if err != nil {
-		t.Fatalf("ReadFrame #2: %v", err)
-	}
-
-	if f.Type != sideband64k.FrameData || len(f.Payload) != 0 {
-		t.Fatalf("frame #2 = %#v, want empty data", f)
-	}
-
-	f, err = dec.ReadFrame()
-	if err != nil {
-		t.Fatalf("ReadFrame #3: %v", err)
-	}
-
-	if f.Type != sideband64k.FrameProgress || string(f.Payload) != "p\n" {
-		t.Fatalf("frame #3 = %#v", f)
-	}
-
-	f, err = dec.ReadFrame()
-	if err != nil {
-		t.Fatalf("ReadFrame #4: %v", err)
-	}
-
-	if f.Type != sideband64k.FrameError || string(f.Payload) != "e\n" {
-		t.Fatalf("frame #4 = %#v", f)
-	}
-
-	f, err = dec.ReadFrame()
-	if err != nil {
-		t.Fatalf("ReadFrame #5: %v", err)
-	}
-
-	if f.Type != sideband64k.FrameDelim {
-		t.Fatalf("frame #5 type = %v, want FrameDelim", f.Type)
-	}
-
-	f, err = dec.ReadFrame()
-	if err != nil {
-		t.Fatalf("ReadFrame #6: %v", err)
-	}
-
-	if f.Type != sideband64k.FrameResponseEnd {
-		t.Fatalf("frame #6 type = %v, want FrameResponseEnd", f.Type)
-	}
-
-	f, err = dec.ReadFrame()
-	if err != nil {
-		t.Fatalf("ReadFrame #7: %v", err)
-	}
-
-	if f.Type != sideband64k.FrameFlush {
-		t.Fatalf("frame #7 type = %v, want FrameFlush", f.Type)
-	}
-}
--- a/format/sideband64k/decoder_invalid_band_test.go
+++ /dev/null
@@ -1,20 +1,0 @@
-package sideband64k_test
-
-import (
-	"errors"
-	"strings"
-	"testing"
-
-	"codeberg.org/lindenii/furgit/format/sideband64k"
-)
-
-func TestDecoderInvalidBand(t *testing.T) {
-	t.Parallel()
-
-	dec := sideband64k.NewDecoder(strings.NewReader("0005\x04"), sideband64k.ReadOptions{})
-	_, err := dec.ReadFrame()
-
-	if _, ok := errors.AsType[*sideband64k.ProtocolError](err); !ok {
-		t.Fatalf("got err %v, want ProtocolError", err)
-	}
-}
--- a/format/sideband64k/decoder_invalid_empty_payload_test.go
+++ /dev/null
@@ -1,20 +1,0 @@
-package sideband64k_test
-
-import (
-	"errors"
-	"strings"
-	"testing"
-
-	"codeberg.org/lindenii/furgit/format/sideband64k"
-)
-
-func TestDecoderInvalidEmptyPayload(t *testing.T) {
-	t.Parallel()
-
-	dec := sideband64k.NewDecoder(strings.NewReader("0004"), sideband64k.ReadOptions{})
-	_, err := dec.ReadFrame()
-
-	if _, ok := errors.AsType[*sideband64k.ProtocolError](err); !ok {
-		t.Fatalf("got err %v, want ProtocolError", err)
-	}
-}
--- a/format/sideband64k/decoder_malformed_pktline_test.go
+++ /dev/null
@@ -1,32 +1,0 @@
-package sideband64k_test
-
-import (
-	"errors"
-	"strings"
-	"testing"
-
-	"codeberg.org/lindenii/furgit/format/pktline"
-	"codeberg.org/lindenii/furgit/format/sideband64k"
-)
-
-func TestDecoderInvalid0003(t *testing.T) {
-	t.Parallel()
-
-	dec := sideband64k.NewDecoder(strings.NewReader("0003"), sideband64k.ReadOptions{})
-	_, err := dec.ReadFrame()
-
-	if _, ok := errors.AsType[*pktline.ProtocolError](err); !ok {
-		t.Fatalf("got err %v, want pktline.ProtocolError", err)
-	}
-}
-
-func TestDecoderRejectsOverMaximumLength(t *testing.T) {
-	t.Parallel()
-
-	dec := sideband64k.NewDecoder(strings.NewReader("fffe"), sideband64k.ReadOptions{})
-	_, err := dec.ReadFrame()
-
-	if _, ok := errors.AsType[*pktline.ProtocolError](err); !ok {
-		t.Fatalf("got err %v, want pktline.ProtocolError", err)
-	}
-}
--- a/format/sideband64k/decoder_partial_read_test.go
+++ /dev/null
@@ -1,32 +1,0 @@
-package sideband64k_test
-
-import (
-	"testing"
-
-	"codeberg.org/lindenii/furgit/format/sideband64k"
-)
-
-func TestDecoderHandlesPartialReads(t *testing.T) {
-	t.Parallel()
-
-	r := &byteReader{data: []byte("0007\x02ok0000")}
-	dec := sideband64k.NewDecoder(r, sideband64k.ReadOptions{})
-
-	f, err := dec.ReadFrame()
-	if err != nil {
-		t.Fatalf("ReadFrame #1: %v", err)
-	}
-
-	if f.Type != sideband64k.FrameProgress || string(f.Payload) != "ok" {
-		t.Fatalf("frame #1 = %#v", f)
-	}
-
-	f, err = dec.ReadFrame()
-	if err != nil {
-		t.Fatalf("ReadFrame #2: %v", err)
-	}
-
-	if f.Type != sideband64k.FrameFlush {
-		t.Fatalf("frame #2 = %#v", f)
-	}
-}
--- a/format/sideband64k/decoder_peek_test.go
+++ /dev/null
@@ -1,34 +1,0 @@
-package sideband64k_test
-
-import (
-	"strings"
-	"testing"
-
-	"codeberg.org/lindenii/furgit/format/sideband64k"
-)
-
-func TestDecoderPeek(t *testing.T) {
-	t.Parallel()
-
-	dec := sideband64k.NewDecoder(strings.NewReader("0006\x01x0000"), sideband64k.ReadOptions{})
-
-	f, err := dec.PeekFrame()
-	if err != nil {
-		t.Fatalf("PeekFrame: %v", err)
-	}
-
-	if f.Type != sideband64k.FrameData || string(f.Payload) != "x" {
-		t.Fatalf("peek frame = %#v", f)
-	}
-
-	f.Payload[0] = 'y'
-
-	f, err = dec.ReadFrame()
-	if err != nil {
-		t.Fatalf("ReadFrame: %v", err)
-	}
-
-	if f.Type != sideband64k.FrameData || string(f.Payload) != "x" {
-		t.Fatalf("read frame = %#v", f)
-	}
-}
--- a/format/sideband64k/decoder_resync_after_over_max_data_test.go
+++ /dev/null
@@ -1,51 +1,0 @@
-package sideband64k_test
-
-import (
-	"bufio"
-	"bytes"
-	"errors"
-	"testing"
-
-	"codeberg.org/lindenii/furgit/format/sideband64k"
-)
-
-func TestDecoderResyncAfterOverMaxData(t *testing.T) {
-	t.Parallel()
-
-	var b bytes.Buffer
-
-	bw := bufio.NewWriter(&b)
-	enc := sideband64k.NewEncoder(bw)
-
-	err := enc.WriteData([]byte("abcd"))
-	if err != nil {
-		t.Fatalf("WriteData #1: %v", err)
-	}
-
-	err = enc.WriteData([]byte("z"))
-	if err != nil {
-		t.Fatalf("WriteData #2: %v", err)
-	}
-
-	err = enc.FlushIO()
-	if err != nil {
-		t.Fatalf("FlushIO: %v", err)
-	}
-
-	dec := sideband64k.NewDecoder(bytes.NewReader(b.Bytes()), sideband64k.ReadOptions{})
-	dec.SetMaxData(1)
-
-	_, err = dec.ReadFrame()
-	if !errors.Is(err, sideband64k.ErrTooLarge) {
-		t.Fatalf("got err %v, want ErrTooLarge", err)
-	}
-
-	f, err := dec.ReadFrame()
-	if err != nil {
-		t.Fatalf("ReadFrame #2: %v", err)
-	}
-
-	if f.Type != sideband64k.FrameData || string(f.Payload) != "z" {
-		t.Fatalf("got frame %#v, want data z", f)
-	}
-}
--- a/format/sideband64k/decoder_resync_after_over_wire_max_test.go
+++ /dev/null
@@ -1,37 +1,0 @@
-package sideband64k_test
-
-import (
-	"bytes"
-	"errors"
-	"testing"
-
-	"codeberg.org/lindenii/furgit/format/pktline"
-	"codeberg.org/lindenii/furgit/format/sideband64k"
-)
-
-func TestDecoderResyncAfterOverWireMax(t *testing.T) {
-	t.Parallel()
-
-	var b bytes.Buffer
-
-	_, _ = b.WriteString("ffff")
-	_, _ = b.Write(bytes.Repeat([]byte{'a'}, 65531))
-	_, _ = b.WriteString("0006\x01z")
-
-	dec := sideband64k.NewDecoder(bytes.NewReader(b.Bytes()), sideband64k.ReadOptions{})
-
-	_, err := dec.ReadFrame()
-
-	if _, ok := errors.AsType[*pktline.ProtocolError](err); !ok {
-		t.Fatalf("got err %v, want pktline.ProtocolError", err)
-	}
-
-	f, err := dec.ReadFrame()
-	if err != nil {
-		t.Fatalf("ReadFrame #2: %v", err)
-	}
-
-	if f.Type != sideband64k.FrameData || string(f.Payload) != "z" {
-		t.Fatalf("got frame %#v, want data z", f)
-	}
-}
--- a/format/sideband64k/decoder_unexpected_eof_test.go
+++ /dev/null
@@ -1,21 +1,0 @@
-package sideband64k_test
-
-import (
-	"errors"
-	"io"
-	"strings"
-	"testing"
-
-	"codeberg.org/lindenii/furgit/format/sideband64k"
-)
-
-func TestDecoderUnexpectedEOF(t *testing.T) {
-	t.Parallel()
-
-	dec := sideband64k.NewDecoder(strings.NewReader("0006\x01"), sideband64k.ReadOptions{})
-
-	_, err := dec.ReadFrame()
-	if !errors.Is(err, io.ErrUnexpectedEOF) {
-		t.Fatalf("got err %v, want io.ErrUnexpectedEOF", err)
-	}
-}
--- a/format/sideband64k/doc.go
+++ /dev/null
@@ -1,2 +1,0 @@
-// Package sideband64k implements Git side-band-64k multiplexing over pkt-line.
-package sideband64k
--- a/format/sideband64k/encoder.go
+++ /dev/null
@@ -1,98 +1,0 @@
-package sideband64k
-
-import (
-	"fmt"
-
-	"codeberg.org/lindenii/furgit/format/pktline"
-)
-
-// Encoder writes side-band-64k frames to a flush-capable output transport.
-//
-// It writes exactly one frame per method call and does not auto-chunk data.
-type Encoder struct {
-	enc     *pktline.Encoder
-	maxData int
-}
-
-// NewEncoder creates an encoder over w.
-func NewEncoder(w pktline.WriteFlusher) *Encoder {
-	return &Encoder{
-		enc:     pktline.NewEncoder(w),
-		maxData: DataMax,
-	}
-}
-
-// SetMaxData sets the maximum payload size accepted by WriteBand.
-//
-// Non-positive n resets to DataMax.
-func (e *Encoder) SetMaxData(n int) {
-	if n <= 0 {
-		e.maxData = DataMax
-
-		return
-	}
-
-	e.maxData = n
-}
-
-// WriteBand writes one side-band-64k data frame for the given band.
-func (e *Encoder) WriteBand(band Band, p []byte) error {
-	if !validBand(band) {
-		return fmt.Errorf("%w: %d", ErrInvalidBand, band)
-	}
-
-	maxData := e.effectiveMaxData()
-	if len(p) > maxData {
-		return fmt.Errorf("%w: %d > %d", ErrTooLarge, len(p), maxData)
-	}
-
-	framed := make([]byte, len(p)+1)
-	framed[0] = byte(band)
-	copy(framed[1:], p)
-
-	return e.enc.WriteData(framed)
-}
-
-// WriteData writes one band-1 data frame.
-func (e *Encoder) WriteData(p []byte) error {
-	return e.WriteBand(BandData, p)
-}
-
-// WriteProgress writes one band-2 progress frame.
-func (e *Encoder) WriteProgress(p []byte) error {
-	return e.WriteBand(BandProgress, p)
-}
-
-// WriteError writes one band-3 error frame.
-func (e *Encoder) WriteError(p []byte) error {
-	return e.WriteBand(BandError, p)
-}
-
-// WriteFlush writes control frame 0000 (flush-pkt).
-func (e *Encoder) WriteFlush() error {
-	return e.enc.WriteFlush()
-}
-
-// WriteDelim writes control frame 0001 (delim-pkt).
-func (e *Encoder) WriteDelim() error {
-	return e.enc.WriteDelim()
-}
-
-// WriteResponseEnd writes control frame 0002 (response-end-pkt).
-func (e *Encoder) WriteResponseEnd() error {
-	return e.enc.WriteResponseEnd()
-}
-
-// FlushIO flushes buffered output in the underlying transport.
-func (e *Encoder) FlushIO() error {
-	return e.enc.FlushIO()
-}
-
-// WriteFlushAndFlushIO writes a flush-pkt (0000) then flushes transport I/O.
-func (e *Encoder) WriteFlushAndFlushIO() error {
-	return e.enc.WriteFlushAndFlushIO()
-}
-
-func (e *Encoder) effectiveMaxData() int {
-	return effectiveMaxData(e.maxData)
-}
--- a/format/sideband64k/encoder_buffered_flush_behavior_test.go
+++ /dev/null
@@ -1,59 +1,0 @@
-package sideband64k_test
-
-import (
-	"bufio"
-	"bytes"
-	"testing"
-
-	"codeberg.org/lindenii/furgit/format/sideband64k"
-)
-
-func TestEncoderBufferedFlushBehavior(t *testing.T) {
-	t.Parallel()
-
-	var out bytes.Buffer
-
-	bw := bufio.NewWriter(&out)
-	enc := sideband64k.NewEncoder(bw)
-
-	err := enc.WriteData([]byte("hello"))
-	if err != nil {
-		t.Fatalf("WriteData: %v", err)
-	}
-
-	err = enc.WriteFlush()
-	if err != nil {
-		t.Fatalf("WriteFlush: %v", err)
-	}
-
-	if out.Len() != 0 {
-		t.Fatalf("WriteFlush should not flush I/O, got %q", out.String())
-	}
-
-	err = enc.FlushIO()
-	if err != nil {
-		t.Fatalf("FlushIO: %v", err)
-	}
-
-	if got, want := out.String(), "000a\x01hello0000"; got != want {
-		t.Fatalf("got %q, want %q", got, want)
-	}
-
-	out.Reset()
-	bw = bufio.NewWriter(&out)
-	enc = sideband64k.NewEncoder(bw)
-
-	err = enc.WriteData([]byte("yo"))
-	if err != nil {
-		t.Fatalf("WriteData: %v", err)
-	}
-
-	err = enc.WriteFlushAndFlushIO()
-	if err != nil {
-		t.Fatalf("WriteFlushAndFlushIO: %v", err)
-	}
-
-	if got, want := out.String(), "0007\x01yo0000"; got != want {
-		t.Fatalf("got %q, want %q", got, want)
-	}
-}
--- a/format/sideband64k/encoder_partial_write_test.go
+++ /dev/null
@@ -1,46 +1,0 @@
-package sideband64k_test
-
-import (
-	"errors"
-	"io"
-	"testing"
-
-	"codeberg.org/lindenii/furgit/format/sideband64k"
-)
-
-func TestEncoderHandlesPartialWrites(t *testing.T) {
-	t.Parallel()
-
-	dst := &limitWriter{maxPerWrite: 2}
-	enc := sideband64k.NewEncoder(dst)
-
-	err := enc.WriteProgress([]byte("abc"))
-	if err != nil {
-		t.Fatalf("WriteProgress: %v", err)
-	}
-
-	err = enc.WriteFlushAndFlushIO()
-	if err != nil {
-		t.Fatalf("WriteFlushAndFlushIO: %v", err)
-	}
-
-	if got, want := dst.buf.String(), "0008\x02abc0000"; got != want {
-		t.Fatalf("got %q, want %q", got, want)
-	}
-
-	if dst.flushes != 1 {
-		t.Fatalf("flushes=%d, want 1", dst.flushes)
-	}
-}
-
-func TestEncoderReturnsShortWrite(t *testing.T) {
-	t.Parallel()
-
-	dst := &limitWriter{shortWrite: true}
-	enc := sideband64k.NewEncoder(dst)
-
-	err := enc.WriteData([]byte("x"))
-	if !errors.Is(err, io.ErrShortWrite) {
-		t.Fatalf("got err %v, want io.ErrShortWrite", err)
-	}
-}
--- a/format/sideband64k/encoder_set_max_data_cannot_exceed_wire_limit_test.go
+++ /dev/null
@@ -1,23 +1,0 @@
-package sideband64k_test
-
-import (
-	"bytes"
-	"errors"
-	"testing"
-
-	"codeberg.org/lindenii/furgit/format/sideband64k"
-)
-
-func TestEncoderSetMaxDataCannotExceedWireLimit(t *testing.T) {
-	t.Parallel()
-
-	var dst limitWriter
-
-	enc := sideband64k.NewEncoder(&dst)
-	enc.SetMaxData(sideband64k.DataMax + 100)
-
-	err := enc.WriteData(bytes.Repeat([]byte{'x'}, sideband64k.DataMax+1))
-	if !errors.Is(err, sideband64k.ErrTooLarge) {
-		t.Fatalf("got err %v, want ErrTooLarge", err)
-	}
-}
--- a/format/sideband64k/encoder_writes_frames_test.go
+++ /dev/null
@@ -1,58 +1,0 @@
-package sideband64k_test
-
-import (
-	"bufio"
-	"bytes"
-	"testing"
-
-	"codeberg.org/lindenii/furgit/format/sideband64k"
-)
-
-func TestEncoderWritesFrames(t *testing.T) {
-	t.Parallel()
-
-	var b bytes.Buffer
-
-	bw := bufio.NewWriter(&b)
-	enc := sideband64k.NewEncoder(bw)
-
-	err := enc.WriteData([]byte("hi"))
-	if err != nil {
-		t.Fatalf("WriteData: %v", err)
-	}
-
-	err = enc.WriteProgress([]byte("ok"))
-	if err != nil {
-		t.Fatalf("WriteProgress: %v", err)
-	}
-
-	err = enc.WriteError([]byte("no"))
-	if err != nil {
-		t.Fatalf("WriteError: %v", err)
-	}
-
-	err = enc.WriteFlush()
-	if err != nil {
-		t.Fatalf("WriteFlush: %v", err)
-	}
-
-	err = enc.WriteDelim()
-	if err != nil {
-		t.Fatalf("WriteDelim: %v", err)
-	}
-
-	err = enc.WriteResponseEnd()
-	if err != nil {
-		t.Fatalf("WriteResponseEnd: %v", err)
-	}
-
-	err = enc.FlushIO()
-	if err != nil {
-		t.Fatalf("FlushIO: %v", err)
-	}
-
-	want := "0007\x01hi0007\x02ok0007\x03no000000010002"
-	if got := b.String(); got != want {
-		t.Fatalf("got %q, want %q", got, want)
-	}
-}
--- a/format/sideband64k/errors.go
+++ /dev/null
@@ -1,27 +1,0 @@
-package sideband64k
-
-import "errors"
-
-var (
-	// ErrTooLarge indicates a payload exceeds configured sideband data limits.
-	ErrTooLarge = errors.New("sideband64k: payload too large")
-	// ErrInvalidBand indicates a data frame has an invalid sideband designator.
-	ErrInvalidBand = errors.New("sideband64k: invalid band designator")
-)
-
-// ProtocolError reports invalid side-band-64k framing.
-type ProtocolError struct {
-	Reason string
-}
-
-func (e *ProtocolError) Error() string {
-	if e == nil {
-		return "<nil>"
-	}
-
-	if e.Reason == "" {
-		return "sideband64k: protocol error"
-	}
-
-	return "sideband64k: protocol error: " + e.Reason
-}
--- a/format/sideband64k/frame.go
+++ /dev/null
@@ -1,12 +1,0 @@
-package sideband64k
-
-// Frame is one decoded side-band-64k frame.
-//
-// For FrameData, FrameProgress, and FrameError, Payload holds frame bytes and
-// may be empty.
-//
-// For control frames, Payload is nil.
-type Frame struct {
-	Type    FrameType
-	Payload []byte
-}
--- a/format/sideband64k/frame_type.go
+++ /dev/null
@@ -1,19 +1,0 @@
-package sideband64k
-
-// FrameType identifies the kind of decoded sideband frame.
-type FrameType uint8
-
-const (
-	// FrameData carries primary payload bytes from band 1.
-	FrameData FrameType = iota
-	// FrameProgress carries progress bytes from band 2.
-	FrameProgress
-	// FrameError carries fatal error bytes from band 3.
-	FrameError
-	// FrameFlush is pkt-line control frame 0000.
-	FrameFlush
-	// FrameDelim is pkt-line control frame 0001.
-	FrameDelim
-	// FrameResponseEnd is pkt-line control frame 0002.
-	FrameResponseEnd
-)
--- a/format/sideband64k/helpers_test.go
+++ /dev/null
@@ -1,46 +1,0 @@
-package sideband64k_test
-
-import (
-	"bytes"
-	"io"
-)
-
-type limitWriter struct {
-	buf         bytes.Buffer
-	maxPerWrite int
-	flushes     int
-	shortWrite  bool
-}
-
-func (w *limitWriter) Write(p []byte) (int, error) {
-	if w.shortWrite {
-		return 0, nil
-	}
-
-	if w.maxPerWrite > 0 && len(p) > w.maxPerWrite {
-		p = p[:w.maxPerWrite]
-	}
-
-	return w.buf.Write(p)
-}
-
-func (w *limitWriter) Flush() error {
-	w.flushes++
-
-	return nil
-}
-
-type byteReader struct {
-	data []byte
-}
-
-func (r *byteReader) Read(p []byte) (int, error) {
-	if len(r.data) == 0 {
-		return 0, io.EOF
-	}
-
-	p[0] = r.data[0]
-	r.data = r.data[1:]
-
-	return 1, nil
-}
--- /dev/null
+++ b/protocol/pktline/append.go
@@ -1,0 +1,39 @@
+package pktline
+
+import "fmt"
+
+// AppendData appends one data frame to dst.
+//
+// Empty payload is encoded as 0004.
+func AppendData(dst, payload []byte) ([]byte, error) {
+	if len(payload) > LargePacketDataMax {
+		return dst, fmt.Errorf("%w: %d > %d", ErrTooLarge, len(payload), LargePacketDataMax)
+	}
+
+	var hdr [4]byte
+
+	err := EncodeLengthHeader(&hdr, len(payload)+4)
+	if err != nil {
+		return dst, err
+	}
+
+	dst = append(dst, hdr[:]...)
+	dst = append(dst, payload...)
+
+	return dst, nil
+}
+
+// AppendFlushPkt appends control frame 0000 (flush-pkt).
+func AppendFlushPkt(dst []byte) []byte {
+	return append(dst, '0', '0', '0', '0')
+}
+
+// AppendDelimPkt appends control frame 0001 (delim-pkt).
+func AppendDelimPkt(dst []byte) []byte {
+	return append(dst, '0', '0', '0', '1')
+}
+
+// AppendResponseEndPkt appends control frame 0002 (response-end-pkt).
+func AppendResponseEndPkt(dst []byte) []byte {
+	return append(dst, '0', '0', '0', '2')
+}
--- /dev/null
+++ b/protocol/pktline/append_data_preserves_dst_on_error_test.go
@@ -1,0 +1,25 @@
+package pktline_test
+
+import (
+	"bytes"
+	"errors"
+	"testing"
+
+	"codeberg.org/lindenii/furgit/protocol/pktline"
+)
+
+func TestAppendDataPreservesDstOnError(t *testing.T) {
+	t.Parallel()
+
+	orig := []byte("seed")
+	dst := append([]byte(nil), orig...)
+
+	out, err := pktline.AppendData(dst, bytes.Repeat([]byte{'x'}, pktline.LargePacketDataMax+1))
+	if !errors.Is(err, pktline.ErrTooLarge) {
+		t.Fatalf("got err %v, want ErrTooLarge", err)
+	}
+
+	if !bytes.Equal(out, orig) {
+		t.Fatalf("got %q, want %q", string(out), string(orig))
+	}
+}
--- /dev/null
+++ b/protocol/pktline/append_helpers_test.go
@@ -1,0 +1,24 @@
+package pktline_test
+
+import (
+	"testing"
+
+	"codeberg.org/lindenii/furgit/protocol/pktline"
+)
+
+func TestAppendHelpers(t *testing.T) {
+	t.Parallel()
+
+	out, err := pktline.AppendData(nil, []byte("ok"))
+	if err != nil {
+		t.Fatalf("AppendData: %v", err)
+	}
+
+	out = pktline.AppendFlushPkt(out)
+	out = pktline.AppendDelimPkt(out)
+	out = pktline.AppendResponseEndPkt(out)
+
+	if got, want := string(out), "0006ok000000010002"; got != want {
+		t.Fatalf("got %q, want %q", got, want)
+	}
+}
--- /dev/null
+++ b/protocol/pktline/chunk_writer.go
@@ -1,0 +1,65 @@
+package pktline
+
+import "io"
+
+// ChunkWriter packetizes arbitrary stream bytes into data pkt-lines.
+// It never writes control packets automatically.
+type ChunkWriter struct {
+	enc *Encoder
+}
+
+// NewChunkWriter creates a chunking adapter over enc.
+func NewChunkWriter(enc *Encoder) *ChunkWriter {
+	return &ChunkWriter{enc: enc}
+}
+
+// Write splits p into data frames not larger than enc's maxData.
+//
+// It implements io.Writer.
+func (cw *ChunkWriter) Write(p []byte) (int, error) {
+	total := 0
+	maxData := cw.enc.effectiveMaxData()
+
+	for len(p) > 0 {
+		n := min(len(p), maxData)
+
+		err := cw.enc.WriteData(p[:n])
+		if err != nil {
+			return total, err
+		}
+
+		total += n
+		p = p[n:]
+	}
+
+	return total, nil
+}
+
+// ReadFrom reads from r and writes pkt-line data frames to the encoder.
+//
+// It implements io.ReaderFrom.
+func (cw *ChunkWriter) ReadFrom(r io.Reader) (int64, error) {
+	buf := make([]byte, cw.enc.effectiveMaxData())
+
+	var total int64
+
+	for {
+		n, err := r.Read(buf)
+		if n > 0 {
+			werr := cw.enc.WriteData(buf[:n])
+			if werr != nil {
+				return total, werr
+			}
+
+			total += int64(n)
+		}
+
+		if err != nil {
+			if err == io.EOF {
+				return total, nil
+			}
+
+			return total, err
+		}
+	}
+}
--- /dev/null
+++ b/protocol/pktline/chunk_writer_write_and_read_from_test.go
@@ -1,0 +1,60 @@
+package pktline_test
+
+import (
+	"bufio"
+	"bytes"
+	"strings"
+	"testing"
+
+	"codeberg.org/lindenii/furgit/protocol/pktline"
+)
+
+func TestChunkWriterWriteAndReadFrom(t *testing.T) {
+	t.Parallel()
+
+	var out bytes.Buffer
+
+	bw := bufio.NewWriter(&out)
+
+	enc := pktline.NewEncoder(bw)
+	enc.SetMaxData(3)
+	cw := pktline.NewChunkWriter(enc)
+
+	n, err := cw.Write([]byte("abcdefg"))
+	if err != nil {
+		t.Fatalf("Write: %v", err)
+	}
+
+	if n != 7 {
+		t.Fatalf("Write n=%d, want 7", n)
+	}
+
+	err = enc.FlushIO()
+	if err != nil {
+		t.Fatalf("FlushIO: %v", err)
+	}
+
+	if got, want := out.String(), "0007abc0007def0005g"; got != want {
+		t.Fatalf("got %q, want %q", got, want)
+	}
+
+	out.Reset()
+
+	rn, err := cw.ReadFrom(strings.NewReader("wxyz"))
+	if err != nil {
+		t.Fatalf("ReadFrom: %v", err)
+	}
+
+	if rn != 4 {
+		t.Fatalf("ReadFrom n=%d, want 4", rn)
+	}
+
+	err = enc.FlushIO()
+	if err != nil {
+		t.Fatalf("FlushIO: %v", err)
+	}
+
+	if got, want := out.String(), "0007wxy0005z"; got != want {
+		t.Fatalf("got %q, want %q", got, want)
+	}
+}
--- /dev/null
+++ b/protocol/pktline/constants.go
@@ -1,0 +1,12 @@
+package pktline
+
+const (
+	// DefaultPacketMax is a conservative packet size commonly used by
+	// line-oriented protocol messages.
+	DefaultPacketMax = 1000
+	// LargePacketMax is the maximum on-wire packet size including the
+	// 4-byte hexadecimal length header.
+	LargePacketMax = 65520
+	// LargePacketDataMax is the maximum payload size in one packet.
+	LargePacketDataMax = LargePacketMax - 4
+)
--- /dev/null
+++ b/protocol/pktline/decoder.go
@@ -1,0 +1,187 @@
+package pktline
+
+import (
+	"errors"
+	"fmt"
+	"io"
+)
+
+// ReadOptions controls decoding behavior.
+type ReadOptions struct {
+	// ChompLF removes one trailing '\n' from PacketData payloads.
+	ChompLF bool
+}
+
+// Decoder reads pkt-line frames from an io.Reader.
+//
+// It is advisable to supply a buffered reader.
+//
+// It preserves frame boundaries and supports one-frame lookahead via PeekFrame.
+type Decoder struct {
+	r       io.Reader
+	maxData int
+	opts    ReadOptions
+
+	peeked  bool
+	peek    Frame
+	peekErr error
+}
+
+// NewDecoder creates a decoder over r.
+func NewDecoder(r io.Reader, opts ReadOptions) *Decoder {
+	return &Decoder{
+		r:       r,
+		maxData: LargePacketDataMax,
+		opts:    opts,
+	}
+}
+
+// SetMaxData sets maximum payload size accepted for one data packet.
+//
+// Non-positive n resets to LargePacketDataMax.
+func (d *Decoder) SetMaxData(n int) {
+	if n <= 0 {
+		d.maxData = LargePacketDataMax
+
+		return
+	}
+
+	d.maxData = n
+}
+
+func cloneFrame(f Frame) Frame {
+	if f.Type != PacketData {
+		return Frame{Type: f.Type}
+	}
+
+	out := Frame{Type: f.Type}
+	if f.Payload != nil {
+		out.Payload = append([]byte(nil), f.Payload...)
+	}
+
+	return out
+}
+
+// ReadFrame reads one frame.
+//
+// 0000 is a PacketFlush
+// 0001 is a PacketDelim
+// 0002 is a PacketResponseEnd
+// 0004 is a PacketData with empty payload
+//
+// 0003 and malformed headers return *ProtocolError.
+func (d *Decoder) ReadFrame() (Frame, error) {
+	if d.peeked {
+		d.peeked = false
+
+		return cloneFrame(d.peek), d.peekErr
+	}
+
+	return d.readFrame()
+}
+
+// PeekFrame returns the next frame without consuming it.
+//
+// A subsequent ReadFrame returns the same frame.
+func (d *Decoder) PeekFrame() (Frame, error) {
+	if !d.peeked {
+		d.peek, d.peekErr = d.readFrame()
+		d.peeked = true
+	}
+
+	return cloneFrame(d.peek), d.peekErr
+}
+
+func (d *Decoder) readFrame() (Frame, error) {
+	var hdr [4]byte
+
+	_, err := io.ReadFull(d.r, hdr[:])
+	if err != nil {
+		if errors.Is(err, io.EOF) {
+			return Frame{}, io.EOF
+		}
+
+		if errors.Is(err, io.ErrUnexpectedEOF) {
+			return Frame{}, io.ErrUnexpectedEOF
+		}
+
+		return Frame{}, err
+	}
+
+	n, err := ParseLengthHeader(hdr)
+	if err != nil {
+		return Frame{}, &ProtocolError{Header: hdr, Reason: err.Error()}
+	}
+
+	switch n {
+	case 0:
+		return Frame{Type: PacketFlush}, nil
+	case 1:
+		return Frame{Type: PacketDelim}, nil
+	case 2:
+		return Frame{Type: PacketResponseEnd}, nil
+	case 3:
+		return Frame{}, &ProtocolError{Header: hdr, Reason: "invalid pkt-line length 3"}
+	}
+
+	if n < 4 {
+		return Frame{}, &ProtocolError{Header: hdr, Reason: fmt.Sprintf("invalid pkt-line length %d", n)}
+	}
+
+	if n > LargePacketMax {
+		perr := &ProtocolError{Header: hdr, Reason: fmt.Sprintf("pkt-line length %d exceeds max %d", n, LargePacketMax)}
+
+		err := d.discardPayload(n - 4)
+		if err != nil {
+			return Frame{}, errors.Join(perr, err)
+		}
+
+		return Frame{}, perr
+	}
+
+	payloadLen := n - 4
+	if payloadLen > d.maxData {
+		serr := fmt.Errorf("%w: %d > %d", ErrTooLarge, payloadLen, d.maxData)
+
+		err := d.discardPayload(payloadLen)
+		if err != nil {
+			return Frame{}, errors.Join(serr, err)
+		}
+
+		return Frame{}, serr
+	}
+
+	payload := make([]byte, payloadLen)
+
+	_, err = io.ReadFull(d.r, payload)
+	if err != nil {
+		if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
+			return Frame{}, io.ErrUnexpectedEOF
+		}
+
+		return Frame{}, err
+	}
+
+	if d.opts.ChompLF && len(payload) > 0 && payload[len(payload)-1] == '\n' {
+		payload = payload[:len(payload)-1]
+	}
+
+	return Frame{Type: PacketData, Payload: payload}, nil
+}
+
+func (d *Decoder) discardPayload(n int) error {
+	if n <= 0 {
+		return nil
+	}
+
+	_, err := io.CopyN(io.Discard, d.r, int64(n))
+	if err == nil {
+		return nil
+	}
+
+	if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
+		return io.ErrUnexpectedEOF
+	}
+
+	return err
+}
--- /dev/null
+++ b/protocol/pktline/decoder_data_control_and_0004_test.go
@@ -1,0 +1,60 @@
+package pktline_test
+
+import (
+	"strings"
+	"testing"
+
+	"codeberg.org/lindenii/furgit/protocol/pktline"
+)
+
+func TestDecoderDataControlAnd0004(t *testing.T) {
+	t.Parallel()
+
+	input := "0006a\n0004000100020000"
+	dec := pktline.NewDecoder(strings.NewReader(input), pktline.ReadOptions{ChompLF: true})
+
+	f, err := dec.ReadFrame()
+	if err != nil {
+		t.Fatalf("ReadFrame #1: %v", err)
+	}
+
+	if f.Type != pktline.PacketData || string(f.Payload) != "a" {
+		t.Fatalf("frame #1 = %#v", f)
+	}
+
+	f, err = dec.ReadFrame()
+	if err != nil {
+		t.Fatalf("ReadFrame #2: %v", err)
+	}
+
+	if f.Type != pktline.PacketData || len(f.Payload) != 0 {
+		t.Fatalf("frame #2 = %#v, want empty data", f)
+	}
+
+	f, err = dec.ReadFrame()
+	if err != nil {
+		t.Fatalf("ReadFrame #3: %v", err)
+	}
+
+	if f.Type != pktline.PacketDelim {
+		t.Fatalf("frame #3 type = %v, want PacketDelim", f.Type)
+	}
+
+	f, err = dec.ReadFrame()
+	if err != nil {
+		t.Fatalf("ReadFrame #4: %v", err)
+	}
+
+	if f.Type != pktline.PacketResponseEnd {
+		t.Fatalf("frame #4 type = %v, want PacketResponseEnd", f.Type)
+	}
+
+	f, err = dec.ReadFrame()
+	if err != nil {
+		t.Fatalf("ReadFrame #5: %v", err)
+	}
+
+	if f.Type != pktline.PacketFlush {
+		t.Fatalf("frame #5 type = %v, want PacketFlush", f.Type)
+	}
+}
--- /dev/null
+++ b/protocol/pktline/decoder_invalid_0003_test.go
@@ -1,0 +1,20 @@
+package pktline_test
+
+import (
+	"errors"
+	"strings"
+	"testing"
+
+	"codeberg.org/lindenii/furgit/protocol/pktline"
+)
+
+func TestDecoderInvalid0003(t *testing.T) {
+	t.Parallel()
+
+	dec := pktline.NewDecoder(strings.NewReader("0003"), pktline.ReadOptions{})
+	_, err := dec.ReadFrame()
+
+	if _, ok := errors.AsType[*pktline.ProtocolError](err); !ok {
+		t.Fatalf("got err %v, want ProtocolError", err)
+	}
+}
--- /dev/null
+++ b/protocol/pktline/decoder_peek_test.go
@@ -1,0 +1,32 @@
+package pktline_test
+
+import (
+	"strings"
+	"testing"
+
+	"codeberg.org/lindenii/furgit/protocol/pktline"
+)
+
+func TestDecoderPeek(t *testing.T) {
+	t.Parallel()
+
+	dec := pktline.NewDecoder(strings.NewReader("0005x0000"), pktline.ReadOptions{})
+
+	f, err := dec.PeekFrame()
+	if err != nil {
+		t.Fatalf("PeekFrame: %v", err)
+	}
+
+	if f.Type != pktline.PacketData || string(f.Payload) != "x" {
+		t.Fatalf("peek frame = %#v", f)
+	}
+
+	f, err = dec.ReadFrame()
+	if err != nil {
+		t.Fatalf("ReadFrame: %v", err)
+	}
+
+	if f.Type != pktline.PacketData || string(f.Payload) != "x" {
+		t.Fatalf("read frame = %#v", f)
+	}
+}
--- /dev/null
+++ b/protocol/pktline/decoder_rejects_over_maximum_length_test.go
@@ -1,0 +1,22 @@
+package pktline_test
+
+import (
+	"errors"
+	"strings"
+	"testing"
+
+	"codeberg.org/lindenii/furgit/protocol/pktline"
+)
+
+func TestDecoderRejectsOverMaximumLength(t *testing.T) {
+	t.Parallel()
+
+	dec := pktline.NewDecoder(strings.NewReader("fffe"), pktline.ReadOptions{})
+	dec.SetMaxData(70000)
+
+	_, err := dec.ReadFrame()
+
+	if _, ok := errors.AsType[*pktline.ProtocolError](err); !ok {
+		t.Fatalf("got err %v, want ProtocolError", err)
+	}
+}
--- /dev/null
+++ b/protocol/pktline/decoder_resync_after_over_max_data_test.go
@@ -1,0 +1,51 @@
+package pktline_test
+
+import (
+	"bufio"
+	"bytes"
+	"errors"
+	"testing"
+
+	"codeberg.org/lindenii/furgit/protocol/pktline"
+)
+
+func TestDecoderResyncAfterOverMaxData(t *testing.T) {
+	t.Parallel()
+
+	var b bytes.Buffer
+
+	bw := bufio.NewWriter(&b)
+	enc := pktline.NewEncoder(bw)
+
+	err := enc.WriteData([]byte("abcd"))
+	if err != nil {
+		t.Fatalf("WriteData #1: %v", err)
+	}
+
+	err = enc.WriteData([]byte("z"))
+	if err != nil {
+		t.Fatalf("WriteData #2: %v", err)
+	}
+
+	err = enc.FlushIO()
+	if err != nil {
+		t.Fatalf("FlushIO: %v", err)
+	}
+
+	dec := pktline.NewDecoder(bytes.NewReader(b.Bytes()), pktline.ReadOptions{})
+	dec.SetMaxData(1)
+
+	_, err = dec.ReadFrame()
+	if !errors.Is(err, pktline.ErrTooLarge) {
+		t.Fatalf("got err %v, want ErrTooLarge", err)
+	}
+
+	f, err := dec.ReadFrame()
+	if err != nil {
+		t.Fatalf("ReadFrame #2: %v", err)
+	}
+
+	if f.Type != pktline.PacketData || string(f.Payload) != "z" {
+		t.Fatalf("got frame %#v, want data z", f)
+	}
+}
--- /dev/null
+++ b/protocol/pktline/decoder_resync_after_over_wire_max_test.go
@@ -1,0 +1,37 @@
+package pktline_test
+
+import (
+	"bytes"
+	"errors"
+	"testing"
+
+	"codeberg.org/lindenii/furgit/protocol/pktline"
+)
+
+func TestDecoderResyncAfterOverWireMax(t *testing.T) {
+	t.Parallel()
+
+	var b bytes.Buffer
+
+	_, _ = b.WriteString("ffff")
+	_, _ = b.Write(bytes.Repeat([]byte{'a'}, 65531))
+	_, _ = b.WriteString("0005z")
+
+	dec := pktline.NewDecoder(bytes.NewReader(b.Bytes()), pktline.ReadOptions{})
+	dec.SetMaxData(70000)
+
+	_, err := dec.ReadFrame()
+
+	if _, ok := errors.AsType[*pktline.ProtocolError](err); !ok {
+		t.Fatalf("got err %v, want ProtocolError", err)
+	}
+
+	f, err := dec.ReadFrame()
+	if err != nil {
+		t.Fatalf("ReadFrame #2: %v", err)
+	}
+
+	if f.Type != pktline.PacketData || string(f.Payload) != "z" {
+		t.Fatalf("got frame %#v, want data z", f)
+	}
+}
--- /dev/null
+++ b/protocol/pktline/decoder_unexpected_eof_test.go
@@ -1,0 +1,21 @@
+package pktline_test
+
+import (
+	"errors"
+	"io"
+	"strings"
+	"testing"
+
+	"codeberg.org/lindenii/furgit/protocol/pktline"
+)
+
+func TestDecoderUnexpectedEOF(t *testing.T) {
+	t.Parallel()
+
+	dec := pktline.NewDecoder(strings.NewReader("0006a"), pktline.ReadOptions{})
+
+	_, err := dec.ReadFrame()
+	if !errors.Is(err, io.ErrUnexpectedEOF) {
+		t.Fatalf("got err %v, want io.ErrUnexpectedEOF", err)
+	}
+}
--- /dev/null
+++ b/protocol/pktline/doc.go
@@ -1,0 +1,2 @@
+// Package pktline implements the pkt-line format specified in gitprotocol-common(5).
+package pktline
--- /dev/null
+++ b/protocol/pktline/encode_length_header_test.go
@@ -1,0 +1,28 @@
+package pktline_test
+
+import (
+	"errors"
+	"testing"
+
+	"codeberg.org/lindenii/furgit/protocol/pktline"
+)
+
+func TestEncodeLengthHeader(t *testing.T) {
+	t.Parallel()
+
+	var hdr [4]byte
+
+	err := pktline.EncodeLengthHeader(&hdr, 4)
+	if err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+
+	if got := string(hdr[:]); got != "0004" {
+		t.Fatalf("got %q, want %q", got, "0004")
+	}
+
+	err = pktline.EncodeLengthHeader(&hdr, pktline.LargePacketMax+1)
+	if !errors.Is(err, pktline.ErrInvalidLength) {
+		t.Fatalf("got err %v, want ErrInvalidLength", err)
+	}
+}
--- /dev/null
+++ b/protocol/pktline/encoder.go
@@ -1,0 +1,145 @@
+package pktline
+
+import (
+	"fmt"
+	"io"
+)
+
+// WriteFlusher is the output transport contract required by Encoder.
+//
+// Write emits framed bytes and Flush pushes buffered transport state.
+type WriteFlusher interface {
+	io.Writer
+	Flush() error
+}
+
+// Encoder writes pkt-line frames to a flush-capable output transport.
+//
+// It writes exactly one frame per method call and does not auto-chunk data.
+type Encoder struct {
+	w       WriteFlusher
+	maxData int
+}
+
+// NewEncoder creates an encoder over w.
+func NewEncoder(w WriteFlusher) *Encoder {
+	return &Encoder{
+		w:       w,
+		maxData: LargePacketDataMax,
+	}
+}
+
+// SetMaxData sets the maximum payload size accepted by WriteData.
+//
+// Non-positive n resets to LargePacketDataMax.
+func (e *Encoder) SetMaxData(n int) {
+	if n <= 0 {
+		e.maxData = LargePacketDataMax
+
+		return
+	}
+
+	e.maxData = n
+}
+
+func writeAll(w io.Writer, b []byte) error {
+	for len(b) > 0 {
+		n, err := w.Write(b)
+		if err != nil {
+			return err
+		}
+
+		if n <= 0 {
+			return io.ErrShortWrite
+		}
+
+		b = b[n:]
+	}
+
+	return nil
+}
+
+// WriteData writes one data frame.
+//
+// Empty payload is encoded as 0004.
+func (e *Encoder) WriteData(p []byte) error {
+	maxData := e.effectiveMaxData()
+	if len(p) > maxData {
+		return fmt.Errorf("%w: %d > %d", ErrTooLarge, len(p), maxData)
+	}
+
+	var hdr [4]byte
+
+	err := EncodeLengthHeader(&hdr, len(p)+4)
+	if err != nil {
+		return err
+	}
+
+	err = writeAll(e.w, hdr[:])
+	if err != nil {
+		return err
+	}
+
+	return writeAll(e.w, p)
+}
+
+// WriteString writes one data frame containing s and returns len(s) on success.
+func (e *Encoder) WriteString(s string) (int, error) {
+	err := e.WriteData([]byte(s))
+	if err != nil {
+		return 0, err
+	}
+
+	return len(s), nil
+}
+
+// WriteFlush writes control frame 0000 (flush-pkt).
+func (e *Encoder) WriteFlush() error {
+	return e.writeControl(0)
+}
+
+// WriteDelim writes control frame 0001 (delim-pkt).
+func (e *Encoder) WriteDelim() error {
+	return e.writeControl(1)
+}
+
+// WriteResponseEnd writes control frame 0002 (response-end-pkt).
+func (e *Encoder) WriteResponseEnd() error {
+	return e.writeControl(2)
+}
+
+// FlushIO flushes buffered output in the underlying transport.
+//
+// FlushIO does not emit any pkt-line control frame.
+func (e *Encoder) FlushIO() error {
+	return e.w.Flush()
+}
+
+// WriteFlushAndFlushIO writes a flush-pkt (0000) then flushes transport I/O.
+func (e *Encoder) WriteFlushAndFlushIO() error {
+	err := e.WriteFlush()
+	if err != nil {
+		return err
+	}
+
+	return e.FlushIO()
+}
+
+func (e *Encoder) writeControl(n int) error {
+	var hdr [4]byte
+
+	err := EncodeLengthHeader(&hdr, n)
+	if err != nil {
+		return err
+	}
+
+	return writeAll(e.w, hdr[:])
+}
+
+func (e *Encoder) effectiveMaxData() int {
+	if e.maxData <= 0 || e.maxData > LargePacketDataMax {
+		return LargePacketDataMax
+	}
+
+	return e.maxData
+}
--- /dev/null
+++ b/protocol/pktline/encoder_buffered_flush_and_f_flush_test.go
@@ -1,0 +1,50 @@
+package pktline_test
+
+import (
+	"bufio"
+	"bytes"
+	"testing"
+
+	"codeberg.org/lindenii/furgit/protocol/pktline"
+)
+
+func TestEncoderBufferedFlushAndFFlush(t *testing.T) {
+	t.Parallel()
+
+	var out bytes.Buffer
+
+	bw := bufio.NewWriter(&out)
+	enc := pktline.NewEncoder(bw)
+
+	err := enc.WriteData([]byte("x"))
+	if err != nil {
+		t.Fatalf("WriteData: %v", err)
+	}
+
+	if out.Len() != 0 {
+		t.Fatalf("unexpected immediate output: %q", out.String())
+	}
+
+	err = enc.FlushIO()
+	if err != nil {
+		t.Fatalf("FlushIO: %v", err)
+	}
+
+	if out.String() != "0005x" {
+		t.Fatalf("got %q, want %q", out.String(), "0005x")
+	}
+
+	out.Reset()
+	bw = bufio.NewWriter(&out)
+
+	enc = pktline.NewEncoder(bw)
+
+	err = enc.WriteFlushAndFlushIO()
+	if err != nil {
+		t.Fatalf("WriteFlushAndFlushIO: %v", err)
+	}
+
+	if out.String() != "0000" {
+		t.Fatalf("got %q, want %q", out.String(), "0000")
+	}
+}
--- /dev/null
+++ b/protocol/pktline/encoder_buffered_flush_behavior_test.go
@@ -1,0 +1,86 @@
+package pktline_test
+
+import (
+	"bufio"
+	"bytes"
+	"testing"
+
+	"codeberg.org/lindenii/furgit/protocol/pktline"
+)
+
+func TestEncoderBufferedFlushBehavior(t *testing.T) {
+	t.Parallel()
+
+	var out bytes.Buffer
+
+	bw := bufio.NewWriter(&out)
+	enc := pktline.NewEncoder(bw)
+
+	err := enc.WriteData([]byte("hello"))
+	if err != nil {
+		t.Fatalf("WriteData: %v", err)
+	}
+
+	err = enc.WriteFlush()
+	if err != nil {
+		t.Fatalf("WriteFlush: %v", err)
+	}
+
+	if out.Len() != 0 {
+		t.Fatalf("WriteFlush should not flush I/O, got %q", out.String())
+	}
+
+	err = enc.FlushIO()
+	if err != nil {
+		t.Fatalf("FlushIO: %v", err)
+	}
+
+	if got, want := out.String(), "0009hello0000"; got != want {
+		t.Fatalf("got %q, want %q", got, want)
+	}
+
+	out.Reset()
+	bw = bufio.NewWriter(&out)
+	enc = pktline.NewEncoder(bw)
+
+	err = enc.WriteData([]byte("ok"))
+	if err != nil {
+		t.Fatalf("WriteData: %v", err)
+	}
+
+	err = enc.WriteFlush()
+	if err != nil {
+		t.Fatalf("WriteFlush: %v", err)
+	}
+
+	if out.Len() != 0 {
+		t.Fatalf("WriteFlush should not flush I/O, got %q", out.String())
+	}
+
+	err = enc.FlushIO()
+	if err != nil {
+		t.Fatalf("FlushIO: %v", err)
+	}
+
+	if got, want := out.String(), "0006ok0000"; got != want {
+		t.Fatalf("got %q, want %q", got, want)
+	}
+
+	out.Reset()
+	bw = bufio.NewWriter(&out)
+	enc = pktline.NewEncoder(bw)
+
+	err = enc.WriteData([]byte("yo"))
+	if err != nil {
+		t.Fatalf("WriteData: %v", err)
+	}
+
+	err = enc.WriteFlushAndFlushIO()
+	if err != nil {
+		t.Fatalf("WriteFlushAndFlushIO: %v", err)
+	}
+
+	if got, want := out.String(), "0006yo0000"; got != want {
+		t.Fatalf("got %q, want %q", got, want)
+	}
+}
--- /dev/null
+++ b/protocol/pktline/encoder_set_max_data_cannot_exceed_wire_limit_test.go
@@ -1,0 +1,26 @@
+package pktline_test
+
+import (
+	"bufio"
+	"bytes"
+	"errors"
+	"testing"
+
+	"codeberg.org/lindenii/furgit/protocol/pktline"
+)
+
+func TestEncoderSetMaxDataCannotExceedWireLimit(t *testing.T) {
+	t.Parallel()
+
+	var out bytes.Buffer
+
+	bw := bufio.NewWriter(&out)
+
+	enc := pktline.NewEncoder(bw)
+	enc.SetMaxData(pktline.LargePacketDataMax + 100)
+
+	err := enc.WriteData(bytes.Repeat([]byte{'x'}, pktline.LargePacketDataMax+1))
+	if !errors.Is(err, pktline.ErrTooLarge) {
+		t.Fatalf("got err %v, want ErrTooLarge", err)
+	}
+}
--- /dev/null
+++ b/protocol/pktline/encoder_writes_frames_test.go
@@ -1,0 +1,51 @@
+package pktline_test
+
+import (
+	"bufio"
+	"bytes"
+	"testing"
+
+	"codeberg.org/lindenii/furgit/protocol/pktline"
+)
+
+func TestEncoderWritesFrames(t *testing.T) {
+	t.Parallel()
+
+	var b bytes.Buffer
+
+	bw := bufio.NewWriter(&b)
+
+	enc := pktline.NewEncoder(bw)
+
+	err := enc.WriteData([]byte("hi"))
+	if err != nil {
+		t.Fatalf("WriteData: %v", err)
+	}
+
+	err = enc.WriteFlush()
+	if err != nil {
+		t.Fatalf("WriteFlush: %v", err)
+	}
+
+	err = enc.WriteDelim()
+	if err != nil {
+		t.Fatalf("WriteDelim: %v", err)
+	}
+
+	err = enc.WriteResponseEnd()
+	if err != nil {
+		t.Fatalf("WriteResponseEnd: %v", err)
+	}
+
+	err = enc.FlushIO()
+	if err != nil {
+		t.Fatalf("FlushIO: %v", err)
+	}
+
+	got := b.String()
+
+	want := "0006hi000000010002"
+	if got != want {
+		t.Fatalf("got %q, want %q", got, want)
+	}
+}
--- /dev/null
+++ b/protocol/pktline/errors.go
@@ -1,0 +1,31 @@
+package pktline
+
+import "errors"
+
+var (
+	// ErrInvalidLength indicates a malformed 4-byte hexadecimal length header.
+	ErrInvalidLength = errors.New("pktline: invalid length header")
+	// ErrTooLarge indicates a payload exceeds configured packet data limits.
+	ErrTooLarge = errors.New("pktline: payload too large")
+)
+
+// ProtocolError reports invalid pkt-line framing.
+//
+// It is returned for protocol violations such as invalid control values
+// (for example 0003) or non-hex length headers.
+type ProtocolError struct {
+	Header [4]byte
+	Reason string
+}
+
+func (e *ProtocolError) Error() string {
+	if e == nil {
+		return "<nil>"
+	}
+
+	if e.Reason == "" {
+		return "pktline: protocol error"
+	}
+
+	return "pktline: protocol error: " + e.Reason
+}
--- /dev/null
+++ b/protocol/pktline/frame.go
@@ -1,0 +1,10 @@
+package pktline
+
+// Frame is one decoded pkt-line frame.
+//
+// For PacketData, Payload holds frame bytes (possibly empty for 0004).
+// For control frames, Payload is nil.
+type Frame struct {
+	Type    PacketType
+	Payload []byte
+}
--- /dev/null
+++ b/protocol/pktline/header.go
@@ -1,0 +1,57 @@
+package pktline
+
+import "fmt"
+
+func hexval(b byte) int {
+	switch {
+	case b >= '0' && b <= '9':
+		return int(b - '0')
+	case b >= 'a' && b <= 'f':
+		return int(b-'a') + 10
+	case b >= 'A' && b <= 'F':
+		return int(b-'A') + 10
+	default:
+		return -1
+	}
+}
+
+// ParseLengthHeader parses a 4-byte hexadecimal pkt-line length header.
+//
+// The returned value is the full on-wire packet size, including the 4-byte
+// header. Semantic interpretation (data/control/error) is done by Decoder.
+//
+// The 4-byte header is only an actual length when above or equal to 4.
+// Otherwise, it indicates some control packet.
+func ParseLengthHeader(h [4]byte) (int, error) {
+	a := hexval(h[0])
+	b := hexval(h[1])
+	c := hexval(h[2])
+	d := hexval(h[3])
+
+	if a < 0 || b < 0 || c < 0 || d < 0 {
+		return 0, fmt.Errorf("%w: %q", ErrInvalidLength, string(h[:]))
+	}
+
+	return (a << 12) | (b << 8) | (c << 4) | d, nil
+}
+
+// EncodeLengthHeader encodes n as a 4-byte hexadecimal pkt-line header.
+//
+// n is the full on-wire packet size including the 4-byte header.
+//
+// The 4-byte header is only an actual length when above or equal to 4.
+// Otherwise, it indicates some control packet.
+func EncodeLengthHeader(dst *[4]byte, n int) error {
+	if n < 0 || n > LargePacketMax {
+		return fmt.Errorf("%w: %d", ErrInvalidLength, n)
+	}
+
+	const hex = "0123456789abcdef"
+
+	dst[0] = hex[(n>>12)&0xf]
+	dst[1] = hex[(n>>8)&0xf]
+	dst[2] = hex[(n>>4)&0xf]
+	dst[3] = hex[n&0xf]
+
+	return nil
+}
--- /dev/null
+++ b/protocol/pktline/parse_length_header_test.go
@@ -1,0 +1,26 @@
+package pktline_test
+
+import (
+	"errors"
+	"testing"
+
+	"codeberg.org/lindenii/furgit/protocol/pktline"
+)
+
+func TestParseLengthHeader(t *testing.T) {
+	t.Parallel()
+
+	n, err := pktline.ParseLengthHeader([4]byte{'0', '0', '0', '4'})
+	if err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+
+	if n != 4 {
+		t.Fatalf("got %d, want 4", n)
+	}
+
+	_, err = pktline.ParseLengthHeader([4]byte{'0', '0', '0', 'x'})
+	if !errors.Is(err, pktline.ErrInvalidLength) {
+		t.Fatalf("got err %v, want ErrInvalidLength", err)
+	}
+}
--- /dev/null
+++ b/protocol/pktline/type.go
@@ -1,0 +1,15 @@
+package pktline
+
+// PacketType identifies the kind of pkt-line frame.
+type PacketType uint8
+
+const (
+	// PacketData is a regular data frame whose payload is application-defined.
+	PacketData PacketType = iota
+	// PacketFlush is control frame 0000 and marks end of a message.
+	PacketFlush
+	// PacketDelim is control frame 0001 and separates sections in protocol v2.
+	PacketDelim
+	// PacketResponseEnd is control frame 0002 and marks response end on stateless v2 transports.
+	PacketResponseEnd
+)
--- /dev/null
+++ b/protocol/sideband64k/append.go
@@ -1,0 +1,40 @@
+package sideband64k
+
+import (
+	"fmt"
+
+	"codeberg.org/lindenii/furgit/protocol/pktline"
+)
+
+// AppendBand appends one side-band-64k data frame to dst.
+func AppendBand(dst []byte, band Band, payload []byte) ([]byte, error) {
+	if !validBand(band) {
+		return dst, fmt.Errorf("%w: %d", ErrInvalidBand, band)
+	}
+
+	maxData := effectiveMaxData(DataMax)
+	if len(payload) > maxData {
+		return dst, fmt.Errorf("%w: %d > %d", ErrTooLarge, len(payload), maxData)
+	}
+
+	framed := make([]byte, len(payload)+1)
+	framed[0] = byte(band)
+	copy(framed[1:], payload)
+
+	return pktline.AppendData(dst, framed)
+}
+
+// AppendData appends one band-1 data frame to dst.
+func AppendData(dst, payload []byte) ([]byte, error) {
+	return AppendBand(dst, BandData, payload)
+}
+
+// AppendProgress appends one band-2 progress frame to dst.
+func AppendProgress(dst, payload []byte) ([]byte, error) {
+	return AppendBand(dst, BandProgress, payload)
+}
+
+// AppendError appends one band-3 error frame to dst.
+func AppendError(dst, payload []byte) ([]byte, error) {
+	return AppendBand(dst, BandError, payload)
+}
--- /dev/null
+++ b/protocol/sideband64k/append_helpers_test.go
@@ -1,0 +1,30 @@
+package sideband64k_test
+
+import (
+	"testing"
+
+	"codeberg.org/lindenii/furgit/protocol/sideband64k"
+)
+
+func TestAppendHelpers(t *testing.T) {
+	t.Parallel()
+
+	out, err := sideband64k.AppendData(nil, []byte("a"))
+	if err != nil {
+		t.Fatalf("AppendData: %v", err)
+	}
+
+	out, err = sideband64k.AppendProgress(out, []byte("b"))
+	if err != nil {
+		t.Fatalf("AppendProgress: %v", err)
+	}
+
+	out, err = sideband64k.AppendError(out, []byte("c"))
+	if err != nil {
+		t.Fatalf("AppendError: %v", err)
+	}
+
+	if got, want := string(out), "0006\x01a0006\x02b0006\x03c"; got != want {
+		t.Fatalf("got %q, want %q", got, want)
+	}
+}
--- /dev/null
+++ b/protocol/sideband64k/append_preserves_dst_on_error_test.go
@@ -1,0 +1,34 @@
+package sideband64k_test
+
+import (
+	"bytes"
+	"errors"
+	"testing"
+
+	"codeberg.org/lindenii/furgit/protocol/sideband64k"
+)
+
+func TestAppendBandPreservesDstOnError(t *testing.T) {
+	t.Parallel()
+
+	orig := []byte("seed")
+	dst := append([]byte(nil), orig...)
+
+	out, err := sideband64k.AppendBand(dst, 4, []byte("x"))
+	if !errors.Is(err, sideband64k.ErrInvalidBand) {
+		t.Fatalf("got err %v, want ErrInvalidBand", err)
+	}
+
+	if !bytes.Equal(out, orig) {
+		t.Fatalf("got %q, want %q", string(out), string(orig))
+	}
+
+	out, err = sideband64k.AppendData(dst, bytes.Repeat([]byte{'x'}, sideband64k.DataMax+1))
+	if !errors.Is(err, sideband64k.ErrTooLarge) {
+		t.Fatalf("got err %v, want ErrTooLarge", err)
+	}
+
+	if !bytes.Equal(out, orig) {
+		t.Fatalf("got %q, want %q", string(out), string(orig))
+	}
+}
--- /dev/null
+++ b/protocol/sideband64k/band.go
@@ -1,0 +1,13 @@
+package sideband64k
+
+// Band identifies the sideband stream within a pkt-line data frame.
+type Band uint8
+
+const (
+	// BandData carries primary payload bytes.
+	BandData Band = 1
+	// BandProgress carries progress or informational messages.
+	BandProgress Band = 2
+	// BandError carries fatal error messages.
+	BandError Band = 3
+)
--- /dev/null
+++ b/protocol/sideband64k/chunk_writer.go
@@ -1,0 +1,64 @@
+package sideband64k
+
+import "io"
+
+// ChunkWriter packetizes arbitrary stream bytes into side-band-64k data frames
+// for one fixed band.
+//
+// It never writes control packets automatically.
+type ChunkWriter struct {
+	enc  *Encoder
+	band Band
+}
+
+// NewChunkWriter creates a chunking adapter over enc for one band.
+func NewChunkWriter(enc *Encoder, band Band) *ChunkWriter {
+	return &ChunkWriter{enc: enc, band: band}
+}
+
+// Write splits p into sideband frames not larger than enc's maxData.
+func (cw *ChunkWriter) Write(p []byte) (int, error) {
+	total := 0
+	maxData := cw.enc.effectiveMaxData()
+
+	for len(p) > 0 {
+		n := min(len(p), maxData)
+
+		err := cw.enc.WriteBand(cw.band, p[:n])
+		if err != nil {
+			return total, err
+		}
+
+		total += n
+		p = p[n:]
+	}
+
+	return total, nil
+}
+
+// ReadFrom reads from r and writes sideband frames to the encoder.
+func (cw *ChunkWriter) ReadFrom(r io.Reader) (int64, error) {
+	buf := make([]byte, cw.enc.effectiveMaxData())
+
+	var total int64
+
+	for {
+		n, err := r.Read(buf)
+		if n > 0 {
+			werr := cw.enc.WriteBand(cw.band, buf[:n])
+			if werr != nil {
+				return total, werr
+			}
+
+			total += int64(n)
+		}
+
+		if err != nil {
+			if err == io.EOF {
+				return total, nil
+			}
+
+			return total, err
+		}
+	}
+}
--- /dev/null
+++ b/protocol/sideband64k/chunk_writer_write_and_read_from_test.go
@@ -1,0 +1,60 @@
+package sideband64k_test
+
+import (
+	"bufio"
+	"bytes"
+	"strings"
+	"testing"
+
+	"codeberg.org/lindenii/furgit/protocol/sideband64k"
+)
+
+func TestChunkWriterWriteAndReadFrom(t *testing.T) {
+	t.Parallel()
+
+	var out bytes.Buffer
+
+	bw := bufio.NewWriter(&out)
+	enc := sideband64k.NewEncoder(bw)
+	enc.SetMaxData(3)
+
+	cw := sideband64k.NewChunkWriter(enc, sideband64k.BandProgress)
+
+	n, err := cw.Write([]byte("abcdefg"))
+	if err != nil {
+		t.Fatalf("Write: %v", err)
+	}
+
+	if n != 7 {
+		t.Fatalf("Write n=%d, want 7", n)
+	}
+
+	err = enc.FlushIO()
+	if err != nil {
+		t.Fatalf("FlushIO: %v", err)
+	}
+
+	if got, want := out.String(), "0008\x02abc0008\x02def0006\x02g"; got != want {
+		t.Fatalf("got %q, want %q", got, want)
+	}
+
+	out.Reset()
+
+	rn, err := cw.ReadFrom(strings.NewReader("wxyz"))
+	if err != nil {
+		t.Fatalf("ReadFrom: %v", err)
+	}
+
+	if rn != 4 {
+		t.Fatalf("ReadFrom n=%d, want 4", rn)
+	}
+
+	err = enc.FlushIO()
+	if err != nil {
+		t.Fatalf("FlushIO: %v", err)
+	}
+
+	if got, want := out.String(), "0008\x02wxy0006\x02z"; got != want {
+		t.Fatalf("got %q, want %q", got, want)
+	}
+}
--- /dev/null
+++ b/protocol/sideband64k/constants.go
@@ -1,0 +1,10 @@
+package sideband64k
+
+import "codeberg.org/lindenii/furgit/protocol/pktline"
+
+const (
+	// PacketMax is the maximum on-wire pkt-line size used by side-band-64k.
+	PacketMax = pktline.LargePacketMax
+	// DataMax is the maximum sideband payload size excluding the 1-byte band designator.
+	DataMax = pktline.LargePacketDataMax - 1
+)
--- /dev/null
+++ b/protocol/sideband64k/decoder.go
@@ -1,0 +1,158 @@
+package sideband64k
+
+import (
+	"fmt"
+	"io"
+
+	"codeberg.org/lindenii/furgit/protocol/pktline"
+)
+
+// ReadOptions controls sideband decoding behavior.
+type ReadOptions struct {
+	// ChompLF removes one trailing '\n' from FrameData payloads only.
+	ChompLF bool
+}
+
+// Decoder reads side-band-64k frames from an io.Reader.
+//
+// It preserves frame boundaries and supports one-frame lookahead via
+// PeekFrame.
+type Decoder struct {
+	dec     *pktline.Decoder
+	maxData int
+	opts    ReadOptions
+
+	peeked  bool
+	peek    Frame
+	peekErr error
+}
+
+// NewDecoder creates a decoder over r.
+func NewDecoder(r io.Reader, opts ReadOptions) *Decoder {
+	d := &Decoder{
+		dec:     pktline.NewDecoder(r, pktline.ReadOptions{}),
+		maxData: DataMax,
+		opts:    opts,
+	}
+	d.dec.SetMaxData(pktline.LargePacketDataMax)
+
+	return d
+}
+
+// SetMaxData sets maximum payload size accepted for one sideband data packet.
+//
+// Non-positive n resets to DataMax.
+func (d *Decoder) SetMaxData(n int) {
+	if n <= 0 {
+		d.maxData = DataMax
+
+		return
+	}
+
+	d.maxData = n
+}
+
+// ReadFrame reads one frame.
+func (d *Decoder) ReadFrame() (Frame, error) {
+	if d.peeked {
+		d.peeked = false
+
+		return cloneFrame(d.peek), d.peekErr
+	}
+
+	return d.readFrame()
+}
+
+// PeekFrame returns the next frame without consuming it.
+func (d *Decoder) PeekFrame() (Frame, error) {
+	if !d.peeked {
+		d.peek, d.peekErr = d.readFrame()
+		d.peeked = true
+	}
+
+	return cloneFrame(d.peek), d.peekErr
+}
+
+func (d *Decoder) readFrame() (Frame, error) {
+	f, err := d.dec.ReadFrame()
+	if err != nil {
+		return Frame{}, err
+	}
+
+	switch f.Type {
+	case pktline.PacketFlush:
+		return Frame{Type: FrameFlush}, nil
+	case pktline.PacketDelim:
+		return Frame{Type: FrameDelim}, nil
+	case pktline.PacketResponseEnd:
+		return Frame{Type: FrameResponseEnd}, nil
+	case pktline.PacketData:
+		if len(f.Payload) == 0 {
+			return Frame{}, &ProtocolError{Reason: "missing sideband designator"}
+		}
+
+		payload := f.Payload[1:]
+		if len(payload) > d.effectiveMaxData() {
+			return Frame{}, fmt.Errorf("%w: %d > %d", ErrTooLarge, len(payload), d.effectiveMaxData())
+		}
+
+		band := Band(f.Payload[0])
+		if !validBand(band) {
+			return Frame{}, &ProtocolError{Reason: fmt.Sprintf("%v: %d", ErrInvalidBand, band)}
+		}
+
+		payload = append([]byte(nil), payload...)
+		if d.opts.ChompLF && band == BandData && len(payload) > 0 && payload[len(payload)-1] == '\n' {
+			payload = payload[:len(payload)-1]
+		}
+
+		return Frame{
+			Type:    frameTypeForBand(band),
+			Payload: payload,
+		}, nil
+	default:
+		return Frame{}, &ProtocolError{Reason: "unknown pkt-line frame type"}
+	}
+}
+
+func (d *Decoder) effectiveMaxData() int {
+	return effectiveMaxData(d.maxData)
+}
+
+func cloneFrame(f Frame) Frame {
+	if f.Type == FrameFlush || f.Type == FrameDelim || f.Type == FrameResponseEnd {
+		return Frame{Type: f.Type}
+	}
+
+	out := Frame{Type: f.Type}
+	if f.Payload != nil {
+		out.Payload = append([]byte(nil), f.Payload...)
+	}
+
+	return out
+}
+
+func validBand(band Band) bool {
+	return band == BandData || band == BandProgress || band == BandError
+}
+
+func frameTypeForBand(band Band) FrameType {
+	switch band {
+	case BandData:
+		return FrameData
+	case BandProgress:
+		return FrameProgress
+	case BandError:
+		return FrameError
+	default:
+		panic("invalid sideband64k band")
+	}
+}
+
+func effectiveMaxData(n int) int {
+	if n <= 0 || n > DataMax {
+		return DataMax
+	}
+
+	return n
+}
--- /dev/null
+++ b/protocol/sideband64k/decoder_data_control_and_keepalive_test.go
@@ -1,0 +1,78 @@
+package sideband64k_test
+
+import (
+	"strings"
+	"testing"
+
+	"codeberg.org/lindenii/furgit/protocol/sideband64k"
+)
+
+func TestDecoderDataControlAndKeepalive(t *testing.T) {
+	t.Parallel()
+
+	input := "0007\x01a\n0005\x010007\x02p\n0007\x03e\n000100020000"
+	dec := sideband64k.NewDecoder(strings.NewReader(input), sideband64k.ReadOptions{ChompLF: true})
+
+	f, err := dec.ReadFrame()
+	if err != nil {
+		t.Fatalf("ReadFrame #1: %v", err)
+	}
+
+	if f.Type != sideband64k.FrameData || string(f.Payload) != "a" {
+		t.Fatalf("frame #1 = %#v", f)
+	}
+
+	f, err = dec.ReadFrame()
+	if err != nil {
+		t.Fatalf("ReadFrame #2: %v", err)
+	}
+
+	if f.Type != sideband64k.FrameData || len(f.Payload) != 0 {
+		t.Fatalf("frame #2 = %#v, want empty data", f)
+	}
+
+	f, err = dec.ReadFrame()
+	if err != nil {
+		t.Fatalf("ReadFrame #3: %v", err)
+	}
+
+	if f.Type != sideband64k.FrameProgress || string(f.Payload) != "p\n" {
+		t.Fatalf("frame #3 = %#v", f)
+	}
+
+	f, err = dec.ReadFrame()
+	if err != nil {
+		t.Fatalf("ReadFrame #4: %v", err)
+	}
+
+	if f.Type != sideband64k.FrameError || string(f.Payload) != "e\n" {
+		t.Fatalf("frame #4 = %#v", f)
+	}
+
+	f, err = dec.ReadFrame()
+	if err != nil {
+		t.Fatalf("ReadFrame #5: %v", err)
+	}
+
+	if f.Type != sideband64k.FrameDelim {
+		t.Fatalf("frame #5 type = %v, want FrameDelim", f.Type)
+	}
+
+	f, err = dec.ReadFrame()
+	if err != nil {
+		t.Fatalf("ReadFrame #6: %v", err)
+	}
+
+	if f.Type != sideband64k.FrameResponseEnd {
+		t.Fatalf("frame #6 type = %v, want FrameResponseEnd", f.Type)
+	}
+
+	f, err = dec.ReadFrame()
+	if err != nil {
+		t.Fatalf("ReadFrame #7: %v", err)
+	}
+
+	if f.Type != sideband64k.FrameFlush {
+		t.Fatalf("frame #7 type = %v, want FrameFlush", f.Type)
+	}
+}
--- /dev/null
+++ b/protocol/sideband64k/decoder_invalid_band_test.go
@@ -1,0 +1,20 @@
+package sideband64k_test
+
+import (
+	"errors"
+	"strings"
+	"testing"
+
+	"codeberg.org/lindenii/furgit/protocol/sideband64k"
+)
+
+func TestDecoderInvalidBand(t *testing.T) {
+	t.Parallel()
+
+	dec := sideband64k.NewDecoder(strings.NewReader("0005\x04"), sideband64k.ReadOptions{})
+	_, err := dec.ReadFrame()
+
+	if _, ok := errors.AsType[*sideband64k.ProtocolError](err); !ok {
+		t.Fatalf("got err %v, want ProtocolError", err)
+	}
+}
--- /dev/null
+++ b/protocol/sideband64k/decoder_invalid_empty_payload_test.go
@@ -1,0 +1,20 @@
+package sideband64k_test
+
+import (
+	"errors"
+	"strings"
+	"testing"
+
+	"codeberg.org/lindenii/furgit/protocol/sideband64k"
+)
+
+func TestDecoderInvalidEmptyPayload(t *testing.T) {
+	t.Parallel()
+
+	dec := sideband64k.NewDecoder(strings.NewReader("0004"), sideband64k.ReadOptions{})
+	_, err := dec.ReadFrame()
+
+	if _, ok := errors.AsType[*sideband64k.ProtocolError](err); !ok {
+		t.Fatalf("got err %v, want ProtocolError", err)
+	}
+}
--- /dev/null
+++ b/protocol/sideband64k/decoder_malformed_pktline_test.go
@@ -1,0 +1,32 @@
+package sideband64k_test
+
+import (
+	"errors"
+	"strings"
+	"testing"
+
+	"codeberg.org/lindenii/furgit/protocol/pktline"
+	"codeberg.org/lindenii/furgit/protocol/sideband64k"
+)
+
+func TestDecoderInvalid0003(t *testing.T) {
+	t.Parallel()
+
+	dec := sideband64k.NewDecoder(strings.NewReader("0003"), sideband64k.ReadOptions{})
+	_, err := dec.ReadFrame()
+
+	if _, ok := errors.AsType[*pktline.ProtocolError](err); !ok {
+		t.Fatalf("got err %v, want pktline.ProtocolError", err)
+	}
+}
+
+func TestDecoderRejectsOverMaximumLength(t *testing.T) {
+	t.Parallel()
+
+	dec := sideband64k.NewDecoder(strings.NewReader("fffe"), sideband64k.ReadOptions{})
+	_, err := dec.ReadFrame()
+
+	if _, ok := errors.AsType[*pktline.ProtocolError](err); !ok {
+		t.Fatalf("got err %v, want pktline.ProtocolError", err)
+	}
+}
--- /dev/null
+++ b/protocol/sideband64k/decoder_partial_read_test.go
@@ -1,0 +1,32 @@
+package sideband64k_test
+
+import (
+	"testing"
+
+	"codeberg.org/lindenii/furgit/protocol/sideband64k"
+)
+
+func TestDecoderHandlesPartialReads(t *testing.T) {
+	t.Parallel()
+
+	r := &byteReader{data: []byte("0007\x02ok0000")}
+	dec := sideband64k.NewDecoder(r, sideband64k.ReadOptions{})
+
+	f, err := dec.ReadFrame()
+	if err != nil {
+		t.Fatalf("ReadFrame #1: %v", err)
+	}
+
+	if f.Type != sideband64k.FrameProgress || string(f.Payload) != "ok" {
+		t.Fatalf("frame #1 = %#v", f)
+	}
+
+	f, err = dec.ReadFrame()
+	if err != nil {
+		t.Fatalf("ReadFrame #2: %v", err)
+	}
+
+	if f.Type != sideband64k.FrameFlush {
+		t.Fatalf("frame #2 = %#v", f)
+	}
+}
--- /dev/null
+++ b/protocol/sideband64k/decoder_peek_test.go
@@ -1,0 +1,34 @@
+package sideband64k_test
+
+import (
+	"strings"
+	"testing"
+
+	"codeberg.org/lindenii/furgit/protocol/sideband64k"
+)
+
+func TestDecoderPeek(t *testing.T) {
+	t.Parallel()
+
+	dec := sideband64k.NewDecoder(strings.NewReader("0006\x01x0000"), sideband64k.ReadOptions{})
+
+	f, err := dec.PeekFrame()
+	if err != nil {
+		t.Fatalf("PeekFrame: %v", err)
+	}
+
+	if f.Type != sideband64k.FrameData || string(f.Payload) != "x" {
+		t.Fatalf("peek frame = %#v", f)
+	}
+
+	f.Payload[0] = 'y'
+
+	f, err = dec.ReadFrame()
+	if err != nil {
+		t.Fatalf("ReadFrame: %v", err)
+	}
+
+	if f.Type != sideband64k.FrameData || string(f.Payload) != "x" {
+		t.Fatalf("read frame = %#v", f)
+	}
+}
--- /dev/null
+++ b/protocol/sideband64k/decoder_resync_after_over_max_data_test.go
@@ -1,0 +1,51 @@
+package sideband64k_test
+
+import (
+	"bufio"
+	"bytes"
+	"errors"
+	"testing"
+
+	"codeberg.org/lindenii/furgit/protocol/sideband64k"
+)
+
+func TestDecoderResyncAfterOverMaxData(t *testing.T) {
+	t.Parallel()
+
+	var b bytes.Buffer
+
+	bw := bufio.NewWriter(&b)
+	enc := sideband64k.NewEncoder(bw)
+
+	err := enc.WriteData([]byte("abcd"))
+	if err != nil {
+		t.Fatalf("WriteData #1: %v", err)
+	}
+
+	err = enc.WriteData([]byte("z"))
+	if err != nil {
+		t.Fatalf("WriteData #2: %v", err)
+	}
+
+	err = enc.FlushIO()
+	if err != nil {
+		t.Fatalf("FlushIO: %v", err)
+	}
+
+	dec := sideband64k.NewDecoder(bytes.NewReader(b.Bytes()), sideband64k.ReadOptions{})
+	dec.SetMaxData(1)
+
+	_, err = dec.ReadFrame()
+	if !errors.Is(err, sideband64k.ErrTooLarge) {
+		t.Fatalf("got err %v, want ErrTooLarge", err)
+	}
+
+	f, err := dec.ReadFrame()
+	if err != nil {
+		t.Fatalf("ReadFrame #2: %v", err)
+	}
+
+	if f.Type != sideband64k.FrameData || string(f.Payload) != "z" {
+		t.Fatalf("got frame %#v, want data z", f)
+	}
+}
--- /dev/null
+++ b/protocol/sideband64k/decoder_resync_after_over_wire_max_test.go
@@ -1,0 +1,37 @@
+package sideband64k_test
+
+import (
+	"bytes"
+	"errors"
+	"testing"
+
+	"codeberg.org/lindenii/furgit/protocol/pktline"
+	"codeberg.org/lindenii/furgit/protocol/sideband64k"
+)
+
+func TestDecoderResyncAfterOverWireMax(t *testing.T) {
+	t.Parallel()
+
+	var b bytes.Buffer
+
+	_, _ = b.WriteString("ffff")
+	_, _ = b.Write(bytes.Repeat([]byte{'a'}, 65531))
+	_, _ = b.WriteString("0006\x01z")
+
+	dec := sideband64k.NewDecoder(bytes.NewReader(b.Bytes()), sideband64k.ReadOptions{})
+
+	_, err := dec.ReadFrame()
+
+	if _, ok := errors.AsType[*pktline.ProtocolError](err); !ok {
+		t.Fatalf("got err %v, want pktline.ProtocolError", err)
+	}
+
+	f, err := dec.ReadFrame()
+	if err != nil {
+		t.Fatalf("ReadFrame #2: %v", err)
+	}
+
+	if f.Type != sideband64k.FrameData || string(f.Payload) != "z" {
+		t.Fatalf("got frame %#v, want data z", f)
+	}
+}
--- /dev/null
+++ b/protocol/sideband64k/decoder_unexpected_eof_test.go
@@ -1,0 +1,21 @@
+package sideband64k_test
+
+import (
+	"errors"
+	"io"
+	"strings"
+	"testing"
+
+	"codeberg.org/lindenii/furgit/protocol/sideband64k"
+)
+
+func TestDecoderUnexpectedEOF(t *testing.T) {
+	t.Parallel()
+
+	dec := sideband64k.NewDecoder(strings.NewReader("0006\x01"), sideband64k.ReadOptions{})
+
+	_, err := dec.ReadFrame()
+	if !errors.Is(err, io.ErrUnexpectedEOF) {
+		t.Fatalf("got err %v, want io.ErrUnexpectedEOF", err)
+	}
+}
--- /dev/null
+++ b/protocol/sideband64k/doc.go
@@ -1,0 +1,2 @@
+// Package sideband64k implements Git side-band-64k multiplexing over pkt-line.
+package sideband64k
--- /dev/null
+++ b/protocol/sideband64k/encoder.go
@@ -1,0 +1,98 @@
+package sideband64k
+
+import (
+	"fmt"
+
+	"codeberg.org/lindenii/furgit/protocol/pktline"
+)
+
+// Encoder writes side-band-64k frames to a flush-capable output transport.
+//
+// It writes exactly one frame per method call and does not auto-chunk data.
+type Encoder struct {
+	enc     *pktline.Encoder
+	maxData int
+}
+
+// NewEncoder creates an encoder over w.
+func NewEncoder(w pktline.WriteFlusher) *Encoder {
+	return &Encoder{
+		enc:     pktline.NewEncoder(w),
+		maxData: DataMax,
+	}
+}
+
+// SetMaxData sets the maximum payload size accepted by WriteBand.
+//
+// Non-positive n resets to DataMax.
+func (e *Encoder) SetMaxData(n int) {
+	if n <= 0 {
+		e.maxData = DataMax
+
+		return
+	}
+
+	e.maxData = n
+}
+
+// WriteBand writes one side-band-64k data frame for the given band.
+func (e *Encoder) WriteBand(band Band, p []byte) error {
+	if !validBand(band) {
+		return fmt.Errorf("%w: %d", ErrInvalidBand, band)
+	}
+
+	maxData := e.effectiveMaxData()
+	if len(p) > maxData {
+		return fmt.Errorf("%w: %d > %d", ErrTooLarge, len(p), maxData)
+	}
+
+	framed := make([]byte, len(p)+1)
+	framed[0] = byte(band)
+	copy(framed[1:], p)
+
+	return e.enc.WriteData(framed)
+}
+
+// WriteData writes one band-1 data frame.
+func (e *Encoder) WriteData(p []byte) error {
+	return e.WriteBand(BandData, p)
+}
+
+// WriteProgress writes one band-2 progress frame.
+func (e *Encoder) WriteProgress(p []byte) error {
+	return e.WriteBand(BandProgress, p)
+}
+
+// WriteError writes one band-3 error frame.
+func (e *Encoder) WriteError(p []byte) error {
+	return e.WriteBand(BandError, p)
+}
+
+// WriteFlush writes control frame 0000 (flush-pkt).
+func (e *Encoder) WriteFlush() error {
+	return e.enc.WriteFlush()
+}
+
+// WriteDelim writes control frame 0001 (delim-pkt).
+func (e *Encoder) WriteDelim() error {
+	return e.enc.WriteDelim()
+}
+
+// WriteResponseEnd writes control frame 0002 (response-end-pkt).
+func (e *Encoder) WriteResponseEnd() error {
+	return e.enc.WriteResponseEnd()
+}
+
+// FlushIO flushes buffered output in the underlying transport.
+func (e *Encoder) FlushIO() error {
+	return e.enc.FlushIO()
+}
+
+// WriteFlushAndFlushIO writes a flush-pkt (0000) then flushes transport I/O.
+func (e *Encoder) WriteFlushAndFlushIO() error {
+	return e.enc.WriteFlushAndFlushIO()
+}
+
+func (e *Encoder) effectiveMaxData() int {
+	return effectiveMaxData(e.maxData)
+}
--- /dev/null
+++ b/protocol/sideband64k/encoder_buffered_flush_behavior_test.go
@@ -1,0 +1,59 @@
+package sideband64k_test
+
+import (
+	"bufio"
+	"bytes"
+	"testing"
+
+	"codeberg.org/lindenii/furgit/protocol/sideband64k"
+)
+
+func TestEncoderBufferedFlushBehavior(t *testing.T) {
+	t.Parallel()
+
+	var out bytes.Buffer
+
+	bw := bufio.NewWriter(&out)
+	enc := sideband64k.NewEncoder(bw)
+
+	err := enc.WriteData([]byte("hello"))
+	if err != nil {
+		t.Fatalf("WriteData: %v", err)
+	}
+
+	err = enc.WriteFlush()
+	if err != nil {
+		t.Fatalf("WriteFlush: %v", err)
+	}
+
+	if out.Len() != 0 {
+		t.Fatalf("WriteFlush should not flush I/O, got %q", out.String())
+	}
+
+	err = enc.FlushIO()
+	if err != nil {
+		t.Fatalf("FlushIO: %v", err)
+	}
+
+	if got, want := out.String(), "000a\x01hello0000"; got != want {
+		t.Fatalf("got %q, want %q", got, want)
+	}
+
+	out.Reset()
+	bw = bufio.NewWriter(&out)
+	enc = sideband64k.NewEncoder(bw)
+
+	err = enc.WriteData([]byte("yo"))
+	if err != nil {
+		t.Fatalf("WriteData: %v", err)
+	}
+
+	err = enc.WriteFlushAndFlushIO()
+	if err != nil {
+		t.Fatalf("WriteFlushAndFlushIO: %v", err)
+	}
+
+	if got, want := out.String(), "0007\x01yo0000"; got != want {
+		t.Fatalf("got %q, want %q", got, want)
+	}
+}
--- /dev/null
+++ b/protocol/sideband64k/encoder_partial_write_test.go
@@ -1,0 +1,46 @@
+package sideband64k_test
+
+import (
+	"errors"
+	"io"
+	"testing"
+
+	"codeberg.org/lindenii/furgit/protocol/sideband64k"
+)
+
+func TestEncoderHandlesPartialWrites(t *testing.T) {
+	t.Parallel()
+
+	dst := &limitWriter{maxPerWrite: 2}
+	enc := sideband64k.NewEncoder(dst)
+
+	err := enc.WriteProgress([]byte("abc"))
+	if err != nil {
+		t.Fatalf("WriteProgress: %v", err)
+	}
+
+	err = enc.WriteFlushAndFlushIO()
+	if err != nil {
+		t.Fatalf("WriteFlushAndFlushIO: %v", err)
+	}
+
+	if got, want := dst.buf.String(), "0008\x02abc0000"; got != want {
+		t.Fatalf("got %q, want %q", got, want)
+	}
+
+	if dst.flushes != 1 {
+		t.Fatalf("flushes=%d, want 1", dst.flushes)
+	}
+}
+
+func TestEncoderReturnsShortWrite(t *testing.T) {
+	t.Parallel()
+
+	dst := &limitWriter{shortWrite: true}
+	enc := sideband64k.NewEncoder(dst)
+
+	err := enc.WriteData([]byte("x"))
+	if !errors.Is(err, io.ErrShortWrite) {
+		t.Fatalf("got err %v, want io.ErrShortWrite", err)
+	}
+}
--- /dev/null
+++ b/protocol/sideband64k/encoder_set_max_data_cannot_exceed_wire_limit_test.go
@@ -1,0 +1,23 @@
+package sideband64k_test
+
+import (
+	"bytes"
+	"errors"
+	"testing"
+
+	"codeberg.org/lindenii/furgit/protocol/sideband64k"
+)
+
+func TestEncoderSetMaxDataCannotExceedWireLimit(t *testing.T) {
+	t.Parallel()
+
+	var dst limitWriter
+
+	enc := sideband64k.NewEncoder(&dst)
+	enc.SetMaxData(sideband64k.DataMax + 100)
+
+	err := enc.WriteData(bytes.Repeat([]byte{'x'}, sideband64k.DataMax+1))
+	if !errors.Is(err, sideband64k.ErrTooLarge) {
+		t.Fatalf("got err %v, want ErrTooLarge", err)
+	}
+}
--- /dev/null
+++ b/protocol/sideband64k/encoder_writes_frames_test.go
@@ -1,0 +1,58 @@
+package sideband64k_test
+
+import (
+	"bufio"
+	"bytes"
+	"testing"
+
+	"codeberg.org/lindenii/furgit/protocol/sideband64k"
+)
+
+func TestEncoderWritesFrames(t *testing.T) {
+	t.Parallel()
+
+	var b bytes.Buffer
+
+	bw := bufio.NewWriter(&b)
+	enc := sideband64k.NewEncoder(bw)
+
+	err := enc.WriteData([]byte("hi"))
+	if err != nil {
+		t.Fatalf("WriteData: %v", err)
+	}
+
+	err = enc.WriteProgress([]byte("ok"))
+	if err != nil {
+		t.Fatalf("WriteProgress: %v", err)
+	}
+
+	err = enc.WriteError([]byte("no"))
+	if err != nil {
+		t.Fatalf("WriteError: %v", err)
+	}
+
+	err = enc.WriteFlush()
+	if err != nil {
+		t.Fatalf("WriteFlush: %v", err)
+	}
+
+	err = enc.WriteDelim()
+	if err != nil {
+		t.Fatalf("WriteDelim: %v", err)
+	}
+
+	err = enc.WriteResponseEnd()
+	if err != nil {
+		t.Fatalf("WriteResponseEnd: %v", err)
+	}
+
+	err = enc.FlushIO()
+	if err != nil {
+		t.Fatalf("FlushIO: %v", err)
+	}
+
+	want := "0007\x01hi0007\x02ok0007\x03no000000010002"
+	if got := b.String(); got != want {
+		t.Fatalf("got %q, want %q", got, want)
+	}
+}
--- /dev/null
+++ b/protocol/sideband64k/errors.go
@@ -1,0 +1,27 @@
+package sideband64k
+
+import "errors"
+
+var (
+	// ErrTooLarge indicates a payload exceeds configured sideband data limits.
+	ErrTooLarge = errors.New("sideband64k: payload too large")
+	// ErrInvalidBand indicates a data frame has an invalid sideband designator.
+	ErrInvalidBand = errors.New("sideband64k: invalid band designator")
+)
+
+// ProtocolError reports invalid side-band-64k framing.
+type ProtocolError struct {
+	Reason string
+}
+
+func (e *ProtocolError) Error() string {
+	if e == nil {
+		return "<nil>"
+	}
+
+	if e.Reason == "" {
+		return "sideband64k: protocol error"
+	}
+
+	return "sideband64k: protocol error: " + e.Reason
+}
--- /dev/null
+++ b/protocol/sideband64k/frame.go
@@ -1,0 +1,12 @@
+package sideband64k
+
+// Frame is one decoded side-band-64k frame.
+//
+// For FrameData, FrameProgress, and FrameError, Payload holds frame bytes and
+// may be empty.
+//
+// For control frames, Payload is nil.
+type Frame struct {
+	Type    FrameType
+	Payload []byte
+}
--- /dev/null
+++ b/protocol/sideband64k/frame_type.go
@@ -1,0 +1,19 @@
+package sideband64k
+
+// FrameType identifies the kind of decoded sideband frame.
+type FrameType uint8
+
+const (
+	// FrameData carries primary payload bytes from band 1.
+	FrameData FrameType = iota
+	// FrameProgress carries progress bytes from band 2.
+	FrameProgress
+	// FrameError carries fatal error bytes from band 3.
+	FrameError
+	// FrameFlush is pkt-line control frame 0000.
+	FrameFlush
+	// FrameDelim is pkt-line control frame 0001.
+	FrameDelim
+	// FrameResponseEnd is pkt-line control frame 0002.
+	FrameResponseEnd
+)
--- /dev/null
+++ b/protocol/sideband64k/helpers_test.go
@@ -1,0 +1,46 @@
+package sideband64k_test
+
+import (
+	"bytes"
+	"io"
+)
+
+type limitWriter struct {
+	buf         bytes.Buffer
+	maxPerWrite int
+	flushes     int
+	shortWrite  bool
+}
+
+func (w *limitWriter) Write(p []byte) (int, error) {
+	if w.shortWrite {
+		return 0, nil
+	}
+
+	if w.maxPerWrite > 0 && len(p) > w.maxPerWrite {
+		p = p[:w.maxPerWrite]
+	}
+
+	return w.buf.Write(p)
+}
+
+func (w *limitWriter) Flush() error {
+	w.flushes++
+
+	return nil
+}
+
+type byteReader struct {
+	data []byte
+}
+
+func (r *byteReader) Read(p []byte) (int, error) {
+	if len(r.data) == 0 {
+		return 0, io.EOF
+	}
+
+	p[0] = r.data[0]
+	r.data = r.data[1:]
+
+	return 1, nil
+}
--- a/protocol/v0v1/server/frame.go
+++ b/protocol/v0v1/server/frame.go
@@ -1,6 +1,6 @@
 package server
 
-import "codeberg.org/lindenii/furgit/format/pktline"
+import "codeberg.org/lindenii/furgit/protocol/pktline"
 
 // FrameType identifies one low-level v0/v1 server pkt-line frame type.
 type FrameType = pktline.PacketType
--- a/protocol/v0v1/server/receivepack/parse_test.go
+++ b/protocol/v0v1/server/receivepack/parse_test.go
@@ -5,7 +5,7 @@
 	"strings"
 	"testing"
 
-	"codeberg.org/lindenii/furgit/format/pktline"
+	"codeberg.org/lindenii/furgit/protocol/pktline"
 	"codeberg.org/lindenii/furgit/internal/testgit"
 	"codeberg.org/lindenii/furgit/objectid"
 	common "codeberg.org/lindenii/furgit/protocol/v0v1/server"
--- a/protocol/v0v1/server/receivepack/report_status.go
+++ b/protocol/v0v1/server/receivepack/report_status.go
@@ -3,7 +3,7 @@
 import (
 	"fmt"
 
-	"codeberg.org/lindenii/furgit/format/pktline"
+	"codeberg.org/lindenii/furgit/protocol/pktline"
 )
 
 // WriteReportStatus writes one classic report-status response.
--- a/protocol/v0v1/server/receivepack/report_status_test.go
+++ b/protocol/v0v1/server/receivepack/report_status_test.go
@@ -6,8 +6,8 @@
 	"strings"
 	"testing"
 
-	"codeberg.org/lindenii/furgit/format/pktline"
-	"codeberg.org/lindenii/furgit/format/sideband64k"
+	"codeberg.org/lindenii/furgit/protocol/pktline"
+	"codeberg.org/lindenii/furgit/protocol/sideband64k"
 	"codeberg.org/lindenii/furgit/internal/testgit"
 	"codeberg.org/lindenii/furgit/objectid"
 	common "codeberg.org/lindenii/furgit/protocol/v0v1/server"
--- a/protocol/v0v1/server/session.go
+++ b/protocol/v0v1/server/session.go
@@ -3,8 +3,8 @@
 import (
 	"io"
 
-	"codeberg.org/lindenii/furgit/format/pktline"
-	"codeberg.org/lindenii/furgit/format/sideband64k"
+	"codeberg.org/lindenii/furgit/protocol/pktline"
+	"codeberg.org/lindenii/furgit/protocol/sideband64k"
 	"codeberg.org/lindenii/furgit/objectid"
 )
 
--- a/receivepack/int_test.go
+++ b/receivepack/int_test.go
@@ -9,8 +9,8 @@
 	"testing"
 	"time"
 
-	"codeberg.org/lindenii/furgit/format/pktline"
-	"codeberg.org/lindenii/furgit/format/sideband64k"
+	"codeberg.org/lindenii/furgit/protocol/pktline"
+	"codeberg.org/lindenii/furgit/protocol/sideband64k"
 	"codeberg.org/lindenii/furgit/internal/testgit"
 	"codeberg.org/lindenii/furgit/objectid"
 	receivepack "codeberg.org/lindenii/furgit/receivepack"
--- a/receivepack/receivepack.go
+++ b/receivepack/receivepack.go
@@ -4,7 +4,7 @@
 	"context"
 	"io"
 
-	"codeberg.org/lindenii/furgit/format/pktline"
+	"codeberg.org/lindenii/furgit/protocol/pktline"
 	common "codeberg.org/lindenii/furgit/protocol/v0v1/server"
 	protoreceive "codeberg.org/lindenii/furgit/protocol/v0v1/server/receivepack"
 	"codeberg.org/lindenii/furgit/receivepack/service"
--