From 4bc5eca7ea6fc664b64283a43fa74bfc8d532ef3 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Thu, 4 Jun 2026 23:17:35 +0200 Subject: [PATCH 1/5] repository: merge check.go into checker.go --- internal/repository/check.go | 215 --------------------------------- internal/repository/checker.go | 200 +++++++++++++++++++++++++++++- 2 files changed, 199 insertions(+), 216 deletions(-) delete mode 100644 internal/repository/check.go diff --git a/internal/repository/check.go b/internal/repository/check.go deleted file mode 100644 index 0f7d322e7..000000000 --- a/internal/repository/check.go +++ /dev/null @@ -1,215 +0,0 @@ -package repository - -import ( - "bufio" - "bytes" - "context" - "crypto/sha256" - "fmt" - "io" - - "github.com/klauspost/compress/zstd" - "github.com/restic/restic/internal/backend" - "github.com/restic/restic/internal/debug" - "github.com/restic/restic/internal/errors" - "github.com/restic/restic/internal/repository/hashing" - "github.com/restic/restic/internal/repository/pack" - "github.com/restic/restic/internal/restic" -) - -// ErrPackData is returned if errors are discovered while verifying a packfile -type ErrPackData struct { - PackID restic.ID - errs []error -} - -func (e *ErrPackData) Error() string { - return fmt.Sprintf("pack %v contains %v errors: %v", e.PackID, len(e.errs), e.errs) -} - -type partialReadError struct { - err error -} - -func (e *partialReadError) Error() string { - return e.err.Error() -} - -// CheckPack reads a pack and checks the integrity of all blobs. -func CheckPack(ctx context.Context, r *Repository, id restic.ID, blobs restic.Blobs, size int64, bufRd *bufio.Reader, dec *zstd.Decoder) error { - err := checkPackInner(ctx, r, id, blobs, size, bufRd, dec) - if err != nil { - if r.cache != nil { - // ignore error as there's not much we can do here - _ = r.cache.Forget(backend.Handle{Type: restic.PackFile, Name: id.String()}) - } - - // retry pack verification to detect transient errors - err2 := checkPackInner(ctx, r, id, blobs, size, bufRd, dec) - if err2 != nil { - err = err2 - } else { - err = fmt.Errorf("check successful on second attempt, original error %w", err) - } - } - return err -} - -func checkPackInner(ctx context.Context, r *Repository, id restic.ID, blobs restic.Blobs, size int64, bufRd *bufio.Reader, dec *zstd.Decoder) error { - - debug.Log("checking pack %v", id.String()) - - if len(blobs) == 0 { - return &ErrPackData{PackID: id, errs: []error{errors.New("pack is empty or not indexed")}} - } - - // sanity check blobs in index - blobs.Sort() - idxHdrSize := pack.CalculateHeaderSize(blobs) - lastBlobEnd := 0 - nonContinuousPack := false - for _, blob := range blobs { - if lastBlobEnd != int(blob.Offset) { - nonContinuousPack = true - } - lastBlobEnd = int(blob.Offset + blob.Length) - } - // size was calculated by masterindex.PackSize, thus there's no need to recalculate it here - - var errs []error - if nonContinuousPack { - debug.Log("Index for pack contains gaps / overlaps, blobs: %v", blobs) - errs = append(errs, errors.New("index for pack contains gaps / overlapping blobs")) - } - - // calculate hash on-the-fly while reading the pack and capture pack header - var hash restic.ID - var hdrBuf []byte - // must use a separate slice from `errs` here as we're only interested in the last retry - var blobErrors []error - h := backend.Handle{Type: backend.PackFile, Name: id.String()} - err := r.be.Load(ctx, h, int(size), 0, func(rd io.Reader) error { - hrd := hashing.NewReader(rd, sha256.New()) - bufRd.Reset(hrd) - // reset blob errors for each retry - blobErrors = nil - - it := newPackBlobIterator(id, newBufReader(bufRd), 0, blobs, r.Key(), dec) - for { - if ctx.Err() != nil { - return ctx.Err() - } - - val, err := it.Next() - if err == errPackEOF { - break - } else if err != nil { - return &partialReadError{err} - } - debug.Log(" check blob %v: %v", val.Handle.ID, val.Handle) - if val.Err != nil { - debug.Log(" error verifying blob %v: %v", val.Handle.ID, val.Err) - blobErrors = append(blobErrors, errors.Errorf("blob %v: %v", val.Handle.ID, val.Err)) - } - } - - // skip enough bytes until we reach the possible header start - curPos := lastBlobEnd - minHdrStart := int(size) - pack.MaxHeaderSize - if minHdrStart > curPos { - _, err := bufRd.Discard(minHdrStart - curPos) - if err != nil { - return &partialReadError{err} - } - curPos += minHdrStart - curPos - } - - // read remainder, which should be the pack header - var err error - hdrBuf = make([]byte, int(size-int64(curPos))) - _, err = io.ReadFull(bufRd, hdrBuf) - if err != nil { - return &partialReadError{err} - } - - hash = restic.IDFromHash(hrd.Sum(nil)) - return nil - }) - errs = append(errs, blobErrors...) - if err != nil { - var e *partialReadError - isPartialReadError := errors.As(err, &e) - // failed to load the pack file, return as further checks cannot succeed anyways - debug.Log(" error streaming pack (partial %v): %v", isPartialReadError, err) - if isPartialReadError { - return &ErrPackData{PackID: id, errs: append(errs, fmt.Errorf("partial download error: %w", err))} - } - - // The check command suggests to repair files for which a `ErrPackData` is returned. However, this file - // completely failed to download such that there's no point in repairing anything. - return fmt.Errorf("download error: %w", err) - } - if !hash.Equal(id) { - debug.Log("pack ID does not match, want %v, got %v", id, hash) - return &ErrPackData{PackID: id, errs: append(errs, errors.Errorf("unexpected pack id %v", hash))} - } - - blobs, hdrSize, err := pack.List(r.Key(), bytes.NewReader(hdrBuf), int64(len(hdrBuf))) - if err != nil { - return &ErrPackData{PackID: id, errs: append(errs, err)} - } - - if uint32(idxHdrSize) != hdrSize { - debug.Log("Pack header size does not match, want %v, got %v", idxHdrSize, hdrSize) - errs = append(errs, errors.Errorf("pack header size does not match, want %v, got %v", idxHdrSize, hdrSize)) - } - - for _, blob := range blobs { - // Check if blob is contained in index and position is correct - idxHas := false - for _, pb := range r.LookupBlob(blob.BlobHandle.Type, blob.BlobHandle.ID) { - if pb.PackID == id && pb.Blob == blob { - idxHas = true - break - } - } - if !idxHas { - errs = append(errs, errors.Errorf("blob %v is not contained in index or position is incorrect", blob.ID)) - continue - } - } - - if len(errs) > 0 { - return &ErrPackData{PackID: id, errs: errs} - } - - return nil -} - -type bufReader struct { - rd *bufio.Reader - buf []byte -} - -func newBufReader(rd *bufio.Reader) *bufReader { - return &bufReader{ - rd: rd, - } -} - -func (b *bufReader) Discard(n int) (discarded int, err error) { - return b.rd.Discard(n) -} - -func (b *bufReader) ReadFull(n int) (buf []byte, err error) { - if cap(b.buf) < n { - b.buf = make([]byte, n) - } - b.buf = b.buf[:n] - - _, err = io.ReadFull(b.rd, b.buf) - if err != nil { - return nil, err - } - return b.buf, nil -} diff --git a/internal/repository/checker.go b/internal/repository/checker.go index 691ffe961..b0c978390 100644 --- a/internal/repository/checker.go +++ b/internal/repository/checker.go @@ -2,12 +2,17 @@ package repository import ( "bufio" + "bytes" "context" + "crypto/sha256" "fmt" + "io" "github.com/klauspost/compress/zstd" + "github.com/restic/restic/internal/backend" "github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/errors" + "github.com/restic/restic/internal/repository/hashing" "github.com/restic/restic/internal/repository/index" "github.com/restic/restic/internal/repository/pack" "github.com/restic/restic/internal/restic" @@ -58,6 +63,16 @@ func (e *PackError) Error() string { return "pack " + e.ID.String() + ": " + e.Err.Error() } +// ErrPackData is returned if errors are discovered while verifying a packfile +type ErrPackData struct { + PackID restic.ID + errs []error +} + +func (e *ErrPackData) Error() string { + return fmt.Sprintf("pack %v contains %v errors: %v", e.PackID, len(e.errs), e.errs) +} + // Checker handles index-related operations for repository checking. type Checker struct { repo *Repository @@ -269,7 +284,7 @@ func (c *Checker) ReadPacks(ctx context.Context, filter func(packs map[restic.ID } } - err := CheckPack(ctx, c.repo, ps.id, ps.blobs, ps.size, bufRd, dec) + err := checkPack(ctx, c.repo, ps.id, ps.blobs, ps.size, bufRd, dec) p.Add(1) if err == nil { continue @@ -309,3 +324,186 @@ func (c *Checker) ReadPacks(ctx context.Context, filter func(packs map[restic.ID } } } + +// checkPack reads a pack and checks the integrity of all blobs. +func checkPack(ctx context.Context, r *Repository, id restic.ID, blobs restic.Blobs, size int64, bufRd *bufio.Reader, dec *zstd.Decoder) error { + err := checkPackInner(ctx, r, id, blobs, size, bufRd, dec) + if err != nil { + if r.cache != nil { + // ignore error as there's not much we can do here + _ = r.cache.Forget(backend.Handle{Type: restic.PackFile, Name: id.String()}) + } + + // retry pack verification to detect transient errors + err2 := checkPackInner(ctx, r, id, blobs, size, bufRd, dec) + if err2 != nil { + err = err2 + } else { + err = fmt.Errorf("check successful on second attempt, original error %w", err) + } + } + return err +} + +func checkPackInner(ctx context.Context, r *Repository, id restic.ID, blobs restic.Blobs, size int64, bufRd *bufio.Reader, dec *zstd.Decoder) error { + + type partialReadError struct { + error + } + + debug.Log("checking pack %v", id.String()) + + if len(blobs) == 0 { + return &ErrPackData{PackID: id, errs: []error{errors.New("pack is empty or not indexed")}} + } + + // sanity check blobs in index + blobs.Sort() + idxHdrSize := pack.CalculateHeaderSize(blobs) + lastBlobEnd := 0 + nonContinuousPack := false + for _, blob := range blobs { + if lastBlobEnd != int(blob.Offset) { + nonContinuousPack = true + } + lastBlobEnd = int(blob.Offset + blob.Length) + } + // size was calculated by masterindex.PackSize, thus there's no need to recalculate it here + + var errs []error + if nonContinuousPack { + debug.Log("Index for pack contains gaps / overlaps, blobs: %v", blobs) + errs = append(errs, errors.New("index for pack contains gaps / overlapping blobs")) + } + + // calculate hash on-the-fly while reading the pack and capture pack header + var hash restic.ID + var hdrBuf []byte + // must use a separate slice from `errs` here as we're only interested in the last retry + var blobErrors []error + h := backend.Handle{Type: backend.PackFile, Name: id.String()} + err := r.be.Load(ctx, h, int(size), 0, func(rd io.Reader) error { + hrd := hashing.NewReader(rd, sha256.New()) + bufRd.Reset(hrd) + // reset blob errors for each retry + blobErrors = nil + + it := newPackBlobIterator(id, newBufReader(bufRd), 0, blobs, r.Key(), dec) + for { + if ctx.Err() != nil { + return ctx.Err() + } + + val, err := it.Next() + if err == errPackEOF { + break + } else if err != nil { + return &partialReadError{err} + } + debug.Log(" check blob %v: %v", val.Handle.ID, val.Handle) + if val.Err != nil { + debug.Log(" error verifying blob %v: %v", val.Handle.ID, val.Err) + blobErrors = append(blobErrors, errors.Errorf("blob %v: %v", val.Handle.ID, val.Err)) + } + } + + // skip enough bytes until we reach the possible header start + curPos := lastBlobEnd + minHdrStart := int(size) - pack.MaxHeaderSize + if minHdrStart > curPos { + _, err := bufRd.Discard(minHdrStart - curPos) + if err != nil { + return &partialReadError{err} + } + curPos += minHdrStart - curPos + } + + // read remainder, which should be the pack header + var err error + hdrBuf = make([]byte, int(size-int64(curPos))) + _, err = io.ReadFull(bufRd, hdrBuf) + if err != nil { + return &partialReadError{err} + } + + hash = restic.IDFromHash(hrd.Sum(nil)) + return nil + }) + errs = append(errs, blobErrors...) + if err != nil { + var e *partialReadError + isPartialReadError := errors.As(err, &e) + // failed to load the pack file, return as further checks cannot succeed anyways + debug.Log(" error streaming pack (partial %v): %v", isPartialReadError, err) + if isPartialReadError { + return &ErrPackData{PackID: id, errs: append(errs, fmt.Errorf("partial download error: %w", err))} + } + + // The check command suggests to repair files for which a `ErrPackData` is returned. However, this file + // completely failed to download such that there's no point in repairing anything. + return fmt.Errorf("download error: %w", err) + } + if !hash.Equal(id) { + debug.Log("pack ID does not match, want %v, got %v", id, hash) + return &ErrPackData{PackID: id, errs: append(errs, errors.Errorf("unexpected pack id %v", hash))} + } + + blobs, hdrSize, err := pack.List(r.Key(), bytes.NewReader(hdrBuf), int64(len(hdrBuf))) + if err != nil { + return &ErrPackData{PackID: id, errs: append(errs, err)} + } + + if uint32(idxHdrSize) != hdrSize { + debug.Log("Pack header size does not match, want %v, got %v", idxHdrSize, hdrSize) + errs = append(errs, errors.Errorf("pack header size does not match, want %v, got %v", idxHdrSize, hdrSize)) + } + + for _, blob := range blobs { + // Check if blob is contained in index and position is correct + idxHas := false + for _, pb := range r.LookupBlob(blob.BlobHandle.Type, blob.BlobHandle.ID) { + if pb.PackID == id && pb.Blob == blob { + idxHas = true + break + } + } + if !idxHas { + errs = append(errs, errors.Errorf("blob %v is not contained in index or position is incorrect", blob.ID)) + continue + } + } + + if len(errs) > 0 { + return &ErrPackData{PackID: id, errs: errs} + } + + return nil +} + +type bufReader struct { + rd *bufio.Reader + buf []byte +} + +func newBufReader(rd *bufio.Reader) *bufReader { + return &bufReader{ + rd: rd, + } +} + +func (b *bufReader) Discard(n int) (discarded int, err error) { + return b.rd.Discard(n) +} + +func (b *bufReader) ReadFull(n int) (buf []byte, err error) { + if cap(b.buf) < n { + b.buf = make([]byte, n) + } + b.buf = b.buf[:n] + + _, err = io.ReadFull(b.rd, b.buf) + if err != nil { + return nil, err + } + return b.buf, nil +} From ccddc1914d4c20c17e2aeb1b356b2e1d07572929 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Thu, 4 Jun 2026 23:17:56 +0200 Subject: [PATCH 2/5] repository: rename PackError to ErrPackMetadata --- cmd/restic/cmd_check.go | 2 +- internal/checker/checker_test.go | 4 ++-- internal/repository/checker.go | 13 +++++++------ 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/cmd/restic/cmd_check.go b/cmd/restic/cmd_check.go index f77cfd182..4a5a54694 100644 --- a/cmd/restic/cmd_check.go +++ b/cmd/restic/cmd_check.go @@ -307,7 +307,7 @@ func runCheck(ctx context.Context, opts CheckOptions, gopts global.Options, args go chkr.Packs(ctx, errChan) for err := range errChan { - var packErr *repository.PackError + var packErr *repository.ErrPackMetadata if errors.As(err, &packErr) { if packErr.Orphaned { orphanedPacks++ diff --git a/internal/checker/checker_test.go b/internal/checker/checker_test.go index 4b2008bee..2519da5bf 100644 --- a/internal/checker/checker_test.go +++ b/internal/checker/checker_test.go @@ -109,7 +109,7 @@ func TestMissingPack(t *testing.T) { test.Assert(t, len(errs) == 1, "expected exactly one error, got %v", len(errs)) - if err, ok := errs[0].(*repository.PackError); ok { + if err, ok := errs[0].(*repository.ErrPackMetadata); ok { test.Equals(t, packID, err.ID) } else { t.Errorf("expected error returned by checker.Packs() to be PackError, got %v", err) @@ -137,7 +137,7 @@ func TestUnreferencedPack(t *testing.T) { test.Assert(t, len(errs) == 1, "expected exactly one error, got %v", len(errs)) - if err, ok := errs[0].(*repository.PackError); ok { + if err, ok := errs[0].(*repository.ErrPackMetadata); ok { test.Equals(t, packID, err.ID.String()) } else { t.Errorf("expected error returned by checker.Packs() to be PackError, got %v", err) diff --git a/internal/repository/checker.go b/internal/repository/checker.go index b0c978390..c3f530abf 100644 --- a/internal/repository/checker.go +++ b/internal/repository/checker.go @@ -51,15 +51,16 @@ func (e *ErrMixedPack) Error() string { return fmt.Sprintf("pack %v contains a mix of tree and data blobs", e.PackID.Str()) } -// PackError describes an error with a specific pack. -type PackError struct { +// ErrPackMetadata describes an error with a specific pack. It is used for missing, truncated or orphaned packs. +// Errors of the actual pack data are returned as ErrPackData. +type ErrPackMetadata struct { ID restic.ID Orphaned bool Truncated bool Err error } -func (e *PackError) Error() string { +func (e *ErrPackMetadata) Error() string { return "pack " + e.ID.String() + ": " + e.Err.Error() } @@ -214,7 +215,7 @@ func (c *Checker) Packs(ctx context.Context, errChan chan<- error) { select { case <-ctx.Done(): return - case errChan <- &PackError{ID: id, Err: errors.New("does not exist")}: + case errChan <- &ErrPackMetadata{ID: id, Err: errors.New("does not exist")}: } continue } @@ -224,7 +225,7 @@ func (c *Checker) Packs(ctx context.Context, errChan chan<- error) { select { case <-ctx.Done(): return - case errChan <- &PackError{ID: id, Truncated: true, Err: errors.Errorf("unexpected file size: got %d, expected %d", reposize, size)}: + case errChan <- &ErrPackMetadata{ID: id, Truncated: true, Err: errors.Errorf("unexpected file size: got %d, expected %d", reposize, size)}: } } } @@ -234,7 +235,7 @@ func (c *Checker) Packs(ctx context.Context, errChan chan<- error) { select { case <-ctx.Done(): return - case errChan <- &PackError{ID: orphanID, Orphaned: true, Err: errors.New("not referenced in any index")}: + case errChan <- &ErrPackMetadata{ID: orphanID, Orphaned: true, Err: errors.New("not referenced in any index")}: } } } From 8c6ee42d17c9415f31f13029083bff2ce2437c6e Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Thu, 4 Jun 2026 23:22:55 +0200 Subject: [PATCH 3/5] debug: move DumpPacks into repository package Processing pack file internals belongs into the repository package. --- cmd/restic/cmd_debug.go | 48 +-------------------------------- internal/repository/debug.go | 52 ++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 47 deletions(-) diff --git a/cmd/restic/cmd_debug.go b/cmd/restic/cmd_debug.go index 0a0b43295..efc022941 100644 --- a/cmd/restic/cmd_debug.go +++ b/cmd/restic/cmd_debug.go @@ -7,7 +7,6 @@ import ( "encoding/json" "fmt" "io" - "sync" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -18,7 +17,6 @@ import ( "github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/ui" - "github.com/restic/restic/internal/ui/progress" ) func registerDebugCommand(cmd *cobra.Command, globalOptions *global.Options) { @@ -118,50 +116,6 @@ func debugPrintSnapshots(ctx context.Context, repo *repository.Repository, wr io }) } -// Pack is the struct used in printPacks. -type Pack struct { - Name string `json:"name"` - - Blobs []Blob `json:"blobs"` -} - -// Blob is the struct used in printPacks. -type Blob struct { - Type restic.BlobType `json:"type"` - Length uint `json:"length"` - ID restic.ID `json:"id"` - Offset uint `json:"offset"` -} - -func printPacks(ctx context.Context, repo *repository.Repository, wr io.Writer, printer progress.Printer) error { - - var m sync.Mutex - return restic.ParallelList(ctx, repo, restic.PackFile, repo.Connections(), func(ctx context.Context, id restic.ID, size int64) error { - blobs, err := repo.ListPack(ctx, id, size) - if err != nil { - printer.E("error for pack %v: %v", id.Str(), err) - return nil - } - - p := Pack{ - Name: id.String(), - Blobs: make([]Blob, len(blobs)), - } - for i, blob := range blobs { - p.Blobs[i] = Blob{ - Type: blob.Type, - Length: blob.Length, - ID: blob.ID, - Offset: blob.Offset, - } - } - - m.Lock() - defer m.Unlock() - return prettyPrintJSON(wr, p) - }) -} - func runDebugDump(ctx context.Context, gopts global.Options, args []string, term ui.Terminal) error { printer := ui.NewProgressPrinter(false, gopts.Verbosity, term) @@ -183,7 +137,7 @@ func runDebugDump(ctx context.Context, gopts global.Options, args []string, term case "snapshots": return debugPrintSnapshots(ctx, repo, gopts.Term.OutputWriter()) case "packs": - return printPacks(ctx, repo, gopts.Term.OutputWriter(), printer) + return repository.DumpPacks(ctx, repo, gopts.Term.OutputWriter(), printer) case "all": printer.S("snapshots:") err := debugPrintSnapshots(ctx, repo, gopts.Term.OutputWriter()) diff --git a/internal/repository/debug.go b/internal/repository/debug.go index fd6533a82..c3d5bfcd5 100644 --- a/internal/repository/debug.go +++ b/internal/repository/debug.go @@ -6,10 +6,12 @@ import ( "context" "crypto/aes" "crypto/cipher" + "encoding/json" "fmt" "io" "os" "runtime" + "sync" "time" "github.com/klauspost/compress/zstd" @@ -22,6 +24,56 @@ import ( "github.com/restic/restic/internal/ui/progress" ) +type packDumpEntry struct { + Name string `json:"name"` + Blobs []packDumpBlob `json:"blobs"` +} + +type packDumpBlob struct { + Type restic.BlobType `json:"type"` + Length uint `json:"length"` + ID restic.ID `json:"id"` + Offset uint `json:"offset"` +} + +func writePackDumpJSON(wr io.Writer, item any) error { + buf, err := json.MarshalIndent(item, "", " ") + if err != nil { + return err + } + _, err = wr.Write(append(buf, '\n')) + return err +} + +// DumpPacks lists each pack file and writes its header blob layout as JSON to wr. +func DumpPacks(ctx context.Context, repo *Repository, wr io.Writer, printer progress.Printer) error { + var m sync.Mutex + return restic.ParallelList(ctx, repo, restic.PackFile, repo.Connections(), func(ctx context.Context, id restic.ID, size int64) error { + blobs, err := repo.ListPack(ctx, id, size) + if err != nil { + printer.E("error for pack %v: %v", id.Str(), err) + return nil + } + + p := packDumpEntry{ + Name: id.String(), + Blobs: make([]packDumpBlob, len(blobs)), + } + for i, blob := range blobs { + p.Blobs[i] = packDumpBlob{ + Type: blob.Type, + Length: blob.Length, + ID: blob.ID, + Offset: blob.Offset, + } + } + + m.Lock() + defer m.Unlock() + return writePackDumpJSON(wr, p) + }) +} + // DumpIndexes loads each on-disk index file and writes its debug dump to wr. func DumpIndexes(ctx context.Context, repo restic.ListerLoaderUnpacked, wr io.Writer, printer progress.Printer) error { return index.ForAllIndexes(ctx, repo, repo, func(id restic.ID, idx *index.Index, err error) error { From 119bb9d9a84eb072f06988a70b9fbb342187aaec Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Thu, 4 Jun 2026 23:23:56 +0200 Subject: [PATCH 4/5] repository: require *Repository for ExaminePack The function wouldn't work with a different Repository implementation anyways. --- internal/repository/debug.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/repository/debug.go b/internal/repository/debug.go index c3d5bfcd5..c6691380c 100644 --- a/internal/repository/debug.go +++ b/internal/repository/debug.go @@ -95,7 +95,7 @@ type ExaminePackOptions struct { } // ExaminePack loads and inspects a pack file and its index entries. -func ExaminePack(ctx context.Context, repo restic.Repository, id restic.ID, opts ExaminePackOptions, printer progress.Printer) error { +func ExaminePack(ctx context.Context, repo *Repository, id restic.ID, opts ExaminePackOptions, printer progress.Printer) error { printer.S("examine %v", id) buf, err := repo.LoadRaw(ctx, restic.PackFile, id) @@ -285,7 +285,7 @@ func decryptUnsigned(k *crypto.Key, buf []byte) []byte { return out } -func loadBlobs(ctx context.Context, opts ExaminePackOptions, repo restic.Repository, packID restic.ID, list restic.Blobs, printer progress.Printer) error { +func loadBlobs(ctx context.Context, opts ExaminePackOptions, repo *Repository, packID restic.ID, list restic.Blobs, printer progress.Printer) error { dec, err := zstd.NewReader(nil) if err != nil { panic(err) From ff95080f36caed6bdf1655076e57455af9ca2523 Mon Sep 17 00:00:00 2001 From: Michael Eischer Date: Thu, 4 Jun 2026 20:00:03 +0200 Subject: [PATCH 5/5] restic: move test helpers and drop unused JSONUnpackedLoader --- internal/restic/backend_find.go | 4 ++-- internal/restic/blob.go | 4 ---- internal/restic/config.go | 5 ----- internal/restic/id.go | 13 ------------- internal/restic/testing.go | 17 +++++++++++++++++ 5 files changed, 19 insertions(+), 24 deletions(-) diff --git a/internal/restic/backend_find.go b/internal/restic/backend_find.go index 2f00595c4..1f9700cde 100644 --- a/internal/restic/backend_find.go +++ b/internal/restic/backend_find.go @@ -24,13 +24,13 @@ func (e *NoIDByPrefixError) Error() string { // Find loads the list of all files of type t and searches for names which // start with prefix. If none is found, nil and ErrNoIDPrefixFound is returned. // If more than one is found, nil and ErrMultipleIDMatches is returned. -func Find(ctx context.Context, be Lister, t FileType, prefix string) (ID, error) { +func Find(ctx context.Context, repo Lister, t FileType, prefix string) (ID, error) { match := ID{} ctx, cancel := context.WithCancel(ctx) defer cancel() - err := be.List(ctx, t, func(id ID, _ int64) error { + err := repo.List(ctx, t, func(id ID, _ int64) error { name := id.String() if len(name) >= len(prefix) && prefix == name[:len(prefix)] { if match.IsNull() { diff --git a/internal/restic/blob.go b/internal/restic/blob.go index 432c91db1..ba9277aac 100644 --- a/internal/restic/blob.go +++ b/internal/restic/blob.go @@ -57,10 +57,6 @@ func (h BlobHandle) String() string { return fmt.Sprintf("<%s/%s>", h.Type, h.ID.Str()) } -func NewRandomBlobHandle() BlobHandle { - return BlobHandle{ID: NewRandomID(), Type: DataBlob} -} - // BlobType specifies what a blob stored in a pack is. type BlobType uint8 diff --git a/internal/restic/config.go b/internal/restic/config.go index 264792e11..8af09c908 100644 --- a/internal/restic/config.go +++ b/internal/restic/config.go @@ -26,11 +26,6 @@ const MaxRepoVersion = 2 // is newly created with Init(). const StableRepoVersion = 2 -// JSONUnpackedLoader loads unpacked JSON. -type JSONUnpackedLoader interface { - LoadJSONUnpacked(context.Context, FileType, ID, interface{}) error -} - // CreateConfig creates a config file with a randomly selected polynomial and // ID. func CreateConfig(version uint) (Config, error) { diff --git a/internal/restic/id.go b/internal/restic/id.go index 0742cd6f1..975148283 100644 --- a/internal/restic/id.go +++ b/internal/restic/id.go @@ -1,11 +1,9 @@ package restic import ( - "crypto/rand" "crypto/sha256" "encoding/hex" "fmt" - "io" ) // Hash returns the ID for data. @@ -40,17 +38,6 @@ func (id ID) String() string { return hex.EncodeToString(id[:]) } -// NewRandomID returns a randomly generated ID. When reading from rand fails, -// the function panics. -func NewRandomID() ID { - id := ID{} - _, err := io.ReadFull(rand.Reader, id[:]) - if err != nil { - panic(err) - } - return id -} - const shortStr = 4 // Str returns the shortened string version of id. diff --git a/internal/restic/testing.go b/internal/restic/testing.go index 73e00be70..f52e6e4de 100644 --- a/internal/restic/testing.go +++ b/internal/restic/testing.go @@ -1,7 +1,9 @@ package restic import ( + "crypto/rand" "fmt" + "io" ) // TestParseID parses s as a ID and panics if that fails. @@ -18,3 +20,18 @@ func TestParseID(s string) ID { func TestParseHandle(s string, t BlobType) BlobHandle { return BlobHandle{ID: TestParseID(s), Type: t} } + +func NewRandomBlobHandle() BlobHandle { + return BlobHandle{ID: NewRandomID(), Type: DataBlob} +} + +// NewRandomID returns a randomly generated ID. When reading from rand fails, +// the function panics. +func NewRandomID() ID { + id := ID{} + _, err := io.ReadFull(rand.Reader, id[:]) + if err != nil { + panic(err) + } + return id +}