smithy

ref: master

pkg/go-git-http/pktparser.go


  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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
package githttp

import (
	"encoding/hex"
	"errors"
	"fmt"
)

// pktLineParser is a parser for git pkt-line Format,
// as documented in https://github.com/git/git/blob/master/Documentation/technical/protocol-common.txt.
// A zero value of pktLineParser is valid to use as a parser in ready state.
// Output should be read from Lines and Error after Step returns finished true.
// pktLineParser reads until a terminating "0000" flush-pkt. It's good for a single use only.
type pktLineParser struct {
	// Lines contains all pkt-lines.
	Lines []string

	// Error contains the first error encountered while parsing, or nil otherwise.
	Error error

	// Internal state machine.
	state state
	next  int // next is the number of bytes that need to be written to buf before its contents should be processed by the state machine.
	buf   []byte
}

// Feed accumulates and parses data.
// It will return early if it reaches end of pkt-line data (indicated by a flush-pkt "0000"),
// or if it encounters a parsing error.
// It must not be called when state is done.
// When done, all of pkt-lines will be available in Lines, and Error will be set if any error occurred.
func (p *pktLineParser) Feed(data []byte) {
	for {
		// If not enough data to reach next state, append it to buf and return.
		if len(data) < p.next {
			p.buf = append(p.buf, data...)
			p.next -= len(data)
			return
		}

		// There's enough data to reach next state. Take from data only what's needed.
		b := data[:p.next]
		data = data[p.next:]
		p.buf = append(p.buf, b...)
		p.next = 0

		// Take a step to next state.
		err := p.step()
		if err != nil {
			p.state = done
			p.Error = err
			return
		}

		// Break out once reached done state.
		if p.state == done {
			return
		}
	}
}

const (
	// pkt-len = 4*(HEXDIG)
	pktLenSize = 4
)

type state uint8

const (
	ready state = iota
	readingLen
	readingPayload
	done
)

// step moves the state machine to the next state.
// buf must contain all the data ready for consumption for current state.
// It must not be called when state is done.
func (p *pktLineParser) step() error {
	switch p.state {
	case ready:
		p.state = readingLen
		p.next = pktLenSize
		return nil
	case readingLen:
		// len(p.buf) is 4.
		pktLen, err := parsePktLen(p.buf)
		if err != nil {
			return err
		}

		switch {
		case pktLen == 0:
			p.state = done
			p.next = 0
			p.buf = nil
			return nil
		default:
			p.state = readingPayload
			p.next = pktLen - pktLenSize // (pkt-len - 4)*(OCTET)
			p.buf = p.buf[:0]
			return nil
		}
	case readingPayload:
		p.state = readingLen
		p.next = pktLenSize
		p.Lines = append(p.Lines, string(p.buf))
		p.buf = p.buf[:0]
		return nil
	default:
		panic(fmt.Errorf("unreachable: %v", p.state))
	}
}

// parsePktLen parses a pkt-len segment.
// len(b) must be 4.
func parsePktLen(b []byte) (int, error) {
	pktLen, err := parseHex(b)
	switch {
	case err != nil:
		return 0, err
	case 1 <= pktLen && pktLen < pktLenSize:
		return 0, fmt.Errorf("invalid pkt-len: %v", pktLen)
	case pktLen > 65524:
		// The maximum length of a pkt-line is 65524 bytes (65520 bytes of payload + 4 bytes of length data).
		return 0, fmt.Errorf("invalid pkt-len: %v", pktLen)
	}
	return int(pktLen), nil
}

// parseHex parses a 4-byte hex number.
// len(h) must be 4.
func parseHex(h []byte) (uint16, error) {
	var b [2]uint8
	n, err := hex.Decode(b[:], h)
	switch {
	case err != nil:
		return 0, err
	case n != 2:
		return 0, errors.New("short output")
	}
	return uint16(b[0])<<8 | uint16(b[1]), nil
}