diff options
| author | 2026-03-04 08:26:56 +0800 | |
|---|---|---|
| committer | 2026-03-04 08:59:53 +0800 | |
| commit | ab7501be34032fb9e5c48726a68ae90a917af9eb (patch) | |
| tree | 20d005647569befea8133e953c3270e8fd2a2a5b /refstore/reftable | |
| parent | *: gofumpt (diff) | |
| signature | No signature | |
*: Lint
Diffstat (limited to 'refstore/reftable')
| -rw-r--r-- | refstore/reftable/lookup.go | 108 | ||||
| -rw-r--r-- | refstore/reftable/parse_helpers.go | 5 | ||||
| -rw-r--r-- | refstore/reftable/reftable_test.go | 31 | ||||
| -rw-r--r-- | refstore/reftable/store.go | 64 | ||||
| -rw-r--r-- | refstore/reftable/table.go | 59 |
5 files changed, 248 insertions, 19 deletions
diff --git a/refstore/reftable/lookup.go b/refstore/reftable/lookup.go index 8862f7e6..53483bbf 100644 --- a/refstore/reftable/lookup.go +++ b/refstore/reftable/lookup.go @@ -16,13 +16,16 @@ func (table *tableFile) resolveRecord(name string) (recordValue, bool, error) { if err != nil { return recordValue{}, false, err } + pos, ok, err := table.resolveRefBlockPosFromIndex(name, indexPos) if err != nil { return recordValue{}, false, err } + if !ok { return recordValue{}, false, nil } + return table.lookupInRefBlock(name, pos) } @@ -32,28 +35,36 @@ func (table *tableFile) resolveRecord(name string) (recordValue, bool, error) { for pos < table.refEnd && table.data[pos] == 0 { pos++ } + if pos >= table.refEnd { break } + if table.data[pos] != blockTypeRef { return recordValue{}, false, fmt.Errorf("refstore/reftable: table %q: unexpected block type %q in ref section", table.name, table.data[pos]) } + block, blockEnd, err := table.readBlockAt(pos) if err != nil { return recordValue{}, false, err } + found, done, rec, err := lookupRecordInRefBlock(table, block, name) if err != nil { return recordValue{}, false, err } + if found { return rec, true, nil } + if done { return recordValue{}, false, nil } + pos = table.nextBlockPos(blockEnd) } + return recordValue{}, false, nil } @@ -63,16 +74,20 @@ func (table *tableFile) resolveRefBlockPosFromIndex(name string, indexPos int) ( if err != nil { return 0, false, err } + if block.blockType != blockTypeIndex { return 0, false, fmt.Errorf("refstore/reftable: table %q: ref index root is not index block", table.name) } + childPos, ok, err := lookupChildPosInIndexBlock(block, name) if err != nil { return 0, false, err } + if !ok { return 0, false, nil } + if childPos < 0 || childPos >= len(table.data) { return 0, false, fmt.Errorf("refstore/reftable: table %q: index child position out of range", table.name) } @@ -94,13 +109,16 @@ func (table *tableFile) lookupInRefBlock(name string, pos int) (recordValue, boo if err != nil { return recordValue{}, false, err } + if block.blockType != blockTypeRef { return recordValue{}, false, fmt.Errorf("refstore/reftable: table %q: expected ref block at %d", table.name, pos) } + found, _, rec, err := lookupRecordInRefBlock(table, block, name) if err != nil { return recordValue{}, false, err } + return rec, found, nil } @@ -108,13 +126,16 @@ func (table *tableFile) lookupInRefBlock(name string, pos int) (recordValue, boo func (table *tableFile) forEachRecord(fn func(name string, rec recordValue) error) error { pos := table.headerLen prevLast := "" + for pos < table.refEnd { for pos < table.refEnd && table.data[pos] == 0 { pos++ } + if pos >= table.refEnd { break } + if table.data[pos] != blockTypeRef { return fmt.Errorf("refstore/reftable: table %q: unexpected block type %q in ref section", table.name, table.data[pos]) } @@ -123,25 +144,33 @@ func (table *tableFile) forEachRecord(fn func(name string, rec recordValue) erro if err != nil { return err } + var first, last string + err = forEachRecordInRefBlock(table, block, func(name string, rec recordValue) error { if first == "" { first = name } + last = name + return fn(name, rec) }) if err != nil { return err } + if prevLast != "" && first != "" && strings.Compare(first, prevLast) <= 0 { return fmt.Errorf("refstore/reftable: table %q: ref blocks are not strictly ordered", table.name) } + if last != "" { prevLast = last } + pos = table.nextBlockPos(blockEnd) } + return nil } @@ -159,22 +188,29 @@ func (table *tableFile) readBlockAt(pos int) (blockView, int, error) { if pos < 0 || pos+4 > len(table.data) { return blockView{}, 0, fmt.Errorf("refstore/reftable: table %q: block header out of range", table.name) } + blockLen := int(readUint24(table.data[pos+1 : pos+4])) + effectiveLen := blockLen if pos == table.headerLen { if blockLen < table.headerLen { return blockView{}, 0, fmt.Errorf("refstore/reftable: table %q: invalid first block length", table.name) } + effectiveLen = blockLen - table.headerLen } + if effectiveLen < 4 { return blockView{}, 0, fmt.Errorf("refstore/reftable: table %q: invalid block length", table.name) } + end := pos + effectiveLen if end > len(table.data) { return blockView{}, 0, fmt.Errorf("refstore/reftable: table %q: block out of range", table.name) } + view := blockView{blockType: table.data[pos], start: pos, end: end, first: pos == table.headerLen, payload: table.data[pos:end]} + return view, end, nil } @@ -183,6 +219,7 @@ func (table *tableFile) nextBlockPos(blockEnd int) int { if table.blockSize > 0 { return alignUp(blockEnd, table.blockSize) } + return blockEnd } @@ -192,35 +229,45 @@ func lookupChildPosInIndexBlock(block blockView, key string) (int, bool, error) if err != nil { return 0, false, err } - if err := validateRestarts(block, restarts, off, recordsEnd, true); err != nil { + + err = validateRestarts(block, restarts, off, recordsEnd, true) + if err != nil { return 0, false, err } + prev := "" for off < recordsEnd { name, v, nextOff, err := parseKeyedRecord(block.payload, off, recordsEnd, prev) if err != nil { return 0, false, err } + if (v & 0x7) != 0 { return 0, false, fmt.Errorf("index value_type must be 0") } + childPos, nextOff, err := readVarint(block.payload, nextOff, recordsEnd) if err != nil { return 0, false, err } + if strings.Compare(key, name) <= 0 { childPosInt, err := intconv.Uint64ToInt(childPos) if err != nil { return 0, false, fmt.Errorf("index child position conversion: %w", err) } + return childPosInt, true, nil } + prev = name off = nextOff } + if off != recordsEnd { return 0, false, fmt.Errorf("malformed index block") } + return 0, false, nil } @@ -230,37 +277,48 @@ func lookupRecordInRefBlock(table *tableFile, block blockView, key string) (foun if err != nil { return false, false, recordValue{}, err } - if err := validateRestarts(block, restarts, off, recordsEnd, true); err != nil { + + err = validateRestarts(block, restarts, off, recordsEnd, true) + if err != nil { return false, false, recordValue{}, err } + prev := "" for off < recordsEnd { name, v, nextOff, err := parseKeyedRecord(block.payload, off, recordsEnd, prev) if err != nil { return false, false, recordValue{}, err } + typeBits := byte(v & 0x7) + _, nextOff, err = readVarint(block.payload, nextOff, recordsEnd) if err != nil { return false, false, recordValue{}, err } + recVal, nextOff, err := parseRefValue(block.payload, nextOff, recordsEnd, table.algo, typeBits) if err != nil { return false, false, recordValue{}, err } + cmp := strings.Compare(name, key) if cmp == 0 { return true, true, recVal, nil } + if cmp > 0 { return false, true, recordValue{}, nil } + prev = name off = nextOff } + if off != recordsEnd { return false, false, recordValue{}, fmt.Errorf("malformed ref block") } + return false, false, recordValue{}, nil } @@ -270,33 +328,44 @@ func forEachRecordInRefBlock(table *tableFile, block blockView, fn func(name str if err != nil { return err } - if err := validateRestarts(block, restarts, off, recordsEnd, true); err != nil { + + err = validateRestarts(block, restarts, off, recordsEnd, true) + if err != nil { return err } + prev := "" for off < recordsEnd { name, v, nextOff, err := parseKeyedRecord(block.payload, off, recordsEnd, prev) if err != nil { return err } + typeBits := byte(v & 0x7) + _, nextOff, err = readVarint(block.payload, nextOff, recordsEnd) if err != nil { return err } + recVal, nextOff, err := parseRefValue(block.payload, nextOff, recordsEnd, table.algo, typeBits) if err != nil { return err } - if err := fn(name, recVal); err != nil { + + err = fn(name, recVal) + if err != nil { return err } + prev = name off = nextOff } + if off != recordsEnd { return fmt.Errorf("malformed ref block") } + return nil } @@ -305,51 +374,63 @@ func parseBlockLayout(block blockView) (recordsStart, recordsEnd int, restarts [ if len(block.payload) < 6 { return 0, 0, nil, fmt.Errorf("short block") } + restartCount := int(binary.BigEndian.Uint16(block.payload[len(block.payload)-2:])) if restartCount <= 0 { return 0, 0, nil, fmt.Errorf("invalid restart count") } + restarts = make([]int, restartCount) restartBytes := restartCount * 3 + restartsStart := len(block.payload) - 2 - restartBytes if restartsStart < 4 { return 0, 0, nil, fmt.Errorf("invalid restart table") } + for i := range restartCount { off := restartsStart + i*3 rel := int(readUint24(block.payload[off : off+3])) + base := block.start if block.first { // In the first block, restart offsets are relative to file start. base = 0 } + abs := base + rel restarts[i] = abs - block.start } + return 4, restartsStart, restarts, nil } // validateRestarts validates restart monotonicity, bounds and record-prefix invariants. func validateRestarts(block blockView, restarts []int, recordsStart, recordsEnd int, requirePrefixZero bool) error { prev := -1 + for _, off := range restarts { if off < recordsStart || off >= recordsEnd { return fmt.Errorf("restart offset out of range") } + if off <= prev { return fmt.Errorf("restart offsets not strictly increasing") } + prev = off if requirePrefixZero { prefix, _, err := readVarint(block.payload, off, recordsEnd) if err != nil { return err } + if prefix != 0 { return fmt.Errorf("restart record prefix length must be zero") } } } + return nil } @@ -359,26 +440,33 @@ func parseKeyedRecord(buf []byte, off, end int, prev string) (name string, rawTy if err != nil { return "", 0, 0, err } + suffixAndType, next, err := readVarint(buf, next, end) if err != nil { return "", 0, 0, err } + suffixLen, err := intconv.Uint64ToInt(suffixAndType >> 3) if err != nil || suffixLen < 0 || next+suffixLen > end { return "", 0, 0, fmt.Errorf("invalid suffix length") } + prefixLenInt, err := intconv.Uint64ToInt(prefixLen) if err != nil { return "", 0, 0, fmt.Errorf("invalid prefix length") } + if prefixLenInt > len(prev) { return "", 0, 0, fmt.Errorf("invalid prefix length") } + name = prev[:prefixLenInt] + string(buf[next:next+suffixLen]) next += suffixLen + if prev != "" && strings.Compare(name, prev) <= 0 { return "", 0, 0, fmt.Errorf("keys not strictly increasing") } + return name, suffixAndType, next, nil } @@ -392,40 +480,50 @@ func parseRefValue(buf []byte, off, end int, algo objectid.Algorithm, valueType if err != nil { return recordValue{}, 0, err } + return recordValue{detachedID: id, hasDetached: true}, next, nil case 0x2: id, next, err := readObjectID(buf, off, end, algo) if err != nil { return recordValue{}, 0, err } + peeled, next, err := readObjectID(buf, next, end, algo) if err != nil { return recordValue{}, 0, err } + peeledCopy := peeled + return recordValue{detachedID: id, hasDetached: true, peeled: &peeledCopy}, next, nil case 0x3: targetLen, next, err := readVarint(buf, off, end) if err != nil { return recordValue{}, 0, err } + remaining := end - next if remaining < 0 { return recordValue{}, 0, fmt.Errorf("invalid symref target length") } + remainingU64, err := intconv.IntToUint64(remaining) if err != nil { return recordValue{}, 0, fmt.Errorf("invalid symref target length") } + if targetLen > remainingU64 { return recordValue{}, 0, fmt.Errorf("invalid symref target length") } + targetLenInt, err := intconv.Uint64ToInt(targetLen) if err != nil { return recordValue{}, 0, fmt.Errorf("invalid symref target length") } + target := string(buf[next : next+targetLenInt]) next += targetLenInt + return recordValue{symbolicTarget: target}, next, nil default: return recordValue{}, 0, fmt.Errorf("unsupported ref value type %d", valueType) @@ -438,9 +536,11 @@ func readObjectID(buf []byte, off, end int, algo objectid.Algorithm) (objectid.O if off < 0 || sz < 0 || off+sz > end { return objectid.ObjectID{}, 0, fmt.Errorf("truncated object id") } + id, err := objectid.FromBytes(algo, buf[off:off+sz]) if err != nil { return objectid.ObjectID{}, 0, err } + return id, off + sz, nil } diff --git a/refstore/reftable/parse_helpers.go b/refstore/reftable/parse_helpers.go index b5da555e..5b5fae24 100644 --- a/refstore/reftable/parse_helpers.go +++ b/refstore/reftable/parse_helpers.go @@ -13,6 +13,7 @@ func alignUp(pos, blockSize int) int { if rem == 0 { return pos } + return pos + (blockSize - rem) } @@ -21,16 +22,20 @@ func readVarint(buf []byte, off, end int) (uint64, int, error) { if off >= end { return 0, 0, fmt.Errorf("unexpected EOF") } + b := buf[off] val := uint64(b & 0x7f) + off++ for b&0x80 != 0 { if off >= end { return 0, 0, fmt.Errorf("unexpected EOF") } + b = buf[off] off++ val = ((val + 1) << 7) | uint64(b&0x7f) } + return val, off, nil } diff --git a/refstore/reftable/reftable_test.go b/refstore/reftable/reftable_test.go index 2a6e0738..26aa7584 100644 --- a/refstore/reftable/reftable_test.go +++ b/refstore/reftable/reftable_test.go @@ -17,6 +17,7 @@ import ( // newBareReftableRepo creates a bare repository that uses reftable ref storage. func newBareReftableRepo(tb testing.TB, algo objectid.Algorithm) *testgit.TestRepo { tb.Helper() + return testgit.NewRepo(tb, testgit.RepoOptions{ ObjectFormat: algo, Bare: true, @@ -27,15 +28,19 @@ func newBareReftableRepo(tb testing.TB, algo objectid.Algorithm) *testgit.TestRe // openStore opens a reftable store against repoDir/reftable. func openStore(tb testing.TB, repoDir string, algo objectid.Algorithm) *reftable.Store { tb.Helper() + root, err := os.OpenRoot(filepath.Join(repoDir, "reftable")) if err != nil { tb.Fatalf("OpenRoot(reftable): %v", err) } + tb.Cleanup(func() { _ = root.Close() }) + store, err := reftable.New(root, algo) if err != nil { tb.Fatalf("reftable.New: %v", err) } + return store } @@ -48,14 +53,17 @@ func TestResolveAndResolveFully(t *testing.T) { repo.SymbolicRef(t, "HEAD", "refs/heads/main") store := openStore(t, repo.Dir(), algo) + head, err := store.Resolve("HEAD") if err != nil { t.Fatalf("Resolve(HEAD): %v", err) } + sym, ok := head.(ref.Symbolic) if !ok { t.Fatalf("Resolve(HEAD) type = %T, want ref.Symbolic", head) } + if sym.Target != "refs/heads/main" { t.Fatalf("Resolve(HEAD) target = %q, want refs/heads/main", sym.Target) } @@ -64,11 +72,13 @@ func TestResolveAndResolveFully(t *testing.T) { if err != nil { t.Fatalf("ResolveFully(HEAD): %v", err) } + if main.ID != id { t.Fatalf("ResolveFully(HEAD) id = %s, want %s", main.ID, id) } - if _, err := store.Resolve("refs/heads/missing"); !errors.Is(err, refstore.ErrReferenceNotFound) { + _, err = store.Resolve("refs/heads/missing") + if !errors.Is(err, refstore.ErrReferenceNotFound) { t.Fatalf("Resolve(missing) error = %v", err) } }) @@ -82,7 +92,9 @@ func TestResolveFullyCycle(t *testing.T) { repo.SymbolicRef(t, "refs/heads/b", "refs/heads/a") store := openStore(t, repo.Dir(), algo) - if _, err := store.ResolveFully("refs/heads/a"); err == nil { + + _, err := store.ResolveFully("refs/heads/a") + if err == nil { t.Fatalf("ResolveFully(cycle) expected error") } }) @@ -99,14 +111,17 @@ func TestListAndShorten(t *testing.T) { repo.UpdateRef(t, "refs/remotes/origin/main", id) store := openStore(t, repo.Dir(), algo) + all, err := store.List("") if err != nil { t.Fatalf("List(all): %v", err) } + names := make([]string, 0, len(all)) for _, entry := range all { names = append(names, entry.Name()) } + want := []string{"HEAD", "refs/heads/feature", "refs/heads/main", "refs/remotes/origin/main", "refs/tags/main"} if !slices.Equal(names, want) { t.Fatalf("List(all) = %v, want %v", names, want) @@ -116,10 +131,12 @@ func TestListAndShorten(t *testing.T) { if err != nil { t.Fatalf("List(heads): %v", err) } + headNames := make([]string, 0, len(heads)) for _, entry := range heads { headNames = append(headNames, entry.Name()) } + wantHeads := []string{"refs/heads/feature", "refs/heads/main"} if !slices.Equal(headNames, wantHeads) { t.Fatalf("List(heads) = %v, want %v", headNames, wantHeads) @@ -129,6 +146,7 @@ func TestListAndShorten(t *testing.T) { if err != nil { t.Fatalf("Shorten(remote): %v", err) } + if short != "origin/main" { t.Fatalf("Shorten(remote) = %q, want origin/main", short) } @@ -146,7 +164,9 @@ func TestTombstoneNewestWins(t *testing.T) { repo.DeleteRef(t, "refs/heads/main") store := openStore(t, repo.Dir(), algo) - if _, err := store.Resolve("refs/heads/main"); !errors.Is(err, refstore.ErrReferenceNotFound) { + + _, err := store.Resolve("refs/heads/main") + if !errors.Is(err, refstore.ErrReferenceNotFound) { t.Fatalf("Resolve(main) after delete error = %v", err) } }) @@ -160,20 +180,25 @@ func TestAnnotatedTagPeeled(t *testing.T) { tagID := repo.TagAnnotated(t, "v1.0.0", commitID, "annotated") store := openStore(t, repo.Dir(), algo) + resolved, err := store.Resolve("refs/tags/v1.0.0") if err != nil { t.Fatalf("Resolve(tag): %v", err) } + detached, ok := resolved.(ref.Detached) if !ok { t.Fatalf("Resolve(tag) type = %T, want ref.Detached", resolved) } + if detached.ID != tagID { t.Fatalf("Resolve(tag) id = %s, want %s", detached.ID, tagID) } + if detached.Peeled == nil { t.Fatalf("Resolve(tag) peeled = nil") } + if *detached.Peeled != commitID { t.Fatalf("Resolve(tag) peeled = %s, want %s", *detached.Peeled, commitID) } diff --git a/refstore/reftable/store.go b/refstore/reftable/store.go index 7c02c157..d0d906fc 100644 --- a/refstore/reftable/store.go +++ b/refstore/reftable/store.go @@ -42,6 +42,7 @@ func New(root *os.Root, algo objectid.Algorithm) (*Store, error) { if algo.Size() == 0 { return nil, objectid.ErrInvalidAlgorithm } + return &Store{root: root, algo: algo}, nil } @@ -50,25 +51,33 @@ func (store *Store) Close() error { store.stateMu.Lock() if store.closed { store.stateMu.Unlock() + return nil } + store.closed = true root := store.root tables := store.tables store.stateMu.Unlock() var closeErr error + for _, table := range tables { if table == nil { continue } - if err := table.close(); err != nil && closeErr == nil { + + err := table.close() + if err != nil && closeErr == nil { closeErr = err } } - if err := root.Close(); err != nil && closeErr == nil { + + err := root.Close() + if err != nil && closeErr == nil { closeErr = err } + return closeErr } @@ -78,23 +87,29 @@ func (store *Store) Resolve(name string) (ref.Ref, error) { if err != nil { return nil, err } + for i := len(tables) - 1; i >= 0; i-- { rec, found, err := tables[i].resolveRecord(name) if err != nil { return nil, err } + if !found { continue } + if rec.deleted { return nil, refstore.ErrReferenceNotFound } + resolved, err := rec.toRef(name) if err != nil { return nil, err } + return resolved, nil } + return nil, refstore.ErrReferenceNotFound } @@ -104,16 +119,21 @@ func (store *Store) Resolve(name string) (ref.Ref, error) { // annotated tag objects. func (store *Store) ResolveFully(name string) (ref.Detached, error) { seen := map[string]struct{}{} + cur := name for { - if _, exists := seen[cur]; exists { + _, exists := seen[cur] + if exists { return ref.Detached{}, errors.New("refstore/reftable: symbolic reference cycle") } + seen[cur] = struct{}{} + resolved, err := store.Resolve(cur) if err != nil { return ref.Detached{}, err } + switch resolved := resolved.(type) { case ref.Detached: return resolved, nil @@ -121,6 +141,7 @@ func (store *Store) ResolveFully(name string) (ref.Detached, error) { if resolved.Target == "" { return ref.Detached{}, errors.New("refstore/reftable: symbolic reference has empty target") } + cur = resolved.Target default: return ref.Detached{}, errors.New("refstore/reftable: unsupported reference type") @@ -137,32 +158,41 @@ func (store *Store) List(pattern string) ([]ref.Ref, error) { if err != nil { return nil, err } + visible := make(map[string]ref.Ref) masked := make(map[string]struct{}) for i := len(tables) - 1; i >= 0; i-- { - if err := tables[i].forEachRecord(func(name string, rec recordValue) error { - if _, done := masked[name]; done { + err := tables[i].forEachRecord(func(name string, rec recordValue) error { + _, done := masked[name] + if done { return nil } + masked[name] = struct{}{} + if rec.deleted { return nil } + resolved, err := rec.toRef(name) if err != nil { return err } + visible[name] = resolved + return nil - }); err != nil { + }) + if err != nil { return nil, err } } matchAll := pattern == "" if !matchAll { - if _, err := pathMatch(pattern, "refs/heads/main"); err != nil { + _, err := pathMatch(pattern, "refs/heads/main") + if err != nil { return nil, err } } @@ -171,6 +201,7 @@ func (store *Store) List(pattern string) ([]ref.Ref, error) { for name := range visible { names = append(names, name) } + sort.Strings(names) out := make([]ref.Ref, 0, len(names)) @@ -180,12 +211,15 @@ func (store *Store) List(pattern string) ([]ref.Ref, error) { if err != nil { return nil, err } + if !ok { continue } } + out = append(out, visible[name]) } + return out, nil } @@ -195,21 +229,27 @@ func (store *Store) Shorten(name string) (string, error) { if err != nil { return "", err } + names := make([]string, 0, len(refs)) found := false + for _, entry := range refs { if entry == nil { continue } + full := entry.Name() + names = append(names, full) if full == name { found = true } } + if !found { return "", refstore.ErrReferenceNotFound } + return refstore.ShortenName(name, names), nil } @@ -225,9 +265,11 @@ func (store *Store) ensureTables() ([]*tableFile, error) { store.stateMu.RLock() defer store.stateMu.RUnlock() + if store.closed { return nil, errors.New("refstore/reftable: store is closed") } + return store.tables, store.loadErr } @@ -238,18 +280,23 @@ func (store *Store) loadTables() ([]*tableFile, error) { if errors.Is(err, os.ErrNotExist) { return nil, nil } + return nil, err } + lines := strings.Split(string(listRaw), "\n") + names := make([]string, 0, len(lines)) for _, line := range lines { line = strings.TrimSuffix(line, "\r") if line == "" { continue } + if strings.Contains(line, "/") { return nil, errors.New("refstore/reftable: invalid table name") } + names = append(names, line) } @@ -260,9 +307,12 @@ func (store *Store) loadTables() ([]*tableFile, error) { for _, opened := range out { _ = opened.close() } + return nil, err } + out = append(out, table) } + return out, nil } diff --git a/refstore/reftable/table.go b/refstore/reftable/table.go index 35982bf9..5c05a633 100644 --- a/refstore/reftable/table.go +++ b/refstore/reftable/table.go @@ -71,49 +71,69 @@ func openTableFile(root *os.Root, name string, algo objectid.Algorithm) (*tableF if err != nil { return nil, err } + info, err := file.Stat() if err != nil { _ = file.Close() + return nil, err } + size := info.Size() if size < 0 || size > int64(int(^uint(0)>>1)) { _ = file.Close() + return nil, fmt.Errorf("refstore/reftable: table %q has unsupported size", name) } + fd, err := intconv.UintptrToInt(file.Fd()) if err != nil { _ = file.Close() + return nil, err } + data, err := syscall.Mmap(fd, 0, int(size), syscall.PROT_READ, syscall.MAP_PRIVATE) if err != nil { _ = file.Close() + return nil, err } + out := &tableFile{name: name, algo: algo, file: file, data: data} - if err := out.parseMeta(); err != nil { + + err = out.parseMeta() + if err != nil { _ = out.close() + return nil, err } + return out, nil } // close unmaps and closes one table file. func (table *tableFile) close() error { var closeErr error + if table.data != nil { - if err := syscall.Munmap(table.data); err != nil && closeErr == nil { + err := syscall.Munmap(table.data) + if err != nil && closeErr == nil { closeErr = err } + table.data = nil } + if table.file != nil { - if err := table.file.Close(); err != nil && closeErr == nil { + err := table.file.Close() + if err != nil && closeErr == nil { closeErr = err } + table.file = nil } + return closeErr } @@ -122,9 +142,11 @@ func (table *tableFile) parseMeta() error { if len(table.data) < 24 { return fmt.Errorf("refstore/reftable: table %q: file too short", table.name) } + if string(table.data[:4]) != reftableMagic { return fmt.Errorf("refstore/reftable: table %q: bad magic", table.name) } + version := table.data[4] switch version { case version1: @@ -137,35 +159,47 @@ func (table *tableFile) parseMeta() error { if len(table.data) < table.headerLen { return fmt.Errorf("refstore/reftable: table %q: truncated header", table.name) } + hashID := binary.BigEndian.Uint32(table.data[24:28]) - if err := validateHashID(hashID, table.algo); err != nil { + + err := validateHashID(hashID, table.algo) + if err != nil { return fmt.Errorf("refstore/reftable: table %q: %w", table.name, err) } default: return fmt.Errorf("refstore/reftable: table %q: unsupported version %d", table.name, version) } + table.blockSize = int(readUint24(table.data[5:8])) footerLen := 68 if version == version2 { footerLen = 72 } + if len(table.data) < footerLen { return fmt.Errorf("refstore/reftable: table %q: missing footer", table.name) } + footerStart := len(table.data) - footerLen + footer := table.data[footerStart:] if string(footer[:4]) != reftableMagic || footer[4] != version { return fmt.Errorf("refstore/reftable: table %q: invalid footer header", table.name) } + wantCRC := binary.BigEndian.Uint32(footer[footerLen-4:]) + haveCRC := crc32.ChecksumIEEE(footer[:footerLen-4]) if wantCRC != haveCRC { return fmt.Errorf("refstore/reftable: table %q: footer crc mismatch", table.name) } + if version == version2 { hashID := binary.BigEndian.Uint32(footer[24:28]) - if err := validateHashID(hashID, table.algo); err != nil { + + err := validateHashID(hashID, table.algo) + if err != nil { return fmt.Errorf("refstore/reftable: table %q: %w", table.name, err) } } @@ -188,34 +222,44 @@ func (table *tableFile) parseMeta() error { if err != nil { return fmt.Errorf("refstore/reftable: table %q: invalid footer offset: %w", table.name, err) } + if table.refIndexPos != 0 && table.refIndexPos < refEnd { refEnd = table.refIndexPos } + if objPos != 0 && objPos < refEnd { refEnd = objPos } + if logPos != 0 && logPos < refEnd { refEnd = logPos } + headerLenU64, err := intconv.IntToUint64(table.headerLen) if err != nil { return fmt.Errorf("refstore/reftable: table %q: invalid header length: %w", table.name, err) } + dataLenU64, err := intconv.IntToUint64(len(table.data)) if err != nil { return fmt.Errorf("refstore/reftable: table %q: invalid data length: %w", table.name, err) } + if refEnd < headerLenU64 || refEnd > dataLenU64 { return fmt.Errorf("refstore/reftable: table %q: invalid ref section", table.name) } + if table.refIndexPos > dataLenU64 { return fmt.Errorf("refstore/reftable: table %q: invalid ref index position", table.name) } + refEndInt, err := intconv.Uint64ToInt(refEnd) if err != nil { return fmt.Errorf("refstore/reftable: table %q: invalid ref section end: %w", table.name, err) } + table.refEnd = refEndInt + return nil } @@ -226,11 +270,13 @@ func validateHashID(hashID uint32, algo objectid.Algorithm) error { if algo != objectid.AlgorithmSHA1 { return errors.New("hash id sha1 mismatch") } + return nil case hashIDSHA256: if algo != objectid.AlgorithmSHA256 { return errors.New("hash id s256 mismatch") } + return nil default: return fmt.Errorf("unknown hash id 0x%08x", hashID) @@ -242,11 +288,14 @@ func (record recordValue) toRef(name string) (ref.Ref, error) { if record.deleted { return nil, errors.New("refstore/reftable: cannot materialize deleted record") } + if record.symbolicTarget != "" { return ref.Symbolic{RefName: name, Target: record.symbolicTarget}, nil } + if !record.hasDetached { return nil, errors.New("refstore/reftable: malformed detached record") } + return ref.Detached{RefName: name, ID: record.detachedID, Peeled: record.peeled}, nil } |
