//go:build unix //nolint:testpackage // white-box tests exercise unexported internals package mfer import ( "bytes" "context" "os" "path/filepath" "syscall" "testing" "time" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // pipeDeadline is how long a test lets a run go before it opens the test's // named pipes for writing. const pipeDeadline = 5 * time.Second // makePipe makes a named pipe at path. Opening a named pipe for reading // blocks until it is opened for writing, so once pipeDeadline has passed, // unless the test has ended, makePipe opens it for writing and closes it // again. A run that wrongly reads the pipe then ends, and the test fails on // what it reports instead of hanging. That lets one read through, so each // path a wrong run would read gets a pipe of its own. func makePipe(t *testing.T, path string) { t.Helper() require.NoError(t, syscall.Mkfifo(path, 0o600)) timer := time.AfterFunc(pipeDeadline, func() { f, err := os.OpenFile(path, os.O_WRONLY, 0) //nolint:gosec // test-controlled path if err == nil { _ = f.Close() } }) t.Cleanup(func() { timer.Stop() }) } // TestScannerSkipsPipes scans a tree holding a named pipe and a symlink to // another one, following symlinks: the manifest lists the regular file only. func TestScannerSkipsPipes(t *testing.T) { t.Parallel() dir := t.TempDir() require.NoError(t, os.WriteFile(filepath.Join(dir, testFile1), []byte("a"), 0o600)) makePipe(t, filepath.Join(dir, "pipe")) // The symlink points outside the tree, so no pipe is read twice. target := filepath.Join(t.TempDir(), "pipe") makePipe(t, target) require.NoError(t, os.Symlink(target, filepath.Join(dir, "link"))) s := NewScannerWithOptions(&ScannerOptions{FollowSymLinks: true}) require.NoError(t, s.EnumeratePath(dir, nil)) var buf bytes.Buffer require.NoError(t, s.ToManifest(context.Background(), &buf, nil)) m, err := NewManifestFromReader(&buf) require.NoError(t, err) require.Len(t, m.Files(), 1) assert.Equal(t, testFile1, m.Files()[0].GetPath()) } // TestCheckPipes checks a tree against a manifest that lists, as an empty // file, a path that is a named pipe on disk. The tree holds another pipe the // manifest does not list. check reports the listed pipe as an error without // reading it, and the search for extra files does not report the other. func TestCheckPipes(t *testing.T) { t.Parallel() dir := t.TempDir() manifestPath := filepath.Join(dir, "index.mf") content := []byte("in manifest") listedPipe := RelFilePath("listed-pipe") fs := afero.NewOsFs() createTestManifest(t, fs, manifestPath, map[string][]byte{ testFile1: content, string(listedPipe): {}, }) require.NoError(t, afero.WriteFile(fs, filepath.Join(dir, testFile1), content, 0o600)) makePipe(t, filepath.Join(dir, string(listedPipe))) makePipe(t, filepath.Join(dir, "unlisted-pipe")) chk, err := NewChecker(&CheckerOptions{ ManifestPath: manifestPath, BasePath: dir, Fs: fs, }) require.NoError(t, err) results := make(chan Result, 10) require.NoError(t, chk.Check(context.Background(), results, nil)) byPath := map[RelFilePath]Result{} for r := range results { byPath[r.Path] = r } assert.Equal(t, StatusOK, byPath[testFile1].Status) assert.Equal(t, Result{ Path: listedPipe, Status: StatusError, Message: "not a regular file", }, byPath[listedPipe]) extras := make(chan Result, 10) require.NoError(t, chk.FindExtraFiles(context.Background(), extras)) var extraPaths []RelFilePath for r := range extras { extraPaths = append(extraPaths, r.Path) } assert.Empty(t, extraPaths) }