Artifact 678cdcb6d0a95037206897bff4b1159727983c00d8da6a9f445f3a0c4ed20861:
- File
internal/verify/check_blobs.go
— part of check-in
[9a9bc65f92]
at
2026-10-19 01:50:41
on branch feat/submanifest-reader
— perf(verify): check blob integrity by walking delta trees once
Verify and Rebuild expanded every blob one at a time through a 256 MiB LRU cache. A real repository's delta chains run hundreds of revisions deep, so evicted chains were walked again from their roots: the blob check on the Fossil SCM repository ran for over 45 minutes.
content.ExpandAll walks each delta tree from its root, as fossil's rebuild_step does, expanding every blob once from its parent's content and binding it to its name. It reports for each blob what a caching Expand of that blob alone reports, and expands blobs no sound root reaches (cycles, phantom-grounded or too-deep chains) on their own for that error. checkBlobs and Verify's checkCheckins both use it and sort their issues by rid, so reports are unchanged. (user: [email protected], size: 2601) [annotate] [blame] [check-ins using] [more...]
package verify import ( "errors" "fmt" "sort" "github.com/danmestas/go-libfossil/internal/content" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" "github.com/danmestas/go-libfossil/internal/repo" ) // checkBlobs verifies blob integrity: content hashing and delta application. // It expands every non-phantom blob once, in delta-tree order, and checks the // content against the blob's stored UUID; see content.ExpandAll for why a // blob-by-blob expansion is quadratic in chain depth on a real repository. // Issues are reported in rid order. func checkBlobs(r *repo.Repo, report *Report) error { if r == nil { panic("checkBlobs: nil *repo.Repo") } if report == nil { panic("checkBlobs: nil *Report") } var issues []Issue err := content.ExpandAll(r.DB(), func(rid libfossil.FslID, uuid string, _ []byte, err error) error { report.BlobsChecked++ if issue := blobIssue(rid, uuid, err); issue != nil { report.BlobsFailed++ issues = append(issues, *issue) return nil } report.BlobsOK++ return nil }) if err != nil { return fmt.Errorf("checkBlobs: %w", err) } sort.Slice(issues, func(i, j int) bool { return issues[i].RID < issues[j].RID }) for _, issue := range issues { report.addIssue(issue) } // Postcondition: every checked blob is either OK or failed. if report.BlobsChecked != report.BlobsOK+report.BlobsFailed { panic("checkBlobs: postcondition: BlobsChecked != BlobsOK + BlobsFailed") } return nil } // blobIssue classifies the outcome of expanding one blob, whose content // content.ExpandAll has already bound to uuid. It returns nil when the blob is // sound. func blobIssue(rid libfossil.FslID, uuid string, err error) *Issue { if err == nil { return nil } issue := &Issue{RID: rid, UUID: uuid, Table: "blob"} var mismatch *content.HashMismatchError if errors.As(err, &mismatch) && mismatch.RID != rid { // An interior delta base is the misnamed blob; its own row reports // the mismatch. This one cannot be expanded because its base failed. issue.Kind = IssueBlobCorrupt issue.Message = fmt.Sprintf("rid %d: expand failed: base %v", rid, err) return issue } if errors.Is(err, content.ErrHashMismatch) { // Expansion binds content to its name, so a mismatch surfaces as // this error. The content expanded; reporting it as corrupt would // hide that only the name is wrong. issue.Kind = IssueHashMismatch issue.Message = fmt.Sprintf("rid %d: %v", rid, err) return issue } issue.Kind = IssueBlobCorrupt issue.Message = fmt.Sprintf("rid %d: expand failed: %v", rid, err) return issue }