refactor(snapshot): unify manifest validation
- commit
- 3156ed0d288e2e3615a248c1ce683f688b9d99c7
- author
- brice · · 2026-07-21 19:43 UTC
- parent
- 5c945b2
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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) { | |||
| 349 | 349 | ||
| 350 | 350 | func writeNode(w *payloadWriter, n *layout.Node) error { | |
| 351 | 351 | if n == nil { | |
| 352 | - | w.putUint8(3) | |
| 352 | + | w.putUint8(manifestNodeNil) | |
| 353 | 353 | return nil | |
| 354 | 354 | } | |
| 355 | 355 | switch n.Kind { | |
| 356 | 356 | case layout.Leaf: | |
| 357 | - | w.putUint8(0) | |
| 357 | + | w.putUint8(manifestNodeLeaf) | |
| 358 | 358 | return w.putString(string(n.Leaf)) | |
| 359 | 359 | case layout.Split: | |
| 360 | - | w.putUint8(1) | |
| 360 | + | w.putUint8(manifestNodeSplit) | |
| 361 | 361 | w.putUint8(uint8(n.Dir)) | |
| 362 | 362 | return writeChildren(w, n.Children) | |
| 363 | 363 | case layout.Stack: | |
| 364 | - | w.putUint8(2) | |
| 364 | + | w.putUint8(manifestNodeStack) | |
| 365 | 365 | if err := w.putString(string(n.Expanded)); err != nil { | |
| 366 | 366 | return err | |
| 367 | 367 | } | |
| @@ -390,13 +390,13 @@ func readNode(r *payloadReader) (*layout.Node, error) { | |||
| 390 | 390 | return nil, err | |
| 391 | 391 | } | |
| 392 | 392 | switch kind { | |
| 393 | - | case 0: | |
| 393 | + | case manifestNodeLeaf: | |
| 394 | 394 | leaf, err := r.getString() | |
| 395 | 395 | if err != nil { | |
| 396 | 396 | return nil, err | |
| 397 | 397 | } | |
| 398 | 398 | return layout.NewLeaf(layout.PaneID(leaf)), nil | |
| 399 | - | case 1: | |
| 399 | + | case manifestNodeSplit: | |
| 400 | 400 | dir, err := r.getUint8() | |
| 401 | 401 | if err != nil { | |
| 402 | 402 | return nil, err | |
| @@ -406,7 +406,7 @@ func readNode(r *payloadReader) (*layout.Node, error) { | |||
| 406 | 406 | return nil, err | |
| 407 | 407 | } | |
| 408 | 408 | return &layout.Node{Kind: layout.Split, Dir: layout.SplitDir(dir), Children: children}, nil | |
| 409 | - | case 2: | |
| 409 | + | case manifestNodeStack: | |
| 410 | 410 | exp, err := r.getString() | |
| 411 | 411 | if err != nil { | |
| 412 | 412 | return nil, err | |
| @@ -416,7 +416,7 @@ func readNode(r *payloadReader) (*layout.Node, error) { | |||
| 416 | 416 | return nil, err | |
| 417 | 417 | } | |
| 418 | 418 | return &layout.Node{Kind: layout.Stack, Expanded: layout.PaneID(exp), Children: children}, nil | |
| 419 | - | case 3: | |
| 419 | + | case manifestNodeNil: | |
| 420 | 420 | return nil, nil | |
| 421 | 421 | default: | |
| 422 | 422 | return nil, fmt.Errorf("%w: unknown node kind", ErrInvalidData) | |
| @@ -516,13 +516,13 @@ func readPane(r *payloadReader) (Pane, error) { | |||
| 516 | 516 | ||
| 517 | 517 | func writeProcess(w *payloadWriter, p *Process) error { | |
| 518 | 518 | if p == nil { | |
| 519 | - | w.putUint8(0) | |
| 519 | + | w.putUint8(processAbsent) | |
| 520 | 520 | return nil | |
| 521 | 521 | } | |
| 522 | 522 | if len(p.Argv) == 0 { | |
| 523 | 523 | return fmt.Errorf("%w: process argv empty", ErrInvalidData) | |
| 524 | 524 | } | |
| 525 | - | w.putUint8(1) | |
| 525 | + | w.putUint8(processPresent) | |
| 526 | 526 | if err := w.putStrings(p.Argv); err != nil { | |
| 527 | 527 | return err | |
| 528 | 528 | } | |
| @@ -537,10 +537,10 @@ func readProcess(r *payloadReader) (*Process, error) { | |||
| 537 | 537 | if err != nil { | |
| 538 | 538 | return nil, err | |
| 539 | 539 | } | |
| 540 | - | if present == 0 { | |
| 540 | + | if present == processAbsent { | |
| 541 | 541 | return nil, nil | |
| 542 | 542 | } | |
| 543 | - | if present != 1 { | |
| 543 | + | if present != processPresent { | |
| 544 | 544 | return nil, fmt.Errorf("%w: process presence", ErrInvalidData) | |
| 545 | 545 | } | |
| 546 | 546 | 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 nil | 353 | 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 err | 366 | return err | ||
| 367 | } | 367 | } | ||
| @@ -390,13 +390,13 @@ func readNode(r *payloadReader) (*layout.Node, error) { | |||||
| 390 | return nil, err | 390 | 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, err | 396 | return nil, err | ||
| 397 | } | 397 | } | ||
| 398 | return layout.NewLeaf(layout.PaneID(leaf)), nil | 398 | 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, err | 402 | return nil, err | ||
| @@ -406,7 +406,7 @@ func readNode(r *payloadReader) (*layout.Node, error) { | |||||
| 406 | return nil, err | 406 | return nil, err | ||
| 407 | } | 407 | } | ||
| 408 | return &layout.Node{Kind: layout.Split, Dir: layout.SplitDir(dir), Children: children}, nil | 408 | 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, err | 412 | return nil, err | ||
| @@ -416,7 +416,7 @@ func readNode(r *payloadReader) (*layout.Node, error) { | |||||
| 416 | return nil, err | 416 | return nil, err | ||
| 417 | } | 417 | } | ||
| 418 | return &layout.Node{Kind: layout.Stack, Expanded: layout.PaneID(exp), Children: children}, nil | 418 | return &layout.Node{Kind: layout.Stack, Expanded: layout.PaneID(exp), Children: children}, nil | ||
| 419 | - | case 3: | 419 | + | case manifestNodeNil: |
| 420 | return nil, nil | 420 | 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 nil | 520 | 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 err | 527 | 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, err | 538 | return nil, err | ||
| 539 | } | 539 | } | ||
| 540 | - | if present == 0 { | 540 | + | if present == processAbsent { |
| 541 | return nil, nil | 541 | 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 ( | |||
| 18 | 18 | manifestHeaderSize = 16 | |
| 19 | 19 | objectEnvelopeBodyPrefixSize = 1 + 4 // object kind and payload length | |
| 20 | 20 | 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 | 31 | // maxDecodedBodySize includes the object kind and payload-length prefix. | |
| 22 | 32 | maxObjectEnvelopeSize = manifestHeaderSize + maxDecodedBodySize | |
| 23 | 33 | maxObjectPayloadSize = maxDecodedBodySize - objectEnvelopeBodyPrefixSize | |
| @@ -109,6 +119,12 @@ func UnmarshalManifest(encoded []byte) (Manifest, error) { | |||
| 109 | 119 | if err := preflightManifest(body); err != nil { | |
| 110 | 120 | 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 | 128 | r := payloadReader{b: body} | |
| 113 | 129 | generation, err := r.getUint64() | |
| 114 | 130 | if err != nil { | |
| @@ -365,19 +381,32 @@ func readManifestPane(r *payloadReader) (ManifestPane, error) { | |||
| 365 | 381 | return pane, err | |
| 366 | 382 | } | |
| 367 | 383 | func readObjectRef(r *payloadReader) (ObjectRef, error) { | |
| 368 | - | kind, err := r.getUint8() | |
| 384 | + | kind, digestBytes, size, err := parseObjectRef(r) | |
| 369 | 385 | if err != nil { | |
| 370 | 386 | return ObjectRef{}, err | |
| 371 | 387 | } | |
| 372 | - | if len(r.b) < sha256.Size+4 { | |
| 373 | - | return ObjectRef{}, ErrShortPayload | |
| 374 | - | } | |
| 375 | 388 | 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 | |
| 381 | 410 | } | |
| 382 | 411 | ||
| 383 | 412 | func validateManifest(m Manifest) error { | |
| @@ -428,7 +457,11 @@ func validatePaneRefs(p ManifestPane) error { | |||
| 428 | 457 | return validateObjectRef(p.Visible, Visible) | |
| 429 | 458 | } | |
| 430 | 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 | 465 | return fmt.Errorf("%w: object reference", ErrInvalidData) | |
| 433 | 466 | } | |
| 434 | 467 | return nil | |
| @@ -440,7 +473,7 @@ func validObjectEnvelopeSize(size uint32) bool { | |||
| 440 | 473 | func validObjectKind(kind ObjectKind) bool { | |
| 441 | 474 | return kind == HistoryChunk || kind == HistoryTail || kind == Visible | |
| 442 | 475 | } | |
| 443 | - | func isZeroDigest(d SnapshotDigest) bool { | |
| 476 | + | func isZeroDigestBytes(d []byte) bool { | |
| 444 | 477 | for _, b := range d { | |
| 445 | 478 | if b != 0 { | |
| 446 | 479 | return false | |
| @@ -18,6 +18,16 @@ const ( | |||||
| 18 | manifestHeaderSize = 16 | 18 | manifestHeaderSize = 16 | ||
| 19 | objectEnvelopeBodyPrefixSize = 1 + 4 // object kind and payload length | 19 | objectEnvelopeBodyPrefixSize = 1 + 4 // object kind and payload length | ||
| 20 | minObjectEnvelopeSize = manifestHeaderSize + objectEnvelopeBodyPrefixSize + 1 | 20 | 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 + maxDecodedBodySize | 32 | maxObjectEnvelopeSize = manifestHeaderSize + maxDecodedBodySize | ||
| 23 | maxObjectPayloadSize = maxDecodedBodySize - objectEnvelopeBodyPrefixSize | 33 | 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{}, err | 120 | 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, err | 381 | 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{}, err | 386 | return ObjectRef{}, err | ||
| 371 | } | 387 | } | ||
| 372 | - | if len(r.b) < sha256.Size+4 { | |||
| 373 | - | return ObjectRef{}, ErrShortPayload | |||
| 374 | - | } | |||
| 375 | var digest SnapshotDigest | 388 | 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}, nil | 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 | |||
| 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 nil | 467 | 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 == Visible | 474 | 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 false | 479 | return false | ||
M internal/usecase/snapshot/manifest_preflight.go +41 -56
| @@ -14,11 +14,11 @@ func preflightManifest(body []byte) error { | |||
| 14 | 14 | if _, err := r.getUint64(); err != nil { | |
| 15 | 15 | return err | |
| 16 | 16 | } | |
| 17 | - | name, err := preflightString(&r, &budget) | |
| 17 | + | name, err := preflightManifestStringBytes(&r, &budget) | |
| 18 | 18 | if err != nil { | |
| 19 | 19 | return err | |
| 20 | 20 | } | |
| 21 | - | if name == "" { | |
| 21 | + | if len(name) == 0 { | |
| 22 | 22 | return fmt.Errorf("%w: manifest name", ErrInvalidData) | |
| 23 | 23 | } | |
| 24 | 24 | if _, err := r.getUint64(); err != nil { | |
| @@ -56,24 +56,38 @@ func (b *manifestPreflightBudget) add(n uint64) bool { | |||
| 56 | 56 | 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 | 60 | n, err := r.getUint16() | |
| 61 | 61 | if err != nil { | |
| 62 | - | return "", err | |
| 62 | + | return nil, err | |
| 63 | 63 | } | |
| 64 | 64 | if uint64(n) > uint64(len(r.b)) { | |
| 65 | - | return "", ErrShortPayload | |
| 65 | + | return nil, ErrShortPayload | |
| 66 | 66 | } | |
| 67 | 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 | 71 | 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 | |
| 73 | 87 | } | |
| 74 | 88 | ||
| 75 | 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 | 91 | return err | |
| 78 | 92 | } | |
| 79 | 93 | cols, err := r.getUint16() | |
| @@ -90,7 +104,7 @@ func preflightManifestTab(r *payloadReader, budget *manifestPreflightBudget) err | |||
| 90 | 104 | if _, err := r.getUint64(); err != nil { | |
| 91 | 105 | return err | |
| 92 | 106 | } | |
| 93 | - | focus, err := preflightString(r, budget) | |
| 107 | + | focus, err := preflightManifestString(r, budget) | |
| 94 | 108 | if err != nil { | |
| 95 | 109 | return err | |
| 96 | 110 | } | |
| @@ -139,14 +153,14 @@ func preflightManifestNode(r *payloadReader, depth int, root bool, budget *manif | |||
| 139 | 153 | return err | |
| 140 | 154 | } | |
| 141 | 155 | switch kind { | |
| 142 | - | case 0: | |
| 143 | - | leaf, err := preflightString(r, budget) | |
| 156 | + | case manifestNodeLeaf: | |
| 157 | + | leaf, err := preflightManifestString(r, budget) | |
| 144 | 158 | if err != nil { | |
| 145 | 159 | return err | |
| 146 | 160 | } | |
| 147 | 161 | *refs = append(*refs, layout.PaneID(leaf)) | |
| 148 | 162 | return nil | |
| 149 | - | case 1: | |
| 163 | + | case manifestNodeSplit: | |
| 150 | 164 | dir, err := r.getUint8() | |
| 151 | 165 | if err != nil { | |
| 152 | 166 | return err | |
| @@ -154,15 +168,15 @@ func preflightManifestNode(r *payloadReader, depth int, root bool, budget *manif | |||
| 154 | 168 | if layout.SplitDir(dir) != layout.Horizontal && layout.SplitDir(dir) != layout.Vertical { | |
| 155 | 169 | return fmt.Errorf("%w: split dir", ErrInvalidData) | |
| 156 | 170 | } | |
| 157 | - | case 2: | |
| 158 | - | expanded, err := preflightString(r, budget) | |
| 171 | + | case manifestNodeStack: | |
| 172 | + | expanded, err := preflightManifestString(r, budget) | |
| 159 | 173 | if err != nil { | |
| 160 | 174 | return err | |
| 161 | 175 | } | |
| 162 | 176 | if expanded != "" { | |
| 163 | 177 | *refs = append(*refs, layout.PaneID(expanded)) | |
| 164 | 178 | } | |
| 165 | - | case 3: | |
| 179 | + | case manifestNodeNil: | |
| 166 | 180 | if !root { | |
| 167 | 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 | 202 | func preflightManifestPane(r *payloadReader, budget *manifestPreflightBudget) (layout.PaneID, error) { | |
| 189 | - | id, err := preflightString(r, budget) | |
| 203 | + | id, err := preflightManifestString(r, budget) | |
| 190 | 204 | if err != nil { | |
| 191 | 205 | return "", err | |
| 192 | 206 | } | |
| 193 | - | if _, err := preflightString(r, budget); err != nil { | |
| 207 | + | if err := skipManifestString(r, budget); err != nil { | |
| 194 | 208 | return "", err | |
| 195 | 209 | } | |
| 196 | - | if _, err := preflightString(r, budget); err != nil { | |
| 210 | + | if err := skipManifestString(r, budget); err != nil { | |
| 197 | 211 | return "", err | |
| 198 | 212 | } | |
| 199 | 213 | sealed, err := r.getUint16() | |
| 200 | 214 | if err != nil { | |
| 201 | 215 | 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 | 218 | return "", fmt.Errorf("%w: sealed allocation", ErrInvalidData) | |
| 205 | 219 | } | |
| 206 | 220 | for range sealed { | |
| @@ -220,50 +234,22 @@ func preflightManifestPane(r *payloadReader, budget *manifestPreflightBudget) (l | |||
| 220 | 234 | return layout.PaneID(id), nil | |
| 221 | 235 | } | |
| 222 | 236 | ||
| 223 | - | const sha256Size = 32 | |
| 224 | - | ||
| 225 | 237 | 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) | |
| 242 | 239 | if err != nil { | |
| 243 | 240 | return err | |
| 244 | 241 | } | |
| 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) | |
| 257 | 243 | } | |
| 258 | 244 | func preflightManifestProcess(r *payloadReader, budget *manifestPreflightBudget) error { | |
| 259 | 245 | present, err := r.getUint8() | |
| 260 | 246 | if err != nil { | |
| 261 | 247 | return err | |
| 262 | 248 | } | |
| 263 | - | if present == 0 { | |
| 249 | + | if present == processAbsent { | |
| 264 | 250 | return nil | |
| 265 | 251 | } | |
| 266 | - | if present != 1 { | |
| 252 | + | if present != processPresent { | |
| 267 | 253 | return fmt.Errorf("%w: process presence", ErrInvalidData) | |
| 268 | 254 | } | |
| 269 | 255 | n, err := r.getUint16() | |
| @@ -274,13 +260,12 @@ func preflightManifestProcess(r *payloadReader, budget *manifestPreflightBudget) | |||
| 274 | 260 | return fmt.Errorf("%w: process argv", ErrInvalidData) | |
| 275 | 261 | } | |
| 276 | 262 | for range n { | |
| 277 | - | if _, err := preflightString(r, budget); err != nil { | |
| 263 | + | if err := skipManifestString(r, budget); err != nil { | |
| 278 | 264 | return err | |
| 279 | 265 | } | |
| 280 | 266 | } | |
| 281 | - | if _, err := preflightString(r, budget); err != nil { | |
| 267 | + | if err := skipManifestString(r, budget); err != nil { | |
| 282 | 268 | return err | |
| 283 | 269 | } | |
| 284 | - | _, err = preflightString(r, budget) | |
| 285 | - | return err | |
| 270 | + | return skipManifestString(r, budget) | |
| 286 | 271 | } | |
| @@ -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 err | 15 | 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 err | 19 | 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 true | 56 | 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 "", err | 62 | + | return nil, err |
| 63 | } | 63 | } | ||
| 64 | if uint64(n) > uint64(len(r.b)) { | 64 | if uint64(n) > uint64(len(r.b)) { | ||
| 65 | - | return "", ErrShortPayload | 65 | + | 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, 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 | |||
| 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 err | 91 | 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 err | 105 | 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 err | 109 | return err | ||
| 96 | } | 110 | } | ||
| @@ -139,14 +153,14 @@ func preflightManifestNode(r *payloadReader, depth int, root bool, budget *manif | |||||
| 139 | return err | 153 | 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 err | 159 | return err | ||
| 146 | } | 160 | } | ||
| 147 | *refs = append(*refs, layout.PaneID(leaf)) | 161 | *refs = append(*refs, layout.PaneID(leaf)) | ||
| 148 | return nil | 162 | 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 err | 166 | 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 err | 174 | 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 "", err | 205 | return "", err | ||
| 192 | } | 206 | } | ||
| 193 | - | if _, err := preflightString(r, budget); err != nil { | 207 | + | if err := skipManifestString(r, budget); err != nil { |
| 194 | return "", err | 208 | return "", err | ||
| 195 | } | 209 | } | ||
| 196 | - | if _, err := preflightString(r, budget); err != nil { | 210 | + | if err := skipManifestString(r, budget); err != nil { |
| 197 | return "", err | 211 | return "", err | ||
| 198 | } | 212 | } | ||
| 199 | sealed, err := r.getUint16() | 213 | sealed, err := r.getUint16() | ||
| 200 | if err != nil { | 214 | if err != nil { | ||
| 201 | return "", err | 215 | 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), nil | 234 | 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 err | 240 | 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 err | 247 | return err | ||
| 262 | } | 248 | } | ||
| 263 | - | if present == 0 { | 249 | + | if present == processAbsent { |
| 264 | return nil | 250 | 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 err | 264 | return err | ||
| 279 | } | 265 | } | ||
| 280 | } | 266 | } | ||
| 281 | - | if _, err := preflightString(r, budget); err != nil { | 267 | + | if err := skipManifestString(r, budget); err != nil { |
| 282 | return err | 268 | 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 | + | } | |||