detached 9177a2f

fix(spa): parse negotiated formats whose values are single-value choices

commit
9177a2fa271473fba82c330039a9a5c0daab95ff
author
brice · · 2026-10-14 19:28 UTC
parent
5f60d27
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +45 -5
M spa/pod.go +24 -5
@@ -307,6 +307,11 @@ func MetaHeader(dst []byte) ([]byte, error) {
307307 // ParseVideoFormat extracts the video format and size from a Format param
308308 // (an Object pod). It does not allocate. ok is false for malformed,
309309 // truncated or non-raw-video pods.
310+//
311+// PipeWire wraps every value of a negotiated Format in a Choice of type None
312+// holding a single value; plain values are accepted as well. Choices that
313+// still offer alternatives (Enum, Range, ...) are not a fixed format and are
314+// rejected.
310315 func ParseVideoFormat(pod []byte) (format uint32, w, h int, ok bool) {
311316 if len(pod) < 16 {
312317 return 0, 0, 0, false
@@ -330,6 +335,25 @@ func ParseVideoFormat(pod []byte) (format uint32, w, h int, ok bool) {
330335 if vsize < 0 || body+vsize > end {
331336 return 0, 0, 0, false
332337 }
338+ next := (body + vsize + 7) &^ 7
339+ if next <= off {
340+ return 0, 0, 0, false
341+ }
342+ if vtype == TypeChoice {
343+ // Choice body: type, flags, then the child pod header (size, type)
344+ // and the values.
345+ if vsize < 16 || le.Uint32(pod[body:]) != ChoiceNone {
346+ return 0, 0, 0, false
347+ }
348+ childSize := int(le.Uint32(pod[body+8:]))
349+ vtype = le.Uint32(pod[body+12:])
350+ body += 16
351+ vsize -= 16
352+ if childSize < 0 || childSize > vsize {
353+ return 0, 0, 0, false
354+ }
355+ vsize = childSize
356+ }
333357 switch {
334358 case key == FormatKeyMediaType && vtype == TypeId && vsize >= 4:
335359 isVideo = le.Uint32(pod[body:]) == MediaTypeVideo
@@ -346,11 +370,6 @@ func ParseVideoFormat(pod []byte) (format uint32, w, h int, ok bool) {
346370 w, h = int(uw), int(uh)
347371 haveSize = true
348372 }
349- next := body + vsize
350- next = (next + 7) &^ 7
351- if next <= off {
352- return 0, 0, 0, false
353- }
354373 off = next
355374 }
356375 if !(haveFmt && haveSize && isVideo && isRaw) {
@@ -307,6 +307,11 @@ func MetaHeader(dst []byte) ([]byte, error) {
307 // ParseVideoFormat extracts the video format and size from a Format param307 // ParseVideoFormat extracts the video format and size from a Format param
308 // (an Object pod). It does not allocate. ok is false for malformed,308 // (an Object pod). It does not allocate. ok is false for malformed,
309 // truncated or non-raw-video pods.309 // truncated or non-raw-video pods.
310+//
311+// PipeWire wraps every value of a negotiated Format in a Choice of type None
312+// holding a single value; plain values are accepted as well. Choices that
313+// still offer alternatives (Enum, Range, ...) are not a fixed format and are
314+// rejected.
310 func ParseVideoFormat(pod []byte) (format uint32, w, h int, ok bool) {315 func ParseVideoFormat(pod []byte) (format uint32, w, h int, ok bool) {
311 if len(pod) < 16 {316 if len(pod) < 16 {
312 return 0, 0, 0, false317 return 0, 0, 0, false
@@ -330,6 +335,25 @@ func ParseVideoFormat(pod []byte) (format uint32, w, h int, ok bool) {
330 if vsize < 0 || body+vsize > end {335 if vsize < 0 || body+vsize > end {
331 return 0, 0, 0, false336 return 0, 0, 0, false
332 }337 }
338+ next := (body + vsize + 7) &^ 7
339+ if next <= off {
340+ return 0, 0, 0, false
341+ }
342+ if vtype == TypeChoice {
343+ // Choice body: type, flags, then the child pod header (size, type)
344+ // and the values.
345+ if vsize < 16 || le.Uint32(pod[body:]) != ChoiceNone {
346+ return 0, 0, 0, false
347+ }
348+ childSize := int(le.Uint32(pod[body+8:]))
349+ vtype = le.Uint32(pod[body+12:])
350+ body += 16
351+ vsize -= 16
352+ if childSize < 0 || childSize > vsize {
353+ return 0, 0, 0, false
354+ }
355+ vsize = childSize
356+ }
333 switch {357 switch {
334 case key == FormatKeyMediaType && vtype == TypeId && vsize >= 4:358 case key == FormatKeyMediaType && vtype == TypeId && vsize >= 4:
335 isVideo = le.Uint32(pod[body:]) == MediaTypeVideo359 isVideo = le.Uint32(pod[body:]) == MediaTypeVideo
@@ -346,11 +370,6 @@ func ParseVideoFormat(pod []byte) (format uint32, w, h int, ok bool) {
346 w, h = int(uw), int(uh)370 w, h = int(uw), int(uh)
347 haveSize = true371 haveSize = true
348 }372 }
349- next := body + vsize
350- next = (next + 7) &^ 7
351- if next <= off {
352- return 0, 0, 0, false
353- }
354 off = next373 off = next
355 }374 }
356 if !(haveFmt && haveSize && isVideo && isRaw) {375 if !(haveFmt && haveSize && isVideo && isRaw) {
M spa/pod_test.go +21 -0
@@ -92,6 +92,25 @@ func formatPod(t testing.TB, dst []byte, format VideoFormat, w, h uint32) []byte
9292 return pod
9393 }
9494
95+// goldenNegotiatedFormat is a Format param exactly as PipeWire 1.6.9 delivers
96+// it to param_changed for a 320x240 BGRx stream (captured from a live stream):
97+// every value is a Choice of type None around one value.
98+const goldenNegotiatedFormat = "f80000000f0000000300040004000000010000000000000014000000130000000000000000000000040000000300000002000000000000000200000000000000140000001300000000000000000000000400000003000000010000000000000001000200000000001400000013000000000000000000000004000000030000000800000000000000030002000000000018000000130000000000000000000000080000000a00000040010000f00000000400020000000000080000000b0000000000000001000000050002000000000028000000130000000000000000000000080000000b0000003c0000000100000001000000010000003c00000001000000"
99+
100+func TestParseVideoFormatChoiceNone(t *testing.T) {
101+ f, w, h, ok := ParseVideoFormat(mustHex(t, goldenNegotiatedFormat))
102+ if !ok || VideoFormat(f) != VideoFormatBGRx || w != 320 || h != 240 {
103+ t.Fatalf("parse = %d %d %d %v", f, w, h, ok)
104+ }
105+ // Every truncation of the real pod is rejected without panicking.
106+ pod := mustHex(t, goldenNegotiatedFormat)
107+ for n := 0; n < len(pod); n++ {
108+ if _, _, _, ok := ParseVideoFormat(pod[:n]); ok {
109+ t.Fatalf("truncated to %d bytes accepted", n)
110+ }
111+ }
112+}
113+
95114 func TestParseVideoFormatRoundTrip(t *testing.T) {
96115 var dst [256]byte
97116 for _, tc := range []struct {
@@ -206,12 +225,14 @@ func TestPodBuilderErrors(t *testing.T) {
206225 func TestPodAllocations(t *testing.T) {
207226 var dst [512]byte
208227 pod := append([]byte(nil), formatPod(t, dst[:], VideoFormatBGRx, 320, 240)...)
228+ real := mustHex(t, goldenNegotiatedFormat)
209229 allocs := testing.AllocsPerRun(200, func() {
210230 e, _ := VideoEnumFormat(dst[:], 320, 240, 60, VideoFormatBGRx, VideoFormatBGRA)
211231 _, _ = VideoBuffers(dst[:], 307200, 1280, 2, 8, 16, 1<<DataMemFd)
212232 _, _ = MetaHeader(dst[:])
213233 _ = e
214234 _, _, _, _ = ParseVideoFormat(pod)
235+ _, _, _, _ = ParseVideoFormat(real)
215236 })
216237 if allocs != 0 {
217238 t.Fatalf("allocs = %v, want 0", allocs)
@@ -92,6 +92,25 @@ func formatPod(t testing.TB, dst []byte, format VideoFormat, w, h uint32) []byte
92 return pod92 return pod
93 }93 }
94 94
95+// goldenNegotiatedFormat is a Format param exactly as PipeWire 1.6.9 delivers
96+// it to param_changed for a 320x240 BGRx stream (captured from a live stream):
97+// every value is a Choice of type None around one value.
98+const goldenNegotiatedFormat = "f80000000f0000000300040004000000010000000000000014000000130000000000000000000000040000000300000002000000000000000200000000000000140000001300000000000000000000000400000003000000010000000000000001000200000000001400000013000000000000000000000004000000030000000800000000000000030002000000000018000000130000000000000000000000080000000a00000040010000f00000000400020000000000080000000b0000000000000001000000050002000000000028000000130000000000000000000000080000000b0000003c0000000100000001000000010000003c00000001000000"
99+
100+func TestParseVideoFormatChoiceNone(t *testing.T) {
101+ f, w, h, ok := ParseVideoFormat(mustHex(t, goldenNegotiatedFormat))
102+ if !ok || VideoFormat(f) != VideoFormatBGRx || w != 320 || h != 240 {
103+ t.Fatalf("parse = %d %d %d %v", f, w, h, ok)
104+ }
105+ // Every truncation of the real pod is rejected without panicking.
106+ pod := mustHex(t, goldenNegotiatedFormat)
107+ for n := 0; n < len(pod); n++ {
108+ if _, _, _, ok := ParseVideoFormat(pod[:n]); ok {
109+ t.Fatalf("truncated to %d bytes accepted", n)
110+ }
111+ }
112+}
113+
95 func TestParseVideoFormatRoundTrip(t *testing.T) {114 func TestParseVideoFormatRoundTrip(t *testing.T) {
96 var dst [256]byte115 var dst [256]byte
97 for _, tc := range []struct {116 for _, tc := range []struct {
@@ -206,12 +225,14 @@ func TestPodBuilderErrors(t *testing.T) {
206 func TestPodAllocations(t *testing.T) {225 func TestPodAllocations(t *testing.T) {
207 var dst [512]byte226 var dst [512]byte
208 pod := append([]byte(nil), formatPod(t, dst[:], VideoFormatBGRx, 320, 240)...)227 pod := append([]byte(nil), formatPod(t, dst[:], VideoFormatBGRx, 320, 240)...)
228+ real := mustHex(t, goldenNegotiatedFormat)
209 allocs := testing.AllocsPerRun(200, func() {229 allocs := testing.AllocsPerRun(200, func() {
210 e, _ := VideoEnumFormat(dst[:], 320, 240, 60, VideoFormatBGRx, VideoFormatBGRA)230 e, _ := VideoEnumFormat(dst[:], 320, 240, 60, VideoFormatBGRx, VideoFormatBGRA)
211 _, _ = VideoBuffers(dst[:], 307200, 1280, 2, 8, 16, 1<<DataMemFd)231 _, _ = VideoBuffers(dst[:], 307200, 1280, 2, 8, 16, 1<<DataMemFd)
212 _, _ = MetaHeader(dst[:])232 _, _ = MetaHeader(dst[:])
213 _ = e233 _ = e
214 _, _, _, _ = ParseVideoFormat(pod)234 _, _, _, _ = ParseVideoFormat(pod)
235+ _, _, _, _ = ParseVideoFormat(real)
215 })236 })
216 if allocs != 0 {237 if allocs != 0 {
217 t.Fatalf("allocs = %v, want 0", allocs)238 t.Fatalf("allocs = %v, want 0", allocs)

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 1

Tags 8

↑↓ moveenter openesc close