detached 3156ed0

refactor(snapshot): unify manifest validation

commit
3156ed0d288e2e3615a248c1ce683f688b9d99c7
author
brice · · 2026-07-21 19:43 UTC
parent
5c945b2
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +268 -79
M internal/usecase/snapshot/codec.go +12 -12
@@ -349,19 +349,19 @@ func readTab(r *payloadReader) (Tab, error) {
349349
350350 func writeNode(w *payloadWriter, n *layout.Node) error {
351351 if n == nil {
352- w.putUint8(3)
352+ w.putUint8(manifestNodeNil)
353353 return nil
354354 }
355355 switch n.Kind {
356356 case layout.Leaf:
357- w.putUint8(0)
357+ w.putUint8(manifestNodeLeaf)
358358 return w.putString(string(n.Leaf))
359359 case layout.Split:
360- w.putUint8(1)
360+ w.putUint8(manifestNodeSplit)
361361 w.putUint8(uint8(n.Dir))
362362 return writeChildren(w, n.Children)
363363 case layout.Stack:
364- w.putUint8(2)
364+ w.putUint8(manifestNodeStack)
365365 if err := w.putString(string(n.Expanded)); err != nil {
366366 return err
367367 }
@@ -390,13 +390,13 @@ func readNode(r *payloadReader) (*layout.Node, error) {
390390 return nil, err
391391 }
392392 switch kind {
393- case 0:
393+ case manifestNodeLeaf:
394394 leaf, err := r.getString()
395395 if err != nil {
396396 return nil, err
397397 }
398398 return layout.NewLeaf(layout.PaneID(leaf)), nil
399- case 1:
399+ case manifestNodeSplit:
400400 dir, err := r.getUint8()
401401 if err != nil {
402402 return nil, err
@@ -406,7 +406,7 @@ func readNode(r *payloadReader) (*layout.Node, error) {
406406 return nil, err
407407 }
408408 return &layout.Node{Kind: layout.Split, Dir: layout.SplitDir(dir), Children: children}, nil
409- case 2:
409+ case manifestNodeStack:
410410 exp, err := r.getString()
411411 if err != nil {
412412 return nil, err
@@ -416,7 +416,7 @@ func readNode(r *payloadReader) (*layout.Node, error) {
416416 return nil, err
417417 }
418418 return &layout.Node{Kind: layout.Stack, Expanded: layout.PaneID(exp), Children: children}, nil
419- case 3:
419+ case manifestNodeNil:
420420 return nil, nil
421421 default:
422422 return nil, fmt.Errorf("%w: unknown node kind", ErrInvalidData)
@@ -516,13 +516,13 @@ func readPane(r *payloadReader) (Pane, error) {
516516
517517 func writeProcess(w *payloadWriter, p *Process) error {
518518 if p == nil {
519- w.putUint8(0)
519+ w.putUint8(processAbsent)
520520 return nil
521521 }
522522 if len(p.Argv) == 0 {
523523 return fmt.Errorf("%w: process argv empty", ErrInvalidData)
524524 }
525- w.putUint8(1)
525+ w.putUint8(processPresent)
526526 if err := w.putStrings(p.Argv); err != nil {
527527 return err
528528 }
@@ -537,10 +537,10 @@ func readProcess(r *payloadReader) (*Process, error) {
537537 if err != nil {
538538 return nil, err
539539 }
540- if present == 0 {
540+ if present == processAbsent {
541541 return nil, nil
542542 }
543- if present != 1 {
543+ if present != processPresent {
544544 return nil, fmt.Errorf("%w: process presence", ErrInvalidData)
545545 }
546546 argv, err := r.getStrings()
@@ -349,19 +349,19 @@ func readTab(r *payloadReader) (Tab, error) {
349 349
350 func writeNode(w *payloadWriter, n *layout.Node) error {350 func writeNode(w *payloadWriter, n *layout.Node) error {
351 if n == nil {351 if n == nil {
352- w.putUint8(3)352+ w.putUint8(manifestNodeNil)
353 return nil353 return nil
354 }354 }
355 switch n.Kind {355 switch n.Kind {
356 case layout.Leaf:356 case layout.Leaf:
357- w.putUint8(0)357+ w.putUint8(manifestNodeLeaf)
358 return w.putString(string(n.Leaf))358 return w.putString(string(n.Leaf))
359 case layout.Split:359 case layout.Split:
360- w.putUint8(1)360+ w.putUint8(manifestNodeSplit)
361 w.putUint8(uint8(n.Dir))361 w.putUint8(uint8(n.Dir))
362 return writeChildren(w, n.Children)362 return writeChildren(w, n.Children)
363 case layout.Stack:363 case layout.Stack:
364- w.putUint8(2)364+ w.putUint8(manifestNodeStack)
365 if err := w.putString(string(n.Expanded)); err != nil {365 if err := w.putString(string(n.Expanded)); err != nil {
366 return err366 return err
367 }367 }
@@ -390,13 +390,13 @@ func readNode(r *payloadReader) (*layout.Node, error) {
390 return nil, err390 return nil, err
391 }391 }
392 switch kind {392 switch kind {
393- case 0:393+ case manifestNodeLeaf:
394 leaf, err := r.getString()394 leaf, err := r.getString()
395 if err != nil {395 if err != nil {
396 return nil, err396 return nil, err
397 }397 }
398 return layout.NewLeaf(layout.PaneID(leaf)), nil398 return layout.NewLeaf(layout.PaneID(leaf)), nil
399- case 1:399+ case manifestNodeSplit:
400 dir, err := r.getUint8()400 dir, err := r.getUint8()
401 if err != nil {401 if err != nil {
402 return nil, err402 return nil, err
@@ -406,7 +406,7 @@ func readNode(r *payloadReader) (*layout.Node, error) {
406 return nil, err406 return nil, err
407 }407 }
408 return &layout.Node{Kind: layout.Split, Dir: layout.SplitDir(dir), Children: children}, nil408 return &layout.Node{Kind: layout.Split, Dir: layout.SplitDir(dir), Children: children}, nil
409- case 2:409+ case manifestNodeStack:
410 exp, err := r.getString()410 exp, err := r.getString()
411 if err != nil {411 if err != nil {
412 return nil, err412 return nil, err
@@ -416,7 +416,7 @@ func readNode(r *payloadReader) (*layout.Node, error) {
416 return nil, err416 return nil, err
417 }417 }
418 return &layout.Node{Kind: layout.Stack, Expanded: layout.PaneID(exp), Children: children}, nil418 return &layout.Node{Kind: layout.Stack, Expanded: layout.PaneID(exp), Children: children}, nil
419- case 3:419+ case manifestNodeNil:
420 return nil, nil420 return nil, nil
421 default:421 default:
422 return nil, fmt.Errorf("%w: unknown node kind", ErrInvalidData)422 return nil, fmt.Errorf("%w: unknown node kind", ErrInvalidData)
@@ -516,13 +516,13 @@ func readPane(r *payloadReader) (Pane, error) {
516 516
517 func writeProcess(w *payloadWriter, p *Process) error {517 func writeProcess(w *payloadWriter, p *Process) error {
518 if p == nil {518 if p == nil {
519- w.putUint8(0)519+ w.putUint8(processAbsent)
520 return nil520 return nil
521 }521 }
522 if len(p.Argv) == 0 {522 if len(p.Argv) == 0 {
523 return fmt.Errorf("%w: process argv empty", ErrInvalidData)523 return fmt.Errorf("%w: process argv empty", ErrInvalidData)
524 }524 }
525- w.putUint8(1)525+ w.putUint8(processPresent)
526 if err := w.putStrings(p.Argv); err != nil {526 if err := w.putStrings(p.Argv); err != nil {
527 return err527 return err
528 }528 }
@@ -537,10 +537,10 @@ func readProcess(r *payloadReader) (*Process, error) {
537 if err != nil {537 if err != nil {
538 return nil, err538 return nil, err
539 }539 }
540- if present == 0 {540+ if present == processAbsent {
541 return nil, nil541 return nil, nil
542 }542 }
543- if present != 1 {543+ if present != processPresent {
544 return nil, fmt.Errorf("%w: process presence", ErrInvalidData)544 return nil, fmt.Errorf("%w: process presence", ErrInvalidData)
545 }545 }
546 argv, err := r.getStrings()546 argv, err := r.getStrings()
M internal/usecase/snapshot/manifest.go +44 -11
@@ -18,6 +18,16 @@ const (
1818 manifestHeaderSize = 16
1919 objectEnvelopeBodyPrefixSize = 1 + 4 // object kind and payload length
2020 minObjectEnvelopeSize = manifestHeaderSize + objectEnvelopeBodyPrefixSize + 1
21+ objectRefDigestSize = sha256.Size
22+ objectRefSizeFieldSize = 4
23+
24+ manifestNodeLeaf uint8 = 0
25+ manifestNodeSplit uint8 = 1
26+ manifestNodeStack uint8 = 2
27+ manifestNodeNil uint8 = 3
28+
29+ processAbsent uint8 = 0
30+ processPresent uint8 = 1
2131 // maxDecodedBodySize includes the object kind and payload-length prefix.
2232 maxObjectEnvelopeSize = manifestHeaderSize + maxDecodedBodySize
2333 maxObjectPayloadSize = maxDecodedBodySize - objectEnvelopeBodyPrefixSize
@@ -109,6 +119,12 @@ func UnmarshalManifest(encoded []byte) (Manifest, error) {
109119 if err := preflightManifest(body); err != nil {
110120 return Manifest{}, err
111121 }
122+ return decodeManifest(body)
123+}
124+
125+// decodeManifest decodes an already preflighted payload. Keeping this separate
126+// allows the preflight grammar to be differentially tested against decoding.
127+func decodeManifest(body []byte) (Manifest, error) {
112128 r := payloadReader{b: body}
113129 generation, err := r.getUint64()
114130 if err != nil {
@@ -365,19 +381,32 @@ func readManifestPane(r *payloadReader) (ManifestPane, error) {
365381 return pane, err
366382 }
367383 func readObjectRef(r *payloadReader) (ObjectRef, error) {
368- kind, err := r.getUint8()
384+ kind, digestBytes, size, err := parseObjectRef(r)
369385 if err != nil {
370386 return ObjectRef{}, err
371387 }
372- if len(r.b) < sha256.Size+4 {
373- return ObjectRef{}, ErrShortPayload
374- }
375388 var digest SnapshotDigest
376- copy(digest[:], r.b[:sha256.Size])
377- r.b = r.b[sha256.Size:]
378- size := binary.BigEndian.Uint32(r.b)
379- r.b = r.b[4:]
380- return ObjectRef{Kind: ObjectKind(kind), Digest: digest, Size: size}, nil
389+ copy(digest[:], digestBytes)
390+ return ObjectRef{Kind: kind, Digest: digest, Size: size}, nil
391+}
392+
393+// parseObjectRef advances r over an encoded object reference without copying
394+// its digest. Callers that retain the digest must copy it before reusing input.
395+func parseObjectRef(r *payloadReader) (ObjectKind, []byte, uint32, error) {
396+ kind, err := r.getUint8()
397+ if err != nil {
398+ return 0, nil, 0, err
399+ }
400+ if len(r.b) < objectRefDigestSize+objectRefSizeFieldSize {
401+ return 0, nil, 0, ErrShortPayload
402+ }
403+ digest := r.b[:objectRefDigestSize]
404+ r.b = r.b[objectRefDigestSize:]
405+ size, err := r.getUint32()
406+ if err != nil {
407+ return 0, nil, 0, err
408+ }
409+ return ObjectKind(kind), digest, size, nil
381410 }
382411
383412 func validateManifest(m Manifest) error {
@@ -428,7 +457,11 @@ func validatePaneRefs(p ManifestPane) error {
428457 return validateObjectRef(p.Visible, Visible)
429458 }
430459 func validateObjectRef(ref ObjectRef, want ObjectKind) error {
431- if ref.Kind != want || !validObjectEnvelopeSize(ref.Size) || isZeroDigest(ref.Digest) {
460+ return validateObjectRefFields(ref.Kind, ref.Digest[:], ref.Size, want)
461+}
462+
463+func validateObjectRefFields(kind ObjectKind, digest []byte, size uint32, want ObjectKind) error {
464+ if kind != want || !validObjectEnvelopeSize(size) || isZeroDigestBytes(digest) {
432465 return fmt.Errorf("%w: object reference", ErrInvalidData)
433466 }
434467 return nil
@@ -440,7 +473,7 @@ func validObjectEnvelopeSize(size uint32) bool {
440473 func validObjectKind(kind ObjectKind) bool {
441474 return kind == HistoryChunk || kind == HistoryTail || kind == Visible
442475 }
443-func isZeroDigest(d SnapshotDigest) bool {
476+func isZeroDigestBytes(d []byte) bool {
444477 for _, b := range d {
445478 if b != 0 {
446479 return false
@@ -18,6 +18,16 @@ const (
18 manifestHeaderSize = 1618 manifestHeaderSize = 16
19 objectEnvelopeBodyPrefixSize = 1 + 4 // object kind and payload length19 objectEnvelopeBodyPrefixSize = 1 + 4 // object kind and payload length
20 minObjectEnvelopeSize = manifestHeaderSize + objectEnvelopeBodyPrefixSize + 120 minObjectEnvelopeSize = manifestHeaderSize + objectEnvelopeBodyPrefixSize + 1
21+ objectRefDigestSize = sha256.Size
22+ objectRefSizeFieldSize = 4
23+
24+ manifestNodeLeaf uint8 = 0
25+ manifestNodeSplit uint8 = 1
26+ manifestNodeStack uint8 = 2
27+ manifestNodeNil uint8 = 3
28+
29+ processAbsent uint8 = 0
30+ processPresent uint8 = 1
21 // maxDecodedBodySize includes the object kind and payload-length prefix.31 // maxDecodedBodySize includes the object kind and payload-length prefix.
22 maxObjectEnvelopeSize = manifestHeaderSize + maxDecodedBodySize32 maxObjectEnvelopeSize = manifestHeaderSize + maxDecodedBodySize
23 maxObjectPayloadSize = maxDecodedBodySize - objectEnvelopeBodyPrefixSize33 maxObjectPayloadSize = maxDecodedBodySize - objectEnvelopeBodyPrefixSize
@@ -109,6 +119,12 @@ func UnmarshalManifest(encoded []byte) (Manifest, error) {
109 if err := preflightManifest(body); err != nil {119 if err := preflightManifest(body); err != nil {
110 return Manifest{}, err120 return Manifest{}, err
111 }121 }
122+ return decodeManifest(body)
123+}
124+
125+// decodeManifest decodes an already preflighted payload. Keeping this separate
126+// allows the preflight grammar to be differentially tested against decoding.
127+func decodeManifest(body []byte) (Manifest, error) {
112 r := payloadReader{b: body}128 r := payloadReader{b: body}
113 generation, err := r.getUint64()129 generation, err := r.getUint64()
114 if err != nil {130 if err != nil {
@@ -365,19 +381,32 @@ func readManifestPane(r *payloadReader) (ManifestPane, error) {
365 return pane, err381 return pane, err
366 }382 }
367 func readObjectRef(r *payloadReader) (ObjectRef, error) {383 func readObjectRef(r *payloadReader) (ObjectRef, error) {
368- kind, err := r.getUint8()384+ kind, digestBytes, size, err := parseObjectRef(r)
369 if err != nil {385 if err != nil {
370 return ObjectRef{}, err386 return ObjectRef{}, err
371 }387 }
372- if len(r.b) < sha256.Size+4 {
373- return ObjectRef{}, ErrShortPayload
374- }
375 var digest SnapshotDigest388 var digest SnapshotDigest
376- copy(digest[:], r.b[:sha256.Size])389+ copy(digest[:], digestBytes)
377- r.b = r.b[sha256.Size:]390+ return ObjectRef{Kind: kind, Digest: digest, Size: size}, nil
378- size := binary.BigEndian.Uint32(r.b)391+}
379- r.b = r.b[4:]392+
380- return ObjectRef{Kind: ObjectKind(kind), Digest: digest, Size: size}, nil393+// parseObjectRef advances r over an encoded object reference without copying
394+// its digest. Callers that retain the digest must copy it before reusing input.
395+func parseObjectRef(r *payloadReader) (ObjectKind, []byte, uint32, error) {
396+ kind, err := r.getUint8()
397+ if err != nil {
398+ return 0, nil, 0, err
399+ }
400+ if len(r.b) < objectRefDigestSize+objectRefSizeFieldSize {
401+ return 0, nil, 0, ErrShortPayload
402+ }
403+ digest := r.b[:objectRefDigestSize]
404+ r.b = r.b[objectRefDigestSize:]
405+ size, err := r.getUint32()
406+ if err != nil {
407+ return 0, nil, 0, err
408+ }
409+ return ObjectKind(kind), digest, size, nil
381 }410 }
382 411
383 func validateManifest(m Manifest) error {412 func validateManifest(m Manifest) error {
@@ -428,7 +457,11 @@ func validatePaneRefs(p ManifestPane) error {
428 return validateObjectRef(p.Visible, Visible)457 return validateObjectRef(p.Visible, Visible)
429 }458 }
430 func validateObjectRef(ref ObjectRef, want ObjectKind) error {459 func validateObjectRef(ref ObjectRef, want ObjectKind) error {
431- if ref.Kind != want || !validObjectEnvelopeSize(ref.Size) || isZeroDigest(ref.Digest) {460+ return validateObjectRefFields(ref.Kind, ref.Digest[:], ref.Size, want)
461+}
462+
463+func validateObjectRefFields(kind ObjectKind, digest []byte, size uint32, want ObjectKind) error {
464+ if kind != want || !validObjectEnvelopeSize(size) || isZeroDigestBytes(digest) {
432 return fmt.Errorf("%w: object reference", ErrInvalidData)465 return fmt.Errorf("%w: object reference", ErrInvalidData)
433 }466 }
434 return nil467 return nil
@@ -440,7 +473,7 @@ func validObjectEnvelopeSize(size uint32) bool {
440 func validObjectKind(kind ObjectKind) bool {473 func validObjectKind(kind ObjectKind) bool {
441 return kind == HistoryChunk || kind == HistoryTail || kind == Visible474 return kind == HistoryChunk || kind == HistoryTail || kind == Visible
442 }475 }
443-func isZeroDigest(d SnapshotDigest) bool {476+func isZeroDigestBytes(d []byte) bool {
444 for _, b := range d {477 for _, b := range d {
445 if b != 0 {478 if b != 0 {
446 return false479 return false
M internal/usecase/snapshot/manifest_preflight.go +41 -56
@@ -14,11 +14,11 @@ func preflightManifest(body []byte) error {
1414 if _, err := r.getUint64(); err != nil {
1515 return err
1616 }
17- name, err := preflightString(&r, &budget)
17+ name, err := preflightManifestStringBytes(&r, &budget)
1818 if err != nil {
1919 return err
2020 }
21- if name == "" {
21+ if len(name) == 0 {
2222 return fmt.Errorf("%w: manifest name", ErrInvalidData)
2323 }
2424 if _, err := r.getUint64(); err != nil {
@@ -56,24 +56,38 @@ func (b *manifestPreflightBudget) add(n uint64) bool {
5656 return true
5757 }
5858
59-func preflightString(r *payloadReader, budget *manifestPreflightBudget) (string, error) {
59+func preflightManifestStringBytes(r *payloadReader, budget *manifestPreflightBudget) ([]byte, error) {
6060 n, err := r.getUint16()
6161 if err != nil {
62- return "", err
62+ return nil, err
6363 }
6464 if uint64(n) > uint64(len(r.b)) {
65- return "", ErrShortPayload
65+ return nil, ErrShortPayload
6666 }
6767 if !budget.add(uint64(n) + 16) {
68- return "", fmt.Errorf("%w: string allocation", ErrInvalidData)
68+ return nil, fmt.Errorf("%w: string allocation", ErrInvalidData)
6969 }
70- s := string(r.b[:n])
70+ b := r.b[:n]
7171 r.b = r.b[n:]
72- return s, nil
72+ return b, nil
73+}
74+
75+// skipManifestString accounts for the decoded string without materializing it.
76+func skipManifestString(r *payloadReader, budget *manifestPreflightBudget) error {
77+ _, err := preflightManifestStringBytes(r, budget)
78+ return err
79+}
80+
81+func preflightManifestString(r *payloadReader, budget *manifestPreflightBudget) (string, error) {
82+ b, err := preflightManifestStringBytes(r, budget)
83+ if err != nil {
84+ return "", err
85+ }
86+ return string(b), nil
7387 }
7488
7589 func preflightManifestTab(r *payloadReader, budget *manifestPreflightBudget) error {
76- if _, err := preflightString(r, budget); err != nil {
90+ if err := skipManifestString(r, budget); err != nil {
7791 return err
7892 }
7993 cols, err := r.getUint16()
@@ -90,7 +104,7 @@ func preflightManifestTab(r *payloadReader, budget *manifestPreflightBudget) err
90104 if _, err := r.getUint64(); err != nil {
91105 return err
92106 }
93- focus, err := preflightString(r, budget)
107+ focus, err := preflightManifestString(r, budget)
94108 if err != nil {
95109 return err
96110 }
@@ -139,14 +153,14 @@ func preflightManifestNode(r *payloadReader, depth int, root bool, budget *manif
139153 return err
140154 }
141155 switch kind {
142- case 0:
143- leaf, err := preflightString(r, budget)
156+ case manifestNodeLeaf:
157+ leaf, err := preflightManifestString(r, budget)
144158 if err != nil {
145159 return err
146160 }
147161 *refs = append(*refs, layout.PaneID(leaf))
148162 return nil
149- case 1:
163+ case manifestNodeSplit:
150164 dir, err := r.getUint8()
151165 if err != nil {
152166 return err
@@ -154,15 +168,15 @@ func preflightManifestNode(r *payloadReader, depth int, root bool, budget *manif
154168 if layout.SplitDir(dir) != layout.Horizontal && layout.SplitDir(dir) != layout.Vertical {
155169 return fmt.Errorf("%w: split dir", ErrInvalidData)
156170 }
157- case 2:
158- expanded, err := preflightString(r, budget)
171+ case manifestNodeStack:
172+ expanded, err := preflightManifestString(r, budget)
159173 if err != nil {
160174 return err
161175 }
162176 if expanded != "" {
163177 *refs = append(*refs, layout.PaneID(expanded))
164178 }
165- case 3:
179+ case manifestNodeNil:
166180 if !root {
167181 return fmt.Errorf("%w: nil child node", ErrInvalidData)
168182 }
@@ -186,21 +200,21 @@ func preflightManifestNode(r *payloadReader, depth int, root bool, budget *manif
186200 }
187201
188202 func preflightManifestPane(r *payloadReader, budget *manifestPreflightBudget) (layout.PaneID, error) {
189- id, err := preflightString(r, budget)
203+ id, err := preflightManifestString(r, budget)
190204 if err != nil {
191205 return "", err
192206 }
193- if _, err := preflightString(r, budget); err != nil {
207+ if err := skipManifestString(r, budget); err != nil {
194208 return "", err
195209 }
196- if _, err := preflightString(r, budget); err != nil {
210+ if err := skipManifestString(r, budget); err != nil {
197211 return "", err
198212 }
199213 sealed, err := r.getUint16()
200214 if err != nil {
201215 return "", err
202216 }
203- if uint64(sealed) > uint64(len(r.b))/(sha256Size+5) || !budget.add(uint64(sealed)*48) {
217+ if uint64(sealed) > uint64(len(r.b))/(objectRefDigestSize+objectRefSizeFieldSize+1) || !budget.add(uint64(sealed)*48) {
204218 return "", fmt.Errorf("%w: sealed allocation", ErrInvalidData)
205219 }
206220 for range sealed {
@@ -220,50 +234,22 @@ func preflightManifestPane(r *payloadReader, budget *manifestPreflightBudget) (l
220234 return layout.PaneID(id), nil
221235 }
222236
223-const sha256Size = 32
224-
225237 func preflightObjectRef(r *payloadReader, want ObjectKind) error {
226- kind, err := r.getUint8()
227- if err != nil {
228- return err
229- }
230- if ObjectKind(kind) != want || len(r.b) < sha256Size+4 {
231- if len(r.b) < sha256Size+4 {
232- return ErrShortPayload
233- }
234- return fmt.Errorf("%w: object kind", ErrInvalidData)
235- }
236- digest := r.b[:sha256Size]
237- r.b = r.b[sha256Size:]
238- if isZeroDigestBytes(digest) {
239- return fmt.Errorf("%w: object digest", ErrInvalidData)
240- }
241- size, err := r.getUint32()
238+ kind, digest, size, err := parseObjectRef(r)
242239 if err != nil {
243240 return err
244241 }
245- if !validObjectEnvelopeSize(size) {
246- return fmt.Errorf("%w: object size", ErrInvalidData)
247- }
248- return nil
249-}
250-func isZeroDigestBytes(d []byte) bool {
251- for _, b := range d {
252- if b != 0 {
253- return false
254- }
255- }
256- return true
242+ return validateObjectRefFields(kind, digest, size, want)
257243 }
258244 func preflightManifestProcess(r *payloadReader, budget *manifestPreflightBudget) error {
259245 present, err := r.getUint8()
260246 if err != nil {
261247 return err
262248 }
263- if present == 0 {
249+ if present == processAbsent {
264250 return nil
265251 }
266- if present != 1 {
252+ if present != processPresent {
267253 return fmt.Errorf("%w: process presence", ErrInvalidData)
268254 }
269255 n, err := r.getUint16()
@@ -274,13 +260,12 @@ func preflightManifestProcess(r *payloadReader, budget *manifestPreflightBudget)
274260 return fmt.Errorf("%w: process argv", ErrInvalidData)
275261 }
276262 for range n {
277- if _, err := preflightString(r, budget); err != nil {
263+ if err := skipManifestString(r, budget); err != nil {
278264 return err
279265 }
280266 }
281- if _, err := preflightString(r, budget); err != nil {
267+ if err := skipManifestString(r, budget); err != nil {
282268 return err
283269 }
284- _, err = preflightString(r, budget)
285- return err
270+ return skipManifestString(r, budget)
286271 }
@@ -14,11 +14,11 @@ func preflightManifest(body []byte) error {
14 if _, err := r.getUint64(); err != nil {14 if _, err := r.getUint64(); err != nil {
15 return err15 return err
16 }16 }
17- name, err := preflightString(&r, &budget)17+ name, err := preflightManifestStringBytes(&r, &budget)
18 if err != nil {18 if err != nil {
19 return err19 return err
20 }20 }
21- if name == "" {21+ if len(name) == 0 {
22 return fmt.Errorf("%w: manifest name", ErrInvalidData)22 return fmt.Errorf("%w: manifest name", ErrInvalidData)
23 }23 }
24 if _, err := r.getUint64(); err != nil {24 if _, err := r.getUint64(); err != nil {
@@ -56,24 +56,38 @@ func (b *manifestPreflightBudget) add(n uint64) bool {
56 return true56 return true
57 }57 }
58 58
59-func preflightString(r *payloadReader, budget *manifestPreflightBudget) (string, error) {59+func preflightManifestStringBytes(r *payloadReader, budget *manifestPreflightBudget) ([]byte, error) {
60 n, err := r.getUint16()60 n, err := r.getUint16()
61 if err != nil {61 if err != nil {
62- return "", err62+ return nil, err
63 }63 }
64 if uint64(n) > uint64(len(r.b)) {64 if uint64(n) > uint64(len(r.b)) {
65- return "", ErrShortPayload65+ return nil, ErrShortPayload
66 }66 }
67 if !budget.add(uint64(n) + 16) {67 if !budget.add(uint64(n) + 16) {
68- return "", fmt.Errorf("%w: string allocation", ErrInvalidData)68+ return nil, fmt.Errorf("%w: string allocation", ErrInvalidData)
69 }69 }
70- s := string(r.b[:n])70+ b := r.b[:n]
71 r.b = r.b[n:]71 r.b = r.b[n:]
72- return s, nil72+ return b, nil
73+}
74+
75+// skipManifestString accounts for the decoded string without materializing it.
76+func skipManifestString(r *payloadReader, budget *manifestPreflightBudget) error {
77+ _, err := preflightManifestStringBytes(r, budget)
78+ return err
79+}
80+
81+func preflightManifestString(r *payloadReader, budget *manifestPreflightBudget) (string, error) {
82+ b, err := preflightManifestStringBytes(r, budget)
83+ if err != nil {
84+ return "", err
85+ }
86+ return string(b), nil
73 }87 }
74 88
75 func preflightManifestTab(r *payloadReader, budget *manifestPreflightBudget) error {89 func preflightManifestTab(r *payloadReader, budget *manifestPreflightBudget) error {
76- if _, err := preflightString(r, budget); err != nil {90+ if err := skipManifestString(r, budget); err != nil {
77 return err91 return err
78 }92 }
79 cols, err := r.getUint16()93 cols, err := r.getUint16()
@@ -90,7 +104,7 @@ func preflightManifestTab(r *payloadReader, budget *manifestPreflightBudget) err
90 if _, err := r.getUint64(); err != nil {104 if _, err := r.getUint64(); err != nil {
91 return err105 return err
92 }106 }
93- focus, err := preflightString(r, budget)107+ focus, err := preflightManifestString(r, budget)
94 if err != nil {108 if err != nil {
95 return err109 return err
96 }110 }
@@ -139,14 +153,14 @@ func preflightManifestNode(r *payloadReader, depth int, root bool, budget *manif
139 return err153 return err
140 }154 }
141 switch kind {155 switch kind {
142- case 0:156+ case manifestNodeLeaf:
143- leaf, err := preflightString(r, budget)157+ leaf, err := preflightManifestString(r, budget)
144 if err != nil {158 if err != nil {
145 return err159 return err
146 }160 }
147 *refs = append(*refs, layout.PaneID(leaf))161 *refs = append(*refs, layout.PaneID(leaf))
148 return nil162 return nil
149- case 1:163+ case manifestNodeSplit:
150 dir, err := r.getUint8()164 dir, err := r.getUint8()
151 if err != nil {165 if err != nil {
152 return err166 return err
@@ -154,15 +168,15 @@ func preflightManifestNode(r *payloadReader, depth int, root bool, budget *manif
154 if layout.SplitDir(dir) != layout.Horizontal && layout.SplitDir(dir) != layout.Vertical {168 if layout.SplitDir(dir) != layout.Horizontal && layout.SplitDir(dir) != layout.Vertical {
155 return fmt.Errorf("%w: split dir", ErrInvalidData)169 return fmt.Errorf("%w: split dir", ErrInvalidData)
156 }170 }
157- case 2:171+ case manifestNodeStack:
158- expanded, err := preflightString(r, budget)172+ expanded, err := preflightManifestString(r, budget)
159 if err != nil {173 if err != nil {
160 return err174 return err
161 }175 }
162 if expanded != "" {176 if expanded != "" {
163 *refs = append(*refs, layout.PaneID(expanded))177 *refs = append(*refs, layout.PaneID(expanded))
164 }178 }
165- case 3:179+ case manifestNodeNil:
166 if !root {180 if !root {
167 return fmt.Errorf("%w: nil child node", ErrInvalidData)181 return fmt.Errorf("%w: nil child node", ErrInvalidData)
168 }182 }
@@ -186,21 +200,21 @@ func preflightManifestNode(r *payloadReader, depth int, root bool, budget *manif
186 }200 }
187 201
188 func preflightManifestPane(r *payloadReader, budget *manifestPreflightBudget) (layout.PaneID, error) {202 func preflightManifestPane(r *payloadReader, budget *manifestPreflightBudget) (layout.PaneID, error) {
189- id, err := preflightString(r, budget)203+ id, err := preflightManifestString(r, budget)
190 if err != nil {204 if err != nil {
191 return "", err205 return "", err
192 }206 }
193- if _, err := preflightString(r, budget); err != nil {207+ if err := skipManifestString(r, budget); err != nil {
194 return "", err208 return "", err
195 }209 }
196- if _, err := preflightString(r, budget); err != nil {210+ if err := skipManifestString(r, budget); err != nil {
197 return "", err211 return "", err
198 }212 }
199 sealed, err := r.getUint16()213 sealed, err := r.getUint16()
200 if err != nil {214 if err != nil {
201 return "", err215 return "", err
202 }216 }
203- if uint64(sealed) > uint64(len(r.b))/(sha256Size+5) || !budget.add(uint64(sealed)*48) {217+ if uint64(sealed) > uint64(len(r.b))/(objectRefDigestSize+objectRefSizeFieldSize+1) || !budget.add(uint64(sealed)*48) {
204 return "", fmt.Errorf("%w: sealed allocation", ErrInvalidData)218 return "", fmt.Errorf("%w: sealed allocation", ErrInvalidData)
205 }219 }
206 for range sealed {220 for range sealed {
@@ -220,50 +234,22 @@ func preflightManifestPane(r *payloadReader, budget *manifestPreflightBudget) (l
220 return layout.PaneID(id), nil234 return layout.PaneID(id), nil
221 }235 }
222 236
223-const sha256Size = 32
224-
225 func preflightObjectRef(r *payloadReader, want ObjectKind) error {237 func preflightObjectRef(r *payloadReader, want ObjectKind) error {
226- kind, err := r.getUint8()238+ kind, digest, size, err := parseObjectRef(r)
227- if err != nil {
228- return err
229- }
230- if ObjectKind(kind) != want || len(r.b) < sha256Size+4 {
231- if len(r.b) < sha256Size+4 {
232- return ErrShortPayload
233- }
234- return fmt.Errorf("%w: object kind", ErrInvalidData)
235- }
236- digest := r.b[:sha256Size]
237- r.b = r.b[sha256Size:]
238- if isZeroDigestBytes(digest) {
239- return fmt.Errorf("%w: object digest", ErrInvalidData)
240- }
241- size, err := r.getUint32()
242 if err != nil {239 if err != nil {
243 return err240 return err
244 }241 }
245- if !validObjectEnvelopeSize(size) {242+ return validateObjectRefFields(kind, digest, size, want)
246- return fmt.Errorf("%w: object size", ErrInvalidData)
247- }
248- return nil
249-}
250-func isZeroDigestBytes(d []byte) bool {
251- for _, b := range d {
252- if b != 0 {
253- return false
254- }
255- }
256- return true
257 }243 }
258 func preflightManifestProcess(r *payloadReader, budget *manifestPreflightBudget) error {244 func preflightManifestProcess(r *payloadReader, budget *manifestPreflightBudget) error {
259 present, err := r.getUint8()245 present, err := r.getUint8()
260 if err != nil {246 if err != nil {
261 return err247 return err
262 }248 }
263- if present == 0 {249+ if present == processAbsent {
264 return nil250 return nil
265 }251 }
266- if present != 1 {252+ if present != processPresent {
267 return fmt.Errorf("%w: process presence", ErrInvalidData)253 return fmt.Errorf("%w: process presence", ErrInvalidData)
268 }254 }
269 n, err := r.getUint16()255 n, err := r.getUint16()
@@ -274,13 +260,12 @@ func preflightManifestProcess(r *payloadReader, budget *manifestPreflightBudget)
274 return fmt.Errorf("%w: process argv", ErrInvalidData)260 return fmt.Errorf("%w: process argv", ErrInvalidData)
275 }261 }
276 for range n {262 for range n {
277- if _, err := preflightString(r, budget); err != nil {263+ if err := skipManifestString(r, budget); err != nil {
278 return err264 return err
279 }265 }
280 }266 }
281- if _, err := preflightString(r, budget); err != nil {267+ if err := skipManifestString(r, budget); err != nil {
282 return err268 return err
283 }269 }
284- _, err = preflightString(r, budget)270+ return skipManifestString(r, budget)
285- return err
286 }271 }
A internal/usecase/snapshot/manifest_preflight_test.go +171 -0
@@ -0,0 +1,171 @@
1+package snapshot
2+
3+import (
4+ "encoding/binary"
5+ "errors"
6+ "hash/crc32"
7+ "testing"
8+
9+ "github.com/bnema/vev/internal/usecase/layout"
10+)
11+
12+func TestManifestGrammarRoundTrip(t *testing.T) {
13+ for _, tc := range []struct {
14+ name string
15+ tree *layout.Tree
16+ proc *Process
17+ }{
18+ {"split without process", &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("one"), layout.NewLeaf("two")}}}, nil},
19+ {"stack with process argv", &layout.Tree{Root: &layout.Node{Kind: layout.Stack, Expanded: "one", Children: []*layout.Node{layout.NewLeaf("one"), layout.NewLeaf("two")}}}, &Process{Argv: []string{"sh", "-c", "echo"}, Strategy: "restore", Opts: ProcessOpts{AgentSessionID: "agent"}}},
20+ } {
21+ t.Run(tc.name, func(t *testing.T) {
22+ encoded, err := MarshalManifest(testManifest(tc.tree, tc.proc))
23+ if err != nil {
24+ t.Fatal(err)
25+ }
26+ got, err := UnmarshalManifest(encoded)
27+ if err != nil {
28+ t.Fatal(err)
29+ }
30+ if got.Tabs[0].Tree.Root.Kind != tc.tree.Root.Kind || (got.Tabs[0].Panes[0].Process == nil) != (tc.proc == nil) {
31+ t.Fatalf("decoded tree/process = %#v/%#v", got.Tabs[0].Tree, got.Tabs[0].Panes[0].Process)
32+ }
33+ })
34+ }
35+}
36+
37+func TestManifestEnvelopePreflightParity(t *testing.T) {
38+ encoded, err := MarshalManifest(testManifest(layout.NewTree("one"), nil))
39+ if err != nil {
40+ t.Fatal(err)
41+ }
42+ mutations := []struct {
43+ name string
44+ edit func([]byte) []byte
45+ }{
46+ {"valid", func(b []byte) []byte { return b }},
47+ {"magic", func(b []byte) []byte { b[0] ^= 1; return b }},
48+ {"version", func(b []byte) []byte { binary.BigEndian.PutUint16(b[4:6], ManifestVersion+1); return b }},
49+ {"flags", func(b []byte) []byte { b[7] = 1; return b }},
50+ {"body length", func(b []byte) []byte { binary.BigEndian.PutUint32(b[8:12], uint32(len(b))); return b }},
51+ {"crc", func(b []byte) []byte { b[12] ^= 1; return b }},
52+ {"trailing", func(b []byte) []byte { return append(b, 0) }},
53+ {"body name", func(b []byte) []byte { b[26] ^= 1; binary.BigEndian.PutUint32(b[12:16], checksum(b[16:])); return b }},
54+ {"body generation", func(b []byte) []byte { b[16] ^= 1; binary.BigEndian.PutUint32(b[12:16], checksum(b[16:])); return b }},
55+ {"body node kind", func(b []byte) []byte { b[65] = 99; binary.BigEndian.PutUint32(b[12:16], checksum(b[16:])); return b }},
56+ }
57+ for _, tc := range mutations {
58+ t.Run(tc.name, func(t *testing.T) {
59+ candidate := tc.edit(append([]byte(nil), encoded...))
60+ _, decodeErr := UnmarshalManifest(candidate)
61+ preflightErr := preflightManifestEnvelope(candidate)
62+ uncheckedErr := decodeManifestEnvelope(candidate)
63+ if (decodeErr == nil) != (preflightErr == nil) || (preflightErr == nil) != (uncheckedErr == nil) {
64+ t.Fatalf("preflight error = %v, unchecked decode error = %v, decode error = %v", preflightErr, uncheckedErr, decodeErr)
65+ }
66+ })
67+ }
68+}
69+
70+func TestManifestObjectRefKinds(t *testing.T) {
71+ for _, tc := range []struct {
72+ name string
73+ got ObjectKind
74+ want ObjectKind
75+ valid bool
76+ }{
77+ {"chunk", HistoryChunk, HistoryChunk, true},
78+ {"tail", HistoryTail, HistoryTail, true},
79+ {"visible", Visible, Visible, true},
80+ {"invalid", ObjectKind(99), Visible, false},
81+ {"wrong role", HistoryTail, Visible, false},
82+ } {
83+ t.Run(tc.name, func(t *testing.T) {
84+ var w payloadWriter
85+ writeObjectRef(&w, ObjectRef{Kind: tc.got, Digest: SnapshotDigest{1}, Size: minObjectEnvelopeSize})
86+ err := preflightObjectRef(&payloadReader{b: w.b}, tc.want)
87+ if (err == nil) != tc.valid {
88+ t.Fatalf("preflightObjectRef() error = %v, valid=%v", err, tc.valid)
89+ }
90+ })
91+ }
92+}
93+
94+func TestManifestPreflightHostileDeclarations(t *testing.T) {
95+ t.Run("tab count", func(t *testing.T) {
96+ var w payloadWriter
97+ w.putUint64(1)
98+ _ = w.putString("name")
99+ w.putUint64(1)
100+ w.putUint16(0)
101+ w.putUint16(^uint16(0))
102+ if err := preflightManifest(w.b); !errors.Is(err, ErrInvalidData) {
103+ t.Fatalf("preflightManifest() error = %v", err)
104+ }
105+ })
106+ for _, tc := range []struct {
107+ name string
108+ fn func(*payloadReader, *manifestPreflightBudget) error
109+ body []byte
110+ }{
111+ {"string length", func(r *payloadReader, b *manifestPreflightBudget) error { return skipManifestString(r, b) }, []byte{0, 1}},
112+ {"tree children", func(r *payloadReader, b *manifestPreflightBudget) error {
113+ return preflightManifestNode(r, 0, true, b, nil)
114+ }, []byte{manifestNodeSplit, byte(layout.Horizontal), 0xff, 0xff}},
115+ {"nil child", func(r *payloadReader, b *manifestPreflightBudget) error {
116+ return preflightManifestNode(r, 1, false, b, nil)
117+ }, []byte{manifestNodeNil}},
118+ {"process declaration", func(r *payloadReader, b *manifestPreflightBudget) error { return preflightManifestProcess(r, b) }, []byte{processPresent, 0, 1, 0, 2, 'x'}},
119+ } {
120+ t.Run(tc.name, func(t *testing.T) {
121+ err := tc.fn(&payloadReader{b: tc.body}, &manifestPreflightBudget{})
122+ if err == nil {
123+ t.Fatal("hostile declaration accepted")
124+ }
125+ })
126+ }
127+}
128+
129+func TestSkipManifestStringDoesNotAllocate(t *testing.T) {
130+ raw := make([]byte, 2+1024)
131+ binary.BigEndian.PutUint16(raw, 1024)
132+ if allocations := testing.AllocsPerRun(100, func() {
133+ r := payloadReader{b: raw}
134+ if err := skipManifestString(&r, &manifestPreflightBudget{}); err != nil {
135+ t.Fatal(err)
136+ }
137+ }); allocations != 0 {
138+ t.Fatalf("skipManifestString allocations = %v, want 0", allocations)
139+ }
140+}
141+
142+func preflightManifestEnvelope(encoded []byte) error {
143+ body, err := unmarshalManifestEnvelope(encoded, manifestMagic)
144+ if err != nil {
145+ return err
146+ }
147+ return preflightManifest(body)
148+}
149+
150+func decodeManifestEnvelope(encoded []byte) error {
151+ body, err := unmarshalManifestEnvelope(encoded, manifestMagic)
152+ if err != nil {
153+ return err
154+ }
155+ _, err = decodeManifest(body)
156+ return err
157+}
158+
159+func checksum(body []byte) uint32 {
160+ return crc32.ChecksumIEEE(body)
161+}
162+
163+func testManifest(tree *layout.Tree, process *Process) Manifest {
164+ return Manifest{Name: "named", Tabs: []ManifestTab{{
165+ StableID: "tab", Cols: 80, Rows: 24, Focus: "one", Tree: tree,
166+ Panes: []ManifestPane{
167+ {ID: "one", StableID: "one", Cwd: "/one", Sealed: []ObjectRef{{Kind: HistoryChunk, Digest: SnapshotDigest{1}, Size: minObjectEnvelopeSize}}, Tail: ObjectRef{Kind: HistoryTail, Digest: SnapshotDigest{2}, Size: minObjectEnvelopeSize}, Visible: ObjectRef{Kind: Visible, Digest: SnapshotDigest{3}, Size: minObjectEnvelopeSize}, Process: process},
168+ {ID: "two", StableID: "two", Cwd: "/two", Tail: ObjectRef{Kind: HistoryTail, Digest: SnapshotDigest{4}, Size: minObjectEnvelopeSize}, Visible: ObjectRef{Kind: Visible, Digest: SnapshotDigest{5}, Size: minObjectEnvelopeSize}},
169+ },
170+ }}}
171+}
@@ -0,0 +1,171 @@
1+package snapshot
2+
3+import (
4+ "encoding/binary"
5+ "errors"
6+ "hash/crc32"
7+ "testing"
8+
9+ "github.com/bnema/vev/internal/usecase/layout"
10+)
11+
12+func TestManifestGrammarRoundTrip(t *testing.T) {
13+ for _, tc := range []struct {
14+ name string
15+ tree *layout.Tree
16+ proc *Process
17+ }{
18+ {"split without process", &layout.Tree{Root: &layout.Node{Kind: layout.Split, Dir: layout.Horizontal, Children: []*layout.Node{layout.NewLeaf("one"), layout.NewLeaf("two")}}}, nil},
19+ {"stack with process argv", &layout.Tree{Root: &layout.Node{Kind: layout.Stack, Expanded: "one", Children: []*layout.Node{layout.NewLeaf("one"), layout.NewLeaf("two")}}}, &Process{Argv: []string{"sh", "-c", "echo"}, Strategy: "restore", Opts: ProcessOpts{AgentSessionID: "agent"}}},
20+ } {
21+ t.Run(tc.name, func(t *testing.T) {
22+ encoded, err := MarshalManifest(testManifest(tc.tree, tc.proc))
23+ if err != nil {
24+ t.Fatal(err)
25+ }
26+ got, err := UnmarshalManifest(encoded)
27+ if err != nil {
28+ t.Fatal(err)
29+ }
30+ if got.Tabs[0].Tree.Root.Kind != tc.tree.Root.Kind || (got.Tabs[0].Panes[0].Process == nil) != (tc.proc == nil) {
31+ t.Fatalf("decoded tree/process = %#v/%#v", got.Tabs[0].Tree, got.Tabs[0].Panes[0].Process)
32+ }
33+ })
34+ }
35+}
36+
37+func TestManifestEnvelopePreflightParity(t *testing.T) {
38+ encoded, err := MarshalManifest(testManifest(layout.NewTree("one"), nil))
39+ if err != nil {
40+ t.Fatal(err)
41+ }
42+ mutations := []struct {
43+ name string
44+ edit func([]byte) []byte
45+ }{
46+ {"valid", func(b []byte) []byte { return b }},
47+ {"magic", func(b []byte) []byte { b[0] ^= 1; return b }},
48+ {"version", func(b []byte) []byte { binary.BigEndian.PutUint16(b[4:6], ManifestVersion+1); return b }},
49+ {"flags", func(b []byte) []byte { b[7] = 1; return b }},
50+ {"body length", func(b []byte) []byte { binary.BigEndian.PutUint32(b[8:12], uint32(len(b))); return b }},
51+ {"crc", func(b []byte) []byte { b[12] ^= 1; return b }},
52+ {"trailing", func(b []byte) []byte { return append(b, 0) }},
53+ {"body name", func(b []byte) []byte { b[26] ^= 1; binary.BigEndian.PutUint32(b[12:16], checksum(b[16:])); return b }},
54+ {"body generation", func(b []byte) []byte { b[16] ^= 1; binary.BigEndian.PutUint32(b[12:16], checksum(b[16:])); return b }},
55+ {"body node kind", func(b []byte) []byte { b[65] = 99; binary.BigEndian.PutUint32(b[12:16], checksum(b[16:])); return b }},
56+ }
57+ for _, tc := range mutations {
58+ t.Run(tc.name, func(t *testing.T) {
59+ candidate := tc.edit(append([]byte(nil), encoded...))
60+ _, decodeErr := UnmarshalManifest(candidate)
61+ preflightErr := preflightManifestEnvelope(candidate)
62+ uncheckedErr := decodeManifestEnvelope(candidate)
63+ if (decodeErr == nil) != (preflightErr == nil) || (preflightErr == nil) != (uncheckedErr == nil) {
64+ t.Fatalf("preflight error = %v, unchecked decode error = %v, decode error = %v", preflightErr, uncheckedErr, decodeErr)
65+ }
66+ })
67+ }
68+}
69+
70+func TestManifestObjectRefKinds(t *testing.T) {
71+ for _, tc := range []struct {
72+ name string
73+ got ObjectKind
74+ want ObjectKind
75+ valid bool
76+ }{
77+ {"chunk", HistoryChunk, HistoryChunk, true},
78+ {"tail", HistoryTail, HistoryTail, true},
79+ {"visible", Visible, Visible, true},
80+ {"invalid", ObjectKind(99), Visible, false},
81+ {"wrong role", HistoryTail, Visible, false},
82+ } {
83+ t.Run(tc.name, func(t *testing.T) {
84+ var w payloadWriter
85+ writeObjectRef(&w, ObjectRef{Kind: tc.got, Digest: SnapshotDigest{1}, Size: minObjectEnvelopeSize})
86+ err := preflightObjectRef(&payloadReader{b: w.b}, tc.want)
87+ if (err == nil) != tc.valid {
88+ t.Fatalf("preflightObjectRef() error = %v, valid=%v", err, tc.valid)
89+ }
90+ })
91+ }
92+}
93+
94+func TestManifestPreflightHostileDeclarations(t *testing.T) {
95+ t.Run("tab count", func(t *testing.T) {
96+ var w payloadWriter
97+ w.putUint64(1)
98+ _ = w.putString("name")
99+ w.putUint64(1)
100+ w.putUint16(0)
101+ w.putUint16(^uint16(0))
102+ if err := preflightManifest(w.b); !errors.Is(err, ErrInvalidData) {
103+ t.Fatalf("preflightManifest() error = %v", err)
104+ }
105+ })
106+ for _, tc := range []struct {
107+ name string
108+ fn func(*payloadReader, *manifestPreflightBudget) error
109+ body []byte
110+ }{
111+ {"string length", func(r *payloadReader, b *manifestPreflightBudget) error { return skipManifestString(r, b) }, []byte{0, 1}},
112+ {"tree children", func(r *payloadReader, b *manifestPreflightBudget) error {
113+ return preflightManifestNode(r, 0, true, b, nil)
114+ }, []byte{manifestNodeSplit, byte(layout.Horizontal), 0xff, 0xff}},
115+ {"nil child", func(r *payloadReader, b *manifestPreflightBudget) error {
116+ return preflightManifestNode(r, 1, false, b, nil)
117+ }, []byte{manifestNodeNil}},
118+ {"process declaration", func(r *payloadReader, b *manifestPreflightBudget) error { return preflightManifestProcess(r, b) }, []byte{processPresent, 0, 1, 0, 2, 'x'}},
119+ } {
120+ t.Run(tc.name, func(t *testing.T) {
121+ err := tc.fn(&payloadReader{b: tc.body}, &manifestPreflightBudget{})
122+ if err == nil {
123+ t.Fatal("hostile declaration accepted")
124+ }
125+ })
126+ }
127+}
128+
129+func TestSkipManifestStringDoesNotAllocate(t *testing.T) {
130+ raw := make([]byte, 2+1024)
131+ binary.BigEndian.PutUint16(raw, 1024)
132+ if allocations := testing.AllocsPerRun(100, func() {
133+ r := payloadReader{b: raw}
134+ if err := skipManifestString(&r, &manifestPreflightBudget{}); err != nil {
135+ t.Fatal(err)
136+ }
137+ }); allocations != 0 {
138+ t.Fatalf("skipManifestString allocations = %v, want 0", allocations)
139+ }
140+}
141+
142+func preflightManifestEnvelope(encoded []byte) error {
143+ body, err := unmarshalManifestEnvelope(encoded, manifestMagic)
144+ if err != nil {
145+ return err
146+ }
147+ return preflightManifest(body)
148+}
149+
150+func decodeManifestEnvelope(encoded []byte) error {
151+ body, err := unmarshalManifestEnvelope(encoded, manifestMagic)
152+ if err != nil {
153+ return err
154+ }
155+ _, err = decodeManifest(body)
156+ return err
157+}
158+
159+func checksum(body []byte) uint32 {
160+ return crc32.ChecksumIEEE(body)
161+}
162+
163+func testManifest(tree *layout.Tree, process *Process) Manifest {
164+ return Manifest{Name: "named", Tabs: []ManifestTab{{
165+ StableID: "tab", Cols: 80, Rows: 24, Focus: "one", Tree: tree,
166+ Panes: []ManifestPane{
167+ {ID: "one", StableID: "one", Cwd: "/one", Sealed: []ObjectRef{{Kind: HistoryChunk, Digest: SnapshotDigest{1}, Size: minObjectEnvelopeSize}}, Tail: ObjectRef{Kind: HistoryTail, Digest: SnapshotDigest{2}, Size: minObjectEnvelopeSize}, Visible: ObjectRef{Kind: Visible, Digest: SnapshotDigest{3}, Size: minObjectEnvelopeSize}, Process: process},
168+ {ID: "two", StableID: "two", Cwd: "/two", Tail: ObjectRef{Kind: HistoryTail, Digest: SnapshotDigest{4}, Size: minObjectEnvelopeSize}, Visible: ObjectRef{Kind: Visible, Digest: SnapshotDigest{5}, Size: minObjectEnvelopeSize}},
169+ },
170+ }}}
171+}

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