aboutsummaryrefslogtreecommitdiffstats
path: root/formats/packp/pktline/pktline.go
blob: 58c36feb5755a759dfd043b3edf5eca4f8a30f37 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
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
}