From af08c84539f9353718604988ba27ae3c466860fc Mon Sep 17 00:00:00 2001 From: Runxi Yu Date: Tue, 10 Mar 2026 13:28:59 +0800 Subject: *: Move sideband64k and pktline to protocol/ --- cmd/receivepack9418/errpkt.go | 2 +- cmd/receivepack9418/gitproto.go | 2 +- format/pktline/append.go | 39 ----- .../append_data_preserves_dst_on_error_test.go | 25 --- format/pktline/append_helpers_test.go | 24 --- format/pktline/chunk_writer.go | 65 ------- .../chunk_writer_write_and_read_from_test.go | 60 ------- format/pktline/constants.go | 12 -- format/pktline/decoder.go | 187 --------------------- .../pktline/decoder_data_control_and_0004_test.go | 60 ------- format/pktline/decoder_invalid_0003_test.go | 20 --- format/pktline/decoder_peek_test.go | 32 ---- .../decoder_rejects_over_maximum_length_test.go | 22 --- .../decoder_resync_after_over_max_data_test.go | 51 ------ .../decoder_resync_after_over_wire_max_test.go | 37 ---- format/pktline/decoder_unexpected_eof_test.go | 21 --- format/pktline/doc.go | 2 - format/pktline/encode_length_header_test.go | 28 --- format/pktline/encoder.go | 145 ---------------- .../encoder_buffered_flush_and_f_flush_test.go | 50 ------ .../encoder_buffered_flush_behavior_test.go | 86 ---------- ...r_set_max_data_cannot_exceed_wire_limit_test.go | 26 --- format/pktline/encoder_writes_frames_test.go | 51 ------ format/pktline/errors.go | 31 ---- format/pktline/frame.go | 10 -- format/pktline/header.go | 57 ------- format/pktline/parse_length_header_test.go | 26 --- format/pktline/type.go | 15 -- format/sideband64k/append.go | 40 ----- format/sideband64k/append_helpers_test.go | 30 ---- .../append_preserves_dst_on_error_test.go | 34 ---- format/sideband64k/band.go | 13 -- format/sideband64k/chunk_writer.go | 64 ------- .../chunk_writer_write_and_read_from_test.go | 60 ------- format/sideband64k/constants.go | 10 -- format/sideband64k/decoder.go | 158 ----------------- .../decoder_data_control_and_keepalive_test.go | 78 --------- format/sideband64k/decoder_invalid_band_test.go | 20 --- .../decoder_invalid_empty_payload_test.go | 20 --- .../sideband64k/decoder_malformed_pktline_test.go | 32 ---- format/sideband64k/decoder_partial_read_test.go | 32 ---- format/sideband64k/decoder_peek_test.go | 34 ---- .../decoder_resync_after_over_max_data_test.go | 51 ------ .../decoder_resync_after_over_wire_max_test.go | 37 ---- format/sideband64k/decoder_unexpected_eof_test.go | 21 --- format/sideband64k/doc.go | 2 - format/sideband64k/encoder.go | 98 ----------- .../encoder_buffered_flush_behavior_test.go | 59 ------- format/sideband64k/encoder_partial_write_test.go | 46 ----- ...r_set_max_data_cannot_exceed_wire_limit_test.go | 23 --- format/sideband64k/encoder_writes_frames_test.go | 58 ------- format/sideband64k/errors.go | 27 --- format/sideband64k/frame.go | 12 -- format/sideband64k/frame_type.go | 19 --- format/sideband64k/helpers_test.go | 46 ----- protocol/pktline/append.go | 39 +++++ .../append_data_preserves_dst_on_error_test.go | 25 +++ protocol/pktline/append_helpers_test.go | 24 +++ protocol/pktline/chunk_writer.go | 65 +++++++ .../chunk_writer_write_and_read_from_test.go | 60 +++++++ protocol/pktline/constants.go | 12 ++ protocol/pktline/decoder.go | 187 +++++++++++++++++++++ .../pktline/decoder_data_control_and_0004_test.go | 60 +++++++ protocol/pktline/decoder_invalid_0003_test.go | 20 +++ protocol/pktline/decoder_peek_test.go | 32 ++++ .../decoder_rejects_over_maximum_length_test.go | 22 +++ .../decoder_resync_after_over_max_data_test.go | 51 ++++++ .../decoder_resync_after_over_wire_max_test.go | 37 ++++ protocol/pktline/decoder_unexpected_eof_test.go | 21 +++ protocol/pktline/doc.go | 2 + protocol/pktline/encode_length_header_test.go | 28 +++ protocol/pktline/encoder.go | 145 ++++++++++++++++ .../encoder_buffered_flush_and_f_flush_test.go | 50 ++++++ .../encoder_buffered_flush_behavior_test.go | 86 ++++++++++ ...r_set_max_data_cannot_exceed_wire_limit_test.go | 26 +++ protocol/pktline/encoder_writes_frames_test.go | 51 ++++++ protocol/pktline/errors.go | 31 ++++ protocol/pktline/frame.go | 10 ++ protocol/pktline/header.go | 57 +++++++ protocol/pktline/parse_length_header_test.go | 26 +++ protocol/pktline/type.go | 15 ++ protocol/sideband64k/append.go | 40 +++++ protocol/sideband64k/append_helpers_test.go | 30 ++++ .../append_preserves_dst_on_error_test.go | 34 ++++ protocol/sideband64k/band.go | 13 ++ protocol/sideband64k/chunk_writer.go | 64 +++++++ .../chunk_writer_write_and_read_from_test.go | 60 +++++++ protocol/sideband64k/constants.go | 10 ++ protocol/sideband64k/decoder.go | 158 +++++++++++++++++ .../decoder_data_control_and_keepalive_test.go | 78 +++++++++ protocol/sideband64k/decoder_invalid_band_test.go | 20 +++ .../decoder_invalid_empty_payload_test.go | 20 +++ .../sideband64k/decoder_malformed_pktline_test.go | 32 ++++ protocol/sideband64k/decoder_partial_read_test.go | 32 ++++ protocol/sideband64k/decoder_peek_test.go | 34 ++++ .../decoder_resync_after_over_max_data_test.go | 51 ++++++ .../decoder_resync_after_over_wire_max_test.go | 37 ++++ .../sideband64k/decoder_unexpected_eof_test.go | 21 +++ protocol/sideband64k/doc.go | 2 + protocol/sideband64k/encoder.go | 98 +++++++++++ .../encoder_buffered_flush_behavior_test.go | 59 +++++++ protocol/sideband64k/encoder_partial_write_test.go | 46 +++++ ...r_set_max_data_cannot_exceed_wire_limit_test.go | 23 +++ protocol/sideband64k/encoder_writes_frames_test.go | 58 +++++++ protocol/sideband64k/errors.go | 27 +++ protocol/sideband64k/frame.go | 12 ++ protocol/sideband64k/frame_type.go | 19 +++ protocol/sideband64k/helpers_test.go | 46 +++++ protocol/v0v1/server/frame.go | 2 +- protocol/v0v1/server/receivepack/parse_test.go | 2 +- protocol/v0v1/server/receivepack/report_status.go | 2 +- .../v0v1/server/receivepack/report_status_test.go | 4 +- protocol/v0v1/server/session.go | 4 +- receivepack/int_test.go | 4 +- receivepack/receivepack.go | 2 +- 115 files changed, 2318 insertions(+), 2318 deletions(-) delete mode 100644 format/pktline/append.go delete mode 100644 format/pktline/append_data_preserves_dst_on_error_test.go delete mode 100644 format/pktline/append_helpers_test.go delete mode 100644 format/pktline/chunk_writer.go delete mode 100644 format/pktline/chunk_writer_write_and_read_from_test.go delete mode 100644 format/pktline/constants.go delete mode 100644 format/pktline/decoder.go delete mode 100644 format/pktline/decoder_data_control_and_0004_test.go delete mode 100644 format/pktline/decoder_invalid_0003_test.go delete mode 100644 format/pktline/decoder_peek_test.go delete mode 100644 format/pktline/decoder_rejects_over_maximum_length_test.go delete mode 100644 format/pktline/decoder_resync_after_over_max_data_test.go delete mode 100644 format/pktline/decoder_resync_after_over_wire_max_test.go delete mode 100644 format/pktline/decoder_unexpected_eof_test.go delete mode 100644 format/pktline/doc.go delete mode 100644 format/pktline/encode_length_header_test.go delete mode 100644 format/pktline/encoder.go delete mode 100644 format/pktline/encoder_buffered_flush_and_f_flush_test.go delete mode 100644 format/pktline/encoder_buffered_flush_behavior_test.go delete mode 100644 format/pktline/encoder_set_max_data_cannot_exceed_wire_limit_test.go delete mode 100644 format/pktline/encoder_writes_frames_test.go delete mode 100644 format/pktline/errors.go delete mode 100644 format/pktline/frame.go delete mode 100644 format/pktline/header.go delete mode 100644 format/pktline/parse_length_header_test.go delete mode 100644 format/pktline/type.go delete mode 100644 format/sideband64k/append.go delete mode 100644 format/sideband64k/append_helpers_test.go delete mode 100644 format/sideband64k/append_preserves_dst_on_error_test.go delete mode 100644 format/sideband64k/band.go delete mode 100644 format/sideband64k/chunk_writer.go delete mode 100644 format/sideband64k/chunk_writer_write_and_read_from_test.go delete mode 100644 format/sideband64k/constants.go delete mode 100644 format/sideband64k/decoder.go delete mode 100644 format/sideband64k/decoder_data_control_and_keepalive_test.go delete mode 100644 format/sideband64k/decoder_invalid_band_test.go delete mode 100644 format/sideband64k/decoder_invalid_empty_payload_test.go delete mode 100644 format/sideband64k/decoder_malformed_pktline_test.go delete mode 100644 format/sideband64k/decoder_partial_read_test.go delete mode 100644 format/sideband64k/decoder_peek_test.go delete mode 100644 format/sideband64k/decoder_resync_after_over_max_data_test.go delete mode 100644 format/sideband64k/decoder_resync_after_over_wire_max_test.go delete mode 100644 format/sideband64k/decoder_unexpected_eof_test.go delete mode 100644 format/sideband64k/doc.go delete mode 100644 format/sideband64k/encoder.go delete mode 100644 format/sideband64k/encoder_buffered_flush_behavior_test.go delete mode 100644 format/sideband64k/encoder_partial_write_test.go delete mode 100644 format/sideband64k/encoder_set_max_data_cannot_exceed_wire_limit_test.go delete mode 100644 format/sideband64k/encoder_writes_frames_test.go delete mode 100644 format/sideband64k/errors.go delete mode 100644 format/sideband64k/frame.go delete mode 100644 format/sideband64k/frame_type.go delete mode 100644 format/sideband64k/helpers_test.go create mode 100644 protocol/pktline/append.go create mode 100644 protocol/pktline/append_data_preserves_dst_on_error_test.go create mode 100644 protocol/pktline/append_helpers_test.go create mode 100644 protocol/pktline/chunk_writer.go create mode 100644 protocol/pktline/chunk_writer_write_and_read_from_test.go create mode 100644 protocol/pktline/constants.go create mode 100644 protocol/pktline/decoder.go create mode 100644 protocol/pktline/decoder_data_control_and_0004_test.go create mode 100644 protocol/pktline/decoder_invalid_0003_test.go create mode 100644 protocol/pktline/decoder_peek_test.go create mode 100644 protocol/pktline/decoder_rejects_over_maximum_length_test.go create mode 100644 protocol/pktline/decoder_resync_after_over_max_data_test.go create mode 100644 protocol/pktline/decoder_resync_after_over_wire_max_test.go create mode 100644 protocol/pktline/decoder_unexpected_eof_test.go create mode 100644 protocol/pktline/doc.go create mode 100644 protocol/pktline/encode_length_header_test.go create mode 100644 protocol/pktline/encoder.go create mode 100644 protocol/pktline/encoder_buffered_flush_and_f_flush_test.go create mode 100644 protocol/pktline/encoder_buffered_flush_behavior_test.go create mode 100644 protocol/pktline/encoder_set_max_data_cannot_exceed_wire_limit_test.go create mode 100644 protocol/pktline/encoder_writes_frames_test.go create mode 100644 protocol/pktline/errors.go create mode 100644 protocol/pktline/frame.go create mode 100644 protocol/pktline/header.go create mode 100644 protocol/pktline/parse_length_header_test.go create mode 100644 protocol/pktline/type.go create mode 100644 protocol/sideband64k/append.go create mode 100644 protocol/sideband64k/append_helpers_test.go create mode 100644 protocol/sideband64k/append_preserves_dst_on_error_test.go create mode 100644 protocol/sideband64k/band.go create mode 100644 protocol/sideband64k/chunk_writer.go create mode 100644 protocol/sideband64k/chunk_writer_write_and_read_from_test.go create mode 100644 protocol/sideband64k/constants.go create mode 100644 protocol/sideband64k/decoder.go create mode 100644 protocol/sideband64k/decoder_data_control_and_keepalive_test.go create mode 100644 protocol/sideband64k/decoder_invalid_band_test.go create mode 100644 protocol/sideband64k/decoder_invalid_empty_payload_test.go create mode 100644 protocol/sideband64k/decoder_malformed_pktline_test.go create mode 100644 protocol/sideband64k/decoder_partial_read_test.go create mode 100644 protocol/sideband64k/decoder_peek_test.go create mode 100644 protocol/sideband64k/decoder_resync_after_over_max_data_test.go create mode 100644 protocol/sideband64k/decoder_resync_after_over_wire_max_test.go create mode 100644 protocol/sideband64k/decoder_unexpected_eof_test.go create mode 100644 protocol/sideband64k/doc.go create mode 100644 protocol/sideband64k/encoder.go create mode 100644 protocol/sideband64k/encoder_buffered_flush_behavior_test.go create mode 100644 protocol/sideband64k/encoder_partial_write_test.go create mode 100644 protocol/sideband64k/encoder_set_max_data_cannot_exceed_wire_limit_test.go create mode 100644 protocol/sideband64k/encoder_writes_frames_test.go create mode 100644 protocol/sideband64k/errors.go create mode 100644 protocol/sideband64k/frame.go create mode 100644 protocol/sideband64k/frame_type.go create mode 100644 protocol/sideband64k/helpers_test.go diff --git a/cmd/receivepack9418/errpkt.go b/cmd/receivepack9418/errpkt.go index 379c3cb2..1a5e4837 100644 --- a/cmd/receivepack9418/errpkt.go +++ b/cmd/receivepack9418/errpkt.go @@ -3,7 +3,7 @@ package main import ( "io" - "codeberg.org/lindenii/furgit/format/pktline" + "codeberg.org/lindenii/furgit/protocol/pktline" ) func writeErrPkt(w io.Writer, message string) { diff --git a/cmd/receivepack9418/gitproto.go b/cmd/receivepack9418/gitproto.go index 609f2c12..d0d6c707 100644 --- a/cmd/receivepack9418/gitproto.go +++ b/cmd/receivepack9418/gitproto.go @@ -4,7 +4,7 @@ import ( "fmt" "io" - "codeberg.org/lindenii/furgit/format/pktline" + "codeberg.org/lindenii/furgit/protocol/pktline" ) func readGitProtoRequest(r io.Reader) (gitProtoRequest, error) { diff --git a/format/pktline/append.go b/format/pktline/append.go deleted file mode 100644 index 9425e58e..00000000 --- a/format/pktline/append.go +++ /dev/null @@ -1,39 +0,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') -} diff --git a/format/pktline/append_data_preserves_dst_on_error_test.go b/format/pktline/append_data_preserves_dst_on_error_test.go deleted file mode 100644 index 6645c20d..00000000 --- a/format/pktline/append_data_preserves_dst_on_error_test.go +++ /dev/null @@ -1,25 +0,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)) - } -} diff --git a/format/pktline/append_helpers_test.go b/format/pktline/append_helpers_test.go deleted file mode 100644 index db7ca034..00000000 --- a/format/pktline/append_helpers_test.go +++ /dev/null @@ -1,24 +0,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) - } -} diff --git a/format/pktline/chunk_writer.go b/format/pktline/chunk_writer.go deleted file mode 100644 index b258ff20..00000000 --- a/format/pktline/chunk_writer.go +++ /dev/null @@ -1,65 +0,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 - } - } -} diff --git a/format/pktline/chunk_writer_write_and_read_from_test.go b/format/pktline/chunk_writer_write_and_read_from_test.go deleted file mode 100644 index 8a019f7a..00000000 --- a/format/pktline/chunk_writer_write_and_read_from_test.go +++ /dev/null @@ -1,60 +0,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) - } -} diff --git a/format/pktline/constants.go b/format/pktline/constants.go deleted file mode 100644 index 811eb3c6..00000000 --- a/format/pktline/constants.go +++ /dev/null @@ -1,12 +0,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 -) diff --git a/format/pktline/decoder.go b/format/pktline/decoder.go deleted file mode 100644 index 898d8ad6..00000000 --- a/format/pktline/decoder.go +++ /dev/null @@ -1,187 +0,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 -} diff --git a/format/pktline/decoder_data_control_and_0004_test.go b/format/pktline/decoder_data_control_and_0004_test.go deleted file mode 100644 index 9dea8427..00000000 --- a/format/pktline/decoder_data_control_and_0004_test.go +++ /dev/null @@ -1,60 +0,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) - } -} diff --git a/format/pktline/decoder_invalid_0003_test.go b/format/pktline/decoder_invalid_0003_test.go deleted file mode 100644 index 3a93633c..00000000 --- a/format/pktline/decoder_invalid_0003_test.go +++ /dev/null @@ -1,20 +0,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) - } -} diff --git a/format/pktline/decoder_peek_test.go b/format/pktline/decoder_peek_test.go deleted file mode 100644 index 8ea4e8b4..00000000 --- a/format/pktline/decoder_peek_test.go +++ /dev/null @@ -1,32 +0,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) - } -} diff --git a/format/pktline/decoder_rejects_over_maximum_length_test.go b/format/pktline/decoder_rejects_over_maximum_length_test.go deleted file mode 100644 index 8d897a55..00000000 --- a/format/pktline/decoder_rejects_over_maximum_length_test.go +++ /dev/null @@ -1,22 +0,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) - } -} diff --git a/format/pktline/decoder_resync_after_over_max_data_test.go b/format/pktline/decoder_resync_after_over_max_data_test.go deleted file mode 100644 index 4e170795..00000000 --- a/format/pktline/decoder_resync_after_over_max_data_test.go +++ /dev/null @@ -1,51 +0,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) - } -} diff --git a/format/pktline/decoder_resync_after_over_wire_max_test.go b/format/pktline/decoder_resync_after_over_wire_max_test.go deleted file mode 100644 index 29a3440f..00000000 --- a/format/pktline/decoder_resync_after_over_wire_max_test.go +++ /dev/null @@ -1,37 +0,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) - } -} diff --git a/format/pktline/decoder_unexpected_eof_test.go b/format/pktline/decoder_unexpected_eof_test.go deleted file mode 100644 index ffc0fb45..00000000 --- a/format/pktline/decoder_unexpected_eof_test.go +++ /dev/null @@ -1,21 +0,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) - } -} diff --git a/format/pktline/doc.go b/format/pktline/doc.go deleted file mode 100644 index 3f7cca89..00000000 --- a/format/pktline/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package pktline implements the pkt-line format specified in gitprotocol-common(5). -package pktline diff --git a/format/pktline/encode_length_header_test.go b/format/pktline/encode_length_header_test.go deleted file mode 100644 index ec9a9c9b..00000000 --- a/format/pktline/encode_length_header_test.go +++ /dev/null @@ -1,28 +0,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) - } -} diff --git a/format/pktline/encoder.go b/format/pktline/encoder.go deleted file mode 100644 index b4c6dbf0..00000000 --- a/format/pktline/encoder.go +++ /dev/null @@ -1,145 +0,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 -} diff --git a/format/pktline/encoder_buffered_flush_and_f_flush_test.go b/format/pktline/encoder_buffered_flush_and_f_flush_test.go deleted file mode 100644 index 1e5e2a88..00000000 --- a/format/pktline/encoder_buffered_flush_and_f_flush_test.go +++ /dev/null @@ -1,50 +0,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") - } -} diff --git a/format/pktline/encoder_buffered_flush_behavior_test.go b/format/pktline/encoder_buffered_flush_behavior_test.go deleted file mode 100644 index dc21eba0..00000000 --- a/format/pktline/encoder_buffered_flush_behavior_test.go +++ /dev/null @@ -1,86 +0,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) - } -} diff --git a/format/pktline/encoder_set_max_data_cannot_exceed_wire_limit_test.go b/format/pktline/encoder_set_max_data_cannot_exceed_wire_limit_test.go deleted file mode 100644 index 696f524d..00000000 --- a/format/pktline/encoder_set_max_data_cannot_exceed_wire_limit_test.go +++ /dev/null @@ -1,26 +0,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) - } -} diff --git a/format/pktline/encoder_writes_frames_test.go b/format/pktline/encoder_writes_frames_test.go deleted file mode 100644 index c06047e9..00000000 --- a/format/pktline/encoder_writes_frames_test.go +++ /dev/null @@ -1,51 +0,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) - } -} diff --git a/format/pktline/errors.go b/format/pktline/errors.go deleted file mode 100644 index 866ff467..00000000 --- a/format/pktline/errors.go +++ /dev/null @@ -1,31 +0,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 "" - } - - if e.Reason == "" { - return "pktline: protocol error" - } - - return "pktline: protocol error: " + e.Reason -} diff --git a/format/pktline/frame.go b/format/pktline/frame.go deleted file mode 100644 index a1cf708c..00000000 --- a/format/pktline/frame.go +++ /dev/null @@ -1,10 +0,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 -} diff --git a/format/pktline/header.go b/format/pktline/header.go deleted file mode 100644 index 41e50e04..00000000 --- a/format/pktline/header.go +++ /dev/null @@ -1,57 +0,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 -} diff --git a/format/pktline/parse_length_header_test.go b/format/pktline/parse_length_header_test.go deleted file mode 100644 index dac3fae9..00000000 --- a/format/pktline/parse_length_header_test.go +++ /dev/null @@ -1,26 +0,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) - } -} diff --git a/format/pktline/type.go b/format/pktline/type.go deleted file mode 100644 index 641d1c6c..00000000 --- a/format/pktline/type.go +++ /dev/null @@ -1,15 +0,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 -) diff --git a/format/sideband64k/append.go b/format/sideband64k/append.go deleted file mode 100644 index 59dbdf43..00000000 --- a/format/sideband64k/append.go +++ /dev/null @@ -1,40 +0,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) -} diff --git a/format/sideband64k/append_helpers_test.go b/format/sideband64k/append_helpers_test.go deleted file mode 100644 index 17cd7493..00000000 --- a/format/sideband64k/append_helpers_test.go +++ /dev/null @@ -1,30 +0,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) - } -} diff --git a/format/sideband64k/append_preserves_dst_on_error_test.go b/format/sideband64k/append_preserves_dst_on_error_test.go deleted file mode 100644 index a327326d..00000000 --- a/format/sideband64k/append_preserves_dst_on_error_test.go +++ /dev/null @@ -1,34 +0,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)) - } -} diff --git a/format/sideband64k/band.go b/format/sideband64k/band.go deleted file mode 100644 index 73c61fd8..00000000 --- a/format/sideband64k/band.go +++ /dev/null @@ -1,13 +0,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 -) diff --git a/format/sideband64k/chunk_writer.go b/format/sideband64k/chunk_writer.go deleted file mode 100644 index f95f75d8..00000000 --- a/format/sideband64k/chunk_writer.go +++ /dev/null @@ -1,64 +0,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 - } - } -} diff --git a/format/sideband64k/chunk_writer_write_and_read_from_test.go b/format/sideband64k/chunk_writer_write_and_read_from_test.go deleted file mode 100644 index 8a736107..00000000 --- a/format/sideband64k/chunk_writer_write_and_read_from_test.go +++ /dev/null @@ -1,60 +0,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) - } -} diff --git a/format/sideband64k/constants.go b/format/sideband64k/constants.go deleted file mode 100644 index 40077761..00000000 --- a/format/sideband64k/constants.go +++ /dev/null @@ -1,10 +0,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 -) diff --git a/format/sideband64k/decoder.go b/format/sideband64k/decoder.go deleted file mode 100644 index b54e7d39..00000000 --- a/format/sideband64k/decoder.go +++ /dev/null @@ -1,158 +0,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 -} diff --git a/format/sideband64k/decoder_data_control_and_keepalive_test.go b/format/sideband64k/decoder_data_control_and_keepalive_test.go deleted file mode 100644 index b79a45f7..00000000 --- a/format/sideband64k/decoder_data_control_and_keepalive_test.go +++ /dev/null @@ -1,78 +0,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) - } -} diff --git a/format/sideband64k/decoder_invalid_band_test.go b/format/sideband64k/decoder_invalid_band_test.go deleted file mode 100644 index 12c53891..00000000 --- a/format/sideband64k/decoder_invalid_band_test.go +++ /dev/null @@ -1,20 +0,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) - } -} diff --git a/format/sideband64k/decoder_invalid_empty_payload_test.go b/format/sideband64k/decoder_invalid_empty_payload_test.go deleted file mode 100644 index eb52ad6a..00000000 --- a/format/sideband64k/decoder_invalid_empty_payload_test.go +++ /dev/null @@ -1,20 +0,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) - } -} diff --git a/format/sideband64k/decoder_malformed_pktline_test.go b/format/sideband64k/decoder_malformed_pktline_test.go deleted file mode 100644 index 54ea6ffb..00000000 --- a/format/sideband64k/decoder_malformed_pktline_test.go +++ /dev/null @@ -1,32 +0,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) - } -} diff --git a/format/sideband64k/decoder_partial_read_test.go b/format/sideband64k/decoder_partial_read_test.go deleted file mode 100644 index d5b2f672..00000000 --- a/format/sideband64k/decoder_partial_read_test.go +++ /dev/null @@ -1,32 +0,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) - } -} diff --git a/format/sideband64k/decoder_peek_test.go b/format/sideband64k/decoder_peek_test.go deleted file mode 100644 index 1628ca41..00000000 --- a/format/sideband64k/decoder_peek_test.go +++ /dev/null @@ -1,34 +0,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) - } -} diff --git a/format/sideband64k/decoder_resync_after_over_max_data_test.go b/format/sideband64k/decoder_resync_after_over_max_data_test.go deleted file mode 100644 index 923195c1..00000000 --- a/format/sideband64k/decoder_resync_after_over_max_data_test.go +++ /dev/null @@ -1,51 +0,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) - } -} diff --git a/format/sideband64k/decoder_resync_after_over_wire_max_test.go b/format/sideband64k/decoder_resync_after_over_wire_max_test.go deleted file mode 100644 index c790cd6f..00000000 --- a/format/sideband64k/decoder_resync_after_over_wire_max_test.go +++ /dev/null @@ -1,37 +0,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) - } -} diff --git a/format/sideband64k/decoder_unexpected_eof_test.go b/format/sideband64k/decoder_unexpected_eof_test.go deleted file mode 100644 index 40794471..00000000 --- a/format/sideband64k/decoder_unexpected_eof_test.go +++ /dev/null @@ -1,21 +0,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) - } -} diff --git a/format/sideband64k/doc.go b/format/sideband64k/doc.go deleted file mode 100644 index 55c33650..00000000 --- a/format/sideband64k/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package sideband64k implements Git side-band-64k multiplexing over pkt-line. -package sideband64k diff --git a/format/sideband64k/encoder.go b/format/sideband64k/encoder.go deleted file mode 100644 index 4205d461..00000000 --- a/format/sideband64k/encoder.go +++ /dev/null @@ -1,98 +0,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) -} diff --git a/format/sideband64k/encoder_buffered_flush_behavior_test.go b/format/sideband64k/encoder_buffered_flush_behavior_test.go deleted file mode 100644 index 5c8b358f..00000000 --- a/format/sideband64k/encoder_buffered_flush_behavior_test.go +++ /dev/null @@ -1,59 +0,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) - } -} diff --git a/format/sideband64k/encoder_partial_write_test.go b/format/sideband64k/encoder_partial_write_test.go deleted file mode 100644 index 3d4f8066..00000000 --- a/format/sideband64k/encoder_partial_write_test.go +++ /dev/null @@ -1,46 +0,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) - } -} diff --git a/format/sideband64k/encoder_set_max_data_cannot_exceed_wire_limit_test.go b/format/sideband64k/encoder_set_max_data_cannot_exceed_wire_limit_test.go deleted file mode 100644 index aa0adcf0..00000000 --- a/format/sideband64k/encoder_set_max_data_cannot_exceed_wire_limit_test.go +++ /dev/null @@ -1,23 +0,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) - } -} diff --git a/format/sideband64k/encoder_writes_frames_test.go b/format/sideband64k/encoder_writes_frames_test.go deleted file mode 100644 index 8cfd4a89..00000000 --- a/format/sideband64k/encoder_writes_frames_test.go +++ /dev/null @@ -1,58 +0,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) - } -} diff --git a/format/sideband64k/errors.go b/format/sideband64k/errors.go deleted file mode 100644 index 44e7c165..00000000 --- a/format/sideband64k/errors.go +++ /dev/null @@ -1,27 +0,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 "" - } - - if e.Reason == "" { - return "sideband64k: protocol error" - } - - return "sideband64k: protocol error: " + e.Reason -} diff --git a/format/sideband64k/frame.go b/format/sideband64k/frame.go deleted file mode 100644 index 1335a8e3..00000000 --- a/format/sideband64k/frame.go +++ /dev/null @@ -1,12 +0,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 -} diff --git a/format/sideband64k/frame_type.go b/format/sideband64k/frame_type.go deleted file mode 100644 index 052d8b10..00000000 --- a/format/sideband64k/frame_type.go +++ /dev/null @@ -1,19 +0,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 -) diff --git a/format/sideband64k/helpers_test.go b/format/sideband64k/helpers_test.go deleted file mode 100644 index f9b2608f..00000000 --- a/format/sideband64k/helpers_test.go +++ /dev/null @@ -1,46 +0,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 -} diff --git a/protocol/pktline/append.go b/protocol/pktline/append.go new file mode 100644 index 00000000..9425e58e --- /dev/null +++ b/protocol/pktline/append.go @@ -0,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') +} diff --git a/protocol/pktline/append_data_preserves_dst_on_error_test.go b/protocol/pktline/append_data_preserves_dst_on_error_test.go new file mode 100644 index 00000000..2888be16 --- /dev/null +++ b/protocol/pktline/append_data_preserves_dst_on_error_test.go @@ -0,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)) + } +} diff --git a/protocol/pktline/append_helpers_test.go b/protocol/pktline/append_helpers_test.go new file mode 100644 index 00000000..ae5f4748 --- /dev/null +++ b/protocol/pktline/append_helpers_test.go @@ -0,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) + } +} diff --git a/protocol/pktline/chunk_writer.go b/protocol/pktline/chunk_writer.go new file mode 100644 index 00000000..b258ff20 --- /dev/null +++ b/protocol/pktline/chunk_writer.go @@ -0,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 + } + } +} diff --git a/protocol/pktline/chunk_writer_write_and_read_from_test.go b/protocol/pktline/chunk_writer_write_and_read_from_test.go new file mode 100644 index 00000000..f34aa754 --- /dev/null +++ b/protocol/pktline/chunk_writer_write_and_read_from_test.go @@ -0,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) + } +} diff --git a/protocol/pktline/constants.go b/protocol/pktline/constants.go new file mode 100644 index 00000000..811eb3c6 --- /dev/null +++ b/protocol/pktline/constants.go @@ -0,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 +) diff --git a/protocol/pktline/decoder.go b/protocol/pktline/decoder.go new file mode 100644 index 00000000..898d8ad6 --- /dev/null +++ b/protocol/pktline/decoder.go @@ -0,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 +} diff --git a/protocol/pktline/decoder_data_control_and_0004_test.go b/protocol/pktline/decoder_data_control_and_0004_test.go new file mode 100644 index 00000000..727e1063 --- /dev/null +++ b/protocol/pktline/decoder_data_control_and_0004_test.go @@ -0,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) + } +} diff --git a/protocol/pktline/decoder_invalid_0003_test.go b/protocol/pktline/decoder_invalid_0003_test.go new file mode 100644 index 00000000..9414d715 --- /dev/null +++ b/protocol/pktline/decoder_invalid_0003_test.go @@ -0,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) + } +} diff --git a/protocol/pktline/decoder_peek_test.go b/protocol/pktline/decoder_peek_test.go new file mode 100644 index 00000000..4b2fe2d9 --- /dev/null +++ b/protocol/pktline/decoder_peek_test.go @@ -0,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) + } +} diff --git a/protocol/pktline/decoder_rejects_over_maximum_length_test.go b/protocol/pktline/decoder_rejects_over_maximum_length_test.go new file mode 100644 index 00000000..67dd5053 --- /dev/null +++ b/protocol/pktline/decoder_rejects_over_maximum_length_test.go @@ -0,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) + } +} diff --git a/protocol/pktline/decoder_resync_after_over_max_data_test.go b/protocol/pktline/decoder_resync_after_over_max_data_test.go new file mode 100644 index 00000000..798ca91f --- /dev/null +++ b/protocol/pktline/decoder_resync_after_over_max_data_test.go @@ -0,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) + } +} diff --git a/protocol/pktline/decoder_resync_after_over_wire_max_test.go b/protocol/pktline/decoder_resync_after_over_wire_max_test.go new file mode 100644 index 00000000..3ba98d62 --- /dev/null +++ b/protocol/pktline/decoder_resync_after_over_wire_max_test.go @@ -0,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) + } +} diff --git a/protocol/pktline/decoder_unexpected_eof_test.go b/protocol/pktline/decoder_unexpected_eof_test.go new file mode 100644 index 00000000..b86b5540 --- /dev/null +++ b/protocol/pktline/decoder_unexpected_eof_test.go @@ -0,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) + } +} diff --git a/protocol/pktline/doc.go b/protocol/pktline/doc.go new file mode 100644 index 00000000..3f7cca89 --- /dev/null +++ b/protocol/pktline/doc.go @@ -0,0 +1,2 @@ +// Package pktline implements the pkt-line format specified in gitprotocol-common(5). +package pktline diff --git a/protocol/pktline/encode_length_header_test.go b/protocol/pktline/encode_length_header_test.go new file mode 100644 index 00000000..5ad76daf --- /dev/null +++ b/protocol/pktline/encode_length_header_test.go @@ -0,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) + } +} diff --git a/protocol/pktline/encoder.go b/protocol/pktline/encoder.go new file mode 100644 index 00000000..b4c6dbf0 --- /dev/null +++ b/protocol/pktline/encoder.go @@ -0,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 +} diff --git a/protocol/pktline/encoder_buffered_flush_and_f_flush_test.go b/protocol/pktline/encoder_buffered_flush_and_f_flush_test.go new file mode 100644 index 00000000..395d7310 --- /dev/null +++ b/protocol/pktline/encoder_buffered_flush_and_f_flush_test.go @@ -0,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") + } +} diff --git a/protocol/pktline/encoder_buffered_flush_behavior_test.go b/protocol/pktline/encoder_buffered_flush_behavior_test.go new file mode 100644 index 00000000..89ae5fcf --- /dev/null +++ b/protocol/pktline/encoder_buffered_flush_behavior_test.go @@ -0,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) + } +} diff --git a/protocol/pktline/encoder_set_max_data_cannot_exceed_wire_limit_test.go b/protocol/pktline/encoder_set_max_data_cannot_exceed_wire_limit_test.go new file mode 100644 index 00000000..c4c4f5c9 --- /dev/null +++ b/protocol/pktline/encoder_set_max_data_cannot_exceed_wire_limit_test.go @@ -0,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) + } +} diff --git a/protocol/pktline/encoder_writes_frames_test.go b/protocol/pktline/encoder_writes_frames_test.go new file mode 100644 index 00000000..2a595730 --- /dev/null +++ b/protocol/pktline/encoder_writes_frames_test.go @@ -0,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) + } +} diff --git a/protocol/pktline/errors.go b/protocol/pktline/errors.go new file mode 100644 index 00000000..866ff467 --- /dev/null +++ b/protocol/pktline/errors.go @@ -0,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 "" + } + + if e.Reason == "" { + return "pktline: protocol error" + } + + return "pktline: protocol error: " + e.Reason +} diff --git a/protocol/pktline/frame.go b/protocol/pktline/frame.go new file mode 100644 index 00000000..a1cf708c --- /dev/null +++ b/protocol/pktline/frame.go @@ -0,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 +} diff --git a/protocol/pktline/header.go b/protocol/pktline/header.go new file mode 100644 index 00000000..41e50e04 --- /dev/null +++ b/protocol/pktline/header.go @@ -0,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 +} diff --git a/protocol/pktline/parse_length_header_test.go b/protocol/pktline/parse_length_header_test.go new file mode 100644 index 00000000..4bc743dd --- /dev/null +++ b/protocol/pktline/parse_length_header_test.go @@ -0,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) + } +} diff --git a/protocol/pktline/type.go b/protocol/pktline/type.go new file mode 100644 index 00000000..641d1c6c --- /dev/null +++ b/protocol/pktline/type.go @@ -0,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 +) diff --git a/protocol/sideband64k/append.go b/protocol/sideband64k/append.go new file mode 100644 index 00000000..4da419f3 --- /dev/null +++ b/protocol/sideband64k/append.go @@ -0,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) +} diff --git a/protocol/sideband64k/append_helpers_test.go b/protocol/sideband64k/append_helpers_test.go new file mode 100644 index 00000000..5b64856a --- /dev/null +++ b/protocol/sideband64k/append_helpers_test.go @@ -0,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) + } +} diff --git a/protocol/sideband64k/append_preserves_dst_on_error_test.go b/protocol/sideband64k/append_preserves_dst_on_error_test.go new file mode 100644 index 00000000..7c86a8b7 --- /dev/null +++ b/protocol/sideband64k/append_preserves_dst_on_error_test.go @@ -0,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)) + } +} diff --git a/protocol/sideband64k/band.go b/protocol/sideband64k/band.go new file mode 100644 index 00000000..73c61fd8 --- /dev/null +++ b/protocol/sideband64k/band.go @@ -0,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 +) diff --git a/protocol/sideband64k/chunk_writer.go b/protocol/sideband64k/chunk_writer.go new file mode 100644 index 00000000..f95f75d8 --- /dev/null +++ b/protocol/sideband64k/chunk_writer.go @@ -0,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 + } + } +} diff --git a/protocol/sideband64k/chunk_writer_write_and_read_from_test.go b/protocol/sideband64k/chunk_writer_write_and_read_from_test.go new file mode 100644 index 00000000..ab954d57 --- /dev/null +++ b/protocol/sideband64k/chunk_writer_write_and_read_from_test.go @@ -0,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) + } +} diff --git a/protocol/sideband64k/constants.go b/protocol/sideband64k/constants.go new file mode 100644 index 00000000..f71423ee --- /dev/null +++ b/protocol/sideband64k/constants.go @@ -0,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 +) diff --git a/protocol/sideband64k/decoder.go b/protocol/sideband64k/decoder.go new file mode 100644 index 00000000..57541567 --- /dev/null +++ b/protocol/sideband64k/decoder.go @@ -0,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 +} diff --git a/protocol/sideband64k/decoder_data_control_and_keepalive_test.go b/protocol/sideband64k/decoder_data_control_and_keepalive_test.go new file mode 100644 index 00000000..d829b930 --- /dev/null +++ b/protocol/sideband64k/decoder_data_control_and_keepalive_test.go @@ -0,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) + } +} diff --git a/protocol/sideband64k/decoder_invalid_band_test.go b/protocol/sideband64k/decoder_invalid_band_test.go new file mode 100644 index 00000000..2b28a0ef --- /dev/null +++ b/protocol/sideband64k/decoder_invalid_band_test.go @@ -0,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) + } +} diff --git a/protocol/sideband64k/decoder_invalid_empty_payload_test.go b/protocol/sideband64k/decoder_invalid_empty_payload_test.go new file mode 100644 index 00000000..86481166 --- /dev/null +++ b/protocol/sideband64k/decoder_invalid_empty_payload_test.go @@ -0,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) + } +} diff --git a/protocol/sideband64k/decoder_malformed_pktline_test.go b/protocol/sideband64k/decoder_malformed_pktline_test.go new file mode 100644 index 00000000..9d4030f3 --- /dev/null +++ b/protocol/sideband64k/decoder_malformed_pktline_test.go @@ -0,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) + } +} diff --git a/protocol/sideband64k/decoder_partial_read_test.go b/protocol/sideband64k/decoder_partial_read_test.go new file mode 100644 index 00000000..730363d7 --- /dev/null +++ b/protocol/sideband64k/decoder_partial_read_test.go @@ -0,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) + } +} diff --git a/protocol/sideband64k/decoder_peek_test.go b/protocol/sideband64k/decoder_peek_test.go new file mode 100644 index 00000000..b0b43fa1 --- /dev/null +++ b/protocol/sideband64k/decoder_peek_test.go @@ -0,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) + } +} diff --git a/protocol/sideband64k/decoder_resync_after_over_max_data_test.go b/protocol/sideband64k/decoder_resync_after_over_max_data_test.go new file mode 100644 index 00000000..dea396d1 --- /dev/null +++ b/protocol/sideband64k/decoder_resync_after_over_max_data_test.go @@ -0,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) + } +} diff --git a/protocol/sideband64k/decoder_resync_after_over_wire_max_test.go b/protocol/sideband64k/decoder_resync_after_over_wire_max_test.go new file mode 100644 index 00000000..f646c6ad --- /dev/null +++ b/protocol/sideband64k/decoder_resync_after_over_wire_max_test.go @@ -0,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) + } +} diff --git a/protocol/sideband64k/decoder_unexpected_eof_test.go b/protocol/sideband64k/decoder_unexpected_eof_test.go new file mode 100644 index 00000000..74d5a6fb --- /dev/null +++ b/protocol/sideband64k/decoder_unexpected_eof_test.go @@ -0,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) + } +} diff --git a/protocol/sideband64k/doc.go b/protocol/sideband64k/doc.go new file mode 100644 index 00000000..55c33650 --- /dev/null +++ b/protocol/sideband64k/doc.go @@ -0,0 +1,2 @@ +// Package sideband64k implements Git side-band-64k multiplexing over pkt-line. +package sideband64k diff --git a/protocol/sideband64k/encoder.go b/protocol/sideband64k/encoder.go new file mode 100644 index 00000000..75fb1339 --- /dev/null +++ b/protocol/sideband64k/encoder.go @@ -0,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) +} diff --git a/protocol/sideband64k/encoder_buffered_flush_behavior_test.go b/protocol/sideband64k/encoder_buffered_flush_behavior_test.go new file mode 100644 index 00000000..723a55ec --- /dev/null +++ b/protocol/sideband64k/encoder_buffered_flush_behavior_test.go @@ -0,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) + } +} diff --git a/protocol/sideband64k/encoder_partial_write_test.go b/protocol/sideband64k/encoder_partial_write_test.go new file mode 100644 index 00000000..3cec9324 --- /dev/null +++ b/protocol/sideband64k/encoder_partial_write_test.go @@ -0,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) + } +} diff --git a/protocol/sideband64k/encoder_set_max_data_cannot_exceed_wire_limit_test.go b/protocol/sideband64k/encoder_set_max_data_cannot_exceed_wire_limit_test.go new file mode 100644 index 00000000..2edb609a --- /dev/null +++ b/protocol/sideband64k/encoder_set_max_data_cannot_exceed_wire_limit_test.go @@ -0,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) + } +} diff --git a/protocol/sideband64k/encoder_writes_frames_test.go b/protocol/sideband64k/encoder_writes_frames_test.go new file mode 100644 index 00000000..19fff437 --- /dev/null +++ b/protocol/sideband64k/encoder_writes_frames_test.go @@ -0,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) + } +} diff --git a/protocol/sideband64k/errors.go b/protocol/sideband64k/errors.go new file mode 100644 index 00000000..44e7c165 --- /dev/null +++ b/protocol/sideband64k/errors.go @@ -0,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 "" + } + + if e.Reason == "" { + return "sideband64k: protocol error" + } + + return "sideband64k: protocol error: " + e.Reason +} diff --git a/protocol/sideband64k/frame.go b/protocol/sideband64k/frame.go new file mode 100644 index 00000000..1335a8e3 --- /dev/null +++ b/protocol/sideband64k/frame.go @@ -0,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 +} diff --git a/protocol/sideband64k/frame_type.go b/protocol/sideband64k/frame_type.go new file mode 100644 index 00000000..052d8b10 --- /dev/null +++ b/protocol/sideband64k/frame_type.go @@ -0,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 +) diff --git a/protocol/sideband64k/helpers_test.go b/protocol/sideband64k/helpers_test.go new file mode 100644 index 00000000..f9b2608f --- /dev/null +++ b/protocol/sideband64k/helpers_test.go @@ -0,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 +} diff --git a/protocol/v0v1/server/frame.go b/protocol/v0v1/server/frame.go index 383284d7..3cccba08 100644 --- 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 diff --git a/protocol/v0v1/server/receivepack/parse_test.go b/protocol/v0v1/server/receivepack/parse_test.go index f304ac44..40c67c98 100644 --- a/protocol/v0v1/server/receivepack/parse_test.go +++ b/protocol/v0v1/server/receivepack/parse_test.go @@ -5,7 +5,7 @@ import ( "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" diff --git a/protocol/v0v1/server/receivepack/report_status.go b/protocol/v0v1/server/receivepack/report_status.go index 6649ee88..f6ac4985 100644 --- a/protocol/v0v1/server/receivepack/report_status.go +++ b/protocol/v0v1/server/receivepack/report_status.go @@ -3,7 +3,7 @@ package receivepack import ( "fmt" - "codeberg.org/lindenii/furgit/format/pktline" + "codeberg.org/lindenii/furgit/protocol/pktline" ) // WriteReportStatus writes one classic report-status response. diff --git a/protocol/v0v1/server/receivepack/report_status_test.go b/protocol/v0v1/server/receivepack/report_status_test.go index 48420fc8..a81d45d0 100644 --- a/protocol/v0v1/server/receivepack/report_status_test.go +++ b/protocol/v0v1/server/receivepack/report_status_test.go @@ -6,8 +6,8 @@ import ( "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" diff --git a/protocol/v0v1/server/session.go b/protocol/v0v1/server/session.go index 7e107f53..d3267d10 100644 --- a/protocol/v0v1/server/session.go +++ b/protocol/v0v1/server/session.go @@ -3,8 +3,8 @@ package server 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" ) diff --git a/receivepack/int_test.go b/receivepack/int_test.go index b144c387..4db020a4 100644 --- a/receivepack/int_test.go +++ b/receivepack/int_test.go @@ -9,8 +9,8 @@ import ( "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" diff --git a/receivepack/receivepack.go b/receivepack/receivepack.go index aeec9370..8103ad1f 100644 --- a/receivepack/receivepack.go +++ b/receivepack/receivepack.go @@ -4,7 +4,7 @@ import ( "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" -- cgit v1.3.1-10-gc9f91