detached 4c97002

pod.go RawHistoryFolder

1package spa
2
3import (
4	"encoding/binary"
5	"errors"
6	"math"
7)
8
9// Errors returned by the pod builder.
10var (
11	// ErrPodFull is returned when the destination buffer is too small.
12	ErrPodFull = errors.New("spa: pod buffer full")
13	// ErrPodState is returned for unbalanced Object/End or Prop outside an object.
14	ErrPodState = errors.New("spa: invalid pod builder state")
15)
16
17const podMaxDepth = 4
18
19// Fraction is a SPA fraction (num/denom).
20type Fraction struct{ Num, Denom uint32 }
21
22// PodBuilder encodes SPA pods little-endian into a caller-owned buffer. It
23// never allocates and never grows past len(buf): when the buffer is full the
24// first error is kept and returned by Bytes. The zero value is unusable; use
25// NewPodBuilder.
26type PodBuilder struct {
27	buf   []byte
28	n     int
29	err   error
30	depth int
31	stack [podMaxDepth]int // offset of each open object header
32}
33
34// NewPodBuilder returns a builder writing into buf.
35func NewPodBuilder(buf []byte) PodBuilder { return PodBuilder{buf: buf} }
36
37// Bytes returns the encoded pod bytes (a prefix of the destination buffer) or
38// the first error. It fails if an Object is still open.
39func (b *PodBuilder) Bytes() ([]byte, error) {
40	if b.err != nil {
41		return nil, b.err
42	}
43	if b.depth != 0 {
44		return nil, ErrPodState
45	}
46	return b.buf[:b.n], nil
47}
48
49func (b *PodBuilder) fail(err error) {
50	if b.err == nil {
51		b.err = err
52	}
53}
54
55// room reports whether n more bytes fit, recording ErrPodFull otherwise.
56func (b *PodBuilder) room(n int) bool {
57	if b.err != nil {
58		return false
59	}
60	if n < 0 || b.n+n > len(b.buf) {
61		b.fail(ErrPodFull)
62		return false
63	}
64	return true
65}
66
67func (b *PodBuilder) u32(v uint32) {
68	binary.LittleEndian.PutUint32(b.buf[b.n:], v)
69	b.n += 4
70}
71
72// pad8 zero-pads the write offset to a multiple of 8 bytes.
73func (b *PodBuilder) pad8() {
74	for b.n&7 != 0 {
75		b.buf[b.n] = 0
76		b.n++
77	}
78}
79
80// Object opens an object pod. Close it with End.
81func (b *PodBuilder) Object(objType, id uint32) {
82	if !b.room(16) {
83		return
84	}
85	if b.depth == podMaxDepth {
86		b.fail(ErrPodState)
87		return
88	}
89	b.stack[b.depth] = b.n
90	b.depth++
91	b.u32(0) // size, patched by End
92	b.u32(TypeObject)
93	b.u32(objType)
94	b.u32(id)
95}
96
97// Prop starts an object property with the given key; the next value written
98// is the property value.
99func (b *PodBuilder) Prop(key uint32) {
100	if b.err == nil && b.depth == 0 {
101		b.fail(ErrPodState)
102		return
103	}
104	if !b.room(8) {
105		return
106	}
107	b.u32(key)
108	b.u32(0) // flags
109}
110
111// End closes the innermost open object.
112func (b *PodBuilder) End() {
113	if b.err != nil {
114		return
115	}
116	if b.depth == 0 {
117		b.fail(ErrPodState)
118		return
119	}
120	b.depth--
121	start := b.stack[b.depth]
122	binary.LittleEndian.PutUint32(b.buf[start:], uint32(b.n-start-8))
123	// Padding was already room-checked by the last value; objects always
124	// end on an 8-byte boundary because every value is padded.
125	b.pad8()
126}
127
128func (b *PodBuilder) scalar(typ uint32, size uint32, a, c uint32) {
129	// size is the body size (4 or 8); the pod is padded to 16 bytes.
130	if !b.room(16) {
131		return
132	}
133	b.u32(size)
134	b.u32(typ)
135	b.u32(a)
136	b.u32(c)
137}
138
139// Id appends an Id pod.
140func (b *PodBuilder) Id(v uint32) { b.scalar(TypeId, 4, v, 0) }
141
142// Int appends an Int pod.
143func (b *PodBuilder) Int(v int32) { b.scalar(TypeInt, 4, uint32(v), 0) }
144
145// Rectangle appends a Rectangle pod.
146func (b *PodBuilder) Rectangle(w, h uint32) { b.scalar(TypeRectangle, 8, w, h) }
147
148// Fraction appends a Fraction pod.
149func (b *PodBuilder) Fraction(num, denom uint32) { b.scalar(TypeFraction, 8, num, denom) }
150
151// choiceBegin writes the choice header; it returns the header offset.
152func (b *PodBuilder) choiceBegin(choiceType, childSize, childType uint32, nvals int) (int, bool) {
153	// Room for the header, the values and the padding choiceEnd adds.
154	if !b.room((b.n+24+nvals*int(childSize)+7)&^7 - b.n) {
155		return 0, false
156	}
157	start := b.n
158	b.u32(0) // size, patched in choiceEnd
159	b.u32(TypeChoice)
160	b.u32(choiceType)
161	b.u32(0) // flags
162	b.u32(childSize)
163	b.u32(childType)
164	return start, true
165}
166
167func (b *PodBuilder) choiceEnd(start int) {
168	binary.LittleEndian.PutUint32(b.buf[start:], uint32(b.n-start-8))
169	b.pad8()
170}
171
172// ChoiceEnumId appends an enum choice of Ids. vals is written verbatim: the
173// first value is the default and the remaining values are the alternatives.
174func (b *PodBuilder) ChoiceEnumId(vals ...uint32) {
175	if len(vals) == 0 {
176		b.fail(ErrPodState)
177		return
178	}
179	start, ok := b.choiceBegin(ChoiceEnum, 4, TypeId, len(vals))
180	if !ok {
181		return
182	}
183	for _, v := range vals {
184		b.u32(v)
185	}
186	b.choiceEnd(start)
187}
188
189// ChoiceRangeInt appends a range choice of Ints.
190func (b *PodBuilder) ChoiceRangeInt(def, min, max int32) {
191	start, ok := b.choiceBegin(ChoiceRange, 4, TypeInt, 3)
192	if !ok {
193		return
194	}
195	b.u32(uint32(def))
196	b.u32(uint32(min))
197	b.u32(uint32(max))
198	b.choiceEnd(start)
199}
200
201// ChoiceRangeFraction appends a range choice of Fractions.
202func (b *PodBuilder) ChoiceRangeFraction(def, min, max Fraction) {
203	start, ok := b.choiceBegin(ChoiceRange, 8, TypeFraction, 3)
204	if !ok {
205		return
206	}
207	for _, f := range [3]Fraction{def, min, max} {
208		b.u32(f.Num)
209		b.u32(f.Denom)
210	}
211	b.choiceEnd(start)
212}
213
214// ChoiceFlagsInt appends a flags choice of Ints with a single flags value.
215func (b *PodBuilder) ChoiceFlagsInt(flags uint32) {
216	start, ok := b.choiceBegin(ChoiceFlags, 4, TypeInt, 1)
217	if !ok {
218		return
219	}
220	b.u32(flags)
221	b.choiceEnd(start)
222}
223
224// VideoEnumFormat encodes an EnumFormat param for raw video of the given
225// size, maximum frame rate and formats (the first format is the default).
226// Modifiers are omitted (shared memory only).
227func VideoEnumFormat(dst []byte, w, h, maxFPS int, formats ...VideoFormat) ([]byte, error) {
228	if len(formats) == 0 || w <= 0 || h <= 0 || maxFPS <= 0 {
229		return nil, ErrPodState
230	}
231	b := NewPodBuilder(dst)
232	b.Object(TypeObjectFormat, ParamEnumFormat)
233	b.Prop(FormatKeyMediaType)
234	b.Id(MediaTypeVideo)
235	b.Prop(FormatKeyMediaSubtype)
236	b.Id(MediaSubtypeRaw)
237	b.Prop(FormatKeyVideoFormat)
238	if len(formats) == 1 {
239		b.Id(uint32(formats[0]))
240	} else {
241		b.podEnumFormats(formats)
242	}
243	b.Prop(FormatKeyVideoSize)
244	b.Rectangle(uint32(w), uint32(h))
245	b.Prop(FormatKeyVideoFramerate)
246	b.Fraction(0, 1)
247	b.Prop(FormatKeyVideoMaxRate)
248	b.ChoiceRangeFraction(
249		Fraction{uint32(maxFPS), 1},
250		Fraction{1, 1},
251		Fraction{uint32(maxFPS), 1},
252	)
253	b.End()
254	return b.Bytes()
255}
256
257// podEnumFormats writes an enum choice of video formats. As in SPA's own
258// builders, the first value is the default and is repeated in the list of
259// alternatives.
260func (b *PodBuilder) podEnumFormats(formats []VideoFormat) {
261	start, ok := b.choiceBegin(ChoiceEnum, 4, TypeId, len(formats)+1)
262	if !ok {
263		return
264	}
265	b.u32(uint32(formats[0]))
266	for _, f := range formats {
267		b.u32(uint32(f))
268	}
269	b.choiceEnd(start)
270}
271
272// VideoBuffers encodes a Buffers param: one block of size bytes with the
273// given stride, a buffer count range (minBuf..maxBuf, defBuf by default) and
274// the allowed SPA data types as a bit mask (for example 1<<DataMemFd).
275func VideoBuffers(dst []byte, size, stride, minBuf, defBuf, maxBuf int, dataTypes uint32) ([]byte, error) {
276	if size <= 0 || stride <= 0 || minBuf <= 0 || defBuf < minBuf || maxBuf < defBuf ||
277		size > math.MaxInt32 || stride > math.MaxInt32 || maxBuf > math.MaxInt32 {
278		return nil, ErrPodState
279	}
280	b := NewPodBuilder(dst)
281	b.Object(TypeObjectParamBuffers, ParamBuffers)
282	b.Prop(BuffersKeyBuffers)
283	b.ChoiceRangeInt(int32(defBuf), int32(minBuf), int32(maxBuf))
284	b.Prop(BuffersKeyBlocks)
285	b.Int(1)
286	b.Prop(BuffersKeySize)
287	b.Int(int32(size))
288	b.Prop(BuffersKeyStride)
289	b.Int(int32(stride))
290	b.Prop(BuffersKeyAlign)
291	b.Int(16)
292	b.Prop(BuffersKeyDataType)
293	b.ChoiceFlagsInt(dataTypes)
294	b.End()
295	return b.Bytes()
296}
297
298// MetaHeader encodes a Meta param requesting a spa_meta_header (32 bytes).
299func MetaHeader(dst []byte) ([]byte, error) {
300	b := NewPodBuilder(dst)
301	b.Object(TypeObjectParamMeta, ParamMeta)
302	b.Prop(MetaKeyType)
303	b.Id(MetaTypeHeader)
304	b.Prop(MetaKeySize)
305	b.Int(MetaHeaderSize)
306	b.End()
307	return b.Bytes()
308}
309
310// ParseVideoFormat extracts the video format and size from a Format param
311// (an Object pod). It does not allocate. ok is false for malformed,
312// truncated or non-raw-video pods.
313//
314// PipeWire wraps every value of a negotiated Format in a Choice of type None
315// holding a single value; plain values are accepted as well. Choices that
316// still offer alternatives (Enum, Range, ...) are not a fixed format and are
317// rejected.
318func ParseVideoFormat(pod []byte) (format uint32, w, h int, ok bool) {
319	if len(pod) < 16 {
320		return 0, 0, 0, false
321	}
322	le := binary.LittleEndian
323	size := int(le.Uint32(pod[0:]))
324	if le.Uint32(pod[4:]) != TypeObject || size < 8 || 8+size > len(pod) {
325		return 0, 0, 0, false
326	}
327	if le.Uint32(pod[8:]) != TypeObjectFormat {
328		return 0, 0, 0, false
329	}
330	end := 8 + size
331	off := 16
332	var haveFmt, haveSize, isVideo, isRaw bool
333	for off+16 <= end {
334		key := le.Uint32(pod[off:])
335		vsize := int(le.Uint32(pod[off+8:]))
336		vtype := le.Uint32(pod[off+12:])
337		body := off + 16
338		if vsize < 0 || body+vsize > end {
339			return 0, 0, 0, false
340		}
341		next := (body + vsize + 7) &^ 7
342		if next <= off {
343			return 0, 0, 0, false
344		}
345		if vtype == TypeChoice {
346			// Choice body: type, flags, then the child pod header (size, type)
347			// and the values.
348			if vsize < 16 || le.Uint32(pod[body:]) != ChoiceNone {
349				return 0, 0, 0, false
350			}
351			childSize := int(le.Uint32(pod[body+8:]))
352			vtype = le.Uint32(pod[body+12:])
353			body += 16
354			vsize -= 16
355			if childSize < 0 || childSize > vsize {
356				return 0, 0, 0, false
357			}
358			vsize = childSize
359		}
360		switch {
361		case key == FormatKeyMediaType && vtype == TypeId && vsize >= 4:
362			isVideo = le.Uint32(pod[body:]) == MediaTypeVideo
363		case key == FormatKeyMediaSubtype && vtype == TypeId && vsize >= 4:
364			isRaw = le.Uint32(pod[body:]) == MediaSubtypeRaw
365		case key == FormatKeyVideoFormat && vtype == TypeId && vsize >= 4:
366			format = le.Uint32(pod[body:])
367			haveFmt = true
368		case key == FormatKeyVideoSize && vtype == TypeRectangle && vsize >= 8:
369			uw, uh := le.Uint32(pod[body:]), le.Uint32(pod[body+4:])
370			if uw == 0 || uh == 0 || uw > 1<<20 || uh > 1<<20 {
371				return 0, 0, 0, false
372			}
373			w, h = int(uw), int(uh)
374			haveSize = true
375		}
376		off = next
377	}
378	if !(haveFmt && haveSize && isVideo && isRaw) {
379		return 0, 0, 0, false
380	}
381	return format, w, h, true
382}

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