package hostfiles import ( "context" "encoding/json" "errors" "os" "path/filepath" "strings" "testing" "github.com/carlosframework/platform/internal/release" ) // writeFile is sync_test.go's write, which is unix-only. func writeFile(t *testing.T, dir, rel, body string) { t.Helper() full := filepath.Join(dir, rel) if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(full, []byte(body), 0o640); err != nil { t.Fatal(err) } } // pfx2 is the next generation of pfx's incarnation. const pfx2 = "instances/shop.test/abc/files/g2/" // publishG1 publishes two files and a platform item under pfx. func publishG1(t *testing.T, s release.Store) { t.Helper() p := open(t, s) stage(t, p, "instance.key", "k1") stage(t, p, "uploads/a/b.txt", "hello") if err := p.StagePlatform(context.Background(), "env", []byte("A=1")); err != nil { t.Fatal(err) } commit(t, p) } func TestEntriesCarryTheirOriginQualifiedObjectKey(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) publishG1(t, s) m, _, err := Load(ctx, s, pfx) if err != nil { t.Fatal(err) } if m.V != 2 { t.Fatalf("manifest version = %d, want 2", m.V) } e := m.Entries["instance.key"] if want := pfx + objectKey(e.Seq, e.SHA256); e.Object != want { t.Fatalf("object = %q, want %q", e.Object, want) } if _, _, err := s.Get(ctx, e.Object); err != nil { t.Fatalf("the object is not at its key: %v", err) } } func TestACopiedManifestRestoresTheSameBytesWithoutCopyingObjects(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) publishG1(t, s) m, err := CopyManifest(ctx, s, pfx, pfx2, 0, nil) if err != nil { t.Fatal(err) } if keys, _ := s.List(ctx, pfx2+"objects/"); len(keys) != 0 { t.Fatalf("the copy wrote objects: %v", keys) } src, _, _ := Load(ctx, s, pfx) if m.Seq != src.Seq || len(m.Entries) != len(src.Entries) { t.Fatalf("copy = %+v, source = %+v", m, src) } dir := t.TempDir() if _, _, err := Restore(ctx, s, pfx2, dir, []string{pfx, pfx2}); err != nil { t.Fatal(err) } if read(t, dir, "instance.key") != "k1" || read(t, dir, "uploads/a/b.txt") != "hello" { t.Fatal("restored the wrong bytes") } got, err := ReadPlatform(ctx, s, pfx2, m, "env", []string{pfx, pfx2}) if err != nil || string(got) != "A=1" { t.Fatalf("ReadPlatform = %q, %v", got, err) } // The new generation's publisher continues the sequence. A changed file // is a new object under g2, an unchanged one keeps its g1 reference. p, err := Open(ctx, s, pfx2) if err != nil { t.Fatal(err) } stage(t, p, "instance.key", "k1") stage(t, p, "uploads/a/b.txt", "changed") if seq := commit(t, p); seq != src.Seq+1 { t.Fatalf("seq = %d, want %d", seq, src.Seq+1) } m2, _, _ := Load(ctx, s, pfx2) if !strings.HasPrefix(m2.Entries["instance.key"].Object, pfx) || !strings.HasPrefix(m2.Entries["uploads/a/b.txt"].Object, pfx2) { t.Fatalf("entries = %+v", m2.Entries) } dir2 := t.TempDir() if _, _, err := Restore(ctx, s, pfx2, dir2, []string{pfx, pfx2}); err != nil { t.Fatal(err) } if read(t, dir2, "instance.key") != "k1" || read(t, dir2, "uploads/a/b.txt") != "changed" { t.Fatal("restored the wrong bytes after the second generation published") } // The source generation is untouched. if again, _, _ := Load(ctx, s, pfx); again.Seq != src.Seq { t.Fatalf("source seq moved to %d", again.Seq) } } func TestRestoreRefusesAnEntryOutsideTheAllowedOriginsAndTouchesNothing(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) publishG1(t, s) m, err := CopyManifest(ctx, s, pfx, pfx2, 0, nil) if err != nil { t.Fatal(err) } for name, origins := range map[string][]string{ "default (own prefix only)": nil, "another generation only": {"instances/shop.test/abc/files/g0/", pfx2}, } { dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "instance.key"), []byte("before"), 0o600); err != nil { t.Fatal(err) } if _, _, err := Restore(ctx, s, pfx2, dir, origins); err == nil || !strings.Contains(err.Error(), "origin") { t.Fatalf("%s: Restore err = %v, want an origin refusal", name, err) } if read(t, dir, "instance.key") != "before" { t.Fatalf("%s: Restore wrote before refusing", name) } if _, err := os.Stat(filepath.Join(dir, "uploads")); !os.IsNotExist(err) { t.Fatalf("%s: Restore created uploads/", name) } if _, err := ReadPlatform(ctx, s, pfx2, m, "env", origins); err == nil { t.Fatalf("%s: ReadPlatform read an object outside its origins", name) } } } func TestRestoreRefusesATamperedObjectKey(t *testing.T) { rel := func(e Entry) string { return objectKey(e.Seq, e.SHA256) } cases := map[string]func(e Entry) string{ "wrong seq": func(e Entry) string { return pfx + objectKey(e.Seq+1, e.SHA256) }, "wrong sha": func(e Entry) string { return pfx + objectKey(e.Seq, strings.Repeat("0", 64)) }, "relative (version 1)": rel, "dot-dot to a sibling gen": func(e Entry) string { return pfx + "../g9/" + rel(e) }, "dot-dot inside": func(e Entry) string { return pfx + "objects/../" + rel(e) }, "absolute": func(e Entry) string { return "/" + pfx + rel(e) }, "another host's prefix": func(e Entry) string { return "instances/other.test/abc/files/g1/" + rel(e) }, "nested under the origin": func(e Entry) string { return pfx + "x/" + rel(e) }, "double slash": func(e Entry) string { return strings.TrimSuffix(pfx, "/") + "//" + rel(e) }, "empty": func(Entry) string { return "" }, } for name, key := range cases { for _, platform := range []bool{false, true} { s, dir := putHostile(t, func(m *Manifest) { if platform { e := m.Platform["item"] e.Object = key(e) m.Platform["item"] = e return } e := m.Entries["a"] e.Object = key(e) m.Entries["a"] = e }) if _, _, err := Restore(context.Background(), s, pfx, dir, nil); err == nil { t.Fatalf("%s (platform %v): Restore accepted the manifest", name, platform) } if got := read(t, dir, "gone"); got != "still here" { t.Fatalf("%s (platform %v): gone = %q, want it untouched", name, platform, got) } if _, err := os.Stat(filepath.Join(dir, "a")); !os.IsNotExist(err) { t.Fatalf("%s (platform %v): a was written", name, platform) } } } } func TestRestoreRefusesAMalformedOrigin(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) publishG1(t, s) for _, o := range []string{"", "/", "instances/shop.test/abc/files/g1", "/instances/shop.test/abc/files/g1/", "instances/shop.test/abc/files/../files/g1/", "instances//shop.test/", "./instances/"} { if _, _, err := Restore(ctx, s, pfx, t.TempDir(), []string{o}); err == nil { t.Fatalf("Restore accepted origin %q", o) } } } func TestAManifestOfAnotherVersionIsRefused(t *testing.T) { for _, v := range []int{0, 1, 3} { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) body, _ := json.Marshal(map[string]any{"v": v, "seq": 0, "entries": map[string]any{}, "platform": map[string]any{}}) if _, err := s.Put(ctx, ManifestKey(pfx), body, ""); err != nil { t.Fatal(err) } if _, _, err := Load(ctx, s, pfx); err == nil || !strings.Contains(err.Error(), "version") { t.Fatalf("v%d: Load err = %v", v, err) } if _, err := Open(ctx, s, pfx); err == nil { t.Fatalf("v%d: Open accepted it", v) } if _, _, err := Restore(ctx, s, pfx, t.TempDir(), nil); err == nil { t.Fatalf("v%d: Restore accepted it", v) } if _, err := CopyManifest(ctx, s, pfx, pfx2, 0, nil); err == nil { t.Fatalf("v%d: CopyManifest accepted it", v) } } } func TestCopyManifestIsCreateOnlyAndRetrySafe(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) publishG1(t, s) if _, err := CopyManifest(ctx, s, pfx, pfx2, 0, nil); err != nil { t.Fatal(err) } // A repeat whose first response was lost finds its own bytes: success. if _, err := CopyManifest(ctx, s, pfx, pfx2, 0, nil); err != nil { t.Fatalf("a repeated copy: %v", err) } // Once g2 has moved on, a copy is refused and changes nothing. p, err := Open(ctx, s, pfx2) if err != nil { t.Fatal(err) } stage(t, p, "new", "n") commit(t, p) before, _, _ := s.Get(ctx, ManifestKey(pfx2)) if _, err := CopyManifest(ctx, s, pfx, pfx2, 0, nil); !errors.Is(err, ErrManifestExists) { t.Fatalf("err = %v, want ErrManifestExists", err) } after, _, _ := s.Get(ctx, ManifestKey(pfx2)) if string(before) != string(after) { t.Fatal("a refused copy changed the target") } } func TestCopyManifestRefusesAMissingOrInvalidSourceAndWritesNothing(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) if _, err := CopyManifest(ctx, s, pfx, pfx2, 0, nil); !errors.Is(err, os.ErrNotExist) { t.Fatalf("absent source: err = %v, want os.ErrNotExist", err) } // g2 copied from g1 references g1; copying g2 on to g3 with only g2 // admitted is refused. publishG1(t, s) if _, err := CopyManifest(ctx, s, pfx, pfx2, 0, nil); err != nil { t.Fatal(err) } const pfx3 = "instances/shop.test/abc/files/g3/" if _, err := CopyManifest(ctx, s, pfx2, pfx3, 0, nil); err == nil || !strings.Contains(err.Error(), "origin") { t.Fatalf("err = %v, want an origin refusal", err) } if _, _, err := s.Get(ctx, ManifestKey(pfx3)); !errors.Is(err, os.ErrNotExist) { t.Fatalf("a refused copy wrote a manifest: %v", err) } if _, err := CopyManifest(ctx, s, pfx2, pfx3, 0, []string{pfx, pfx2}); err != nil { t.Fatalf("with g1 admitted: %v", err) } for _, bad := range [][2]string{{pfx, pfx}, {pfx, "instances/shop.test/abc/files/g4"}, {"nope", pfx2}} { if _, err := CopyManifest(ctx, s, bad[0], bad[1], 0, nil); err == nil { t.Fatalf("CopyManifest(%q, %q) accepted", bad[0], bad[1]) } } } func TestPublishCommitsTheDirectoryAsOneRevision(t *testing.T) { ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir() writeFile(t, src, "instance.key", "k1") writeFile(t, src, "uploads/a.txt", "a") writeFile(t, src, "skip/me", "x") skip := func(rel string, isDir bool) bool { return rel == "skip" } seq, st, err := Publish(ctx, s, pfx, src, skip) if err != nil { t.Fatal(err) } if seq != 2 || st.Published != 2 { t.Fatalf("seq = %d, stats = %+v", seq, st) } dir := t.TempDir() m, _, err := Restore(ctx, s, pfx, dir, nil) if err != nil { t.Fatal(err) } if m.Seq != seq || read(t, dir, "instance.key") != "k1" || read(t, dir, "uploads/a.txt") != "a" { t.Fatalf("m = %+v", m) } if _, ok := m.Entries["skip/me"]; ok { t.Fatal("a skipped path was published") } // Publishing again with nothing changed is the same revision. if again, _, err := Publish(ctx, s, pfx, src, skip); err != nil || again != seq { t.Fatalf("again = %d, %v", again, err) } if _, _, err := Publish(ctx, s, pfx, filepath.Join(src, "absent"), nil); err == nil { t.Fatal("published a missing directory") } } // expectSeq pins the copy to the source revision the caller admitted: a // source that has moved on is refused, on a first attempt or a retry, and // nothing is written. func TestCopyManifestRefusesASourceThatIsNotAtTheExpectedSeq(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) publishG1(t, s) src, _, _ := Load(ctx, s, pfx) if _, err := CopyManifest(ctx, s, pfx, pfx2, src.Seq+1, nil); !errors.Is(err, ErrSourceMoved) { t.Fatalf("err = %v, want ErrSourceMoved", err) } if _, _, err := s.Get(ctx, ManifestKey(pfx2)); !errors.Is(err, os.ErrNotExist) { t.Fatalf("a refused copy wrote a manifest: %v", err) } m, err := CopyManifest(ctx, s, pfx, pfx2, src.Seq, nil) if err != nil || m.Seq != src.Seq { t.Fatalf("m.Seq = %d, err = %v", m.Seq, err) } if _, err := CopyManifest(ctx, s, pfx, pfx2, src.Seq, nil); err != nil { t.Fatalf("a repeat at the same seq: %v", err) } // The source commits again: a retry pinned to the old seq is refused, // and the target keeps the pinned copy. p := open(t, s) stage(t, p, "later", "x") commit(t, p) before, _, _ := s.Get(ctx, ManifestKey(pfx2)) if _, err := CopyManifest(ctx, s, pfx, pfx2, src.Seq, nil); !errors.Is(err, ErrSourceMoved) { t.Fatalf("err = %v, want ErrSourceMoved", err) } if after, _, _ := s.Get(ctx, ManifestKey(pfx2)); string(before) != string(after) { t.Fatal("a refused copy changed the target") } } func TestRestoreAndReadPlatformRefuseAMalformedPrefix(t *testing.T) { ctx, s := context.Background(), release.NewDirStore(t.TempDir()) publishG1(t, s) m, _, _ := Load(ctx, s, pfx) for _, bad := range []string{"", "instances/shop.test/abc/files/g1", "/" + pfx, "instances/../" + pfx, "instances//x/"} { // Admitting pfx explicitly means only the prefix check can refuse. if _, _, err := Restore(ctx, s, bad, t.TempDir(), []string{pfx}); err == nil { t.Fatalf("Restore accepted prefix %q", bad) } if _, err := ReadPlatform(ctx, s, bad, m, "env", []string{pfx}); err == nil { t.Fatalf("ReadPlatform accepted prefix %q", bad) } } }