diff options
author | Alberto Cortés <alcortesm@gmail.com> | 2016-10-18 15:23:01 +0200 |
---|---|---|
committer | Máximo Cuadros <mcuadros@gmail.com> | 2016-10-18 15:23:01 +0200 |
commit | 5f7d34066cc5583ee30a315e0661b5326dc548db (patch) | |
tree | c10fdbf1f42d5b51de25c6828ba6573dd28f4536 | |
parent | 6c6a37b9128189ba4cdde8128428a36ef75d1a44 (diff) | |
download | go-git-5f7d34066cc5583ee30a315e0661b5326dc548db.tar.gz |
Substitute old pktline encoder/decoder with new pktline scanner (#84)
* replace old pktline package with new pktline scanner
* remove error checks on pktline.NewFromString
* fix deppend bug
* reduce memory garbage when pktline.NewFromStrings
* improve int to hex conversion to help gc
* make intToHex func private
* clean function names
-rw-r--r-- | clients/common/common.go | 80 | ||||
-rw-r--r-- | clients/common/common_test.go | 11 | ||||
-rw-r--r-- | clients/http/git_upload_pack.go | 22 | ||||
-rw-r--r-- | clients/ssh/git_upload_pack.go | 4 | ||||
-rw-r--r-- | formats/packp/pktline/pktline.go | 114 | ||||
-rw-r--r-- | formats/packp/pktline/pktline_test.go | 199 | ||||
-rw-r--r-- | formats/packp/pktline/scanner.go | 133 | ||||
-rw-r--r-- | formats/packp/pktline/scanner_test.go | 208 | ||||
-rw-r--r-- | formats/pktline/decoder.go | 111 | ||||
-rw-r--r-- | formats/pktline/decoder_test.go | 85 | ||||
-rw-r--r-- | formats/pktline/doc.go | 59 | ||||
-rw-r--r-- | formats/pktline/encoder.go | 64 | ||||
-rw-r--r-- | formats/pktline/encoder_test.go | 50 |
13 files changed, 723 insertions, 417 deletions
diff --git a/clients/common/common.go b/clients/common/common.go index 1518081..df1c233 100644 --- a/clients/common/common.go +++ b/clients/common/common.go @@ -11,7 +11,7 @@ import ( "strings" "gopkg.in/src-d/go-git.v4/core" - "gopkg.in/src-d/go-git.v4/formats/pktline" + "gopkg.in/src-d/go-git.v4/formats/packp/pktline" "gopkg.in/src-d/go-git.v4/storage/memory" ) @@ -204,8 +204,8 @@ func NewGitUploadPackInfo() *GitUploadPackInfo { return &GitUploadPackInfo{Capabilities: NewCapabilities()} } -func (r *GitUploadPackInfo) Decode(d *pktline.Decoder) error { - if err := r.read(d); err != nil { +func (r *GitUploadPackInfo) Decode(s *pktline.Scanner) error { + if err := r.read(s); err != nil { if err == ErrEmptyGitUploadPack { return core.NewPermanentError(err) } @@ -216,16 +216,29 @@ func (r *GitUploadPackInfo) Decode(d *pktline.Decoder) error { return nil } -func (r *GitUploadPackInfo) read(d *pktline.Decoder) error { - lines, err := d.ReadAll() - if err != nil { - return err - } - +func (r *GitUploadPackInfo) read(s *pktline.Scanner) error { isEmpty := true r.Refs = make(memory.ReferenceStorage, 0) - for _, line := range lines { - if !r.isValidLine(line) { + smartCommentIgnore := false + for s.Scan() { + line := string(s.Bytes()) + + if smartCommentIgnore { + // some servers like Github add a flush-pkt after the smart http comment + // that we must ignore to prevent a premature termination of the read. + if len(line) == 0 { + continue + } + smartCommentIgnore = false + } + + // exit on first flush-pkt + if len(line) == 0 { + break + } + + if isSmartHttpComment(line) { + smartCommentIgnore = true continue } @@ -240,11 +253,11 @@ func (r *GitUploadPackInfo) read(d *pktline.Decoder) error { return ErrEmptyGitUploadPack } - return nil + return s.Err() } -func (r *GitUploadPackInfo) isValidLine(line string) bool { - return line[0] != '#' +func isSmartHttpComment(line string) bool { + return line[0] == '#' } func (r *GitUploadPackInfo) readLine(line string) error { @@ -280,21 +293,28 @@ func (r *GitUploadPackInfo) String() string { } func (r *GitUploadPackInfo) Bytes() []byte { - e := pktline.NewEncoder() - e.AddLine("# service=git-upload-pack") - e.AddFlush() - e.AddLine(fmt.Sprintf("%s HEAD\x00%s", r.Head().Hash(), r.Capabilities.String())) + payloads := []string{} + payloads = append(payloads, "# service=git-upload-pack\n") + // inserting a flush-pkt here violates the protocol spec, but some + // servers do it, like Github.com + payloads = append(payloads, "") + + firstLine := fmt.Sprintf("%s HEAD\x00%s\n", r.Head().Hash(), r.Capabilities.String()) + payloads = append(payloads, firstLine) for _, ref := range r.Refs { if ref.Type() != core.HashReference { continue } - e.AddLine(fmt.Sprintf("%s %s", ref.Hash(), ref.Name())) + ref := fmt.Sprintf("%s %s\n", ref.Hash(), ref.Name()) + payloads = append(payloads, ref) } - e.AddFlush() - b, _ := ioutil.ReadAll(e.Reader()) + payloads = append(payloads, "") + pktlines, _ := pktline.NewFromStrings(payloads...) + b, _ := ioutil.ReadAll(pktlines) + return b } @@ -318,21 +338,25 @@ func (r *GitUploadPackRequest) String() string { } func (r *GitUploadPackRequest) Reader() *strings.Reader { - e := pktline.NewEncoder() + payloads := []string{} + for _, want := range r.Wants { - e.AddLine(fmt.Sprintf("want %s", want)) + payloads = append(payloads, fmt.Sprintf("want %s\n", want)) } for _, have := range r.Haves { - e.AddLine(fmt.Sprintf("have %s", have)) + payloads = append(payloads, fmt.Sprintf("have %s\n", have)) } if r.Depth != 0 { - e.AddLine(fmt.Sprintf("deepen %d", r.Depth)) + payloads = append(payloads, fmt.Sprintf("deepen %d\n", r.Depth)) } - e.AddFlush() - e.AddLine("done") + payloads = append(payloads, "") + payloads = append(payloads, "done\n") + + pktlines, _ := pktline.NewFromStrings(payloads...) + b, _ := ioutil.ReadAll(pktlines) - return e.Reader() + return strings.NewReader(string(b)) } diff --git a/clients/common/common_test.go b/clients/common/common_test.go index cc6c6d1..66a49e1 100644 --- a/clients/common/common_test.go +++ b/clients/common/common_test.go @@ -5,9 +5,10 @@ import ( "encoding/base64" "testing" - . "gopkg.in/check.v1" "gopkg.in/src-d/go-git.v4/core" - "gopkg.in/src-d/go-git.v4/formats/pktline" + "gopkg.in/src-d/go-git.v4/formats/packp/pktline" + + . "gopkg.in/check.v1" ) func Test(t *testing.T) { TestingT(t) } @@ -48,7 +49,7 @@ func (s *SuiteCommon) TestGitUploadPackInfo(c *C) { b, _ := base64.StdEncoding.DecodeString(GitUploadPackInfoFixture) i := NewGitUploadPackInfo() - err := i.Decode(pktline.NewDecoder(bytes.NewBuffer(b))) + err := i.Decode(pktline.NewScanner(bytes.NewBuffer(b))) c.Assert(err, IsNil) name := i.Capabilities.SymbolicReference("HEAD") @@ -70,7 +71,7 @@ func (s *SuiteCommon) TestGitUploadPackInfoNoHEAD(c *C) { b, _ := base64.StdEncoding.DecodeString(GitUploadPackInfoNoHEADFixture) i := NewGitUploadPackInfo() - err := i.Decode(pktline.NewDecoder(bytes.NewBuffer(b))) + err := i.Decode(pktline.NewScanner(bytes.NewBuffer(b))) c.Assert(err, IsNil) name := i.Capabilities.SymbolicReference("HEAD") @@ -86,7 +87,7 @@ func (s *SuiteCommon) TestGitUploadPackInfoEmpty(c *C) { b := bytes.NewBuffer(nil) i := NewGitUploadPackInfo() - err := i.Decode(pktline.NewDecoder(b)) + err := i.Decode(pktline.NewScanner(b)) c.Assert(err, ErrorMatches, "permanent.*empty.*") } diff --git a/clients/http/git_upload_pack.go b/clients/http/git_upload_pack.go index 888d279..eb8db0b 100644 --- a/clients/http/git_upload_pack.go +++ b/clients/http/git_upload_pack.go @@ -2,6 +2,7 @@ package http import ( "bufio" + "bytes" "fmt" "io" "net/http" @@ -9,7 +10,7 @@ import ( "gopkg.in/src-d/go-git.v4/clients/common" "gopkg.in/src-d/go-git.v4/core" - "gopkg.in/src-d/go-git.v4/formats/pktline" + "gopkg.in/src-d/go-git.v4/formats/packp/pktline" ) // GitUploadPackService git-upoad-pack service over HTTP @@ -77,7 +78,7 @@ func (s *GitUploadPackService) Info() (*common.GitUploadPackInfo, error) { defer res.Body.Close() i := common.NewGitUploadPackInfo() - return i, i.Decode(pktline.NewDecoder(res.Body)) + return i, i.Decode(pktline.NewScanner(res.Body)) } // Fetch request and returns a reader to a packfile @@ -101,27 +102,22 @@ func (s *GitUploadPackService) Fetch(r *common.GitUploadPackRequest) (io.ReadClo return nil, err } - if err := s.discardResponseInfo(reader); err != nil { + if err := discardResponseInfo(reader); err != nil { return nil, err } return reader, nil } -func (s *GitUploadPackService) discardResponseInfo(r io.Reader) error { - decoder := pktline.NewDecoder(r) - for { - line, err := decoder.ReadLine() - if err != nil { - break - } - - if line == "NAK\n" { +func discardResponseInfo(r io.Reader) error { + s := pktline.NewScanner(r) + for s.Scan() { + if bytes.Equal(s.Bytes(), []byte{'N', 'A', 'K', '\n'}) { break } } - return nil + return s.Err() } func (s *GitUploadPackService) doRequest(method, url string, content *strings.Reader) (*http.Response, error) { diff --git a/clients/ssh/git_upload_pack.go b/clients/ssh/git_upload_pack.go index d83aadb..513e528 100644 --- a/clients/ssh/git_upload_pack.go +++ b/clients/ssh/git_upload_pack.go @@ -11,7 +11,7 @@ import ( "strings" "gopkg.in/src-d/go-git.v4/clients/common" - "gopkg.in/src-d/go-git.v4/formats/pktline" + "gopkg.in/src-d/go-git.v4/formats/packp/pktline" "golang.org/x/crypto/ssh" ) @@ -123,7 +123,7 @@ func (s *GitUploadPackService) Info() (i *common.GitUploadPackInfo, err error) { } i = common.NewGitUploadPackInfo() - return i, i.Decode(pktline.NewDecoder(bytes.NewReader(out))) + return i, i.Decode(pktline.NewScanner(bytes.NewReader(out))) } // Disconnect the SSH client. diff --git a/formats/packp/pktline/pktline.go b/formats/packp/pktline/pktline.go new file mode 100644 index 0000000..58c36fe --- /dev/null +++ b/formats/packp/pktline/pktline.go @@ -0,0 +1,114 @@ +// Package pktline implements reading and creating pkt-lines as per +// https://github.com/git/git/blob/master/Documentation/technical/protocol-common.txt. +package pktline + +import ( + "bytes" + "errors" + "io" + "strings" +) + +const ( + // MaxPayloadSize is the maximum payload size of a pkt-line in bytes. + MaxPayloadSize = 65516 +) + +var ( + flush = []byte{'0', '0', '0', '0'} +) + +// PktLine values represent a succession of pkt-lines. +// Values from this type are not zero-value safe, see the functions New +// and NewFromString below. +type PktLine struct { + io.Reader +} + +// ErrPayloadTooLong is returned by New and NewFromString when any of +// the provided payloads is bigger than MaxPayloadSize. +var ErrPayloadTooLong = errors.New("payload is too long") + +// New returns the concatenation of several pkt-lines, each of them with +// the payload specified by the contents of each input byte slice. An +// empty payload byte slice will produce a flush-pkt. +func New(payloads ...[]byte) (PktLine, error) { + ret := []io.Reader{} + for _, p := range payloads { + if err := add(&ret, p); err != nil { + return PktLine{}, err + } + } + + return PktLine{io.MultiReader(ret...)}, nil +} + +func add(dst *[]io.Reader, e []byte) error { + if len(e) > MaxPayloadSize { + return ErrPayloadTooLong + } + + if len(e) == 0 { + *dst = append(*dst, bytes.NewReader(flush)) + return nil + } + + n := len(e) + 4 + *dst = append(*dst, bytes.NewReader(int16ToHex(n))) + *dst = append(*dst, bytes.NewReader(e)) + + return nil +} + +// susbtitutes fmt.Sprintf("%04x", n) to avoid memory garbage +// generation. +func int16ToHex(n int) []byte { + var ret [4]byte + ret[0] = byteToAsciiHex(byte(n & 0xf000 >> 12)) + ret[1] = byteToAsciiHex(byte(n & 0x0f00 >> 8)) + ret[2] = byteToAsciiHex(byte(n & 0x00f0 >> 4)) + ret[3] = byteToAsciiHex(byte(n & 0x000f)) + + return ret[:] +} + +// turns a byte into its hexadecimal ascii representation. Example: +// from 11 (0xb) into 'b'. +func byteToAsciiHex(n byte) byte { + if n < 10 { + return byte('0' + n) + } + + return byte('a' - 10 + n) +} + +// NewFromStrings returns the concatenation of several pkt-lines, each +// of them with the payload specified by the contents of each input +// string. An empty payload string will produce a flush-pkt. +func NewFromStrings(payloads ...string) (PktLine, error) { + ret := []io.Reader{} + for _, p := range payloads { + if err := addString(&ret, p); err != nil { + return PktLine{}, err + } + } + + return PktLine{io.MultiReader(ret...)}, nil +} + +func addString(dst *[]io.Reader, s string) error { + if len(s) > MaxPayloadSize { + return ErrPayloadTooLong + } + + if len(s) == 0 { + *dst = append(*dst, bytes.NewReader(flush)) + return nil + } + + n := len(s) + 4 + *dst = append(*dst, bytes.NewReader(int16ToHex(n))) + *dst = append(*dst, strings.NewReader(s)) + + return nil +} diff --git a/formats/packp/pktline/pktline_test.go b/formats/packp/pktline/pktline_test.go new file mode 100644 index 0000000..3c18f53 --- /dev/null +++ b/formats/packp/pktline/pktline_test.go @@ -0,0 +1,199 @@ +package pktline_test + +import ( + "io" + "io/ioutil" + "os" + "strings" + "testing" + + "gopkg.in/src-d/go-git.v4/formats/packp/pktline" + + . "gopkg.in/check.v1" +) + +func Test(t *testing.T) { TestingT(t) } + +type SuitePktLine struct { +} + +var _ = Suite(&SuitePktLine{}) + +func (s *SuitePktLine) TestNew(c *C) { + for i, test := range [...]struct { + input [][]byte + expected []byte + }{ + { + input: [][]byte{}, + expected: []byte{}, + }, { + input: [][]byte{ + []byte(nil), + }, + expected: []byte("0000"), + }, { + input: [][]byte{ + []byte{}, + }, + expected: []byte("0000"), + }, { + input: [][]byte{ + []byte(""), + }, + expected: []byte("0000"), + }, { + input: [][]byte{ + []byte("hello\n"), + }, + expected: []byte("000ahello\n"), + }, { + input: [][]byte{ + []byte("hello\n"), + []byte("world!\n"), + []byte(""), + []byte("foo"), + []byte(""), + }, + expected: []byte("000ahello\n000bworld!\n00000007foo0000"), + }, { + input: [][]byte{ + []byte(strings.Repeat("a", pktline.MaxPayloadSize)), + }, + expected: []byte("fff0" + strings.Repeat("a", pktline.MaxPayloadSize)), + }, + } { + r, err := pktline.New(test.input...) + c.Assert(err, IsNil, Commentf("input %d = %v", i, test.input)) + + obtained, err := ioutil.ReadAll(r) + c.Assert(err, IsNil, Commentf("input %d = %v", i, test.input)) + + c.Assert(obtained, DeepEquals, test.expected, + Commentf("input %d = %v", i, test.input)) + } +} + +func (s *SuitePktLine) TestNewErrPayloadTooLong(c *C) { + for _, input := range [...][][]byte{ + [][]byte{ + []byte(strings.Repeat("a", pktline.MaxPayloadSize+1)), + }, + [][]byte{ + []byte("hello world!"), + []byte(""), + []byte(strings.Repeat("a", pktline.MaxPayloadSize+1)), + }, + [][]byte{ + []byte("hello world!"), + []byte(strings.Repeat("a", pktline.MaxPayloadSize+1)), + []byte("foo"), + }, + } { + _, err := pktline.New(input...) + + c.Assert(err, Equals, pktline.ErrPayloadTooLong, + Commentf("%v\n", input)) + } +} + +func (s *SuitePktLine) TestNewFromStrings(c *C) { + for _, test := range [...]struct { + input []string + expected []byte + }{ + { + input: []string(nil), + expected: []byte{}, + }, { + input: []string{}, + expected: []byte{}, + }, { + input: []string{""}, + expected: []byte("0000"), + }, { + input: []string{"hello\n"}, + expected: []byte("000ahello\n"), + }, { + input: []string{"hello\n", "world!\n", "", "foo", ""}, + expected: []byte("000ahello\n000bworld!\n00000007foo0000"), + }, { + input: []string{ + strings.Repeat("a", pktline.MaxPayloadSize), + }, + expected: []byte("fff0" + strings.Repeat("a", pktline.MaxPayloadSize)), + }, + } { + r, err := pktline.NewFromStrings(test.input...) + c.Assert(err, IsNil) + + obtained, err := ioutil.ReadAll(r) + c.Assert(err, IsNil) + + c.Assert(obtained, DeepEquals, test.expected, + Commentf("input = %v\n", test.input)) + } +} + +func (s *SuitePktLine) TestNewFromStringsErrPayloadTooLong(c *C) { + for _, input := range [...][]string{ + []string{ + strings.Repeat("a", pktline.MaxPayloadSize+1), + }, + []string{ + "hello world!", + "", + strings.Repeat("a", pktline.MaxPayloadSize+1), + }, + []string{ + "hello world!", + strings.Repeat("a", pktline.MaxPayloadSize+1), + "foo", + }, + } { + _, err := pktline.NewFromStrings(input...) + + c.Assert(err, Equals, pktline.ErrPayloadTooLong, + Commentf("%v\n", input)) + } +} + +func ExampleNew() { + // These are the payloads we want to turn into pkt-lines, + // the empty slice at the end will generate a flush-pkt. + payloads := [][]byte{ + []byte{'h', 'e', 'l', 'l', 'o', '\n'}, + []byte{'w', 'o', 'r', 'l', 'd', '!', '\n'}, + []byte{}, + } + + // Create the pkt-lines, ignoring errors... + pktlines, _ := pktline.New(payloads...) + + // Send the raw data to stdout, ignoring errors... + _, _ = io.Copy(os.Stdout, pktlines) + + // Output: 000ahello + // 000bworld! + // 0000 +} + +func ExampleNewFromStrings() { + // These are the payloads we want to turn into pkt-lines, + // the empty string at the end will generate a flush-pkt. + payloads := []string{ + "hello\n", + "world!\n", + "", + } + + // Create the pkt-lines, ignoring errors... + pktlines, _ := pktline.NewFromStrings(payloads...) + + // Send the raw data to stdout, ignoring errors... + _, _ = io.Copy(os.Stdout, pktlines) + + // Output: 000ahello + // 000bworld! + // 0000 +} diff --git a/formats/packp/pktline/scanner.go b/formats/packp/pktline/scanner.go new file mode 100644 index 0000000..3ce2adf --- /dev/null +++ b/formats/packp/pktline/scanner.go @@ -0,0 +1,133 @@ +package pktline + +import ( + "errors" + "io" +) + +const ( + lenSize = 4 +) + +// ErrInvalidPktLen is returned by Err() when an invalid pkt-len is found. +var ErrInvalidPktLen = errors.New("invalid pkt-len found") + +// Scanner provides a convenient interface for reading the payloads of a +// series of pkt-lines. It takes an io.Reader providing the source, +// which then can be tokenized through repeated calls to the Scan +// method. +// +// After each Scan call, the Bytes method will return the payload of the +// corresponding pkt-line on a shared buffer, which will be 65516 bytes +// or smaller. Flush pkt-lines are represented by empty byte slices. +// +// Scanning stops at EOF or the first I/O error. +type Scanner struct { + r io.Reader // The reader provided by the client + err error // Sticky error + payload []byte // Last pkt-payload + len [lenSize]byte // Last pkt-len +} + +// NewScanner returns a new Scanner to read from r. +func NewScanner(r io.Reader) *Scanner { + return &Scanner{ + r: r, + } +} + +// Err returns the first error encountered by the Scanner. +func (s *Scanner) Err() error { + return s.err +} + +// Scan advances the Scanner to the next pkt-line, whose payload will +// then be available through the Bytes method. Scanning stops at EOF +// or the first I/O error. After Scan returns false, the Err method +// will return any error that occurred during scanning, except that if +// it was io.EOF, Err will return nil. +func (s *Scanner) Scan() bool { + var l int + l, s.err = s.readPayloadLen() + if s.err == io.EOF { + s.err = nil + return false + } + if s.err != nil { + return false + } + + if cap(s.payload) < l { + s.payload = make([]byte, 0, l) + } + + if _, s.err = io.ReadFull(s.r, s.payload[:l]); s.err != nil { + return false + } + s.payload = s.payload[:l] + + return true +} + +// Bytes returns the most recent payload generated by a call to Scan. +// The underlying array may point to data that will be overwritten by a +// subsequent call to Scan. It does no allocation. +func (s *Scanner) Bytes() []byte { + return s.payload +} + +// Method readPayloadLen returns the payload length by reading the +// pkt-len and substracting the pkt-len size. +func (s *Scanner) readPayloadLen() (int, error) { + if _, err := io.ReadFull(s.r, s.len[:]); err != nil { + if err == io.EOF { + return 0, err + } + return 0, ErrInvalidPktLen + } + + n, err := hexDecode(s.len) + if err != nil { + return 0, err + } + + switch { + case n == 0: + return 0, nil + case n <= lenSize: + return 0, ErrInvalidPktLen + case n > MaxPayloadSize+lenSize: + return 0, ErrInvalidPktLen + default: + return n - lenSize, nil + } +} + +// Turns the hexadecimal representation of a number in a byte slice into +// a number. This function substitute strconv.ParseUint(string(buf), 16, +// 16) and/or hex.Decode, to avoid generating new strings, thus helping the +// GC. +func hexDecode(buf [lenSize]byte) (int, error) { + var ret int + for i := 0; i < lenSize; i++ { + n, err := asciiHexToByte(buf[i]) + if err != nil { + return 0, ErrInvalidPktLen + } + ret = 16*ret + int(n) + } + return ret, nil +} + +// turns the hexadecimal ascii representation of a byte into its +// numerical value. Example: from 'b' to 11 (0xb). +func asciiHexToByte(b byte) (byte, error) { + switch { + case b >= '0' && b <= '9': + return b - '0', nil + case b >= 'a' && b <= 'f': + return b - 'a' + 10, nil + default: + return 0, ErrInvalidPktLen + } +} diff --git a/formats/packp/pktline/scanner_test.go b/formats/packp/pktline/scanner_test.go new file mode 100644 index 0000000..08ca51f --- /dev/null +++ b/formats/packp/pktline/scanner_test.go @@ -0,0 +1,208 @@ +package pktline_test + +import ( + "fmt" + "io" + "strings" + + "gopkg.in/src-d/go-git.v4/formats/packp/pktline" + + . "gopkg.in/check.v1" +) + +type SuiteScanner struct{} + +var _ = Suite(&SuiteScanner{}) + +func (s *SuiteScanner) TestInvalid(c *C) { + for _, test := range [...]string{ + "0001", "0002", "0003", "0004", + "0001asdfsadf", "0004foo", + "fff1", "fff2", + "gorka", + "0", "003", + " 5a", "5 a", "5 \n", + "-001", "-000", + } { + r := strings.NewReader(test) + sc := pktline.NewScanner(r) + _ = sc.Scan() + c.Assert(sc.Err(), ErrorMatches, pktline.ErrInvalidPktLen.Error(), + Commentf("data = %q", test)) + } +} + +func (s *SuiteScanner) TestEmptyReader(c *C) { + r := strings.NewReader("") + sc := pktline.NewScanner(r) + hasPayload := sc.Scan() + c.Assert(hasPayload, Equals, false) + c.Assert(sc.Err(), Equals, nil) +} + +func (s *SuiteScanner) TestFlush(c *C) { + r, err := pktline.NewFromStrings("") + c.Assert(err, IsNil) + sc := pktline.NewScanner(r) + c.Assert(sc.Scan(), Equals, true) + payload := sc.Bytes() + c.Assert(len(payload), Equals, 0) +} + +func (s *SuiteScanner) TestPktLineTooShort(c *C) { + r := strings.NewReader("010cfoobar") + + sc := pktline.NewScanner(r) + + c.Assert(sc.Scan(), Equals, false) + c.Assert(sc.Err(), ErrorMatches, "unexpected EOF") +} + +func (s *SuiteScanner) TestScanAndPayload(c *C) { + for _, test := range [...]string{ + "a", + "a\n", + strings.Repeat("a", 100), + strings.Repeat("a", 100) + "\n", + strings.Repeat("\x00", 100), + strings.Repeat("\x00", 100) + "\n", + strings.Repeat("a", pktline.MaxPayloadSize), + strings.Repeat("a", pktline.MaxPayloadSize-1) + "\n", + } { + r, err := pktline.NewFromStrings(test) + c.Assert(err, IsNil, Commentf("input len=%x, contents=%.10q\n", len(test), test)) + sc := pktline.NewScanner(r) + + c.Assert(sc.Scan(), Equals, true, + Commentf("test = %.20q...", test)) + obtained := sc.Bytes() + c.Assert(obtained, DeepEquals, []byte(test), + Commentf("in = %.20q out = %.20q", test, string(obtained))) + } +} + +func (s *SuiteScanner) TestSkip(c *C) { + for _, test := range [...]struct { + input []string + n int + expected []byte + }{ + { + input: []string{ + "first", + "second", + "third", + ""}, + n: 1, + expected: []byte("second"), + }, + { + input: []string{ + "first", + "second", + "third", + ""}, + n: 2, + expected: []byte("third"), + }, + } { + r, err := pktline.NewFromStrings(test.input...) + c.Assert(err, IsNil) + sc := pktline.NewScanner(r) + for i := 0; i < test.n; i++ { + c.Assert(sc.Scan(), Equals, true, + Commentf("scan error = %s", sc.Err())) + } + c.Assert(sc.Scan(), Equals, true, + Commentf("scan error = %s", sc.Err())) + obtained := sc.Bytes() + c.Assert(obtained, DeepEquals, test.expected, + Commentf("\nin = %.20q\nout = %.20q\nexp = %.20q", + test.input, obtained, test.expected)) + } +} + +func (s *SuiteScanner) TestEOF(c *C) { + r, err := pktline.NewFromStrings("first", "second") + c.Assert(err, IsNil) + sc := pktline.NewScanner(r) + for sc.Scan() { + } + c.Assert(sc.Err(), IsNil) +} + +// A section are several non flush-pkt lines followed by a flush-pkt, which +// how the git protocol sends long messages. +func (s *SuiteScanner) TestReadSomeSections(c *C) { + nSections := 2 + nLines := 4 + data := sectionsExample(c, nSections, nLines) + sc := pktline.NewScanner(data) + + sectionCounter := 0 + lineCounter := 0 + for sc.Scan() { + if len(sc.Bytes()) == 0 { + sectionCounter++ + } + lineCounter++ + } + c.Assert(sc.Err(), IsNil) + c.Assert(sectionCounter, Equals, nSections) + c.Assert(lineCounter, Equals, (1+nLines)*nSections) +} + +// returns nSection sections, each of them with nLines pkt-lines (not +// counting the flush-pkt: +// +// 0009 0.0\n +// 0009 0.1\n +// ... +// 0000 +// and so on +func sectionsExample(c *C, nSections, nLines int) io.Reader { + ss := []string{} + for section := 0; section < nSections; section++ { + for line := 0; line < nLines; line++ { + line := fmt.Sprintf(" %d.%d\n", section, line) + ss = append(ss, line) + } + ss = append(ss, "") + } + + ret, err := pktline.NewFromStrings(ss...) + c.Assert(err, IsNil) + + return ret +} + +func ExampleScanner() { + // A reader is needed as input. + input := strings.NewReader("000ahello\n" + + "000bworld!\n" + + "0000", + ) + + // Create the scanner... + s := pktline.NewScanner(input) + + // and scan every pkt-line found in the input. + for s.Scan() { + payload := s.Bytes() + if len(payload) == 0 { // zero sized payloads correspond to flush-pkts. + fmt.Println("FLUSH-PKT DETECTED\n") + } else { // otherwise, you will be able to access the full payload. + fmt.Printf("PAYLOAD = %q\n", string(payload)) + } + } + + // this will catch any error when reading from the input, if any. + if s.Err() != nil { + fmt.Println(s.Err()) + } + + // Output: + // PAYLOAD = "hello\n" + // PAYLOAD = "world!\n" + // FLUSH-PKT DETECTED +} diff --git a/formats/pktline/decoder.go b/formats/pktline/decoder.go deleted file mode 100644 index 789ba7d..0000000 --- a/formats/pktline/decoder.go +++ /dev/null @@ -1,111 +0,0 @@ -package pktline - -import ( - "errors" - "io" - "strconv" -) - -var ( - // ErrUnderflow is triggered when a line is shorter than the described length - ErrUnderflow = errors.New("unexpected string length (underflow)") - // ErrInvalidHeader invalid pktline header - ErrInvalidHeader = errors.New("invalid header") - // ErrInvalidLen ivanlid line length found, < 0 - ErrInvalidLen = errors.New("invalid length") -) - -// Decoder implements a pkt-line format decoder -type Decoder struct { - r io.Reader -} - -// NewDecoder returns a new Decoder -func NewDecoder(r io.Reader) *Decoder { - return &Decoder{r} -} - -// ReadLine reads and return one pkt-line line from the reader -func (d *Decoder) ReadLine() (string, error) { - return d.readLine() -} - -func (d *Decoder) readLine() (string, error) { - raw := make([]byte, HeaderLength) - if _, err := io.ReadFull(d.r, raw); err != nil { - return "", err - } - - header, err := strconv.ParseInt(string(raw), 16, 16) - if err != nil { - return "", ErrInvalidHeader - } - - if header == 0 { - return "", nil - } - - exp := int(header - HeaderLength) - if exp < 0 { - return "", ErrInvalidLen - } - - line := make([]byte, exp) - if read, err := io.ReadFull(d.r, line); err != nil { - if err == io.ErrUnexpectedEOF && read < exp { - return "", ErrUnderflow - } - - return "", err - } - - return string(line), nil -} - -// ReadBlock reads and return multiple pkt-line lines, it stops at the end -// of the reader or if a flush-pkt is reached -func (d *Decoder) ReadBlock() ([]string, error) { - var o []string - - for { - line, err := d.readLine() - if err == io.EOF { - return o, nil - } - - if err != nil { - return o, err - } - - if err == nil && line == "" { - return o, nil - } - - o = append(o, line) - } -} - -// ReadAll read and returns all the lines -func (d *Decoder) ReadAll() ([]string, error) { - result, err := d.ReadBlock() - if err != nil { - return result, err - } - - for { - lines, err := d.ReadBlock() - if err == io.EOF { - return result, nil - } - - if err != nil { - return result, err - } - - if err == nil && len(lines) == 0 { - return result, nil - } - - result = append(result, lines...) - } -} diff --git a/formats/pktline/decoder_test.go b/formats/pktline/decoder_test.go deleted file mode 100644 index e3cd2f4..0000000 --- a/formats/pktline/decoder_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package pktline - -import ( - "strings" - "testing" - - . "gopkg.in/check.v1" -) - -func Test(t *testing.T) { TestingT(t) } - -type DecoderSuite struct{} - -var _ = Suite(&DecoderSuite{}) - -func (s *DecoderSuite) TestReadLine(c *C) { - j := NewDecoder(strings.NewReader("0006a\n")) - - line, err := j.ReadLine() - c.Assert(err, IsNil) - c.Assert(line, Equals, "a\n") -} - -func (s *DecoderSuite) TestReadLineInvalidHeader(c *C) { - j := NewDecoder(strings.NewReader("foo\n")) - - _, err := j.ReadLine() - c.Assert(err, Equals, ErrInvalidHeader) -} - -func (s *DecoderSuite) TestReadLineBufferUnderflow(c *C) { - j := NewDecoder(strings.NewReader("00e7a\n")) - - line, err := j.ReadLine() - c.Assert(err, Equals, ErrUnderflow) - c.Assert(line, Equals, "") -} - -func (s *DecoderSuite) TestReadLineInvalidLen(c *C) { - j := NewDecoder(strings.NewReader("0001foo\n")) - - line, err := j.ReadLine() - c.Assert(err, Equals, ErrInvalidLen) - c.Assert(line, Equals, "") -} - -func (s *DecoderSuite) TestReadBlock(c *C) { - j := NewDecoder(strings.NewReader("0006a\n")) - - lines, err := j.ReadBlock() - c.Assert(err, IsNil) - c.Assert(lines, HasLen, 1) - c.Assert(lines[0], Equals, "a\n") -} - -func (s *DecoderSuite) TestReadBlockWithFlush(c *C) { - j := NewDecoder(strings.NewReader("0006a\n0006b\n00000006c\n")) - - lines, err := j.ReadBlock() - c.Assert(err, IsNil) - c.Assert(lines, HasLen, 2) - c.Assert(lines[0], Equals, "a\n") - c.Assert(lines[1], Equals, "b\n") -} - -func (s *DecoderSuite) TestReadBlockInvalidLen(c *C) { - j := NewDecoder(strings.NewReader("0001foo\n")) - - lines, err := j.ReadBlock() - c.Assert(err, Equals, ErrInvalidLen) - c.Assert(lines, HasLen, 0) -} - -func (s *DecoderSuite) TestReadAll(c *C) { - j := NewDecoder(strings.NewReader("0006a\n0006b\n00000006c\n0006d\n0006e\n")) - - lines, err := j.ReadAll() - c.Assert(err, IsNil) - c.Assert(lines, HasLen, 5) - c.Assert(lines[0], Equals, "a\n") - c.Assert(lines[1], Equals, "b\n") - c.Assert(lines[2], Equals, "c\n") - c.Assert(lines[3], Equals, "d\n") - c.Assert(lines[4], Equals, "e\n") -} diff --git a/formats/pktline/doc.go b/formats/pktline/doc.go deleted file mode 100644 index 1e14ec0..0000000 --- a/formats/pktline/doc.go +++ /dev/null @@ -1,59 +0,0 @@ -// Package pktline implements a encoder/decoder of pkt-line format -package pktline - -// pkt-line Format -// --------------- -// -// Much (but not all) of the payload is described around pkt-lines. -// -// A pkt-line is a variable length binary string. The first four bytes -// of the line, the pkt-len, indicates the total length of the line, -// in hexadecimal. The pkt-len includes the 4 bytes used to contain -// the length's hexadecimal representation. -// -// A pkt-line MAY contain binary data, so implementors MUST ensure -// pkt-line parsing/formatting routines are 8-bit clean. -// -// A non-binary line SHOULD BE terminated by an LF, which if present -// MUST be included in the total length. -// -// The maximum length of a pkt-line's data component is 65520 bytes. -// Implementations MUST NOT send pkt-line whose length exceeds 65524 -// (65520 bytes of payload + 4 bytes of length data). -// -// Implementations SHOULD NOT send an empty pkt-line ("0004"). -// -// A pkt-line with a length field of 0 ("0000"), called a flush-pkt, -// is a special case and MUST be handled differently than an empty -// pkt-line ("0004"). -// -// ---- -// pkt-line = data-pkt / flush-pkt -// -// data-pkt = pkt-len pkt-payload -// pkt-len = 4*(HEXDIG) -// pkt-payload = (pkt-len - 4)*(OCTET) -// -// flush-pkt = "0000" -// ---- -// -// Examples (as C-style strings): -// -// ---- -// pkt-line actual value -// --------------------------------- -// "0006a\n" "a\n" -// "0005a" "a" -// "000bfoobar\n" "foobar\n" -// "0004" "" -// ---- -// -// Extracted from: -// https://github.com/git/git/blob/master/Documentation/technical/protocol-common.txt - -const ( - // HeaderLength length of the pktline header - HeaderLength = 4 - // MaxLength max line length - MaxLength = 65524 -) diff --git a/formats/pktline/encoder.go b/formats/pktline/encoder.go deleted file mode 100644 index 18efa25..0000000 --- a/formats/pktline/encoder.go +++ /dev/null @@ -1,64 +0,0 @@ -package pktline - -import ( - "errors" - "fmt" - "strings" -) - -var ( - //ErrOverflow is triggered when the line length exceed the MaxLength - ErrOverflow = errors.New("unexpected string length (overflow)") -) - -// Encoder implements a pkt-line format encoder -type Encoder struct { - lines []string -} - -// NewEncoder returns a new Encoder -func NewEncoder() *Encoder { - return &Encoder{make([]string, 0)} -} - -// AddLine encode and adds a line to the encoder -func (e *Encoder) AddLine(line string) error { - le, err := EncodeFromString(line + "\n") - if err != nil { - return err - } - - e.lines = append(e.lines, le) - return nil -} - -// AddFlush adds a flush-pkt to the encoder -func (e *Encoder) AddFlush() { - e.lines = append(e.lines, "0000") -} - -// Reader returns a string.Reader over the encoder -func (e *Encoder) Reader() *strings.Reader { - data := strings.Join(e.lines, "") - - return strings.NewReader(data) -} - -// EncodeFromString encodes a string to pkt-line format -func EncodeFromString(line string) (string, error) { - return Encode([]byte(line)) -} - -// Encode encodes a byte slice to pkt-line format -func Encode(line []byte) (string, error) { - if line == nil { - return "0000", nil - } - - l := len(line) + HeaderLength - if l > MaxLength { - return "", ErrOverflow - } - - return fmt.Sprintf("%04x%s", l, line), nil -} diff --git a/formats/pktline/encoder_test.go b/formats/pktline/encoder_test.go deleted file mode 100644 index f718c33..0000000 --- a/formats/pktline/encoder_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package pktline - -import ( - "bytes" - "io/ioutil" - "strings" - - . "gopkg.in/check.v1" -) - -type EncoderSuite struct{} - -var _ = Suite(&EncoderSuite{}) - -func (s *EncoderSuite) TestEncode(c *C) { - line, err := Encode([]byte("a\n")) - c.Assert(err, IsNil) - c.Assert(string(line), Equals, "0006a\n") -} - -func (s *EncoderSuite) TestEncodeNil(c *C) { - line, err := Encode(nil) - c.Assert(err, IsNil) - c.Assert(string(line), Equals, "0000") -} - -func (s *EncoderSuite) TestEncodeOverflow(c *C) { - _, err := Encode(bytes.Repeat([]byte{'0'}, MaxLength+1)) - c.Assert(err, Equals, ErrOverflow) -} - -func (s *EncoderSuite) TestEncodeFromString(c *C) { - line, err := EncodeFromString("a\n") - c.Assert(err, IsNil) - c.Assert(string(line), Equals, "0006a\n") -} - -func (s *EncoderSuite) TestEncoder(c *C) { - e := NewEncoder() - c.Assert(e.AddLine("a"), IsNil) - e.AddFlush() - c.Assert(e.AddLine("b"), IsNil) - - over := strings.Repeat("0", MaxLength+1) - c.Assert(e.AddLine(over), Equals, ErrOverflow) - - r := e.Reader() - a, _ := ioutil.ReadAll(r) - c.Assert(string(a), Equals, "0006a\n00000006b\n") -} |