test(snapshot): specify v3 snapshot validation
- commit
- bf359d38e6959b18667e7e55f72666ec28836987
- author
- brice · · 2026-07-12 15:22 UTC
- parent
- 2c3eaa1
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +219 -0
M internal/usecase/snapshot/codec_test.go +219 -0
| @@ -107,6 +107,141 @@ func TestRoundTripSessions(t *testing.T) { | |||
| 107 | 107 | } | |
| 108 | 108 | } | |
| 109 | 109 | ||
| 110 | + | // V3 preserves the v2 16-byte big-endian envelope, but replaces its | |
| 111 | + | // compressed row payload with a flags-zero manifest of opaque VT blobs. | |
| 112 | + | // Snapshot owns only the manifest: sealed chunks are oldest-first and all | |
| 113 | + | // chunk, tail, and visible payloads remain self-contained VT bytes. | |
| 114 | + | const ( | |
| 115 | + | snapshotV3Version = uint16(3) | |
| 116 | + | v3EnvelopeSize = 16 | |
| 117 | + | ) | |
| 118 | + | ||
| 119 | + | func TestV3SnapshotRoundTripPreservesExactTerminalData(t *testing.T) { | |
| 120 | + | indexed := renderer.DefaultStyle() | |
| 121 | + | indexed.Bold = true | |
| 122 | + | indexed.Inverse = true | |
| 123 | + | indexed.Attrs = renderer.AttrDim | renderer.AttrUnderline | renderer.AttrBlink | renderer.AttrStrikethrough | |
| 124 | + | indexed.Foreground = 196 | |
| 125 | + | indexed.Background = 17 | |
| 126 | + | indexed.UnderlineStyle = renderer.UnderlineCurly | |
| 127 | + | indexed.HasUnderlineColor = true | |
| 128 | + | indexed.UnderlineColor = 203 | |
| 129 | + | ||
| 130 | + | rgb := renderer.DefaultStyle() | |
| 131 | + | rgb.Italic = true | |
| 132 | + | rgb.HasForegroundRGB = true | |
| 133 | + | rgb.ForegroundRGB = renderer.RGB{R: 1, G: 2, B: 3} | |
| 134 | + | rgb.HasBackgroundRGB = true | |
| 135 | + | rgb.BackgroundRGB = renderer.RGB{R: 4, G: 5, B: 6} | |
| 136 | + | rgb.UnderlineStyle = renderer.UnderlineDashed | |
| 137 | + | rgb.HasUnderlineColorRGB = true | |
| 138 | + | rgb.UnderlineColorRGB = renderer.RGB{R: 7, G: 8, B: 9} | |
| 139 | + | ||
| 140 | + | want := Session{Name: "v3 exact", Tabs: []Tab{{ | |
| 141 | + | Focus: "p", | |
| 142 | + | Tree: layout.NewTree("p"), | |
| 143 | + | Panes: []Pane{{ID: "p", Scrollback: [][]renderer.Cell{{ | |
| 144 | + | {Rune: 'I', Style: indexed}, | |
| 145 | + | {Rune: '好', Style: rgb}, | |
| 146 | + | {Continuation: true, Style: rgb}, | |
| 147 | + | }}, Visible: [][]renderer.Cell{{ | |
| 148 | + | {Rune: 'V', Style: rgb}, | |
| 149 | + | {Rune: '界', Style: indexed}, | |
| 150 | + | {Continuation: true, Style: indexed}, | |
| 151 | + | }}}}, | |
| 152 | + | }}} | |
| 153 | + | ||
| 154 | + | encoded, err := Marshal(want) | |
| 155 | + | if err != nil { | |
| 156 | + | t.Fatalf("Marshal() error = %v", err) | |
| 157 | + | } | |
| 158 | + | requireV3Envelope(t, encoded) | |
| 159 | + | got, err := Unmarshal(encoded) | |
| 160 | + | if err != nil { | |
| 161 | + | t.Fatalf("Unmarshal() error = %v", err) | |
| 162 | + | } | |
| 163 | + | if !equalSession(got, want) { | |
| 164 | + | t.Fatalf("round trip mismatch\ngot %#v\nwant %#v", got, want) | |
| 165 | + | } | |
| 166 | + | } | |
| 167 | + | ||
| 168 | + | func TestUnmarshalRejectsV3LegacyVersions(t *testing.T) { | |
| 169 | + | for _, tc := range []struct { | |
| 170 | + | name string | |
| 171 | + | version uint16 | |
| 172 | + | }{ | |
| 173 | + | {name: "v1", version: 1}, | |
| 174 | + | {name: "v2", version: 2}, | |
| 175 | + | } { | |
| 176 | + | t.Run(tc.name, func(t *testing.T) { | |
| 177 | + | _, err := Unmarshal(v3Envelope(tc.version, 0, nil)) | |
| 178 | + | if !errors.Is(err, ErrBadVersion) { | |
| 179 | + | t.Fatalf("Unmarshal() error = %v, want %v", err, ErrBadVersion) | |
| 180 | + | } | |
| 181 | + | }) | |
| 182 | + | } | |
| 183 | + | } | |
| 184 | + | ||
| 185 | + | func TestUnmarshalRejectsMalformedV3EnvelopeBeforeAllocation(t *testing.T) { | |
| 186 | + | body := v3Manifest(0, nil, nil, 0, nil, 0, nil) | |
| 187 | + | valid := v3Envelope(snapshotV3Version, 0, body) | |
| 188 | + | badCRC := append([]byte(nil), valid...) | |
| 189 | + | badCRC[15] ^= 1 | |
| 190 | + | ||
| 191 | + | for _, tc := range []struct { | |
| 192 | + | name string | |
| 193 | + | data []byte | |
| 194 | + | want error | |
| 195 | + | }{ | |
| 196 | + | {name: "short envelope", data: valid[:v3EnvelopeSize-1], want: ErrShortPayload}, | |
| 197 | + | {name: "bad magic", data: append([]byte("NOPE"), valid[4:]...), want: ErrBadMagic}, | |
| 198 | + | {name: "future version", data: v3Envelope(snapshotV3Version+1, 0, body), want: ErrBadVersion}, | |
| 199 | + | {name: "nonzero flags", data: v3Envelope(snapshotV3Version, 1, body), want: ErrInvalidData}, | |
| 200 | + | {name: "bad checksum", data: badCRC, want: ErrBadCRC}, | |
| 201 | + | {name: "oversized body declaration", data: v3EnvelopeWithLength(snapshotV3Version, 0, nil, math.MaxUint32), want: ErrInvalidData}, | |
| 202 | + | } { | |
| 203 | + | t.Run(tc.name, func(t *testing.T) { | |
| 204 | + | assertV3RejectsWithoutPanic(t, tc.data, tc.want) | |
| 205 | + | }) | |
| 206 | + | } | |
| 207 | + | } | |
| 208 | + | ||
| 209 | + | func TestUnmarshalRejectsHostileV3ManifestDeclarationsBeforeAllocation(t *testing.T) { | |
| 210 | + | for _, tc := range []struct { | |
| 211 | + | name string | |
| 212 | + | body []byte | |
| 213 | + | }{ | |
| 214 | + | {name: "sealed chunk count", body: v3Manifest(math.MaxUint32, nil, nil, 0, nil, 0, nil)}, | |
| 215 | + | {name: "sealed blob length", body: v3Manifest(1, []uint32{math.MaxUint32}, nil, 0, nil, 0, nil)}, | |
| 216 | + | {name: "tail blob length", body: v3Manifest(0, nil, nil, math.MaxUint32, nil, 0, nil)}, | |
| 217 | + | {name: "visible blob length", body: v3Manifest(0, nil, nil, 0, nil, math.MaxUint32, nil)}, | |
| 218 | + | // Aggregate accounting is snapshot-owned even though each payload is | |
| 219 | + | // VT-owned opaque data; declarations must be bounded before a blob can | |
| 220 | + | // cause a count-driven allocation. | |
| 221 | + | {name: "aggregate opaque blob declarations", body: v3Manifest(math.MaxUint32, nil, nil, math.MaxUint32, nil, math.MaxUint32, nil)}, | |
| 222 | + | } { | |
| 223 | + | t.Run(tc.name, func(t *testing.T) { | |
| 224 | + | assertV3RejectsWithoutPanic(t, v3Envelope(snapshotV3Version, 0, tc.body), ErrInvalidData) | |
| 225 | + | }) | |
| 226 | + | } | |
| 227 | + | } | |
| 228 | + | ||
| 229 | + | func TestUnmarshalRejectsEveryV3PrefixAndTrailingGarbage(t *testing.T) { | |
| 230 | + | encoded, err := Marshal(Session{Name: "v3 prefixes", Tabs: []Tab{{ | |
| 231 | + | Focus: "p", | |
| 232 | + | Tree: layout.NewTree("p"), | |
| 233 | + | Panes: []Pane{{ID: "p", Scrollback: rows("history"), Visible: rows("visible")}}, | |
| 234 | + | }}}) | |
| 235 | + | if err != nil { | |
| 236 | + | t.Fatalf("Marshal() error = %v", err) | |
| 237 | + | } | |
| 238 | + | requireV3Envelope(t, encoded) | |
| 239 | + | for n := range len(encoded) { | |
| 240 | + | assertV3RejectsWithoutPanic(t, encoded[:n], nil) | |
| 241 | + | } | |
| 242 | + | assertV3RejectsWithoutPanic(t, append(append([]byte(nil), encoded...), 0), ErrTrailingBytes) | |
| 243 | + | } | |
| 244 | + | ||
| 110 | 245 | func TestUnmarshalRejectsMalformedWithoutPanic(t *testing.T) { | |
| 111 | 246 | good, err := Marshal(Session{Name: "s", Tabs: []Tab{{Tree: layout.NewTree("p"), Focus: "p", Panes: []Pane{{ID: "p"}}}}}) | |
| 112 | 247 | if err != nil { | |
| @@ -431,3 +566,87 @@ func unknownPaneSnapshot(t *testing.T) []byte { | |||
| 431 | 566 | } | |
| 432 | 567 | return b | |
| 433 | 568 | } | |
| 569 | + | ||
| 570 | + | func requireV3Envelope(t *testing.T, data []byte) { | |
| 571 | + | t.Helper() | |
| 572 | + | if len(data) < v3EnvelopeSize { | |
| 573 | + | t.Fatalf("encoded snapshot is %d bytes, want at least %d", len(data), v3EnvelopeSize) | |
| 574 | + | } | |
| 575 | + | if got := binary.BigEndian.Uint16(data[4:6]); got != snapshotV3Version { | |
| 576 | + | t.Fatalf("snapshot version = %d, want v3", got) | |
| 577 | + | } | |
| 578 | + | if got := binary.BigEndian.Uint16(data[6:8]); got != 0 { | |
| 579 | + | t.Fatalf("snapshot flags = %#x, want 0", got) | |
| 580 | + | } | |
| 581 | + | } | |
| 582 | + | ||
| 583 | + | func assertV3RejectsWithoutPanic(t *testing.T, data []byte, want error) { | |
| 584 | + | t.Helper() | |
| 585 | + | defer func() { | |
| 586 | + | if r := recover(); r != nil { | |
| 587 | + | t.Fatalf("Unmarshal panicked: %v", r) | |
| 588 | + | } | |
| 589 | + | }() | |
| 590 | + | _, err := Unmarshal(data) | |
| 591 | + | if err == nil { | |
| 592 | + | t.Fatal("Unmarshal() error = nil") | |
| 593 | + | } | |
| 594 | + | if want != nil && !errors.Is(err, want) { | |
| 595 | + | t.Fatalf("Unmarshal() error = %v, want %v", err, want) | |
| 596 | + | } | |
| 597 | + | } | |
| 598 | + | ||
| 599 | + | func v3Envelope(version, flags uint16, body []byte) []byte { | |
| 600 | + | return v3EnvelopeWithLength(version, flags, body, uint32(len(body))) | |
| 601 | + | } | |
| 602 | + | ||
| 603 | + | func v3EnvelopeWithLength(version, flags uint16, body []byte, bodyLen uint32) []byte { | |
| 604 | + | out := make([]byte, v3EnvelopeSize+len(body)) | |
| 605 | + | copy(out[:4], magic) | |
| 606 | + | binary.BigEndian.PutUint16(out[4:6], version) | |
| 607 | + | binary.BigEndian.PutUint16(out[6:8], flags) | |
| 608 | + | binary.BigEndian.PutUint32(out[8:12], bodyLen) | |
| 609 | + | binary.BigEndian.PutUint32(out[12:16], crc32.ChecksumIEEE(body)) | |
| 610 | + | copy(out[v3EnvelopeSize:], body) | |
| 611 | + | return out | |
| 612 | + | } | |
| 613 | + | ||
| 614 | + | // v3Manifest writes only snapshot-owned fields. Blob contents are deliberately | |
| 615 | + | // opaque: VT validates their style, row, run, cell, and decoded-byte budgets. | |
| 616 | + | func v3Manifest( | |
| 617 | + | sealedCount uint32, | |
| 618 | + | sealedLengths []uint32, | |
| 619 | + | sealedBlobs [][]byte, | |
| 620 | + | tailLength uint32, | |
| 621 | + | tailBlob []byte, | |
| 622 | + | visibleLength uint32, | |
| 623 | + | visibleBlob []byte, | |
| 624 | + | ) []byte { | |
| 625 | + | var w payloadWriter | |
| 626 | + | _ = w.putString("v3") | |
| 627 | + | w.putUint64(0) | |
| 628 | + | w.putUint16(0) | |
| 629 | + | w.putUint16(1) | |
| 630 | + | _ = w.putString("tab") | |
| 631 | + | w.putUint16(80) | |
| 632 | + | w.putUint16(24) | |
| 633 | + | w.putUint64(1) | |
| 634 | + | _ = w.putString("p") | |
| 635 | + | _ = writeNode(&w, layout.NewLeaf("p")) | |
| 636 | + | w.putUint16(1) | |
| 637 | + | _ = w.putString("p") | |
| 638 | + | _ = w.putString("") | |
| 639 | + | _ = w.putString("") | |
| 640 | + | w.putUint32(sealedCount) | |
| 641 | + | for i := range sealedLengths { | |
| 642 | + | w.putUint32(sealedLengths[i]) | |
| 643 | + | if i < len(sealedBlobs) { | |
| 644 | + | w.b = append(w.b, sealedBlobs[i]...) | |
| 645 | + | } | |
| 646 | + | } | |
| 647 | + | w.putUint32(tailLength) | |
| 648 | + | w.b = append(w.b, tailBlob...) | |
| 649 | + | w.putUint32(visibleLength) | |
| 650 | + | w.b = append(w.b, visibleBlob...) | |
| 651 | + | return w.b | |
| 652 | + | } | |
| @@ -107,6 +107,141 @@ func TestRoundTripSessions(t *testing.T) { | |||||
| 107 | } | 107 | } | ||
| 108 | } | 108 | } | ||
| 109 | 109 | ||||
| 110 | + | // V3 preserves the v2 16-byte big-endian envelope, but replaces its | |||
| 111 | + | // compressed row payload with a flags-zero manifest of opaque VT blobs. | |||
| 112 | + | // Snapshot owns only the manifest: sealed chunks are oldest-first and all | |||
| 113 | + | // chunk, tail, and visible payloads remain self-contained VT bytes. | |||
| 114 | + | const ( | |||
| 115 | + | snapshotV3Version = uint16(3) | |||
| 116 | + | v3EnvelopeSize = 16 | |||
| 117 | + | ) | |||
| 118 | + | ||||
| 119 | + | func TestV3SnapshotRoundTripPreservesExactTerminalData(t *testing.T) { | |||
| 120 | + | indexed := renderer.DefaultStyle() | |||
| 121 | + | indexed.Bold = true | |||
| 122 | + | indexed.Inverse = true | |||
| 123 | + | indexed.Attrs = renderer.AttrDim | renderer.AttrUnderline | renderer.AttrBlink | renderer.AttrStrikethrough | |||
| 124 | + | indexed.Foreground = 196 | |||
| 125 | + | indexed.Background = 17 | |||
| 126 | + | indexed.UnderlineStyle = renderer.UnderlineCurly | |||
| 127 | + | indexed.HasUnderlineColor = true | |||
| 128 | + | indexed.UnderlineColor = 203 | |||
| 129 | + | ||||
| 130 | + | rgb := renderer.DefaultStyle() | |||
| 131 | + | rgb.Italic = true | |||
| 132 | + | rgb.HasForegroundRGB = true | |||
| 133 | + | rgb.ForegroundRGB = renderer.RGB{R: 1, G: 2, B: 3} | |||
| 134 | + | rgb.HasBackgroundRGB = true | |||
| 135 | + | rgb.BackgroundRGB = renderer.RGB{R: 4, G: 5, B: 6} | |||
| 136 | + | rgb.UnderlineStyle = renderer.UnderlineDashed | |||
| 137 | + | rgb.HasUnderlineColorRGB = true | |||
| 138 | + | rgb.UnderlineColorRGB = renderer.RGB{R: 7, G: 8, B: 9} | |||
| 139 | + | ||||
| 140 | + | want := Session{Name: "v3 exact", Tabs: []Tab{{ | |||
| 141 | + | Focus: "p", | |||
| 142 | + | Tree: layout.NewTree("p"), | |||
| 143 | + | Panes: []Pane{{ID: "p", Scrollback: [][]renderer.Cell{{ | |||
| 144 | + | {Rune: 'I', Style: indexed}, | |||
| 145 | + | {Rune: '好', Style: rgb}, | |||
| 146 | + | {Continuation: true, Style: rgb}, | |||
| 147 | + | }}, Visible: [][]renderer.Cell{{ | |||
| 148 | + | {Rune: 'V', Style: rgb}, | |||
| 149 | + | {Rune: '界', Style: indexed}, | |||
| 150 | + | {Continuation: true, Style: indexed}, | |||
| 151 | + | }}}}, | |||
| 152 | + | }}} | |||
| 153 | + | ||||
| 154 | + | encoded, err := Marshal(want) | |||
| 155 | + | if err != nil { | |||
| 156 | + | t.Fatalf("Marshal() error = %v", err) | |||
| 157 | + | } | |||
| 158 | + | requireV3Envelope(t, encoded) | |||
| 159 | + | got, err := Unmarshal(encoded) | |||
| 160 | + | if err != nil { | |||
| 161 | + | t.Fatalf("Unmarshal() error = %v", err) | |||
| 162 | + | } | |||
| 163 | + | if !equalSession(got, want) { | |||
| 164 | + | t.Fatalf("round trip mismatch\ngot %#v\nwant %#v", got, want) | |||
| 165 | + | } | |||
| 166 | + | } | |||
| 167 | + | ||||
| 168 | + | func TestUnmarshalRejectsV3LegacyVersions(t *testing.T) { | |||
| 169 | + | for _, tc := range []struct { | |||
| 170 | + | name string | |||
| 171 | + | version uint16 | |||
| 172 | + | }{ | |||
| 173 | + | {name: "v1", version: 1}, | |||
| 174 | + | {name: "v2", version: 2}, | |||
| 175 | + | } { | |||
| 176 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 177 | + | _, err := Unmarshal(v3Envelope(tc.version, 0, nil)) | |||
| 178 | + | if !errors.Is(err, ErrBadVersion) { | |||
| 179 | + | t.Fatalf("Unmarshal() error = %v, want %v", err, ErrBadVersion) | |||
| 180 | + | } | |||
| 181 | + | }) | |||
| 182 | + | } | |||
| 183 | + | } | |||
| 184 | + | ||||
| 185 | + | func TestUnmarshalRejectsMalformedV3EnvelopeBeforeAllocation(t *testing.T) { | |||
| 186 | + | body := v3Manifest(0, nil, nil, 0, nil, 0, nil) | |||
| 187 | + | valid := v3Envelope(snapshotV3Version, 0, body) | |||
| 188 | + | badCRC := append([]byte(nil), valid...) | |||
| 189 | + | badCRC[15] ^= 1 | |||
| 190 | + | ||||
| 191 | + | for _, tc := range []struct { | |||
| 192 | + | name string | |||
| 193 | + | data []byte | |||
| 194 | + | want error | |||
| 195 | + | }{ | |||
| 196 | + | {name: "short envelope", data: valid[:v3EnvelopeSize-1], want: ErrShortPayload}, | |||
| 197 | + | {name: "bad magic", data: append([]byte("NOPE"), valid[4:]...), want: ErrBadMagic}, | |||
| 198 | + | {name: "future version", data: v3Envelope(snapshotV3Version+1, 0, body), want: ErrBadVersion}, | |||
| 199 | + | {name: "nonzero flags", data: v3Envelope(snapshotV3Version, 1, body), want: ErrInvalidData}, | |||
| 200 | + | {name: "bad checksum", data: badCRC, want: ErrBadCRC}, | |||
| 201 | + | {name: "oversized body declaration", data: v3EnvelopeWithLength(snapshotV3Version, 0, nil, math.MaxUint32), want: ErrInvalidData}, | |||
| 202 | + | } { | |||
| 203 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 204 | + | assertV3RejectsWithoutPanic(t, tc.data, tc.want) | |||
| 205 | + | }) | |||
| 206 | + | } | |||
| 207 | + | } | |||
| 208 | + | ||||
| 209 | + | func TestUnmarshalRejectsHostileV3ManifestDeclarationsBeforeAllocation(t *testing.T) { | |||
| 210 | + | for _, tc := range []struct { | |||
| 211 | + | name string | |||
| 212 | + | body []byte | |||
| 213 | + | }{ | |||
| 214 | + | {name: "sealed chunk count", body: v3Manifest(math.MaxUint32, nil, nil, 0, nil, 0, nil)}, | |||
| 215 | + | {name: "sealed blob length", body: v3Manifest(1, []uint32{math.MaxUint32}, nil, 0, nil, 0, nil)}, | |||
| 216 | + | {name: "tail blob length", body: v3Manifest(0, nil, nil, math.MaxUint32, nil, 0, nil)}, | |||
| 217 | + | {name: "visible blob length", body: v3Manifest(0, nil, nil, 0, nil, math.MaxUint32, nil)}, | |||
| 218 | + | // Aggregate accounting is snapshot-owned even though each payload is | |||
| 219 | + | // VT-owned opaque data; declarations must be bounded before a blob can | |||
| 220 | + | // cause a count-driven allocation. | |||
| 221 | + | {name: "aggregate opaque blob declarations", body: v3Manifest(math.MaxUint32, nil, nil, math.MaxUint32, nil, math.MaxUint32, nil)}, | |||
| 222 | + | } { | |||
| 223 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 224 | + | assertV3RejectsWithoutPanic(t, v3Envelope(snapshotV3Version, 0, tc.body), ErrInvalidData) | |||
| 225 | + | }) | |||
| 226 | + | } | |||
| 227 | + | } | |||
| 228 | + | ||||
| 229 | + | func TestUnmarshalRejectsEveryV3PrefixAndTrailingGarbage(t *testing.T) { | |||
| 230 | + | encoded, err := Marshal(Session{Name: "v3 prefixes", Tabs: []Tab{{ | |||
| 231 | + | Focus: "p", | |||
| 232 | + | Tree: layout.NewTree("p"), | |||
| 233 | + | Panes: []Pane{{ID: "p", Scrollback: rows("history"), Visible: rows("visible")}}, | |||
| 234 | + | }}}) | |||
| 235 | + | if err != nil { | |||
| 236 | + | t.Fatalf("Marshal() error = %v", err) | |||
| 237 | + | } | |||
| 238 | + | requireV3Envelope(t, encoded) | |||
| 239 | + | for n := range len(encoded) { | |||
| 240 | + | assertV3RejectsWithoutPanic(t, encoded[:n], nil) | |||
| 241 | + | } | |||
| 242 | + | assertV3RejectsWithoutPanic(t, append(append([]byte(nil), encoded...), 0), ErrTrailingBytes) | |||
| 243 | + | } | |||
| 244 | + | ||||
| 110 | func TestUnmarshalRejectsMalformedWithoutPanic(t *testing.T) { | 245 | func TestUnmarshalRejectsMalformedWithoutPanic(t *testing.T) { | ||
| 111 | good, err := Marshal(Session{Name: "s", Tabs: []Tab{{Tree: layout.NewTree("p"), Focus: "p", Panes: []Pane{{ID: "p"}}}}}) | 246 | good, err := Marshal(Session{Name: "s", Tabs: []Tab{{Tree: layout.NewTree("p"), Focus: "p", Panes: []Pane{{ID: "p"}}}}}) | ||
| 112 | if err != nil { | 247 | if err != nil { | ||
| @@ -431,3 +566,87 @@ func unknownPaneSnapshot(t *testing.T) []byte { | |||||
| 431 | } | 566 | } | ||
| 432 | return b | 567 | return b | ||
| 433 | } | 568 | } | ||
| 569 | + | ||||
| 570 | + | func requireV3Envelope(t *testing.T, data []byte) { | |||
| 571 | + | t.Helper() | |||
| 572 | + | if len(data) < v3EnvelopeSize { | |||
| 573 | + | t.Fatalf("encoded snapshot is %d bytes, want at least %d", len(data), v3EnvelopeSize) | |||
| 574 | + | } | |||
| 575 | + | if got := binary.BigEndian.Uint16(data[4:6]); got != snapshotV3Version { | |||
| 576 | + | t.Fatalf("snapshot version = %d, want v3", got) | |||
| 577 | + | } | |||
| 578 | + | if got := binary.BigEndian.Uint16(data[6:8]); got != 0 { | |||
| 579 | + | t.Fatalf("snapshot flags = %#x, want 0", got) | |||
| 580 | + | } | |||
| 581 | + | } | |||
| 582 | + | ||||
| 583 | + | func assertV3RejectsWithoutPanic(t *testing.T, data []byte, want error) { | |||
| 584 | + | t.Helper() | |||
| 585 | + | defer func() { | |||
| 586 | + | if r := recover(); r != nil { | |||
| 587 | + | t.Fatalf("Unmarshal panicked: %v", r) | |||
| 588 | + | } | |||
| 589 | + | }() | |||
| 590 | + | _, err := Unmarshal(data) | |||
| 591 | + | if err == nil { | |||
| 592 | + | t.Fatal("Unmarshal() error = nil") | |||
| 593 | + | } | |||
| 594 | + | if want != nil && !errors.Is(err, want) { | |||
| 595 | + | t.Fatalf("Unmarshal() error = %v, want %v", err, want) | |||
| 596 | + | } | |||
| 597 | + | } | |||
| 598 | + | ||||
| 599 | + | func v3Envelope(version, flags uint16, body []byte) []byte { | |||
| 600 | + | return v3EnvelopeWithLength(version, flags, body, uint32(len(body))) | |||
| 601 | + | } | |||
| 602 | + | ||||
| 603 | + | func v3EnvelopeWithLength(version, flags uint16, body []byte, bodyLen uint32) []byte { | |||
| 604 | + | out := make([]byte, v3EnvelopeSize+len(body)) | |||
| 605 | + | copy(out[:4], magic) | |||
| 606 | + | binary.BigEndian.PutUint16(out[4:6], version) | |||
| 607 | + | binary.BigEndian.PutUint16(out[6:8], flags) | |||
| 608 | + | binary.BigEndian.PutUint32(out[8:12], bodyLen) | |||
| 609 | + | binary.BigEndian.PutUint32(out[12:16], crc32.ChecksumIEEE(body)) | |||
| 610 | + | copy(out[v3EnvelopeSize:], body) | |||
| 611 | + | return out | |||
| 612 | + | } | |||
| 613 | + | ||||
| 614 | + | // v3Manifest writes only snapshot-owned fields. Blob contents are deliberately | |||
| 615 | + | // opaque: VT validates their style, row, run, cell, and decoded-byte budgets. | |||
| 616 | + | func v3Manifest( | |||
| 617 | + | sealedCount uint32, | |||
| 618 | + | sealedLengths []uint32, | |||
| 619 | + | sealedBlobs [][]byte, | |||
| 620 | + | tailLength uint32, | |||
| 621 | + | tailBlob []byte, | |||
| 622 | + | visibleLength uint32, | |||
| 623 | + | visibleBlob []byte, | |||
| 624 | + | ) []byte { | |||
| 625 | + | var w payloadWriter | |||
| 626 | + | _ = w.putString("v3") | |||
| 627 | + | w.putUint64(0) | |||
| 628 | + | w.putUint16(0) | |||
| 629 | + | w.putUint16(1) | |||
| 630 | + | _ = w.putString("tab") | |||
| 631 | + | w.putUint16(80) | |||
| 632 | + | w.putUint16(24) | |||
| 633 | + | w.putUint64(1) | |||
| 634 | + | _ = w.putString("p") | |||
| 635 | + | _ = writeNode(&w, layout.NewLeaf("p")) | |||
| 636 | + | w.putUint16(1) | |||
| 637 | + | _ = w.putString("p") | |||
| 638 | + | _ = w.putString("") | |||
| 639 | + | _ = w.putString("") | |||
| 640 | + | w.putUint32(sealedCount) | |||
| 641 | + | for i := range sealedLengths { | |||
| 642 | + | w.putUint32(sealedLengths[i]) | |||
| 643 | + | if i < len(sealedBlobs) { | |||
| 644 | + | w.b = append(w.b, sealedBlobs[i]...) | |||
| 645 | + | } | |||
| 646 | + | } | |||
| 647 | + | w.putUint32(tailLength) | |||
| 648 | + | w.b = append(w.b, tailBlob...) | |||
| 649 | + | w.putUint32(visibleLength) | |||
| 650 | + | w.b = append(w.b, visibleBlob...) | |||
| 651 | + | return w.b | |||
| 652 | + | } | |||