diff --git a/mfer/checker.go b/mfer/checker.go index 2e47d3a..1d26a35 100644 --- a/mfer/checker.go +++ b/mfer/checker.go @@ -277,12 +277,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 } @@ -293,16 +295,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 2313bb8..042c0d6 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 }