carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package hostfiles
4
5import (
6 "context"
7 "encoding/json"
8 "errors"
9 "os"
10 "path/filepath"
11 "runtime"
12 "strings"
13 "sync/atomic"
14 "syscall"
15 "testing"
16 "time"
17
18 "github.com/carlosframework/platform/internal/release"
19)
20
21// Astra P1 / Opus M1: a FIFO swapped in at a directory Restore is about to
22// sync (after realParents looked) is refused at once, never waited on.
23func TestRestoreRefusesAFIFOSwappedInAtADirectoryItSyncs(t *testing.T) {
24 ctx := context.Background()
25 s := release.NewDirStore(t.TempDir())
26 p := open(t, s)
27 stage(t, p, "a/b.txt", "restored")
28 commit(t, p)
29 dir := t.TempDir()
30 prev := syncDir
31 syncDir = func(root *os.Root, rel string) error {
32 if rel == "a" {
33 if err := os.Rename(filepath.Join(dir, "a"), filepath.Join(dir, "a.moved")); err != nil {
34 t.Error(err)
35 }
36 if err := syscall.Mkfifo(filepath.Join(dir, "a"), 0o600); err != nil {
37 t.Error(err)
38 }
39 }
40 return prev(root, rel)
41 }
42 t.Cleanup(func() { syncDir = prev })
43 done := make(chan error, 1)
44 go func() { _, _, err := Restore(ctx, s, pfx, dir, nil); done <- err }()
45 select {
46 case err := <-done:
47 if err == nil {
48 t.Fatal("Restore synced a FIFO as a directory")
49 }
50 case <-time.After(20 * time.Second):
51 t.Fatal("Restore blocked on a FIFO swapped in at a directory it syncs")
52 }
53}
54
55// Astra P2 / Opus M4: with the caller's evidence of no env file, a
56// directory renamed away and back while Present looks is never read as
57// "no env file there" when one is.
58func TestPresentNeverReadsAVanishingDirectoryAsNoEnvFile(t *testing.T) {
59 none := Manifest{V: ManifestVersion, Entries: map[string]Entry{}, Platform: map[string]Entry{}}
60 dir := filepath.Join(t.TempDir(), "inst")
61 if err := os.Mkdir(dir, 0o700); err != nil {
62 t.Fatal(err)
63 }
64 env := filepath.Join(dir, "shop.example.env")
65 if err := os.WriteFile(env, []byte("X=1\n"), 0o600); err != nil {
66 t.Fatal(err)
67 }
68 var stop atomic.Bool
69 flapped := make(chan struct{})
70 go func() {
71 defer close(flapped)
72 for !stop.Load() {
73 os.Rename(dir, dir+".away")
74 os.Rename(dir+".away", dir)
75 }
76 }()
77 deadline := time.Now().Add(1500 * time.Millisecond)
78 for time.Now().Before(deadline) {
79 if ok, _ := Present(t.TempDir(), env, none, true); ok {
80 stop.Store(true)
81 <-flapped
82 t.Fatal("Present read a directory that vanished between its steps as no env file")
83 }
84 }
85 stop.Store(true)
86 <-flapped
87}
88
89// Opus I1 (Fix 4: the byte cap alone): the walk refuses a path past
90// MaxPathBytes bytes, at the boundary exactly, accepts a deep tree within
91// it, and a chain far past it is
92// refused quickly with bounded allocation.
93func TestSyncDirRefusesATreePastItsCaps(t *testing.T) {
94 build := func(t *testing.T, comps []string, file bool) string {
95 t.Helper()
96 dir := t.TempDir()
97 root, err := os.OpenRoot(dir)
98 if err != nil {
99 t.Fatal(err)
100 }
101 defer root.Close()
102 dirs := comps
103 if file {
104 dirs = comps[:len(comps)-1]
105 }
106 if len(dirs) > 0 {
107 if err := root.MkdirAll(strings.Join(dirs, "/"), 0o700); err != nil {
108 t.Fatal(err)
109 }
110 }
111 if file {
112 if err := root.WriteFile(strings.Join(comps, "/"), []byte("x"), 0o600); err != nil {
113 t.Fatal(err)
114 }
115 }
116 return dir
117 }
118 repeat := func(n int, c string) []string {
119 out := make([]string, n)
120 for i := range out {
121 out[i] = c
122 }
123 return out
124 }
125 sync := func(t *testing.T, ctx context.Context, dir string) error {
126 t.Helper()
127 _, err := SyncDir(ctx, open(t, release.NewDirStore(t.TempDir())), dir, nil)
128 return err
129 }
130 for _, tc := range []struct {
131 name string
132 comps []string
133 file bool
134 ok bool
135 }{
136 {"a file at 257 one-byte components", repeat(257, "d"), true, true},
137 {"directories past the byte cap, no file", repeat(2100, "d"), false, false},
138 {"a path of MaxPathBytes", strings.Split(pathOfBytes(MaxPathBytes), "/"), true, true},
139 {"a path of MaxPathBytes+1", strings.Split(pathOfBytes(MaxPathBytes+1), "/"), true, false},
140 } {
141 t.Run(tc.name, func(t *testing.T) {
142 if got := len(strings.Join(tc.comps, "/")); strings.Contains(tc.name, "MaxPathBytes") && got != MaxPathBytes && got != MaxPathBytes+1 {
143 t.Fatalf("built a %d-byte path", got)
144 }
145 err := sync(t, context.Background(), build(t, tc.comps, tc.file))
146 if tc.ok && err != nil {
147 t.Fatalf("a tree within the caps: %v", err)
148 }
149 if !tc.ok && !errors.Is(err, ErrTooDeep) {
150 t.Fatalf("a tree past the caps: %v; want ErrTooDeep", err)
151 }
152 })
153 }
154 t.Run("a chain far past the cap", func(t *testing.T) {
155 dir := build(t, repeat(4000, "d"), true)
156 var before, after runtime.MemStats
157 runtime.GC()
158 runtime.ReadMemStats(&before)
159 start := time.Now()
160 err := sync(t, context.Background(), dir)
161 runtime.ReadMemStats(&after)
162 if !errors.Is(err, ErrTooDeep) {
163 t.Fatalf("got %v, want ErrTooDeep", err)
164 }
165 if d := time.Since(start); d > 5*time.Second {
166 t.Fatalf("refusing took %v", d)
167 }
168 if alloc := after.TotalAlloc - before.TotalAlloc; alloc > 16<<20 {
169 t.Fatalf("refusing allocated %d bytes", alloc)
170 }
171 })
172 t.Run("cancellation stops a walk with no files", func(t *testing.T) {
173 ctx, cancel := context.WithCancel(context.Background())
174 cancel()
175 if err := sync(t, ctx, build(t, repeat(20, "d"), false)); !errors.Is(err, context.Canceled) {
176 t.Fatalf("got %v, want context.Canceled", err)
177 }
178 })
179}
180
181// checkManifest holds a manifest's paths to the walk's caps, so Restore
182// refuses one a walk could not have published before touching anything.
183func TestRestoreRefusesAManifestPathPastTheCaps(t *testing.T) {
184 sha := strings.Repeat("a", 64)
185 entry := Entry{Object: pfx + objectKey(1, sha), SHA256: sha, Seq: 1, Size: 1}
186 man := func(p string) Manifest {
187 return Manifest{V: ManifestVersion, Seq: 1, Entries: map[string]Entry{p: entry}, Platform: map[string]Entry{}}
188 }
189 deep := func(n int) string { return strings.TrimSuffix(strings.Repeat("d/", n), "/") }
190 for _, tc := range []struct {
191 path string
192 ok bool
193 }{
194 {deep(257), true}, {deep(2048), true}, {deep(2049), false},
195 {pathOfBytes(MaxPathBytes), true},
196 {pathOfBytes(MaxPathBytes + 1), false},
197 } {
198 err := checkManifest(man(tc.path), []string{pfx})
199 if tc.ok && err != nil || !tc.ok && !errors.Is(err, ErrTooDeep) {
200 t.Fatalf("%d bytes, %d components: %v (ok %v)", len(tc.path), strings.Count(tc.path, "/")+1, err, tc.ok)
201 }
202 }
203 ctx, s := context.Background(), release.NewDirStore(t.TempDir())
204 body, err := json.Marshal(man(pathOfBytes(MaxPathBytes + 1)))
205 if err != nil {
206 t.Fatal(err)
207 }
208 if _, err := s.Put(ctx, ManifestKey(pfx), body, ""); err != nil {
209 t.Fatal(err)
210 }
211 dir := t.TempDir()
212 if _, _, err := Restore(ctx, s, pfx, dir, nil); !errors.Is(err, ErrTooDeep) {
213 t.Fatalf("Restore: %v; want ErrTooDeep", err)
214 }
215 if names, _ := os.ReadDir(dir); len(names) != 0 {
216 t.Fatalf("a refused restore touched the directory: %v", names)
217 }
218}
219
220// pathOfBytes is a relative path of exactly n bytes in components of at
221// most 200 bytes.
222func pathOfBytes(n int) string {
223 var b strings.Builder
224 for b.Len() < n {
225 if b.Len() > 0 {
226 b.WriteByte('/')
227 }
228 b.WriteString(strings.Repeat("x", min(200, n-b.Len())))
229 }
230 return b.String()
231}
232