1 package main 2 3 import ( 4 "bufio" 5 "bytes" 6 "compress/zlib" 7 "errors" 8 "fmt" 9 "io" 10 "os" 11 "path/filepath" 12 "strconv" 13 "sync" 14 "time" 15 16 "vimagination.zapto.org/byteio" 17 "vimagination.zapto.org/memio" 18 ) 19 20 const ( 21 ObjectCommit = 1 22 ObjectTree = 2 23 ObjectBlob = 3 24 // ObjectTag = 4 25 ObjectOffsetDelta = 6 26 ObjectRefDelta = 7 27 ) 28 29 var ( 30 objectHeaders = [...]string{ 31 "", 32 "commit ", 33 "tree ", 34 "blob ", 35 "tag ", 36 } 37 bufPool = sync.Pool{ 38 New: func() interface{} { 39 return new([21]byte) 40 }, 41 } 42 zHeader = memio.Buffer{8, 29} 43 zPool = sync.Pool{ 44 New: func() interface{} { 45 h := zHeader 46 z, _ := zlib.NewReader(&h) 47 return &zReadCloser{ 48 Reader: z, 49 } 50 }, 51 } 52 ) 53 54 type zReadCloser readCloser 55 56 func (z *zReadCloser) Close() error { 57 err := z.Closer.Close() 58 z.Closer = nil 59 zPool.Put(z) 60 return err 61 } 62 63 func decompress(r io.ReadCloser) (io.ReadCloser, error) { 64 z := zPool.Get().(*zReadCloser) 65 if err := z.Reader.(zlib.Resetter).Reset(r, nil); err != nil { 66 return nil, err 67 } 68 z.Closer = r 69 return z, nil 70 } 71 72 type packObject struct { 73 pack string 74 offset uint64 75 } 76 77 type pack struct { 78 data []byte 79 80 mu sync.RWMutex 81 objects map[uint64]object 82 } 83 84 type object struct { 85 typ int 86 data []byte 87 } 88 89 type Repo struct { 90 path string 91 loadPacks sync.Once 92 packsErr error 93 packs map[string]*pack 94 packObjects map[string]packObject 95 96 cacheMu sync.RWMutex 97 cache map[string]interface{} 98 lastCommit string 99 } 100 101 func OpenRepo(path string) *Repo { 102 return &Repo{ 103 path: path, 104 cache: make(map[string]interface{}), 105 } 106 } 107 108 type readCloser struct { 109 io.Reader 110 io.Closer 111 } 112 113 func (r *Repo) GetDescription() string { 114 var desc string 115 f, err := os.Open(filepath.Join(r.path, "description")) 116 if err == nil { 117 d, err := io.ReadAll(f) 118 f.Close() 119 if err == nil { 120 if string(d) != defaultDesc { 121 desc = string(d[:len(d)-1]) 122 } 123 } 124 } 125 return desc 126 } 127 128 func (r *Repo) readHeadRef() (string, error) { 129 f, err := os.Open(filepath.Join(r.path, "HEAD")) 130 if err != nil { 131 return "", fmt.Errorf("error opening HEAD: %w", err) 132 } 133 defer f.Close() 134 var buf [256]byte 135 if _, err := io.ReadFull(f, buf[:5]); err != nil { 136 return "", fmt.Errorf("error while reading HEAD: %w", err) 137 } 138 if string(buf[:5]) != "ref: " { 139 return "", errors.New("invalid HEAD file") 140 } 141 n, err := io.ReadFull(f, buf[:]) 142 if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) { 143 return "", fmt.Errorf("error while reading HEAD: %w", err) 144 } 145 return string(buf[:n-1]), nil 146 } 147 148 func (r *Repo) GetLatestCommitID() (string, error) { 149 r.cacheMu.RLock() 150 id := r.lastCommit 151 r.cacheMu.RUnlock() 152 if id != "" { 153 return id, nil 154 } 155 head, err := r.readHeadRef() 156 if err != nil { 157 return "", err 158 } 159 f, err := os.Open(filepath.Join(r.path, head)) 160 if err != nil { 161 return "", fmt.Errorf("error opening ref: %w", err) 162 } 163 defer f.Close() 164 var buf [256]byte 165 n, err := io.ReadFull(f, buf[:]) 166 if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) { 167 return "", fmt.Errorf("error while reading ref: %w", err) 168 } 169 id = checkSHA(buf[:n-1]) 170 if id == "" { 171 return "", errors.New("invalid id") 172 } 173 r.cacheMu.Lock() 174 r.lastCommit = id 175 r.cacheMu.Unlock() 176 return id, nil 177 } 178 179 var newLine = []byte{'\n'} 180 181 func (r *Repo) loadPacksData() { 182 f, err := os.Open(filepath.Join(r.path, "objects", "info", "packs")) 183 if err != nil { 184 if !os.IsNotExist(err) { 185 r.packsErr = fmt.Errorf("error opening packs file: %w", err) 186 } 187 return 188 } 189 data, err := io.ReadAll(f) 190 f.Close() 191 if err != nil { 192 r.packsErr = fmt.Errorf("error reading packs file: %w", err) 193 return 194 } 195 packs := bytes.Split(data, newLine) 196 r.packObjects = make(map[string]packObject) 197 for _, p := range packs { 198 if len(p) > 5 && p[0] == 'P' && p[1] == ' ' && string(p[len(p)-5:]) == ".pack" { 199 pack := string(p[2:]) 200 idx, err := os.Open(filepath.Join(r.path, "objects", "pack", pack[:len(pack)-4]+"idx")) 201 if err != nil { 202 r.packsErr = fmt.Errorf("error opening pack index for %s: %w", pack, err) 203 return 204 } 205 sidx := byteio.StickyBigEndianReader{Reader: bufio.NewReader(idx)} 206 a := sidx.ReadUint32() 207 if a == 4285812579 { // 0xff + 't0c' 208 if version := sidx.ReadUint32(); version != 2 { 209 idx.Close() 210 r.packsErr = fmt.Errorf("unsupported version number (%d) in pack index for %s", version, pack) 211 return 212 } 213 io.CopyN(io.Discard, &sidx, 4*255) // ignore fan 214 a = sidx.ReadUint32() 215 names := make([]string, a) 216 var name [20]byte 217 for n := range names { 218 sidx.Read(name[:]) 219 names[n] = fmt.Sprintf("%x", name) 220 } 221 io.CopyN(io.Discard, &sidx, 4*int64(a)) // ignore CRC32's 222 larger := make(map[uint32]string) 223 var largest uint32 224 for _, name := range names { 225 offset := sidx.ReadUint32() 226 if offset&0x80000000 != 0 { 227 index := offset & 0x7fffffff 228 if largest <= index { 229 largest = index + 1 230 } 231 larger[index] = name 232 } else { 233 r.packObjects[name] = packObject{ 234 pack: pack, 235 offset: uint64(offset), 236 } 237 } 238 } 239 for i := uint32(0); i < largest && sidx.Err == nil; i++ { 240 offset := sidx.ReadUint64() 241 if name, ok := larger[i]; ok { 242 r.packObjects[name] = packObject{ 243 pack: pack, 244 offset: offset, 245 } 246 } 247 } 248 } else { 249 r.packsErr = fmt.Errorf("version 1 unsupported in pack index for %s", pack) 250 } 251 idx.Close() 252 if sidx.Err != nil { 253 r.packsErr = fmt.Errorf("error reading pack index for %s: %w", pack, sidx.Err) 254 return 255 } 256 } 257 } 258 r.packs = make(map[string]*pack, len(packs)) 259 for _, p := range packs { 260 if len(p) > 5 && p[0] == 'P' && p[1] == ' ' && string(p[len(p)-5:]) == ".pack" { 261 packID := string(p[2:]) 262 f, err := os.Open(filepath.Join(r.path, "objects", "pack", packID)) 263 if err != nil { 264 r.packsErr = fmt.Errorf("error opening pack file for %s: %w", packID, err) 265 return 266 } 267 b, err := io.ReadAll(f) 268 f.Close() 269 if err != nil { 270 r.packsErr = fmt.Errorf("error reading pack file for %s: %w", packID, err) 271 return 272 } 273 if string(b[:4]) != "PACK" { 274 r.packsErr = errors.New("invalid pack header") 275 return 276 } 277 if b[4] != 0 || b[5] != 0 || b[6] != 0 || b[7] != 2 { 278 r.packsErr = fmt.Errorf("read unsupported pack version: %x", b[4:8]) 279 return 280 } 281 r.packs[packID] = &pack{ 282 data: b, 283 objects: make(map[uint64]object), 284 } 285 286 } 287 } 288 } 289 290 func (r *Repo) readPackOffset(p string, o uint64, want int) (io.ReadCloser, error) { 291 pd, ok := r.packs[p] 292 if !ok { 293 return nil, errors.New("invalid pack file") 294 } 295 pd.mu.RLock() 296 if po, ok := pd.objects[o]; ok { 297 pd.mu.RUnlock() 298 if po.typ != want { 299 return nil, errors.New("wrong packed type") 300 } 301 b := memio.LimitedBuffer(po.data) 302 return &b, nil 303 } 304 pd.mu.RUnlock() 305 pack := memio.Open(pd.data) 306 if _, err := pack.Seek(int64(o), io.SeekStart); err != nil { 307 return nil, fmt.Errorf("error seeking to object offset: %w", err) 308 } 309 buf, err := pack.ReadByte() 310 if err != nil { 311 return nil, fmt.Errorf("error reading pack object type: %w", err) 312 } 313 typ := (buf >> 4) & 7 314 if int(typ) != want && typ != ObjectRefDelta && typ != ObjectOffsetDelta { 315 return nil, errors.New("wrong packed type") 316 } 317 size := int64(buf & 15) 318 shift := 4 319 for buf&0x80 != 0 { 320 buf, err = pack.ReadByte() 321 if err != nil { 322 return nil, fmt.Errorf("error reading pack object size: %w", err) 323 } 324 size |= int64(buf&0x7f) << shift 325 shift += 7 326 } 327 var base io.ReadCloser 328 switch typ { 329 case ObjectCommit, ObjectTree, ObjectBlob: 330 z, err := decompress(pack) 331 if err != nil { 332 return nil, fmt.Errorf("error starting to decompress object: %w", err) 333 } 334 return z, nil 335 case ObjectOffsetDelta: 336 ber := byteio.BigEndianReader{Reader: pack} 337 baseOffset, _, err := ber.ReadUintX() 338 if err != nil { 339 return nil, fmt.Errorf("error reading offset: %w", err) 340 } 341 if baseOffset >= o { 342 return nil, errors.New("invalid offset for OffsetDelta") 343 } 344 if base, err = r.readPackOffset(p, o-baseOffset, want); err != nil { 345 return nil, fmt.Errorf("error reading base object: %w", err) 346 } 347 case ObjectRefDelta: 348 var ( 349 ref [20]byte 350 err error 351 ) 352 if _, err := pack.Read(ref[:]); err != nil { 353 return nil, fmt.Errorf("error reading delta ref: %w", err) 354 } 355 base, err = r.getObject(fmt.Sprintf("%x", ref[:]), want) 356 if err != nil { 357 return nil, fmt.Errorf("error reading base object: %w", err) 358 } 359 default: 360 return nil, errors.New("invalid pack type") 361 } 362 z, err := decompress(pack) 363 if err != nil { 364 return nil, fmt.Errorf("error starting to decompress object: %w", err) 365 } 366 defer z.Close() 367 b := byteio.StickyLittleEndianReader{Reader: z} 368 var bSize uint64 369 bs := byte(0x80) 370 shift = 0 371 for bs&0x80 != 0 { 372 bs = b.ReadUint8() 373 bSize |= uint64(bs&0x7f) << shift 374 shift += 7 375 } 376 var baseBuf memio.LimitedBuffer 377 switch base := base.(type) { 378 case *memio.LimitedBuffer: 379 if uint64(len(*base)) != bSize { 380 return nil, errors.New("invalid packed base size") 381 } 382 baseBuf = *base 383 default: 384 baseBuf = make(memio.LimitedBuffer, 0, bSize) 385 _, err := baseBuf.ReadFrom(base) 386 if err != nil { 387 return nil, fmt.Errorf("error reading base object: %w", err) 388 } else if len(baseBuf) != cap(baseBuf) { 389 return nil, errors.New("invalid packed base size") 390 } 391 } 392 bs = 0x80 393 shift = 0 394 bSize = 0 395 for bs&0x80 != 0 { 396 bs = b.ReadUint8() 397 bSize |= uint64(bs&0x7f) << shift 398 shift += 7 399 } 400 patched := make(memio.LimitedBuffer, 0, bSize) 401 for { 402 if b.Err != nil { 403 return nil, fmt.Errorf("error reading patch: %w", b.Err) 404 } 405 instr := b.ReadUint8() 406 if instr&0x80 == 0 { 407 l := instr & 0x7f 408 if l == 0 { 409 break 410 } 411 if _, err := io.CopyN(&patched, &b, int64(l)); err != nil { 412 return nil, fmt.Errorf("error copying data from patch: %w", err) 413 } 414 } else { 415 var offset, size uint32 416 for i := 0; i < 4; i++ { 417 if instr&1 == 1 { 418 offset |= uint32(b.ReadUint8()) << (i * 8) 419 } 420 instr >>= 1 421 } 422 for i := 0; i < 3; i++ { 423 if instr&1 == 1 { 424 size |= uint32(b.ReadUint8()) << (i * 8) 425 } 426 instr >>= 1 427 } 428 if size == 0 { 429 size = 0x10000 430 } 431 if uint32(len(patched))+size > uint32(cap(patched)) { 432 return nil, errors.New("patch overwrite") 433 } 434 patched = append(patched, baseBuf[offset:offset+size]...) 435 } 436 } 437 if len(patched) != cap(patched) { 438 return nil, errors.New("failed to read complete patched object") 439 } 440 pd.mu.Lock() 441 pd.objects[o] = object{ 442 typ: want, 443 data: patched, 444 } 445 pd.mu.Unlock() 446 return &patched, nil 447 } 448 449 func (r *Repo) getObject(id string, want int) (io.ReadCloser, error) { 450 f, err := os.Open(filepath.Join(r.path, "objects", id[:2], id[2:])) 451 if os.IsNotExist(err) { 452 r.loadPacks.Do(r.loadPacksData) 453 if r.packsErr != nil { 454 err = r.packsErr 455 } else if p, ok := r.packObjects[id]; ok { 456 return r.readPackOffset(p.pack, p.offset, want) 457 } 458 } 459 if err != nil { 460 return nil, fmt.Errorf("error opening object file (%s): %w", id, err) 461 } 462 z, err := decompress(f) 463 if err != nil { 464 f.Close() 465 return nil, fmt.Errorf("error decompressing object file (%s): %s", id, err) 466 } 467 close := true 468 defer func() { 469 if close { 470 z.Close() 471 } 472 }() 473 header := objectHeaders[want] 474 buf := bufPool.Get().(*[21]byte) 475 defer bufPool.Put(buf) 476 if _, err := io.ReadFull(z, buf[:len(header)]); err != nil { 477 return nil, fmt.Errorf("error reading object header: %w", err) 478 } 479 if string(buf[:len(header)]) != header { 480 return nil, errors.New("wrong type") 481 } 482 size := false 483 for n := range buf { 484 if _, err := z.Read(buf[n : n+1]); err != nil { 485 return nil, fmt.Errorf("error reading object size: %w", err) 486 } 487 if buf[n] == 0 { 488 size = true 489 break 490 } else if buf[n] < '0' || buf[n] > '9' { 491 return nil, errors.New("invalid object size") 492 } 493 } 494 if !size { 495 return nil, errors.New("invalid object size") 496 } 497 close = false 498 return z, nil 499 } 500 501 type Commit struct { 502 Tree, Parent, Msg string 503 Time time.Time 504 } 505 506 func (r *Repo) GetCommit(id string) (*Commit, error) { 507 r.cacheMu.RLock() 508 co, ok := r.cache[id] 509 r.cacheMu.RUnlock() 510 if ok { 511 if c, ok := co.(*Commit); ok { 512 return c, nil 513 } 514 return nil, errors.New("wrong type") 515 } 516 o, err := r.getObject(id, ObjectCommit) 517 if err != nil { 518 return nil, fmt.Errorf("error while opening commit object: %w", err) 519 } 520 var buf []byte 521 if m, ok := o.(*memio.LimitedBuffer); ok { 522 buf = *m 523 } else { 524 buf, err = io.ReadAll(o) 525 o.Close() 526 if err != nil { 527 return nil, fmt.Errorf("error reading commit: %w", err) 528 } 529 } 530 c := new(Commit) 531 for { 532 p := bytes.IndexByte(buf, '\n') 533 line := buf[:p] 534 buf = buf[p+1:] 535 if p == 0 { 536 break 537 } else if p < 0 { 538 return nil, errors.New("invalid commit") 539 } 540 if p > 5 && string(line[:5]) == "tree " { 541 if c.Tree == "" { 542 if c.Tree = checkSHA(line[5:]); c.Tree == "" { 543 return nil, errors.New("invalid tree SHA") 544 } 545 } 546 } else if p > 7 && string(line[:7]) == "parent " { 547 if c.Parent == "" { 548 if c.Parent = checkSHA(line[7:]); c.Parent == "" { 549 return nil, errors.New("invalid parent SHA") 550 } 551 } 552 } else if p > 10 && string(line[:10]) == "committer " { 553 if c.Time.IsZero() { 554 line = line[10:] 555 z := bytes.LastIndexByte(line, ' ') 556 if z < 0 { 557 return nil, errors.New("invalid timezone") 558 } 559 zoneOffset, err := strconv.ParseInt(string(line[z+1:]), 10, 16) 560 if err != nil { 561 return nil, errors.New("invalid timezone string") 562 } 563 hours := zoneOffset / 100 564 mins := zoneOffset % 100 565 s := bytes.LastIndexByte(line[:z], ' ') 566 if s < 0 { 567 return nil, errors.New("invalid timestamp") 568 } 569 unix, err := strconv.ParseInt(string(line[s+1:z]), 10, 64) 570 if err != nil { 571 return nil, fmt.Errorf("invalid timestamp string: %w", err) 572 } 573 c.Time = time.Unix(unix, 0).In(time.FixedZone("UTC", int(hours*3600+mins*60))) 574 } 575 } 576 } 577 c.Msg = string(buf[:len(buf)-1]) 578 r.cacheMu.Lock() 579 r.cache[id] = c 580 r.cacheMu.Unlock() 581 return c, nil 582 } 583 584 type Tree map[string]string 585 586 func (r *Repo) GetTree(id string) (Tree, error) { 587 r.cacheMu.RLock() 588 co, ok := r.cache[id] 589 r.cacheMu.RUnlock() 590 if ok { 591 if t, ok := co.(Tree); ok { 592 return t, nil 593 } 594 return nil, errors.New("wrong type") 595 } 596 o, err := r.getObject(id, ObjectTree) 597 if err != nil { 598 return nil, fmt.Errorf("error while opening tree object: %w", err) 599 } 600 var buf []byte 601 if m, ok := o.(*memio.LimitedBuffer); ok { 602 buf = *m 603 } else { 604 buf, err = io.ReadAll(o) 605 o.Close() 606 if err != nil { 607 return nil, err 608 } 609 } 610 files := make(Tree) 611 for len(buf) > 0 { 612 p := bytes.IndexByte(buf, ' ') 613 if p == -1 { 614 return nil, errors.New("unable to read file mode") 615 } 616 mode := buf[:p] 617 buf = buf[p+1:] 618 p = bytes.IndexByte(buf, 0) 619 if p == -1 { 620 return nil, errors.New("unable to read file mode") 621 } 622 name := string(buf[:p]) 623 if string(mode) == "40000" { 624 name += "/" 625 } else if string(mode) == "120000" { 626 name = "/" + name 627 } 628 buf = buf[p+1:] 629 files[name] = fmt.Sprintf("%x", buf[:20]) 630 buf = buf[20:] 631 } 632 r.cacheMu.Lock() 633 r.cache[id] = files 634 r.cacheMu.Unlock() 635 return files, nil 636 } 637 638 func (r *Repo) GetBlob(id string) (io.ReadCloser, error) { 639 b, err := r.getObject(id, ObjectBlob) 640 if err != nil { 641 return nil, fmt.Errorf("error reading blob: %w", err) 642 } 643 return b, nil 644 } 645 646 func checkSHA(sha []byte) string { 647 for _, c := range sha { 648 if (c < '0' || c > '9') && (c < 'A' || c > 'Z') && (c < 'a' || c > 'z') { 649 return "" 650 } 651 } 652 return string(sha) 653 } 654 655 const defaultDesc = "Unnamed repository; edit this file 'description' to name the repository.\n" 656