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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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) { | |||
| 307 | 307 | // ParseVideoFormat extracts the video format and size from a Format param | |
| 308 | 308 | // (an Object pod). It does not allocate. ok is false for malformed, | |
| 309 | 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 | 315 | func ParseVideoFormat(pod []byte) (format uint32, w, h int, ok bool) { | |
| 311 | 316 | if len(pod) < 16 { | |
| 312 | 317 | return 0, 0, 0, false | |
| @@ -330,6 +335,25 @@ func ParseVideoFormat(pod []byte) (format uint32, w, h int, ok bool) { | |||
| 330 | 335 | if vsize < 0 || body+vsize > end { | |
| 331 | 336 | 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 | 357 | switch { | |
| 334 | 358 | case key == FormatKeyMediaType && vtype == TypeId && vsize >= 4: | |
| 335 | 359 | isVideo = le.Uint32(pod[body:]) == MediaTypeVideo | |
| @@ -346,11 +370,6 @@ func ParseVideoFormat(pod []byte) (format uint32, w, h int, ok bool) { | |||
| 346 | 370 | w, h = int(uw), int(uh) | |
| 347 | 371 | 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 | 373 | off = next | |
| 355 | 374 | } | |
| 356 | 375 | 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 param | 307 | // 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, false | 317 | 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, false | 336 | 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:]) == MediaTypeVideo | 359 | 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 = true | 371 | 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 = next | 373 | 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 | |||
| 92 | 92 | 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 | 114 | func TestParseVideoFormatRoundTrip(t *testing.T) { | |
| 96 | 115 | var dst [256]byte | |
| 97 | 116 | for _, tc := range []struct { | |
| @@ -206,12 +225,14 @@ func TestPodBuilderErrors(t *testing.T) { | |||
| 206 | 225 | func TestPodAllocations(t *testing.T) { | |
| 207 | 226 | var dst [512]byte | |
| 208 | 227 | pod := append([]byte(nil), formatPod(t, dst[:], VideoFormatBGRx, 320, 240)...) | |
| 228 | + | real := mustHex(t, goldenNegotiatedFormat) | |
| 209 | 229 | allocs := testing.AllocsPerRun(200, func() { | |
| 210 | 230 | e, _ := VideoEnumFormat(dst[:], 320, 240, 60, VideoFormatBGRx, VideoFormatBGRA) | |
| 211 | 231 | _, _ = VideoBuffers(dst[:], 307200, 1280, 2, 8, 16, 1<<DataMemFd) | |
| 212 | 232 | _, _ = MetaHeader(dst[:]) | |
| 213 | 233 | _ = e | |
| 214 | 234 | _, _, _, _ = ParseVideoFormat(pod) | |
| 235 | + | _, _, _, _ = ParseVideoFormat(real) | |
| 215 | 236 | }) | |
| 216 | 237 | if allocs != 0 { | |
| 217 | 238 | 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 pod | 92 | 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]byte | 115 | 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]byte | 226 | 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 | _ = e | 233 | _ = 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) | ||