{"record":{"id":"357c2402783d1b45","repo":"restic/restic","slug":"internal-error","errorCode":null,"errorMessage":"internal error","messagePattern":"internal error","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/restorer/restorer.go","lineNumber":489,"sourceCode":"\t\t\t}\n\n\t\t\tif node == nil {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\terr := res.restoreNodeMetadataTo(node, target, location)\n\t\t\tif err == nil {\n\t\t\t\tres.opts.Progress.AddProgress(location, ActionDirRestored, 0, 0)\n\t\t\t}\n\t\t\treturn err\n\t\t},\n\t})\n\treturn restoredFileCount, err\n}\n\nfunc (res *Restorer) removeUnexpectedFiles(ctx context.Context, target, location string, expectedFilenames []string) error {\n\tif !res.opts.Delete {\n\t\tpanic(\"internal error\")\n\t}\n\n\tentries, err := fs.Readdirnames(fs.NewLocal(), target, fs.O_NOFOLLOW)\n\tif errors.Is(err, os.ErrNotExist) {\n\t\treturn nil\n\t} else if err != nil {\n\t\treturn err\n\t}\n\n\tkeep := map[string]struct{}{}\n\tfor _, name := range expectedFilenames {\n\t\tkeep[toComparableFilename(name)] = struct{}{}\n\t}\n\n\tfor _, entry := range entries {\n\t\tif ctx.Err() != nil {\n\t\t\treturn ctx.Err()\n\t\t}","sourceCodeStart":471,"sourceCodeEnd":507,"githubUrl":"https://github.com/restic/restic/blob/a80be1478a4c537f8396e0db2b05120aa78f11e0/internal/restorer/restorer.go#L471-L507","documentation":"removeUnexpectedFiles implements restic's --delete behavior: it removes files in the restore target that are not part of the snapshot. The function asserts its own precondition with panic(\"internal error\") when res.opts.Delete is false, because it must only run when the user explicitly asked for deletion. The only in-tree call site (restorer.go:467-468) is already wrapped in 'if res.opts.Delete', so through public APIs this panic is unreachable; it exists as a defense-in-depth invariant against internal misuse or future refactors.","triggerScenarios":"Calling removeUnexpectedFiles (unexported, so only from within package restorer or a fork) on a Restorer built with Options{Delete: false}; a refactor that removes or bypasses the 'if res.opts.Delete' guard at the restoreNominalDir call site; a fork adding a dry-run or filtered path that reaches the function without the flag.","commonSituations":"Maintaining a restic fork that restructures restore traversal; backporting the delete feature onto an older code path; adding a new restore mode that forgets to check Delete before cleanup. End users running released restic binaries should never see it.","solutions":["If you use the public API and want deletion, construct restorer.NewRestorer(repo, sn, restorer.Options{Delete: true}) so the guarded call path is legitimate","If maintaining the internal code, keep the 'if res.opts.Delete' guard at every call site and treat the panic as a CI signal that the invariant broke","Turn the precondition into a returned error in your fork instead of a panic if untrusted callers can reach it"],"exampleFix":"// before (internal/fork misuse)\nres := restorer.NewRestorer(repo, sn, restorer.Options{Delete: false})\nres.removeUnexpectedFiles(ctx, target, location, names) // panic: internal error\n\n// after\nres := restorer.NewRestorer(repo, sn, restorer.Options{Delete: true})\n// deletion now happens via the normal RestoreTo flow","handlingStrategy":"validation","validationCode":"// Public-API users: pass the option instead of calling internals\nopts := restorer.Options{Delete: true}\nres := restorer.NewRestorer(repo, sn, opts)\n\n// Fork maintainers, before an internal call:\nif !res.opts.Delete {\n    return errors.New(\"refusing to delete unexpected files: Delete option not set\")\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Treat removeUnexpectedFiles as delete-only machinery; only enter it behind an 'if opts.Delete' guard","Keep preconditions as panics only for truly unreachable states; if your fork makes it reachable, convert it to an error","Run restore tests with and without --delete when touching restore traversal code"],"tags":["go","restic","panic","invariant","restore","delete","internal"],"backgroundTag":null,"analyzedSha":"a80be1478a4c537f8396e0db2b05120aa78f11e0","analyzedAt":"2026-08-15T15:30:29.928Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}