//go:build unix package hostfiles import ( "context" "os" "path/filepath" "testing" "github.com/carlosframework/platform/internal/release" ) // A parent directory swapped for a symlink, to outside or to an excluded // directory inside, must not let a queued path be read through it. func TestPublishOneNeverReadsThroughASwappedAncestor(t *testing.T) { for name, target := range map[string]string{"a link to outside": "OUTSIDE", "a link to an excluded directory inside": "cache"} { t.Run(name, func(t *testing.T) { ctx, s, src, outside := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir(), t.TempDir() write(t, src, "sub/x", "ours") write(t, src, "cache/x", "excluded") if err := os.WriteFile(filepath.Join(outside, "x"), []byte("outside secret"), 0o600); err != nil { t.Fatal(err) } skip := func(rel string, isDir bool) bool { return isDir && rel == "cache" } p := open(t, s) if _, err := SyncDir(ctx, p, src, skip); err != nil { t.Fatal(err) } commit(t, p) if target == "OUTSIDE" { target = outside } swapAt(t, 1, func() { // before "sub" is opened if err := os.RemoveAll(filepath.Join(src, "sub")); err != nil { t.Error(err) } if err := os.Symlink(target, filepath.Join(src, "sub")); err != nil { t.Error(err) } }) if _, err := publishOne(ctx, p, tree(t, src), "sub/x", skip); err != nil { t.Fatal(err) } commit(t, p) m, _, _ := Load(ctx, s, pfx) if e := m.Entries["sub/x"]; !e.Deleted { t.Fatalf("sub/x = %+v; the directory is gone, so the file is: want a tombstone, and no content read through the link", e) } }) } } // A queued path whose file exists but cannot be read is an error, and // what is published for it stays. func TestPublishOneKeepsAnUnreadableFilesPublishedEntry(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) } if _, err := publishOne(ctx, p, tree(t, src), "locked", nil); err == nil { t.Fatal("publishOne reported success for a file it could not read") } if p.Dirty() { t.Fatal("publishOne staged a change for a file it could not read") } } // A regular file replaced by another regular file mid-look is read again // at the next deadline, not tombstoned. func TestPublishOneRetriesARegularFileReplacedMidLook(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) } }) again, err := publishOne(ctx, p, tree(t, src), "f", nil) if err != nil || !again || p.Dirty() { t.Fatalf("again = %v, err = %v, dirty = %v; want a retry and nothing staged", again, err, p.Dirty()) } again, err = publishOne(ctx, p, tree(t, src), "f", nil) if err != nil || again { t.Fatalf("second look: again = %v, err = %v", again, err) } commit(t, p) if !manifestHas(s, "f", sum([]byte("v2"))) { t.Fatal("the replacement was not published") } } // descend must never leave the tree: a component that is empty, "." or // ".." is an error, not a missing directory. func TestDescendRefusesAnythingButPlainNames(t *testing.T) { src := t.TempDir() write(t, src, "a/x", "x") write(t, src, "b/x", "x") root := tree(t, src) for _, rel := range []string{"..", "a/../b", "./a", "a//b", ".", "a/.", "/a", "a/"} { sub, gone, err := root.descend(rel) if err == nil || gone || sub != nil { if sub != nil && sub != root { sub.Close() } t.Errorf("descend(%q) = (%v, gone=%v, %v); want an error", rel, sub, gone, err) } } if sub, gone, err := root.descend(""); err != nil || gone || sub != root { t.Errorf("descend(\"\") = (%v, %v, %v); want the root itself", sub, gone, err) } sub, gone, err := root.descend("a") if err != nil || gone || sub == nil { t.Fatalf("descend(a) = (%v, %v, %v)", sub, gone, err) } sub.Close() }