From 7c91f43d76b17445ee7c93a4cdbfba41e2f1552b Mon Sep 17 00:00:00 2001 From: user Date: Tue, 10 Feb 2026 18:34:51 -0800 Subject: [PATCH] Fix FindExtraFiles reporting manifest file and dotfiles as extra (closes #16) FindExtraFiles now skips hidden files/directories (dotfiles) and the manifest file itself when walking the filesystem. The manifest's relative path is computed at Checker construction time. --- mfer/checker.go | 51 ++++++++++++++++++++++++++++++---------- mfer/checker_test.go | 55 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 12 deletions(-) diff --git a/mfer/checker.go b/mfer/checker.go index 0abf0b4..875dd78 100644 --- a/mfer/checker.go +++ b/mfer/checker.go @@ -70,6 +70,8 @@ type Checker struct { fs afero.Fs // manifestPaths is a set of paths in the manifest for quick lookup manifestPaths map[RelFilePath]struct{} + // manifestRelPath is the relative path of the manifest file from basePath (for exclusion) + manifestRelPath RelFilePath // signature info from the manifest signature []byte signer []byte @@ -100,14 +102,25 @@ func NewChecker(manifestPath string, basePath string, fs afero.Fs) (*Checker, er manifestPaths[RelFilePath(f.Path)] = struct{}{} } + // Compute manifest's relative path from basePath for exclusion in FindExtraFiles + absManifest, err := filepath.Abs(manifestPath) + if err != nil { + return nil, err + } + manifestRel, err := filepath.Rel(abs, absManifest) + if err != nil { + manifestRel = "" + } + return &Checker{ - basePath: AbsFilePath(abs), - files: files, - fs: fs, - manifestPaths: manifestPaths, - signature: m.pbOuter.Signature, - signer: m.pbOuter.Signer, - signingPubKey: m.pbOuter.SigningPubKey, + basePath: AbsFilePath(abs), + files: files, + fs: fs, + manifestPaths: manifestPaths, + manifestRelPath: RelFilePath(manifestRel), + signature: m.pbOuter.Signature, + signer: m.pbOuter.Signer, + signingPubKey: m.pbOuter.SigningPubKey, }, nil } @@ -288,18 +301,32 @@ func (c *Checker) FindExtraFiles(ctx context.Context, results chan<- Result) err default: } - // Skip directories - if info.IsDir() { - return nil - } - // Get relative path rel, err := filepath.Rel(string(c.basePath), path) 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 + } + relPath := RelFilePath(rel) + // Skip the manifest file itself + if relPath == c.manifestRelPath { + return nil + } + // Check if path is in manifest if _, exists := c.manifestPaths[relPath]; !exists { if results != nil { diff --git a/mfer/checker_test.go b/mfer/checker_test.go index 44a9ac0..a8633dc 100644 --- a/mfer/checker_test.go +++ b/mfer/checker_test.go @@ -414,6 +414,61 @@ func TestCheckMissingFileDetectedWithoutFallback(t *testing.T) { assert.Equal(t, 0, statusCounts[StatusError], "no files should be ERROR") } +func TestFindExtraFilesSkipsDotfiles(t *testing.T) { + // Regression test for #16: FindExtraFiles should not report dotfiles + // or the manifest file itself as extra files. + fs := afero.NewMemMapFs() + files := map[string][]byte{ + "file1.txt": []byte("in manifest"), + } + createTestManifest(t, fs, "/data/.index.mf", files) + createFilesOnDisk(t, fs, "/data", files) + + // Add dotfiles and manifest file on disk + require.NoError(t, afero.WriteFile(fs, "/data/.hidden", []byte("dotfile"), 0o644)) + require.NoError(t, fs.MkdirAll("/data/.git", 0o755)) + require.NoError(t, afero.WriteFile(fs, "/data/.git/config", []byte("git config"), 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 report NO extra files — dotfiles and manifest should be skipped + assert.Empty(t, extras, "FindExtraFiles should not report dotfiles or manifest file as extra; got: %v", extras) +} + +func TestFindExtraFilesSkipsManifestFile(t *testing.T) { + // The manifest file itself should never be reported as extra + fs := afero.NewMemMapFs() + files := map[string][]byte{ + "file1.txt": []byte("content"), + } + createTestManifest(t, fs, "/data/index.mf", files) + createFilesOnDisk(t, fs, "/data", files) + + 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) + } + + assert.Empty(t, extras, "manifest file should not be reported as extra; got: %v", extras) +} + func TestCheckEmptyManifest(t *testing.T) { fs := afero.NewMemMapFs() // Create manifest with no files