1package snapshotcodec
2
3import (
4 "bytes"
5 "crypto/sha256"
6 "encoding/binary"
7 "errors"
8 "math"
9 "testing"
10
11 "github.com/bnema/vev/internal/domain"
12 "github.com/bnema/vev/internal/domain/layout"
13 "github.com/bnema/vev/internal/ports"
14)
15
16func TestManifestIncarnation(t *testing.T) {
17 manifest := manifestWithTailSize(minObjectEnvelopeSize)
18 manifest.Generation = 2
19 manifest.IncarnationID = domain.IncarnationID{1}
20 manifest.ParentCheckpoint = &domain.CheckpointRef{Generation: 1, ManifestDigest: [32]byte{2}}
21
22 encoded, err := MarshalManifest(manifest)
23 if err != nil {
24 t.Fatal(err)
25 }
26 decoded, err := UnmarshalManifest(encoded)
27 if err != nil {
28 t.Fatal(err)
29 }
30 if decoded.IncarnationID != manifest.IncarnationID || decoded.ParentCheckpoint == nil || *decoded.ParentCheckpoint != *manifest.ParentCheckpoint {
31 t.Fatalf("round trip = %#v", decoded)
32 }
33 reencoded, err := MarshalManifest(decoded)
34 if err != nil || !bytes.Equal(encoded, reencoded) {
35 t.Fatalf("manifest bytes are not canonical: %v", err)
36 }
37 if got := binary.BigEndian.Uint16(encoded[4:6]); got != ManifestVersion {
38 t.Fatalf("manifest version = %d", got)
39 }
40 for n := range len(encoded) {
41 if _, err := UnmarshalManifest(encoded[:n]); err == nil {
42 t.Fatalf("prefix %d accepted", n)
43 }
44 }
45 if _, err := UnmarshalManifest(append(encoded, 0)); err == nil {
46 t.Fatal("trailing data accepted")
47 }
48 versionOne := append([]byte(nil), encoded...)
49 binary.BigEndian.PutUint16(versionOne[4:6], 1)
50 if _, err := UnmarshalManifest(versionOne); !errors.Is(err, ErrBadVersion) {
51 t.Fatalf("manifest v1 error = %v", err)
52 }
53 manifest.IncarnationID = domain.IncarnationID{}
54 if _, err := MarshalManifest(manifest); err == nil {
55 t.Fatal("zero incarnation accepted")
56 }
57}
58
59func TestManifestCodecUsesGenerationAndOrderedTypedReferences(t *testing.T) {
60 chunk, err := MarshalObject(HistoryChunk, []byte("history"))
61 if err != nil {
62 t.Fatal(err)
63 }
64 tail, err := MarshalObject(HistoryTail, []byte("tail"))
65 if err != nil {
66 t.Fatal(err)
67 }
68 transcript, err := MarshalObject(RecoveryTranscript, []byte("transcript"))
69 if err != nil {
70 t.Fatal(err)
71 }
72 manifest := Manifest{Generation: 7, IncarnationID: domain.IncarnationID{1}, Name: "named", CreatedAt: 9, Active: 0, Tabs: []ManifestTab{{
73 StableID: "tab", Cols: 80, Rows: 24, NextPaneID: 2, Focus: "pane", Tree: layout.NewTree("pane"),
74 Panes: []ManifestPane{{ID: "pane", StableID: "pane-stable", Cwd: "/tmp",
75 Sealed: []ObjectRef{{Kind: HistoryChunk, Digest: chunk.Digest, Size: uint32(len(chunk.Data))}},
76 Tail: ObjectRef{Kind: HistoryTail, Digest: tail.Digest, Size: uint32(len(tail.Data))},
77 Transcript: ObjectRef{Kind: RecoveryTranscript, Digest: transcript.Digest, Size: uint32(len(transcript.Data))},
78 }},
79 }}}
80 encoded, err := MarshalManifest(manifest)
81 if err != nil {
82 t.Fatal(err)
83 }
84 got, err := UnmarshalManifest(encoded)
85 if err != nil {
86 t.Fatal(err)
87 }
88 if got.Generation != manifest.Generation || got.Tabs[0].Panes[0].Tail != manifest.Tabs[0].Panes[0].Tail {
89 t.Fatalf("round trip = %#v", got)
90 }
91 gotTranscript := got.Tabs[0].Panes[0].Transcript
92 if gotTranscript.Kind != RecoveryTranscript || gotTranscript.Digest != transcript.Digest || gotTranscript.Size != uint32(len(transcript.Data)) {
93 t.Fatalf("transcript round trip = %#v", gotTranscript)
94 }
95 for n := range len(encoded) {
96 if _, err := UnmarshalManifest(encoded[:n]); err == nil {
97 t.Fatalf("prefix %d accepted", n)
98 }
99 }
100 if _, err := UnmarshalManifest(append(encoded, 0)); !errors.Is(err, ErrTrailingBytes) {
101 t.Fatalf("trailing data error = %v", err)
102 }
103
104 head := Head{Generation: 7, ManifestDigest: sha256.Sum256(encoded)}
105 if head.Generation != manifest.Generation || head.ManifestDigest != ManifestDigest(encoded) {
106 t.Fatalf("head = %#v", head)
107 }
108}
109
110func TestObjectCodecReturnsContentAddressedObjectAndPayload(t *testing.T) {
111 object, err := MarshalObject(RecoveryTranscript, []byte("canonical-vt"))
112 if err != nil {
113 t.Fatal(err)
114 }
115 if object.Digest != sha256.Sum256(object.Data) {
116 t.Fatalf("digest does not address VEVO bytes")
117 }
118 kind, payload, err := UnmarshalObject(object.Data)
119 if err != nil || kind != RecoveryTranscript || !bytes.Equal(payload, []byte("canonical-vt")) {
120 t.Fatalf("round trip = %v %q %v", kind, payload, err)
121 }
122 kind, err = PreflightObject(object.Data)
123 if err != nil || kind != RecoveryTranscript {
124 t.Fatalf("preflight = %v %v", kind, err)
125 }
126 for n := range len(object.Data) {
127 if _, _, err := UnmarshalObject(object.Data[:n]); err == nil {
128 t.Fatalf("prefix %d accepted", n)
129 }
130 }
131 if _, _, err := UnmarshalObject(append(object.Data, 0)); !errors.Is(err, ErrTrailingBytes) {
132 t.Fatalf("trailing data error = %v", err)
133 }
134}
135
136func TestManifestDimensionBoundaries(t *testing.T) {
137 ref := ObjectRef{Kind: HistoryTail, Digest: ports.SnapshotDigest{1}, Size: minObjectEnvelopeSize}
138 transcript := ObjectRef{Kind: RecoveryTranscript, Digest: ports.SnapshotDigest{2}, Size: minObjectEnvelopeSize}
139 for _, tc := range []struct {
140 name string
141 cols, rows uint16
142 wantErr bool
143 }{
144 {"max cols safe rows", math.MaxUint16, 1, false},
145 {"max rows safe cols", 1, math.MaxUint16, false},
146 {"hostile product", math.MaxUint16, math.MaxUint16, true},
147 } {
148 t.Run(tc.name, func(t *testing.T) {
149 m := Manifest{Generation: 1, IncarnationID: domain.IncarnationID{1}, Name: "safe", Tabs: []ManifestTab{{Cols: tc.cols, Rows: tc.rows, Panes: []ManifestPane{{ID: "p", Tail: ref, Transcript: transcript}}}}}
150 _, err := MarshalManifest(m)
151 if (err != nil) != tc.wantErr {
152 t.Fatalf("MarshalManifest() error = %v, want error=%v", err, tc.wantErr)
153 }
154 })
155 }
156}
157
158func TestManifestRejectsImpossibleObjectEnvelopeSize(t *testing.T) {
159 manifest := Manifest{Generation: 1, IncarnationID: domain.IncarnationID{1}, Name: "named", Tabs: []ManifestTab{{Cols: 1, Rows: 1, Panes: []ManifestPane{{
160 ID: "pane",
161 Tail: ObjectRef{Kind: HistoryTail, Digest: SnapshotDigest{1}, Size: 1},
162 Transcript: ObjectRef{Kind: RecoveryTranscript, Digest: SnapshotDigest{2}, Size: minObjectEnvelopeSize},
163 }}}}}
164 if _, err := MarshalManifest(manifest); !errors.Is(err, ErrInvalidData) {
165 t.Fatalf("MarshalManifest() error = %v, want invalid data", err)
166 }
167}
168
169func TestObjectRefEnvelopeSizeBoundaries(t *testing.T) {
170 const (
171 minimum = manifestHeaderSize + 1 + 4 + 1
172 maximum = manifestHeaderSize + maxDecodedBodySize
173 )
174 if minObjectEnvelopeSize != minimum || maxObjectEnvelopeSize != maximum {
175 t.Fatalf("object envelope range = [%d, %d], want [%d, %d]", minObjectEnvelopeSize, maxObjectEnvelopeSize, minimum, maximum)
176 }
177 object, err := MarshalObject(HistoryTail, []byte{0})
178 if err != nil {
179 t.Fatalf("MarshalObject() error = %v", err)
180 }
181 // The smallest payload is still a complete zlib stream, so real objects sit
182 // above the declared lower bound the manifest accepts.
183 if len(object.Data) < minimum {
184 t.Fatalf("MarshalObject() size = %d, want >= %d", len(object.Data), minimum)
185 }
186
187 for _, tc := range []struct {
188 name string
189 size uint32
190 valid bool
191 }{
192 {name: "one below minimum", size: minimum - 1},
193 {name: "minimum", size: minimum, valid: true},
194 {name: "maximum declaration", size: maximum, valid: true},
195 {name: "one above maximum", size: maximum + 1},
196 } {
197 t.Run(tc.name, func(t *testing.T) {
198 manifest := manifestWithTailSize(tc.size)
199 encoded, err := MarshalManifest(manifest)
200 if tc.valid {
201 if err != nil {
202 t.Fatalf("MarshalManifest() error = %v", err)
203 }
204 if _, err := UnmarshalManifest(encoded); err != nil {
205 t.Fatalf("UnmarshalManifest() error = %v", err)
206 }
207 } else if !errors.Is(err, ErrInvalidData) {
208 t.Fatalf("MarshalManifest() error = %v, want invalid data", err)
209 }
210
211 var w payloadWriter
212 writeObjectRef(&w, ObjectRef{Kind: HistoryTail, Digest: SnapshotDigest{1}, Size: tc.size})
213 err = preflightObjectRef(&payloadReader{b: w.b}, HistoryTail)
214 if tc.valid && err != nil {
215 t.Fatalf("preflightObjectRef() error = %v", err)
216 }
217 if !tc.valid && !errors.Is(err, ErrInvalidData) {
218 t.Fatalf("preflightObjectRef() error = %v, want invalid data", err)
219 }
220 })
221 }
222}
223
224func manifestWithTailSize(size uint32) Manifest {
225 return Manifest{Generation: 1, IncarnationID: domain.IncarnationID{1}, Name: "named", Tabs: []ManifestTab{{
226 Cols: 1,
227 Rows: 1,
228 Panes: []ManifestPane{{
229 ID: "pane",
230 Tail: ObjectRef{Kind: HistoryTail, Digest: SnapshotDigest{1}, Size: size},
231 Transcript: ObjectRef{Kind: RecoveryTranscript, Digest: SnapshotDigest{2}, Size: minObjectEnvelopeSize},
232 }},
233 }}}
234}