aboutsummaryrefslogtreecommitdiffstats
path: root/plumbing/protocol/packp/srvresp.go
blob: b3a7ee804c511f7ffdd670e23e95a16bab35f74b (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
package packp

import (
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"io"

	"github.com/go-git/go-git/v5/plumbing"
	"github.com/go-git/go-git/v5/plumbing/format/pktline"
)

const ackLineLen = 44

// ServerResponse object acknowledgement from upload-pack service
type ServerResponse struct {
	ACKs []plumbing.Hash
}

// Decode decodes the response into the struct, isMultiACK should be true, if
// the request was done with multi_ack or multi_ack_detailed capabilities.
func (r *ServerResponse) Decode(reader *bufio.Reader, isMultiACK bool) error {
	// TODO: implement support for multi_ack or multi_ack_detailed responses
	if isMultiACK {
		return errors.New("multi_ack and multi_ack_detailed are not supported")
	}

	s := pktline.NewScanner(reader)

	for s.Scan() {
		line := s.Bytes()

		if err := r.decodeLine(line); err != nil {
			return err
		}

		// we need to detect when the end of a response header and the beginning
		// of a packfile header happened, some requests to the git daemon
		// produces a duplicate ACK header even when multi_ack is not supported.
		stop, err := r.stopReading(reader)
		if err != nil {
			return err
		}

		if stop {
			break
		}
	}

	return s.Err()
}

// stopReading detects when a valid command such as ACK or NAK is found to be
// read in the buffer without moving the read pointer.
func (r *ServerResponse) stopReading(reader *bufio.Reader) (bool, error) {
	ahead, err := reader.Peek(7)
	if err == io.EOF {
		return true, nil
	}

	if err != nil {
		return false, err
	}

	if len(ahead) > 4 && r.isValidCommand(ahead[0:3]) {
		return false, nil
	}

	if len(ahead) == 7 && r.isValidCommand(ahead[4:]) {
		return false, nil
	}

	return true, nil
}

func (r *ServerResponse) isValidCommand(b []byte) bool {
	commands := [][]byte{ack, nak}
	for _, c := range commands {
		if bytes.Equal(b, c) {
			return true
		}
	}

	return false
}

func (r *ServerResponse) decodeLine(line []byte) error {
	if len(line) == 0 {
		return fmt.Errorf("unexpected flush")
	}

	if bytes.Equal(line[0:3], ack) {
		return r.decodeACKLine(line)
	}

	if bytes.Equal(line[0:3], nak) {
		return nil
	}

	return fmt.Errorf("unexpected content %q", string(line))
}

func (r *ServerResponse) decodeACKLine(line []byte) error {
	if len(line) < ackLineLen {
		return fmt.Errorf("malformed ACK %q", line)
	}

	sp := bytes.Index(line, []byte(" "))
	h := plumbing.NewHash(string(line[sp+1 : sp+41]))
	r.ACKs = append(r.ACKs, h)
	return nil
}

// Encode encodes the ServerResponse into a writer.
func (r *ServerResponse) Encode(w io.Writer) error {
	if len(r.ACKs) > 1 {
		return errors.New("multi_ack and multi_ack_detailed are not supported")
	}

	e := pktline.NewEncoder(w)
	if len(r.ACKs) == 0 {
		return e.Encodef("%s\n", nak)
	}

	return e.Encodef("%s %s\n", ack, r.ACKs[0].String())
}