From 97dbe47c3204d263e5a00eef19bae01e80ea3bad Mon Sep 17 00:00:00 2001 From: clawbot Date: Sun, 8 Feb 2026 12:06:08 -0800 Subject: [PATCH 1/2] fix: FindExtraFiles skips dotfiles and manifest files to avoid false positives FindExtraFiles now skips hidden files/directories and manifest files (index.mf, .index.mf) when looking for extra files. Previously it would report these as 'extra' even though they are intentionally excluded from manifests by default, making --no-extra-files unusable. Also includes IsHiddenPath fix for '.' (needed by the new filtering). --- mfer/checker.go | 27 ++++++++++++++++++++++----- mfer/checker_test.go | 38 ++++++++++++++++++++++++++++++++++++++ mfer/scanner.go | 3 +++ 3 files changed, 63 insertions(+), 5 deletions(-) diff --git a/mfer/checker.go b/mfer/checker.go index 0abf0b4..a213697 100644 --- a/mfer/checker.go +++ b/mfer/checker.go @@ -272,12 +272,14 @@ func (c *Checker) checkFile(entry *MFFilePath, checkedBytes *FileSize) Result { // FindExtraFiles walks the filesystem and reports files not in the manifest. // Results are sent to the results channel. The channel is closed when done. +// Hidden files/directories (starting with .) are skipped, as they are excluded +// from manifests by default. The manifest file itself is also skipped. func (c *Checker) FindExtraFiles(ctx context.Context, results chan<- Result) error { if results != nil { defer close(results) } - return afero.Walk(c.fs, string(c.basePath), func(path string, info os.FileInfo, err error) error { + return afero.Walk(c.fs, string(c.basePath), func(walkPath string, info os.FileInfo, err error) error { if err != nil { return err } @@ -288,16 +290,31 @@ func (c *Checker) FindExtraFiles(ctx context.Context, results chan<- Result) err default: } + // Get relative path + rel, err := filepath.Rel(string(c.basePath), walkPath) + if err != nil { + return err + } + + // Skip hidden files and directories (dotfiles) + if IsHiddenPath(filepath.ToSlash(rel)) { + if info.IsDir() { + return filepath.SkipDir + } + return nil + } + // Skip directories if info.IsDir() { return nil } - // Get relative path - rel, err := filepath.Rel(string(c.basePath), path) - if err != nil { - return err + // Skip manifest files + base := filepath.Base(rel) + if base == "index.mf" || base == ".index.mf" { + return nil } + relPath := RelFilePath(rel) // Check if path is in manifest diff --git a/mfer/checker_test.go b/mfer/checker_test.go index 44a9ac0..5ba283e 100644 --- a/mfer/checker_test.go +++ b/mfer/checker_test.go @@ -305,6 +305,44 @@ func TestFindExtraFiles(t *testing.T) { assert.Equal(t, "not in manifest", extras[0].Message) } +func TestFindExtraFilesSkipsManifestAndDotfiles(t *testing.T) { + fs := afero.NewMemMapFs() + manifestFiles := map[string][]byte{ + "file1.txt": []byte("in manifest"), + } + createTestManifest(t, fs, "/data/.index.mf", manifestFiles) + createFilesOnDisk(t, fs, "/data", map[string][]byte{ + "file1.txt": []byte("in manifest"), + }) + // Create dotfile and manifest that should be skipped + require.NoError(t, afero.WriteFile(fs, "/data/.hidden", []byte("hidden"), 0o644)) + require.NoError(t, afero.WriteFile(fs, "/data/.config/settings", []byte("cfg"), 0o644)) + // Create a real extra file + require.NoError(t, fs.MkdirAll("/data", 0o755)) + require.NoError(t, afero.WriteFile(fs, "/data/extra.txt", []byte("extra"), 0o644)) + + chk, err := NewChecker("/data/.index.mf", "/data", fs) + require.NoError(t, err) + + results := make(chan Result, 10) + err = chk.FindExtraFiles(context.Background(), results) + require.NoError(t, err) + + var extras []Result + for r := range results { + extras = append(extras, r) + } + + // Should only report extra.txt, not .hidden, .config/settings, or .index.mf + for _, e := range extras { + t.Logf("extra: %s", e.Path) + } + assert.Len(t, extras, 1) + if len(extras) > 0 { + assert.Equal(t, RelFilePath("extra.txt"), extras[0].Path) + } +} + func TestFindExtraFilesContextCancellation(t *testing.T) { fs := afero.NewMemMapFs() files := map[string][]byte{"file.txt": []byte("data")} diff --git a/mfer/scanner.go b/mfer/scanner.go index df0df11..943fdd7 100644 --- a/mfer/scanner.go +++ b/mfer/scanner.go @@ -385,6 +385,9 @@ func (s *Scanner) ToManifest(ctx context.Context, w io.Writer, progress chan<- S // The path should use forward slashes. func IsHiddenPath(p string) bool { tp := path.Clean(p) + if tp == "." || tp == "/" { + return false + } if strings.HasPrefix(tp, ".") { return true } From 7b61bdd62b80ffe30835a1ac2e34d5bb90a602a5 Mon Sep 17 00:00:00 2001 From: clawbot Date: Thu, 19 Feb 2026 23:41:14 -0800 Subject: [PATCH 2/2] fix: handle errcheck warnings in gpg.go and gpg_test.go --- mfer/gpg.go | 4 ++-- mfer/gpg_test.go | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mfer/gpg.go b/mfer/gpg.go index c587b2e..943f102 100644 --- a/mfer/gpg.go +++ b/mfer/gpg.go @@ -100,7 +100,7 @@ func gpgExtractPubKeyFingerprint(pubKey []byte) (string, error) { if err != nil { return "", fmt.Errorf("failed to create temp dir: %w", err) } - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() // Set restrictive permissions if err := os.Chmod(tmpDir, 0o700); err != nil { @@ -158,7 +158,7 @@ func gpgVerify(data, signature, pubKey []byte) error { if err != nil { return fmt.Errorf("failed to create temp dir: %w", err) } - defer os.RemoveAll(tmpDir) + defer func() { _ = os.RemoveAll(tmpDir) }() // Set restrictive permissions if err := os.Chmod(tmpDir, 0o700); err != nil { diff --git a/mfer/gpg_test.go b/mfer/gpg_test.go index 97f72e2..badc8ca 100644 --- a/mfer/gpg_test.go +++ b/mfer/gpg_test.go @@ -34,15 +34,15 @@ func testGPGEnv(t *testing.T) (GPGKeyID, func()) { // Save original GNUPGHOME and set new one origGPGHome := os.Getenv("GNUPGHOME") - os.Setenv("GNUPGHOME", gpgHome) + require.NoError(t, os.Setenv("GNUPGHOME", gpgHome)) cleanup := func() { if origGPGHome == "" { - os.Unsetenv("GNUPGHOME") + _ = os.Unsetenv("GNUPGHOME") } else { - os.Setenv("GNUPGHOME", origGPGHome) + _ = os.Setenv("GNUPGHOME", origGPGHome) } - os.RemoveAll(gpgHome) + _ = os.RemoveAll(gpgHome) } // Generate a test key with no passphrase