aboutsummaryrefslogtreecommitdiffstats
path: root/formats/pktline
diff options
context:
space:
mode:
Diffstat (limited to 'formats/pktline')
-rw-r--r--formats/pktline/decoder.go106
-rw-r--r--formats/pktline/decoder_test.go85
-rw-r--r--formats/pktline/doc.go56
-rw-r--r--formats/pktline/encoder.go63
-rw-r--r--formats/pktline/encoder_test.go50
5 files changed, 360 insertions, 0 deletions
diff --git a/formats/pktline/decoder.go b/formats/pktline/decoder.go
new file mode 100644
index 0000000..a078475
--- /dev/null
+++ b/formats/pktline/decoder.go
@@ -0,0 +1,106 @@
+package pktline
+
+import (
+ "errors"
+ "io"
+ "strconv"
+)
+
+var (
+ ErrUnderflow = errors.New("unexpected string length (underflow)")
+ ErrInvalidHeader = errors.New("invalid header")
+ 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 := d.r.Read(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 := d.r.Read(line); err != nil {
+ return "", err
+ } else if read != exp {
+ return "", ErrUnderflow
+ }
+
+ 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
new file mode 100644
index 0000000..e3cd2f4
--- /dev/null
+++ b/formats/pktline/decoder_test.go
@@ -0,0 +1,85 @@
+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
new file mode 100644
index 0000000..0ae22e3
--- /dev/null
+++ b/formats/pktline/doc.go
@@ -0,0 +1,56 @@
+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 = 4
+ MaxLength = 65524
+)
diff --git a/formats/pktline/encoder.go b/formats/pktline/encoder.go
new file mode 100644
index 0000000..dfa53e2
--- /dev/null
+++ b/formats/pktline/encoder.go
@@ -0,0 +1,63 @@
+package pktline
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+)
+
+var (
+ 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
new file mode 100644
index 0000000..f718c33
--- /dev/null
+++ b/formats/pktline/encoder_test.go
@@ -0,0 +1,50 @@
+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")
+}