From 22fe81f342538ae51442a72356036768f7f1a2f9 Mon Sep 17 00:00:00 2001 From: Máximo Cuadros Date: Tue, 6 Dec 2016 15:46:09 +0100 Subject: protocol/packp: UploadPackResponse implementation (#161) * plumbing/protocol: paktp avoid duplication of haves, wants and shallow * protocol/pakp: UploadPackResponse implementation * changes * changes * changes * debug * changes --- plumbing/protocol/packp/common.go | 11 +++- plumbing/protocol/packp/shallowupd.go | 73 +++++++++++++++++++++++++ plumbing/protocol/packp/shallowupd_test.go | 63 +++++++++++++++++++++ plumbing/protocol/packp/srvresp.go | 70 ++++++++++++++++++++++++ plumbing/protocol/packp/srvresp_test.go | 48 ++++++++++++++++ plumbing/protocol/packp/ulreq.go | 13 +++++ plumbing/protocol/packp/ulreq_encode_test.go | 2 +- plumbing/protocol/packp/uppackreq.go | 14 +++-- plumbing/protocol/packp/uppackreq_test.go | 4 +- plumbing/protocol/packp/uppackresp.go | 77 ++++++++++++++++++++++++++ plumbing/protocol/packp/uppackresp_test.go | 82 ++++++++++++++++++++++++++++ 11 files changed, 446 insertions(+), 11 deletions(-) create mode 100644 plumbing/protocol/packp/shallowupd.go create mode 100644 plumbing/protocol/packp/shallowupd_test.go create mode 100644 plumbing/protocol/packp/srvresp.go create mode 100644 plumbing/protocol/packp/srvresp_test.go create mode 100644 plumbing/protocol/packp/uppackresp.go create mode 100644 plumbing/protocol/packp/uppackresp_test.go (limited to 'plumbing/protocol') diff --git a/plumbing/protocol/packp/common.go b/plumbing/protocol/packp/common.go index c8db931..2328eda 100644 --- a/plumbing/protocol/packp/common.go +++ b/plumbing/protocol/packp/common.go @@ -17,12 +17,12 @@ var ( eol = []byte("\n") eq = []byte{'='} - // advrefs + // advertised-refs null = []byte("\x00") peeled = []byte("^{}") noHeadMark = []byte(" capabilities^{}\x00") - // ulreq + // upload-request want = []byte("want ") shallow = []byte("shallow ") deepen = []byte("deepen") @@ -30,6 +30,13 @@ var ( deepenSince = []byte("deepen-since ") deepenReference = []byte("deepen-not ") + // shallow-update + unshallow = []byte("unshallow ") + + // server-response + ack = []byte("ACK") + nak = []byte("NAK") + // updreq shallowNoSp = []byte("shallow") ) diff --git a/plumbing/protocol/packp/shallowupd.go b/plumbing/protocol/packp/shallowupd.go new file mode 100644 index 0000000..89063de --- /dev/null +++ b/plumbing/protocol/packp/shallowupd.go @@ -0,0 +1,73 @@ +package packp + +import ( + "bytes" + "fmt" + "io" + + "gopkg.in/src-d/go-git.v4/plumbing" + "gopkg.in/src-d/go-git.v4/plumbing/format/pktline" +) + +const ( + shallowLineLen = 48 + unshallowLineLen = 50 +) + +type ShallowUpdate struct { + Shallows []plumbing.Hash + Unshallows []plumbing.Hash +} + +func (r *ShallowUpdate) Decode(reader io.Reader) error { + s := pktline.NewScanner(reader) + + for s.Scan() { + line := s.Bytes() + + var err error + switch { + case bytes.HasPrefix(line, shallow): + err = r.decodeShallowLine(line) + case bytes.HasPrefix(line, unshallow): + err = r.decodeUnshallowLine(line) + case bytes.Compare(line, pktline.Flush) == 0: + return nil + } + + if err != nil { + return err + } + } + + return s.Err() +} + +func (r *ShallowUpdate) decodeShallowLine(line []byte) error { + hash, err := r.decodeLine(line, shallow, shallowLineLen) + if err != nil { + return err + } + + r.Shallows = append(r.Shallows, hash) + return nil +} + +func (r *ShallowUpdate) decodeUnshallowLine(line []byte) error { + hash, err := r.decodeLine(line, unshallow, unshallowLineLen) + if err != nil { + return err + } + + r.Unshallows = append(r.Unshallows, hash) + return nil +} + +func (r *ShallowUpdate) decodeLine(line, prefix []byte, expLen int) (plumbing.Hash, error) { + if len(line) != expLen { + return plumbing.ZeroHash, fmt.Errorf("malformed %s%q", prefix, line) + } + + raw := string(line[expLen-40 : expLen]) + return plumbing.NewHash(raw), nil +} diff --git a/plumbing/protocol/packp/shallowupd_test.go b/plumbing/protocol/packp/shallowupd_test.go new file mode 100644 index 0000000..d64fb5d --- /dev/null +++ b/plumbing/protocol/packp/shallowupd_test.go @@ -0,0 +1,63 @@ +package packp + +import ( + "bytes" + + "gopkg.in/src-d/go-git.v4/plumbing" + + . "gopkg.in/check.v1" +) + +type ShallowUpdateSuite struct{} + +var _ = Suite(&ShallowUpdateSuite{}) + +func (s *ShallowUpdateSuite) TestDecode(c *C) { + raw := "" + + "0034shallow aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "0034shallow bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + + "0000" + + su := &ShallowUpdate{} + err := su.Decode(bytes.NewBufferString(raw)) + c.Assert(err, IsNil) + + plumbing.HashesSort(su.Shallows) + + c.Assert(su.Unshallows, HasLen, 0) + c.Assert(su.Shallows, HasLen, 2) + c.Assert(su.Shallows, DeepEquals, []plumbing.Hash{ + plumbing.NewHash("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + plumbing.NewHash("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), + }) +} + +func (s *ShallowUpdateSuite) TestDecodeUnshallow(c *C) { + raw := "" + + "0036unshallow aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "0036unshallow bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + + "0000" + + su := &ShallowUpdate{} + err := su.Decode(bytes.NewBufferString(raw)) + c.Assert(err, IsNil) + + plumbing.HashesSort(su.Unshallows) + + c.Assert(su.Shallows, HasLen, 0) + c.Assert(su.Unshallows, HasLen, 2) + c.Assert(su.Unshallows, DeepEquals, []plumbing.Hash{ + plumbing.NewHash("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + plumbing.NewHash("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), + }) +} + +func (s *ShallowUpdateSuite) TestDecodeMalformed(c *C) { + raw := "" + + "0035unshallow aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + + "0000" + + su := &ShallowUpdate{} + err := su.Decode(bytes.NewBufferString(raw)) + c.Assert(err, NotNil) +} diff --git a/plumbing/protocol/packp/srvresp.go b/plumbing/protocol/packp/srvresp.go new file mode 100644 index 0000000..3284fa2 --- /dev/null +++ b/plumbing/protocol/packp/srvresp.go @@ -0,0 +1,70 @@ +package packp + +import ( + "bytes" + "errors" + "fmt" + "io" + + "gopkg.in/src-d/go-git.v4/plumbing" + "gopkg.in/src-d/go-git.v4/plumbing/format/pktline" +) + +const ackLineLen = 44 + +// ServerResponse object acknowledgement from upload-pack service +// TODO: implement support for multi_ack or multi_ack_detailed responses +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 io.Reader, isMultiACK bool) error { + 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 + } + + if !isMultiACK { + break + } + } + + return s.Err() +} + +func (r *ServerResponse) decodeLine(line []byte) error { + if len(line) == 0 { + return fmt.Errorf("unexpected flush") + } + + if bytes.Compare(line[0:3], ack) == 0 { + return r.decodeACKLine(line) + } + + if bytes.Compare(line[0:3], nak) == 0 { + 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 +} diff --git a/plumbing/protocol/packp/srvresp_test.go b/plumbing/protocol/packp/srvresp_test.go new file mode 100644 index 0000000..6078855 --- /dev/null +++ b/plumbing/protocol/packp/srvresp_test.go @@ -0,0 +1,48 @@ +package packp + +import ( + "bytes" + + "gopkg.in/src-d/go-git.v4/plumbing" + + . "gopkg.in/check.v1" +) + +type ServerResponseSuite struct{} + +var _ = Suite(&ServerResponseSuite{}) + +func (s *ServerResponseSuite) TestDecodeNAK(c *C) { + raw := "0008NAK\n" + + sr := &ServerResponse{} + err := sr.Decode(bytes.NewBufferString(raw), false) + c.Assert(err, IsNil) + + c.Assert(sr.ACKs, HasLen, 0) +} + +func (s *ServerResponseSuite) TestDecodeACK(c *C) { + raw := "0031ACK 6ecf0ef2c2dffb796033e5a02219af86ec6584e5\n" + + sr := &ServerResponse{} + err := sr.Decode(bytes.NewBufferString(raw), false) + c.Assert(err, IsNil) + + c.Assert(sr.ACKs, HasLen, 1) + c.Assert(sr.ACKs[0], Equals, plumbing.NewHash("6ecf0ef2c2dffb796033e5a02219af86ec6584e5")) +} + +func (s *ServerResponseSuite) TestDecodeMalformed(c *C) { + raw := "0029ACK 6ecf0ef2c2dffb796033e5a02219af86ec6584e\n" + + sr := &ServerResponse{} + err := sr.Decode(bytes.NewBufferString(raw), false) + c.Assert(err, NotNil) +} + +func (s *ServerResponseSuite) TestDecodeMultiACK(c *C) { + sr := &ServerResponse{} + err := sr.Decode(bytes.NewBuffer(nil), true) + c.Assert(err, NotNil) +} diff --git a/plumbing/protocol/packp/ulreq.go b/plumbing/protocol/packp/ulreq.go index 254e85e..d57c3fc 100644 --- a/plumbing/protocol/packp/ulreq.go +++ b/plumbing/protocol/packp/ulreq.go @@ -25,6 +25,7 @@ type UploadRequest struct { // DepthCommit, DepthSince and DepthReference. type Depth interface { isDepth() + IsZero() bool } // DepthCommits values stores the maximum number of requested commits in @@ -34,16 +35,28 @@ type DepthCommits int func (d DepthCommits) isDepth() {} +func (d DepthCommits) IsZero() bool { + return d == 0 +} + // DepthSince values requests only commits newer than the specified time. type DepthSince time.Time func (d DepthSince) isDepth() {} +func (d DepthSince) IsZero() bool { + return time.Time(d).IsZero() +} + // DepthReference requests only commits not to found in the specified reference. type DepthReference string func (d DepthReference) isDepth() {} +func (d DepthReference) IsZero() bool { + return string(d) == "" +} + // NewUploadRequest returns a pointer to a new UploadRequest value, ready to be // used. It has no capabilities, wants or shallows and an infinite depth. Please // note that to encode an upload-request it has to have at least one wanted hash. diff --git a/plumbing/protocol/packp/ulreq_encode_test.go b/plumbing/protocol/packp/ulreq_encode_test.go index b414a37..0890678 100644 --- a/plumbing/protocol/packp/ulreq_encode_test.go +++ b/plumbing/protocol/packp/ulreq_encode_test.go @@ -6,9 +6,9 @@ import ( "gopkg.in/src-d/go-git.v4/plumbing" "gopkg.in/src-d/go-git.v4/plumbing/format/pktline" + "gopkg.in/src-d/go-git.v4/plumbing/protocol/packp/capability" . "gopkg.in/check.v1" - "gopkg.in/src-d/go-git.v4/plumbing/protocol/packp/capability" ) type UlReqEncodeSuite struct{} diff --git a/plumbing/protocol/packp/uppackreq.go b/plumbing/protocol/packp/uppackreq.go index 887d27a..84e2b4e 100644 --- a/plumbing/protocol/packp/uppackreq.go +++ b/plumbing/protocol/packp/uppackreq.go @@ -13,15 +13,16 @@ import ( // UploadPackRequest represents a upload-pack request. // Zero-value is not safe, use NewUploadPackRequest instead. type UploadPackRequest struct { - *UploadRequest - *UploadHaves + UploadRequest + UploadHaves } // NewUploadPackRequest creates a new UploadPackRequest and returns a pointer. func NewUploadPackRequest() *UploadPackRequest { + ur := NewUploadRequest() return &UploadPackRequest{ - UploadHaves: &UploadHaves{}, - UploadRequest: NewUploadRequest(), + UploadHaves: UploadHaves{}, + UploadRequest: *ur, } } @@ -30,9 +31,10 @@ func NewUploadPackRequest() *UploadPackRequest { // ones, based on the adv value (advertaised capabilities), the UploadPackRequest // it has no wants, haves or shallows and an infinite depth func NewUploadPackRequestFromCapabilities(adv *capability.List) *UploadPackRequest { + ur := NewUploadRequestFromCapabilities(adv) return &UploadPackRequest{ - UploadHaves: &UploadHaves{}, - UploadRequest: NewUploadRequestFromCapabilities(adv), + UploadHaves: UploadHaves{}, + UploadRequest: *ur, } } diff --git a/plumbing/protocol/packp/uppackreq_test.go b/plumbing/protocol/packp/uppackreq_test.go index e551f45..273f916 100644 --- a/plumbing/protocol/packp/uppackreq_test.go +++ b/plumbing/protocol/packp/uppackreq_test.go @@ -1,11 +1,11 @@ package packp import ( + "bytes" + "gopkg.in/src-d/go-git.v4/plumbing" "gopkg.in/src-d/go-git.v4/plumbing/protocol/packp/capability" - "bytes" - . "gopkg.in/check.v1" ) diff --git a/plumbing/protocol/packp/uppackresp.go b/plumbing/protocol/packp/uppackresp.go new file mode 100644 index 0000000..a117956 --- /dev/null +++ b/plumbing/protocol/packp/uppackresp.go @@ -0,0 +1,77 @@ +package packp + +import ( + "errors" + "io" + + "gopkg.in/src-d/go-git.v4/plumbing/protocol/packp/capability" +) + +// ErrUploadPackResponseNotDecoded is returned if Read is called without +// decoding first +var ErrUploadPackResponseNotDecoded = errors.New("upload-pack-response should be decoded") + +// UploadPackResponse contains all the information responded by the upload-pack +// service, the response implements io.ReadCloser that allows to read the +// packfile directly from it. +type UploadPackResponse struct { + ShallowUpdate + ServerResponse + r io.ReadCloser + + isShallow bool + isMultiACK bool + isOk bool +} + +// NewUploadPackResponse create a new UploadPackResponse instance, the request +// being responded by the response is required. +func NewUploadPackResponse(req *UploadPackRequest) *UploadPackResponse { + isShallow := !req.Depth.IsZero() + isMultiACK := req.Capabilities.Supports(capability.MultiACK) || + req.Capabilities.Supports(capability.MultiACKDetailed) + + return &UploadPackResponse{ + isShallow: isShallow, + isMultiACK: isMultiACK, + } +} + +// Decode decodes all the responses sent by upload-pack service into the struct +// and prepares it to read the packfile using the Read method +func (r *UploadPackResponse) Decode(reader io.ReadCloser) error { + if r.isShallow { + if err := r.ShallowUpdate.Decode(reader); err != nil { + return err + } + } + + if err := r.ServerResponse.Decode(reader, r.isMultiACK); err != nil { + return err + } + + // now the reader is ready to read the packfile content + r.r = reader + + return nil +} + +// Read reads the packfile data, if the request was done with any Sideband +// capability the content read should be demultiplexed. If the methods wasn't +// called before the ErrUploadPackResponseNotDecoded will be return +func (r *UploadPackResponse) Read(p []byte) (int, error) { + if r.r == nil { + return 0, ErrUploadPackResponseNotDecoded + } + + return r.r.Read(p) +} + +// Close the underlying reader, if any +func (r *UploadPackResponse) Close() error { + if r.r == nil { + return nil + } + + return r.r.Close() +} diff --git a/plumbing/protocol/packp/uppackresp_test.go b/plumbing/protocol/packp/uppackresp_test.go new file mode 100644 index 0000000..c81bb76 --- /dev/null +++ b/plumbing/protocol/packp/uppackresp_test.go @@ -0,0 +1,82 @@ +package packp + +import ( + "bytes" + "io/ioutil" + + "gopkg.in/src-d/go-git.v4/plumbing/protocol/packp/capability" + + . "gopkg.in/check.v1" +) + +type UploadPackResponseSuite struct{} + +var _ = Suite(&UploadPackResponseSuite{}) + +func (s *UploadPackResponseSuite) TestDecodeNAK(c *C) { + raw := "0008NAK\n[PACK]" + + req := NewUploadPackRequest() + res := NewUploadPackResponse(req) + defer res.Close() + + err := res.Decode(ioutil.NopCloser(bytes.NewBufferString(raw))) + c.Assert(err, IsNil) + + pack, err := ioutil.ReadAll(res) + c.Assert(err, IsNil) + c.Assert(pack, DeepEquals, []byte("[PACK]")) +} + +func (s *UploadPackResponseSuite) TestDecodeDepth(c *C) { + raw := "00000008NAK\n[PACK]" + + req := NewUploadPackRequest() + req.Depth = DepthCommits(1) + + res := NewUploadPackResponse(req) + defer res.Close() + + err := res.Decode(ioutil.NopCloser(bytes.NewBufferString(raw))) + c.Assert(err, IsNil) + + pack, err := ioutil.ReadAll(res) + c.Assert(err, IsNil) + c.Assert(pack, DeepEquals, []byte("[PACK]")) +} + +func (s *UploadPackResponseSuite) TestDecodeMalformed(c *C) { + raw := "00000008ACK\n[PACK]" + + req := NewUploadPackRequest() + req.Depth = DepthCommits(1) + + res := NewUploadPackResponse(req) + defer res.Close() + + err := res.Decode(ioutil.NopCloser(bytes.NewBufferString(raw))) + c.Assert(err, NotNil) +} + +func (s *UploadPackResponseSuite) TestDecodeMultiACK(c *C) { + req := NewUploadPackRequest() + req.Capabilities.Set(capability.MultiACK) + + res := NewUploadPackResponse(req) + defer res.Close() + + err := res.Decode(ioutil.NopCloser(bytes.NewBuffer(nil))) + c.Assert(err, NotNil) +} + +func (s *UploadPackResponseSuite) TestReadNoDecode(c *C) { + req := NewUploadPackRequest() + req.Capabilities.Set(capability.MultiACK) + + res := NewUploadPackResponse(req) + defer res.Close() + + n, err := res.Read(nil) + c.Assert(err, Equals, ErrUploadPackResponseNotDecoded) + c.Assert(n, Equals, 0) +} -- cgit