1package spa
2
3import (
4 "encoding/binary"
5 "encoding/hex"
6 "errors"
7 "testing"
8)
9
10// Golden vectors are the bytes produced by the SPA reference builder
11// (spa/pod/builder.h, libspa 0.2 / PipeWire 1.6.9) for the same parameters
12// via spa_pod_builder_add_object / SPA_POD_CHOICE_* on amd64.
13const (
14 goldenEnumFormatSingle = "b80000000f00000003000400030000000100000000000000040000000300000002000000000000000200000000000000040000000300000001000000000000000100020000000000040000000300000008000000000000000300020000000000080000000a00000040010000f00000000400020000000000080000000b0000000000000001000000050002000000000028000000130000000100000000000000080000000b0000003c0000000100000001000000010000003c00000001000000"
15 goldenEnumFormatMulti = "d00000000f000000030004000300000001000000000000000400000003000000020000000000000002000000000000000400000003000000010000000000000001000200000000001c000000130000000300000000000000040000000300000008000000080000000c000000000000000300020000000000080000000a00000080070000380400000400020000000000080000000b0000000000000001000000050002000000000028000000130000000100000000000000080000000b000000900000000100000001000000010000009000000001000000"
16 goldenBuffers = "c00000000f000000040004000500000001000000000000001c0000001300000001000000000000000400000004000000080000000200000010000000000000000200000000000000040000000400000001000000000000000300000000000000040000000400000000b004000000000004000000000000000400000004000000000500000000000005000000000000000400000004000000100000000000000006000000000000001400000013000000040000000000000004000000040000000400000000000000"
17 goldenMetaHeader = "380000000f0000000500040006000000010000000000000004000000030000000100000000000000020000000000000004000000040000002000000000000000"
18)
19
20func mustHex(t testing.TB, s string) []byte {
21 t.Helper()
22 b, err := hex.DecodeString(s)
23 if err != nil {
24 t.Fatal(err)
25 }
26 return b
27}
28
29func TestVideoEnumFormatGoldenSingle(t *testing.T) {
30 var dst [512]byte
31 got, err := VideoEnumFormat(dst[:], 320, 240, 60, VideoFormatBGRx)
32 if err != nil {
33 t.Fatal(err)
34 }
35 if want := mustHex(t, goldenEnumFormatSingle); hex.EncodeToString(got) != hex.EncodeToString(want) {
36 t.Fatalf("got %x\nwant %x", got, want)
37 }
38}
39
40func TestVideoEnumFormatGoldenMulti(t *testing.T) {
41 var dst [512]byte
42 got, err := VideoEnumFormat(dst[:], 1920, 1080, 144, VideoFormatBGRx, VideoFormatBGRA)
43 if err != nil {
44 t.Fatal(err)
45 }
46 if want := mustHex(t, goldenEnumFormatMulti); hex.EncodeToString(got) != hex.EncodeToString(want) {
47 t.Fatalf("got %x\nwant %x", got, want)
48 }
49}
50
51func TestVideoBuffersGolden(t *testing.T) {
52 var dst [512]byte
53 got, err := VideoBuffers(dst[:], 320*240*4, 320*4, 2, 8, 16, 1<<DataMemFd)
54 if err != nil {
55 t.Fatal(err)
56 }
57 if want := mustHex(t, goldenBuffers); hex.EncodeToString(got) != hex.EncodeToString(want) {
58 t.Fatalf("got %x\nwant %x", got, want)
59 }
60}
61
62// TestBuildersExactFit checks each builder accepts a destination exactly as
63// long as its pod and rejects one byte less.
64func TestBuildersExactFit(t *testing.T) {
65 builds := map[string]func(dst []byte) ([]byte, error){
66 "enum single": func(dst []byte) ([]byte, error) { return VideoEnumFormat(dst, 320, 240, 60, VideoFormatBGRx) },
67 "enum multi": func(dst []byte) ([]byte, error) {
68 return VideoEnumFormat(dst, 1920, 1080, 144, VideoFormatBGRx, VideoFormatBGRA)
69 },
70 "buffers": func(dst []byte) ([]byte, error) { return VideoBuffers(dst, 320*240*4, 320*4, 2, 8, 16, 1<<DataMemFd) },
71 "meta": MetaHeader,
72 }
73 for name, build := range builds {
74 var big [512]byte
75 pod, err := build(big[:])
76 if err != nil {
77 t.Fatalf("%s: %v", name, err)
78 }
79 if _, err := build(make([]byte, len(pod))); err != nil {
80 t.Errorf("%s: exact %d-byte destination: %v", name, len(pod), err)
81 }
82 if _, err := build(make([]byte, len(pod)-1)); !errors.Is(err, ErrPodFull) {
83 t.Errorf("%s: short destination: %v, want ErrPodFull", name, err)
84 }
85 }
86}
87
88func TestVideoBuffersRejectsInt32Overflow(t *testing.T) {
89 var dst [512]byte
90 if _, err := VideoBuffers(dst[:], 1<<31, 1<<17, 2, 8, 16, 1<<DataMemFd); !errors.Is(err, ErrPodState) {
91 t.Fatalf("size 1<<31: %v, want ErrPodState", err)
92 }
93}
94
95func TestMetaHeaderGolden(t *testing.T) {
96 var dst [128]byte
97 got, err := MetaHeader(dst[:])
98 if err != nil {
99 t.Fatal(err)
100 }
101 if want := mustHex(t, goldenMetaHeader); hex.EncodeToString(got) != hex.EncodeToString(want) {
102 t.Fatalf("got %x\nwant %x", got, want)
103 }
104}
105
106// formatPod builds a negotiated (non-choice) Format object for parser tests.
107func formatPod(t testing.TB, dst []byte, format VideoFormat, w, h uint32) []byte {
108 t.Helper()
109 b := NewPodBuilder(dst)
110 b.Object(TypeObjectFormat, ParamFormat)
111 b.Prop(FormatKeyMediaType)
112 b.Id(MediaTypeVideo)
113 b.Prop(FormatKeyMediaSubtype)
114 b.Id(MediaSubtypeRaw)
115 b.Prop(FormatKeyVideoFormat)
116 b.Id(uint32(format))
117 b.Prop(FormatKeyVideoSize)
118 b.Rectangle(w, h)
119 b.Prop(FormatKeyVideoFramerate)
120 b.Fraction(60, 1)
121 b.End()
122 pod, err := b.Bytes()
123 if err != nil {
124 t.Fatal(err)
125 }
126 return pod
127}
128
129// goldenNegotiatedFormat is a Format param exactly as PipeWire 1.6.9 delivers
130// it to param_changed for a 320x240 BGRx stream (captured from a live stream):
131// every value is a Choice of type None around one value.
132const goldenNegotiatedFormat = "f80000000f0000000300040004000000010000000000000014000000130000000000000000000000040000000300000002000000000000000200000000000000140000001300000000000000000000000400000003000000010000000000000001000200000000001400000013000000000000000000000004000000030000000800000000000000030002000000000018000000130000000000000000000000080000000a00000040010000f00000000400020000000000080000000b0000000000000001000000050002000000000028000000130000000000000000000000080000000b0000003c0000000100000001000000010000003c00000001000000"
133
134func TestParseVideoFormatChoiceNone(t *testing.T) {
135 f, w, h, ok := ParseVideoFormat(mustHex(t, goldenNegotiatedFormat))
136 if !ok || VideoFormat(f) != VideoFormatBGRx || w != 320 || h != 240 {
137 t.Fatalf("parse = %d %d %d %v", f, w, h, ok)
138 }
139 // Every truncation of the real pod is rejected without panicking.
140 pod := mustHex(t, goldenNegotiatedFormat)
141 for n := 0; n < len(pod); n++ {
142 if _, _, _, ok := ParseVideoFormat(pod[:n]); ok {
143 t.Fatalf("truncated to %d bytes accepted", n)
144 }
145 }
146}
147
148func TestParseVideoFormatRoundTrip(t *testing.T) {
149 var dst [256]byte
150 for _, tc := range []struct {
151 f VideoFormat
152 w, h int
153 }{{VideoFormatBGRx, 320, 240}, {VideoFormatBGRA, 3840, 2160}} {
154 pod := formatPod(t, dst[:], tc.f, uint32(tc.w), uint32(tc.h))
155 f, w, h, ok := ParseVideoFormat(pod)
156 if !ok || VideoFormat(f) != tc.f || w != tc.w || h != tc.h {
157 t.Fatalf("parse = %d %d %d %v, want %d %d %d", f, w, h, ok, tc.f, tc.w, tc.h)
158 }
159 }
160}
161
162func TestParseVideoFormatRejectsMalformed(t *testing.T) {
163 var dst [256]byte
164 pod := append([]byte(nil), formatPod(t, dst[:], VideoFormatBGRx, 320, 240)...)
165
166 // Every truncation must be rejected without panicking.
167 for n := 0; n < len(pod); n++ {
168 if _, _, _, ok := ParseVideoFormat(pod[:n]); ok {
169 t.Fatalf("truncated to %d bytes accepted", n)
170 }
171 }
172 // Non-object, wrong object type, oversized declared size.
173 bad := append([]byte(nil), pod...)
174 binary.LittleEndian.PutUint32(bad[4:], TypeInt)
175 if _, _, _, ok := ParseVideoFormat(bad); ok {
176 t.Fatal("non-object accepted")
177 }
178 bad = append([]byte(nil), pod...)
179 binary.LittleEndian.PutUint32(bad[8:], TypeObjectParamBuffers)
180 if _, _, _, ok := ParseVideoFormat(bad); ok {
181 t.Fatal("wrong object type accepted")
182 }
183 bad = append([]byte(nil), pod...)
184 binary.LittleEndian.PutUint32(bad[0:], 1<<30)
185 if _, _, _, ok := ParseVideoFormat(bad); ok {
186 t.Fatal("oversized pod accepted")
187 }
188 // Property value size overflowing the object.
189 bad = append([]byte(nil), pod...)
190 binary.LittleEndian.PutUint32(bad[16+8:], 1<<31)
191 if _, _, _, ok := ParseVideoFormat(bad); ok {
192 t.Fatal("oversized property accepted")
193 }
194 // Zero and absurd sizes.
195 if _, _, _, ok := ParseVideoFormat(formatPod(t, dst[:], VideoFormatBGRx, 0, 240)); ok {
196 t.Fatal("zero width accepted")
197 }
198 if _, _, _, ok := ParseVideoFormat(formatPod(t, dst[:], VideoFormatBGRx, 1<<24, 240)); ok {
199 t.Fatal("huge width accepted")
200 }
201 // An EnumFormat with a choice for the format is not a negotiated format.
202 var big [512]byte
203 enum, err := VideoEnumFormat(big[:], 320, 240, 60, VideoFormatBGRx, VideoFormatBGRA)
204 if err != nil {
205 t.Fatal(err)
206 }
207 if _, _, _, ok := ParseVideoFormat(enum); ok {
208 t.Fatal("choice format accepted")
209 }
210}
211
212func TestPodBuilderErrors(t *testing.T) {
213 // Buffer too small: error, no panic, for every size up to the full pod.
214 var full [512]byte
215 pod, err := VideoEnumFormat(full[:], 320, 240, 60, VideoFormatBGRx, VideoFormatBGRA)
216 if err != nil {
217 t.Fatal(err)
218 }
219 for n := 0; n < len(pod); n++ {
220 small := make([]byte, n)
221 if _, err := VideoEnumFormat(small, 320, 240, 60, VideoFormatBGRx, VideoFormatBGRA); err != ErrPodFull {
222 t.Fatalf("size %d: err = %v, want ErrPodFull", n, err)
223 }
224 }
225 if _, err := VideoBuffers(make([]byte, 16), 1, 1, 2, 8, 16, 4); err != ErrPodFull {
226 t.Fatalf("buffers small: %v", err)
227 }
228 if _, err := MetaHeader(make([]byte, 8)); err != ErrPodFull {
229 t.Fatalf("meta small: %v", err)
230 }
231 // Invalid arguments.
232 if _, err := VideoEnumFormat(full[:], 0, 240, 60, VideoFormatBGRx); err == nil {
233 t.Fatal("zero width accepted")
234 }
235 if _, err := VideoEnumFormat(full[:], 320, 240, 60); err == nil {
236 t.Fatal("no formats accepted")
237 }
238 if _, err := VideoBuffers(full[:], 0, 4, 2, 8, 16, 4); err == nil {
239 t.Fatal("zero size accepted")
240 }
241 // Unbalanced builder use.
242 b := NewPodBuilder(full[:])
243 b.Prop(1)
244 if _, err := b.Bytes(); err != ErrPodState {
245 t.Fatalf("prop outside object: %v", err)
246 }
247 b = NewPodBuilder(full[:])
248 b.Object(TypeObjectFormat, ParamFormat)
249 if _, err := b.Bytes(); err != ErrPodState {
250 t.Fatalf("open object: %v", err)
251 }
252 b = NewPodBuilder(full[:])
253 b.End()
254 if _, err := b.Bytes(); err != ErrPodState {
255 t.Fatalf("end without object: %v", err)
256 }
257}
258
259func TestPodAllocations(t *testing.T) {
260 var dst [512]byte
261 pod := append([]byte(nil), formatPod(t, dst[:], VideoFormatBGRx, 320, 240)...)
262 real := mustHex(t, goldenNegotiatedFormat)
263 allocs := testing.AllocsPerRun(200, func() {
264 e, _ := VideoEnumFormat(dst[:], 320, 240, 60, VideoFormatBGRx, VideoFormatBGRA)
265 _, _ = VideoBuffers(dst[:], 307200, 1280, 2, 8, 16, 1<<DataMemFd)
266 _, _ = MetaHeader(dst[:])
267 _ = e
268 _, _, _, _ = ParseVideoFormat(pod)
269 _, _, _, _ = ParseVideoFormat(real)
270 })
271 if allocs != 0 {
272 t.Fatalf("allocs = %v, want 0", allocs)
273 }
274}