Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Respect pktline error-line errors #936

Merged
merged 3 commits into from
Nov 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
51 changes: 51 additions & 0 deletions plumbing/format/pktline/error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package pktline

import (
"bytes"
"errors"
"io"
"strings"
)

var (
// ErrInvalidErrorLine is returned by Decode when the packet line is not an
// error line.
ErrInvalidErrorLine = errors.New("expected an error-line")

errPrefix = []byte("ERR ")
)

// ErrorLine is a packet line that contains an error message.
// Once this packet is sent by client or server, the data transfer process is
// terminated.
// See https://git-scm.com/docs/pack-protocol#_pkt_line_format
type ErrorLine struct {
Text string
}

// Error implements the error interface.
func (e *ErrorLine) Error() string {
return e.Text
}

// Encode encodes the ErrorLine into a packet line.
func (e *ErrorLine) Encode(w io.Writer) error {
p := NewEncoder(w)
return p.Encodef("%s%s\n", string(errPrefix), e.Text)
}

// Decode decodes a packet line into an ErrorLine.
func (e *ErrorLine) Decode(r io.Reader) error {
s := NewScanner(r)
if !s.Scan() {
return s.Err()
}

line := s.Bytes()
if !bytes.HasPrefix(line, errPrefix) {
return ErrInvalidErrorLine
}

e.Text = strings.TrimSpace(string(line[4:]))
return nil
}
68 changes: 68 additions & 0 deletions plumbing/format/pktline/error_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package pktline

import (
"bytes"
"errors"
"io"
"testing"
)

func TestEncodeEmptyErrorLine(t *testing.T) {
e := &ErrorLine{}
err := e.Encode(io.Discard)
if err != nil {
t.Fatal(err)
}
}

func TestEncodeErrorLine(t *testing.T) {
e := &ErrorLine{
Text: "something",
}
var buf bytes.Buffer
err := e.Encode(&buf)
if err != nil {
t.Fatal(err)
}
if buf.String() != "0012ERR something\n" {
t.Fatalf("unexpected encoded error line: %q", buf.String())
}
}

func TestDecodeEmptyErrorLine(t *testing.T) {
var buf bytes.Buffer
e := &ErrorLine{}
err := e.Decode(&buf)
if err != nil {
t.Fatal(err)
}
if e.Text != "" {
t.Fatalf("unexpected error line: %q", e.Text)
}
}

func TestDecodeErrorLine(t *testing.T) {
var buf bytes.Buffer
buf.WriteString("000eERR foobar")
var e *ErrorLine
err := e.Decode(&buf)
if !errors.As(err, &e) {
t.Fatalf("expected error line, got: %T: %v", err, err)
}
if e.Text != "foobar" {
t.Fatalf("unexpected error line: %q", e.Text)
}
}

func TestDecodeErrorLineLn(t *testing.T) {
var buf bytes.Buffer
buf.WriteString("000fERR foobar\n")
var e *ErrorLine
err := e.Decode(&buf)
if !errors.As(err, &e) {
t.Fatalf("expected error line, got: %T: %v", err, err)
}
if e.Text != "foobar" {
t.Fatalf("unexpected error line: %q", e.Text)
}
}
9 changes: 9 additions & 0 deletions plumbing/format/pktline/scanner.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package pktline

import (
"bytes"
"errors"
"io"
"strings"

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

if bytes.HasPrefix(s.payload, errPrefix) {
s.err = &ErrorLine{
Text: strings.TrimSpace(string(s.payload[4:])),
}
return false
}

return true
}

Expand Down
5 changes: 5 additions & 0 deletions plumbing/protocol/packp/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ func isFlush(payload []byte) bool {
return len(payload) == 0
}

var (
// ErrNilWriter is returned when a nil writer is passed to the encoder.
ErrNilWriter = fmt.Errorf("nil writer")
)

// ErrUnexpectedData represents an unexpected data decoding a message
type ErrUnexpectedData struct {
Msg string
Expand Down
120 changes: 120 additions & 0 deletions plumbing/protocol/packp/gitproto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package packp

import (
"fmt"
"io"
"strings"

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

var (
// ErrInvalidGitProtoRequest is returned by Decode if the input is not a
// valid git protocol request.
ErrInvalidGitProtoRequest = fmt.Errorf("invalid git protocol request")
)

// GitProtoRequest is a command request for the git protocol.
// It is used to send the command, endpoint, and extra parameters to the
// remote.
// See https://git-scm.com/docs/pack-protocol#_git_transport
type GitProtoRequest struct {
RequestCommand string
Pathname string

// Optional
Host string

// Optional
ExtraParams []string
}

// validate validates the request.
func (g *GitProtoRequest) validate() error {
if g.RequestCommand == "" {
return fmt.Errorf("%w: empty request command", ErrInvalidGitProtoRequest)
}

if g.Pathname == "" {
return fmt.Errorf("%w: empty pathname", ErrInvalidGitProtoRequest)
}

return nil
}

// Encode encodes the request into the writer.
func (g *GitProtoRequest) Encode(w io.Writer) error {
if w == nil {
return ErrNilWriter
}

if err := g.validate(); err != nil {
return err
}

aymanbagabas marked this conversation as resolved.
Show resolved Hide resolved
p := pktline.NewEncoder(w)
req := fmt.Sprintf("%s %s\x00", g.RequestCommand, g.Pathname)
if host := g.Host; host != "" {
req += fmt.Sprintf("host=%s\x00", host)
}

if len(g.ExtraParams) > 0 {
req += "\x00"
for _, param := range g.ExtraParams {
req += param + "\x00"
}
}

if err := p.Encode([]byte(req)); err != nil {
return err
}

return nil
}

// Decode decodes the request from the reader.
func (g *GitProtoRequest) Decode(r io.Reader) error {
s := pktline.NewScanner(r)
if !s.Scan() {
err := s.Err()
if err == nil {
return ErrInvalidGitProtoRequest
}
return err
}

line := string(s.Bytes())
if len(line) == 0 {
return io.EOF
}

if line[len(line)-1] != 0 {
return fmt.Errorf("%w: missing null terminator", ErrInvalidGitProtoRequest)
}

parts := strings.SplitN(line, " ", 2)
if len(parts) != 2 {
return fmt.Errorf("%w: short request", ErrInvalidGitProtoRequest)
}

g.RequestCommand = parts[0]
params := strings.Split(parts[1], string(null))
if len(params) < 1 {
return fmt.Errorf("%w: missing pathname", ErrInvalidGitProtoRequest)
}

g.Pathname = params[0]
if len(params) > 1 {
g.Host = strings.TrimPrefix(params[1], "host=")
}

if len(params) > 2 {
for _, param := range params[2:] {
if param != "" {
g.ExtraParams = append(g.ExtraParams, param)
}
}
}

return nil
}
99 changes: 99 additions & 0 deletions plumbing/protocol/packp/gitproto_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package packp

import (
"bytes"
"testing"
)

func TestEncodeEmptyGitProtoRequest(t *testing.T) {
var buf bytes.Buffer
var p GitProtoRequest
err := p.Encode(&buf)
if err == nil {
t.Fatal("expected error")
}
}

func TestEncodeGitProtoRequest(t *testing.T) {
var buf bytes.Buffer
p := GitProtoRequest{
RequestCommand: "command",
Pathname: "pathname",
Host: "host",
ExtraParams: []string{"param1", "param2"},
}
err := p.Encode(&buf)
if err != nil {
t.Fatal(err)
}
expected := "002ecommand pathname\x00host=host\x00\x00param1\x00param2\x00"
if buf.String() != expected {
t.Fatalf("expected %q, got %q", expected, buf.String())
}
}

func TestEncodeInvalidGitProtoRequest(t *testing.T) {
var buf bytes.Buffer
p := GitProtoRequest{
RequestCommand: "command",
}
err := p.Encode(&buf)
if err == nil {
t.Fatal("expected error")
}
}

func TestDecodeEmptyGitProtoRequest(t *testing.T) {
var buf bytes.Buffer
var p GitProtoRequest
err := p.Decode(&buf)
if err == nil {
t.Fatal("expected error")
}
}

func TestDecodeGitProtoRequest(t *testing.T) {
var buf bytes.Buffer
buf.WriteString("002ecommand pathname\x00host=host\x00\x00param1\x00param2\x00")
var p GitProtoRequest
err := p.Decode(&buf)
if err != nil {
t.Fatal(err)
}
expected := GitProtoRequest{
RequestCommand: "command",
Pathname: "pathname",
Host: "host",
ExtraParams: []string{"param1", "param2"},
}
if p.RequestCommand != expected.RequestCommand {
t.Fatalf("expected %q, got %q", expected.RequestCommand, p.RequestCommand)
}
if p.Pathname != expected.Pathname {
t.Fatalf("expected %q, got %q", expected.Pathname, p.Pathname)
}
if p.Host != expected.Host {
t.Fatalf("expected %q, got %q", expected.Host, p.Host)
}
if len(p.ExtraParams) != len(expected.ExtraParams) {
t.Fatalf("expected %d, got %d", len(expected.ExtraParams), len(p.ExtraParams))
}
}

func TestDecodeInvalidGitProtoRequest(t *testing.T) {
var buf bytes.Buffer
buf.WriteString("0026command \x00host=host\x00\x00param1\x00param2")
var p GitProtoRequest
err := p.Decode(&buf)
if err == nil {
t.Fatal("expected error")
}
}

func TestValidateEmptyGitProtoRequest(t *testing.T) {
var p GitProtoRequest
err := p.validate()
if err == nil {
t.Fatal("expected error")
}
}