| 1 | //go:build unix |
| 2 | |
| 3 | package hostfiles |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | "os" |
| 8 | "path/filepath" |
| 9 | "testing" |
| 10 | |
| 11 | "github.com/carlosframework/platform/internal/release" |
| 12 | ) |
| 13 | |
| 14 | // A parent directory swapped for a symlink, to outside or to an excluded |
| 15 | // directory inside, must not let a queued path be read through it. |
| 16 | func TestPublishOneNeverReadsThroughASwappedAncestor(t *testing.T) { |
| 17 | for name, target := range map[string]string{"a link to outside": "OUTSIDE", "a link to an excluded directory inside": "cache"} { |
| 18 | t.Run(name, func(t *testing.T) { |
| 19 | ctx, s, src, outside := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir(), t.TempDir() |
| 20 | write(t, src, "sub/x", "ours") |
| 21 | write(t, src, "cache/x", "excluded") |
| 22 | if err := os.WriteFile(filepath.Join(outside, "x"), []byte("outside secret"), 0o600); err != nil { |
| 23 | t.Fatal(err) |
| 24 | } |
| 25 | skip := func(rel string, isDir bool) bool { return isDir && rel == "cache" } |
| 26 | p := open(t, s) |
| 27 | if _, err := SyncDir(ctx, p, src, skip); err != nil { |
| 28 | t.Fatal(err) |
| 29 | } |
| 30 | commit(t, p) |
| 31 | if target == "OUTSIDE" { |
| 32 | target = outside |
| 33 | } |
| 34 | swapAt(t, 1, func() { // before "sub" is opened |
| 35 | if err := os.RemoveAll(filepath.Join(src, "sub")); err != nil { |
| 36 | t.Error(err) |
| 37 | } |
| 38 | if err := os.Symlink(target, filepath.Join(src, "sub")); err != nil { |
| 39 | t.Error(err) |
| 40 | } |
| 41 | }) |
| 42 | if _, err := publishOne(ctx, p, tree(t, src), "sub/x", skip); err != nil { |
| 43 | t.Fatal(err) |
| 44 | } |
| 45 | commit(t, p) |
| 46 | m, _, _ := Load(ctx, s, pfx) |
| 47 | if e := m.Entries["sub/x"]; !e.Deleted { |
| 48 | t.Fatalf("sub/x = %+v; the directory is gone, so the file is: want a tombstone, and no content read through the link", e) |
| 49 | } |
| 50 | }) |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | // A queued path whose file exists but cannot be read is an error, and |
| 55 | // what is published for it stays. |
| 56 | func TestPublishOneKeepsAnUnreadableFilesPublishedEntry(t *testing.T) { |
| 57 | if os.Getuid() == 0 { |
| 58 | t.Skip("root can read anything") |
| 59 | } |
| 60 | ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir() |
| 61 | write(t, src, "locked", "v1") |
| 62 | p := open(t, s) |
| 63 | if _, err := SyncDir(ctx, p, src, nil); err != nil { |
| 64 | t.Fatal(err) |
| 65 | } |
| 66 | commit(t, p) |
| 67 | if err := os.Chmod(filepath.Join(src, "locked"), 0); err != nil { |
| 68 | t.Fatal(err) |
| 69 | } |
| 70 | if _, err := publishOne(ctx, p, tree(t, src), "locked", nil); err == nil { |
| 71 | t.Fatal("publishOne reported success for a file it could not read") |
| 72 | } |
| 73 | if p.Dirty() { |
| 74 | t.Fatal("publishOne staged a change for a file it could not read") |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | // A regular file replaced by another regular file mid-look is read again |
| 79 | // at the next deadline, not tombstoned. |
| 80 | func TestPublishOneRetriesARegularFileReplacedMidLook(t *testing.T) { |
| 81 | ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir() |
| 82 | write(t, src, "f", "v1") |
| 83 | p := open(t, s) |
| 84 | if _, err := SyncDir(ctx, p, src, nil); err != nil { |
| 85 | t.Fatal(err) |
| 86 | } |
| 87 | commit(t, p) |
| 88 | swapAt(t, 1, func() { |
| 89 | tmp := filepath.Join(src, "f.new") |
| 90 | if err := os.WriteFile(tmp, []byte("v2"), 0o640); err != nil { |
| 91 | t.Error(err) |
| 92 | } |
| 93 | if err := os.Rename(tmp, filepath.Join(src, "f")); err != nil { |
| 94 | t.Error(err) |
| 95 | } |
| 96 | }) |
| 97 | again, err := publishOne(ctx, p, tree(t, src), "f", nil) |
| 98 | if err != nil || !again || p.Dirty() { |
| 99 | t.Fatalf("again = %v, err = %v, dirty = %v; want a retry and nothing staged", again, err, p.Dirty()) |
| 100 | } |
| 101 | again, err = publishOne(ctx, p, tree(t, src), "f", nil) |
| 102 | if err != nil || again { |
| 103 | t.Fatalf("second look: again = %v, err = %v", again, err) |
| 104 | } |
| 105 | commit(t, p) |
| 106 | if !manifestHas(s, "f", sum([]byte("v2"))) { |
| 107 | t.Fatal("the replacement was not published") |
| 108 | } |
| 109 | } |
| 110 | |
| 111 | // descend must never leave the tree: a component that is empty, "." or |
| 112 | // ".." is an error, not a missing directory. |
| 113 | func TestDescendRefusesAnythingButPlainNames(t *testing.T) { |
| 114 | src := t.TempDir() |
| 115 | write(t, src, "a/x", "x") |
| 116 | write(t, src, "b/x", "x") |
| 117 | root := tree(t, src) |
| 118 | for _, rel := range []string{"..", "a/../b", "./a", "a//b", ".", "a/.", "/a", "a/"} { |
| 119 | sub, gone, err := root.descend(rel) |
| 120 | if err == nil || gone || sub != nil { |
| 121 | if sub != nil && sub != root { |
| 122 | sub.Close() |
| 123 | } |
| 124 | t.Errorf("descend(%q) = (%v, gone=%v, %v); want an error", rel, sub, gone, err) |
| 125 | } |
| 126 | } |
| 127 | if sub, gone, err := root.descend(""); err != nil || gone || sub != root { |
| 128 | t.Errorf("descend(\"\") = (%v, %v, %v); want the root itself", sub, gone, err) |
| 129 | } |
| 130 | sub, gone, err := root.descend("a") |
| 131 | if err != nil || gone || sub == nil { |
| 132 | t.Fatalf("descend(a) = (%v, %v, %v)", sub, gone, err) |
| 133 | } |
| 134 | sub.Close() |
| 135 | } |
| 136 | |