1package snapshotcodec
2
3import (
4 "bytes"
5 "encoding/binary"
6 "encoding/hex"
7 "math"
8 "testing"
9
10 "github.com/bnema/vev/internal/domain/layout"
11)
12
13func TestManifestWeightRoundTripAndOldCompatibility(t *testing.T) {
14 weighted := testManifest(&layout.Tree{Root: &layout.Node{
15 Kind: layout.Split,
16 Dir: layout.Horizontal,
17 Children: []*layout.Node{
18 {Kind: layout.Leaf, Leaf: "one", Weight: 2},
19 {Kind: layout.Leaf, Leaf: "two", Weight: 1},
20 },
21 }}, nil)
22
23 encoded, err := MarshalManifest(weighted)
24 if err != nil {
25 t.Fatal(err)
26 }
27 weightedBody, err := unmarshalManifestEnvelope(encoded, manifestMagic)
28 if err != nil {
29 t.Fatal(err)
30 }
31 extensionSize := len(manifestWeightTag) + 4 + 3*manifestWeightSize
32 if len(weightedBody) < extensionSize {
33 t.Fatalf("weighted body length = %d, want at least %d", len(weightedBody), extensionSize)
34 }
35 unweightedBody := weightedBody[:len(weightedBody)-extensionSize]
36 extension := weightedBody[len(unweightedBody):]
37 if string(extension[:len(manifestWeightTag)]) != manifestWeightTag || binary.BigEndian.Uint32(extension[len(manifestWeightTag):]) != 3 {
38 t.Fatalf("weight extension header = %x", extension[:len(manifestWeightTag)+4])
39 }
40 for i, want := range []float64{0, 2, 1} {
41 offset := len(manifestWeightTag) + 4 + i*manifestWeightSize
42 if got := math.Float64frombits(binary.BigEndian.Uint64(extension[offset:])); got != want {
43 t.Fatalf("preorder weight %d = %v, want %v", i, got, want)
44 }
45 }
46
47 got, err := UnmarshalManifest(encoded)
48 if err != nil {
49 t.Fatal(err)
50 }
51 if got.Tabs[0].Tree.Root.Weight != 0 || got.Tabs[0].Tree.Root.Children[0].Weight != 2 || got.Tabs[0].Tree.Root.Children[1].Weight != 1 {
52 t.Fatalf("decoded weights = root %v, children %v/%v", got.Tabs[0].Tree.Root.Weight, got.Tabs[0].Tree.Root.Children[0].Weight, got.Tabs[0].Tree.Root.Children[1].Weight)
53 }
54
55 unweighted := testManifest(&layout.Tree{Root: &layout.Node{
56 Kind: layout.Split,
57 Dir: layout.Horizontal,
58 Children: []*layout.Node{
59 layout.NewLeaf("one"),
60 layout.NewLeaf("two"),
61 },
62 }}, nil)
63 oldEncoded, err := MarshalManifest(unweighted)
64 if err != nil {
65 t.Fatal(err)
66 }
67 oldBody, err := unmarshalManifestEnvelope(oldEncoded, manifestMagic)
68 if err != nil {
69 t.Fatal(err)
70 }
71 if !bytes.Equal(oldBody, unweightedBody) {
72 t.Fatalf("weighted manifest changed the legacy body before its extension")
73 }
74 oldDecoded, err := UnmarshalManifest(oldEncoded)
75 if err != nil {
76 t.Fatal(err)
77 }
78 for _, node := range manifestNodes(oldDecoded) {
79 if node.Weight != 0 {
80 t.Fatalf("old manifest decoded weight = %v, want zero", node.Weight)
81 }
82 }
83}
84
85func TestSnapshotEnvelopeCompatibility(t *testing.T) {
86 const (
87 headGolden = "56455648000100000000002898ebcac200000000000000070100000000000000000000000000000000000000000000000000000000000000"
88 objectGolden = "5645564f000200000000001ef35457a7030000000c7801000c00f3ff63616e6f6e6963616c2d767403001f2b04c0"
89 manifestGolden = "5645564d0005000000000133d118cc030000000000000001010000000000000000000000000000000000056e616d6564000000000000000000000001000374616200500018000000000000000000036f6e65010000020000036f6e6500000374776f000200036f6e6500036f6e6500042f6f6e65000101010000000000000000000000000000000000000000000000000000000000000000000016020200000000000000000000000000000000000000000000000000000000000000000000160303000000000000000000000000000000000000000000000000000000000000000000001600000374776f000374776f00042f74776f0000020400000000000000000000000000000000000000000000000000000000000000000000160305000000000000000000000000000000000000000000000000000000000000000000001600"
90 )
91 if ManifestVersion != 5 {
92 t.Fatalf("ManifestVersion = %d, want 5", ManifestVersion)
93 }
94
95 head, err := MarshalHead(Head{Generation: 7, ManifestDigest: SnapshotDigest{1}})
96 if err != nil {
97 t.Fatal(err)
98 }
99 object, err := MarshalObject(RecoveryTranscript, []byte("canonical-vt"))
100 if err != nil {
101 t.Fatal(err)
102 }
103 manifest, err := MarshalManifest(unweightedTestManifest())
104 if err != nil {
105 t.Fatal(err)
106 }
107 for _, tc := range []struct {
108 name string
109 got []byte
110 golden string
111 }{
112 {name: "HEAD", got: head, golden: headGolden},
113 {name: "object", got: object.Data, golden: objectGolden},
114 {name: "unweighted manifest", got: manifest, golden: manifestGolden},
115 } {
116 t.Run(tc.name, func(t *testing.T) {
117 want, err := hex.DecodeString(tc.golden)
118 if err != nil {
119 t.Fatal(err)
120 }
121 if !bytes.Equal(tc.got, want) {
122 t.Fatalf("bytes changed:\n got %x\nwant %x", tc.got, want)
123 }
124 })
125 }
126}
127
128func TestManifestWeightExtensionMalformedCorpus(t *testing.T) {
129 oldEncoded, err := MarshalManifest(unweightedTestManifest())
130 if err != nil {
131 t.Fatal(err)
132 }
133 oldBody, err := unmarshalManifestEnvelope(oldEncoded, manifestMagic)
134 if err != nil {
135 t.Fatal(err)
136 }
137
138 weighted := unweightedTestManifest()
139 weighted.Tabs[0].Tree.Root.Children[0].Weight = 2
140 weightedEncoded, err := MarshalManifest(weighted)
141 if err != nil {
142 t.Fatal(err)
143 }
144 weightedBody, err := unmarshalManifestEnvelope(weightedEncoded, manifestMagic)
145 if err != nil {
146 t.Fatal(err)
147 }
148 extensionOffset := len(oldBody)
149 if len(weightedBody) <= extensionOffset || string(weightedBody[extensionOffset:extensionOffset+len(manifestWeightTag)]) != manifestWeightTag {
150 t.Fatalf("weighted body has no %q extension", manifestWeightTag)
151 }
152
153 mutate := func(fn func([]byte)) []byte {
154 body := append([]byte(nil), weightedBody...)
155 fn(body)
156 return body
157 }
158 firstWeight := extensionOffset + len(manifestWeightTag) + 4
159 corpus := []struct {
160 name string
161 body []byte
162 valid bool
163 }{
164 {name: "legacy without extension", body: oldBody, valid: true},
165 {name: "valid extension", body: weightedBody, valid: true},
166 {name: "malformed tag", body: mutate(func(body []byte) { body[extensionOffset] ^= 0xff })},
167 {name: "node count mismatch", body: mutate(func(body []byte) { binary.BigEndian.PutUint32(body[extensionOffset+len(manifestWeightTag):], 2) })},
168 {name: "truncated weight", body: weightedBody[:len(weightedBody)-1]},
169 {name: "trailing garbage", body: append(append([]byte(nil), weightedBody...), 0)},
170 {name: "negative weight", body: mutate(func(body []byte) { binary.BigEndian.PutUint64(body[firstWeight:], math.Float64bits(-1)) })},
171 {name: "NaN weight", body: mutate(func(body []byte) { binary.BigEndian.PutUint64(body[firstWeight:], math.Float64bits(math.NaN())) })},
172 {name: "infinite weight", body: mutate(func(body []byte) { binary.BigEndian.PutUint64(body[firstWeight:], math.Float64bits(math.Inf(1))) })},
173 }
174
175 for _, tc := range corpus {
176 t.Run(tc.name, func(t *testing.T) {
177 preflightErr := preflightManifest(tc.body)
178 _, decodeErr := decodeManifest(tc.body)
179 if (preflightErr == nil) != tc.valid {
180 t.Fatalf("preflight error = %v, valid=%v", preflightErr, tc.valid)
181 }
182 if (decodeErr == nil) != tc.valid {
183 t.Fatalf("decode error = %v, valid=%v", decodeErr, tc.valid)
184 }
185 if (preflightErr == nil) != (decodeErr == nil) {
186 t.Fatalf("preflight/decode disagreement: %v / %v", preflightErr, decodeErr)
187 }
188 })
189 }
190}
191
192func TestMarshalManifestRejectsInvalidWeights(t *testing.T) {
193 for _, weight := range []float64{-1, math.NaN(), math.Inf(1), math.Inf(-1)} {
194 manifest := unweightedTestManifest()
195 manifest.Tabs[0].Tree.Root.Weight = weight
196 if _, err := MarshalManifest(manifest); err == nil {
197 t.Fatalf("MarshalManifest() accepted weight %v", weight)
198 }
199 }
200}
201
202func unweightedTestManifest() Manifest {
203 return testManifest(&layout.Tree{Root: &layout.Node{
204 Kind: layout.Split,
205 Dir: layout.Horizontal,
206 Children: []*layout.Node{
207 layout.NewLeaf("one"),
208 layout.NewLeaf("two"),
209 },
210 }}, nil)
211}