Skip to content

Commit

Permalink
Merge pull request #936 from aymanbagabas/more-packp
Browse files Browse the repository at this point in the history
Respect pktline error-line errors
  • Loading branch information
pjbgf committed Nov 25, 2023
2 parents fecea41 + f46d04a commit 90348bd
Show file tree
Hide file tree
Showing 9 changed files with 403 additions and 112 deletions.
51 changes: 51 additions & 0 deletions plumbing/format/pktline/error.go
@@ -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
@@ -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
@@ -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
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
@@ -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
}

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
@@ -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")
}
}

0 comments on commit 90348bd

Please sign in to comment.