diff options
Diffstat (limited to 'formats/pktline')
-rw-r--r-- | formats/pktline/decoder.go | 106 | ||||
-rw-r--r-- | formats/pktline/decoder_test.go | 85 | ||||
-rw-r--r-- | formats/pktline/doc.go | 56 | ||||
-rw-r--r-- | formats/pktline/encoder.go | 63 | ||||
-rw-r--r-- | formats/pktline/encoder_test.go | 50 |
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") +} |