diff --git a/.golangci.yml b/.golangci.yml index 34a8e31..26b1610 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,5 +1,9 @@ version: "2" +# Config schema uses the golangci-lint v2 layout (settings live under +# linters.settings, not top-level linters-settings) so that the +# thresholds below are actually applied by golangci-lint >= v2. + run: timeout: 5m modules-download-mode: readonly @@ -14,19 +18,17 @@ linters: - wsl # Deprecated, replaced by wsl_v5 - wrapcheck # Too verbose for internal packages - varnamelen # Short names like db, id are idiomatic Go - -linters-settings: - lll: - line-length: 88 - funlen: - lines: 80 - statements: 50 - cyclop: - max-complexity: 15 - dupl: - threshold: 100 + settings: + lll: + line-length: 88 + funlen: + lines: 80 + statements: 50 + cyclop: + max-complexity: 15 + dupl: + threshold: 100 issues: - exclude-use-default: false max-issues-per-linter: 0 max-same-issues: 0 diff --git a/Dockerfile b/Dockerfile index d1530bf..2aa9b32 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,8 +4,8 @@ FROM golang@sha256:f6751d823c26342f9506c03797d2527668d095b0a15f1862cddb4d927a7a4 RUN apk add --no-cache git make gcc musl-dev binutils-gold -# golangci-lint v2.10.1 -RUN go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@5d1e709b7be35cb2025444e19de266b056b7b7ee +# golangci-lint v2.12.2, 2026-08-07 +RUN go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 # goimports v0.42.0 RUN go install golang.org/x/tools/cmd/goimports@009367f5c17a8d4c45a961a3a509277190a9a6f0 diff --git a/TODO.md b/TODO.md index 3a9a45e..0aaae82 100644 --- a/TODO.md +++ b/TODO.md @@ -26,6 +26,11 @@ $(HOME)/Documents/_SYSADMIN/cyberdyne path. # Completed Steps +* 2026-08-07: updated golangci-lint to v2.12.2: canonical + `.golangci.yml` (settings moved under `linters.settings` so the + configured thresholds actually apply), version pins bumped in + `Dockerfile` and `script/bootstrap`, and long lines wrapped to + satisfy the now-effective `lll` limit of 88 * 2026-02-02: correctness pass: track actual bytes read instead of stale file size, atomic failure tracking in ProcessCheck, detect file modification during checksum (TOCTOU), propagate countFiles diff --git a/attrsum.go b/attrsum.go index e9b0e1c..7c37162 100644 --- a/attrsum.go +++ b/attrsum.go @@ -72,7 +72,8 @@ func (s *Stats) Print(opts *options, operation string) { return } - fmt.Fprintf(os.Stderr, "\n%s complete: %d files processed, %d skipped, %d failed, %s bytes in %s\n", + fmt.Fprintf(os.Stderr, + "\n%s complete: %d files processed, %d skipped, %d failed, %s bytes in %s\n", operation, s.FilesProcessed, s.FilesSkipped, @@ -166,11 +167,15 @@ func expandPaths(args []string) ([]string, error) { } // processFunc processes a single path within a command's run loop. -type processFunc func(opts *options, path string, stats *Stats, bar *progressbar.ProgressBar) error +type processFunc func( + opts *options, path string, stats *Stats, bar *progressbar.ProgressBar, +) error // countAndBar counts the files under paths and returns a progress bar sized // to that total. It always returns either a non-nil bar or a non-nil error. -func countAndBar(opts *options, paths []string, desc string) (*progressbar.ProgressBar, error) { +func countAndBar( + opts *options, paths []string, desc string, +) (*progressbar.ProgressBar, error) { total, err := countFilesMultiple(opts, paths) if err != nil { return nil, err @@ -188,7 +193,9 @@ func finishBar(bar *progressbar.ProgressBar) { // runOverPaths runs process over each path, sharing the progress/stats // bookkeeping common to the sum-add, sum-update and clear commands. -func runOverPaths(opts *options, args []string, desc, op string, process processFunc) error { +func runOverPaths( + opts *options, args []string, desc, op string, process processFunc, +) error { paths, err := expandPaths(args) if err != nil { return err @@ -253,46 +260,54 @@ func newSumCmd(opts *options) *cobra.Command { return cmd } -func processSumAdd(opts *options, dir string, stats *Stats, bar *progressbar.ProgressBar) error { - return walkAndProcess(opts, dir, stats, bar, func(p string, info os.FileInfo, s *Stats) error { - if hasXattr(p, checksumKey) { - atomic.AddInt64(&s.FilesSkipped, 1) +func processSumAdd( + opts *options, dir string, stats *Stats, bar *progressbar.ProgressBar, +) error { + return walkAndProcess(opts, dir, stats, bar, + func(p string, info os.FileInfo, s *Stats) error { + if hasXattr(p, checksumKey) { + atomic.AddInt64(&s.FilesSkipped, 1) - return nil - } + return nil + } - err := writeChecksumAndTime(opts, p, info, s) - if err != nil { - atomic.AddInt64(&s.FilesFailed, 1) - - return err - } - - return nil - }) -} - -func processSumUpdate(opts *options, dir string, stats *Stats, bar *progressbar.ProgressBar) error { - return walkAndProcess(opts, dir, stats, bar, func(p string, info os.FileInfo, s *Stats) error { - t, err := readSumTime(p) - if err != nil || info.ModTime().After(t) { - werr := writeChecksumAndTime(opts, p, info, s) - if werr != nil { + err := writeChecksumAndTime(opts, p, info, s) + if err != nil { atomic.AddInt64(&s.FilesFailed, 1) - return werr + return err } return nil - } - - atomic.AddInt64(&s.FilesSkipped, 1) - - return nil - }) + }) } -func writeChecksumAndTime(opts *options, path string, info os.FileInfo, stats *Stats) error { +func processSumUpdate( + opts *options, dir string, stats *Stats, bar *progressbar.ProgressBar, +) error { + return walkAndProcess(opts, dir, stats, bar, + func(p string, info os.FileInfo, s *Stats) error { + t, err := readSumTime(p) + if err != nil || info.ModTime().After(t) { + werr := writeChecksumAndTime(opts, p, info, s) + if werr != nil { + atomic.AddInt64(&s.FilesFailed, 1) + + return werr + } + + return nil + } + + atomic.AddInt64(&s.FilesSkipped, 1) + + return nil + }) +} + +func writeChecksumAndTime( + opts *options, path string, info os.FileInfo, stats *Stats, +) error { // Record mtime before hashing to detect modifications during hash. mtimeBefore := info.ModTime() @@ -363,24 +378,27 @@ func newClearCmd(opts *options) *cobra.Command { } } -func processClear(opts *options, dir string, stats *Stats, bar *progressbar.ProgressBar) error { - return walkAndProcess(opts, dir, stats, bar, func(p string, info os.FileInfo, s *Stats) error { - cleared, err := clearOne(opts, p) - if err != nil { - atomic.AddInt64(&s.FilesFailed, 1) +func processClear( + opts *options, dir string, stats *Stats, bar *progressbar.ProgressBar, +) error { + return walkAndProcess(opts, dir, stats, bar, + func(p string, info os.FileInfo, s *Stats) error { + cleared, err := clearOne(opts, p) + if err != nil { + atomic.AddInt64(&s.FilesFailed, 1) - return err - } + return err + } - if cleared { - atomic.AddInt64(&s.FilesProcessed, 1) - atomic.AddInt64(&s.BytesProcessed, info.Size()) - } else { - atomic.AddInt64(&s.FilesSkipped, 1) - } + if cleared { + atomic.AddInt64(&s.FilesProcessed, 1) + atomic.AddInt64(&s.BytesProcessed, info.Size()) + } else { + atomic.AddInt64(&s.FilesSkipped, 1) + } - return nil - }) + return nil + }) } // clearOne removes both checksum xattrs from a single path, reporting @@ -428,7 +446,8 @@ func newCheckCmd(opts *options) *cobra.Command { return runCheck(opts, a, cont) }, } - cmd.Flags().BoolVar(&cont, "continue", false, "continue after errors and report each file") + cmd.Flags().BoolVar(&cont, "continue", false, + "continue after errors and report each file") return cmd } @@ -472,13 +491,16 @@ func runCheck(opts *options, args []string, cont bool) error { return finalErr } -func processCheck(opts *options, dir string, cont bool, stats *Stats, bar *progressbar.ProgressBar) error { +func processCheck( + opts *options, dir string, cont bool, stats *Stats, bar *progressbar.ProgressBar, +) error { // Track initial failed count to detect failures during this walk. initialFailed := atomic.LoadInt64(&stats.FilesFailed) - err := walkAndProcess(opts, dir, stats, bar, func(p string, _ os.FileInfo, s *Stats) error { - return checkOne(opts, p, cont, s) - }) + err := walkAndProcess(opts, dir, stats, bar, + func(p string, _ os.FileInfo, s *Stats) error { + return checkOne(opts, p, cont, s) + }) if err != nil { if errors.Is(err, errVerification) { return errVerification @@ -742,7 +764,8 @@ func hasXattr(path, key string) bool { func fileMultihash(path string) ([]byte, int64, error) { // The path is supplied by the operator as the tree to checksum; reading // it is the entire purpose of the tool. - f, err := os.Open(path) //nolint:gosec // G304: operator-specified path is the intended input + //nolint:gosec // G304: operator-specified path is the intended input + f, err := os.Open(path) if err != nil { return nil, 0, err } diff --git a/script/bootstrap b/script/bootstrap index ffcb29a..ed9bb61 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -4,14 +4,14 @@ # installed tools are skipped. Base tooling comes from nix, apt, brew, # or apk (detected in that order); assumes nothing is present. # golangci-lint and goimports are installed via `go install` at the same -# pinned commits the Dockerfile uses (never "latest"). +# pinned refs the Dockerfile uses (never "latest"). set -eu ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" -# Pinned versions, 2026-07-07 (same pins as the Dockerfile) -# golangci-lint v2.10.1 -GOLANGCI_LINT_REF="github.com/golangci/golangci-lint/v2/cmd/golangci-lint@5d1e709b7be35cb2025444e19de266b056b7b7ee" +# Pinned versions, 2026-08-07 (same pins as the Dockerfile) +# golangci-lint v2.12.2 +GOLANGCI_LINT_REF="github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2" # goimports v0.42.0 GOIMPORTS_REF="golang.org/x/tools/cmd/goimports@009367f5c17a8d4c45a961a3a509277190a9a6f0"