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
}