1package packp
2
3import (
4	"bytes"
5	"encoding/hex"
6	"errors"
7	"fmt"
8	"io"
9
10	"github.com/go-git/go-git/v5/plumbing"
11	"github.com/go-git/go-git/v5/plumbing/format/pktline"
12)
13
14// Decode reads the next advertised-refs message form its input and
15// stores it in the AdvRefs.
16func (a *AdvRefs) Decode(r io.Reader) error {
17	d := newAdvRefsDecoder(r)
18	return d.Decode(a)
19}
20
21type advRefsDecoder struct {
22	s     *pktline.Scanner // a pkt-line scanner from the input stream
23	line  []byte           // current pkt-line contents, use parser.nextLine() to make it advance
24	nLine int              // current pkt-line number for debugging, begins at 1
25	hash  plumbing.Hash    // last hash read
26	err   error            // sticky error, use the parser.error() method to fill this out
27	data  *AdvRefs         // parsed data is stored here
28}
29
30var (
31	// ErrEmptyAdvRefs is returned by Decode if it gets an empty advertised
32	// references message.
33	ErrEmptyAdvRefs = errors.New("empty advertised-ref message")
34	// ErrEmptyInput is returned by Decode if the input is empty.
35	ErrEmptyInput = errors.New("empty input")
36)
37
38func newAdvRefsDecoder(r io.Reader) *advRefsDecoder {
39	return &advRefsDecoder{
40		s: pktline.NewScanner(r),
41	}
42}
43
44func (d *advRefsDecoder) Decode(v *AdvRefs) error {
45	d.data = v
46
47	for state := decodePrefix; state != nil; {
48		state = state(d)
49	}
50
51	return d.err
52}
53
54type decoderStateFn func(*advRefsDecoder) decoderStateFn
55
56// fills out the parser sticky error
57func (d *advRefsDecoder) error(format string, a ...interface{}) {
58	msg := fmt.Sprintf(
59		"pkt-line %d: %s", d.nLine,
60		fmt.Sprintf(format, a...),
61	)
62
63	d.err = NewErrUnexpectedData(msg, d.line)
64}
65
66// Reads a new pkt-line from the scanner, makes its payload available as
67// p.line and increments p.nLine.  A successful invocation returns true,
68// otherwise, false is returned and the sticky error is filled out
69// accordingly.  Trims eols at the end of the payloads.
70func (d *advRefsDecoder) nextLine() bool {
71	d.nLine++
72
73	if !d.s.Scan() {
74		if d.err = d.s.Err(); d.err != nil {
75			return false
76		}
77
78		if d.nLine == 1 {
79			d.err = ErrEmptyInput
80			return false
81		}
82
83		d.error("EOF")
84		return false
85	}
86
87	d.line = d.s.Bytes()
88	d.line = bytes.TrimSuffix(d.line, eol)
89
90	return true
91}
92
93// The HTTP smart prefix is often followed by a flush-pkt.
94func decodePrefix(d *advRefsDecoder) decoderStateFn {
95	if ok := d.nextLine(); !ok {
96		return nil
97	}
98
99	if !isPrefix(d.line) {
100		return decodeFirstHash
101	}
102
103	tmp := make([]byte, len(d.line))
104	copy(tmp, d.line)
105	d.data.Prefix = append(d.data.Prefix, tmp)
106	if ok := d.nextLine(); !ok {
107		return nil
108	}
109
110	if !isFlush(d.line) {
111		return decodeFirstHash
112	}
113
114	d.data.Prefix = append(d.data.Prefix, pktline.Flush)
115	if ok := d.nextLine(); !ok {
116		return nil
117	}
118
119	return decodeFirstHash
120}
121
122func isPrefix(payload []byte) bool {
123	return len(payload) > 0 && payload[0] == '#'
124}
125
126// If the first hash is zero, then a no-refs is coming. Otherwise, a
127// list-of-refs is coming, and the hash will be followed by the first
128// advertised ref.
129func decodeFirstHash(p *advRefsDecoder) decoderStateFn {
130	// If the repository is empty, we receive a flush here (HTTP).
131	if isFlush(p.line) {
132		p.err = ErrEmptyAdvRefs
133		return nil
134	}
135
136	if len(p.line) < hashSize {
137		p.error("cannot read hash, pkt-line too short")
138		return nil
139	}
140
141	if _, err := hex.Decode(p.hash[:], p.line[:hashSize]); err != nil {
142		p.error("invalid hash text: %s", err)
143		return nil
144	}
145
146	p.line = p.line[hashSize:]
147
148	if p.hash.IsZero() {
149		return decodeSkipNoRefs
150	}
151
152	return decodeFirstRef
153}
154
155// Skips SP "capabilities^{}" NUL
156func decodeSkipNoRefs(p *advRefsDecoder) decoderStateFn {
157	if len(p.line) < len(noHeadMark) {
158		p.error("too short zero-id ref")
159		return nil
160	}
161
162	if !bytes.HasPrefix(p.line, noHeadMark) {
163		p.error("malformed zero-id ref")
164		return nil
165	}
166
167	p.line = p.line[len(noHeadMark):]
168
169	return decodeCaps
170}
171
172// decode the refname, expects SP refname NULL
173func decodeFirstRef(l *advRefsDecoder) decoderStateFn {
174	if len(l.line) < 3 {
175		l.error("line too short after hash")
176		return nil
177	}
178
179	if !bytes.HasPrefix(l.line, sp) {
180		l.error("no space after hash")
181		return nil
182	}
183	l.line = l.line[1:]
184
185	chunks := bytes.SplitN(l.line, null, 2)
186	if len(chunks) < 2 {
187		l.error("NULL not found")
188		return nil
189	}
190	ref := chunks[0]
191	l.line = chunks[1]
192
193	if bytes.Equal(ref, []byte(head)) {
194		l.data.Head = &l.hash
195	} else {
196		l.data.References[string(ref)] = l.hash
197	}
198
199	return decodeCaps
200}
201
202func decodeCaps(p *advRefsDecoder) decoderStateFn {
203	if err := p.data.Capabilities.Decode(p.line); err != nil {
204		p.error("invalid capabilities: %s", err)
205		return nil
206	}
207
208	return decodeOtherRefs
209}
210
211// The refs are either tips (obj-id SP refname) or a peeled (obj-id SP refname^{}).
212// If there are no refs, then there might be a shallow or flush-ptk.
213func decodeOtherRefs(p *advRefsDecoder) decoderStateFn {
214	if ok := p.nextLine(); !ok {
215		return nil
216	}
217
218	if bytes.HasPrefix(p.line, shallow) {
219		return decodeShallow
220	}
221
222	if len(p.line) == 0 {
223		return nil
224	}
225
226	saveTo := p.data.References
227	if bytes.HasSuffix(p.line, peeled) {
228		p.line = bytes.TrimSuffix(p.line, peeled)
229		saveTo = p.data.Peeled
230	}
231
232	ref, hash, err := readRef(p.line)
233	if err != nil {
234		p.error("%s", err)
235		return nil
236	}
237	saveTo[ref] = hash
238
239	return decodeOtherRefs
240}
241
242// Reads a ref-name
243func readRef(data []byte) (string, plumbing.Hash, error) {
244	chunks := bytes.Split(data, sp)
245	switch {
246	case len(chunks) == 1:
247		return "", plumbing.ZeroHash, fmt.Errorf("malformed ref data: no space was found")
248	case len(chunks) > 2:
249		return "", plumbing.ZeroHash, fmt.Errorf("malformed ref data: more than one space found")
250	default:
251		return string(chunks[1]), plumbing.NewHash(string(chunks[0])), nil
252	}
253}
254
255// Keeps reading shallows until a flush-pkt is found
256func decodeShallow(p *advRefsDecoder) decoderStateFn {
257	if !bytes.HasPrefix(p.line, shallow) {
258		p.error("malformed shallow prefix, found %q... instead", p.line[:len(shallow)])
259		return nil
260	}
261	p.line = bytes.TrimPrefix(p.line, shallow)
262
263	if len(p.line) != hashSize {
264		p.error(fmt.Sprintf(
265			"malformed shallow hash: wrong length, expected 40 bytes, read %d bytes",
266			len(p.line)))
267		return nil
268	}
269
270	text := p.line[:hashSize]
271	var h plumbing.Hash
272	if _, err := hex.Decode(h[:], text); err != nil {
273		p.error("invalid hash text: %s", err)
274		return nil
275	}
276
277	p.data.Shallows = append(p.data.Shallows, h)
278
279	if ok := p.nextLine(); !ok {
280		return nil
281	}
282
283	if len(p.line) == 0 {
284		return nil // successful parse of the advertised-refs message
285	}
286
287	return decodeShallow
288}
289