1package packp 2 3import ( 4 "bytes" 5 "encoding/hex" 6 "errors" 7 "fmt" 8 "io" 9 10 "github.com/go-git/go-git/v5/plumbing" 11 "github.com/go-git/go-git/v5/plumbing/format/pktline" 12) 13 14// Decode reads the next advertised-refs message form its input and 15// stores it in the AdvRefs. 16func (a *AdvRefs) Decode(r io.Reader) error { 17 d := newAdvRefsDecoder(r) 18 return d.Decode(a) 19} 20 21type advRefsDecoder struct { 22 s *pktline.Scanner // a pkt-line scanner from the input stream 23 line []byte // current pkt-line contents, use parser.nextLine() to make it advance 24 nLine int // current pkt-line number for debugging, begins at 1 25 hash plumbing.Hash // last hash read 26 err error // sticky error, use the parser.error() method to fill this out 27 data *AdvRefs // parsed data is stored here 28} 29 30var ( 31 // ErrEmptyAdvRefs is returned by Decode if it gets an empty advertised 32 // references message. 33 ErrEmptyAdvRefs = errors.New("empty advertised-ref message") 34 // ErrEmptyInput is returned by Decode if the input is empty. 35 ErrEmptyInput = errors.New("empty input") 36) 37 38func newAdvRefsDecoder(r io.Reader) *advRefsDecoder { 39 return &advRefsDecoder{ 40 s: pktline.NewScanner(r), 41 } 42} 43 44func (d *advRefsDecoder) Decode(v *AdvRefs) error { 45 d.data = v 46 47 for state := decodePrefix; state != nil; { 48 state = state(d) 49 } 50 51 return d.err 52} 53 54type decoderStateFn func(*advRefsDecoder) decoderStateFn 55 56// fills out the parser sticky error 57func (d *advRefsDecoder) error(format string, a ...interface{}) { 58 msg := fmt.Sprintf( 59 "pkt-line %d: %s", d.nLine, 60 fmt.Sprintf(format, a...), 61 ) 62 63 d.err = NewErrUnexpectedData(msg, d.line) 64} 65 66// Reads a new pkt-line from the scanner, makes its payload available as 67// p.line and increments p.nLine. A successful invocation returns true, 68// otherwise, false is returned and the sticky error is filled out 69// accordingly. Trims eols at the end of the payloads. 70func (d *advRefsDecoder) nextLine() bool { 71 d.nLine++ 72 73 if !d.s.Scan() { 74 if d.err = d.s.Err(); d.err != nil { 75 return false 76 } 77 78 if d.nLine == 1 { 79 d.err = ErrEmptyInput 80 return false 81 } 82 83 d.error("EOF") 84 return false 85 } 86 87 d.line = d.s.Bytes() 88 d.line = bytes.TrimSuffix(d.line, eol) 89 90 return true 91} 92 93// The HTTP smart prefix is often followed by a flush-pkt. 94func decodePrefix(d *advRefsDecoder) decoderStateFn { 95 if ok := d.nextLine(); !ok { 96 return nil 97 } 98 99 if !isPrefix(d.line) { 100 return decodeFirstHash 101 } 102 103 tmp := make([]byte, len(d.line)) 104 copy(tmp, d.line) 105 d.data.Prefix = append(d.data.Prefix, tmp) 106 if ok := d.nextLine(); !ok { 107 return nil 108 } 109 110 if !isFlush(d.line) { 111 return decodeFirstHash 112 } 113 114 d.data.Prefix = append(d.data.Prefix, pktline.Flush) 115 if ok := d.nextLine(); !ok { 116 return nil 117 } 118 119 return decodeFirstHash 120} 121 122func isPrefix(payload []byte) bool { 123 return len(payload) > 0 && payload[0] == '#' 124} 125 126// If the first hash is zero, then a no-refs is coming. Otherwise, a 127// list-of-refs is coming, and the hash will be followed by the first 128// advertised ref. 129func decodeFirstHash(p *advRefsDecoder) decoderStateFn { 130 // If the repository is empty, we receive a flush here (HTTP). 131 if isFlush(p.line) { 132 p.err = ErrEmptyAdvRefs 133 return nil 134 } 135 136 if len(p.line) < hashSize { 137 p.error("cannot read hash, pkt-line too short") 138 return nil 139 } 140 141 if _, err := hex.Decode(p.hash[:], p.line[:hashSize]); err != nil { 142 p.error("invalid hash text: %s", err) 143 return nil 144 } 145 146 p.line = p.line[hashSize:] 147 148 if p.hash.IsZero() { 149 return decodeSkipNoRefs 150 } 151 152 return decodeFirstRef 153} 154 155// Skips SP "capabilities^{}" NUL 156func decodeSkipNoRefs(p *advRefsDecoder) decoderStateFn { 157 if len(p.line) < len(noHeadMark) { 158 p.error("too short zero-id ref") 159 return nil 160 } 161 162 if !bytes.HasPrefix(p.line, noHeadMark) { 163 p.error("malformed zero-id ref") 164 return nil 165 } 166 167 p.line = p.line[len(noHeadMark):] 168 169 return decodeCaps 170} 171 172// decode the refname, expects SP refname NULL 173func decodeFirstRef(l *advRefsDecoder) decoderStateFn { 174 if len(l.line) < 3 { 175 l.error("line too short after hash") 176 return nil 177 } 178 179 if !bytes.HasPrefix(l.line, sp) { 180 l.error("no space after hash") 181 return nil 182 } 183 l.line = l.line[1:] 184 185 chunks := bytes.SplitN(l.line, null, 2) 186 if len(chunks) < 2 { 187 l.error("NULL not found") 188 return nil 189 } 190 ref := chunks[0] 191 l.line = chunks[1] 192 193 if bytes.Equal(ref, []byte(head)) { 194 l.data.Head = &l.hash 195 } else { 196 l.data.References[string(ref)] = l.hash 197 } 198 199 return decodeCaps 200} 201 202func decodeCaps(p *advRefsDecoder) decoderStateFn { 203 if err := p.data.Capabilities.Decode(p.line); err != nil { 204 p.error("invalid capabilities: %s", err) 205 return nil 206 } 207 208 return decodeOtherRefs 209} 210 211// The refs are either tips (obj-id SP refname) or a peeled (obj-id SP refname^{}). 212// If there are no refs, then there might be a shallow or flush-ptk. 213func decodeOtherRefs(p *advRefsDecoder) decoderStateFn { 214 if ok := p.nextLine(); !ok { 215 return nil 216 } 217 218 if bytes.HasPrefix(p.line, shallow) { 219 return decodeShallow 220 } 221 222 if len(p.line) == 0 { 223 return nil 224 } 225 226 saveTo := p.data.References 227 if bytes.HasSuffix(p.line, peeled) { 228 p.line = bytes.TrimSuffix(p.line, peeled) 229 saveTo = p.data.Peeled 230 } 231 232 ref, hash, err := readRef(p.line) 233 if err != nil { 234 p.error("%s", err) 235 return nil 236 } 237 saveTo[ref] = hash 238 239 return decodeOtherRefs 240} 241 242// Reads a ref-name 243func readRef(data []byte) (string, plumbing.Hash, error) { 244 chunks := bytes.Split(data, sp) 245 switch { 246 case len(chunks) == 1: 247 return "", plumbing.ZeroHash, fmt.Errorf("malformed ref data: no space was found") 248 case len(chunks) > 2: 249 return "", plumbing.ZeroHash, fmt.Errorf("malformed ref data: more than one space found") 250 default: 251 return string(chunks[1]), plumbing.NewHash(string(chunks[0])), nil 252 } 253} 254 255// Keeps reading shallows until a flush-pkt is found 256func decodeShallow(p *advRefsDecoder) decoderStateFn { 257 if !bytes.HasPrefix(p.line, shallow) { 258 p.error("malformed shallow prefix, found %q... instead", p.line[:len(shallow)]) 259 return nil 260 } 261 p.line = bytes.TrimPrefix(p.line, shallow) 262 263 if len(p.line) != hashSize { 264 p.error(fmt.Sprintf( 265 "malformed shallow hash: wrong length, expected 40 bytes, read %d bytes", 266 len(p.line))) 267 return nil 268 } 269 270 text := p.line[:hashSize] 271 var h plumbing.Hash 272 if _, err := hex.Decode(h[:], text); err != nil { 273 p.error("invalid hash text: %s", err) 274 return nil 275 } 276 277 p.data.Shallows = append(p.data.Shallows, h) 278 279 if ok := p.nextLine(); !ok { 280 return nil 281 } 282 283 if len(p.line) == 0 { 284 return nil // successful parse of the advertised-refs message 285 } 286 287 return decodeShallow 288} 289