1package snapshot
2
3import (
4 "errors"
5 "os"
6 "path/filepath"
7 "strings"
8 "syscall"
9 "testing"
10
11 codec "github.com/bnema/vev/internal/snapshotcodec"
12)
13
14func TestReadBoundedRejectsUnsafeFiles(t *testing.T) {
15 dir := t.TempDir()
16 regular := filepath.Join(dir, "regular")
17 if err := os.WriteFile(regular, []byte("safe"), 0o600); err != nil {
18 t.Fatal(err)
19 }
20 tooLarge := filepath.Join(dir, "large")
21 f, err := os.Create(tooLarge)
22 if err != nil {
23 t.Fatal(err)
24 }
25 if err := f.Truncate(maxRepositoryRead + 1); err != nil {
26 t.Fatal(err)
27 }
28 if err := f.Close(); err != nil {
29 t.Fatal(err)
30 }
31 unsafeMode := filepath.Join(dir, "mode")
32 if err := os.WriteFile(unsafeMode, []byte("unsafe"), 0o644); err != nil {
33 t.Fatal(err)
34 }
35 link := filepath.Join(dir, "link")
36 if err := os.Symlink(regular, link); err != nil {
37 t.Fatal(err)
38 }
39 fifo := filepath.Join(dir, "fifo")
40 if err := syscall.Mkfifo(fifo, 0o600); err != nil {
41 t.Fatal(err)
42 }
43
44 for _, path := range []string{tooLarge, unsafeMode, link, fifo} {
45 t.Run(filepath.Base(path), func(t *testing.T) {
46 if _, err := readBounded(path); err == nil {
47 t.Fatal("readBounded accepted unsafe file")
48 }
49 })
50 }
51
52 wrongOwner := filepath.Join(dir, "owner")
53 if os.Geteuid() == 0 {
54 if err := os.WriteFile(wrongOwner, []byte("unsafe"), 0o600); err != nil {
55 t.Fatal(err)
56 }
57 if err := os.Chown(wrongOwner, 1, -1); err != nil {
58 t.Fatal(err)
59 }
60 if _, err := readBounded(wrongOwner); err == nil {
61 t.Fatal("readBounded accepted wrong-owner file")
62 }
63 }
64}
65
66func TestReadSizedFile(t *testing.T) {
67 const limit = 16
68 tests := []struct {
69 name string
70 content string
71 hintSize int
72 want string
73 }{
74 {name: "empty", content: "", hintSize: 0, want: ""},
75 {name: "exact size hint", content: "snapshot object", hintSize: 15, want: "snapshot object"},
76 {name: "file grew after stat", content: "snapshot", hintSize: 4, want: "snapshot"},
77 {name: "file shrank after stat", content: "tiny", hintSize: 64, want: "tiny"},
78 {name: "file grew past limit stops one byte over", content: "snapshot object grew far", hintSize: 4, want: "snapshot object g"},
79 }
80 for _, tt := range tests {
81 t.Run(tt.name, func(t *testing.T) {
82 got, err := readSizedFile(strings.NewReader(tt.content), tt.hintSize, limit)
83 if err != nil {
84 t.Fatal(err)
85 }
86 if string(got) != tt.want {
87 t.Fatalf("readSizedFile = %q, want %q", got, tt.want)
88 }
89 })
90 }
91
92 // An exact size hint must cost one buffer, unlike io.ReadAll's growth.
93 content := strings.Repeat("x", 1<<20)
94 allocs := testing.AllocsPerRun(10, func() {
95 _, _ = readSizedFile(strings.NewReader(content), len(content), maxRepositoryRead)
96 })
97 if allocs > 3 {
98 t.Fatalf("readSizedFile allocations = %v, want at most 3", allocs)
99 }
100}
101
102func TestValidatePublicationRejectsUnloadableAggregate(t *testing.T) {
103 pub := repositoryPublication(t, "named", 1, []byte("state"))
104 manifest, err := codec.UnmarshalManifest(pub.Manifest)
105 if err != nil {
106 t.Fatal(err)
107 }
108 manifest.Tabs[0].Panes[0].Tail.Size = uint32(maxRepositoryRead)
109 manifest.Tabs[0].Panes[0].Transcript.Size = uint32(maxRepositoryRead)
110 pub.Manifest, err = codec.MarshalManifest(manifest)
111 if err != nil {
112 t.Fatal(err)
113 }
114 pub.Objects = nil // validation permits omitted objects for immutable reuse.
115 if _, _, err := validatePublication(pub); err == nil {
116 t.Fatal("validatePublication accepted unloadable aggregate")
117 } else if errors.Is(err, os.ErrNotExist) {
118 t.Fatalf("validatePublication returned unrelated error: %v", err)
119 }
120}