v0.8.0 cad1d13

object_compress_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close