From a041d523de389b65b98a5373a8034041db2a8d83 Mon Sep 17 00:00:00 2001 From: Runxi Yu Date: Thu, 2 Apr 2026 06:23:30 +0000 Subject: *: Remove --- network/doc.go | 5 - network/protocol/doc.go | 2 - network/protocol/pktline/append.go | 39 - .../append_data_preserves_dst_on_error_test.go | 25 - network/protocol/pktline/append_helpers_test.go | 24 - network/protocol/pktline/chunk_writer.go | 74 -- .../chunk_writer_write_and_read_from_test.go | 60 -- network/protocol/pktline/constants.go | 12 - network/protocol/pktline/decoder.go | 191 ---- .../pktline/decoder_data_control_and_0004_test.go | 60 -- .../protocol/pktline/decoder_invalid_0003_test.go | 20 - network/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 - .../pktline/decoder_unexpected_eof_test.go | 21 - network/protocol/pktline/doc.go | 2 - .../protocol/pktline/encode_length_header_test.go | 28 - network/protocol/pktline/encoder.go | 143 --- .../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 - network/protocol/pktline/errors.go | 31 - network/protocol/pktline/frame.go | 10 - network/protocol/pktline/header.go | 57 - .../protocol/pktline/parse_length_header_test.go | 26 - network/protocol/pktline/type.go | 15 - network/protocol/sideband64k/append.go | 40 - .../protocol/sideband64k/append_helpers_test.go | 30 - .../append_preserves_dst_on_error_test.go | 34 - network/protocol/sideband64k/band.go | 13 - network/protocol/sideband64k/chunk_writer.go | 73 -- .../chunk_writer_write_and_read_from_test.go | 60 -- network/protocol/sideband64k/constants.go | 10 - network/protocol/sideband64k/decoder.go | 162 --- .../decoder_data_control_and_keepalive_test.go | 78 -- .../sideband64k/decoder_invalid_band_test.go | 20 - .../decoder_invalid_empty_payload_test.go | 20 - .../sideband64k/decoder_malformed_pktline_test.go | 32 - .../sideband64k/decoder_partial_read_test.go | 32 - network/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 - network/protocol/sideband64k/doc.go | 2 - network/protocol/sideband64k/encoder.go | 103 -- .../encoder_buffered_flush_behavior_test.go | 59 -- .../sideband64k/encoder_partial_write_test.go | 46 - ...r_set_max_data_cannot_exceed_wire_limit_test.go | 23 - .../sideband64k/encoder_writes_frames_test.go | 58 -- network/protocol/sideband64k/errors.go | 27 - network/protocol/sideband64k/frame.go | 12 - network/protocol/sideband64k/frame_type.go | 19 - network/protocol/sideband64k/helpers_test.go | 46 - network/protocol/v0v1/doc.go | 2 - network/protocol/v0v1/server/advertise.go | 53 - network/protocol/v0v1/server/advertise_test.go | 101 -- network/protocol/v0v1/server/advertised_ref.go | 22 - network/protocol/v0v1/server/doc.go | 2 - network/protocol/v0v1/server/errors.go | 18 - network/protocol/v0v1/server/frame.go | 20 - network/protocol/v0v1/server/helpers.go | 29 - network/protocol/v0v1/server/helpers_test.go | 28 - .../v0v1/server/receivepack/capabilities.go | 192 ---- network/protocol/v0v1/server/receivepack/doc.go | 2 - network/protocol/v0v1/server/receivepack/errors.go | 11 - .../v0v1/server/receivepack/helpers_test.go | 28 - .../protocol/v0v1/server/receivepack/parse_test.go | 255 ----- .../v0v1/server/receivepack/report_status.go | 185 ---- .../v0v1/server/receivepack/report_status_test.go | 293 ------ .../protocol/v0v1/server/receivepack/session.go | 303 ------ network/protocol/v0v1/server/receivepack/types.go | 45 - network/protocol/v0v1/server/session.go | 142 --- network/protocol/v0v1/server/version.go | 12 - network/receivepack/advertise.go | 57 - network/receivepack/capabilities_defaults.go | 17 - network/receivepack/commands.go | 19 - network/receivepack/doc.go | 3 - network/receivepack/errors.go | 15 - network/receivepack/hook.go | 97 -- network/receivepack/hooks/chain.go | 51 - network/receivepack/hooks/doc.go | 2 - network/receivepack/hooks/reject_force_push.go | 69 -- network/receivepack/int_test.go | 1095 -------------------- network/receivepack/options.go | 71 -- network/receivepack/receivepack.go | 139 --- network/receivepack/results.go | 26 - network/receivepack/service/apply.go | 137 --- network/receivepack/service/command.go | 26 - network/receivepack/service/command_result.go | 13 - network/receivepack/service/doc.go | 6 - network/receivepack/service/execute.go | 120 --- network/receivepack/service/hook.go | 48 - network/receivepack/service/hook_apply.go | 31 - network/receivepack/service/ingest_quarantine.go | 81 -- network/receivepack/service/options.go | 31 - network/receivepack/service/request.go | 15 - network/receivepack/service/result.go | 9 - network/receivepack/service/run_hook.go | 93 -- network/receivepack/service/service.go | 13 - network/receivepack/service/service_test.go | 129 --- network/receivepack/service/update.go | 11 - network/receivepack/version.go | 35 - 104 files changed, 6544 deletions(-) delete mode 100644 network/doc.go delete mode 100644 network/protocol/doc.go delete mode 100644 network/protocol/pktline/append.go delete mode 100644 network/protocol/pktline/append_data_preserves_dst_on_error_test.go delete mode 100644 network/protocol/pktline/append_helpers_test.go delete mode 100644 network/protocol/pktline/chunk_writer.go delete mode 100644 network/protocol/pktline/chunk_writer_write_and_read_from_test.go delete mode 100644 network/protocol/pktline/constants.go delete mode 100644 network/protocol/pktline/decoder.go delete mode 100644 network/protocol/pktline/decoder_data_control_and_0004_test.go delete mode 100644 network/protocol/pktline/decoder_invalid_0003_test.go delete mode 100644 network/protocol/pktline/decoder_peek_test.go delete mode 100644 network/protocol/pktline/decoder_rejects_over_maximum_length_test.go delete mode 100644 network/protocol/pktline/decoder_resync_after_over_max_data_test.go delete mode 100644 network/protocol/pktline/decoder_resync_after_over_wire_max_test.go delete mode 100644 network/protocol/pktline/decoder_unexpected_eof_test.go delete mode 100644 network/protocol/pktline/doc.go delete mode 100644 network/protocol/pktline/encode_length_header_test.go delete mode 100644 network/protocol/pktline/encoder.go delete mode 100644 network/protocol/pktline/encoder_buffered_flush_and_f_flush_test.go delete mode 100644 network/protocol/pktline/encoder_buffered_flush_behavior_test.go delete mode 100644 network/protocol/pktline/encoder_set_max_data_cannot_exceed_wire_limit_test.go delete mode 100644 network/protocol/pktline/encoder_writes_frames_test.go delete mode 100644 network/protocol/pktline/errors.go delete mode 100644 network/protocol/pktline/frame.go delete mode 100644 network/protocol/pktline/header.go delete mode 100644 network/protocol/pktline/parse_length_header_test.go delete mode 100644 network/protocol/pktline/type.go delete mode 100644 network/protocol/sideband64k/append.go delete mode 100644 network/protocol/sideband64k/append_helpers_test.go delete mode 100644 network/protocol/sideband64k/append_preserves_dst_on_error_test.go delete mode 100644 network/protocol/sideband64k/band.go delete mode 100644 network/protocol/sideband64k/chunk_writer.go delete mode 100644 network/protocol/sideband64k/chunk_writer_write_and_read_from_test.go delete mode 100644 network/protocol/sideband64k/constants.go delete mode 100644 network/protocol/sideband64k/decoder.go delete mode 100644 network/protocol/sideband64k/decoder_data_control_and_keepalive_test.go delete mode 100644 network/protocol/sideband64k/decoder_invalid_band_test.go delete mode 100644 network/protocol/sideband64k/decoder_invalid_empty_payload_test.go delete mode 100644 network/protocol/sideband64k/decoder_malformed_pktline_test.go delete mode 100644 network/protocol/sideband64k/decoder_partial_read_test.go delete mode 100644 network/protocol/sideband64k/decoder_peek_test.go delete mode 100644 network/protocol/sideband64k/decoder_resync_after_over_max_data_test.go delete mode 100644 network/protocol/sideband64k/decoder_resync_after_over_wire_max_test.go delete mode 100644 network/protocol/sideband64k/decoder_unexpected_eof_test.go delete mode 100644 network/protocol/sideband64k/doc.go delete mode 100644 network/protocol/sideband64k/encoder.go delete mode 100644 network/protocol/sideband64k/encoder_buffered_flush_behavior_test.go delete mode 100644 network/protocol/sideband64k/encoder_partial_write_test.go delete mode 100644 network/protocol/sideband64k/encoder_set_max_data_cannot_exceed_wire_limit_test.go delete mode 100644 network/protocol/sideband64k/encoder_writes_frames_test.go delete mode 100644 network/protocol/sideband64k/errors.go delete mode 100644 network/protocol/sideband64k/frame.go delete mode 100644 network/protocol/sideband64k/frame_type.go delete mode 100644 network/protocol/sideband64k/helpers_test.go delete mode 100644 network/protocol/v0v1/doc.go delete mode 100644 network/protocol/v0v1/server/advertise.go delete mode 100644 network/protocol/v0v1/server/advertise_test.go delete mode 100644 network/protocol/v0v1/server/advertised_ref.go delete mode 100644 network/protocol/v0v1/server/doc.go delete mode 100644 network/protocol/v0v1/server/errors.go delete mode 100644 network/protocol/v0v1/server/frame.go delete mode 100644 network/protocol/v0v1/server/helpers.go delete mode 100644 network/protocol/v0v1/server/helpers_test.go delete mode 100644 network/protocol/v0v1/server/receivepack/capabilities.go delete mode 100644 network/protocol/v0v1/server/receivepack/doc.go delete mode 100644 network/protocol/v0v1/server/receivepack/errors.go delete mode 100644 network/protocol/v0v1/server/receivepack/helpers_test.go delete mode 100644 network/protocol/v0v1/server/receivepack/parse_test.go delete mode 100644 network/protocol/v0v1/server/receivepack/report_status.go delete mode 100644 network/protocol/v0v1/server/receivepack/report_status_test.go delete mode 100644 network/protocol/v0v1/server/receivepack/session.go delete mode 100644 network/protocol/v0v1/server/receivepack/types.go delete mode 100644 network/protocol/v0v1/server/session.go delete mode 100644 network/protocol/v0v1/server/version.go delete mode 100644 network/receivepack/advertise.go delete mode 100644 network/receivepack/capabilities_defaults.go delete mode 100644 network/receivepack/commands.go delete mode 100644 network/receivepack/doc.go delete mode 100644 network/receivepack/errors.go delete mode 100644 network/receivepack/hook.go delete mode 100644 network/receivepack/hooks/chain.go delete mode 100644 network/receivepack/hooks/doc.go delete mode 100644 network/receivepack/hooks/reject_force_push.go delete mode 100644 network/receivepack/int_test.go delete mode 100644 network/receivepack/options.go delete mode 100644 network/receivepack/receivepack.go delete mode 100644 network/receivepack/results.go delete mode 100644 network/receivepack/service/apply.go delete mode 100644 network/receivepack/service/command.go delete mode 100644 network/receivepack/service/command_result.go delete mode 100644 network/receivepack/service/doc.go delete mode 100644 network/receivepack/service/execute.go delete mode 100644 network/receivepack/service/hook.go delete mode 100644 network/receivepack/service/hook_apply.go delete mode 100644 network/receivepack/service/ingest_quarantine.go delete mode 100644 network/receivepack/service/options.go delete mode 100644 network/receivepack/service/request.go delete mode 100644 network/receivepack/service/result.go delete mode 100644 network/receivepack/service/run_hook.go delete mode 100644 network/receivepack/service/service.go delete mode 100644 network/receivepack/service/service_test.go delete mode 100644 network/receivepack/service/update.go delete mode 100644 network/receivepack/version.go (limited to 'network') diff --git a/network/doc.go b/network/doc.go deleted file mode 100644 index d964997b..00000000 --- a/network/doc.go +++ /dev/null @@ -1,5 +0,0 @@ -// Package network encapsulates network-facing Git packages. -// -// These packages implement wire formats, protocol framing, and application -// services built on top of them. -package network diff --git a/network/protocol/doc.go b/network/protocol/doc.go deleted file mode 100644 index d1e00447..00000000 --- a/network/protocol/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package protocol encapsulates network protocol implementations. -package protocol diff --git a/network/protocol/pktline/append.go b/network/protocol/pktline/append.go deleted file mode 100644 index 9425e58e..00000000 --- a/network/protocol/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/network/protocol/pktline/append_data_preserves_dst_on_error_test.go b/network/protocol/pktline/append_data_preserves_dst_on_error_test.go deleted file mode 100644 index d127fb39..00000000 --- a/network/protocol/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/network/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/network/protocol/pktline/append_helpers_test.go b/network/protocol/pktline/append_helpers_test.go deleted file mode 100644 index 259ada16..00000000 --- a/network/protocol/pktline/append_helpers_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package pktline_test - -import ( - "testing" - - "codeberg.org/lindenii/furgit/network/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/network/protocol/pktline/chunk_writer.go b/network/protocol/pktline/chunk_writer.go deleted file mode 100644 index f009978e..00000000 --- a/network/protocol/pktline/chunk_writer.go +++ /dev/null @@ -1,74 +0,0 @@ -package pktline - -import "io" - -// ChunkWriter packetizes arbitrary stream bytes into data pkt-lines. -// It never writes control packets automatically. -// -// Labels: MT-Unsafe. -type ChunkWriter struct { - enc *Encoder -} - -// NewChunkWriter creates a chunking adapter over enc. -// -// Labels: Deps-Borrowed, Life-Parent. -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 - } - } -} - -// Flush flushes buffered output in the underlying transport. -func (cw *ChunkWriter) Flush() error { - return cw.enc.Flush() -} diff --git a/network/protocol/pktline/chunk_writer_write_and_read_from_test.go b/network/protocol/pktline/chunk_writer_write_and_read_from_test.go deleted file mode 100644 index efe19e23..00000000 --- a/network/protocol/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/network/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.Flush() - if err != nil { - t.Fatalf("Flush: %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.Flush() - if err != nil { - t.Fatalf("Flush: %v", err) - } - - if got, want := out.String(), "0007wxy0005z"; got != want { - t.Fatalf("got %q, want %q", got, want) - } -} diff --git a/network/protocol/pktline/constants.go b/network/protocol/pktline/constants.go deleted file mode 100644 index 811eb3c6..00000000 --- a/network/protocol/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/network/protocol/pktline/decoder.go b/network/protocol/pktline/decoder.go deleted file mode 100644 index 682dd164..00000000 --- a/network/protocol/pktline/decoder.go +++ /dev/null @@ -1,191 +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. -// -// Labels: MT-Unsafe. -type Decoder struct { - r io.Reader - maxData int - opts ReadOptions - - peeked bool - peek Frame - peekErr error -} - -// NewDecoder creates a decoder over r. -// -// Labels: Deps-Borrowed, Life-Parent. -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/network/protocol/pktline/decoder_data_control_and_0004_test.go b/network/protocol/pktline/decoder_data_control_and_0004_test.go deleted file mode 100644 index ab92b603..00000000 --- a/network/protocol/pktline/decoder_data_control_and_0004_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package pktline_test - -import ( - "strings" - "testing" - - "codeberg.org/lindenii/furgit/network/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/network/protocol/pktline/decoder_invalid_0003_test.go b/network/protocol/pktline/decoder_invalid_0003_test.go deleted file mode 100644 index 716da3f2..00000000 --- a/network/protocol/pktline/decoder_invalid_0003_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package pktline_test - -import ( - "errors" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/network/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/network/protocol/pktline/decoder_peek_test.go b/network/protocol/pktline/decoder_peek_test.go deleted file mode 100644 index a67da881..00000000 --- a/network/protocol/pktline/decoder_peek_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package pktline_test - -import ( - "strings" - "testing" - - "codeberg.org/lindenii/furgit/network/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/network/protocol/pktline/decoder_rejects_over_maximum_length_test.go b/network/protocol/pktline/decoder_rejects_over_maximum_length_test.go deleted file mode 100644 index 357bfc36..00000000 --- a/network/protocol/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/network/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/network/protocol/pktline/decoder_resync_after_over_max_data_test.go b/network/protocol/pktline/decoder_resync_after_over_max_data_test.go deleted file mode 100644 index 42a7572e..00000000 --- a/network/protocol/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/network/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.Flush() - if err != nil { - t.Fatalf("Flush: %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/network/protocol/pktline/decoder_resync_after_over_wire_max_test.go b/network/protocol/pktline/decoder_resync_after_over_wire_max_test.go deleted file mode 100644 index 9413823b..00000000 --- a/network/protocol/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/network/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/network/protocol/pktline/decoder_unexpected_eof_test.go b/network/protocol/pktline/decoder_unexpected_eof_test.go deleted file mode 100644 index e1bf4457..00000000 --- a/network/protocol/pktline/decoder_unexpected_eof_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package pktline_test - -import ( - "errors" - "io" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/network/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/network/protocol/pktline/doc.go b/network/protocol/pktline/doc.go deleted file mode 100644 index 3f7cca89..00000000 --- a/network/protocol/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/network/protocol/pktline/encode_length_header_test.go b/network/protocol/pktline/encode_length_header_test.go deleted file mode 100644 index 38a980f0..00000000 --- a/network/protocol/pktline/encode_length_header_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package pktline_test - -import ( - "errors" - "testing" - - "codeberg.org/lindenii/furgit/network/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/network/protocol/pktline/encoder.go b/network/protocol/pktline/encoder.go deleted file mode 100644 index a7b17108..00000000 --- a/network/protocol/pktline/encoder.go +++ /dev/null @@ -1,143 +0,0 @@ -package pktline - -import ( - "fmt" - "io" - - "codeberg.org/lindenii/furgit/common/iowrap" -) - -// 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. -// -// Labels: MT-Unsafe. -type Encoder struct { - w iowrap.WriteFlusher - maxData int -} - -// NewEncoder creates an encoder over w. -// -// Labels: Deps-Borrowed, Life-Parent. -func NewEncoder(w iowrap.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 -} - -// WriteFlushPacket writes control frame 0000 (flush-pkt). -func (e *Encoder) WriteFlushPacket() error { - return e.writeControl(0) -} - -// WriteDelimPacket writes control frame 0001 (delim-pkt). -func (e *Encoder) WriteDelimPacket() error { - return e.writeControl(1) -} - -// WriteResponseEndPacket writes control frame 0002 (response-end-pkt). -func (e *Encoder) WriteResponseEndPacket() error { - return e.writeControl(2) -} - -// Flush flushes buffered output in the underlying transport. -// -// Flush does not emit any pkt-line control frame. -func (e *Encoder) Flush() error { - return e.w.Flush() -} - -// WriteFlushPacketAndFlush writes a flush-pkt (0000) then flushes transport I/O. -func (e *Encoder) WriteFlushPacketAndFlush() error { - err := e.WriteFlushPacket() - if err != nil { - return err - } - - return e.Flush() -} - -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/network/protocol/pktline/encoder_buffered_flush_and_f_flush_test.go b/network/protocol/pktline/encoder_buffered_flush_and_f_flush_test.go deleted file mode 100644 index d0f26878..00000000 --- a/network/protocol/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/network/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.Flush() - if err != nil { - t.Fatalf("Flush: %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.WriteFlushPacketAndFlush() - if err != nil { - t.Fatalf("WriteFlushPacketAndFlush: %v", err) - } - - if out.String() != "0000" { - t.Fatalf("got %q, want %q", out.String(), "0000") - } -} diff --git a/network/protocol/pktline/encoder_buffered_flush_behavior_test.go b/network/protocol/pktline/encoder_buffered_flush_behavior_test.go deleted file mode 100644 index b6d14b4b..00000000 --- a/network/protocol/pktline/encoder_buffered_flush_behavior_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package pktline_test - -import ( - "bufio" - "bytes" - "testing" - - "codeberg.org/lindenii/furgit/network/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.WriteFlushPacket() - if err != nil { - t.Fatalf("WriteFlushPacket: %v", err) - } - - if out.Len() != 0 { - t.Fatalf("WriteFlushPacket should not flush I/O, got %q", out.String()) - } - - err = enc.Flush() - if err != nil { - t.Fatalf("Flush: %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.WriteFlushPacket() - if err != nil { - t.Fatalf("WriteFlushPacket: %v", err) - } - - if out.Len() != 0 { - t.Fatalf("WriteFlushPacket should not flush I/O, got %q", out.String()) - } - - err = enc.Flush() - if err != nil { - t.Fatalf("Flush: %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.WriteFlushPacketAndFlush() - if err != nil { - t.Fatalf("WriteFlushPacketAndFlush: %v", err) - } - - if got, want := out.String(), "0006yo0000"; got != want { - t.Fatalf("got %q, want %q", got, want) - } -} diff --git a/network/protocol/pktline/encoder_set_max_data_cannot_exceed_wire_limit_test.go b/network/protocol/pktline/encoder_set_max_data_cannot_exceed_wire_limit_test.go deleted file mode 100644 index d73baa4f..00000000 --- a/network/protocol/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/network/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/network/protocol/pktline/encoder_writes_frames_test.go b/network/protocol/pktline/encoder_writes_frames_test.go deleted file mode 100644 index 1922b277..00000000 --- a/network/protocol/pktline/encoder_writes_frames_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package pktline_test - -import ( - "bufio" - "bytes" - "testing" - - "codeberg.org/lindenii/furgit/network/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.WriteFlushPacket() - if err != nil { - t.Fatalf("WriteFlushPacket: %v", err) - } - - err = enc.WriteDelimPacket() - if err != nil { - t.Fatalf("WriteDelimPacket: %v", err) - } - - err = enc.WriteResponseEndPacket() - if err != nil { - t.Fatalf("WriteResponseEndPacket: %v", err) - } - - err = enc.Flush() - if err != nil { - t.Fatalf("Flush: %v", err) - } - - got := b.String() - - want := "0006hi000000010002" - if got != want { - t.Fatalf("got %q, want %q", got, want) - } -} diff --git a/network/protocol/pktline/errors.go b/network/protocol/pktline/errors.go deleted file mode 100644 index 866ff467..00000000 --- a/network/protocol/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/network/protocol/pktline/frame.go b/network/protocol/pktline/frame.go deleted file mode 100644 index a1cf708c..00000000 --- a/network/protocol/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/network/protocol/pktline/header.go b/network/protocol/pktline/header.go deleted file mode 100644 index 41e50e04..00000000 --- a/network/protocol/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/network/protocol/pktline/parse_length_header_test.go b/network/protocol/pktline/parse_length_header_test.go deleted file mode 100644 index b1a4c1e5..00000000 --- a/network/protocol/pktline/parse_length_header_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package pktline_test - -import ( - "errors" - "testing" - - "codeberg.org/lindenii/furgit/network/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/network/protocol/pktline/type.go b/network/protocol/pktline/type.go deleted file mode 100644 index 641d1c6c..00000000 --- a/network/protocol/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/network/protocol/sideband64k/append.go b/network/protocol/sideband64k/append.go deleted file mode 100644 index db6527f8..00000000 --- a/network/protocol/sideband64k/append.go +++ /dev/null @@ -1,40 +0,0 @@ -package sideband64k - -import ( - "fmt" - - "codeberg.org/lindenii/furgit/network/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/network/protocol/sideband64k/append_helpers_test.go b/network/protocol/sideband64k/append_helpers_test.go deleted file mode 100644 index 03196c38..00000000 --- a/network/protocol/sideband64k/append_helpers_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package sideband64k_test - -import ( - "testing" - - "codeberg.org/lindenii/furgit/network/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/network/protocol/sideband64k/append_preserves_dst_on_error_test.go b/network/protocol/sideband64k/append_preserves_dst_on_error_test.go deleted file mode 100644 index 6fed4e4a..00000000 --- a/network/protocol/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/network/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/network/protocol/sideband64k/band.go b/network/protocol/sideband64k/band.go deleted file mode 100644 index 73c61fd8..00000000 --- a/network/protocol/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/network/protocol/sideband64k/chunk_writer.go b/network/protocol/sideband64k/chunk_writer.go deleted file mode 100644 index 78c66edf..00000000 --- a/network/protocol/sideband64k/chunk_writer.go +++ /dev/null @@ -1,73 +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. -// -// Labels: MT-Unsafe. -type ChunkWriter struct { - enc *Encoder - band Band -} - -// NewChunkWriter creates a chunking adapter over enc for one band. -// -// Labels: Deps-Borrowed, Life-Parent. -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 - } - } -} - -// Flush flushes buffered output in the underlying transport. -func (cw *ChunkWriter) Flush() error { - return cw.enc.Flush() -} diff --git a/network/protocol/sideband64k/chunk_writer_write_and_read_from_test.go b/network/protocol/sideband64k/chunk_writer_write_and_read_from_test.go deleted file mode 100644 index ef2b0fff..00000000 --- a/network/protocol/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/network/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.Flush() - if err != nil { - t.Fatalf("Flush: %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.Flush() - if err != nil { - t.Fatalf("Flush: %v", err) - } - - if got, want := out.String(), "0008\x02wxy0006\x02z"; got != want { - t.Fatalf("got %q, want %q", got, want) - } -} diff --git a/network/protocol/sideband64k/constants.go b/network/protocol/sideband64k/constants.go deleted file mode 100644 index 2a6a2e47..00000000 --- a/network/protocol/sideband64k/constants.go +++ /dev/null @@ -1,10 +0,0 @@ -package sideband64k - -import "codeberg.org/lindenii/furgit/network/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/network/protocol/sideband64k/decoder.go b/network/protocol/sideband64k/decoder.go deleted file mode 100644 index e34f5d12..00000000 --- a/network/protocol/sideband64k/decoder.go +++ /dev/null @@ -1,162 +0,0 @@ -package sideband64k - -import ( - "fmt" - "io" - - "codeberg.org/lindenii/furgit/network/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. -// -// Labels: MT-Unsafe. -type Decoder struct { - dec *pktline.Decoder - maxData int - opts ReadOptions - - peeked bool - peek Frame - peekErr error -} - -// NewDecoder creates a decoder over r. -// -// Labels: Deps-Borrowed, Life-Parent. -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/network/protocol/sideband64k/decoder_data_control_and_keepalive_test.go b/network/protocol/sideband64k/decoder_data_control_and_keepalive_test.go deleted file mode 100644 index 9103c492..00000000 --- a/network/protocol/sideband64k/decoder_data_control_and_keepalive_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package sideband64k_test - -import ( - "strings" - "testing" - - "codeberg.org/lindenii/furgit/network/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/network/protocol/sideband64k/decoder_invalid_band_test.go b/network/protocol/sideband64k/decoder_invalid_band_test.go deleted file mode 100644 index a4bc11a9..00000000 --- a/network/protocol/sideband64k/decoder_invalid_band_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package sideband64k_test - -import ( - "errors" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/network/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/network/protocol/sideband64k/decoder_invalid_empty_payload_test.go b/network/protocol/sideband64k/decoder_invalid_empty_payload_test.go deleted file mode 100644 index df9faa71..00000000 --- a/network/protocol/sideband64k/decoder_invalid_empty_payload_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package sideband64k_test - -import ( - "errors" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/network/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/network/protocol/sideband64k/decoder_malformed_pktline_test.go b/network/protocol/sideband64k/decoder_malformed_pktline_test.go deleted file mode 100644 index 5e4e4551..00000000 --- a/network/protocol/sideband64k/decoder_malformed_pktline_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package sideband64k_test - -import ( - "errors" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/network/protocol/pktline" - "codeberg.org/lindenii/furgit/network/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/network/protocol/sideband64k/decoder_partial_read_test.go b/network/protocol/sideband64k/decoder_partial_read_test.go deleted file mode 100644 index 3f103787..00000000 --- a/network/protocol/sideband64k/decoder_partial_read_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package sideband64k_test - -import ( - "testing" - - "codeberg.org/lindenii/furgit/network/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/network/protocol/sideband64k/decoder_peek_test.go b/network/protocol/sideband64k/decoder_peek_test.go deleted file mode 100644 index 31397762..00000000 --- a/network/protocol/sideband64k/decoder_peek_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package sideband64k_test - -import ( - "strings" - "testing" - - "codeberg.org/lindenii/furgit/network/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/network/protocol/sideband64k/decoder_resync_after_over_max_data_test.go b/network/protocol/sideband64k/decoder_resync_after_over_max_data_test.go deleted file mode 100644 index b0ae600a..00000000 --- a/network/protocol/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/network/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.Flush() - if err != nil { - t.Fatalf("Flush: %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/network/protocol/sideband64k/decoder_resync_after_over_wire_max_test.go b/network/protocol/sideband64k/decoder_resync_after_over_wire_max_test.go deleted file mode 100644 index 73966925..00000000 --- a/network/protocol/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/network/protocol/pktline" - "codeberg.org/lindenii/furgit/network/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/network/protocol/sideband64k/decoder_unexpected_eof_test.go b/network/protocol/sideband64k/decoder_unexpected_eof_test.go deleted file mode 100644 index d9d71fb9..00000000 --- a/network/protocol/sideband64k/decoder_unexpected_eof_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package sideband64k_test - -import ( - "errors" - "io" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/network/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/network/protocol/sideband64k/doc.go b/network/protocol/sideband64k/doc.go deleted file mode 100644 index 55c33650..00000000 --- a/network/protocol/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/network/protocol/sideband64k/encoder.go b/network/protocol/sideband64k/encoder.go deleted file mode 100644 index 6cdef38a..00000000 --- a/network/protocol/sideband64k/encoder.go +++ /dev/null @@ -1,103 +0,0 @@ -package sideband64k - -import ( - "fmt" - - "codeberg.org/lindenii/furgit/common/iowrap" - "codeberg.org/lindenii/furgit/network/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. -// -// Labels: MT-Unsafe. -type Encoder struct { - enc *pktline.Encoder - maxData int -} - -// NewEncoder creates an encoder over w. -// -// Labels: Deps-Borrowed, Life-Parent. -func NewEncoder(w iowrap.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) -} - -// WriteFlushPacket writes control frame 0000 (flush-pkt). -func (e *Encoder) WriteFlushPacket() error { - return e.enc.WriteFlushPacket() -} - -// WriteDelimPacket writes control frame 0001 (delim-pkt). -func (e *Encoder) WriteDelimPacket() error { - return e.enc.WriteDelimPacket() -} - -// WriteResponseEndPacket writes control frame 0002 (response-end-pkt). -func (e *Encoder) WriteResponseEndPacket() error { - return e.enc.WriteResponseEndPacket() -} - -// Flush flushes buffered output in the underlying transport. -func (e *Encoder) Flush() error { - return e.enc.Flush() -} - -// WriteFlushPacketAndFlush writes a flush-pkt (0000) then flushes transport I/O. -func (e *Encoder) WriteFlushPacketAndFlush() error { - return e.enc.WriteFlushPacketAndFlush() -} - -func (e *Encoder) effectiveMaxData() int { - return effectiveMaxData(e.maxData) -} diff --git a/network/protocol/sideband64k/encoder_buffered_flush_behavior_test.go b/network/protocol/sideband64k/encoder_buffered_flush_behavior_test.go deleted file mode 100644 index 83103ea3..00000000 --- a/network/protocol/sideband64k/encoder_buffered_flush_behavior_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package sideband64k_test - -import ( - "bufio" - "bytes" - "testing" - - "codeberg.org/lindenii/furgit/network/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.WriteFlushPacket() - if err != nil { - t.Fatalf("WriteFlushPacket: %v", err) - } - - if out.Len() != 0 { - t.Fatalf("WriteFlushPacket should not flush I/O, got %q", out.String()) - } - - err = enc.Flush() - if err != nil { - t.Fatalf("Flush: %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.WriteFlushPacketAndFlush() - if err != nil { - t.Fatalf("WriteFlushPacketAndFlush: %v", err) - } - - if got, want := out.String(), "0007\x01yo0000"; got != want { - t.Fatalf("got %q, want %q", got, want) - } -} diff --git a/network/protocol/sideband64k/encoder_partial_write_test.go b/network/protocol/sideband64k/encoder_partial_write_test.go deleted file mode 100644 index 97c8f762..00000000 --- a/network/protocol/sideband64k/encoder_partial_write_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package sideband64k_test - -import ( - "errors" - "io" - "testing" - - "codeberg.org/lindenii/furgit/network/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.WriteFlushPacketAndFlush() - if err != nil { - t.Fatalf("WriteFlushPacketAndFlush: %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/network/protocol/sideband64k/encoder_set_max_data_cannot_exceed_wire_limit_test.go b/network/protocol/sideband64k/encoder_set_max_data_cannot_exceed_wire_limit_test.go deleted file mode 100644 index 2bfcf073..00000000 --- a/network/protocol/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/network/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/network/protocol/sideband64k/encoder_writes_frames_test.go b/network/protocol/sideband64k/encoder_writes_frames_test.go deleted file mode 100644 index 85fe5845..00000000 --- a/network/protocol/sideband64k/encoder_writes_frames_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package sideband64k_test - -import ( - "bufio" - "bytes" - "testing" - - "codeberg.org/lindenii/furgit/network/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.WriteFlushPacket() - if err != nil { - t.Fatalf("WriteFlushPacket: %v", err) - } - - err = enc.WriteDelimPacket() - if err != nil { - t.Fatalf("WriteDelimPacket: %v", err) - } - - err = enc.WriteResponseEndPacket() - if err != nil { - t.Fatalf("WriteResponseEndPacket: %v", err) - } - - err = enc.Flush() - if err != nil { - t.Fatalf("Flush: %v", err) - } - - want := "0007\x01hi0007\x02ok0007\x03no000000010002" - if got := b.String(); got != want { - t.Fatalf("got %q, want %q", got, want) - } -} diff --git a/network/protocol/sideband64k/errors.go b/network/protocol/sideband64k/errors.go deleted file mode 100644 index 44e7c165..00000000 --- a/network/protocol/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/network/protocol/sideband64k/frame.go b/network/protocol/sideband64k/frame.go deleted file mode 100644 index 1335a8e3..00000000 --- a/network/protocol/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/network/protocol/sideband64k/frame_type.go b/network/protocol/sideband64k/frame_type.go deleted file mode 100644 index 052d8b10..00000000 --- a/network/protocol/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/network/protocol/sideband64k/helpers_test.go b/network/protocol/sideband64k/helpers_test.go deleted file mode 100644 index f9b2608f..00000000 --- a/network/protocol/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/network/protocol/v0v1/doc.go b/network/protocol/v0v1/doc.go deleted file mode 100644 index 2c96ea23..00000000 --- a/network/protocol/v0v1/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package v0v1 provides common constants and routines for the V0 and V1 protocols. -package v0v1 diff --git a/network/protocol/v0v1/server/advertise.go b/network/protocol/v0v1/server/advertise.go deleted file mode 100644 index 8ec7dfd9..00000000 --- a/network/protocol/v0v1/server/advertise.go +++ /dev/null @@ -1,53 +0,0 @@ -package server - -import ( - "fmt" - "strings" -) - -// AdvertiseRefs writes one server ref advertisement. -func (session *Session) AdvertiseRefs(ad Advertisement, capabilityTokens []string) error { - if session.opts.Version == Version1 { - err := session.enc.WriteData([]byte("version 1\n")) - if err != nil { - return err - } - } - - capList := strings.Join(capabilityTokens, " ") - - refs := sortAdvertisedRefs(ad.Refs) - if len(refs) == 0 { - line := fmt.Sprintf("%s capabilities^{}\x00%s\n", session.opts.Algorithm.Zero(), capList) - - err := session.enc.WriteData([]byte(line)) - if err != nil { - return err - } - - return session.WriteFlushPacket() - } - - for i, entry := range refs { - line := fmt.Sprintf("%s %s", entry.ID, entry.Name) - if i == 0 { - line += "\x00" + capList - } - - err := session.enc.WriteData([]byte(line + "\n")) - if err != nil { - return err - } - - if entry.Peeled != nil { - peeled := fmt.Sprintf("%s %s^{}\n", *entry.Peeled, entry.Name) - - err = session.enc.WriteData([]byte(peeled)) - if err != nil { - return err - } - } - } - - return session.WriteFlushPacket() -} diff --git a/network/protocol/v0v1/server/advertise_test.go b/network/protocol/v0v1/server/advertise_test.go deleted file mode 100644 index 3ad7a725..00000000 --- a/network/protocol/v0v1/server/advertise_test.go +++ /dev/null @@ -1,101 +0,0 @@ -package server_test - -import ( - "strings" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - server "codeberg.org/lindenii/furgit/network/protocol/v0v1/server" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -func TestAdvertiseRefsWritesVersionOneHeadCapsAndPeeledTag(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - headID := mustHexID(t, algo, "1") - tagID := mustHexID(t, algo, "2") - peeledID := mustHexID(t, algo, "3") - mainID := mustHexID(t, algo, "4") - - var out bufferWriteFlusher - - session := server.NewSession( - strings.NewReader(""), - &out, - server.Options{ - Version: server.Version1, - Algorithm: algo, - }, - ) - - err := session.AdvertiseRefs(server.Advertisement{ - Refs: []server.AdvertisedRef{ - {Name: "refs/tags/v1", ID: tagID, Peeled: &peeledID}, - {Name: "HEAD", ID: headID}, - {Name: "refs/heads/main", ID: mainID}, - }, - }, []string{ - "report-status", - "delete-refs", - "object-format=" + algo.String(), - "agent=furgit-test/1", - }) - if err != nil { - t.Fatalf("AdvertiseRefs: %v", err) - } - - got := out.String() - wantParts := []string{ - "000eversion 1\n", - headID.String() + " HEAD\x00report-status delete-refs object-format=" + algo.String() + " agent=furgit-test/1\n", - mainID.String() + " refs/heads/main\n", - tagID.String() + " refs/tags/v1\n", - peeledID.String() + " refs/tags/v1^{}\n", - "0000", - } - - for _, part := range wantParts { - if !strings.Contains(got, part) { - t.Fatalf("advertisement missing %q in %q", part, got) - } - } - }) -} - -func TestAdvertiseRefsWritesNoRefsCapabilitiesLine(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - var out bufferWriteFlusher - - session := server.NewSession( - strings.NewReader(""), - &out, - server.Options{ - Algorithm: algo, - }, - ) - - err := session.AdvertiseRefs(server.Advertisement{}, []string{ - "report-status", - "object-format=" + algo.String(), - }) - if err != nil { - t.Fatalf("AdvertiseRefs: %v", err) - } - - got := out.String() - - want := algo.Zero().String() + " capabilities^{}\x00report-status object-format=" + algo.String() + "\n" - if !strings.Contains(got, want) { - t.Fatalf("unexpected no-refs advertisement %q", got) - } - }) -} diff --git a/network/protocol/v0v1/server/advertised_ref.go b/network/protocol/v0v1/server/advertised_ref.go deleted file mode 100644 index cf6ddcc8..00000000 --- a/network/protocol/v0v1/server/advertised_ref.go +++ /dev/null @@ -1,22 +0,0 @@ -package server - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// AdvertisedRef is one ref entry in one v0/v1 server advertisement. -type AdvertisedRef struct { - // Name is the advertised reference name. It may be HEAD or one full - // reference name. - Name string - // ID is the object ID currently advertised for Name. - ID objectid.ObjectID - // Peeled is the peeled annotated-tag target when available. - // - // If set, advertisement writes one immediate "^{}" line after the - // main entry, matching Git's advertisement rules. - Peeled *objectid.ObjectID -} - -// Advertisement is one server-side ref advertisement. -type Advertisement struct { - Refs []AdvertisedRef -} diff --git a/network/protocol/v0v1/server/doc.go b/network/protocol/v0v1/server/doc.go deleted file mode 100644 index ea0b3f18..00000000 --- a/network/protocol/v0v1/server/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package server implements shared server-side Git protocol v0/v1 framing. -package server diff --git a/network/protocol/v0v1/server/errors.go b/network/protocol/v0v1/server/errors.go deleted file mode 100644 index 6a456234..00000000 --- a/network/protocol/v0v1/server/errors.go +++ /dev/null @@ -1,18 +0,0 @@ -package server - -// ProtocolError reports one malformed or unsupported protocol input. -type ProtocolError struct { - Reason string -} - -// Error returns the formatted error string. -func (err *ProtocolError) Error() string { - return "protocol/v0v1/server: protocol error: " + err.Reason -} - -// ErrUnexpectedPacket reports one unexpected pkt-line control packet. -var ErrUnexpectedPacket = &ProtocolError{Reason: "unexpected control packet"} - -// ErrSideBandNotEnabled reports one attempt to write sideband frames without a -// negotiated side-band-64k session. -var ErrSideBandNotEnabled = &ProtocolError{Reason: "side-band-64k not enabled"} diff --git a/network/protocol/v0v1/server/frame.go b/network/protocol/v0v1/server/frame.go deleted file mode 100644 index ad2a0801..00000000 --- a/network/protocol/v0v1/server/frame.go +++ /dev/null @@ -1,20 +0,0 @@ -package server - -import "codeberg.org/lindenii/furgit/network/protocol/pktline" - -// FrameType identifies one low-level v0/v1 server pkt-line frame type. -type FrameType = pktline.PacketType - -const ( - // FrameData is one data pkt-line. - FrameData = pktline.PacketData - // FrameFlush is one flush-pkt. - FrameFlush = pktline.PacketFlush - // FrameDelim is one delim-pkt. - FrameDelim = pktline.PacketDelim - // FrameResponseEnd is one response-end-pkt. - FrameResponseEnd = pktline.PacketResponseEnd -) - -// Frame is one decoded low-level pkt-line frame. -type Frame = pktline.Frame diff --git a/network/protocol/v0v1/server/helpers.go b/network/protocol/v0v1/server/helpers.go deleted file mode 100644 index 9a62f714..00000000 --- a/network/protocol/v0v1/server/helpers.go +++ /dev/null @@ -1,29 +0,0 @@ -package server - -import ( - "slices" -) - -func sortAdvertisedRefs(refs []AdvertisedRef) []AdvertisedRef { - out := append([]AdvertisedRef(nil), refs...) - slices.SortFunc(out, func(left, right AdvertisedRef) int { - if left.Name == "HEAD" && right.Name != "HEAD" { - return -1 - } - - if left.Name != "HEAD" && right.Name == "HEAD" { - return 1 - } - - switch { - case left.Name < right.Name: - return -1 - case left.Name > right.Name: - return 1 - default: - return 0 - } - }) - - return out -} diff --git a/network/protocol/v0v1/server/helpers_test.go b/network/protocol/v0v1/server/helpers_test.go deleted file mode 100644 index 261bbdc5..00000000 --- a/network/protocol/v0v1/server/helpers_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package server_test - -import ( - "bytes" - "strings" - "testing" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -type bufferWriteFlusher struct { - bytes.Buffer -} - -func (bufferWriteFlusher) Flush() error { - return nil -} - -func mustHexID(tb testing.TB, algo objectid.Algorithm, digit string) objectid.ObjectID { - tb.Helper() - - id, err := objectid.ParseHex(algo, strings.Repeat(digit, algo.HexLen())) - if err != nil { - tb.Fatalf("objectid.ParseHex(%q): %v", strings.Repeat(digit, algo.HexLen()), err) - } - - return id -} diff --git a/network/protocol/v0v1/server/receivepack/capabilities.go b/network/protocol/v0v1/server/receivepack/capabilities.go deleted file mode 100644 index e0ff51a3..00000000 --- a/network/protocol/v0v1/server/receivepack/capabilities.go +++ /dev/null @@ -1,192 +0,0 @@ -package receivepack - -import ( - "fmt" - "slices" - "strings" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// Capabilities describes one receive-pack capability set. -type Capabilities struct { - ReportStatus bool - ReportStatusV2 bool - DeleteRefs bool - SideBand64K bool - Quiet bool - Atomic bool - OfsDelta bool - PushOptions bool - PushCertNonce string - ObjectFormat objectid.Algorithm - SessionID string - Agent string -} - -// Normalize returns one normalized copy of caps. -func (caps Capabilities) Normalize(defaultAlgorithm objectid.Algorithm) Capabilities { - if caps.ObjectFormat == objectid.AlgorithmUnknown { - caps.ObjectFormat = defaultAlgorithm - } - - return caps -} - -// Tokens returns capabilities in Git advertisement order. -func (caps Capabilities) Tokens(defaultAlgorithm objectid.Algorithm) []string { - caps = caps.Normalize(defaultAlgorithm) - - tokens := make([]string, 0, 11) - if caps.ReportStatus { - tokens = append(tokens, "report-status") - } - - if caps.ReportStatusV2 { - tokens = append(tokens, "report-status-v2") - } - - if caps.DeleteRefs { - tokens = append(tokens, "delete-refs") - } - - if caps.SideBand64K { - tokens = append(tokens, "side-band-64k") - } - - if caps.Quiet { - tokens = append(tokens, "quiet") - } - - if caps.Atomic { - tokens = append(tokens, "atomic") - } - - if caps.OfsDelta { - tokens = append(tokens, "ofs-delta") - } - - if caps.PushCertNonce != "" { - tokens = append(tokens, "push-cert="+caps.PushCertNonce) - } - - if caps.PushOptions { - tokens = append(tokens, "push-options") - } - - if caps.SessionID != "" { - tokens = append(tokens, "session-id="+caps.SessionID) - } - - if caps.ObjectFormat != objectid.AlgorithmUnknown { - tokens = append(tokens, "object-format="+caps.ObjectFormat.String()) - } - - if caps.Agent != "" { - tokens = append(tokens, "agent="+caps.Agent) - } - - return tokens -} - -func (caps Capabilities) supportsToken(token string, defaultAlgorithm objectid.Algorithm) bool { - name, value, _ := strings.Cut(token, "=") - - switch name { - case "report-status": - return caps.ReportStatus && value == "" - case "report-status-v2": - return caps.ReportStatusV2 && value == "" - case "delete-refs": - return caps.DeleteRefs && value == "" - case "side-band-64k": - return caps.SideBand64K && value == "" - case "quiet": - return caps.Quiet && value == "" - case "atomic": - return caps.Atomic && value == "" - case "ofs-delta": - return caps.OfsDelta && value == "" - case "push-options": - return caps.PushOptions && value == "" - case "push-cert": - return caps.PushCertNonce != "" && value != "" - case "object-format": - if value == "" { - return false - } - - algo, ok := objectid.ParseAlgorithm(value) - - return ok && algo == caps.Normalize(defaultAlgorithm).ObjectFormat - case "session-id": - return caps.SessionID != "" && value != "" - case "agent": - return caps.Agent != "" && value != "" - default: - return false - } -} - -func parseCapabilityList(s string) ([]string, error) { - s = strings.TrimSuffix(s, "\n") - if s == "" { - return nil, nil - } - - tokens := strings.Fields(s) - if slices.Contains(tokens, "") { - return nil, &ProtocolError{Reason: "empty capability token"} - } - - return tokens, nil -} - -func parseRequestedCapabilities( - tokens []string, - supported Capabilities, - defaultAlgorithm objectid.Algorithm, -) (Capabilities, error) { - var requested Capabilities - - requested.ObjectFormat = defaultAlgorithm - - for _, token := range tokens { - if !supported.supportsToken(token, defaultAlgorithm) { - return Capabilities{}, &ProtocolError{ - Reason: fmt.Sprintf("unsupported capability %q", token), - } - } - - name, value, _ := strings.Cut(token, "=") - switch name { - case "report-status": - requested.ReportStatus = true - case "report-status-v2": - requested.ReportStatusV2 = true - case "delete-refs": - requested.DeleteRefs = true - case "side-band-64k": - requested.SideBand64K = true - case "quiet": - requested.Quiet = true - case "atomic": - requested.Atomic = true - case "ofs-delta": - requested.OfsDelta = true - case "push-options": - requested.PushOptions = true - case "push-cert": - requested.PushCertNonce = value - case "object-format": - algo, _ := objectid.ParseAlgorithm(value) - requested.ObjectFormat = algo - case "session-id": - requested.SessionID = value - case "agent": - requested.Agent = value - } - } - - return requested, nil -} diff --git a/network/protocol/v0v1/server/receivepack/doc.go b/network/protocol/v0v1/server/receivepack/doc.go deleted file mode 100644 index 65793831..00000000 --- a/network/protocol/v0v1/server/receivepack/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package receivepack implements the receive-pack-specific server side of Git protocol v0/v1. -package receivepack diff --git a/network/protocol/v0v1/server/receivepack/errors.go b/network/protocol/v0v1/server/receivepack/errors.go deleted file mode 100644 index d89f8959..00000000 --- a/network/protocol/v0v1/server/receivepack/errors.go +++ /dev/null @@ -1,11 +0,0 @@ -package receivepack - -// ProtocolError reports one malformed or unsupported receive-pack protocol input. -type ProtocolError struct { - Reason string -} - -// Error returns the formatted error string. -func (err *ProtocolError) Error() string { - return "protocol/v0v1/server/receivepack: protocol error: " + err.Reason -} diff --git a/network/protocol/v0v1/server/receivepack/helpers_test.go b/network/protocol/v0v1/server/receivepack/helpers_test.go deleted file mode 100644 index 5db8e6a6..00000000 --- a/network/protocol/v0v1/server/receivepack/helpers_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package receivepack_test - -import ( - "bytes" - "strings" - "testing" - - objectid "codeberg.org/lindenii/furgit/object/id" -) - -type bufferWriteFlusher struct { - bytes.Buffer -} - -func (bufferWriteFlusher) Flush() error { - return nil -} - -func mustHexID(tb testing.TB, algo objectid.Algorithm, digit string) objectid.ObjectID { - tb.Helper() - - id, err := objectid.ParseHex(algo, strings.Repeat(digit, algo.HexLen())) - if err != nil { - tb.Fatalf("objectid.ParseHex(%q): %v", strings.Repeat(digit, algo.HexLen()), err) - } - - return id -} diff --git a/network/protocol/v0v1/server/receivepack/parse_test.go b/network/protocol/v0v1/server/receivepack/parse_test.go deleted file mode 100644 index d54d8f8d..00000000 --- a/network/protocol/v0v1/server/receivepack/parse_test.go +++ /dev/null @@ -1,255 +0,0 @@ -package receivepack_test - -import ( - "errors" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - "codeberg.org/lindenii/furgit/network/protocol/pktline" - common "codeberg.org/lindenii/furgit/network/protocol/v0v1/server" - receivepack "codeberg.org/lindenii/furgit/network/protocol/v0v1/server/receivepack" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -func TestReadRequestParsesCommandsAndPushOptions(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - oldZero := algo.Zero().String() - oneID := mustHexID(t, algo, "1") - - var wire bufferWriteFlusher - - enc := pktline.NewEncoder(&wire) - - err := enc.WriteData([]byte( - oldZero + " " + oneID.String() + " refs/heads/main\x00report-status push-options object-format=" + algo.String() + "\n", - )) - if err != nil { - t.Fatalf("WriteData(first): %v", err) - } - - err = enc.WriteData([]byte( - oneID.String() + " " + oldZero + " refs/heads/old\n", - )) - if err != nil { - t.Fatalf("WriteData(second): %v", err) - } - - err = enc.WriteFlushPacket() - if err != nil { - t.Fatalf("WriteFlushPacket(commands): %v", err) - } - - err = enc.WriteData([]byte("ci.skip\n")) - if err != nil { - t.Fatalf("WriteData(push-option): %v", err) - } - - err = enc.WriteFlushPacket() - if err != nil { - t.Fatalf("WriteFlushPacket(push-options): %v", err) - } - - base := common.NewSession(strings.NewReader(wire.String()), &bufferWriteFlusher{}, common.Options{ - Algorithm: algo, - }) - session := receivepack.NewSession(base, receivepack.Capabilities{ - ReportStatus: true, - PushOptions: true, - ObjectFormat: algo, - }) - - req, err := session.ReadRequest() - if err != nil { - t.Fatalf("ReadRequest: %v", err) - } - - if len(req.Commands) != 2 { - t.Fatalf("len(req.Commands) = %d, want 2", len(req.Commands)) - } - - if !req.Capabilities.ReportStatus || !req.Capabilities.PushOptions { - t.Fatalf("capabilities = %#v", req.Capabilities) - } - - if len(req.PushOptions) != 1 || req.PushOptions[0] != "ci.skip" { - t.Fatalf("push options = %#v", req.PushOptions) - } - - if !req.PackExpected { - t.Fatalf("PackExpected = false, want true") - } - - if req.DeleteOnly { - t.Fatalf("DeleteOnly = true, want false") - } - }) -} - -func TestReadRequestDeleteOnlyDoesNotExpectPack(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - oneID := mustHexID(t, algo, "1") - - var wire bufferWriteFlusher - - enc := pktline.NewEncoder(&wire) - - err := enc.WriteData([]byte( - oneID.String() + " " + algo.Zero().String() + " refs/heads/old\x00delete-refs object-format=" + algo.String() + "\n", - )) - if err != nil { - t.Fatalf("WriteData: %v", err) - } - - err = enc.WriteFlushPacket() - if err != nil { - t.Fatalf("WriteFlushPacket: %v", err) - } - - base := common.NewSession(strings.NewReader(wire.String()), &bufferWriteFlusher{}, common.Options{ - Algorithm: algo, - }) - session := receivepack.NewSession(base, receivepack.Capabilities{ - DeleteRefs: true, - ObjectFormat: algo, - }) - - req, err := session.ReadRequest() - if err != nil { - t.Fatalf("ReadRequest: %v", err) - } - - if req.PackExpected { - t.Fatalf("PackExpected = true, want false") - } - - if !req.DeleteOnly { - t.Fatalf("DeleteOnly = false, want true") - } - }) -} - -func TestReadRequestRejectsUnsupportedCapability(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - oneID := mustHexID(t, algo, "1") - - var wire bufferWriteFlusher - - enc := pktline.NewEncoder(&wire) - - err := enc.WriteData([]byte( - algo.Zero().String() + " " + oneID.String() + " refs/heads/main\x00atomic object-format=" + algo.String() + "\n", - )) - if err != nil { - t.Fatalf("WriteData: %v", err) - } - - err = enc.WriteFlushPacket() - if err != nil { - t.Fatalf("WriteFlushPacket: %v", err) - } - - base := common.NewSession(strings.NewReader(wire.String()), &bufferWriteFlusher{}, common.Options{ - Algorithm: algo, - }) - session := receivepack.NewSession(base, receivepack.Capabilities{ObjectFormat: algo}) - - _, err = session.ReadRequest() - if err == nil { - t.Fatalf("ReadRequest error = nil, want error") - } - - protocolErr, ok := errors.AsType[*receivepack.ProtocolError](err) - if !ok { - t.Fatalf("errors.AsType[*receivepack.ProtocolError](%T) = false", err) - } - - if !strings.Contains(protocolErr.Reason, "unsupported capability") { - t.Fatalf("ProtocolError.Reason = %q", protocolErr.Reason) - } - }) -} - -func TestReadRequestParsesPushCertificate(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - oneID := mustHexID(t, algo, "1") - - var wire bufferWriteFlusher - - enc := pktline.NewEncoder(&wire) - - err := enc.WriteData([]byte("push-cert\x00push-cert=nonce object-format=" + algo.String() + "\n")) - if err != nil { - t.Fatalf("WriteData(push-cert): %v", err) - } - - lines := []string{ - "certificate version 0.1\n", - "pusher Example \n", - "nonce nonce\n", - "push-option ci.skip\n", - "\n", - algo.Zero().String() + " " + oneID.String() + " refs/heads/main\n", - "-----BEGIN PGP SIGNATURE-----\n", - "abcdef\n", - "push-cert-end\n", - } - - for _, line := range lines { - err = enc.WriteData([]byte(line)) - if err != nil { - t.Fatalf("WriteData(%q): %v", line, err) - } - } - - err = enc.WriteFlushPacket() - if err != nil { - t.Fatalf("WriteFlushPacket: %v", err) - } - - base := common.NewSession(strings.NewReader(wire.String()), &bufferWriteFlusher{}, common.Options{ - Algorithm: algo, - }) - session := receivepack.NewSession(base, receivepack.Capabilities{ - PushCertNonce: "server-nonce", - ObjectFormat: algo, - }) - - req, err := session.ReadRequest() - if err != nil { - t.Fatalf("ReadRequest: %v", err) - } - - if req.PushCert == nil { - t.Fatalf("PushCert = nil, want parsed certificate") - } - - if len(req.Commands) != 1 { - t.Fatalf("len(req.Commands) = %d, want 1", len(req.Commands)) - } - - if len(req.PushCert.EmbeddedOption) != 1 || req.PushCert.EmbeddedOption[0] != "ci.skip" { - t.Fatalf("embedded options = %#v", req.PushCert.EmbeddedOption) - } - }) -} diff --git a/network/protocol/v0v1/server/receivepack/report_status.go b/network/protocol/v0v1/server/receivepack/report_status.go deleted file mode 100644 index d852a161..00000000 --- a/network/protocol/v0v1/server/receivepack/report_status.go +++ /dev/null @@ -1,185 +0,0 @@ -package receivepack - -import ( - "fmt" - - "codeberg.org/lindenii/furgit/network/protocol/pktline" -) - -// WriteReportStatus writes one classic report-status response. -func (session *Session) WriteReportStatus(result ReportStatusResult) error { - unpackResult := "ok" - if result.UnpackError != "" { - unpackResult = result.UnpackError - } - - if !session.negotiated.SideBand64K { - err := session.base.WriteData(fmt.Appendf(nil, "unpack %s\n", unpackResult)) - if err != nil { - return err - } - - for _, command := range result.Commands { - line := fmt.Sprintf("ok %s\n", command.Name) - if command.Error != "" { - line = fmt.Sprintf("ng %s %s\n", command.Name, command.Error) - } - - err = session.base.WriteData([]byte(line)) - if err != nil { - return err - } - } - - return session.base.WriteFlushPacket() - } - - buf, err := pktline.AppendData(nil, fmt.Appendf(nil, "unpack %s\n", unpackResult)) - if err != nil { - return err - } - - for _, command := range result.Commands { - line := fmt.Sprintf("ok %s\n", command.Name) - if command.Error != "" { - line = fmt.Sprintf("ng %s %s\n", command.Name, command.Error) - } - - buf, err = pktline.AppendData(buf, []byte(line)) - if err != nil { - return err - } - } - - buf = pktline.AppendFlushPkt(buf) - - w := session.base.PrimaryDataWriter() - - _, err = w.Write(buf) - if err != nil { - return err - } - - return session.base.WriteFlushPacket() -} - -// WriteReportStatusV2 writes one report-status-v2 response. -func (session *Session) WriteReportStatusV2(result ReportStatusResult) error { - unpackResult := "ok" - if result.UnpackError != "" { - unpackResult = result.UnpackError - } - - if !session.negotiated.SideBand64K { //nolint:nestif - err := session.base.WriteData(fmt.Appendf(nil, "unpack %s\n", unpackResult)) - if err != nil { - return err - } - - for _, command := range result.Commands { - if command.Error != "" { - err = session.base.WriteData(fmt.Appendf(nil, "ng %s %s\n", command.Name, command.Error)) - if err != nil { - return err - } - - continue - } - - err = session.base.WriteData(fmt.Appendf(nil, "ok %s\n", command.Name)) - if err != nil { - return err - } - - if command.RefName != "" { - err = session.base.WriteData(fmt.Appendf(nil, "option refname %s\n", command.RefName)) - if err != nil { - return err - } - } - - if command.OldID != nil { - err = session.base.WriteData(fmt.Appendf(nil, "option old-oid %s\n", *command.OldID)) - if err != nil { - return err - } - } - - if command.NewID != nil { - err = session.base.WriteData(fmt.Appendf(nil, "option new-oid %s\n", *command.NewID)) - if err != nil { - return err - } - } - - if command.ForcedUpdate { - err = session.base.WriteData([]byte("option forced-update\n")) - if err != nil { - return err - } - } - } - - return session.base.WriteFlushPacket() - } - - buf, err := pktline.AppendData(nil, fmt.Appendf(nil, "unpack %s\n", unpackResult)) - if err != nil { - return err - } - - for _, command := range result.Commands { - if command.Error != "" { - buf, err = pktline.AppendData(buf, fmt.Appendf(nil, "ng %s %s\n", command.Name, command.Error)) - if err != nil { - return err - } - - continue - } - - buf, err = pktline.AppendData(buf, fmt.Appendf(nil, "ok %s\n", command.Name)) - if err != nil { - return err - } - - if command.RefName != "" { - buf, err = pktline.AppendData(buf, fmt.Appendf(nil, "option refname %s\n", command.RefName)) - if err != nil { - return err - } - } - - if command.OldID != nil { - buf, err = pktline.AppendData(buf, fmt.Appendf(nil, "option old-oid %s\n", *command.OldID)) - if err != nil { - return err - } - } - - if command.NewID != nil { - buf, err = pktline.AppendData(buf, fmt.Appendf(nil, "option new-oid %s\n", *command.NewID)) - if err != nil { - return err - } - } - - if command.ForcedUpdate { - buf, err = pktline.AppendData(buf, []byte("option forced-update\n")) - if err != nil { - return err - } - } - } - - buf = pktline.AppendFlushPkt(buf) - - w := session.base.PrimaryDataWriter() - - _, err = w.Write(buf) - if err != nil { - return err - } - - return session.base.WriteFlushPacket() -} diff --git a/network/protocol/v0v1/server/receivepack/report_status_test.go b/network/protocol/v0v1/server/receivepack/report_status_test.go deleted file mode 100644 index 3cde5103..00000000 --- a/network/protocol/v0v1/server/receivepack/report_status_test.go +++ /dev/null @@ -1,293 +0,0 @@ -package receivepack_test - -import ( - "errors" - "io" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - "codeberg.org/lindenii/furgit/network/protocol/pktline" - "codeberg.org/lindenii/furgit/network/protocol/sideband64k" - common "codeberg.org/lindenii/furgit/network/protocol/v0v1/server" - receivepack "codeberg.org/lindenii/furgit/network/protocol/v0v1/server/receivepack" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -func TestWriteReportStatusWritesClassicStatus(t *testing.T) { - t.Parallel() - - var out bufferWriteFlusher - - base := common.NewSession(strings.NewReader(""), &out, common.Options{}) - session := receivepack.NewSession(base, receivepack.Capabilities{}) - - err := session.WriteReportStatus(receivepack.ReportStatusResult{ - Commands: []receivepack.CommandResult{ - {Name: "refs/heads/main"}, - {Name: "refs/heads/dev", Error: "non-fast-forward"}, - }, - }) - if err != nil { - t.Fatalf("WriteReportStatus: %v", err) - } - - got := out.String() - wantParts := []string{ - "unpack ok\n", - "ok refs/heads/main\n", - "ng refs/heads/dev non-fast-forward\n", - "0000", - } - - for _, part := range wantParts { - if !strings.Contains(got, part) { - t.Fatalf("report-status missing %q in %q", part, got) - } - } -} - -func TestWriteReportStatusUsesSideBand64KWhenNegotiated(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - var requestWire bufferWriteFlusher - - requestEnc := pktline.NewEncoder(&requestWire) - - err := requestEnc.WriteData([]byte( - algo.Zero().String() + " " + mustHexID(t, algo, "1").String() + " refs/heads/main\x00report-status side-band-64k object-format=" + algo.String() + "\n", - )) - if err != nil { - t.Fatalf("WriteData(request): %v", err) - } - - err = requestEnc.WriteFlushPacket() - if err != nil { - t.Fatalf("WriteFlushPacket(request): %v", err) - } - - var out bufferWriteFlusher - - base := common.NewSession(strings.NewReader(requestWire.String()), &out, common.Options{ - Algorithm: algo, - }) - session := receivepack.NewSession(base, receivepack.Capabilities{ - ReportStatus: true, - SideBand64K: true, - ObjectFormat: algo, - }) - - _, err = session.ReadRequest() - if err != nil { - t.Fatalf("ReadRequest: %v", err) - } - - err = session.WriteReportStatus(receivepack.ReportStatusResult{ - Commands: []receivepack.CommandResult{ - {Name: "refs/heads/main"}, - }, - }) - if err != nil { - t.Fatalf("WriteReportStatus: %v", err) - } - - dec := sideband64k.NewDecoder(strings.NewReader(out.String()), sideband64k.ReadOptions{}) - - frame, err := dec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame(unpack): %v", err) - } - - if frame.Type != sideband64k.FrameData { - t.Fatalf("first frame = %#v", frame) - } - - statusDec := pktline.NewDecoder(strings.NewReader(string(frame.Payload)), pktline.ReadOptions{}) - - statusFrame, err := statusDec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame(unpack status): %v", err) - } - - if statusFrame.Type != pktline.PacketData || string(statusFrame.Payload) != "unpack ok\n" { - t.Fatalf("first status frame = %#v", statusFrame) - } - - statusFrame, err = statusDec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame(ok status): %v", err) - } - - if statusFrame.Type != pktline.PacketData || string(statusFrame.Payload) != "ok refs/heads/main\n" { - t.Fatalf("second status frame = %#v", statusFrame) - } - - statusFrame, err = statusDec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame(status flush): %v", err) - } - - if statusFrame.Type != pktline.PacketFlush { - t.Fatalf("status flush frame.Type = %v, want FrameFlush", statusFrame.Type) - } - - frame, err = dec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame(outer flush): %v", err) - } - - if frame.Type != sideband64k.FrameFlush { - t.Fatalf("outer flush frame.Type = %v, want FrameFlush", frame.Type) - } - }) -} - -func TestWriteReportStatusV2WritesOptionLines(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - oldID := mustHexID(t, algo, "1") - newID := mustHexID(t, algo, "2") - - var out bufferWriteFlusher - - base := common.NewSession(strings.NewReader(""), &out, common.Options{}) - session := receivepack.NewSession(base, receivepack.Capabilities{}) - - err := session.WriteReportStatusV2(receivepack.ReportStatusResult{ - Commands: []receivepack.CommandResult{ - { - Name: "refs/pseudo/proc", - RefName: "refs/heads/main", - OldID: &oldID, - NewID: &newID, - ForcedUpdate: true, - }, - {Name: "refs/heads/dev", Error: "rejected"}, - }, - }) - if err != nil { - t.Fatalf("WriteReportStatusV2: %v", err) - } - - got := out.String() - wantParts := []string{ - "unpack ok\n", - "ok refs/pseudo/proc\n", - "option refname refs/heads/main\n", - "option old-oid " + oldID.String() + "\n", - "option new-oid " + newID.String() + "\n", - "option forced-update\n", - "ng refs/heads/dev rejected\n", - "0000", - } - - for _, part := range wantParts { - if !strings.Contains(got, part) { - t.Fatalf("report-status-v2 missing %q in %q", part, got) - } - } - }) -} - -func TestWriteProgressRequiresSideBand64K(t *testing.T) { - t.Parallel() - - base := common.NewSession(strings.NewReader(""), &bufferWriteFlusher{}, common.Options{}) - session := receivepack.NewSession(base, receivepack.Capabilities{}) - - err := session.WriteProgress([]byte("progress\n")) - if !errors.Is(err, common.ErrSideBandNotEnabled) { - t.Fatalf("WriteProgress error = %v, want %v", err, common.ErrSideBandNotEnabled) - } -} - -func TestProgressWriterDiscardsWithoutSideBand64K(t *testing.T) { - t.Parallel() - - var out bufferWriteFlusher - - base := common.NewSession(strings.NewReader(""), &out, common.Options{}) - session := receivepack.NewSession(base, receivepack.Capabilities{}) - - n, err := io.WriteString(session.ProgressWriter(), "progress line\n") - if err != nil { - t.Fatalf("ProgressWriter.Write: %v", err) - } - - if n != len("progress line\n") { - t.Fatalf("ProgressWriter.Write n = %d, want %d", n, len("progress line\n")) - } - - if out.String() != "" { - t.Fatalf("unexpected wire output without side-band-64k: %q", out.String()) - } -} - -func TestProgressWriterUsesSideBand64KWhenNegotiated(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - var requestWire bufferWriteFlusher - - requestEnc := pktline.NewEncoder(&requestWire) - - err := requestEnc.WriteData([]byte( - algo.Zero().String() + " " + mustHexID(t, algo, "1").String() + " refs/heads/main\x00report-status side-band-64k object-format=" + algo.String() + "\n", - )) - if err != nil { - t.Fatalf("WriteData(request): %v", err) - } - - err = requestEnc.WriteFlushPacket() - if err != nil { - t.Fatalf("WriteFlushPacket(request): %v", err) - } - - var out bufferWriteFlusher - - base := common.NewSession(strings.NewReader(requestWire.String()), &out, common.Options{ - Algorithm: algo, - }) - session := receivepack.NewSession(base, receivepack.Capabilities{ - ReportStatus: true, - SideBand64K: true, - ObjectFormat: algo, - }) - - _, err = session.ReadRequest() - if err != nil { - t.Fatalf("ReadRequest: %v", err) - } - - _, err = io.WriteString(session.ProgressWriter(), "remote: stage 1\r") - if err != nil { - t.Fatalf("ProgressWriter.Write: %v", err) - } - - dec := sideband64k.NewDecoder(strings.NewReader(out.String()), sideband64k.ReadOptions{}) - - frame, err := dec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame(progress): %v", err) - } - - if frame.Type != sideband64k.FrameProgress { - t.Fatalf("frame.Type = %v, want FrameProgress", frame.Type) - } - - if string(frame.Payload) != "remote: stage 1\r" { - t.Fatalf("frame.Payload = %q, want %q", frame.Payload, "remote: stage 1\r") - } - }) -} diff --git a/network/protocol/v0v1/server/receivepack/session.go b/network/protocol/v0v1/server/receivepack/session.go deleted file mode 100644 index 5299b42d..00000000 --- a/network/protocol/v0v1/server/receivepack/session.go +++ /dev/null @@ -1,303 +0,0 @@ -package receivepack - -import ( - "fmt" - "strings" - - "codeberg.org/lindenii/furgit/common/iowrap" - common "codeberg.org/lindenii/furgit/network/protocol/v0v1/server" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// Session is one stateful server-side receive-pack protocol session. -// -// Labels: MT-Unsafe. -type Session struct { - base *common.Session - supported Capabilities - negotiated Capabilities -} - -// NewSession creates one receive-pack session over one common server session. -// -// Labels: Deps-Borrowed, Life-Parent. -func NewSession(base *common.Session, supported Capabilities) *Session { - return &Session{ - base: base, - supported: supported, - } -} - -// AdvertiseRefs writes one receive-pack ref advertisement. -func (session *Session) AdvertiseRefs(ad common.Advertisement) error { - return session.base.AdvertiseRefs(ad, session.supported.Tokens(session.base.Algorithm())) -} - -// ReadRequest reads one receive-pack request through optional push-options. -func (session *Session) ReadRequest() (*Request, error) { - req := &Request{} - - var sawCommands bool - - for { - frame, err := session.base.ReadFrame() - if err != nil { - return nil, err - } - - switch frame.Type { - case common.FrameFlush: - goto afterCommands - case common.FrameData: - case common.FrameDelim, common.FrameResponseEnd: - return nil, &ProtocolError{Reason: fmt.Sprintf("unexpected packet type %v", frame.Type)} - } - - payload := string(frame.Payload) - if strings.HasPrefix(payload, "shallow ") { - line := trimOneLF(payload) - - shallowID, err := parseObjectID(session.base.Algorithm(), line[len("shallow "):]) - if err != nil { - return nil, err - } - - req.Shallow = append(req.Shallow, shallowID) - - continue - } - - if strings.HasPrefix(payload, "push-cert\x00") { - if sawCommands { - return nil, &ProtocolError{Reason: "got both push certificate and unsigned commands"} - } - - capabilityTokens, err := parseCapabilityList(payload[len("push-cert\x00"):]) - if err != nil { - return nil, err - } - - requested, err := parseRequestedCapabilities( - capabilityTokens, - session.supported, - session.base.Algorithm(), - ) - if err != nil { - return nil, err - } - - req.Capabilities = requested - - cert, err := session.readPushCertificate() - if err != nil { - return nil, err - } - - req.PushCert = cert - req.Commands = append(req.Commands, cert.Commands...) - sawCommands = true - - continue - } - - line := trimOneLF(payload) - if !sawCommands && strings.Contains(line, "\x00") { - commandPart, capPart, _ := strings.Cut(line, "\x00") - - capabilityTokens, err := parseCapabilityList(capPart) - if err != nil { - return nil, err - } - - requested, err := parseRequestedCapabilities( - capabilityTokens, - session.supported, - session.base.Algorithm(), - ) - if err != nil { - return nil, err - } - - req.Capabilities = requested - line = commandPart - } - - cmd, err := parseCommand(session.base.Algorithm(), line) - if err != nil { - return nil, err - } - - req.Commands = append(req.Commands, cmd) - sawCommands = true - } - -afterCommands: - if req.Capabilities.PushOptions { - for { - frame, err := session.base.ReadFrame() - if err != nil { - return nil, err - } - - switch frame.Type { - case common.FrameFlush: - goto afterPushOptions - case common.FrameData: - req.PushOptions = append(req.PushOptions, trimOneLF(string(frame.Payload))) - case common.FrameDelim, common.FrameResponseEnd: - return nil, &ProtocolError{Reason: fmt.Sprintf("unexpected packet type %v", frame.Type)} - } - } - } - -afterPushOptions: - req.DeleteOnly = deleteOnly(req.Commands) - - req.PackExpected = len(req.Commands) > 0 && !req.DeleteOnly - - session.negotiated = req.Capabilities - - if req.Capabilities.SideBand64K { - session.base.EnableSideBand64K() - } - - return req, nil -} - -// WriteProgress writes one progress packet. -func (session *Session) WriteProgress(p []byte) error { - return session.base.WriteProgress(p) -} - -// ProgressWriter returns one chunking writer for sideband progress output. -// -// When side-band-64k was not negotiated, writes are discarded. -// -// Labels: Life-Parent. -func (session *Session) ProgressWriter() iowrap.WriteFlusher { - return session.base.ProgressWriter() -} - -// WriteError writes one fatal error packet. -func (session *Session) WriteError(p []byte) error { - return session.base.WriteError(p) -} - -// ErrorWriter returns one chunking writer for sideband error output. -// -// When side-band-64k was not negotiated, writes are discarded. -// -// Labels: Life-Parent. -func (session *Session) ErrorWriter() iowrap.WriteFlusher { - return session.base.ErrorWriter() -} - -func trimOneLF(s string) string { - return strings.TrimSuffix(s, "\n") -} - -func parseObjectID(algo objectid.Algorithm, s string) (objectid.ObjectID, error) { - id, err := objectid.ParseHex(algo, s) - if err != nil { - return objectid.ObjectID{}, &ProtocolError{ - Reason: fmt.Sprintf("invalid object id %q", s), - } - } - - return id, nil -} - -func commandIsDelete(cmd Command) bool { - return cmd.NewID == cmd.NewID.Algorithm().Zero() -} - -func deleteOnly(commands []Command) bool { - if len(commands) == 0 { - return false - } - - for _, cmd := range commands { - if !commandIsDelete(cmd) { - return false - } - } - - return true -} - -func parseCommand(algo objectid.Algorithm, line string) (Command, error) { - fields := strings.Fields(line) - if len(fields) != 3 { - return Command{}, &ProtocolError{Reason: fmt.Sprintf("malformed command %q", line)} - } - - oldID, err := parseObjectID(algo, fields[0]) - if err != nil { - return Command{}, err - } - - newID, err := parseObjectID(algo, fields[1]) - if err != nil { - return Command{}, err - } - - return Command{OldID: oldID, NewID: newID, Name: fields[2]}, nil -} - -func (session *Session) readPushCertificate() (*PushCertificate, error) { - cert := &PushCertificate{} - inCommands := false - inSignature := false - - for { - frame, err := session.base.ReadFrame() - if err != nil { - return nil, err - } - - switch frame.Type { - case common.FrameFlush: - return nil, &ProtocolError{Reason: "unexpected flush inside push certificate"} - case common.FrameData: - case common.FrameDelim, common.FrameResponseEnd: - return nil, &ProtocolError{Reason: fmt.Sprintf("unexpected packet type %v", frame.Type)} - } - - line := string(frame.Payload) - if line == "push-cert-end\n" { - return cert, nil - } - - if !inCommands { - if line == "\n" { - inCommands = true - - continue - } - - trimmed := trimOneLF(line) - cert.HeaderLines = append(cert.HeaderLines, trimmed) - - if strings.HasPrefix(trimmed, "push-option ") { - cert.EmbeddedOption = append(cert.EmbeddedOption, trimmed[len("push-option "):]) - } - - continue - } - - if !inSignature { - trimmed := trimOneLF(line) - - cmd, err := parseCommand(session.base.Algorithm(), trimmed) - if err == nil { - cert.Commands = append(cert.Commands, cmd) - - continue - } - - inSignature = true - } - - cert.SignatureLines = append(cert.SignatureLines, trimOneLF(line)) - } -} diff --git a/network/protocol/v0v1/server/receivepack/types.go b/network/protocol/v0v1/server/receivepack/types.go deleted file mode 100644 index b281a86b..00000000 --- a/network/protocol/v0v1/server/receivepack/types.go +++ /dev/null @@ -1,45 +0,0 @@ -package receivepack - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// Command is one requested reference update. -type Command struct { - OldID objectid.ObjectID - NewID objectid.ObjectID - Name string -} - -// PushCertificate is one parsed push certificate block. -type PushCertificate struct { - HeaderLines []string - EmbeddedOption []string - Commands []Command - SignatureLines []string -} - -// Request is one parsed receive-pack request. -type Request struct { - Capabilities Capabilities - Shallow []objectid.ObjectID - Commands []Command - PushCert *PushCertificate - PushOptions []string - PackExpected bool - DeleteOnly bool -} - -// CommandResult is one per-command report-status result. -type CommandResult struct { - Name string - Error string - RefName string - OldID *objectid.ObjectID - NewID *objectid.ObjectID - ForcedUpdate bool -} - -// ReportStatusResult is one report-status payload. -type ReportStatusResult struct { - UnpackError string - Commands []CommandResult -} diff --git a/network/protocol/v0v1/server/session.go b/network/protocol/v0v1/server/session.go deleted file mode 100644 index a66cc37a..00000000 --- a/network/protocol/v0v1/server/session.go +++ /dev/null @@ -1,142 +0,0 @@ -package server - -import ( - "io" - - "codeberg.org/lindenii/furgit/common/iowrap" - "codeberg.org/lindenii/furgit/network/protocol/pktline" - "codeberg.org/lindenii/furgit/network/protocol/sideband64k" - objectid "codeberg.org/lindenii/furgit/object/id" -) - -// Options configures one server-side v0/v1 session. -type Options struct { - // Version selects protocol v0 or v1 framing. - Version Version - // Algorithm is the repository object ID algorithm for this session. - Algorithm objectid.Algorithm -} - -// Session is one stateful server-side v0/v1 server protocol session. -// -// Labels: MT-Unsafe. -type Session struct { - dec *pktline.Decoder - enc *pktline.Encoder - sideband *sideband64k.Encoder - opts Options - useSideBand bool -} - -// NewSession creates one v0/v1 server session over r and w. -// -// Labels: Deps-Borrowed, Life-Parent. -func NewSession(r io.Reader, w iowrap.WriteFlusher, opts Options) *Session { - return &Session{ - dec: pktline.NewDecoder(r, pktline.ReadOptions{}), - enc: pktline.NewEncoder(w), - sideband: sideband64k.NewEncoder(w), - opts: opts, - } -} - -// Algorithm returns the session object ID algorithm. -func (session *Session) Algorithm() objectid.Algorithm { - return session.opts.Algorithm -} - -// ReadFrame reads one low-level pkt-line frame from the session input. -func (session *Session) ReadFrame() (Frame, error) { - return session.dec.ReadFrame() -} - -// EnableSideBand64K enables side-band-64k output framing for subsequent data, -// progress, error, and flush writes. -func (session *Session) EnableSideBand64K() { - session.useSideBand = true -} - -// WriteData writes one primary output packet. -func (session *Session) WriteData(p []byte) error { - if session.useSideBand { - return session.sideband.WriteData(p) - } - - return session.enc.WriteData(p) -} - -// WriteProgress writes one progress packet. -func (session *Session) WriteProgress(p []byte) error { - if !session.useSideBand { - return ErrSideBandNotEnabled - } - - return session.sideband.WriteProgress(p) -} - -// WriteError writes one fatal error packet. -func (session *Session) WriteError(p []byte) error { - if !session.useSideBand { - return ErrSideBandNotEnabled - } - - return session.sideband.WriteError(p) -} - -// WriteFlushPacket writes one trailing flush packet. -func (session *Session) WriteFlushPacket() error { - if session.useSideBand { - return session.sideband.WriteFlushPacket() - } - - return session.enc.WriteFlushPacket() -} - -// Flush flushes buffered transport output without emitting pkt-line frames. -func (session *Session) Flush() error { - if session.useSideBand { - return session.sideband.Flush() - } - - return session.enc.Flush() -} - -// ProgressWriter returns one chunking writer for sideband progress output. -// -// When side-band-64k was not negotiated, writes are discarded. -// -// Labels: Life-Parent. -func (session *Session) ProgressWriter() iowrap.WriteFlusher { - if !session.useSideBand { - return iowrap.NopFlush(io.Discard) - } - - return sideband64k.NewChunkWriter(session.sideband, sideband64k.BandProgress) -} - -// ErrorWriter returns one chunking writer for sideband error output. -// -// When side-band-64k was not negotiated, writes are discarded. -// -// Labels: Life-Parent. -func (session *Session) ErrorWriter() iowrap.WriteFlusher { - if !session.useSideBand { - return iowrap.NopFlush(io.Discard) - } - - return sideband64k.NewChunkWriter(session.sideband, sideband64k.BandError) -} - -// PrimaryDataWriter returns one chunking writer for primary output bytes. -// -// When side-band-64k is enabled, writes are chunked into band-1 sideband -// frames. Otherwise writes are chunked into direct pkt-line data frames. -// -// Labels: Life-Parent. -func (session *Session) PrimaryDataWriter() iowrap.WriteFlusher { - if session.useSideBand { - return sideband64k.NewChunkWriter(session.sideband, sideband64k.BandData) - } - - return pktline.NewChunkWriter(session.enc) -} diff --git a/network/protocol/v0v1/server/version.go b/network/protocol/v0v1/server/version.go deleted file mode 100644 index 23ae9466..00000000 --- a/network/protocol/v0v1/server/version.go +++ /dev/null @@ -1,12 +0,0 @@ -package server - -// Version identifies the protocol version used on one v0/v1 server session. -type Version uint8 - -const ( - // Version0 is the original protocol framing with no leading version line. - Version0 Version = iota - // Version1 is protocol v1, which is v0 plus one leading "version 1\n" - // pkt-line before ref advertisement. - Version1 -) diff --git a/network/receivepack/advertise.go b/network/receivepack/advertise.go deleted file mode 100644 index 0fa010bf..00000000 --- a/network/receivepack/advertise.go +++ /dev/null @@ -1,57 +0,0 @@ -package receivepack - -import ( - "errors" - - common "codeberg.org/lindenii/furgit/network/protocol/v0v1/server" - "codeberg.org/lindenii/furgit/ref" - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -func advertisedRefs(opts Options) ([]common.AdvertisedRef, error) { - listed, err := opts.Refs.List("") - if err != nil { - return nil, err - } - - return buildAdvertisedRefs(opts, listed) -} - -func buildAdvertisedRefs(opts Options, listed []ref.Ref) ([]common.AdvertisedRef, error) { - refs := make([]common.AdvertisedRef, 0, len(listed)) - for _, entry := range listed { - switch resolved := entry.(type) { - case ref.Detached: - advertised := common.AdvertisedRef{ - Name: resolved.Name(), - ID: resolved.ID, - } - - if resolved.Peeled != nil { - advertised.Peeled = resolved.Peeled - } - - refs = append(refs, advertised) - case ref.Symbolic: - if resolved.Name() != "HEAD" { - continue - } - - head, err := opts.Refs.ResolveToDetached("HEAD") - if err != nil { - if errors.Is(err, refstore.ErrReferenceNotFound) { - continue - } - - return nil, err - } - - refs = append(refs, common.AdvertisedRef{ - Name: "HEAD", - ID: head.ID, - }) - } - } - - return refs, nil -} diff --git a/network/receivepack/capabilities_defaults.go b/network/receivepack/capabilities_defaults.go deleted file mode 100644 index 72c36c30..00000000 --- a/network/receivepack/capabilities_defaults.go +++ /dev/null @@ -1,17 +0,0 @@ -package receivepack - -import ( - "crypto/rand" -) - -func defaultAgent() string { - return "furgit" -} - -func defaultSessionID() string { - return "furgit-" + rand.Text() -} - -func defaultPushCertNonce() string { - return "furgit-" + rand.Text() -} diff --git a/network/receivepack/commands.go b/network/receivepack/commands.go deleted file mode 100644 index a9edec1a..00000000 --- a/network/receivepack/commands.go +++ /dev/null @@ -1,19 +0,0 @@ -package receivepack - -import ( - protoreceive "codeberg.org/lindenii/furgit/network/protocol/v0v1/server/receivepack" - "codeberg.org/lindenii/furgit/network/receivepack/service" -) - -func translateCommands(commands []protoreceive.Command) []service.Command { - out := make([]service.Command, 0, len(commands)) - for _, command := range commands { - out = append(out, service.Command{ - OldID: command.OldID, - NewID: command.NewID, - Name: command.Name, - }) - } - - return out -} diff --git a/network/receivepack/doc.go b/network/receivepack/doc.go deleted file mode 100644 index b63f49d5..00000000 --- a/network/receivepack/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package receivepack provides the application-facing server-side push entry -// point. -package receivepack diff --git a/network/receivepack/errors.go b/network/receivepack/errors.go deleted file mode 100644 index 18e7a135..00000000 --- a/network/receivepack/errors.go +++ /dev/null @@ -1,15 +0,0 @@ -package receivepack - -import "errors" - -var ( - // ErrMissingAlgorithm reports one missing repository hash algorithm. - ErrMissingAlgorithm = errors.New("receivepack: missing object id algorithm") - // ErrMissingRefs reports one missing reference store dependency. - ErrMissingRefs = errors.New("receivepack: missing refs store") - // ErrMissingObjects reports one missing object store dependency. - ErrMissingObjects = errors.New("receivepack: missing objects store") - // ErrUnsupportedProtocol reports one unsupported requested Git protocol - // version. - ErrUnsupportedProtocol = errors.New("receivepack: unsupported protocol version") -) diff --git a/network/receivepack/hook.go b/network/receivepack/hook.go deleted file mode 100644 index 9c323bcc..00000000 --- a/network/receivepack/hook.go +++ /dev/null @@ -1,97 +0,0 @@ -package receivepack - -import ( - "context" - - "codeberg.org/lindenii/furgit/common/iowrap" - commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" - "codeberg.org/lindenii/furgit/network/receivepack/service" - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -type HookIO struct { - Progress iowrap.WriteFlusher - Error iowrap.WriteFlusher -} - -// RefUpdate is one requested reference update presented to a receive-pack hook. -type RefUpdate struct { - Name string - OldID objectid.ObjectID - NewID objectid.ObjectID -} - -// UpdateDecision is one hook decision for a requested reference update. -type UpdateDecision struct { - Accept bool - Message string -} - -// HookRequest is the input presented to a receive-pack hook before quarantine -// promotion and ref updates. -// -// Labels: Life-Call. -type HookRequest struct { - Refs refstore.Reader - ExistingObjects objectstore.Reader - // QuarantinedObjects exposes quarantined objects for this push. - // - // When the push did not create a quarantine, QuarantinedObjects is nil. - QuarantinedObjects objectstore.Reader - CommitGraph *commitgraphread.Reader - Updates []RefUpdate - PushOptions []string - IO HookIO -} - -// Hook decides whether each requested update should proceed. -// -// The hook runs after pack ingestion into quarantine and before quarantine -// promotion or ref updates. The returned decisions must have the same length as -// HookRequest.Updates. -type Hook func(context.Context, HookRequest) ([]UpdateDecision, error) - -func translateHook(hook Hook) service.Hook { - if hook == nil { - return nil - } - - return func(ctx context.Context, req service.HookRequest) ([]service.UpdateDecision, error) { - translatedUpdates := make([]RefUpdate, 0, len(req.Updates)) - for _, update := range req.Updates { - translatedUpdates = append(translatedUpdates, RefUpdate{ - Name: update.Name, - OldID: update.OldID, - NewID: update.NewID, - }) - } - - decisions, err := hook(ctx, HookRequest{ - Refs: req.Refs, - ExistingObjects: req.ExistingObjects, - QuarantinedObjects: req.QuarantinedObjects, - CommitGraph: req.CommitGraph, - Updates: translatedUpdates, - PushOptions: append([]string(nil), req.PushOptions...), - IO: HookIO{ - Progress: req.IO.Progress, - Error: req.IO.Error, - }, - }) - if err != nil { - return nil, err - } - - out := make([]service.UpdateDecision, 0, len(decisions)) - for _, decision := range decisions { - out = append(out, service.UpdateDecision{ - Accept: decision.Accept, - Message: decision.Message, - }) - } - - return out, nil - } -} diff --git a/network/receivepack/hooks/chain.go b/network/receivepack/hooks/chain.go deleted file mode 100644 index f98c06f8..00000000 --- a/network/receivepack/hooks/chain.go +++ /dev/null @@ -1,51 +0,0 @@ -package hooks - -import ( - "context" - "fmt" - - receivepack "codeberg.org/lindenii/furgit/network/receivepack" -) - -// Chain combines hooks by running them in order and intersecting their -// decisions. The first rejecting message for each update is preserved. -func Chain(hooks ...receivepack.Hook) receivepack.Hook { - return func( - ctx context.Context, - req receivepack.HookRequest, - ) ([]receivepack.UpdateDecision, error) { - decisions := make([]receivepack.UpdateDecision, len(req.Updates)) - for i := range decisions { - decisions[i].Accept = true - } - - for _, hook := range hooks { - if hook == nil { - continue - } - - hookDecisions, err := hook(ctx, req) - if err != nil { - return nil, err - } - - if len(hookDecisions) != len(req.Updates) { - return nil, fmt.Errorf("hook returned %d decisions for %d updates", len(hookDecisions), len(req.Updates)) - } - - for i, decision := range hookDecisions { - if decision.Accept { - continue - } - - if decisions[i].Accept { - decisions[i].Message = decision.Message - } - - decisions[i].Accept = false - } - } - - return decisions, nil - } -} diff --git a/network/receivepack/hooks/doc.go b/network/receivepack/hooks/doc.go deleted file mode 100644 index bef2baf9..00000000 --- a/network/receivepack/hooks/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package hooks provides a few pre-defined hooks that callers might find useful. -package hooks diff --git a/network/receivepack/hooks/reject_force_push.go b/network/receivepack/hooks/reject_force_push.go deleted file mode 100644 index 5840a031..00000000 --- a/network/receivepack/hooks/reject_force_push.go +++ /dev/null @@ -1,69 +0,0 @@ -package hooks - -import ( - "context" - "errors" - "fmt" - - "codeberg.org/lindenii/furgit/commitquery" - receivepack "codeberg.org/lindenii/furgit/network/receivepack" - "codeberg.org/lindenii/furgit/object/fetch" - objectmix "codeberg.org/lindenii/furgit/object/store/mix" - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -// RejectForcePush rejects updates whose new value is not a fast-forward of the -// currently resolved reference. -func RejectForcePush() receivepack.Hook { - return func( - ctx context.Context, - req receivepack.HookRequest, - ) ([]receivepack.UpdateDecision, error) { - _ = ctx - - objects := req.ExistingObjects - if req.QuarantinedObjects != nil { - objects = objectmix.New(req.QuarantinedObjects, req.ExistingObjects) - } - - queries := commitquery.New(fetch.New(objects), req.CommitGraph) - - decisions := make([]receivepack.UpdateDecision, len(req.Updates)) - for i := range decisions { - decisions[i].Accept = true - } - - for i, update := range req.Updates { - if update.OldID == update.OldID.Algorithm().Zero() || update.NewID == update.NewID.Algorithm().Zero() { - continue - } - - current, err := req.Refs.ResolveToDetached(update.Name) - switch { - case err == nil: - case errors.Is(err, refstore.ErrReferenceNotFound): - continue - default: - return nil, fmt.Errorf("resolve %s: %w", update.Name, err) - } - - if current.ID == update.NewID { - continue - } - - ok, err := queries.IsAncestor(current.ID, update.NewID) - if err != nil { - return nil, fmt.Errorf("check fast-forward %s: %w", update.Name, err) - } - - if !ok { - decisions[i] = receivepack.UpdateDecision{ - Accept: false, - Message: "non-fast-forward", - } - } - } - - return decisions, nil - } -} diff --git a/network/receivepack/int_test.go b/network/receivepack/int_test.go deleted file mode 100644 index 352bbe7b..00000000 --- a/network/receivepack/int_test.go +++ /dev/null @@ -1,1095 +0,0 @@ -package receivepack_test - -import ( - "context" - "fmt" - "io" - "os" - "strings" - "testing" - "time" - - "codeberg.org/lindenii/furgit/internal/testgit" - "codeberg.org/lindenii/furgit/network/protocol/pktline" - "codeberg.org/lindenii/furgit/network/protocol/sideband64k" - receivepack "codeberg.org/lindenii/furgit/network/receivepack" - receivepackhooks "codeberg.org/lindenii/furgit/network/receivepack/hooks" - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - objectdual "codeberg.org/lindenii/furgit/object/store/dual" - objectloose "codeberg.org/lindenii/furgit/object/store/loose" - objectpacked "codeberg.org/lindenii/furgit/object/store/packed" -) - -func TestReceivePackDeleteOnlyAtomicDeleteSucceeds(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := testRepo.MakeCommit(t, "base") - testRepo.UpdateRef(t, "refs/heads/main", commitID) - - repo := testRepo.OpenRepository(t) - - var ( - input strings.Builder - output bufferWriteFlusher - ) - - input.WriteString(pktlineData( - commitID.String() + " " + algo.Zero().String() + " refs/heads/main\x00report-status atomic delete-refs object-format=" + algo.String() + "\n", - )) - input.WriteString("0000") - - err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{ - GitProtocol: "", - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - }) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - got := output.String() - if !strings.Contains(got, "ok refs/heads/main\n") { - t.Fatalf("unexpected receive-pack output %q", got) - } - - _, err = repo.Refs().Resolve("refs/heads/main") - if err == nil { - t.Fatal("refs/heads/main still exists after delete push") - } - }) -} - -func TestReceivePackDeleteOnlyNonAtomicAppliesIndependentDeletes(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := testRepo.MakeCommit(t, "base") - _, _, staleID := testRepo.MakeCommit(t, "stale") - testRepo.UpdateRef(t, "refs/heads/main", commitID) - testRepo.UpdateRef(t, "refs/heads/topic", commitID) - - repo := testRepo.OpenRepository(t) - - var ( - input strings.Builder - output bufferWriteFlusher - ) - - input.WriteString(pktlineData( - staleID.String() + " " + algo.Zero().String() + " refs/heads/main\x00report-status delete-refs object-format=" + algo.String() + "\n", - )) - input.WriteString(pktlineData( - commitID.String() + " " + algo.Zero().String() + " refs/heads/topic\n", - )) - input.WriteString("0000") - - err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{ - GitProtocol: "", - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - }) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - got := output.String() - if !strings.Contains(got, "ng refs/heads/main ") || !strings.Contains(got, "ok refs/heads/topic\n") { - t.Fatalf("unexpected receive-pack output %q", got) - } - - _, err = repo.Refs().Resolve("refs/heads/main") - if err != nil { - t.Fatalf("Resolve(main): %v", err) - } - - _, err = repo.Refs().Resolve("refs/heads/topic") - if err == nil { - t.Fatal("refs/heads/topic still exists after successful delete") - } - }) -} - -func TestReceivePackDeleteOnlyAtomicFailureLeavesAllRefsUntouched(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := testRepo.MakeCommit(t, "base") - _, _, staleID := testRepo.MakeCommit(t, "stale") - testRepo.UpdateRef(t, "refs/heads/main", commitID) - testRepo.UpdateRef(t, "refs/heads/topic", commitID) - - repo := testRepo.OpenRepository(t) - - var ( - input strings.Builder - output bufferWriteFlusher - ) - - input.WriteString(pktlineData( - staleID.String() + " " + algo.Zero().String() + " refs/heads/main\x00report-status atomic delete-refs object-format=" + algo.String() + "\n", - )) - input.WriteString(pktlineData( - commitID.String() + " " + algo.Zero().String() + " refs/heads/topic\n", - )) - input.WriteString("0000") - - err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{ - GitProtocol: "", - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - }) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - got := output.String() - if !strings.Contains(got, "ng refs/heads/main ") || !strings.Contains(got, "ng refs/heads/topic ") { - t.Fatalf("unexpected receive-pack output %q", got) - } - - _, err = repo.Refs().Resolve("refs/heads/main") - if err != nil { - t.Fatalf("Resolve(main): %v", err) - } - - _, err = repo.Refs().Resolve("refs/heads/topic") - if err != nil { - t.Fatalf("Resolve(topic): %v", err) - } - }) -} - -func TestReceivePackAdvertisesResolvedHEAD(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := testRepo.MakeCommit(t, "base") - testRepo.UpdateRef(t, "refs/heads/main", commitID) - testRepo.SymbolicRef(t, "HEAD", "refs/heads/main") - - repo := testRepo.OpenRepository(t) - - var ( - input strings.Builder - output bufferWriteFlusher - ) - - input.WriteString("0000") - - err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - }) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - got := output.String() - - want := commitID.String() + " HEAD" - if !strings.Contains(got, want) { - t.Fatalf("HEAD advertisement missing %q in %q", want, got) - } - }) -} - -func TestReceivePackVersion2FallsBackToV0(t *testing.T) { - t.Parallel() - - testReceivePackProtocolFallback(t, "version=2") -} - -func TestReceivePackHighestRequestedVersionFallsBackToV0ForV2(t *testing.T) { - t.Parallel() - - testReceivePackProtocolFallback(t, "version=1:version=2") -} - -func TestReceivePackWithoutReportStatusWritesNoStatusPayload(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := testRepo.MakeCommit(t, "base") - testRepo.UpdateRef(t, "refs/heads/main", commitID) - - repo := testRepo.OpenRepository(t) - - var ( - input strings.Builder - output bufferWriteFlusher - ) - - input.WriteString(pktlineData( - commitID.String() + " " + algo.Zero().String() + " refs/heads/main\x00delete-refs atomic object-format=" + algo.String() + "\n", - )) - input.WriteString("0000") - - err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - }) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - got := output.String() - if strings.Contains(got, "unpack ") || strings.Contains(got, "ng refs/heads/main ") || strings.Contains(got, "ok refs/heads/main\n") { - t.Fatalf("unexpected status payload %q", got) - } - }) -} - -func testReceivePackProtocolFallback(t *testing.T, gitProtocol string) { - t.Helper() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := testRepo.MakeCommit(t, "base") - testRepo.UpdateRef(t, "refs/heads/main", commitID) - - repo := testRepo.OpenRepository(t) - - var ( - input strings.Builder - output bufferWriteFlusher - ) - - input.WriteString(pktlineData( - commitID.String() + " " + algo.Zero().String() + " refs/heads/main\x00report-status atomic delete-refs object-format=" + algo.String() + "\n", - )) - input.WriteString("0000") - - err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{ - GitProtocol: gitProtocol, - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - }) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - if strings.HasPrefix(output.String(), pktlineData("version 1\n")) { - t.Fatalf("receive-pack output started with protocol v1 preface for %q: %q", gitProtocol, output.String()) - } - }) -} - -func TestReceivePackPackRequestWithoutObjectIngressReportsNotConfigured(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := testRepo.MakeCommit(t, "base") - testRepo.UpdateRef(t, "refs/heads/main", commitID) - - repo := testRepo.OpenRepository(t) - - var ( - input strings.Builder - output bufferWriteFlusher - ) - - input.WriteString(pktlineData( - commitID.String() + " " + commitID.String() + " refs/heads/main\x00report-status object-format=" + algo.String() + "\n", - )) - input.WriteString("0000") - - err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - }) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - got := output.String() - if !strings.Contains(got, "unpack object ingress not configured\n") { - t.Fatalf("unexpected receive-pack output %q", got) - } - }) -} - -func TestReceivePackPackCreatePromotesObjectsAndUpdatesRef(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - sender := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := sender.MakeCommit(t, "pushed commit") - - receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - repo := receiver.OpenRepository(t) - objectIngress := openReceivePackIngress(t, receiver, algo) - - packStream := sender.PackObjectsReader(t, []string{commitID.String()}, false) - t.Cleanup(func() { - _ = packStream.Close() - }) - - var ( - input strings.Builder - output bufferWriteFlusher - ) - - input.WriteString(pktlineData( - algo.Zero().String() + " " + commitID.String() + " refs/heads/main\x00report-status-v2 atomic object-format=" + algo.String() + "\n", - )) - input.WriteString("0000") - - err := receivepack.ReceivePack( - context.Background(), - &output, - io.MultiReader(strings.NewReader(input.String()), packStream), - receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - ObjectIngress: objectIngress, - }, - ) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - got := output.String() - if !strings.Contains(got, "unpack ok\n") || !strings.Contains(got, "ok refs/heads/main\n") { - t.Fatalf("unexpected receive-pack output %q", got) - } - - reopened := receiver.OpenRepository(t) - - resolved, err := reopened.Refs().ResolveToDetached("refs/heads/main") - if err != nil { - t.Fatalf("ResolveToDetached(main): %v", err) - } - - if resolved.ID != commitID { - t.Fatalf("refs/heads/main = %s, want %s", resolved.ID, commitID) - } - - if gotType := receiver.Run(t, "cat-file", "-t", commitID.String()); gotType != "commit" { - t.Fatalf("cat-file -t = %q, want commit", gotType) - } - - packs := receiver.Run(t, "count-objects", "-v") - if !strings.Contains(packs, "packs: 1") { - t.Fatalf("count-objects output missing promoted pack: %q", packs) - } - }) -} - -func TestReceivePackHookSeesQuarantinedObjectsAndCanRejectBeforePromotion(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - sender := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := sender.MakeCommit(t, "pushed commit") - - receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - repo := receiver.OpenRepository(t) - objectIngress := openReceivePackIngress(t, receiver, algo) - - packStream := sender.PackObjectsReader(t, []string{commitID.String()}, false) - t.Cleanup(func() { - _ = packStream.Close() - }) - - var ( - input strings.Builder - output bufferWriteFlusher - hookCalled bool - ) - - input.WriteString(pktlineData( - algo.Zero().String() + " " + commitID.String() + " refs/heads/main\x00report-status-v2 atomic object-format=" + algo.String() + "\n", - )) - input.WriteString("0000") - - err := receivepack.ReceivePack( - context.Background(), - &output, - io.MultiReader(strings.NewReader(input.String()), packStream), - receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - ObjectIngress: objectIngress, - Hook: func(ctx context.Context, req receivepack.HookRequest) ([]receivepack.UpdateDecision, error) { - hookCalled = true - - if len(req.Updates) != 1 || req.Updates[0].NewID != commitID { - t.Fatalf("unexpected hook updates: %+v", req.Updates) - } - - _, _, err := req.ExistingObjects.ReadHeader(commitID) - if err == nil { - t.Fatalf("existing objects unexpectedly contained quarantined commit %s", commitID) - } - - _, _, err = req.QuarantinedObjects.ReadHeader(commitID) - if err != nil { - t.Fatalf("quarantined objects missing commit %s: %v", commitID, err) - } - - return []receivepack.UpdateDecision{{ - Accept: false, - Message: "blocked by hook", - }}, nil - }, - }, - ) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - if !hookCalled { - t.Fatal("hook was not called") - } - - got := output.String() - if !strings.Contains(got, "unpack ok\n") || !strings.Contains(got, "ng refs/heads/main blocked by hook\n") { - t.Fatalf("unexpected receive-pack output %q", got) - } - - _, err = repo.Refs().Resolve("refs/heads/main") - if err == nil { - t.Fatal("refs/heads/main exists after hook rejection") - } - - packs := receiver.Run(t, "count-objects", "-v") - if !strings.Contains(packs, "packs: 0") { - t.Fatalf("count-objects output shows unexpected promoted pack: %q", packs) - } - }) -} - -func TestReceivePackHookCanRejectSubsetOfNonAtomicDeleteOnlyPush(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := testRepo.MakeCommit(t, "base") - testRepo.UpdateRef(t, "refs/heads/main", commitID) - testRepo.UpdateRef(t, "refs/heads/topic", commitID) - - repo := testRepo.OpenRepository(t) - - var ( - input strings.Builder - output bufferWriteFlusher - ) - - input.WriteString(pktlineData( - commitID.String() + " " + algo.Zero().String() + " refs/heads/main\x00report-status delete-refs object-format=" + algo.String() + "\n", - )) - input.WriteString(pktlineData( - commitID.String() + " " + algo.Zero().String() + " refs/heads/topic\n", - )) - input.WriteString("0000") - - err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - Hook: func(ctx context.Context, req receivepack.HookRequest) ([]receivepack.UpdateDecision, error) { - return []receivepack.UpdateDecision{ - {Accept: false, Message: "leave main alone"}, - {Accept: true}, - }, nil - }, - }) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - got := output.String() - if !strings.Contains(got, "ng refs/heads/main leave main alone\n") || !strings.Contains(got, "ok refs/heads/topic\n") { - t.Fatalf("unexpected receive-pack output %q", got) - } - - _, err = repo.Refs().Resolve("refs/heads/main") - if err != nil { - t.Fatalf("Resolve(main): %v", err) - } - - _, err = repo.Refs().Resolve("refs/heads/topic") - if err == nil { - t.Fatal("refs/heads/topic still exists after successful delete") - } - }) -} - -func TestReceivePackHookProgressUsesSideBand64K(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := testRepo.MakeCommit(t, "base") - testRepo.UpdateRef(t, "refs/heads/main", commitID) - - repo := testRepo.OpenRepository(t) - - var ( - input strings.Builder - output bufferWriteFlusher - ) - - input.WriteString(pktlineData( - commitID.String() + " " + algo.Zero().String() + " refs/heads/main\x00report-status side-band-64k atomic delete-refs object-format=" + algo.String() + "\n", - )) - input.WriteString("0000") - - err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - Hook: func(ctx context.Context, req receivepack.HookRequest) ([]receivepack.UpdateDecision, error) { - _, err := io.WriteString(req.IO.Progress, "hook says hello\n") - if err != nil { - return nil, err - } - - return []receivepack.UpdateDecision{{Accept: true}}, nil - }, - }) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - _, sidebandWire, ok := strings.Cut(output.String(), "0000") - if !ok { - t.Fatalf("output missing advertisement flush: %q", output.String()) - } - - dec := sideband64k.NewDecoder(strings.NewReader(sidebandWire), sideband64k.ReadOptions{}) - - sawHookProgress := false - - var frame sideband64k.Frame - - for { - var err error - - frame, err = dec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame: %v", err) - } - - if frame.Type == sideband64k.FrameProgress && string(frame.Payload) == "hook says hello\n" { - sawHookProgress = true - } - - if frame.Type == sideband64k.FrameData { - break - } - } - - if !sawHookProgress { - t.Fatal("missing hook progress frame") - } - - statusDec := pktline.NewDecoder(strings.NewReader(string(frame.Payload)), pktline.ReadOptions{}) - - statusFrame, err := statusDec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame(status unpack): %v", err) - } - - if statusFrame.Type != pktline.PacketData || string(statusFrame.Payload) != "unpack ok\n" { - t.Fatalf("status frame = %#v", statusFrame) - } - }) -} - -func TestReceivePackPredefinedRejectForcePushHookRejectsNonFastForward(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - _, treeID := testRepo.MakeSingleFileTree(t, "base.txt", []byte("base\n")) - baseID := testRepo.CommitTree(t, treeID, "base") - currentID := testRepo.CommitTree(t, treeID, "current", baseID) - forcedID := testRepo.CommitTree(t, treeID, "forced", baseID) - testRepo.UpdateRef(t, "refs/heads/main", currentID) - - repo := testRepo.OpenRepository(t) - objectIngress := openReceivePackIngress(t, testRepo, algo) - packStream := testRepo.PackObjectsReader(t, []string{forcedID.String(), "^" + currentID.String()}, false) - t.Cleanup(func() { - _ = packStream.Close() - }) - - var ( - input strings.Builder - output bufferWriteFlusher - ) - - input.WriteString(pktlineData( - currentID.String() + " " + forcedID.String() + " refs/heads/main\x00report-status atomic object-format=" + algo.String() + "\n", - )) - input.WriteString("0000") - - err := receivepack.ReceivePack( - context.Background(), - &output, - io.MultiReader(strings.NewReader(input.String()), packStream), - receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - ObjectIngress: objectIngress, - Hook: receivepackhooks.RejectForcePush(), - }, - ) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - got := output.String() - if !strings.Contains(got, "ng refs/heads/main non-fast-forward\n") { - t.Fatalf("unexpected receive-pack output %q", got) - } - - resolved, err := repo.Refs().ResolveToDetached("refs/heads/main") - if err != nil { - t.Fatalf("ResolveToDetached(main): %v", err) - } - - if resolved.ID != currentID { - t.Fatalf("refs/heads/main = %s, want %s", resolved.ID, currentID) - } - }) -} - -func TestReceivePackReportStatusV2IncludesRefDetails(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := testRepo.MakeCommit(t, "base") - testRepo.UpdateRef(t, "refs/heads/main", commitID) - - repo := testRepo.OpenRepository(t) - - var ( - input strings.Builder - output bufferWriteFlusher - ) - - input.WriteString(pktlineData( - commitID.String() + " " + algo.Zero().String() + " refs/heads/main\x00report-status-v2 atomic delete-refs object-format=" + algo.String() + "\n", - )) - input.WriteString("0000") - - err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - }) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - got := output.String() - if !strings.Contains(got, "option refname refs/heads/main\n") { - t.Fatalf("missing option refname in %q", got) - } - - if !strings.Contains(got, "option old-oid "+commitID.String()+"\n") { - t.Fatalf("missing option old-oid in %q", got) - } - - if !strings.Contains(got, "option new-oid "+algo.Zero().String()+"\n") { - t.Fatalf("missing option new-oid in %q", got) - } - }) -} - -func TestReceivePackGitPushCreatesBranch(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - t.Parallel() - - sender := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := sender.MakeCommit(t, "pushed commit") - sender.UpdateRef(t, "refs/heads/main", commitID) - - receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - repo := receiver.OpenRepository(t) - objectIngress := openReceivePackIngress(t, receiver, algo) - - stdout, stderr, clientErr, serverErr := runGitPushFD( - t, - sender, - receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - ObjectIngress: objectIngress, - }, - "push", "--porcelain", "fd::3,4/test", "refs/heads/main:refs/heads/main", - ) - if clientErr != nil { - t.Fatalf("git push failed: %v\nstdout=%s\nstderr=%s", clientErr, stdout, stderr) - } - - if serverErr != nil { - t.Fatalf("ReceivePack: %v", serverErr) - } - - resolved, err := receiver.OpenRepository(t).Refs().ResolveToDetached("refs/heads/main") - if err != nil { - t.Fatalf("ResolveToDetached(main): %v", err) - } - - if resolved.ID != commitID { - t.Fatalf("refs/heads/main = %s, want %s", resolved.ID, commitID) - } - }) -} - -func TestReceivePackGitPushRefUpdateWithoutNewObjectsSucceeds(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - t.Parallel() - - sender := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - blobID, treeID := sender.MakeSingleFileTree(t, "base.txt", []byte("base\n")) - commitID := sender.CommitTree(t, treeID, "base") - sender.UpdateRef(t, "refs/heads/main", commitID) - - receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - receiver.HashObject(t, "blob", sender.RunBytes(t, "cat-file", "blob", blobID.String())) - receiver.HashObject(t, "tree", sender.RunBytes(t, "cat-file", "tree", treeID.String())) - receiver.HashObject(t, "commit", sender.RunBytes(t, "cat-file", "commit", commitID.String())) - receiver.UpdateRef(t, "refs/heads/main", commitID) - - repo := receiver.OpenRepository(t) - objectIngress := openReceivePackIngress(t, receiver, algo) - - stdout, stderr, clientErr, serverErr := runGitPushFD( - t, - sender, - receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - ObjectIngress: objectIngress, - }, - "push", "--porcelain", "fd::3,4/test", "refs/heads/main:refs/heads/topic", - ) - if clientErr != nil { - t.Fatalf("git push failed: %v\nstdout=%s\nstderr=%s", clientErr, stdout, stderr) - } - - if serverErr != nil { - t.Fatalf("ReceivePack: %v", serverErr) - } - - resolved, err := receiver.OpenRepository(t).Refs().ResolveToDetached("refs/heads/topic") - if err != nil { - t.Fatalf("ResolveToDetached(topic): %v", err) - } - - if resolved.ID != commitID { - t.Fatalf("refs/heads/topic = %s, want %s", resolved.ID, commitID) - } - - packs := receiver.Run(t, "count-objects", "-v") - if !strings.Contains(packs, "packs: 0") { - t.Fatalf("count-objects output shows unexpected promoted pack: %q", packs) - } - }) -} - -func TestReceivePackGitPushAtomicDelete(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - t.Parallel() - - sender := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - _, _, commitID := receiver.MakeCommit(t, "base") - receiver.UpdateRef(t, "refs/heads/main", commitID) - - repo := receiver.OpenRepository(t) - - stdout, stderr, clientErr, serverErr := runGitPushFD( - t, - sender, - receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - }, - "push", "--porcelain", "--atomic", "fd::3,4/test", ":refs/heads/main", - ) - if clientErr != nil { - t.Fatalf("git push failed: %v\nstdout=%s\nstderr=%s", clientErr, stdout, stderr) - } - - if serverErr != nil { - t.Fatalf("ReceivePack: %v", serverErr) - } - - _, err := receiver.OpenRepository(t).Refs().Resolve("refs/heads/main") - if err == nil { - t.Fatal("refs/heads/main still exists after delete push") - } - }) -} - -func TestReceivePackGitPushRejectsForcedUpdateViaHook(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - t.Parallel() - - sender := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - blobID, treeID := sender.MakeSingleFileTree(t, "base.txt", []byte("base\n")) - baseID := sender.CommitTree(t, treeID, "base") - currentID := sender.CommitTree(t, treeID, "current", baseID) - forcedID := sender.CommitTree(t, treeID, "forced", baseID) - sender.UpdateRef(t, "refs/heads/main", forcedID) - - receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - receiver.HashObject(t, "blob", sender.RunBytes(t, "cat-file", "blob", blobID.String())) - receiver.HashObject(t, "tree", sender.RunBytes(t, "cat-file", "tree", treeID.String())) - receiver.HashObject(t, "commit", sender.RunBytes(t, "cat-file", "commit", baseID.String())) - receiver.HashObject(t, "commit", sender.RunBytes(t, "cat-file", "commit", currentID.String())) - receiver.UpdateRef(t, "refs/heads/main", currentID) - - repo := receiver.OpenRepository(t) - objectIngress := openReceivePackIngress(t, receiver, algo) - - stdout, stderr, clientErr, serverErr := runGitPushFD( - t, - sender, - receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - ObjectIngress: objectIngress, - Hook: receivepackhooks.RejectForcePush(), - }, - "push", "--porcelain", "--force", "fd::3,4/test", "refs/heads/main:refs/heads/main", - ) - if clientErr == nil { - t.Fatalf("git push unexpectedly succeeded\nstdout=%s\nstderr=%s", stdout, stderr) - } - - if serverErr != nil { - t.Fatalf("ReceivePack: %v", serverErr) - } - - if !strings.Contains(stdout, "non-fast-forward") && !strings.Contains(stderr, "non-fast-forward") { - t.Fatalf("git push output missing non-fast-forward message\nstdout=%s\nstderr=%s", stdout, stderr) - } - - resolved, err := receiver.OpenRepository(t).Refs().ResolveToDetached("refs/heads/main") - if err != nil { - t.Fatalf("ResolveToDetached(main): %v", err) - } - - if resolved.ID != currentID { - t.Fatalf("refs/heads/main = %s, want %s", resolved.ID, currentID) - } - }) -} - -type bufferWriteFlusher struct { - strings.Builder -} - -func (bufferWriteFlusher) Flush() error { - return nil -} - -func pktlineData(payload string) string { - return fmt.Sprintf("%04x%s", len(payload)+4, payload) -} - -func openReceivePackIngress( - tb testing.TB, - testRepo *testgit.TestRepo, - algo objectid.Algorithm, -) objectstore.Quarantiner { - tb.Helper() - - objectsRoot := testRepo.OpenObjectsRoot(tb) - - err := objectsRoot.Mkdir("pack", 0o755) - if err != nil && !os.IsExist(err) { - tb.Fatalf("Mkdir(pack): %v", err) - } - - packRoot, err := objectsRoot.OpenRoot("pack") - if err != nil { - tb.Fatalf("OpenRoot(pack): %v", err) - } - - tb.Cleanup(func() { - _ = packRoot.Close() - }) - - looseStore, err := objectloose.New(objectsRoot, algo) - if err != nil { - tb.Fatalf("loose.New: %v", err) - } - - tb.Cleanup(func() { - _ = looseStore.Close() - }) - - packedStore, err := objectpacked.New(packRoot, algo, objectpacked.Options{WriteRev: true}) - if err != nil { - tb.Fatalf("packed.New: %v", err) - } - - tb.Cleanup(func() { - _ = packedStore.Close() - }) - - return objectdual.New(looseStore, packedStore) -} - -type fileWriteFlusher struct { - *os.File -} - -func (fileWriteFlusher) Flush() error { - return nil -} - -func runGitPushFD( - tb testing.TB, - sender *testgit.TestRepo, - opts receivepack.Options, - gitArgs ...string, -) (stdout string, stderr string, clientErr error, serverErr error) { - tb.Helper() - - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - serverRead, clientWrite, err := os.Pipe() - if err != nil { - tb.Fatalf("os.Pipe(serverRead/clientWrite): %v", err) - } - - clientRead, serverWrite, err := os.Pipe() - if err != nil { - tb.Fatalf("os.Pipe(clientRead/serverWrite): %v", err) - } - - tb.Cleanup(func() { - _ = serverRead.Close() - _ = clientWrite.Close() - _ = clientRead.Close() - _ = serverWrite.Close() - }) - - go func() { - <-ctx.Done() - - _ = serverRead.Close() - _ = clientWrite.Close() - _ = clientRead.Close() - _ = serverWrite.Close() - }() - - serverErrCh := make(chan error, 1) - - go func() { - defer func() { - _ = serverRead.Close() - _ = serverWrite.Close() - }() - - serverErrCh <- receivepack.ReceivePack( - ctx, - fileWriteFlusher{serverWrite}, - serverRead, - opts, - ) - }() - - stdoutBytes, stderrBytes, clientErr := sender.RunWithExtraFilesEnvContextE( - tb, - ctx, - nil, - []*os.File{clientRead, clientWrite}, - gitArgs..., - ) - _ = clientRead.Close() - _ = clientWrite.Close() - - serverErr = <-serverErrCh - - if ctx.Err() != nil { - tb.Fatalf( - "git push fd:: timed out\nstdout=%s\nstderr=%s\nclientErr=%v\nserverErr=%v", - stdoutBytes, - stderrBytes, - clientErr, - serverErr, - ) - } - - return string(stdoutBytes), string(stderrBytes), clientErr, serverErr -} diff --git a/network/receivepack/options.go b/network/receivepack/options.go deleted file mode 100644 index 30792765..00000000 --- a/network/receivepack/options.go +++ /dev/null @@ -1,71 +0,0 @@ -package receivepack - -import ( - commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -// Options configures one receive-pack invocation. -// -// ReceivePack borrows all configured dependencies. -// -// Refs and ExistingObjects are required and must be non-nil. -// ObjectIngress is required if the invocation may need to ingest or quarantine a -// pack. -type Options struct { - // GitProtocol is the raw Git protocol version string from the transport, - // such as "version=1". - GitProtocol string - // Algorithm is the repository object ID algorithm used by the push session. - Algorithm objectid.Algorithm - // Refs is the reference store visible to the push. - Refs interface { - refstore.Reader - refstore.Transactioner - refstore.Batcher - } - // ExistingObjects is the object store visible to the push before any newly - // uploaded quarantined objects are promoted. - ExistingObjects objectstore.Reader - // ObjectIngress creates coordinated quarantines for quarantined object and - // pack ingestion during the push. - ObjectIngress objectstore.Quarantiner - // CommitGraph is an optional commit-graph snapshot corresponding to - // ExistingObjects. - CommitGraph *commitgraphread.Reader - // Hook, when non-nil, runs after pack ingestion into quarantine and before - // quarantine promotion or ref updates. Hook is borrowed for the duration of - // ReceivePack. - Hook Hook - // Agent is the receive-pack agent string advertised via capability. - // - // When empty, ReceivePack derives one from build info and falls back to - // "furgit". - Agent string - // SessionID is the advertised receive-pack session-id capability value. - // - // When empty, ReceivePack generates one random value per invocation. - SessionID string - // PushCertNonce is the advertised push-cert nonce capability value. - // - // When empty, ReceivePack generates one random value per invocation. - PushCertNonce string -} - -func validateOptions(opts Options) error { - if opts.Algorithm == 0 { - return ErrMissingAlgorithm - } - - if opts.Refs == nil { - return ErrMissingRefs - } - - if opts.ExistingObjects == nil { - return ErrMissingObjects - } - - return nil -} diff --git a/network/receivepack/receivepack.go b/network/receivepack/receivepack.go deleted file mode 100644 index d58e9fa0..00000000 --- a/network/receivepack/receivepack.go +++ /dev/null @@ -1,139 +0,0 @@ -package receivepack - -import ( - "context" - "io" - - "codeberg.org/lindenii/furgit/common/iowrap" - common "codeberg.org/lindenii/furgit/network/protocol/v0v1/server" - protoreceive "codeberg.org/lindenii/furgit/network/protocol/v0v1/server/receivepack" - "codeberg.org/lindenii/furgit/network/receivepack/service" -) - -// TODO: Some more designing to do. In particular, we'd like to have access to -// commit graphs and stored object abstractions and such here, especially because -// hooks might want to access full repos, but we risk creating -// circular dependencies if we import repository/ here. Might need an interface-ish -// design, but that risks being over-complicated. -// Theoretically we could also just give the hooks an os.Root but that -// feels a bit ugly. - -// ReceivePack serves one receive-pack session over r/w. -// -// Labels: Deps-Borrowed. -func ReceivePack( - ctx context.Context, - w iowrap.WriteFlusher, - r io.Reader, - opts Options, -) error { - err := validateOptions(opts) - if err != nil { - return err - } - - version := parseVersion(opts.GitProtocol) - - base := common.NewSession(r, w, common.Options{ - Version: version, - Algorithm: opts.Algorithm, - }) - - agent := opts.Agent - if agent == "" { - agent = defaultAgent() - } - - sessionID := opts.SessionID - if sessionID == "" { - sessionID = defaultSessionID() - } - - pushCertNonce := opts.PushCertNonce - if pushCertNonce == "" { - pushCertNonce = defaultPushCertNonce() - } - - protoSession := protoreceive.NewSession(base, protoreceive.Capabilities{ - ReportStatus: true, - ReportStatusV2: true, - DeleteRefs: true, - SideBand64K: true, - Quiet: true, - Atomic: true, - OfsDelta: true, - PushOptions: true, - PushCertNonce: pushCertNonce, - SessionID: sessionID, - ObjectFormat: opts.Algorithm, - Agent: agent, - }) - - refs, err := advertisedRefs(opts) - if err != nil { - return err - } - - err = protoSession.AdvertiseRefs(common.Advertisement{Refs: refs}) - if err != nil { - return err - } - - err = base.Flush() - if err != nil { - return err - } - - req, err := protoSession.ReadRequest() - if err != nil { - return err - } - - progress := protoSession.ProgressWriter() - - if req.Capabilities.Quiet { - progress = iowrap.NopFlush(io.Discard) - } - - serviceReq := &service.Request{ - Commands: translateCommands(req.Commands), - PushOptions: append([]string(nil), req.PushOptions...), - Atomic: req.Capabilities.Atomic, - PackExpected: req.PackExpected, - Pack: r, - } - - svc := service.New(service.Options{ - Refs: opts.Refs, - ExistingObjects: opts.ExistingObjects, - ObjectIngress: opts.ObjectIngress, - CommitGraph: opts.CommitGraph, - Progress: progress, - Hook: translateHook(opts.Hook), - HookIO: service.HookIO{ - Progress: progress, - Error: protoSession.ErrorWriter(), - }, - }) - - result, err := svc.Execute(ctx, serviceReq) - if err != nil { - return err - } - - protoResult := translateResult(result) - - if req.Capabilities.ReportStatusV2 { - err = protoSession.WriteReportStatusV2(protoResult) - if err != nil { - return err - } - } else if req.Capabilities.ReportStatus { - err = protoSession.WriteReportStatus(protoResult) - if err != nil { - return err - } - } - - return base.Flush() -} diff --git a/network/receivepack/results.go b/network/receivepack/results.go deleted file mode 100644 index d43bee73..00000000 --- a/network/receivepack/results.go +++ /dev/null @@ -1,26 +0,0 @@ -package receivepack - -import ( - protoreceive "codeberg.org/lindenii/furgit/network/protocol/v0v1/server/receivepack" - "codeberg.org/lindenii/furgit/network/receivepack/service" -) - -func translateResult(result *service.Result) protoreceive.ReportStatusResult { - out := protoreceive.ReportStatusResult{ - UnpackError: result.UnpackError, - Commands: make([]protoreceive.CommandResult, 0, len(result.Commands)), - } - - for _, command := range result.Commands { - out.Commands = append(out.Commands, protoreceive.CommandResult{ - Name: command.Name, - Error: command.Error, - RefName: command.RefName, - OldID: command.OldID, - NewID: command.NewID, - ForcedUpdate: command.ForcedUpdate, - }) - } - - return out -} diff --git a/network/receivepack/service/apply.go b/network/receivepack/service/apply.go deleted file mode 100644 index fdf3eef6..00000000 --- a/network/receivepack/service/apply.go +++ /dev/null @@ -1,137 +0,0 @@ -package service - -import ( - "codeberg.org/lindenii/furgit/internal/utils" - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -func (service *Service) applyAtomic(result *Result, commands []Command) error { - total := len(commands) - utils.BestEffortFprintf(service.opts.Progress, "updating refs: 0/%d\r", total) - - tx, err := service.opts.Refs.BeginTransaction() - if err != nil { - return err - } - - for i, command := range commands { - err = queueWriteTransaction(tx, command) - if err != nil { - _ = tx.Abort() - - fillCommandErrors(result, commands, err.Error()) - utils.BestEffortFprintf(service.opts.Progress, "updating refs: failed at %d/%d.\n", i+1, total) - - return nil - } - - utils.BestEffortFprintf(service.opts.Progress, "updating refs: %d/%d\r", i+1, total) - } - - err = tx.Commit() - if err != nil { - fillCommandErrors(result, commands, err.Error()) - utils.BestEffortFprintf(service.opts.Progress, "updating refs: failed at commit.\n") - - return nil - } - - result.Applied = true - for _, command := range commands { - result.Commands = append(result.Commands, successCommandResult(command)) - } - - utils.BestEffortFprintf(service.opts.Progress, "updating refs: done.\n") - - return nil -} - -func (service *Service) applyBatch(result *Result, commands []Command) error { - total := len(commands) - - utils.BestEffortFprintf(service.opts.Progress, "updating refs...\r") - - batch, err := service.opts.Refs.BeginBatch() - if err != nil { - return err - } - - for _, command := range commands { - err = queueWriteBatch(batch, command) - if err != nil { - _ = batch.Abort() - - fillCommandErrors(result, commands, err.Error()) - utils.BestEffortFprintf(service.opts.Progress, "updating refs: failed while queueing batch.\n") - - return nil - } - } - - batchResults, err := batch.Apply() - if err != nil && len(batchResults) == 0 { - utils.BestEffortFprintf(service.opts.Progress, "updating refs: failed at apply.\n") - - return err - } - - appliedAny := false - failedCount := 0 - - for i, command := range commands { - item := successCommandResult(command) - if i < len(batchResults) && batchResults[i].Error != nil { - item.Error = batchResults[i].Error.Error() - failedCount++ - } else { - appliedAny = true - } - - result.Commands = append(result.Commands, item) - - utils.BestEffortFprintf(service.opts.Progress, "updating refs: %d/%d\r", i+1, total) - } - - result.Applied = appliedAny - - if failedCount == 0 { - utils.BestEffortFprintf(service.opts.Progress, "updating refs: done.\n") - } else { - utils.BestEffortFprintf(service.opts.Progress, "updating refs: failed (%d/%d).\n", failedCount, total) - } - - return nil -} - -func queueWriteTransaction(tx refstore.Transaction, command Command) error { - if isDelete(command) { - return tx.Delete(command.Name, command.OldID) - } - - if command.OldID == command.OldID.Algorithm().Zero() { - return tx.Create(command.Name, command.NewID) - } - - return tx.Update(command.Name, command.NewID, command.OldID) -} - -func queueWriteBatch(batch refstore.Batch, command Command) error { - if isDelete(command) { - return batch.Delete(command.Name, command.OldID) - } - - if command.OldID == command.OldID.Algorithm().Zero() { - return batch.Create(command.Name, command.NewID) - } - - return batch.Update(command.Name, command.NewID, command.OldID) -} - -func successCommandResult(command Command) CommandResult { - return CommandResult{ - Name: command.Name, - RefName: command.Name, - OldID: new(command.OldID), - NewID: new(command.NewID), - } -} diff --git a/network/receivepack/service/command.go b/network/receivepack/service/command.go deleted file mode 100644 index 9ad50c4f..00000000 --- a/network/receivepack/service/command.go +++ /dev/null @@ -1,26 +0,0 @@ -package service - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// Command is one protocol-independent requested ref update. -type Command struct { - OldID objectid.ObjectID - NewID objectid.ObjectID - Name string -} - -func fillCommandErrors(result *Result, commands []Command, errText string) { - for _, command := range commands { - result.Commands = append(result.Commands, CommandResult{ - Name: command.Name, - Error: errText, - RefName: command.Name, - OldID: new(command.OldID), - NewID: new(command.NewID), - }) - } -} - -func isDelete(command Command) bool { - return command.NewID == command.NewID.Algorithm().Zero() -} diff --git a/network/receivepack/service/command_result.go b/network/receivepack/service/command_result.go deleted file mode 100644 index 37549f08..00000000 --- a/network/receivepack/service/command_result.go +++ /dev/null @@ -1,13 +0,0 @@ -package service - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// CommandResult is one per-command execution result. -type CommandResult struct { - Name string - Error string - RefName string - OldID *objectid.ObjectID - NewID *objectid.ObjectID - ForcedUpdate bool -} diff --git a/network/receivepack/service/doc.go b/network/receivepack/service/doc.go deleted file mode 100644 index c3fa3041..00000000 --- a/network/receivepack/service/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// Package service implements the protocol-independent receive-pack service. -// -// A Service borrows the stores, hooks, and I/O endpoints supplied in -// Options. Callers retain ownership of those dependencies and must keep them -// valid for each Execute call that uses them. -package service diff --git a/network/receivepack/service/execute.go b/network/receivepack/service/execute.go deleted file mode 100644 index 92d34a63..00000000 --- a/network/receivepack/service/execute.go +++ /dev/null @@ -1,120 +0,0 @@ -package service - -import ( - "context" - - "codeberg.org/lindenii/furgit/internal/utils" - objectstore "codeberg.org/lindenii/furgit/object/store" -) - -// Execute validates one receive-pack request, optionally ingests its pack into -// quarantine, runs the optional hook, and applies allowed ref updates. -// -// Labels: Deps-Borrowed. -func (service *Service) Execute(ctx context.Context, req *Request) (*Result, error) { - result := &Result{ - Commands: make([]CommandResult, 0, len(req.Commands)), - } - - var err error - - quarantine, ok := service.ingestQuarantine(result, req.Commands, req) - if !ok { - return result, nil - } - - if quarantine != nil { - defer func(q objectstore.Quarantine) { - _ = q.Discard() - }(quarantine) - } - - for _, command := range req.Commands { - result.Planned = append(result.Planned, PlannedUpdate{ - Name: command.Name, - OldID: command.OldID, - NewID: command.NewID, - Delete: isDelete(command), - }) - } - - if len(req.Commands) == 0 { - return result, nil - } - - allowedCommands, allowedIndices, rejected, ok, errText := service.runHook( - ctx, - req, - req.Commands, - quarantine, - ) - if !ok { - fillCommandErrors(result, req.Commands, errText) - - return result, nil - } - - if req.Atomic && len(rejected) != 0 { - result.Commands = make([]CommandResult, 0, len(req.Commands)) - for index, command := range req.Commands { - message := rejected[index] - if message == "" { - message = "atomic push rejected by hook" - } - - result.Commands = append(result.Commands, resultForHookRejection(command, message)) - } - - return result, nil - } - - if len(allowedCommands) == 0 { - result.Commands = mergeCommandResults(req.Commands, rejected, nil, nil) - - return result, nil - } - - if req.PackExpected && quarantine != nil { - // Git migrates quarantined objects into permanent storage immediately - // before starting ref updates. - utils.BestEffortFprintf(service.opts.Progress, "promoting quarantine...\r") - - err := quarantine.Promote() - if err != nil { - utils.BestEffortFprintf(service.opts.Progress, "promoting quarantine: failed: %v.\n", err) - - result.UnpackError = err.Error() - fillCommandErrors(result, req.Commands, err.Error()) - - return result, nil - } - - utils.BestEffortFprintf(service.opts.Progress, "promoting quarantine: done.\n") - } - - if req.Atomic { - subresult := &Result{} - - err := service.applyAtomic(subresult, allowedCommands) - if err != nil { - return result, err - } - - result.Commands = mergeCommandResults(req.Commands, rejected, subresult.Commands, allowedIndices) - result.Applied = subresult.Applied - - return result, nil - } - - subresult := &Result{} - - err = service.applyBatch(subresult, allowedCommands) - if err != nil { - return result, err - } - - result.Commands = mergeCommandResults(req.Commands, rejected, subresult.Commands, allowedIndices) - result.Applied = subresult.Applied - - return result, nil -} diff --git a/network/receivepack/service/hook.go b/network/receivepack/service/hook.go deleted file mode 100644 index 3826e6fb..00000000 --- a/network/receivepack/service/hook.go +++ /dev/null @@ -1,48 +0,0 @@ -package service - -import ( - "context" - - "codeberg.org/lindenii/furgit/common/iowrap" - commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -type HookIO struct { - Progress iowrap.WriteFlusher - Error iowrap.WriteFlusher -} - -type RefUpdate struct { - Name string - OldID objectid.ObjectID - NewID objectid.ObjectID -} - -type UpdateDecision struct { - Accept bool - Message string -} - -// HookRequest is the view passed to one Hook invocation. -// -// Labels: Life-Call. -type HookRequest struct { - Refs refstore.Reader - ExistingObjects objectstore.Reader - // QuarantinedObjects exposes quarantined objects for this push. - // - // When the push did not create a quarantine, QuarantinedObjects is nil. - QuarantinedObjects objectstore.Reader - CommitGraph *commitgraphread.Reader - Updates []RefUpdate - PushOptions []string - IO HookIO -} - -// Hook is an optional per-request validation hook. -// -// The returned decisions must have the same length as HookRequest.Updates. -type Hook func(context.Context, HookRequest) ([]UpdateDecision, error) diff --git a/network/receivepack/service/hook_apply.go b/network/receivepack/service/hook_apply.go deleted file mode 100644 index 97d25009..00000000 --- a/network/receivepack/service/hook_apply.go +++ /dev/null @@ -1,31 +0,0 @@ -package service - -func resultForHookRejection(command Command, message string) CommandResult { - result := successCommandResult(command) - result.Error = message - - return result -} - -func mergeCommandResults( - commands []Command, - rejected map[int]string, - applied []CommandResult, - appliedIndices []int, -) []CommandResult { - out := make([]CommandResult, len(commands)) - - for index, message := range rejected { - out[index] = resultForHookRejection(commands[index], message) - } - - for i, appliedResult := range applied { - if i >= len(appliedIndices) { - break - } - - out[appliedIndices[i]] = appliedResult - } - - return out -} diff --git a/network/receivepack/service/ingest_quarantine.go b/network/receivepack/service/ingest_quarantine.go deleted file mode 100644 index 75f3a790..00000000 --- a/network/receivepack/service/ingest_quarantine.go +++ /dev/null @@ -1,81 +0,0 @@ -package service - -import ( - "codeberg.org/lindenii/furgit/internal/utils" - objectstore "codeberg.org/lindenii/furgit/object/store" -) - -func (service *Service) ingestQuarantine( - result *Result, - commands []Command, - req *Request, -) (objectstore.Quarantine, bool) { - if !req.PackExpected { - return nil, true - } - - if req.Pack == nil { - utils.BestEffortFprintf(service.opts.Progress, "unpack failed: missing pack stream.\n") - - result.UnpackError = "missing pack stream" - fillCommandErrors(result, commands, "missing pack stream") - - return nil, false - } - - if service.opts.ObjectIngress == nil { - utils.BestEffortFprintf(service.opts.Progress, "unpack failed: object ingress not configured.\n") - - result.UnpackError = "object ingress not configured" - fillCommandErrors(result, commands, "object ingress not configured") - - return nil, false - } - - var err error - - err = service.opts.ExistingObjects.Refresh() - if err != nil { - utils.BestEffortFprintf(service.opts.Progress, "unpack failed: refresh existing objects: %v.\n", err) - - result.UnpackError = err.Error() - fillCommandErrors(result, commands, err.Error()) - - return nil, false - } - - utils.BestEffortFprintf(service.opts.Progress, "creating quarantine...\r") - - quarantine, err := service.opts.ObjectIngress.BeginQuarantine(objectstore.QuarantineOptions{}) - if err != nil { - utils.BestEffortFprintf(service.opts.Progress, "unpack failed: %v.\n", err) - - result.UnpackError = err.Error() - fillCommandErrors(result, commands, err.Error()) - - return nil, false - } - - utils.BestEffortFprintf(service.opts.Progress, "creating quarantine: done.\n") - utils.BestEffortFprintf(service.opts.Progress, "unpacking...\r") - - err = quarantine.WritePack(req.Pack, objectstore.PackWriteOptions{ - ThinBase: service.opts.ExistingObjects, - Progress: service.opts.Progress, - RequireTrailingEOF: false, - }) - if err != nil { - utils.BestEffortFprintf(service.opts.Progress, "unpack failed: %v.\n", err) - - result.UnpackError = err.Error() - fillCommandErrors(result, commands, err.Error()) - - _ = quarantine.Discard() - - return nil, false - } - - utils.BestEffortFprintf(service.opts.Progress, "unpacking: done.\n") - - return quarantine, true -} diff --git a/network/receivepack/service/options.go b/network/receivepack/service/options.go deleted file mode 100644 index b6e71d64..00000000 --- a/network/receivepack/service/options.go +++ /dev/null @@ -1,31 +0,0 @@ -package service - -import ( - "codeberg.org/lindenii/furgit/common/iowrap" - commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read" - objectstore "codeberg.org/lindenii/furgit/object/store" - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -// Options configures one protocol-independent receive-pack service. -// -// Service borrows all configured dependencies. -// -// Refs and ExistingObjects are required and must be non-nil. -// ObjectIngress is required if Execute may need to ingest or quarantine a -// pack. -// CommitGraph, Progress, Hook, and HookIO are optional; when provided they are also -// borrowed for the duration of Execute. -type Options struct { - Refs interface { - refstore.Reader - refstore.Transactioner - refstore.Batcher - } - ExistingObjects objectstore.Reader - ObjectIngress objectstore.Quarantiner - CommitGraph *commitgraphread.Reader - Progress iowrap.WriteFlusher - Hook Hook - HookIO HookIO -} diff --git a/network/receivepack/service/request.go b/network/receivepack/service/request.go deleted file mode 100644 index 33e3796f..00000000 --- a/network/receivepack/service/request.go +++ /dev/null @@ -1,15 +0,0 @@ -package service - -import "io" - -// Request is one protocol-independent receive-pack execution request. -// -// If PackExpected is true, Pack must be non-nil and remain valid until -// Execute finishes consuming it. -type Request struct { - Commands []Command - PushOptions []string - Atomic bool - PackExpected bool - Pack io.Reader -} diff --git a/network/receivepack/service/result.go b/network/receivepack/service/result.go deleted file mode 100644 index 7a75be11..00000000 --- a/network/receivepack/service/result.go +++ /dev/null @@ -1,9 +0,0 @@ -package service - -// Result is one receive-pack execution result. -type Result struct { - UnpackError string - Commands []CommandResult - Planned []PlannedUpdate - Applied bool -} diff --git a/network/receivepack/service/run_hook.go b/network/receivepack/service/run_hook.go deleted file mode 100644 index c8b1b76c..00000000 --- a/network/receivepack/service/run_hook.go +++ /dev/null @@ -1,93 +0,0 @@ -package service - -import ( - "context" - - "codeberg.org/lindenii/furgit/internal/utils" - objectstore "codeberg.org/lindenii/furgit/object/store" -) - -func (service *Service) runHook( - ctx context.Context, - req *Request, - commands []Command, - quarantinedObjects objectstore.Reader, -) ( - allowedCommands []Command, - allowedIndices []int, - rejected map[int]string, - ok bool, - errText string, -) { - allowedCommands = append([]Command(nil), commands...) - - allowedIndices = make([]int, 0, len(commands)) - for index := range commands { - allowedIndices = append(allowedIndices, index) - } - - rejected = make(map[int]string) - if service.opts.Hook == nil { - return allowedCommands, allowedIndices, rejected, true, "" - } - - utils.BestEffortFprintf(service.opts.Progress, "running hooks...\r") - - updates := make([]RefUpdate, 0, len(commands)) - for _, command := range commands { - updates = append(updates, RefUpdate{ - Name: command.Name, - OldID: command.OldID, - NewID: command.NewID, - }) - } - - decisions, err := service.opts.Hook(ctx, HookRequest{ - Refs: service.opts.Refs, - ExistingObjects: service.opts.ExistingObjects, - QuarantinedObjects: quarantinedObjects, - CommitGraph: service.opts.CommitGraph, - Updates: updates, - PushOptions: append([]string(nil), req.PushOptions...), - IO: service.opts.HookIO, - }) - if err != nil { - utils.BestEffortFprintf(service.opts.Progress, "running hooks: failed: %v.\n", err) - - return nil, nil, nil, false, err.Error() - } - - if len(decisions) != len(commands) { - utils.BestEffortFprintf(service.opts.Progress, "running hooks: failed: wrong decision count.\n") - - return nil, nil, nil, false, "hook returned wrong number of update decisions" - } - - allowedCommands = allowedCommands[:0] - allowedIndices = allowedIndices[:0] - - for index, decision := range decisions { - if decision.Accept { - allowedCommands = append(allowedCommands, commands[index]) - allowedIndices = append(allowedIndices, index) - - continue - } - - message := decision.Message - if message == "" { - message = "rejected by hook" - } - - rejected[index] = message - } - - utils.BestEffortFprintf( - service.opts.Progress, - "running hooks: done (%d/%d accepted).\n", - len(allowedCommands), - len(commands), - ) - - return allowedCommands, allowedIndices, rejected, true, "" -} diff --git a/network/receivepack/service/service.go b/network/receivepack/service/service.go deleted file mode 100644 index 0d931b64..00000000 --- a/network/receivepack/service/service.go +++ /dev/null @@ -1,13 +0,0 @@ -package service - -// Service executes protocol-independent receive-pack requests. -type Service struct { - opts Options -} - -// New creates one receive-pack service. -// -// Labels: Deps-Borrowed, Life-Parent. -func New(opts Options) *Service { - return &Service{opts: opts} -} diff --git a/network/receivepack/service/service_test.go b/network/receivepack/service/service_test.go deleted file mode 100644 index 94e105da..00000000 --- a/network/receivepack/service/service_test.go +++ /dev/null @@ -1,129 +0,0 @@ -package service_test - -import ( - "context" - "os" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - "codeberg.org/lindenii/furgit/network/receivepack/service" - objectid "codeberg.org/lindenii/furgit/object/id" - objectstore "codeberg.org/lindenii/furgit/object/store" - objectdual "codeberg.org/lindenii/furgit/object/store/dual" - objectloose "codeberg.org/lindenii/furgit/object/store/loose" - "codeberg.org/lindenii/furgit/object/store/memory" - objectpacked "codeberg.org/lindenii/furgit/object/store/packed" -) - -func TestExecutePackExpectedWithoutObjectIngress(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - store := memory.New(algo) - svc := service.New(service.Options{ - ExistingObjects: store, - }) - - result, err := svc.Execute(context.Background(), &service.Request{ - Commands: []service.Command{{ - Name: "refs/heads/main", - OldID: algo.Zero(), - NewID: algo.Zero(), - }}, - PackExpected: true, - Pack: strings.NewReader("not a pack"), - }) - if err != nil { - t.Fatalf("Execute: %v", err) - } - - if result.UnpackError != "object ingress not configured" { - t.Fatalf("unexpected unpack error %q", result.UnpackError) - } - }) -} - -func TestExecuteDiscardedQuarantineAfterIngestFailure(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - store := memory.New(algo) - objectIngress := newDualIngress(t, algo) - - svc := service.New(service.Options{ - ExistingObjects: store, - ObjectIngress: objectIngress, - }) - - result, err := svc.Execute(context.Background(), &service.Request{ - Commands: []service.Command{{ - Name: "refs/heads/main", - OldID: algo.Zero(), - NewID: algo.Zero(), - }}, - PackExpected: true, - Pack: strings.NewReader("not a pack"), - }) - if err != nil { - t.Fatalf("Execute: %v", err) - } - - if result.UnpackError == "" { - t.Fatal("Execute returned empty unpack error for invalid pack") - } - }) -} - -func newDualIngress(tb testing.TB, algo objectid.Algorithm) objectstore.Quarantiner { - tb.Helper() - - objectsRoot, err := os.OpenRoot(tb.TempDir()) - if err != nil { - tb.Fatalf("os.OpenRoot: %v", err) - } - - tb.Cleanup(func() { - _ = objectsRoot.Close() - }) - - err = objectsRoot.Mkdir("pack", 0o755) - if err != nil { - tb.Fatalf("Mkdir(pack): %v", err) - } - - packRoot, err := objectsRoot.OpenRoot("pack") - if err != nil { - tb.Fatalf("OpenRoot(pack): %v", err) - } - - tb.Cleanup(func() { - _ = packRoot.Close() - }) - - looseStore, err := objectloose.New(objectsRoot, algo) - if err != nil { - tb.Fatalf("loose.New: %v", err) - } - - tb.Cleanup(func() { - _ = looseStore.Close() - }) - - packedStore, err := objectpacked.New(packRoot, algo, objectpacked.Options{WriteRev: true}) - if err != nil { - tb.Fatalf("packed.New: %v", err) - } - - tb.Cleanup(func() { - _ = packedStore.Close() - }) - - return objectdual.New(looseStore, packedStore) -} diff --git a/network/receivepack/service/update.go b/network/receivepack/service/update.go deleted file mode 100644 index 753e3b02..00000000 --- a/network/receivepack/service/update.go +++ /dev/null @@ -1,11 +0,0 @@ -package service - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// PlannedUpdate is one requested ref update planned for this execution. -type PlannedUpdate struct { - Name string - OldID objectid.ObjectID - NewID objectid.ObjectID - Delete bool -} diff --git a/network/receivepack/version.go b/network/receivepack/version.go deleted file mode 100644 index 9a4544dc..00000000 --- a/network/receivepack/version.go +++ /dev/null @@ -1,35 +0,0 @@ -package receivepack - -import ( - "strings" - - common "codeberg.org/lindenii/furgit/network/protocol/v0v1/server" -) - -func parseVersion(gitProtocol string) common.Version { - if gitProtocol == "" { - return common.Version0 - } - - var highestRequested uint8 - - for field := range strings.SplitSeq(gitProtocol, ":") { - switch field { - case "version=0": - case "version=1": - if highestRequested < 1 { - highestRequested = 1 - } - case "version=2": - if highestRequested < 2 { - highestRequested = 2 - } - } - } - - if highestRequested == 1 { - return common.Version1 - } - - return common.Version0 -} -- cgit v1.3.1-10-gc9f91