1package snapshot
2
3import (
4 "context"
5 "errors"
6 "os"
7 "path/filepath"
8 "strings"
9 "syscall"
10 "testing"
11
12 "github.com/bnema/vev/internal/domain"
13 codec "github.com/bnema/vev/internal/snapshotcodec"
14)
15
16func TestRepositoryRejectsSymlinkedRoot(t *testing.T) {
17 configuredRoot := filepath.Join(t.TempDir(), "configured")
18 externalRoot := privateDir(t)
19 if err := os.Symlink(externalRoot, configuredRoot); err != nil {
20 t.Fatal(err)
21 }
22 if _, err := NewRepository(configuredRoot).openRoot(); err == nil {
23 t.Fatal("openRoot succeeded through configured-root symlink")
24 }
25
26 configuredRoot = privateDir(t)
27 repo := NewRepository(configuredRoot)
28 publication := repositoryPublication(t, "named", 1, []byte("configured"))
29 if err := repo.Publish(context.Background(), publication); err != nil {
30 t.Fatal(err)
31 }
32 external := NewRepository(externalRoot)
33 externalPublication := repositoryPublication(t, publication.Name, 1, []byte("external"))
34 if err := external.Publish(context.Background(), externalPublication); err != nil {
35 t.Fatal(err)
36 }
37 if err := os.Rename(configuredRoot, configuredRoot+"-real"); err != nil {
38 t.Fatal(err)
39 }
40 if err := os.Symlink(externalRoot, configuredRoot); err != nil {
41 t.Fatal(err)
42 }
43
44 if _, err := loadPublication(context.Background(), repo, publication); err == nil {
45 t.Fatal("LoadCheckpoint succeeded through replaced configured-root symlink")
46 }
47 keep := map[domain.IncarnationID]domain.CheckpointRef{publication.IncarnationID: {Generation: publication.Generation, ManifestDigest: codec.ManifestDigest(publication.Manifest)}}
48 if err := collectAll(context.Background(), repo, keep); err == nil {
49 t.Fatal("GC succeeded through replaced configured-root symlink")
50 }
51 loaded, err := loadPublication(context.Background(), external, externalPublication)
52 if err != nil {
53 t.Fatal(err)
54 }
55 if string(loaded.Manifest) != string(externalPublication.Manifest) {
56 t.Fatal("external repository changed")
57 }
58}
59
60func TestOpenRootRejectsUnsafePinnedRoot(t *testing.T) {
61 dir := privateDir(t)
62 if err := os.Mkdir(dir, 0o700); err != nil {
63 t.Fatal(err)
64 }
65 if err := os.Chmod(dir, 0o755); err != nil {
66 t.Fatal(err)
67 }
68 if _, err := NewRepository(dir).openRoot(); err == nil || !strings.Contains(err.Error(), "not private") {
69 t.Fatalf("openRoot error = %v, want private-directory rejection", err)
70 }
71}
72
73func TestRepositoryRejectsSymlinkedGenerationAndObjectShards(t *testing.T) {
74 for _, target := range []string{repositoryGenerations, repositoryObjectsDir} {
75 t.Run(target, func(t *testing.T) {
76 repo := NewRepository(privateDir(t))
77 publication := repositoryPublication(t, "named", 1, []byte("state"))
78 if err := repo.Publish(context.Background(), publication); err != nil {
79 t.Fatal(err)
80 }
81 inside := filepath.Join(repo.sessionPath(publication.IncarnationID), target)
82 outside := t.TempDir()
83 guard := filepath.Join(outside, "must-not-change")
84 if err := os.WriteFile(guard, []byte("guard"), 0o600); err != nil {
85 t.Fatal(err)
86 }
87 if err := os.Rename(inside, inside+"-real"); err != nil {
88 t.Fatal(err)
89 }
90 if err := os.Symlink(outside, inside); err != nil {
91 t.Fatal(err)
92 }
93 if _, err := loadPublication(context.Background(), repo, publication); err == nil {
94 t.Fatal("LoadCheckpoint succeeded through symlinked repository component")
95 }
96 keep := map[domain.IncarnationID]domain.CheckpointRef{publication.IncarnationID: {Generation: publication.Generation, ManifestDigest: codec.ManifestDigest(publication.Manifest)}}
97 if err := collectAll(context.Background(), repo, keep); err == nil {
98 t.Fatal("GC succeeded through symlinked repository component")
99 }
100 got, err := os.ReadFile(guard)
101 if err != nil || string(got) != "guard" {
102 t.Fatalf("outside guard = %q, %v", got, err)
103 }
104 })
105 }
106}
107
108func TestFinalSymlinksRejectedByRootOperations(t *testing.T) {
109 repo := NewRepository(privateDir(t))
110 if err := repo.ensurePrivateDirectory(repo.dir); err != nil {
111 t.Fatal(err)
112 }
113 target := filepath.Join(repo.dir, "target")
114 if err := os.Mkdir(target, 0o700); err != nil {
115 t.Fatal(err)
116 }
117 link := filepath.Join(repo.dir, "link")
118 if err := os.Symlink(target, link); err != nil {
119 t.Fatal(err)
120 }
121 if _, err := repo.openDirectory(link); !errors.Is(err, syscall.ELOOP) || strings.Contains(err.Error(), repo.dir) {
122 t.Fatalf("openDirectory error = %v, want sanitized ELOOP", err)
123 }
124 if _, err := repo.readBounded(link); !errors.Is(err, syscall.ELOOP) || strings.Contains(err.Error(), repo.dir) {
125 t.Fatalf("readBounded error = %v, want sanitized ELOOP", err)
126 }
127}