//go:build unix package hostfiles import ( "context" "io" "net" "os" "path/filepath" "reflect" "syscall" "testing" "time" "github.com/carlosframework/platform/internal/release" ) // within fails the test if fn has not returned in two seconds: the way a // blocked open shows up. func within(t *testing.T, what string, fn func()) { t.Helper() done := make(chan struct{}) go func() { defer close(done); fn() }() select { case <-done: case <-time.After(2 * time.Second): t.Fatalf("%s did not return: something blocked", what) } } // swapAt arranges for fn to run once, at the nth pause between a look and // an open (1 is the first). func swapAt(t *testing.T, n int, fn func()) { t.Helper() orig, calls := betweenLookAndOpen, 0 t.Cleanup(func() { betweenLookAndOpen = orig }) betweenLookAndOpen = func() { calls++ if calls == n { fn() } } } func tree(t *testing.T, dir string) *dirHandle { t.Helper() d, err := openTree(dir) if err != nil { t.Fatal(err) } t.Cleanup(func() { d.Close() }) return d } func fifo(t *testing.T, path string) { t.Helper() if err := syscall.Mkfifo(path, 0o600); err != nil { t.Skipf("no FIFOs here: %v", err) } } // Review Focus 2: special files are skipped, never opened for reading. func TestSyncDirSkipsAFIFO(t *testing.T) { ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir() write(t, src, "real.txt", "r") fifo(t, filepath.Join(src, "pipe")) p := open(t, s) var st SyncStats within(t, "SyncDir", func() { st, _ = SyncDir(ctx, p, src, nil) }) if st.Published != 1 || !reflect.DeepEqual(st.NotRegular, []string{"pipe"}) { t.Fatalf("stats = %+v", st) } } func TestSyncDirSkipsASocket(t *testing.T) { ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir() l, err := net.Listen("unix", filepath.Join(src, "sock")) if err != nil { t.Skipf("no Unix sockets here: %v", err) } defer l.Close() p := open(t, s) var st SyncStats within(t, "SyncDir", func() { st, _ = SyncDir(ctx, p, src, nil) }) if !reflect.DeepEqual(st.NotRegular, []string{"sock"}) || len(p.Paths()) != 0 { t.Fatalf("stats = %+v, paths = %v", st, p.Paths()) } } func TestSyncDirSkipsADevice(t *testing.T) { if os.Getuid() != 0 { t.Skip("making a device node needs root") } ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir() if err := syscall.Mknod(filepath.Join(src, "null"), syscall.S_IFCHR|0o600, 1<<8|3); err != nil { t.Skipf("mknod: %v", err) } p := open(t, s) st, err := SyncDir(ctx, p, src, nil) if err != nil || !reflect.DeepEqual(st.NotRegular, []string{"null"}) { t.Fatalf("stats = %+v, err = %v", st, err) } } // A symlinked directory is never descended into, wherever it points. func TestSyncDirDoesNotDescendASymlinkedDirectory(t *testing.T) { ctx, s, src, outside := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir(), t.TempDir() if err := os.WriteFile(filepath.Join(outside, "secret"), []byte("s"), 0o600); err != nil { t.Fatal(err) } write(t, src, "inside/own", "o") for link, target := range map[string]string{"out": outside, "in": "inside"} { if err := os.Symlink(target, filepath.Join(src, link)); err != nil { t.Fatal(err) } } p := open(t, s) st, err := SyncDir(ctx, p, src, nil) if err != nil { t.Fatal(err) } if got := p.Paths(); !reflect.DeepEqual(got, []string{"inside/own"}) { t.Fatalf("Paths = %v", got) } if !reflect.DeepEqual(st.NotRegular, []string{"in", "out"}) { t.Fatalf("NotRegular = %v", st.NotRegular) } } // A regular file swapped for a FIFO after it was inspected must neither // block the open nor be read. func TestReadStableDoesNotBlockOnAFIFOSwappedIn(t *testing.T) { dir := t.TempDir() write(t, dir, "f", "regular") d := tree(t, dir) swapAt(t, 1, func() { full := filepath.Join(dir, "f") if err := os.Remove(full); err != nil { t.Error(err) } if err := syscall.Mkfifo(full, 0o600); err != nil { t.Error(err) } }) var body []byte var state fileState within(t, "readStable", func() { body, _, state, _ = readStable(d, "f") }) if state != fileNotRegular || body != nil { t.Fatalf("state = %v, %d bytes; want fileNotRegular and nothing read", state, len(body)) } } // A directory swapped for a FIFO between being listed and being entered // must not hang the scan. func TestSyncDirDoesNotBlockOnADirectorySwappedForAFIFO(t *testing.T) { ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir() write(t, src, "d/child", "c") write(t, src, "z.txt", "z") swapAt(t, 1, func() { // the first pause is before entering "d" if err := os.RemoveAll(filepath.Join(src, "d")); err != nil { t.Error(err) } if err := syscall.Mkfifo(filepath.Join(src, "d"), 0o600); err != nil { t.Error(err) } }) p := open(t, s) var st SyncStats var err error within(t, "SyncDir", func() { st, err = SyncDir(ctx, p, src, nil) }) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(st.NotRegular, []string{"d"}) || !reflect.DeepEqual(p.Paths(), []string{"z.txt"}) { t.Fatalf("stats = %+v, paths = %v", st, p.Paths()) } } // Plan round 2, finding 2: a file that still exists is never tombstoned // just because it could not be read. func TestSyncDirKeepsAnUnreadableFilesPublishedEntry(t *testing.T) { if os.Getuid() == 0 { t.Skip("root can read anything") } ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir() write(t, src, "locked", "v1") p := open(t, s) if _, err := SyncDir(ctx, p, src, nil); err != nil { t.Fatal(err) } commit(t, p) if err := os.Chmod(filepath.Join(src, "locked"), 0); err != nil { t.Fatal(err) } st, err := SyncDir(ctx, p, src, nil) if err != nil { t.Fatal(err) } if st.Deleted != 0 || p.Dirty() || !reflect.DeepEqual(st.Unreadable, []string{"locked"}) { t.Fatalf("stats = %+v dirty = %v", st, p.Dirty()) } } // A regular file replaced by another regular file between the look and // the open is neither read nor declared gone: the caller asks again. func TestReadStableAsksAgainWhenARegularFileIsReplacedMidLook(t *testing.T) { dir := t.TempDir() write(t, dir, "f", "v1") d := tree(t, dir) swapAt(t, 1, func() { tmp := filepath.Join(dir, "f.new") if err := os.WriteFile(tmp, []byte("v2"), 0o640); err != nil { t.Error(err) } if err := os.Rename(tmp, filepath.Join(dir, "f")); err != nil { t.Error(err) } }) body, _, state, err := readStable(d, "f") if err != nil || state != fileRetry || body != nil { t.Fatalf("state = %v, body = %q, err = %v; want fileRetry and nothing read", state, body, err) } body, _, state, err = readStable(d, "f") if err != nil || state != fileOK || string(body) != "v2" { t.Fatalf("second look: state = %v, body = %q, err = %v", state, body, err) } } // And through a scan: the entry the manifest already has is kept. func TestSyncDirKeepsAnEntryWhoseFileWasReplacedMidLook(t *testing.T) { ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir() write(t, src, "f", "v1") p := open(t, s) if _, err := SyncDir(ctx, p, src, nil); err != nil { t.Fatal(err) } commit(t, p) swapAt(t, 1, func() { tmp := filepath.Join(src, "f.new") if err := os.WriteFile(tmp, []byte("v2"), 0o640); err != nil { t.Error(err) } if err := os.Rename(tmp, filepath.Join(src, "f")); err != nil { t.Error(err) } }) st, err := SyncDir(ctx, p, src, nil) if err != nil || st.Deleted != 0 || p.Dirty() { t.Fatalf("stats = %+v dirty = %v err = %v; the replaced file must be left for the next scan", st, p.Dirty(), err) } if _, err := SyncDir(ctx, p, src, nil); err != nil || !p.Dirty() { t.Fatalf("the next scan did not pick the replacement up: dirty = %v err = %v", p.Dirty(), err) } } // Publish is an installation's record of its directory: a file it could // not read would be missing from it, so it commits nothing and says so. func TestPublishRefusesADirectoryWithAnUnreadableFile(t *testing.T) { if os.Getuid() == 0 { t.Skip("root can read anything") } ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir() write(t, src, "ok", "v1") write(t, src, "locked", "v1") if err := os.Chmod(filepath.Join(src, "locked"), 0); err != nil { t.Fatal(err) } if _, st, err := Publish(ctx, s, pfx, src, nil); err == nil || !reflect.DeepEqual(st.Unreadable, []string{"locked"}) { t.Fatalf("err = %v, stats = %+v", err, st) } if _, found, err := Load(ctx, s, pfx); found || err != nil { t.Fatalf("a manifest was committed: found = %v, err = %v", found, err) } } // replaceAt swaps src/f for new content at the n-th look-to-open window. func replaceAt(t *testing.T, n int, src string) { t.Helper() swapAt(t, n, func() { tmp := filepath.Join(src, "f.new") if err := os.WriteFile(tmp, []byte("v2"), 0o640); err != nil { t.Error(err) } if err := os.Rename(tmp, filepath.Join(src, "f")); err != nil { t.Error(err) } }) } // A file replaced mid-look is reported, not silently left out. func TestSyncDirReportsAFileReplacedMidLook(t *testing.T) { ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir() write(t, src, "f", "v1") replaceAt(t, 1, src) st, err := SyncDir(ctx, open(t, s), src, nil) if err != nil || !reflect.DeepEqual(st.Retry, []string{"f"}) { t.Fatalf("stats = %+v err = %v, want Retry [f]", st, err) } } // A first install whose only file was replaced mid-look would commit a // manifest without it. func TestPublishRefusesADirectoryWithAFileReplacedMidLook(t *testing.T) { ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir() write(t, src, "ok", "v1") write(t, src, "f", "v1") replaceAt(t, 1, src) // "f" sorts before "ok" if _, st, err := Publish(ctx, s, pfx, src, nil); err == nil || !reflect.DeepEqual(st.Retry, []string{"f"}) { t.Fatalf("err = %v, stats = %+v", err, st) } if _, found, err := Load(ctx, s, pfx); found || err != nil { t.Fatalf("a manifest was committed: found = %v, err = %v", found, err) } } // tearAt makes the read of src/f see the file grow while it is read. func tearAt(t *testing.T, src string) { t.Helper() orig := readAll t.Cleanup(func() { readAll = orig }) readAll = func(r io.Reader) ([]byte, error) { b, err := orig(r) f, oerr := os.OpenFile(filepath.Join(src, "f"), os.O_APPEND|os.O_WRONLY, 0) if oerr != nil { t.Error(oerr) return b, err } f.Write([]byte("more")) f.Close() return b, err } } func TestSyncDirReportsATornRead(t *testing.T) { ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir() write(t, src, "f", "v1") tearAt(t, src) st, err := SyncDir(ctx, open(t, s), src, nil) if err != nil || !reflect.DeepEqual(st.Torn, []string{"f"}) { t.Fatalf("stats = %+v err = %v, want Torn [f]", st, err) } } // Publish is an install-time record: a torn image is not committed. func TestPublishRefusesATornRead(t *testing.T) { ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir() write(t, src, "f", "v1") tearAt(t, src) if _, st, err := Publish(ctx, s, pfx, src, nil); err == nil || !reflect.DeepEqual(st.Torn, []string{"f"}) { t.Fatalf("err = %v, stats = %+v", err, st) } if _, found, err := Load(ctx, s, pfx); found || err != nil { t.Fatalf("a manifest was committed: found = %v, err = %v", found, err) } }