aboutsummaryrefslogtreecommitdiff
path: root/internal/protostream/response.go
blob: 773ed228e85ed069e4910d24a5a1c2cfe4017682 (about) (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
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
}