detached 871b49b

feat(snapshot): persist pane weights in manifests

commit
871b49ba273519184d3b60450c3b97a1420a4a81
author
brice · · 2026-07-25 04:17 UTC
parent
8bef307
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +351 -3
M internal/usecase/snapshot/manifest.go +96 -1
@@ -29,6 +29,9 @@ const (
2929
3030 processAbsent uint8 = 0
3131 processPresent uint8 = 1
32+
33+ manifestWeightTag = "WGT1"
34+ manifestWeightSize = 8
3235 // maxDecodedBodySize includes the object kind and payload-length prefix.
3336 maxObjectEnvelopeSize = manifestHeaderSize + maxDecodedBodySize
3437 maxObjectPayloadSize = maxDecodedBodySize - objectEnvelopeBodyPrefixSize
@@ -140,6 +143,9 @@ func MarshalManifest(m Manifest) ([]byte, error) {
140143 return nil, err
141144 }
142145 }
146+ if err := writeManifestWeights(&w, manifestNodes(m)); err != nil {
147+ return nil, err
148+ }
143149 return marshalManifestEnvelope(manifestMagic, w.b)
144150 }
145151
@@ -184,7 +190,7 @@ func decodeManifest(body []byte) (Manifest, error) {
184190 return Manifest{}, err
185191 }
186192 }
187- if err := r.done(); err != nil {
193+ if err := readManifestWeights(&r, manifestNodes(m)); err != nil {
188194 return Manifest{}, err
189195 }
190196 if err := validateManifest(m); err != nil {
@@ -316,6 +322,90 @@ func writeManifestTab(w *payloadWriter, tab ManifestTab) error {
316322 }
317323 return nil
318324 }
325+func writeManifestWeights(w *payloadWriter, nodes []*layout.Node) error {
326+ hasWeights := false
327+ for _, node := range nodes {
328+ if node.Weight != 0 {
329+ hasWeights = true
330+ break
331+ }
332+ }
333+ if !hasWeights {
334+ return nil
335+ }
336+ if len(nodes) > math.MaxUint32 {
337+ return fmt.Errorf("%w: too many weighted nodes", ErrInvalidData)
338+ }
339+ w.b = append(w.b, manifestWeightTag...)
340+ w.putUint32(uint32(len(nodes)))
341+ for _, node := range nodes {
342+ w.putUint64(math.Float64bits(node.Weight))
343+ }
344+ return nil
345+}
346+
347+func readManifestWeights(r *payloadReader, nodes []*layout.Node) error {
348+ if len(r.b) == 0 {
349+ return nil
350+ }
351+ if len(r.b) < len(manifestWeightTag) {
352+ return ErrShortPayload
353+ }
354+ if string(r.b[:len(manifestWeightTag)]) != manifestWeightTag {
355+ return fmt.Errorf("%w: weight extension tag", ErrInvalidData)
356+ }
357+ r.b = r.b[len(manifestWeightTag):]
358+ count, err := r.getUint32()
359+ if err != nil {
360+ return err
361+ }
362+ if uint64(count) != uint64(len(nodes)) {
363+ return fmt.Errorf("%w: weight node count", ErrInvalidData)
364+ }
365+ if uint64(count) > uint64(len(r.b))/manifestWeightSize {
366+ return ErrShortPayload
367+ }
368+ if uint64(len(r.b)) != uint64(count)*manifestWeightSize {
369+ return ErrTrailingBytes
370+ }
371+ for i := range nodes {
372+ bits, err := r.getUint64()
373+ if err != nil {
374+ return err
375+ }
376+ weight := math.Float64frombits(bits)
377+ if !validManifestWeight(weight) {
378+ return fmt.Errorf("%w: node weight", ErrInvalidData)
379+ }
380+ nodes[i].Weight = weight
381+ }
382+ return r.done()
383+}
384+
385+func manifestNodes(m Manifest) []*layout.Node {
386+ var nodes []*layout.Node
387+ var visit func(*layout.Node)
388+ visit = func(node *layout.Node) {
389+ if node == nil {
390+ return
391+ }
392+ nodes = append(nodes, node)
393+ for _, child := range node.Children {
394+ visit(child)
395+ }
396+ }
397+ for _, tab := range m.Tabs {
398+ if tab.Tree != nil {
399+ visit(tab.Tree.Root)
400+ }
401+ }
402+ return nodes
403+}
404+
405+func validManifestWeight(weight float64) bool {
406+ return weight >= 0 && !math.IsNaN(weight) && !math.IsInf(weight, 0)
407+}
408+
319409 func writeManifestPane(w *payloadWriter, pane ManifestPane) error {
320410 if err := w.putString(string(pane.ID)); err != nil {
321411 return err
@@ -448,6 +538,11 @@ func validateManifest(m Manifest) error {
448538 if err := validateActive(m.Active, len(m.Tabs)); err != nil {
449539 return err
450540 }
541+ for _, node := range manifestNodes(m) {
542+ if !validManifestWeight(node.Weight) {
543+ return fmt.Errorf("%w: node weight", ErrInvalidData)
544+ }
545+ }
451546 for _, tab := range m.Tabs {
452547 if !safeDimensions(tab.Cols, tab.Rows) {
453548 return fmt.Errorf("%w: dimensions", ErrInvalidData)
@@ -29,6 +29,9 @@ const (
29 29
30 processAbsent uint8 = 030 processAbsent uint8 = 0
31 processPresent uint8 = 131 processPresent uint8 = 1
32+
33+ manifestWeightTag = "WGT1"
34+ manifestWeightSize = 8
32 // maxDecodedBodySize includes the object kind and payload-length prefix.35 // maxDecodedBodySize includes the object kind and payload-length prefix.
33 maxObjectEnvelopeSize = manifestHeaderSize + maxDecodedBodySize36 maxObjectEnvelopeSize = manifestHeaderSize + maxDecodedBodySize
34 maxObjectPayloadSize = maxDecodedBodySize - objectEnvelopeBodyPrefixSize37 maxObjectPayloadSize = maxDecodedBodySize - objectEnvelopeBodyPrefixSize
@@ -140,6 +143,9 @@ func MarshalManifest(m Manifest) ([]byte, error) {
140 return nil, err143 return nil, err
141 }144 }
142 }145 }
146+ if err := writeManifestWeights(&w, manifestNodes(m)); err != nil {
147+ return nil, err
148+ }
143 return marshalManifestEnvelope(manifestMagic, w.b)149 return marshalManifestEnvelope(manifestMagic, w.b)
144 }150 }
145 151
@@ -184,7 +190,7 @@ func decodeManifest(body []byte) (Manifest, error) {
184 return Manifest{}, err190 return Manifest{}, err
185 }191 }
186 }192 }
187- if err := r.done(); err != nil {193+ if err := readManifestWeights(&r, manifestNodes(m)); err != nil {
188 return Manifest{}, err194 return Manifest{}, err
189 }195 }
190 if err := validateManifest(m); err != nil {196 if err := validateManifest(m); err != nil {
@@ -316,6 +322,90 @@ func writeManifestTab(w *payloadWriter, tab ManifestTab) error {
316 }322 }
317 return nil323 return nil
318 }324 }
325+func writeManifestWeights(w *payloadWriter, nodes []*layout.Node) error {
326+ hasWeights := false
327+ for _, node := range nodes {
328+ if node.Weight != 0 {
329+ hasWeights = true
330+ break
331+ }
332+ }
333+ if !hasWeights {
334+ return nil
335+ }
336+ if len(nodes) > math.MaxUint32 {
337+ return fmt.Errorf("%w: too many weighted nodes", ErrInvalidData)
338+ }
339+ w.b = append(w.b, manifestWeightTag...)
340+ w.putUint32(uint32(len(nodes)))
341+ for _, node := range nodes {
342+ w.putUint64(math.Float64bits(node.Weight))
343+ }
344+ return nil
345+}
346+
347+func readManifestWeights(r *payloadReader, nodes []*layout.Node) error {
348+ if len(r.b) == 0 {
349+ return nil
350+ }
351+ if len(r.b) < len(manifestWeightTag) {
352+ return ErrShortPayload
353+ }
354+ if string(r.b[:len(manifestWeightTag)]) != manifestWeightTag {
355+ return fmt.Errorf("%w: weight extension tag", ErrInvalidData)
356+ }
357+ r.b = r.b[len(manifestWeightTag):]
358+ count, err := r.getUint32()
359+ if err != nil {
360+ return err
361+ }
362+ if uint64(count) != uint64(len(nodes)) {
363+ return fmt.Errorf("%w: weight node count", ErrInvalidData)
364+ }
365+ if uint64(count) > uint64(len(r.b))/manifestWeightSize {
366+ return ErrShortPayload
367+ }
368+ if uint64(len(r.b)) != uint64(count)*manifestWeightSize {
369+ return ErrTrailingBytes
370+ }
371+ for i := range nodes {
372+ bits, err := r.getUint64()
373+ if err != nil {
374+ return err
375+ }
376+ weight := math.Float64frombits(bits)
377+ if !validManifestWeight(weight) {
378+ return fmt.Errorf("%w: node weight", ErrInvalidData)
379+ }
380+ nodes[i].Weight = weight
381+ }
382+ return r.done()
383+}
384+
385+func manifestNodes(m Manifest) []*layout.Node {
386+ var nodes []*layout.Node
387+ var visit func(*layout.Node)
388+ visit = func(node *layout.Node) {
389+ if node == nil {
390+ return
391+ }
392+ nodes = append(nodes, node)
393+ for _, child := range node.Children {
394+ visit(child)
395+ }
396+ }
397+ for _, tab := range m.Tabs {
398+ if tab.Tree != nil {
399+ visit(tab.Tree.Root)
400+ }
401+ }
402+ return nodes
403+}
404+
405+func validManifestWeight(weight float64) bool {
406+ return weight >= 0 && !math.IsNaN(weight) && !math.IsInf(weight, 0)
407+}
408+
319 func writeManifestPane(w *payloadWriter, pane ManifestPane) error {409 func writeManifestPane(w *payloadWriter, pane ManifestPane) error {
320 if err := w.putString(string(pane.ID)); err != nil {410 if err := w.putString(string(pane.ID)); err != nil {
321 return err411 return err
@@ -448,6 +538,11 @@ func validateManifest(m Manifest) error {
448 if err := validateActive(m.Active, len(m.Tabs)); err != nil {538 if err := validateActive(m.Active, len(m.Tabs)); err != nil {
449 return err539 return err
450 }540 }
541+ for _, node := range manifestNodes(m) {
542+ if !validManifestWeight(node.Weight) {
543+ return fmt.Errorf("%w: node weight", ErrInvalidData)
544+ }
545+ }
451 for _, tab := range m.Tabs {546 for _, tab := range m.Tabs {
452 if !safeDimensions(tab.Cols, tab.Rows) {547 if !safeDimensions(tab.Cols, tab.Rows) {
453 return fmt.Errorf("%w: dimensions", ErrInvalidData)548 return fmt.Errorf("%w: dimensions", ErrInvalidData)
M internal/usecase/snapshot/manifest_preflight.go +44 -2
@@ -2,6 +2,7 @@ package snapshot
22
33 import (
44 "fmt"
5+ "math"
56
67 "github.com/bnema/vev/internal/usecase/layout"
78 )
@@ -43,10 +44,13 @@ func preflightManifest(body []byte) error {
4344 return err
4445 }
4546 }
46- return r.done()
47+ return preflightManifestWeights(&r, budget.nodes)
4748 }
4849
49-type manifestPreflightBudget struct{ allocation uint64 }
50+type manifestPreflightBudget struct {
51+ allocation uint64
52+ nodes uint32
53+}
5054
5155 func (b *manifestPreflightBudget) add(n uint64) bool {
5256 if n > maxSnapshotDecodedAllocation-b.allocation {
@@ -158,6 +162,7 @@ func preflightManifestNode(r *payloadReader, depth int, root bool, budget *manif
158162 if err != nil {
159163 return err
160164 }
165+ budget.nodes++
161166 *refs = append(*refs, layout.PaneID(leaf))
162167 return nil
163168 case manifestNodeSplit:
@@ -184,6 +189,7 @@ func preflightManifestNode(r *payloadReader, depth int, root bool, budget *manif
184189 default:
185190 return fmt.Errorf("%w: node kind", ErrInvalidData)
186191 }
192+ budget.nodes++
187193 n, err := r.getUint16()
188194 if err != nil {
189195 return err
@@ -199,6 +205,42 @@ func preflightManifestNode(r *payloadReader, depth int, root bool, budget *manif
199205 return nil
200206 }
201207
208+func preflightManifestWeights(r *payloadReader, wantNodes uint32) error {
209+ if len(r.b) == 0 {
210+ return nil
211+ }
212+ if len(r.b) < len(manifestWeightTag) {
213+ return ErrShortPayload
214+ }
215+ if string(r.b[:len(manifestWeightTag)]) != manifestWeightTag {
216+ return fmt.Errorf("%w: weight extension tag", ErrInvalidData)
217+ }
218+ r.b = r.b[len(manifestWeightTag):]
219+ count, err := r.getUint32()
220+ if err != nil {
221+ return err
222+ }
223+ if count != wantNodes {
224+ return fmt.Errorf("%w: weight node count", ErrInvalidData)
225+ }
226+ if uint64(count) > uint64(len(r.b))/manifestWeightSize {
227+ return ErrShortPayload
228+ }
229+ if uint64(len(r.b)) != uint64(count)*manifestWeightSize {
230+ return ErrTrailingBytes
231+ }
232+ for range count {
233+ bits, err := r.getUint64()
234+ if err != nil {
235+ return err
236+ }
237+ if !validManifestWeight(math.Float64frombits(bits)) {
238+ return fmt.Errorf("%w: node weight", ErrInvalidData)
239+ }
240+ }
241+ return r.done()
242+}
243+
202244 func preflightManifestPane(r *payloadReader, budget *manifestPreflightBudget) (layout.PaneID, error) {
203245 id, err := preflightManifestString(r, budget)
204246 if err != nil {
@@ -2,6 +2,7 @@ package snapshot
2 2
3 import (3 import (
4 "fmt"4 "fmt"
5+ "math"
5 6
6 "github.com/bnema/vev/internal/usecase/layout"7 "github.com/bnema/vev/internal/usecase/layout"
7 )8 )
@@ -43,10 +44,13 @@ func preflightManifest(body []byte) error {
43 return err44 return err
44 }45 }
45 }46 }
46- return r.done()47+ return preflightManifestWeights(&r, budget.nodes)
47 }48 }
48 49
49-type manifestPreflightBudget struct{ allocation uint64 }50+type manifestPreflightBudget struct {
51+ allocation uint64
52+ nodes uint32
53+}
50 54
51 func (b *manifestPreflightBudget) add(n uint64) bool {55 func (b *manifestPreflightBudget) add(n uint64) bool {
52 if n > maxSnapshotDecodedAllocation-b.allocation {56 if n > maxSnapshotDecodedAllocation-b.allocation {
@@ -158,6 +162,7 @@ func preflightManifestNode(r *payloadReader, depth int, root bool, budget *manif
158 if err != nil {162 if err != nil {
159 return err163 return err
160 }164 }
165+ budget.nodes++
161 *refs = append(*refs, layout.PaneID(leaf))166 *refs = append(*refs, layout.PaneID(leaf))
162 return nil167 return nil
163 case manifestNodeSplit:168 case manifestNodeSplit:
@@ -184,6 +189,7 @@ func preflightManifestNode(r *payloadReader, depth int, root bool, budget *manif
184 default:189 default:
185 return fmt.Errorf("%w: node kind", ErrInvalidData)190 return fmt.Errorf("%w: node kind", ErrInvalidData)
186 }191 }
192+ budget.nodes++
187 n, err := r.getUint16()193 n, err := r.getUint16()
188 if err != nil {194 if err != nil {
189 return err195 return err
@@ -199,6 +205,42 @@ func preflightManifestNode(r *payloadReader, depth int, root bool, budget *manif
199 return nil205 return nil
200 }206 }
201 207
208+func preflightManifestWeights(r *payloadReader, wantNodes uint32) error {
209+ if len(r.b) == 0 {
210+ return nil
211+ }
212+ if len(r.b) < len(manifestWeightTag) {
213+ return ErrShortPayload
214+ }
215+ if string(r.b[:len(manifestWeightTag)]) != manifestWeightTag {
216+ return fmt.Errorf("%w: weight extension tag", ErrInvalidData)
217+ }
218+ r.b = r.b[len(manifestWeightTag):]
219+ count, err := r.getUint32()
220+ if err != nil {
221+ return err
222+ }
223+ if count != wantNodes {
224+ return fmt.Errorf("%w: weight node count", ErrInvalidData)
225+ }
226+ if uint64(count) > uint64(len(r.b))/manifestWeightSize {
227+ return ErrShortPayload
228+ }
229+ if uint64(len(r.b)) != uint64(count)*manifestWeightSize {
230+ return ErrTrailingBytes
231+ }
232+ for range count {
233+ bits, err := r.getUint64()
234+ if err != nil {
235+ return err
236+ }
237+ if !validManifestWeight(math.Float64frombits(bits)) {
238+ return fmt.Errorf("%w: node weight", ErrInvalidData)
239+ }
240+ }
241+ return r.done()
242+}
243+
202 func preflightManifestPane(r *payloadReader, budget *manifestPreflightBudget) (layout.PaneID, error) {244 func preflightManifestPane(r *payloadReader, budget *manifestPreflightBudget) (layout.PaneID, error) {
203 id, err := preflightManifestString(r, budget)245 id, err := preflightManifestString(r, budget)
204 if err != nil {246 if err != nil {
A internal/usecase/snapshot/weights_test.go +211 -0
@@ -0,0 +1,211 @@
1+package snapshot
2+
3+import (
4+ "bytes"
5+ "encoding/binary"
6+ "encoding/hex"
7+ "math"
8+ "testing"
9+
10+ "github.com/bnema/vev/internal/usecase/layout"
11+)
12+
13+func 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+
85+func TestSnapshotEnvelopeCompatibility(t *testing.T) {
86+ const (
87+ headGolden = "56455648000100000000002898ebcac200000000000000070100000000000000000000000000000000000000000000000000000000000000"
88+ objectGolden = "5645564f00010000000000111cb8f09f030000000c63616e6f6e6963616c2d7674"
89+ manifestGolden = "5645564d0001000000000122f8f8f0af000000000000000000056e616d6564000000000000000000000001000374616200500018000000000000000000036f6e65010000020000036f6e6500000374776f000200036f6e6500036f6e6500042f6f6e65000101010000000000000000000000000000000000000000000000000000000000000000000016020200000000000000000000000000000000000000000000000000000000000000000000160303000000000000000000000000000000000000000000000000000000000000000000001600000374776f000374776f00042f74776f0000020400000000000000000000000000000000000000000000000000000000000000000000160305000000000000000000000000000000000000000000000000000000000000000000001600"
90+ )
91+ if ManifestVersion != 1 {
92+ t.Fatalf("ManifestVersion = %d, want 1", 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(Visible, []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+
128+func 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+
192+func 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+
202+func 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+}
@@ -0,0 +1,211 @@
1+package snapshot
2+
3+import (
4+ "bytes"
5+ "encoding/binary"
6+ "encoding/hex"
7+ "math"
8+ "testing"
9+
10+ "github.com/bnema/vev/internal/usecase/layout"
11+)
12+
13+func 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+
85+func TestSnapshotEnvelopeCompatibility(t *testing.T) {
86+ const (
87+ headGolden = "56455648000100000000002898ebcac200000000000000070100000000000000000000000000000000000000000000000000000000000000"
88+ objectGolden = "5645564f00010000000000111cb8f09f030000000c63616e6f6e6963616c2d7674"
89+ manifestGolden = "5645564d0001000000000122f8f8f0af000000000000000000056e616d6564000000000000000000000001000374616200500018000000000000000000036f6e65010000020000036f6e6500000374776f000200036f6e6500036f6e6500042f6f6e65000101010000000000000000000000000000000000000000000000000000000000000000000016020200000000000000000000000000000000000000000000000000000000000000000000160303000000000000000000000000000000000000000000000000000000000000000000001600000374776f000374776f00042f74776f0000020400000000000000000000000000000000000000000000000000000000000000000000160305000000000000000000000000000000000000000000000000000000000000000000001600"
90+ )
91+ if ManifestVersion != 1 {
92+ t.Fatalf("ManifestVersion = %d, want 1", 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(Visible, []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+
128+func 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+
192+func 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+
202+func 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+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close