1package packp
2
3import (
4	"bufio"
5	"bytes"
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
14const ackLineLen = 44
15
16// ServerResponse object acknowledgement from upload-pack service
17type ServerResponse struct {
18	ACKs []plumbing.Hash
19}
20
21// Decode decodes the response into the struct, isMultiACK should be true, if
22// the request was done with multi_ack or multi_ack_detailed capabilities.
23func (r *ServerResponse) Decode(reader *bufio.Reader, isMultiACK bool) error {
24	// TODO: implement support for multi_ack or multi_ack_detailed responses
25	if isMultiACK {
26		return errors.New("multi_ack and multi_ack_detailed are not supported")
27	}
28
29	s := pktline.NewScanner(reader)
30
31	for s.Scan() {
32		line := s.Bytes()
33
34		if err := r.decodeLine(line); err != nil {
35			return err
36		}
37
38		// we need to detect when the end of a response header and the beginning
39		// of a packfile header happened, some requests to the git daemon
40		// produces a duplicate ACK header even when multi_ack is not supported.
41		stop, err := r.stopReading(reader)
42		if err != nil {
43			return err
44		}
45
46		if stop {
47			break
48		}
49	}
50
51	return s.Err()
52}
53
54// stopReading detects when a valid command such as ACK or NAK is found to be
55// read in the buffer without moving the read pointer.
56func (r *ServerResponse) stopReading(reader *bufio.Reader) (bool, error) {
57	ahead, err := reader.Peek(7)
58	if err == io.EOF {
59		return true, nil
60	}
61
62	if err != nil {
63		return false, err
64	}
65
66	if len(ahead) > 4 && r.isValidCommand(ahead[0:3]) {
67		return false, nil
68	}
69
70	if len(ahead) == 7 && r.isValidCommand(ahead[4:]) {
71		return false, nil
72	}
73
74	return true, nil
75}
76
77func (r *ServerResponse) isValidCommand(b []byte) bool {
78	commands := [][]byte{ack, nak}
79	for _, c := range commands {
80		if bytes.Equal(b, c) {
81			return true
82		}
83	}
84
85	return false
86}
87
88func (r *ServerResponse) decodeLine(line []byte) error {
89	if len(line) == 0 {
90		return fmt.Errorf("unexpected flush")
91	}
92
93	if bytes.Equal(line[0:3], ack) {
94		return r.decodeACKLine(line)
95	}
96
97	if bytes.Equal(line[0:3], nak) {
98		return nil
99	}
100
101	return fmt.Errorf("unexpected content %q", string(line))
102}
103
104func (r *ServerResponse) decodeACKLine(line []byte) error {
105	if len(line) < ackLineLen {
106		return fmt.Errorf("malformed ACK %q", line)
107	}
108
109	sp := bytes.Index(line, []byte(" "))
110	h := plumbing.NewHash(string(line[sp+1 : sp+41]))
111	r.ACKs = append(r.ACKs, h)
112	return nil
113}
114
115// Encode encodes the ServerResponse into a writer.
116func (r *ServerResponse) Encode(w io.Writer) error {
117	if len(r.ACKs) > 1 {
118		return errors.New("multi_ack and multi_ack_detailed are not supported")
119	}
120
121	e := pktline.NewEncoder(w)
122	if len(r.ACKs) == 0 {
123		return e.Encodef("%s\n", nak)
124	}
125
126	return e.Encodef("%s %s\n", ack, r.ACKs[0].String())
127}
128