Skip to content

Commit 90348bd

Browse files
authored
Merge pull request #936 from aymanbagabas/more-packp
Respect pktline error-line errors
2 parents fecea41 + f46d04a commit 90348bd

File tree

9 files changed

+403
-112
lines changed

9 files changed

+403
-112
lines changed

Diff for: plumbing/format/pktline/error.go

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package pktline
2+
3+
import (
4+
"bytes"
5+
"errors"
6+
"io"
7+
"strings"
8+
)
9+
10+
var (
11+
// ErrInvalidErrorLine is returned by Decode when the packet line is not an
12+
// error line.
13+
ErrInvalidErrorLine = errors.New("expected an error-line")
14+
15+
errPrefix = []byte("ERR ")
16+
)
17+
18+
// ErrorLine is a packet line that contains an error message.
19+
// Once this packet is sent by client or server, the data transfer process is
20+
// terminated.
21+
// See https://git-scm.com/docs/pack-protocol#_pkt_line_format
22+
type ErrorLine struct {
23+
Text string
24+
}
25+
26+
// Error implements the error interface.
27+
func (e *ErrorLine) Error() string {
28+
return e.Text
29+
}
30+
31+
// Encode encodes the ErrorLine into a packet line.
32+
func (e *ErrorLine) Encode(w io.Writer) error {
33+
p := NewEncoder(w)
34+
return p.Encodef("%s%s\n", string(errPrefix), e.Text)
35+
}
36+
37+
// Decode decodes a packet line into an ErrorLine.
38+
func (e *ErrorLine) Decode(r io.Reader) error {
39+
s := NewScanner(r)
40+
if !s.Scan() {
41+
return s.Err()
42+
}
43+
44+
line := s.Bytes()
45+
if !bytes.HasPrefix(line, errPrefix) {
46+
return ErrInvalidErrorLine
47+
}
48+
49+
e.Text = strings.TrimSpace(string(line[4:]))
50+
return nil
51+
}

Diff for: plumbing/format/pktline/error_test.go

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package pktline
2+
3+
import (
4+
"bytes"
5+
"errors"
6+
"io"
7+
"testing"
8+
)
9+
10+
func TestEncodeEmptyErrorLine(t *testing.T) {
11+
e := &ErrorLine{}
12+
err := e.Encode(io.Discard)
13+
if err != nil {
14+
t.Fatal(err)
15+
}
16+
}
17+
18+
func TestEncodeErrorLine(t *testing.T) {
19+
e := &ErrorLine{
20+
Text: "something",
21+
}
22+
var buf bytes.Buffer
23+
err := e.Encode(&buf)
24+
if err != nil {
25+
t.Fatal(err)
26+
}
27+
if buf.String() != "0012ERR something\n" {
28+
t.Fatalf("unexpected encoded error line: %q", buf.String())
29+
}
30+
}
31+
32+
func TestDecodeEmptyErrorLine(t *testing.T) {
33+
var buf bytes.Buffer
34+
e := &ErrorLine{}
35+
err := e.Decode(&buf)
36+
if err != nil {
37+
t.Fatal(err)
38+
}
39+
if e.Text != "" {
40+
t.Fatalf("unexpected error line: %q", e.Text)
41+
}
42+
}
43+
44+
func TestDecodeErrorLine(t *testing.T) {
45+
var buf bytes.Buffer
46+
buf.WriteString("000eERR foobar")
47+
var e *ErrorLine
48+
err := e.Decode(&buf)
49+
if !errors.As(err, &e) {
50+
t.Fatalf("expected error line, got: %T: %v", err, err)
51+
}
52+
if e.Text != "foobar" {
53+
t.Fatalf("unexpected error line: %q", e.Text)
54+
}
55+
}
56+
57+
func TestDecodeErrorLineLn(t *testing.T) {
58+
var buf bytes.Buffer
59+
buf.WriteString("000fERR foobar\n")
60+
var e *ErrorLine
61+
err := e.Decode(&buf)
62+
if !errors.As(err, &e) {
63+
t.Fatalf("expected error line, got: %T: %v", err, err)
64+
}
65+
if e.Text != "foobar" {
66+
t.Fatalf("unexpected error line: %q", e.Text)
67+
}
68+
}

Diff for: plumbing/format/pktline/scanner.go

+9
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package pktline
22

33
import (
4+
"bytes"
45
"errors"
56
"io"
7+
"strings"
68

79
"github.com/go-git/go-git/v5/utils/trace"
810
)
@@ -69,6 +71,13 @@ func (s *Scanner) Scan() bool {
6971
s.payload = s.payload[:l]
7072
trace.Packet.Printf("packet: < %04x %s", l, s.payload)
7173

74+
if bytes.HasPrefix(s.payload, errPrefix) {
75+
s.err = &ErrorLine{
76+
Text: strings.TrimSpace(string(s.payload[4:])),
77+
}
78+
return false
79+
}
80+
7281
return true
7382
}
7483

Diff for: plumbing/protocol/packp/common.go

+5
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ func isFlush(payload []byte) bool {
4848
return len(payload) == 0
4949
}
5050

51+
var (
52+
// ErrNilWriter is returned when a nil writer is passed to the encoder.
53+
ErrNilWriter = fmt.Errorf("nil writer")
54+
)
55+
5156
// ErrUnexpectedData represents an unexpected data decoding a message
5257
type ErrUnexpectedData struct {
5358
Msg string

Diff for: plumbing/protocol/packp/gitproto.go

+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package packp
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"strings"
7+
8+
"github.com/go-git/go-git/v5/plumbing/format/pktline"
9+
)
10+
11+
var (
12+
// ErrInvalidGitProtoRequest is returned by Decode if the input is not a
13+
// valid git protocol request.
14+
ErrInvalidGitProtoRequest = fmt.Errorf("invalid git protocol request")
15+
)
16+
17+
// GitProtoRequest is a command request for the git protocol.
18+
// It is used to send the command, endpoint, and extra parameters to the
19+
// remote.
20+
// See https://git-scm.com/docs/pack-protocol#_git_transport
21+
type GitProtoRequest struct {
22+
RequestCommand string
23+
Pathname string
24+
25+
// Optional
26+
Host string
27+
28+
// Optional
29+
ExtraParams []string
30+
}
31+
32+
// validate validates the request.
33+
func (g *GitProtoRequest) validate() error {
34+
if g.RequestCommand == "" {
35+
return fmt.Errorf("%w: empty request command", ErrInvalidGitProtoRequest)
36+
}
37+
38+
if g.Pathname == "" {
39+
return fmt.Errorf("%w: empty pathname", ErrInvalidGitProtoRequest)
40+
}
41+
42+
return nil
43+
}
44+
45+
// Encode encodes the request into the writer.
46+
func (g *GitProtoRequest) Encode(w io.Writer) error {
47+
if w == nil {
48+
return ErrNilWriter
49+
}
50+
51+
if err := g.validate(); err != nil {
52+
return err
53+
}
54+
55+
p := pktline.NewEncoder(w)
56+
req := fmt.Sprintf("%s %s\x00", g.RequestCommand, g.Pathname)
57+
if host := g.Host; host != "" {
58+
req += fmt.Sprintf("host=%s\x00", host)
59+
}
60+
61+
if len(g.ExtraParams) > 0 {
62+
req += "\x00"
63+
for _, param := range g.ExtraParams {
64+
req += param + "\x00"
65+
}
66+
}
67+
68+
if err := p.Encode([]byte(req)); err != nil {
69+
return err
70+
}
71+
72+
return nil
73+
}
74+
75+
// Decode decodes the request from the reader.
76+
func (g *GitProtoRequest) Decode(r io.Reader) error {
77+
s := pktline.NewScanner(r)
78+
if !s.Scan() {
79+
err := s.Err()
80+
if err == nil {
81+
return ErrInvalidGitProtoRequest
82+
}
83+
return err
84+
}
85+
86+
line := string(s.Bytes())
87+
if len(line) == 0 {
88+
return io.EOF
89+
}
90+
91+
if line[len(line)-1] != 0 {
92+
return fmt.Errorf("%w: missing null terminator", ErrInvalidGitProtoRequest)
93+
}
94+
95+
parts := strings.SplitN(line, " ", 2)
96+
if len(parts) != 2 {
97+
return fmt.Errorf("%w: short request", ErrInvalidGitProtoRequest)
98+
}
99+
100+
g.RequestCommand = parts[0]
101+
params := strings.Split(parts[1], string(null))
102+
if len(params) < 1 {
103+
return fmt.Errorf("%w: missing pathname", ErrInvalidGitProtoRequest)
104+
}
105+
106+
g.Pathname = params[0]
107+
if len(params) > 1 {
108+
g.Host = strings.TrimPrefix(params[1], "host=")
109+
}
110+
111+
if len(params) > 2 {
112+
for _, param := range params[2:] {
113+
if param != "" {
114+
g.ExtraParams = append(g.ExtraParams, param)
115+
}
116+
}
117+
}
118+
119+
return nil
120+
}

Diff for: plumbing/protocol/packp/gitproto_test.go

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package packp
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
)
7+
8+
func TestEncodeEmptyGitProtoRequest(t *testing.T) {
9+
var buf bytes.Buffer
10+
var p GitProtoRequest
11+
err := p.Encode(&buf)
12+
if err == nil {
13+
t.Fatal("expected error")
14+
}
15+
}
16+
17+
func TestEncodeGitProtoRequest(t *testing.T) {
18+
var buf bytes.Buffer
19+
p := GitProtoRequest{
20+
RequestCommand: "command",
21+
Pathname: "pathname",
22+
Host: "host",
23+
ExtraParams: []string{"param1", "param2"},
24+
}
25+
err := p.Encode(&buf)
26+
if err != nil {
27+
t.Fatal(err)
28+
}
29+
expected := "002ecommand pathname\x00host=host\x00\x00param1\x00param2\x00"
30+
if buf.String() != expected {
31+
t.Fatalf("expected %q, got %q", expected, buf.String())
32+
}
33+
}
34+
35+
func TestEncodeInvalidGitProtoRequest(t *testing.T) {
36+
var buf bytes.Buffer
37+
p := GitProtoRequest{
38+
RequestCommand: "command",
39+
}
40+
err := p.Encode(&buf)
41+
if err == nil {
42+
t.Fatal("expected error")
43+
}
44+
}
45+
46+
func TestDecodeEmptyGitProtoRequest(t *testing.T) {
47+
var buf bytes.Buffer
48+
var p GitProtoRequest
49+
err := p.Decode(&buf)
50+
if err == nil {
51+
t.Fatal("expected error")
52+
}
53+
}
54+
55+
func TestDecodeGitProtoRequest(t *testing.T) {
56+
var buf bytes.Buffer
57+
buf.WriteString("002ecommand pathname\x00host=host\x00\x00param1\x00param2\x00")
58+
var p GitProtoRequest
59+
err := p.Decode(&buf)
60+
if err != nil {
61+
t.Fatal(err)
62+
}
63+
expected := GitProtoRequest{
64+
RequestCommand: "command",
65+
Pathname: "pathname",
66+
Host: "host",
67+
ExtraParams: []string{"param1", "param2"},
68+
}
69+
if p.RequestCommand != expected.RequestCommand {
70+
t.Fatalf("expected %q, got %q", expected.RequestCommand, p.RequestCommand)
71+
}
72+
if p.Pathname != expected.Pathname {
73+
t.Fatalf("expected %q, got %q", expected.Pathname, p.Pathname)
74+
}
75+
if p.Host != expected.Host {
76+
t.Fatalf("expected %q, got %q", expected.Host, p.Host)
77+
}
78+
if len(p.ExtraParams) != len(expected.ExtraParams) {
79+
t.Fatalf("expected %d, got %d", len(expected.ExtraParams), len(p.ExtraParams))
80+
}
81+
}
82+
83+
func TestDecodeInvalidGitProtoRequest(t *testing.T) {
84+
var buf bytes.Buffer
85+
buf.WriteString("0026command \x00host=host\x00\x00param1\x00param2")
86+
var p GitProtoRequest
87+
err := p.Decode(&buf)
88+
if err == nil {
89+
t.Fatal("expected error")
90+
}
91+
}
92+
93+
func TestValidateEmptyGitProtoRequest(t *testing.T) {
94+
var p GitProtoRequest
95+
err := p.validate()
96+
if err == nil {
97+
t.Fatal("expected error")
98+
}
99+
}

0 commit comments

Comments
 (0)