1package snapshotcodec
2
3import (
4 "bytes"
5 "encoding/binary"
6 "errors"
7 "hash/crc32"
8 "runtime"
9 "testing"
10)
11
12// reframeObject rewrites the VEVO body and fixes length and CRC so a test
13// reaches the payload checks instead of failing on framing.
14func reframeObject(t *testing.T, encoded []byte, mutate func(body []byte) []byte) []byte {
15 t.Helper()
16 body := mutate(append([]byte(nil), encoded[manifestHeaderSize:]...))
17 out := append([]byte(nil), encoded[:manifestHeaderSize]...)
18 binary.BigEndian.PutUint32(out[8:12], uint32(len(body)))
19 binary.BigEndian.PutUint32(out[12:16], crc32.ChecksumIEEE(body))
20 return append(out, body...)
21}
22
23func TestObjectCompressionRejectsMalformedPayloads(t *testing.T) {
24 payload := bytes.Repeat([]byte("history row "), 512)
25 object, err := MarshalObject(HistoryChunk, payload)
26 if err != nil {
27 t.Fatal(err)
28 }
29 if len(object.Data) >= len(payload)/4 {
30 t.Fatalf("object = %d bytes, want well under %d raw bytes", len(object.Data), len(payload))
31 }
32 again, err := MarshalObject(HistoryChunk, payload)
33 if err != nil || !bytes.Equal(again.Data, object.Data) || again.Digest != object.Digest {
34 t.Fatal("equal payloads must share one deterministic content address")
35 }
36
37 for _, tc := range []struct {
38 name string
39 mutate func(body []byte) []byte
40 want error
41 }{
42 {name: "declared shorter", mutate: func(b []byte) []byte {
43 binary.BigEndian.PutUint32(b[1:5], uint32(len(payload)-1))
44 return b
45 }, want: ErrInvalidData},
46 {name: "declared longer", mutate: func(b []byte) []byte {
47 binary.BigEndian.PutUint32(b[1:5], uint32(len(payload)+1))
48 return b
49 }, want: ErrInvalidData},
50 {name: "declared maximum on a small stream", mutate: func(b []byte) []byte {
51 binary.BigEndian.PutUint32(b[1:5], maxObjectPayloadSize)
52 return b
53 }, want: ErrInvalidData},
54 {name: "truncated stream", mutate: func(b []byte) []byte { return b[:len(b)-1] }, want: ErrInvalidData},
55 {name: "corrupt stream", mutate: func(b []byte) []byte { b[len(b)-6] ^= 0xff; return b }, want: ErrInvalidData},
56 {name: "trailing garbage", mutate: func(b []byte) []byte { return append(b, 0) }, want: ErrTrailingBytes},
57 } {
58 t.Run(tc.name, func(t *testing.T) {
59 bad := reframeObject(t, object.Data, tc.mutate)
60 if _, err := PreflightObject(bad); err != nil {
61 t.Fatalf("preflight must accept valid framing, got %v", err)
62 }
63 if _, _, err := UnmarshalObject(bad); !errors.Is(err, tc.want) {
64 t.Fatalf("UnmarshalObject() error = %v, want %v", err, tc.want)
65 }
66 })
67 }
68}
69
70// TestInflateObjectPayloadAllocation pins the inflate memory bound: a payload
71// costs about its own size, and a corrupt maximum declared length on a small
72// stream never allocates the declared size.
73func TestInflateObjectPayloadAllocation(t *testing.T) {
74 for _, tc := range []struct {
75 name string
76 size int
77 declared uint32
78 maxBytes int64
79 }{
80 {name: "small payload", size: 64 << 10, maxBytes: 64<<10 + 64<<10},
81 {name: "payload at the initial cap", size: inflateInitialCap, maxBytes: inflateInitialCap + 64<<10},
82 {name: "payload above the initial cap", size: 3 << 20, maxBytes: 3<<20 + 64<<10},
83 {name: "declared maximum on a small stream", size: 64 << 10, declared: maxObjectPayloadSize, maxBytes: 2*inflateInitialCap + 64<<10},
84 } {
85 t.Run(tc.name, func(t *testing.T) {
86 payload := bytes.Repeat([]byte("history row "), tc.size/12+1)[:tc.size]
87 compressed, err := compressObjectPayload(nil, payload)
88 if err != nil {
89 t.Fatal(err)
90 }
91 declared := uint32(tc.size)
92 if tc.declared != 0 {
93 declared = tc.declared
94 }
95 _, _ = inflateObjectPayload(compressed, declared) // warm the decompressor pool
96 const runs = 8
97 var before, after runtime.MemStats
98 runtime.ReadMemStats(&before)
99 for range runs {
100 _, _ = inflateObjectPayload(compressed, declared)
101 }
102 runtime.ReadMemStats(&after)
103 if got := int64(after.TotalAlloc-before.TotalAlloc) / runs; got > tc.maxBytes {
104 t.Fatalf("inflate allocated %d bytes/op, want at most %d", got, tc.maxBytes)
105 }
106 })
107 }
108}