aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Runxi Yu2026-01-30 22:02:35 +0100
committerGravatar Runxi Yu2026-01-30 22:02:35 +0100
commitc91ccc8d139dbf967b73262265712b9ee37cdbf1 (patch)
treef9989242255652144103918d98bae86d321f436d
parentpktline: Make Reader/Writer structs (diff)
signatureNo signature
protostream: Add a helper package to frame protocol-v2 responses
This should take care of sideband-all
-rw-r--r--internal/protostream/response.go143
1 files changed, 143 insertions, 0 deletions
diff --git a/internal/protostream/response.go b/internal/protostream/response.go
new file mode 100644
index 00000000..773ed228
--- /dev/null
+++ b/internal/protostream/response.go
@@ -0,0 +1,143 @@
+// Package protostream provides helpers for framing protocol-v2 responses.
+package protostream
+
+import (
+ "io"
+
+ "codeberg.org/lindenii/furgit/pktline"
+)
+
+// StreamCode identifies the multiplexed stream in a side-band pkt-line payload.
+type StreamCode byte
+
+const (
+ StreamData StreamCode = 1
+ StreamProgress StreamCode = 2
+ StreamError StreamCode = 3
+)
+
+const (
+ // maxPktLineData is the maximum pkt-line payload size (len excludes header).
+ // See gitprotocol-common(5); the maximum pkt-line length is 65520, including header.
+ maxPktLineData = 65516
+ // maxSidebandData is max pkt-line payload minus 1 byte for the stream code.
+ maxSidebandData = maxPktLineData - 1
+)
+
+// ResponseOptions configures how responses are framed.
+type ResponseOptions struct {
+ // NoProgress suppresses progress messages (stream code 2).
+ NoProgress bool
+ // SidebandAll enables side-banding for all non-flush/delim/response-end pkt-lines.
+ SidebandAll bool
+}
+
+// ResponseWriter writes protocol-v2 responses with optional side-band multiplexing.
+type ResponseWriter struct {
+ pw *pktline.Writer
+ opts ResponseOptions
+}
+
+// NewResponseWriter returns a ResponseWriter wrapping pw.
+func NewResponseWriter(pw *pktline.Writer, opts ResponseOptions) *ResponseWriter {
+ return &ResponseWriter{
+ pw: pw,
+ opts: opts,
+ }
+}
+
+// WriteLine writes a pkt-line payload. If SidebandAll is enabled, the payload
+// is sent on StreamData.
+func (rw *ResponseWriter) WriteLine(payload []byte) error {
+ if rw.opts.SidebandAll {
+ return rw.writeStream(StreamData, payload, false)
+ }
+ return rw.pw.WriteLine(payload)
+}
+
+// Flush writes a flush-pkt.
+func (rw *ResponseWriter) Flush() error {
+ return rw.pw.Flush()
+}
+
+// Delim writes a delim-pkt.
+func (rw *ResponseWriter) Delim() error {
+ return rw.pw.Delim()
+}
+
+// ResponseEnd writes a response-end pkt.
+func (rw *ResponseWriter) ResponseEnd() error {
+ return rw.pw.ResponseEnd()
+}
+
+// PackWriter returns an io.Writer that emits pack data on StreamData.
+func (rw *ResponseWriter) PackWriter() io.Writer {
+ return packWriter{rw: rw}
+}
+
+// WritePack writes pack data on StreamData.
+func (rw *ResponseWriter) WritePack(p []byte) error {
+ if len(p) == 0 {
+ return nil
+ }
+ return rw.writeStream(StreamData, p, false)
+}
+
+// WriteProgress writes a progress message on StreamProgress.
+func (rw *ResponseWriter) WriteProgress(p []byte) error {
+ if rw.opts.NoProgress || len(p) == 0 {
+ return nil
+ }
+ return rw.writeStream(StreamProgress, p, false)
+}
+
+// WriteError writes a fatal error message on StreamError.
+func (rw *ResponseWriter) WriteError(p []byte) error {
+ if len(p) == 0 {
+ return nil
+ }
+ return rw.writeStream(StreamError, p, false)
+}
+
+// Keepalive writes a side-band progress keepalive ("0005\\2").
+func (rw *ResponseWriter) Keepalive() error {
+ if rw.opts.NoProgress {
+ return nil
+ }
+ return rw.writeStream(StreamProgress, nil, true)
+}
+
+func (rw *ResponseWriter) writeStream(code StreamCode, p []byte, allowEmpty bool) error {
+ if !allowEmpty && len(p) == 0 {
+ return nil
+ }
+
+ buf := make([]byte, 1+maxSidebandData)
+ for len(p) > 0 || allowEmpty {
+ n := len(p)
+ if n > maxSidebandData {
+ n = maxSidebandData
+ }
+ buf[0] = byte(code)
+ if n > 0 {
+ copy(buf[1:], p[:n])
+ }
+ if err := rw.pw.WriteLine(buf[:1+n]); err != nil {
+ return err
+ }
+ p = p[n:]
+ allowEmpty = false
+ }
+ return nil
+}
+
+type packWriter struct {
+ rw *ResponseWriter
+}
+
+func (pw packWriter) Write(p []byte) (int, error) {
+ if err := pw.rw.WritePack(p); err != nil {
+ return 0, err
+ }
+ return len(p), nil
+}