Compare commits
42 Commits
master
...
wip-pipewi
| Author | SHA1 | Date | |
|---|---|---|---|
| 5c12425d48 | |||
| cbe86dc4f0 | |||
| d08a1081bd | |||
| 72a2601d74 | |||
| 1dab87aaf0 | |||
| 2bafde99e3 | |||
| 91efeb101a | |||
| dcb22a61c0 | |||
| e028a61fc1 | |||
| 73987be7d4 | |||
| 563b5e66fc | |||
| 2edcfe1e68 | |||
| 2698ca00e8 | |||
| 1d0143386d | |||
| a55c209099 | |||
| 10ff276da1 | |||
| fd4d379b67 | |||
| 77f5b89a41 | |||
| 14e33f17e5 | |||
| cfeb7818eb | |||
| 05391da556 | |||
| 463f8836e6 | |||
| 2e465c94da | |||
| 26009fd3f7 | |||
| 2d7b896a8c | |||
| a0eb010aab | |||
| b1b27ac1df | |||
| fc3d78fe01 | |||
| 591637264a | |||
| e77652bf89 | |||
| 88d3e46413 | |||
| e51e81bb22 | |||
| 8f4a3bcf9f | |||
| 827dc9e1ba | |||
| d92de1c709 | |||
| 5bcafcf734 | |||
| 9f7b0c2f46 | |||
| 3e87187c4c | |||
| b651d95e77 | |||
| aab92ce3c1 | |||
| a495e09a8f | |||
| 3afca2bd5b |
@ -2,7 +2,6 @@ name: Test
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
- push
|
- push
|
||||||
- pull_request
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
hakurei:
|
hakurei:
|
||||||
|
|||||||
@ -12,8 +12,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"hakurei.app/hst"
|
"hakurei.app/hst"
|
||||||
"hakurei.app/internal/env"
|
|
||||||
"hakurei.app/internal/info"
|
|
||||||
"hakurei.app/internal/outcome"
|
"hakurei.app/internal/outcome"
|
||||||
"hakurei.app/internal/store"
|
"hakurei.app/internal/store"
|
||||||
"hakurei.app/message"
|
"hakurei.app/message"
|
||||||
@ -23,16 +21,14 @@ import (
|
|||||||
func printShowSystem(output io.Writer, short, flagJSON bool) {
|
func printShowSystem(output io.Writer, short, flagJSON bool) {
|
||||||
t := newPrinter(output)
|
t := newPrinter(output)
|
||||||
defer t.MustFlush()
|
defer t.MustFlush()
|
||||||
|
hi := outcome.Info()
|
||||||
hi := &hst.Info{Version: info.Version(), User: new(outcome.Hsu).MustID(nil)}
|
|
||||||
env.CopyPaths().Copy(&hi.Paths, hi.User)
|
|
||||||
|
|
||||||
if flagJSON {
|
if flagJSON {
|
||||||
encodeJSON(log.Fatal, output, short, hi)
|
encodeJSON(log.Fatal, output, short, hi)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Printf("Version:\t%s\n", hi.Version)
|
t.Printf("Version:\t%s (libwayland %s)\n", hi.Version, hi.WaylandVersion)
|
||||||
t.Printf("User:\t%d\n", hi.User)
|
t.Printf("User:\t%d\n", hi.User)
|
||||||
t.Printf("TempDir:\t%s\n", hi.TempDir)
|
t.Printf("TempDir:\t%s\n", hi.TempDir)
|
||||||
t.Printf("SharePath:\t%s\n", hi.SharePath)
|
t.Printf("SharePath:\t%s\n", hi.SharePath)
|
||||||
|
|||||||
@ -54,6 +54,9 @@ type Paths struct {
|
|||||||
|
|
||||||
// Info holds basic system information collected from the implementation.
|
// Info holds basic system information collected from the implementation.
|
||||||
type Info struct {
|
type Info struct {
|
||||||
|
// WaylandVersion is the libwayland value of WAYLAND_VERSION.
|
||||||
|
WaylandVersion string `json:"WAYLAND_VERSION"`
|
||||||
|
|
||||||
// Version is a hardcoded version string.
|
// Version is a hardcoded version string.
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
// User is the userid according to hsu.
|
// User is the userid according to hsu.
|
||||||
|
|||||||
@ -11,10 +11,22 @@ import (
|
|||||||
"hakurei.app/hst"
|
"hakurei.app/hst"
|
||||||
"hakurei.app/internal/acl"
|
"hakurei.app/internal/acl"
|
||||||
"hakurei.app/internal/env"
|
"hakurei.app/internal/env"
|
||||||
|
"hakurei.app/internal/info"
|
||||||
"hakurei.app/internal/system"
|
"hakurei.app/internal/system"
|
||||||
|
"hakurei.app/internal/wayland"
|
||||||
"hakurei.app/message"
|
"hakurei.app/message"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Info returns the address to a populated [hst.Info].
|
||||||
|
//
|
||||||
|
// This must not be called from within package outcome.
|
||||||
|
func Info() *hst.Info {
|
||||||
|
hi := hst.Info{WaylandVersion: wayland.Version,
|
||||||
|
Version: info.Version(), User: new(Hsu).MustID(nil)}
|
||||||
|
env.CopyPaths().Copy(&hi.Paths, hi.User)
|
||||||
|
return &hi
|
||||||
|
}
|
||||||
|
|
||||||
// envAllocSize is the initial size of the env map pre-allocated when the configured env map is nil.
|
// envAllocSize is the initial size of the env map pre-allocated when the configured env map is nil.
|
||||||
// It should be large enough to fit all insertions by outcomeOp.toContainer.
|
// It should be large enough to fit all insertions by outcomeOp.toContainer.
|
||||||
const envAllocSize = 1 << 6
|
const envAllocSize = 1 << 6
|
||||||
|
|||||||
76
internal/pipewire/client.go
Normal file
76
internal/pipewire/client.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package pipewire
|
||||||
|
|
||||||
|
/* pipewire/client.h */
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_TYPE_INTERFACE_Client = PW_TYPE_INFO_INTERFACE_BASE + "Client"
|
||||||
|
PW_CLIENT_PERM_MASK = PW_PERM_RWXM
|
||||||
|
PW_VERSION_CLIENT = 3
|
||||||
|
|
||||||
|
PW_ID_CLIENT = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_CLIENT_CHANGE_MASK_PROPS = 1 << iota
|
||||||
|
|
||||||
|
PW_CLIENT_CHANGE_MASK_ALL = 1<<iota - 1
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_CLIENT_EVENT_INFO = iota
|
||||||
|
PW_CLIENT_EVENT_PERMISSIONS
|
||||||
|
PW_CLIENT_EVENT_NUM
|
||||||
|
|
||||||
|
PW_VERSION_CLIENT_EVENTS = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_CLIENT_METHOD_ADD_LISTENER = iota
|
||||||
|
PW_CLIENT_METHOD_ERROR
|
||||||
|
PW_CLIENT_METHOD_UPDATE_PROPERTIES
|
||||||
|
PW_CLIENT_METHOD_GET_PERMISSIONS
|
||||||
|
PW_CLIENT_METHOD_UPDATE_PERMISSIONS
|
||||||
|
PW_CLIENT_METHOD_NUM
|
||||||
|
|
||||||
|
PW_VERSION_CLIENT_METHODS = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
// The ClientInfo event provides client information updates.
|
||||||
|
// This is emitted when binding to a client or when the client info is updated later.
|
||||||
|
type ClientInfo struct {
|
||||||
|
// The global id of the client.
|
||||||
|
ID Int `json:"id"`
|
||||||
|
// The changes emitted by this event.
|
||||||
|
ChangeMask Long `json:"change_mask"`
|
||||||
|
// Properties of this object, valid when change_mask has PW_CLIENT_CHANGE_MASK_PROPS.
|
||||||
|
Properties *SPADict `json:"props"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size satisfies [KnownSize] with a value computed at runtime.
|
||||||
|
func (c *ClientInfo) Size() Word {
|
||||||
|
return SizePrefix +
|
||||||
|
Size(SizeInt) +
|
||||||
|
Size(SizeLong) +
|
||||||
|
c.Properties.Size()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
|
||||||
|
func (c *ClientInfo) MarshalBinary() ([]byte, error) { return Marshal(c) }
|
||||||
|
|
||||||
|
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
|
||||||
|
func (c *ClientInfo) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
|
||||||
|
|
||||||
|
// ClientUpdateProperties is used to update the properties of a client.
|
||||||
|
type ClientUpdateProperties struct {
|
||||||
|
// Properties to update on the client.
|
||||||
|
Properties *SPADict `json:"props"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size satisfies [KnownSize] with a value computed at runtime.
|
||||||
|
func (c *ClientUpdateProperties) Size() Word { return SizePrefix + c.Properties.Size() }
|
||||||
|
|
||||||
|
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
|
||||||
|
func (c *ClientUpdateProperties) MarshalBinary() ([]byte, error) { return Marshal(c) }
|
||||||
|
|
||||||
|
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
|
||||||
|
func (c *ClientUpdateProperties) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
|
||||||
156
internal/pipewire/client_test.go
Normal file
156
internal/pipewire/client_test.go
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
package pipewire_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/internal/pipewire"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClientInfo(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
encodingTestCases[pipewire.ClientInfo, *pipewire.ClientInfo]{
|
||||||
|
{"sample", samplePWContainer[1][2][1], pipewire.ClientInfo{
|
||||||
|
ID: 34,
|
||||||
|
ChangeMask: pipewire.PW_CLIENT_CHANGE_MASK_PROPS,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "pipewire.protocol", Value: "protocol-native"},
|
||||||
|
{Key: "core.name", Value: "pipewire-0"},
|
||||||
|
{Key: "pipewire.sec.socket", Value: "pipewire-0-manager"},
|
||||||
|
{Key: "pipewire.sec.pid", Value: "1443"},
|
||||||
|
{Key: "pipewire.sec.uid", Value: "1000"},
|
||||||
|
{Key: "pipewire.sec.gid", Value: "100"},
|
||||||
|
{Key: "module.id", Value: "2"},
|
||||||
|
{Key: "object.id", Value: "34"},
|
||||||
|
{Key: "object.serial", Value: "34"},
|
||||||
|
}}, nil},
|
||||||
|
|
||||||
|
{"sample*", samplePWContainer[1][3][1], pipewire.ClientInfo{
|
||||||
|
ID: 34,
|
||||||
|
ChangeMask: pipewire.PW_CLIENT_CHANGE_MASK_PROPS,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "pipewire.protocol", Value: "protocol-native"},
|
||||||
|
{Key: "core.name", Value: "pipewire-alice-1443"},
|
||||||
|
{Key: "pipewire.sec.socket", Value: "pipewire-0-manager"},
|
||||||
|
{Key: "pipewire.sec.pid", Value: "1443"},
|
||||||
|
{Key: "pipewire.sec.uid", Value: "1000"},
|
||||||
|
{Key: "pipewire.sec.gid", Value: "100"},
|
||||||
|
{Key: "module.id", Value: "2"},
|
||||||
|
{Key: "object.id", Value: "34"},
|
||||||
|
{Key: "object.serial", Value: "34"},
|
||||||
|
{Key: "remote.intention", Value: "manager"},
|
||||||
|
{Key: "application.name", Value: "pw-container"},
|
||||||
|
{Key: "application.process.binary", Value: "pw-container"},
|
||||||
|
{Key: "application.language", Value: "en_US.UTF-8"},
|
||||||
|
{Key: "application.process.id", Value: "1443"},
|
||||||
|
{Key: "application.process.user", Value: "alice"},
|
||||||
|
{Key: "application.process.host", Value: "nixos"},
|
||||||
|
{Key: "application.process.session-id", Value: "1"},
|
||||||
|
{Key: "window.x11.display", Value: ":0"},
|
||||||
|
{Key: "cpu.vm.name", Value: "qemu"},
|
||||||
|
{Key: "log.level", Value: "0"},
|
||||||
|
{Key: "cpu.max-align", Value: "32"},
|
||||||
|
{Key: "default.clock.rate", Value: "48000"},
|
||||||
|
{Key: "default.clock.quantum", Value: "1024"},
|
||||||
|
{Key: "default.clock.min-quantum", Value: "32"},
|
||||||
|
{Key: "default.clock.max-quantum", Value: "2048"},
|
||||||
|
{Key: "default.clock.quantum-limit", Value: "8192"},
|
||||||
|
{Key: "default.clock.quantum-floor", Value: "4"},
|
||||||
|
{Key: "default.video.width", Value: "640"},
|
||||||
|
{Key: "default.video.height", Value: "480"},
|
||||||
|
{Key: "default.video.rate.num", Value: "25"},
|
||||||
|
{Key: "default.video.rate.denom", Value: "1"},
|
||||||
|
{Key: "clock.power-of-two-quantum", Value: "true"},
|
||||||
|
{Key: "link.max-buffers", Value: "64"},
|
||||||
|
{Key: "mem.warn-mlock", Value: "false"},
|
||||||
|
{Key: "mem.allow-mlock", Value: "true"},
|
||||||
|
{Key: "settings.check-quantum", Value: "false"},
|
||||||
|
{Key: "settings.check-rate", Value: "false"},
|
||||||
|
{Key: "core.version", Value: "1.4.7"},
|
||||||
|
}}, nil},
|
||||||
|
|
||||||
|
{"sample**", samplePWContainer[1][4][1], pipewire.ClientInfo{
|
||||||
|
ID: 34,
|
||||||
|
ChangeMask: pipewire.PW_CLIENT_CHANGE_MASK_PROPS,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "pipewire.protocol", Value: "protocol-native"},
|
||||||
|
{Key: "core.name", Value: "pipewire-alice-1443"},
|
||||||
|
{Key: "pipewire.sec.socket", Value: "pipewire-0-manager"},
|
||||||
|
{Key: "pipewire.sec.pid", Value: "1443"},
|
||||||
|
{Key: "pipewire.sec.uid", Value: "1000"},
|
||||||
|
{Key: "pipewire.sec.gid", Value: "100"},
|
||||||
|
{Key: "module.id", Value: "2"},
|
||||||
|
{Key: "object.id", Value: "34"},
|
||||||
|
{Key: "object.serial", Value: "34"},
|
||||||
|
{Key: "remote.intention", Value: "manager"},
|
||||||
|
{Key: "application.name", Value: "pw-container"},
|
||||||
|
{Key: "application.process.binary", Value: "pw-container"},
|
||||||
|
{Key: "application.language", Value: "en_US.UTF-8"},
|
||||||
|
{Key: "application.process.id", Value: "1443"},
|
||||||
|
{Key: "application.process.user", Value: "alice"},
|
||||||
|
{Key: "application.process.host", Value: "nixos"},
|
||||||
|
{Key: "application.process.session-id", Value: "1"},
|
||||||
|
{Key: "window.x11.display", Value: ":0"},
|
||||||
|
{Key: "cpu.vm.name", Value: "qemu"},
|
||||||
|
{Key: "log.level", Value: "0"},
|
||||||
|
{Key: "cpu.max-align", Value: "32"},
|
||||||
|
{Key: "default.clock.rate", Value: "48000"},
|
||||||
|
{Key: "default.clock.quantum", Value: "1024"},
|
||||||
|
{Key: "default.clock.min-quantum", Value: "32"},
|
||||||
|
{Key: "default.clock.max-quantum", Value: "2048"},
|
||||||
|
{Key: "default.clock.quantum-limit", Value: "8192"},
|
||||||
|
{Key: "default.clock.quantum-floor", Value: "4"},
|
||||||
|
{Key: "default.video.width", Value: "640"},
|
||||||
|
{Key: "default.video.height", Value: "480"},
|
||||||
|
{Key: "default.video.rate.num", Value: "25"},
|
||||||
|
{Key: "default.video.rate.denom", Value: "1"},
|
||||||
|
{Key: "clock.power-of-two-quantum", Value: "true"},
|
||||||
|
{Key: "link.max-buffers", Value: "64"},
|
||||||
|
{Key: "mem.warn-mlock", Value: "false"},
|
||||||
|
{Key: "mem.allow-mlock", Value: "true"},
|
||||||
|
{Key: "settings.check-quantum", Value: "false"},
|
||||||
|
{Key: "settings.check-rate", Value: "false"},
|
||||||
|
{Key: "core.version", Value: "1.4.7"},
|
||||||
|
{Key: "pipewire.access", Value: "unrestricted"},
|
||||||
|
}}, nil},
|
||||||
|
}.run(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientUpdateProperties(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
encodingTestCases[pipewire.ClientUpdateProperties, *pipewire.ClientUpdateProperties]{
|
||||||
|
{"sample", samplePWContainer[0][1][1], pipewire.ClientUpdateProperties{Properties: &pipewire.SPADict{
|
||||||
|
{Key: "remote.intention", Value: "manager"},
|
||||||
|
{Key: "application.name", Value: "pw-container"},
|
||||||
|
{Key: "application.process.binary", Value: "pw-container"},
|
||||||
|
{Key: "application.language", Value: "en_US.UTF-8"},
|
||||||
|
{Key: "application.process.id", Value: "1443"},
|
||||||
|
{Key: "application.process.user", Value: "alice"},
|
||||||
|
{Key: "application.process.host", Value: "nixos"},
|
||||||
|
{Key: "application.process.session-id", Value: "1"},
|
||||||
|
{Key: "window.x11.display", Value: ":0"},
|
||||||
|
{Key: "cpu.vm.name", Value: "qemu"},
|
||||||
|
{Key: "log.level", Value: "0"},
|
||||||
|
{Key: "cpu.max-align", Value: "32"},
|
||||||
|
{Key: "default.clock.rate", Value: "48000"},
|
||||||
|
{Key: "default.clock.quantum", Value: "1024"},
|
||||||
|
{Key: "default.clock.min-quantum", Value: "32"},
|
||||||
|
{Key: "default.clock.max-quantum", Value: "2048"},
|
||||||
|
{Key: "default.clock.quantum-limit", Value: "8192"},
|
||||||
|
{Key: "default.clock.quantum-floor", Value: "4"},
|
||||||
|
{Key: "default.video.width", Value: "640"},
|
||||||
|
{Key: "default.video.height", Value: "480"},
|
||||||
|
{Key: "default.video.rate.num", Value: "25"},
|
||||||
|
{Key: "default.video.rate.denom", Value: "1"},
|
||||||
|
{Key: "clock.power-of-two-quantum", Value: "true"},
|
||||||
|
{Key: "link.max-buffers", Value: "64"},
|
||||||
|
{Key: "mem.warn-mlock", Value: "false"},
|
||||||
|
{Key: "mem.allow-mlock", Value: "true"},
|
||||||
|
{Key: "settings.check-quantum", Value: "false"},
|
||||||
|
{Key: "settings.check-rate", Value: "false"},
|
||||||
|
{Key: "core.version", Value: "1.4.7"},
|
||||||
|
{Key: "core.name", Value: "pipewire-alice-1443"},
|
||||||
|
}}, nil},
|
||||||
|
}.run(t)
|
||||||
|
}
|
||||||
257
internal/pipewire/core.go
Normal file
257
internal/pipewire/core.go
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
package pipewire
|
||||||
|
|
||||||
|
/* pipewire/core.h */
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_TYPE_INTERFACE_Core = PW_TYPE_INFO_INTERFACE_BASE + "Core"
|
||||||
|
PW_TYPE_INTERFACE_Registry = PW_TYPE_INFO_INTERFACE_BASE + "Registry"
|
||||||
|
PW_CORE_PERM_MASK = PW_PERM_R | PW_PERM_X | PW_PERM_M
|
||||||
|
PW_VERSION_CORE = 4
|
||||||
|
PW_VERSION_REGISTRY = 3
|
||||||
|
|
||||||
|
PW_DEFAULT_REMOTE = "pipewire-0"
|
||||||
|
PW_ID_CORE = 0
|
||||||
|
PW_ID_ANY = Word(0xffffffff)
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_CORE_CHANGE_MASK_PROPS = 1 << iota
|
||||||
|
|
||||||
|
PW_CORE_CHANGE_MASK_ALL = 1<<iota - 1
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_CORE_EVENT_INFO = iota
|
||||||
|
PW_CORE_EVENT_DONE
|
||||||
|
PW_CORE_EVENT_PING
|
||||||
|
PW_CORE_EVENT_ERROR
|
||||||
|
PW_CORE_EVENT_REMOVE_ID
|
||||||
|
PW_CORE_EVENT_BOUND_ID
|
||||||
|
PW_CORE_EVENT_ADD_MEM
|
||||||
|
PW_CORE_EVENT_REMOVE_MEM
|
||||||
|
PW_CORE_EVENT_BOUND_PROPS
|
||||||
|
PW_CORE_EVENT_NUM
|
||||||
|
|
||||||
|
PW_VERSION_CORE_EVENTS = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_CORE_METHOD_ADD_LISTENER = iota
|
||||||
|
PW_CORE_METHOD_HELLO
|
||||||
|
PW_CORE_METHOD_SYNC
|
||||||
|
PW_CORE_METHOD_PONG
|
||||||
|
PW_CORE_METHOD_ERROR
|
||||||
|
PW_CORE_METHOD_GET_REGISTRY
|
||||||
|
PW_CORE_METHOD_CREATE_OBJECT
|
||||||
|
PW_CORE_METHOD_DESTROY
|
||||||
|
PW_CORE_METHOD_NUM
|
||||||
|
|
||||||
|
PW_VERSION_CORE_METHODS = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_REGISTRY_EVENT_GLOBAL = iota
|
||||||
|
PW_REGISTRY_EVENT_GLOBAL_REMOVE
|
||||||
|
PW_REGISTRY_EVENT_NUM
|
||||||
|
|
||||||
|
PW_VERSION_REGISTRY_EVENTS = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_REGISTRY_METHOD_ADD_LISTENER = iota
|
||||||
|
PW_REGISTRY_METHOD_BIND
|
||||||
|
PW_REGISTRY_METHOD_DESTROY
|
||||||
|
PW_REGISTRY_METHOD_NUM
|
||||||
|
|
||||||
|
PW_VERSION_REGISTRY_METHODS = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
FOOTER_CORE_OPCODE_GENERATION = iota
|
||||||
|
|
||||||
|
FOOTER_CORE_OPCODE_LAST
|
||||||
|
)
|
||||||
|
|
||||||
|
// The FooterCoreGeneration indicates to the client what is the current
|
||||||
|
// registry generation number of the Context on the server side.
|
||||||
|
//
|
||||||
|
// The server shall include this footer in the next message it sends that
|
||||||
|
// follows the increment of the registry generation number.
|
||||||
|
type FooterCoreGeneration struct {
|
||||||
|
RegistryGeneration Long `json:"registry_generation"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// A CoreInfo event is emitted by the server upon connection
|
||||||
|
// with the more information about the server.
|
||||||
|
type CoreInfo struct {
|
||||||
|
// The id of the server (PW_ID_CORE).
|
||||||
|
ID Int `json:"id"`
|
||||||
|
// A unique cookie for this server.
|
||||||
|
Cookie Int `json:"cookie"`
|
||||||
|
// The name of the user running the server.
|
||||||
|
UserName String `json:"user_name"`
|
||||||
|
// The name of the host running the server.
|
||||||
|
HostName String `json:"host_name"`
|
||||||
|
// A version string of the server.
|
||||||
|
Version String `json:"version"`
|
||||||
|
// The name of the server.
|
||||||
|
Name String `json:"name"`
|
||||||
|
// A set of bits with changes to the info.
|
||||||
|
ChangeMask Long `json:"change_mask"`
|
||||||
|
// Optional key/value properties, valid when change_mask has PW_CORE_CHANGE_MASK_PROPS.
|
||||||
|
Properties *SPADict `json:"props"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size satisfies [KnownSize] with a value computed at runtime.
|
||||||
|
func (c *CoreInfo) Size() Word {
|
||||||
|
return SizePrefix +
|
||||||
|
Size(SizeInt) +
|
||||||
|
Size(SizeInt) +
|
||||||
|
SizeString[Word](c.UserName) +
|
||||||
|
SizeString[Word](c.HostName) +
|
||||||
|
SizeString[Word](c.Version) +
|
||||||
|
SizeString[Word](c.Name) +
|
||||||
|
Size(SizeLong) +
|
||||||
|
c.Properties.Size()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
|
||||||
|
func (c *CoreInfo) MarshalBinary() ([]byte, error) { return Marshal(c) }
|
||||||
|
|
||||||
|
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
|
||||||
|
func (c *CoreInfo) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
|
||||||
|
|
||||||
|
// The CoreDone event is emitted as a result of a client Sync method.
|
||||||
|
type CoreDone struct {
|
||||||
|
// Passed from [CoreSync.ID].
|
||||||
|
ID Int `json:"id"`
|
||||||
|
// Passed from [CoreSync.Sequence].
|
||||||
|
Sequence Int `json:"sequence"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size satisfies [KnownSize] with a constant value.
|
||||||
|
func (c *CoreDone) Size() Word { return SizePrefix + Size(SizeInt) + Size(SizeInt) }
|
||||||
|
|
||||||
|
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
|
||||||
|
func (c *CoreDone) MarshalBinary() ([]byte, error) { return Marshal(c) }
|
||||||
|
|
||||||
|
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
|
||||||
|
func (c *CoreDone) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
|
||||||
|
|
||||||
|
// The CoreBoundProps event is emitted when a local object ID is bound to a global ID.
|
||||||
|
// It is emitted before the global becomes visible in the registry.
|
||||||
|
type CoreBoundProps struct {
|
||||||
|
// A proxy id.
|
||||||
|
ID Int `json:"id"`
|
||||||
|
// The global_id as it will appear in the registry.
|
||||||
|
GlobalID Int `json:"global_id"`
|
||||||
|
// The properties of the global.
|
||||||
|
Properties *SPADict `json:"props"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size satisfies [KnownSize] with a value computed at runtime.
|
||||||
|
func (c *CoreBoundProps) Size() Word {
|
||||||
|
return SizePrefix +
|
||||||
|
Size(SizeInt) +
|
||||||
|
Size(SizeInt) +
|
||||||
|
c.Properties.Size()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
|
||||||
|
func (c *CoreBoundProps) MarshalBinary() ([]byte, error) { return Marshal(c) }
|
||||||
|
|
||||||
|
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
|
||||||
|
func (c *CoreBoundProps) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
|
||||||
|
|
||||||
|
// CoreHello is the first message sent by a client.
|
||||||
|
type CoreHello struct {
|
||||||
|
// The version number of the client, usually PW_VERSION_CORE.
|
||||||
|
Version Int `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size satisfies [KnownSize] with a constant value.
|
||||||
|
func (c *CoreHello) Size() Word { return SizePrefix + Size(SizeInt) }
|
||||||
|
|
||||||
|
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
|
||||||
|
func (c *CoreHello) MarshalBinary() ([]byte, error) { return Marshal(c) }
|
||||||
|
|
||||||
|
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
|
||||||
|
func (c *CoreHello) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
|
||||||
|
|
||||||
|
const (
|
||||||
|
// CoreSyncSequenceOffset is the offset to [Header.Sequence] to produce [CoreSync.Sequence].
|
||||||
|
CoreSyncSequenceOffset = 0x40000000
|
||||||
|
)
|
||||||
|
|
||||||
|
// The CoreSync message will result in a Done event from the server.
|
||||||
|
// When the Done event is received, the client can be sure that all
|
||||||
|
// operations before the Sync method have been completed.
|
||||||
|
type CoreSync struct {
|
||||||
|
// The id will be returned in the Done event.
|
||||||
|
ID Int `json:"id"`
|
||||||
|
// Usually generated automatically and will be returned in the Done event.
|
||||||
|
Sequence Int `json:"sequence"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size satisfies [KnownSize] with a constant value.
|
||||||
|
func (c *CoreSync) Size() Word { return SizePrefix + Size(SizeInt) + Size(SizeInt) }
|
||||||
|
|
||||||
|
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
|
||||||
|
func (c *CoreSync) MarshalBinary() ([]byte, error) { return Marshal(c) }
|
||||||
|
|
||||||
|
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
|
||||||
|
func (c *CoreSync) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
|
||||||
|
|
||||||
|
// CoreGetRegistry is sent when a client requests to bind to the
|
||||||
|
// registry object and list the available objects on the server.
|
||||||
|
//
|
||||||
|
// Like with all bindings, first the client allocates a new proxy
|
||||||
|
// id and puts this as the new_id field. Methods and Events can
|
||||||
|
// then be sent and received on the new_id (in the message Id field).
|
||||||
|
type CoreGetRegistry struct {
|
||||||
|
// The version of the registry interface used on the client,
|
||||||
|
// usually PW_VERSION_REGISTRY.
|
||||||
|
Version Int `json:"version"`
|
||||||
|
// The id of the new proxy with the registry interface,
|
||||||
|
// ends up as [Header.ID] in future messages.
|
||||||
|
NewID Int `json:"new_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size satisfies [KnownSize] with a constant value.
|
||||||
|
func (c *CoreGetRegistry) Size() Word { return SizePrefix + Size(SizeInt) + Size(SizeInt) }
|
||||||
|
|
||||||
|
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
|
||||||
|
func (c *CoreGetRegistry) MarshalBinary() ([]byte, error) { return Marshal(c) }
|
||||||
|
|
||||||
|
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
|
||||||
|
func (c *CoreGetRegistry) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
|
||||||
|
|
||||||
|
// A RegistryGlobal event is emitted to notify a client about a new global object.
|
||||||
|
type RegistryGlobal struct {
|
||||||
|
// The global id.
|
||||||
|
ID Int `json:"id"`
|
||||||
|
// Permission bits.
|
||||||
|
Permissions Int `json:"permissions"`
|
||||||
|
// The type of object.
|
||||||
|
Type String `json:"type"`
|
||||||
|
// The server version of the object.
|
||||||
|
Version Int `json:"version"`
|
||||||
|
// Extra global properties.
|
||||||
|
Properties *SPADict `json:"props"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size satisfies [KnownSize] with a value computed at runtime.
|
||||||
|
func (c *RegistryGlobal) Size() Word {
|
||||||
|
return SizePrefix +
|
||||||
|
Size(SizeInt) +
|
||||||
|
Size(SizeInt) +
|
||||||
|
SizeString[Word](c.Type) +
|
||||||
|
Size(SizeInt) +
|
||||||
|
c.Properties.Size()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
|
||||||
|
func (c *RegistryGlobal) MarshalBinary() ([]byte, error) { return Marshal(c) }
|
||||||
|
|
||||||
|
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
|
||||||
|
func (c *RegistryGlobal) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }
|
||||||
573
internal/pipewire/core_test.go
Normal file
573
internal/pipewire/core_test.go
Normal file
@ -0,0 +1,573 @@
|
|||||||
|
package pipewire_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/internal/pipewire"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFooterCoreGeneration(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
encodingTestCases[pipewire.Footer[pipewire.FooterCoreGeneration], *pipewire.Footer[pipewire.FooterCoreGeneration]]{
|
||||||
|
{"sample", samplePWContainer[1][0][2], pipewire.Footer[pipewire.FooterCoreGeneration]{
|
||||||
|
Opcode: pipewire.FOOTER_CORE_OPCODE_GENERATION,
|
||||||
|
Payload: pipewire.FooterCoreGeneration{RegistryGeneration: 0x22},
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"sample*", samplePWContainer[1][5][2], pipewire.Footer[pipewire.FooterCoreGeneration]{
|
||||||
|
Opcode: pipewire.FOOTER_CORE_OPCODE_GENERATION,
|
||||||
|
Payload: pipewire.FooterCoreGeneration{RegistryGeneration: 0x23},
|
||||||
|
}, nil},
|
||||||
|
}.run(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCoreInfo(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
encodingTestCases[pipewire.CoreInfo, *pipewire.CoreInfo]{
|
||||||
|
{"sample", samplePWContainer[1][0][1], pipewire.CoreInfo{
|
||||||
|
ID: 0,
|
||||||
|
Cookie: -2069267610,
|
||||||
|
UserName: "alice",
|
||||||
|
HostName: "nixos",
|
||||||
|
Version: "1.4.7",
|
||||||
|
Name: "pipewire-0",
|
||||||
|
ChangeMask: pipewire.PW_CORE_CHANGE_MASK_PROPS,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "config.name", Value: "pipewire.conf"},
|
||||||
|
{Key: "application.name", Value: "pipewire"},
|
||||||
|
{Key: "application.process.binary", Value: "pipewire"},
|
||||||
|
{Key: "application.language", Value: "en_US.UTF-8"},
|
||||||
|
{Key: "application.process.id", Value: "1446"},
|
||||||
|
{Key: "application.process.user", Value: "alice"},
|
||||||
|
{Key: "application.process.host", Value: "nixos"},
|
||||||
|
{Key: "window.x11.display", Value: ":0"},
|
||||||
|
{Key: "cpu.vm.name", Value: "qemu"},
|
||||||
|
{Key: "link.max-buffers", Value: "16"},
|
||||||
|
{Key: "core.daemon", Value: "true"},
|
||||||
|
{Key: "core.name", Value: "pipewire-0"},
|
||||||
|
{Key: "default.clock.min-quantum", Value: "1024"},
|
||||||
|
{Key: "cpu.max-align", Value: "32"},
|
||||||
|
{Key: "default.clock.rate", Value: "48000"},
|
||||||
|
{Key: "default.clock.quantum", Value: "1024"},
|
||||||
|
{Key: "default.clock.max-quantum", Value: "2048"},
|
||||||
|
{Key: "default.clock.quantum-limit", Value: "8192"},
|
||||||
|
{Key: "default.clock.quantum-floor", Value: "4"},
|
||||||
|
{Key: "default.video.width", Value: "640"},
|
||||||
|
{Key: "default.video.height", Value: "480"},
|
||||||
|
{Key: "default.video.rate.num", Value: "25"},
|
||||||
|
{Key: "default.video.rate.denom", Value: "1"},
|
||||||
|
{Key: "log.level", Value: "2"},
|
||||||
|
{Key: "clock.power-of-two-quantum", Value: "true"},
|
||||||
|
{Key: "mem.warn-mlock", Value: "false"},
|
||||||
|
{Key: "mem.allow-mlock", Value: "true"},
|
||||||
|
{Key: "settings.check-quantum", Value: "false"},
|
||||||
|
{Key: "settings.check-rate", Value: "false"},
|
||||||
|
{Key: "object.id", Value: "0"},
|
||||||
|
{Key: "object.serial", Value: "0"}},
|
||||||
|
}, nil},
|
||||||
|
}.run(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCoreDone(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
encodingTestCases[pipewire.CoreDone, *pipewire.CoreDone]{
|
||||||
|
{"sample", samplePWContainer[1][5][1], pipewire.CoreDone{
|
||||||
|
ID: -1,
|
||||||
|
Sequence: 0,
|
||||||
|
}, nil},
|
||||||
|
}.run(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCoreBoundProps(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
encodingTestCases[pipewire.CoreBoundProps, *pipewire.CoreBoundProps]{
|
||||||
|
{"sample", samplePWContainer[1][1][1], pipewire.CoreBoundProps{
|
||||||
|
ID: pipewire.PW_ID_CLIENT,
|
||||||
|
GlobalID: 34,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "object.serial", Value: "34"},
|
||||||
|
{Key: "module.id", Value: "2"},
|
||||||
|
{Key: "pipewire.protocol", Value: "protocol-native"},
|
||||||
|
{Key: "pipewire.sec.pid", Value: "1443"},
|
||||||
|
{Key: "pipewire.sec.uid", Value: "1000"},
|
||||||
|
{Key: "pipewire.sec.gid", Value: "100"},
|
||||||
|
{Key: "pipewire.sec.socket", Value: "pipewire-0-manager"}},
|
||||||
|
}, nil},
|
||||||
|
}.run(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCoreHello(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
encodingTestCases[pipewire.CoreHello, *pipewire.CoreHello]{
|
||||||
|
{"sample", samplePWContainer[0][0][1], pipewire.CoreHello{
|
||||||
|
Version: pipewire.PW_VERSION_CORE,
|
||||||
|
}, nil},
|
||||||
|
}.run(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCoreSync(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
encodingTestCases[pipewire.CoreSync, *pipewire.CoreSync]{
|
||||||
|
{"sample", samplePWContainer[0][3][1], pipewire.CoreSync{
|
||||||
|
ID: pipewire.PW_ID_CORE,
|
||||||
|
Sequence: pipewire.CoreSyncSequenceOffset + 3,
|
||||||
|
}, nil},
|
||||||
|
}.run(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCoreGetRegistry(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
encodingTestCases[pipewire.CoreGetRegistry, *pipewire.CoreGetRegistry]{
|
||||||
|
{"sample", samplePWContainer[0][2][1], pipewire.CoreGetRegistry{
|
||||||
|
Version: pipewire.PW_VERSION_REGISTRY,
|
||||||
|
// this ends up as the Id of PW_TYPE_INTERFACE_Registry
|
||||||
|
NewID: 2,
|
||||||
|
}, nil},
|
||||||
|
}.run(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRegistryGlobal(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
encodingTestCases[pipewire.RegistryGlobal, *pipewire.RegistryGlobal]{
|
||||||
|
{"sample0", samplePWContainer[1][6][1], pipewire.RegistryGlobal{
|
||||||
|
ID: pipewire.PW_ID_CORE,
|
||||||
|
Permissions: pipewire.PW_CORE_PERM_MASK,
|
||||||
|
Type: pipewire.PW_TYPE_INTERFACE_Core,
|
||||||
|
Version: pipewire.PW_VERSION_CORE,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "object.serial", Value: "0"},
|
||||||
|
{Key: "core.name", Value: "pipewire-0"},
|
||||||
|
},
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"sample1", samplePWContainer[1][7][1], pipewire.RegistryGlobal{
|
||||||
|
ID: 1,
|
||||||
|
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||||
|
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||||
|
Version: pipewire.PW_VERSION_MODULE,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "object.serial", Value: "1"},
|
||||||
|
{Key: "module.name", Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-rt"},
|
||||||
|
},
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"sample2", samplePWContainer[1][8][1], pipewire.RegistryGlobal{
|
||||||
|
ID: 3, // registry takes up 2
|
||||||
|
Permissions: pipewire.PW_SECURITY_CONTEXT_PERM_MASK,
|
||||||
|
Type: pipewire.PW_TYPE_INTERFACE_SecurityContext,
|
||||||
|
Version: pipewire.PW_VERSION_SECURITY_CONTEXT,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "object.serial", Value: "3"},
|
||||||
|
},
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"sample3", samplePWContainer[1][9][1], pipewire.RegistryGlobal{
|
||||||
|
ID: 2,
|
||||||
|
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||||
|
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||||
|
Version: pipewire.PW_VERSION_MODULE,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "object.serial", Value: "2"},
|
||||||
|
{Key: "module.name", Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-protocol-native"},
|
||||||
|
},
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"sample4", samplePWContainer[1][10][1], pipewire.RegistryGlobal{
|
||||||
|
ID: 5,
|
||||||
|
Permissions: pipewire.PW_PROFILER_PERM_MASK,
|
||||||
|
Type: pipewire.PW_TYPE_INTERFACE_Profiler,
|
||||||
|
Version: pipewire.PW_VERSION_PROFILER,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "object.serial", Value: "5"},
|
||||||
|
},
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"sample5", samplePWContainer[1][11][1], pipewire.RegistryGlobal{
|
||||||
|
ID: 4,
|
||||||
|
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||||
|
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||||
|
Version: pipewire.PW_VERSION_MODULE,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "object.serial", Value: "4"},
|
||||||
|
{Key: "module.name", Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-profiler"},
|
||||||
|
},
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"sample6", samplePWContainer[1][12][1], pipewire.RegistryGlobal{
|
||||||
|
ID: 6,
|
||||||
|
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||||
|
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||||
|
Version: pipewire.PW_VERSION_MODULE,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "object.serial", Value: "6"},
|
||||||
|
{Key: "module.name", Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-metadata"},
|
||||||
|
},
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"sample7", samplePWContainer[1][13][1], pipewire.RegistryGlobal{
|
||||||
|
ID: 7,
|
||||||
|
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||||
|
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||||
|
Version: pipewire.PW_VERSION_FACTORY,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "object.serial", Value: "7"},
|
||||||
|
{Key: "module.id", Value: "6"},
|
||||||
|
{Key: "factory.name", Value: "metadata"},
|
||||||
|
{Key: "factory.type.name", Value: pipewire.PW_TYPE_INTERFACE_Metadata},
|
||||||
|
{Key: "factory.type.version", Value: "3"},
|
||||||
|
},
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"sample8", samplePWContainer[1][14][1], pipewire.RegistryGlobal{
|
||||||
|
ID: 8,
|
||||||
|
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||||
|
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||||
|
Version: pipewire.PW_VERSION_MODULE,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "object.serial", Value: "8"},
|
||||||
|
{Key: "module.name", Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-spa-device-factory"},
|
||||||
|
},
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"sample9", samplePWContainer[1][15][1], pipewire.RegistryGlobal{
|
||||||
|
ID: 9,
|
||||||
|
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||||
|
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||||
|
Version: pipewire.PW_VERSION_FACTORY,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "object.serial", Value: "9"},
|
||||||
|
{Key: "module.id", Value: "8"},
|
||||||
|
{Key: "factory.name", Value: "spa-device-factory"},
|
||||||
|
{Key: "factory.type.name", Value: pipewire.PW_TYPE_INTERFACE_Device},
|
||||||
|
{Key: "factory.type.version", Value: "3"},
|
||||||
|
},
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"sample10", samplePWContainer[1][16][1], pipewire.RegistryGlobal{
|
||||||
|
ID: 10,
|
||||||
|
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||||
|
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||||
|
Version: pipewire.PW_VERSION_MODULE,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "object.serial", Value: "10"},
|
||||||
|
{Key: "module.name", Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-spa-node-factory"},
|
||||||
|
},
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"sample11", samplePWContainer[1][17][1], pipewire.RegistryGlobal{
|
||||||
|
ID: 11,
|
||||||
|
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||||
|
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||||
|
Version: pipewire.PW_VERSION_FACTORY,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "object.serial", Value: "11"},
|
||||||
|
{Key: "module.id", Value: "10"},
|
||||||
|
{Key: "factory.name", Value: "spa-node-factory"},
|
||||||
|
{Key: "factory.type.name", Value: pipewire.PW_TYPE_INTERFACE_Node},
|
||||||
|
{Key: "factory.type.version", Value: "3"},
|
||||||
|
},
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"sample12", samplePWContainer[1][18][1], pipewire.RegistryGlobal{
|
||||||
|
ID: 12,
|
||||||
|
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||||
|
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||||
|
Version: pipewire.PW_VERSION_MODULE,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "object.serial", Value: "12"},
|
||||||
|
{Key: "module.name", Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-client-node"},
|
||||||
|
},
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"sample13", samplePWContainer[1][19][1], pipewire.RegistryGlobal{
|
||||||
|
ID: 13,
|
||||||
|
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||||
|
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||||
|
Version: pipewire.PW_VERSION_FACTORY,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "object.serial", Value: "13"},
|
||||||
|
{Key: "module.id", Value: "12"},
|
||||||
|
{Key: "factory.name", Value: "client-node"},
|
||||||
|
{Key: "factory.type.name", Value: pipewire.PW_TYPE_INTERFACE_ClientNode},
|
||||||
|
{Key: "factory.type.version", Value: "6"},
|
||||||
|
},
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"sample14", samplePWContainer[1][20][1], pipewire.RegistryGlobal{
|
||||||
|
ID: 14,
|
||||||
|
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||||
|
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||||
|
Version: pipewire.PW_VERSION_MODULE,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "object.serial", Value: "14"},
|
||||||
|
{Key: "module.name", Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-client-device"},
|
||||||
|
},
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"sample15", samplePWContainer[1][21][1], pipewire.RegistryGlobal{
|
||||||
|
ID: 15,
|
||||||
|
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||||
|
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||||
|
Version: pipewire.PW_VERSION_FACTORY,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "object.serial", Value: "15"},
|
||||||
|
{Key: "module.id", Value: "14"},
|
||||||
|
{Key: "factory.name", Value: "client-device"},
|
||||||
|
{Key: "factory.type.name", Value: "Spa:Pointer:Interface:Device"},
|
||||||
|
{Key: "factory.type.version", Value: "0"},
|
||||||
|
},
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"sample16", samplePWContainer[1][22][1], pipewire.RegistryGlobal{
|
||||||
|
ID: 16,
|
||||||
|
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||||
|
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||||
|
Version: pipewire.PW_VERSION_MODULE,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "object.serial", Value: "16"},
|
||||||
|
{Key: "module.name", Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-portal"},
|
||||||
|
},
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"sample17", samplePWContainer[1][23][1], pipewire.RegistryGlobal{
|
||||||
|
ID: 17,
|
||||||
|
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||||
|
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||||
|
Version: pipewire.PW_VERSION_MODULE,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "object.serial", Value: "17"},
|
||||||
|
{Key: "module.name", Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-access"},
|
||||||
|
},
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"sample18", samplePWContainer[1][24][1], pipewire.RegistryGlobal{
|
||||||
|
ID: 18,
|
||||||
|
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||||
|
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||||
|
Version: pipewire.PW_VERSION_MODULE,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "object.serial", Value: "18"},
|
||||||
|
{Key: "module.name", Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-adapter"},
|
||||||
|
},
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"sample19", samplePWContainer[1][25][1], pipewire.RegistryGlobal{
|
||||||
|
ID: 19,
|
||||||
|
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||||
|
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||||
|
Version: pipewire.PW_VERSION_FACTORY,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "object.serial", Value: "19"},
|
||||||
|
{Key: "module.id", Value: "18"},
|
||||||
|
{Key: "factory.name", Value: "adapter"},
|
||||||
|
{Key: "factory.type.name", Value: pipewire.PW_TYPE_INTERFACE_Node},
|
||||||
|
{Key: "factory.type.version", Value: "3"},
|
||||||
|
},
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"sample20", samplePWContainer[1][26][1], pipewire.RegistryGlobal{
|
||||||
|
ID: 20,
|
||||||
|
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||||
|
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||||
|
Version: pipewire.PW_VERSION_MODULE,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "object.serial", Value: "20"},
|
||||||
|
{Key: "module.name", Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-link-factory"},
|
||||||
|
},
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"sample21", samplePWContainer[1][27][1], pipewire.RegistryGlobal{
|
||||||
|
ID: 21,
|
||||||
|
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||||
|
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||||
|
Version: pipewire.PW_VERSION_FACTORY,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "object.serial", Value: "21"},
|
||||||
|
{Key: "module.id", Value: "20"},
|
||||||
|
{Key: "factory.name", Value: "link-factory"},
|
||||||
|
{Key: "factory.type.name", Value: pipewire.PW_TYPE_INTERFACE_Link},
|
||||||
|
{Key: "factory.type.version", Value: "3"},
|
||||||
|
},
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"sample22", samplePWContainer[1][28][1], pipewire.RegistryGlobal{
|
||||||
|
ID: 22,
|
||||||
|
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||||
|
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||||
|
Version: pipewire.PW_VERSION_MODULE,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "object.serial", Value: "22"},
|
||||||
|
{Key: "module.name", Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-session-manager"},
|
||||||
|
},
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"sample23", samplePWContainer[1][29][1], pipewire.RegistryGlobal{
|
||||||
|
ID: 23,
|
||||||
|
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||||
|
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||||
|
Version: pipewire.PW_VERSION_FACTORY,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "object.serial", Value: "23"},
|
||||||
|
{Key: "module.id", Value: "22"},
|
||||||
|
{Key: "factory.name", Value: "client-endpoint"},
|
||||||
|
{Key: "factory.type.name", Value: "PipeWire:Interface:ClientEndpoint"},
|
||||||
|
{Key: "factory.type.version", Value: "0"},
|
||||||
|
},
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"sample24", samplePWContainer[1][30][1], pipewire.RegistryGlobal{
|
||||||
|
ID: 24,
|
||||||
|
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||||
|
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||||
|
Version: pipewire.PW_VERSION_FACTORY,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "object.serial", Value: "24"},
|
||||||
|
{Key: "module.id", Value: "22"},
|
||||||
|
{Key: "factory.name", Value: "client-session"},
|
||||||
|
{Key: "factory.type.name", Value: "PipeWire:Interface:ClientSession"},
|
||||||
|
{Key: "factory.type.version", Value: "0"},
|
||||||
|
},
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"sample25", samplePWContainer[1][31][1], pipewire.RegistryGlobal{
|
||||||
|
ID: 25,
|
||||||
|
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||||
|
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||||
|
Version: pipewire.PW_VERSION_FACTORY,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "object.serial", Value: "25"},
|
||||||
|
{Key: "module.id", Value: "22"},
|
||||||
|
{Key: "factory.name", Value: "session"},
|
||||||
|
{Key: "factory.type.name", Value: "PipeWire:Interface:Session"},
|
||||||
|
{Key: "factory.type.version", Value: "0"},
|
||||||
|
},
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"sample26", samplePWContainer[1][32][1], pipewire.RegistryGlobal{
|
||||||
|
ID: 26,
|
||||||
|
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||||
|
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||||
|
Version: pipewire.PW_VERSION_FACTORY,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "object.serial", Value: "26"},
|
||||||
|
{Key: "module.id", Value: "22"},
|
||||||
|
{Key: "factory.name", Value: "endpoint"},
|
||||||
|
{Key: "factory.type.name", Value: "PipeWire:Interface:Endpoint"},
|
||||||
|
{Key: "factory.type.version", Value: "0"},
|
||||||
|
},
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"sample27", samplePWContainer[1][33][1], pipewire.RegistryGlobal{
|
||||||
|
ID: 27,
|
||||||
|
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||||
|
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||||
|
Version: pipewire.PW_VERSION_FACTORY,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "object.serial", Value: "27"},
|
||||||
|
{Key: "module.id", Value: "22"},
|
||||||
|
{Key: "factory.name", Value: "endpoint-stream"},
|
||||||
|
{Key: "factory.type.name", Value: "PipeWire:Interface:EndpointStream"},
|
||||||
|
{Key: "factory.type.version", Value: "0"},
|
||||||
|
},
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"sample28", samplePWContainer[1][34][1], pipewire.RegistryGlobal{
|
||||||
|
ID: 28,
|
||||||
|
Permissions: pipewire.PW_FACTORY_PERM_MASK,
|
||||||
|
Type: pipewire.PW_TYPE_INTERFACE_Factory,
|
||||||
|
Version: pipewire.PW_VERSION_FACTORY,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "object.serial", Value: "28"},
|
||||||
|
{Key: "module.id", Value: "22"},
|
||||||
|
{Key: "factory.name", Value: "endpoint-link"},
|
||||||
|
{Key: "factory.type.name", Value: "PipeWire:Interface:EndpointLink"},
|
||||||
|
{Key: "factory.type.version", Value: "0"},
|
||||||
|
},
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"sample29", samplePWContainer[1][35][1], pipewire.RegistryGlobal{
|
||||||
|
ID: 29,
|
||||||
|
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||||
|
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||||
|
Version: pipewire.PW_VERSION_MODULE,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "object.serial", Value: "29"},
|
||||||
|
{Key: "module.name", Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-x11-bell"},
|
||||||
|
},
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"sample30", samplePWContainer[1][36][1], pipewire.RegistryGlobal{
|
||||||
|
ID: 30,
|
||||||
|
Permissions: pipewire.PW_MODULE_PERM_MASK,
|
||||||
|
Type: pipewire.PW_TYPE_INTERFACE_Module,
|
||||||
|
Version: pipewire.PW_VERSION_MODULE,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "object.serial", Value: "30"},
|
||||||
|
{Key: "module.name", Value: pipewire.PIPEWIRE_MODULE_PREFIX + "module-jackdbus-detect"},
|
||||||
|
},
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"sample31", samplePWContainer[1][37][1], pipewire.RegistryGlobal{
|
||||||
|
ID: 31,
|
||||||
|
Permissions: pipewire.PW_PERM_RWXM, // why is this not PW_NODE_PERM_MASK?
|
||||||
|
Type: pipewire.PW_TYPE_INTERFACE_Node,
|
||||||
|
Version: pipewire.PW_VERSION_NODE,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "object.serial", Value: "31"},
|
||||||
|
{Key: "factory.id", Value: "11"},
|
||||||
|
{Key: "priority.driver", Value: "200000"},
|
||||||
|
{Key: "node.name", Value: "Dummy-Driver"},
|
||||||
|
},
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"sample32", samplePWContainer[1][38][1], pipewire.RegistryGlobal{
|
||||||
|
ID: 32,
|
||||||
|
Permissions: pipewire.PW_PERM_RWXM, // why is this not PW_NODE_PERM_MASK?
|
||||||
|
Type: pipewire.PW_TYPE_INTERFACE_Node,
|
||||||
|
Version: pipewire.PW_VERSION_NODE,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "object.serial", Value: "32"},
|
||||||
|
{Key: "factory.id", Value: "11"},
|
||||||
|
{Key: "priority.driver", Value: "190000"},
|
||||||
|
{Key: "node.name", Value: "Freewheel-Driver"},
|
||||||
|
},
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"sample33", samplePWContainer[1][39][1], pipewire.RegistryGlobal{
|
||||||
|
ID: 33,
|
||||||
|
Permissions: pipewire.PW_METADATA_PERM_MASK,
|
||||||
|
Type: pipewire.PW_TYPE_INTERFACE_Metadata,
|
||||||
|
Version: pipewire.PW_VERSION_METADATA,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "object.serial", Value: "33"},
|
||||||
|
{Key: "metadata.name", Value: "settings"},
|
||||||
|
},
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"sample34", samplePWContainer[1][40][1], pipewire.RegistryGlobal{
|
||||||
|
ID: 34,
|
||||||
|
Permissions: pipewire.PW_CLIENT_PERM_MASK,
|
||||||
|
Type: pipewire.PW_TYPE_INTERFACE_Client,
|
||||||
|
Version: pipewire.PW_VERSION_CLIENT,
|
||||||
|
Properties: &pipewire.SPADict{
|
||||||
|
{Key: "object.serial", Value: "34"},
|
||||||
|
{Key: "module.id", Value: "2"},
|
||||||
|
{Key: "pipewire.protocol", Value: "protocol-native"},
|
||||||
|
{Key: "pipewire.sec.pid", Value: "1443"},
|
||||||
|
{Key: "pipewire.sec.uid", Value: "1000"},
|
||||||
|
{Key: "pipewire.sec.gid", Value: "100"},
|
||||||
|
{Key: "pipewire.sec.socket", Value: "pipewire-0-manager"},
|
||||||
|
{Key: "pipewire.access", Value: "unrestricted"},
|
||||||
|
{Key: "application.name", Value: "pw-container"},
|
||||||
|
},
|
||||||
|
}, nil},
|
||||||
|
}.run(t)
|
||||||
|
}
|
||||||
73
internal/pipewire/header.go
Normal file
73
internal/pipewire/header.go
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
package pipewire
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// SizeHeader is the fixed size of [Header].
|
||||||
|
SizeHeader = 16
|
||||||
|
// SizeMax is the largest value of [Header.Size] that can be represented in its 3-byte segment.
|
||||||
|
SizeMax = 0x00ffffff
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrSizeRange indicates that the value of [Header.Size] cannot be represented in its 3-byte segment.
|
||||||
|
ErrSizeRange = errors.New("size out of range")
|
||||||
|
// ErrBadHeader indicates that the header slice does not have length [HeaderSize].
|
||||||
|
ErrBadHeader = errors.New("incorrect header size")
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Header is the fixed-size message header described in protocol native.
|
||||||
|
type Header struct {
|
||||||
|
// The message id this is the destination resource/proxy id.
|
||||||
|
ID Word `json:"Id"`
|
||||||
|
// The opcode on the resource/proxy interface.
|
||||||
|
Opcode byte `json:"opcode"`
|
||||||
|
// The size of the payload and optional footer of the message.
|
||||||
|
// Note: this value is only 24 bits long in the format.
|
||||||
|
Size uint32 `json:"size"`
|
||||||
|
// An increasing sequence number for each message.
|
||||||
|
Sequence Word `json:"seq"`
|
||||||
|
// Number of file descriptors in this message.
|
||||||
|
FileCount Word `json:"n_fds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// append appends the protocol native message header to data.
|
||||||
|
//
|
||||||
|
// Callers must perform bounds check on [Header.Size].
|
||||||
|
func (h *Header) append(data []byte) []byte {
|
||||||
|
data = binary.NativeEndian.AppendUint32(data, h.ID)
|
||||||
|
data = binary.NativeEndian.AppendUint32(data, Word(h.Opcode)<<24|h.Size)
|
||||||
|
data = binary.NativeEndian.AppendUint32(data, h.Sequence)
|
||||||
|
data = binary.NativeEndian.AppendUint32(data, h.FileCount)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalBinary encodes the protocol native message header.
|
||||||
|
func (h *Header) MarshalBinary() (data []byte, err error) {
|
||||||
|
if h.Size&^SizeMax != 0 {
|
||||||
|
return nil, ErrSizeRange
|
||||||
|
}
|
||||||
|
return h.append(make([]byte, 0, SizeHeader)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// unmarshalBinary decodes the protocol native message header.
|
||||||
|
func (h *Header) unmarshalBinary(data [SizeHeader]byte) {
|
||||||
|
h.ID = binary.NativeEndian.Uint32(data[0:4])
|
||||||
|
h.Size = binary.NativeEndian.Uint32(data[4:8])
|
||||||
|
h.Opcode = byte(h.Size >> 24)
|
||||||
|
h.Size &= SizeMax
|
||||||
|
h.Sequence = binary.NativeEndian.Uint32(data[8:])
|
||||||
|
h.FileCount = binary.NativeEndian.Uint32(data[12:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBinary decodes the protocol native message header.
|
||||||
|
func (h *Header) UnmarshalBinary(data []byte) error {
|
||||||
|
if len(data) != SizeHeader {
|
||||||
|
return ErrBadHeader
|
||||||
|
}
|
||||||
|
h.unmarshalBinary(([SizeHeader]byte)(data))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
328
internal/pipewire/header_test.go
Normal file
328
internal/pipewire/header_test.go
Normal file
@ -0,0 +1,328 @@
|
|||||||
|
package pipewire_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/internal/pipewire"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHeader(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
encodingTestCases[pipewire.Header, *pipewire.Header]{
|
||||||
|
{"PW_CORE_METHOD_HELLO", samplePWContainer[0][0][0], pipewire.Header{
|
||||||
|
ID: pipewire.PW_ID_CORE,
|
||||||
|
Opcode: pipewire.PW_CORE_METHOD_HELLO,
|
||||||
|
Size: 0x18, Sequence: 0, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_CLIENT_METHOD_UPDATE_PROPERTIES", samplePWContainer[0][1][0], pipewire.Header{
|
||||||
|
ID: pipewire.PW_ID_CLIENT,
|
||||||
|
Opcode: pipewire.PW_CLIENT_METHOD_UPDATE_PROPERTIES,
|
||||||
|
Size: 0x600, Sequence: 1, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_CORE_METHOD_GET_REGISTRY", samplePWContainer[0][2][0], pipewire.Header{
|
||||||
|
ID: pipewire.PW_ID_CORE,
|
||||||
|
Opcode: pipewire.PW_CORE_METHOD_GET_REGISTRY,
|
||||||
|
Size: 0x28, Sequence: 2, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_CORE_METHOD_SYNC", samplePWContainer[0][3][0], pipewire.Header{
|
||||||
|
ID: pipewire.PW_ID_CORE,
|
||||||
|
Opcode: pipewire.PW_CORE_METHOD_SYNC,
|
||||||
|
Size: 0x28, Sequence: 3, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_CORE_EVENT_INFO", samplePWContainer[1][0][0], pipewire.Header{
|
||||||
|
ID: pipewire.PW_ID_CORE,
|
||||||
|
Opcode: pipewire.PW_CORE_EVENT_INFO,
|
||||||
|
Size: 0x6b8, Sequence: 0, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_CORE_EVENT_BOUND_PROPS", samplePWContainer[1][1][0], pipewire.Header{
|
||||||
|
ID: pipewire.PW_ID_CORE,
|
||||||
|
Opcode: pipewire.PW_CORE_EVENT_BOUND_PROPS,
|
||||||
|
Size: 0x198, Sequence: 1, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_CLIENT_EVENT_INFO", samplePWContainer[1][2][0], pipewire.Header{
|
||||||
|
ID: pipewire.PW_ID_CLIENT,
|
||||||
|
Opcode: pipewire.PW_CLIENT_EVENT_INFO,
|
||||||
|
Size: 0x1f0, Sequence: 2, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_CLIENT_EVENT_INFO*", samplePWContainer[1][3][0], pipewire.Header{
|
||||||
|
ID: pipewire.PW_ID_CLIENT,
|
||||||
|
Opcode: pipewire.PW_CLIENT_EVENT_INFO,
|
||||||
|
Size: 0x7a0, Sequence: 3, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_CLIENT_EVENT_INFO**", samplePWContainer[1][4][0], pipewire.Header{
|
||||||
|
ID: pipewire.PW_ID_CLIENT,
|
||||||
|
Opcode: pipewire.PW_CLIENT_EVENT_INFO,
|
||||||
|
Size: 0x7d0, Sequence: 4, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_CORE_EVENT_DONE", samplePWContainer[1][5][0], pipewire.Header{
|
||||||
|
ID: pipewire.PW_ID_CORE,
|
||||||
|
Opcode: pipewire.PW_CORE_EVENT_DONE,
|
||||||
|
Size: 0x58, Sequence: 5, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_REGISTRY_EVENT_GLOBAL 0", samplePWContainer[1][6][0], pipewire.Header{
|
||||||
|
ID: 2, // this is specified by Core::GetRegistry in samplePWContainer[0][2][1]
|
||||||
|
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||||
|
Size: 0xc8, Sequence: 6, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_REGISTRY_EVENT_GLOBAL 1", samplePWContainer[1][7][0], pipewire.Header{
|
||||||
|
ID: 2,
|
||||||
|
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||||
|
Size: 0xd8, Sequence: 7, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_REGISTRY_EVENT_GLOBAL 2", samplePWContainer[1][8][0], pipewire.Header{
|
||||||
|
ID: 2,
|
||||||
|
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||||
|
Size: 0xa8, Sequence: 8, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_REGISTRY_EVENT_GLOBAL 3", samplePWContainer[1][9][0], pipewire.Header{
|
||||||
|
ID: 2,
|
||||||
|
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||||
|
Size: 0xe8, Sequence: 9, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_REGISTRY_EVENT_GLOBAL 4", samplePWContainer[1][10][0], pipewire.Header{
|
||||||
|
ID: 2,
|
||||||
|
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||||
|
Size: 0xa0, Sequence: 10, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_REGISTRY_EVENT_GLOBAL 5", samplePWContainer[1][11][0], pipewire.Header{
|
||||||
|
ID: 2,
|
||||||
|
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||||
|
Size: 0xe0, Sequence: 11, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_REGISTRY_EVENT_GLOBAL 6", samplePWContainer[1][12][0], pipewire.Header{
|
||||||
|
ID: 2,
|
||||||
|
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||||
|
Size: 0xe0, Sequence: 12, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_REGISTRY_EVENT_GLOBAL 7", samplePWContainer[1][13][0], pipewire.Header{
|
||||||
|
ID: 2,
|
||||||
|
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||||
|
Size: 0x170, Sequence: 13, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_REGISTRY_EVENT_GLOBAL 8", samplePWContainer[1][14][0], pipewire.Header{
|
||||||
|
ID: 2,
|
||||||
|
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||||
|
Size: 0xe8, Sequence: 14, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_REGISTRY_EVENT_GLOBAL 9", samplePWContainer[1][15][0], pipewire.Header{
|
||||||
|
ID: 2,
|
||||||
|
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||||
|
Size: 0x178, Sequence: 15, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_REGISTRY_EVENT_GLOBAL 10", samplePWContainer[1][16][0], pipewire.Header{
|
||||||
|
ID: 2,
|
||||||
|
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||||
|
Size: 0xe8, Sequence: 16, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_REGISTRY_EVENT_GLOBAL 11", samplePWContainer[1][17][0], pipewire.Header{
|
||||||
|
ID: 2,
|
||||||
|
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||||
|
Size: 0x170, Sequence: 17, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_REGISTRY_EVENT_GLOBAL 12", samplePWContainer[1][18][0], pipewire.Header{
|
||||||
|
ID: 2,
|
||||||
|
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||||
|
Size: 0xe0, Sequence: 18, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_REGISTRY_EVENT_GLOBAL 13", samplePWContainer[1][19][0], pipewire.Header{
|
||||||
|
ID: 2,
|
||||||
|
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||||
|
Size: 0x170, Sequence: 19, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_REGISTRY_EVENT_GLOBAL 14", samplePWContainer[1][20][0], pipewire.Header{
|
||||||
|
ID: 2,
|
||||||
|
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||||
|
Size: 0xe8, Sequence: 20, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_REGISTRY_EVENT_GLOBAL 15", samplePWContainer[1][21][0], pipewire.Header{
|
||||||
|
ID: 2,
|
||||||
|
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||||
|
Size: 0x170, Sequence: 21, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_REGISTRY_EVENT_GLOBAL 16", samplePWContainer[1][22][0], pipewire.Header{
|
||||||
|
ID: 2,
|
||||||
|
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||||
|
Size: 0xe0, Sequence: 22, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_REGISTRY_EVENT_GLOBAL 17", samplePWContainer[1][23][0], pipewire.Header{
|
||||||
|
ID: 2,
|
||||||
|
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||||
|
Size: 0xe0, Sequence: 23, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_REGISTRY_EVENT_GLOBAL 18", samplePWContainer[1][24][0], pipewire.Header{
|
||||||
|
ID: 2,
|
||||||
|
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||||
|
Size: 0xe0, Sequence: 24, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_REGISTRY_EVENT_GLOBAL 19", samplePWContainer[1][25][0], pipewire.Header{
|
||||||
|
ID: 2,
|
||||||
|
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||||
|
Size: 0x160, Sequence: 25, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_REGISTRY_EVENT_GLOBAL 20", samplePWContainer[1][26][0], pipewire.Header{
|
||||||
|
ID: 2,
|
||||||
|
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||||
|
Size: 0xe0, Sequence: 26, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_REGISTRY_EVENT_GLOBAL 21", samplePWContainer[1][27][0], pipewire.Header{
|
||||||
|
ID: 2,
|
||||||
|
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||||
|
Size: 0x168, Sequence: 27, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_REGISTRY_EVENT_GLOBAL 22", samplePWContainer[1][28][0], pipewire.Header{
|
||||||
|
ID: 2,
|
||||||
|
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||||
|
Size: 0xe8, Sequence: 28, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_REGISTRY_EVENT_GLOBAL 23", samplePWContainer[1][29][0], pipewire.Header{
|
||||||
|
ID: 2,
|
||||||
|
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||||
|
Size: 0x178, Sequence: 29, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_REGISTRY_EVENT_GLOBAL 24", samplePWContainer[1][30][0], pipewire.Header{
|
||||||
|
ID: 2,
|
||||||
|
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||||
|
Size: 0x178, Sequence: 30, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_REGISTRY_EVENT_GLOBAL 25", samplePWContainer[1][31][0], pipewire.Header{
|
||||||
|
ID: 2,
|
||||||
|
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||||
|
Size: 0x168, Sequence: 31, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_REGISTRY_EVENT_GLOBAL 26", samplePWContainer[1][32][0], pipewire.Header{
|
||||||
|
ID: 2,
|
||||||
|
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||||
|
Size: 0x170, Sequence: 32, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_REGISTRY_EVENT_GLOBAL 27", samplePWContainer[1][33][0], pipewire.Header{
|
||||||
|
ID: 2,
|
||||||
|
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||||
|
Size: 0x178, Sequence: 33, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_REGISTRY_EVENT_GLOBAL 28", samplePWContainer[1][34][0], pipewire.Header{
|
||||||
|
ID: 2,
|
||||||
|
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||||
|
Size: 0x170, Sequence: 34, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_REGISTRY_EVENT_GLOBAL 29", samplePWContainer[1][35][0], pipewire.Header{
|
||||||
|
ID: 2,
|
||||||
|
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||||
|
Size: 0xe0, Sequence: 35, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_REGISTRY_EVENT_GLOBAL 30", samplePWContainer[1][36][0], pipewire.Header{
|
||||||
|
ID: 2,
|
||||||
|
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||||
|
Size: 0xe8, Sequence: 36, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_REGISTRY_EVENT_GLOBAL 31", samplePWContainer[1][37][0], pipewire.Header{
|
||||||
|
ID: 2,
|
||||||
|
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||||
|
Size: 0x118, Sequence: 37, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_REGISTRY_EVENT_GLOBAL 32", samplePWContainer[1][38][0], pipewire.Header{
|
||||||
|
ID: 2,
|
||||||
|
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||||
|
Size: 0x120, Sequence: 38, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_REGISTRY_EVENT_GLOBAL 33", samplePWContainer[1][39][0], pipewire.Header{
|
||||||
|
ID: 2,
|
||||||
|
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||||
|
Size: 0xd0, Sequence: 39, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_REGISTRY_EVENT_GLOBAL 34", samplePWContainer[1][40][0], pipewire.Header{
|
||||||
|
ID: 2,
|
||||||
|
Opcode: pipewire.PW_REGISTRY_EVENT_GLOBAL,
|
||||||
|
Size: 0x238, Sequence: 40, FileCount: 0,
|
||||||
|
}, nil},
|
||||||
|
|
||||||
|
{"PW_SECURITY_CONTEXT_METHOD_CREATE", []byte{
|
||||||
|
// Id
|
||||||
|
3, 0, 0, 0,
|
||||||
|
// size
|
||||||
|
0xd8, 0, 0,
|
||||||
|
// opcode
|
||||||
|
1,
|
||||||
|
// seq
|
||||||
|
5, 0, 0, 0,
|
||||||
|
// n_fds
|
||||||
|
2, 0, 0, 0,
|
||||||
|
}, pipewire.Header{ID: 3, Opcode: pipewire.PW_SECURITY_CONTEXT_METHOD_CREATE,
|
||||||
|
Size: 0xd8, Sequence: 5, FileCount: 2}, nil},
|
||||||
|
|
||||||
|
{"PW_SECURITY_CONTEXT_METHOD_NUM", []byte{
|
||||||
|
// Id
|
||||||
|
0, 0, 0, 0,
|
||||||
|
// size
|
||||||
|
0x28, 0, 0,
|
||||||
|
// opcode
|
||||||
|
2,
|
||||||
|
// seq
|
||||||
|
6, 0, 0, 0,
|
||||||
|
// n_fds
|
||||||
|
0, 0, 0, 0,
|
||||||
|
}, pipewire.Header{ID: 0, Opcode: pipewire.PW_SECURITY_CONTEXT_METHOD_NUM,
|
||||||
|
Size: 0x28, Sequence: 6, FileCount: 0}, nil},
|
||||||
|
}.run(t)
|
||||||
|
|
||||||
|
t.Run("size range", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if _, err := (&pipewire.Header{Size: 0xff000000}).MarshalBinary(); !reflect.DeepEqual(err, pipewire.ErrSizeRange) {
|
||||||
|
t.Errorf("UnmarshalBinary: error = %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("header size", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if err := (*pipewire.Header)(nil).UnmarshalBinary(nil); !reflect.DeepEqual(err, pipewire.ErrBadHeader) {
|
||||||
|
t.Errorf("UnmarshalBinary: error = %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
393
internal/pipewire/pipewire.go
Normal file
393
internal/pipewire/pipewire.go
Normal file
@ -0,0 +1,393 @@
|
|||||||
|
// Package pipewire provides a partial implementation of the PipeWire protocol native.
|
||||||
|
//
|
||||||
|
// This implementation is created based on black box analysis and very limited static
|
||||||
|
// analysis. The PipeWire documentation is vague and mostly nonexistent, and source code
|
||||||
|
// readability is not great due to frequent macro abuse, confusing and inconsistent naming
|
||||||
|
// schemes, almost complete absence of comments and the multiple layers of abstractions
|
||||||
|
// even internal to the library. The convoluted build system and frequent (mis)use of
|
||||||
|
// dlopen(3) further complicates static analysis efforts.
|
||||||
|
//
|
||||||
|
// Because of this, extreme care must be taken when reusing any code found in this package.
|
||||||
|
// While it is extensively tested to be correct for its role within Hakurei, remember that
|
||||||
|
// work is only done against PipeWire behaviour specific to this use case, and it is nearly
|
||||||
|
// impossible to guarantee that this interpretation of its behaviour is intended, or correct
|
||||||
|
// for any other uses of the protocol.
|
||||||
|
package pipewire
|
||||||
|
|
||||||
|
/* pipewire/device.h */
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_TYPE_INTERFACE_Device = PW_TYPE_INFO_INTERFACE_BASE + "Device"
|
||||||
|
PW_DEVICE_PERM_MASK = PW_PERM_RWXM
|
||||||
|
PW_VERSION_DEVICE = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_DEVICE_CHANGE_MASK_PROPS = 1 << iota
|
||||||
|
PW_DEVICE_CHANGE_MASK_PARAMS
|
||||||
|
|
||||||
|
PW_DEVICE_CHANGE_MASK_ALL = 1<<iota - 1
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_DEVICE_EVENT_INFO = iota
|
||||||
|
PW_DEVICE_EVENT_PARAM
|
||||||
|
PW_DEVICE_EVENT_NUM
|
||||||
|
|
||||||
|
PW_VERSION_DEVICE_EVENTS = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_DEVICE_METHOD_ADD_LISTENER = iota
|
||||||
|
PW_DEVICE_METHOD_SUBSCRIBE_PARAMS
|
||||||
|
PW_DEVICE_METHOD_ENUM_PARAMS
|
||||||
|
PW_DEVICE_METHOD_SET_PARAM
|
||||||
|
PW_DEVICE_METHOD_NUM
|
||||||
|
|
||||||
|
PW_VERSION_DEVICE_METHODS = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
/* pipewire/factory.h */
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_TYPE_INTERFACE_Factory = PW_TYPE_INFO_INTERFACE_BASE + "Factory"
|
||||||
|
PW_FACTORY_PERM_MASK = PW_PERM_R | PW_PERM_M
|
||||||
|
PW_VERSION_FACTORY = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_FACTORY_CHANGE_MASK_PROPS = 1 << iota
|
||||||
|
|
||||||
|
PW_FACTORY_CHANGE_MASK_ALL = 1<<iota - 1
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_FACTORY_EVENT_INFO = iota
|
||||||
|
PW_FACTORY_EVENT_NUM
|
||||||
|
|
||||||
|
PW_VERSION_FACTORY_EVENTS = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_FACTORY_METHOD_ADD_LISTENER = iota
|
||||||
|
PW_FACTORY_METHOD_NUM
|
||||||
|
|
||||||
|
PW_VERSION_FACTORY_METHODS = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
/* pipewire/link.h */
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_TYPE_INTERFACE_Link = PW_TYPE_INFO_INTERFACE_BASE + "Link"
|
||||||
|
PW_LINK_PERM_MASK = PW_PERM_R | PW_PERM_X
|
||||||
|
PW_VERSION_LINK = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_LINK_STATE_ERROR = iota - 2 // the link is in error
|
||||||
|
PW_LINK_STATE_UNLINKED // the link is unlinked
|
||||||
|
PW_LINK_STATE_INIT // the link is initialized
|
||||||
|
PW_LINK_STATE_NEGOTIATING // the link is negotiating formats
|
||||||
|
PW_LINK_STATE_ALLOCATING // the link is allocating buffers
|
||||||
|
PW_LINK_STATE_PAUSED // the link is paused
|
||||||
|
PW_LINK_STATE_ACTIVE // the link is active
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_LINK_CHANGE_MASK_STATE = (1 << iota)
|
||||||
|
PW_LINK_CHANGE_MASK_FORMAT
|
||||||
|
PW_LINK_CHANGE_MASK_PROPS
|
||||||
|
|
||||||
|
PW_LINK_CHANGE_MASK_ALL = 1<<iota - 1
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_LINK_EVENT_INFO = iota
|
||||||
|
PW_LINK_EVENT_NUM
|
||||||
|
|
||||||
|
PW_VERSION_LINK_EVENTS = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_LINK_METHOD_ADD_LISTENER = iota
|
||||||
|
PW_LINK_METHOD_NUM
|
||||||
|
|
||||||
|
PW_VERSION_LINK_METHODS = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
/* pipewire/module.h */
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_TYPE_INTERFACE_Module = PW_TYPE_INFO_INTERFACE_BASE + "Module"
|
||||||
|
PW_MODULE_PERM_MASK = PW_PERM_R | PW_PERM_M
|
||||||
|
PW_VERSION_MODULE = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_MODULE_CHANGE_MASK_PROPS = 1 << iota
|
||||||
|
|
||||||
|
PW_MODULE_CHANGE_MASK_ALL = 1<<iota - 1
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_MODULE_EVENT_INFO = iota
|
||||||
|
PW_MODULE_EVENT_NUM
|
||||||
|
|
||||||
|
PW_VERSION_MODULE_EVENTS = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_MODULE_METHOD_ADD_LISTENER = iota
|
||||||
|
PW_MODULE_METHOD_NUM
|
||||||
|
|
||||||
|
PW_VERSION_MODULE_METHODS = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
/* pipewire/impl-module.h */
|
||||||
|
|
||||||
|
const (
|
||||||
|
PIPEWIRE_SYMBOL_MODULE_INIT = "pipewire__module_init"
|
||||||
|
PIPEWIRE_MODULE_PREFIX = "libpipewire-"
|
||||||
|
|
||||||
|
PW_VERSION_IMPL_MODULE_EVENTS = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
/* pipewire/node.h */
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_TYPE_INTERFACE_Node = PW_TYPE_INFO_INTERFACE_BASE + "Node"
|
||||||
|
PW_NODE_PERM_MASK = PW_PERM_RWXML
|
||||||
|
PW_VERSION_NODE = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_NODE_STATE_ERROR = iota - 1 // error state
|
||||||
|
PW_NODE_STATE_CREATING // the node is being created
|
||||||
|
PW_NODE_STATE_SUSPENDED // the node is suspended, the device might be closed
|
||||||
|
PW_NODE_STATE_IDLE // the node is running but there is no active port
|
||||||
|
PW_NODE_STATE_RUNNING // the node is running
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_NODE_CHANGE_MASK_INPUT_PORTS = 1 << iota
|
||||||
|
PW_NODE_CHANGE_MASK_OUTPUT_PORTS
|
||||||
|
PW_NODE_CHANGE_MASK_STATE
|
||||||
|
PW_NODE_CHANGE_MASK_PROPS
|
||||||
|
PW_NODE_CHANGE_MASK_PARAMS
|
||||||
|
|
||||||
|
PW_NODE_CHANGE_MASK_ALL = 1<<iota - 1
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_NODE_EVENT_INFO = iota
|
||||||
|
PW_NODE_EVENT_PARAM
|
||||||
|
PW_NODE_EVENT_NUM
|
||||||
|
|
||||||
|
PW_VERSION_NODE_EVENTS = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_NODE_METHOD_ADD_LISTENER = iota
|
||||||
|
PW_NODE_METHOD_SUBSCRIBE_PARAMS
|
||||||
|
PW_NODE_METHOD_ENUM_PARAMS
|
||||||
|
PW_NODE_METHOD_SET_PARAM
|
||||||
|
PW_NODE_METHOD_SEND_COMMAND
|
||||||
|
PW_NODE_METHOD_NUM
|
||||||
|
|
||||||
|
PW_VERSION_NODE_METHODS = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
/* pipewire/permission.h */
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_PERM_R = 0400 // object can be seen and events can be received
|
||||||
|
PW_PERM_W = 0200 // methods can be called that modify the object
|
||||||
|
PW_PERM_X = 0100 // methods can be called on the object. The W flag must be present in order to call methods that modify the object.
|
||||||
|
PW_PERM_M = 0010 // metadata can be set on object, Since 0.3.9
|
||||||
|
PW_PERM_L = 0020 // a link can be made between a node that doesn't have permission to see the other node, Since 0.3.77
|
||||||
|
|
||||||
|
PW_PERM_RW = PW_PERM_R | PW_PERM_W
|
||||||
|
PW_PERM_RWX = PW_PERM_RW | PW_PERM_X
|
||||||
|
PW_PERM_RWXM = PW_PERM_RWX | PW_PERM_M
|
||||||
|
PW_PERM_RWXML = PW_PERM_RWXM | PW_PERM_L
|
||||||
|
|
||||||
|
PW_PERM_ALL = PW_PERM_RWXM
|
||||||
|
PW_PERM_INVALID Word = 0xffffffff
|
||||||
|
)
|
||||||
|
|
||||||
|
/* pipewire/port.h */
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_TYPE_INTERFACE_Port = PW_TYPE_INFO_INTERFACE_BASE + "Port"
|
||||||
|
PW_PORT_PERM_MASK = PW_PERM_R | PW_PERM_X | PW_PERM_M
|
||||||
|
PW_VERSION_PORT = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_PORT_CHANGE_MASK_PROPS = 1 << iota
|
||||||
|
PW_PORT_CHANGE_MASK_PARAMS
|
||||||
|
|
||||||
|
PW_PORT_CHANGE_MASK_ALL = 1<<iota - 1
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_PORT_EVENT_INFO = iota
|
||||||
|
PW_PORT_EVENT_PARAM
|
||||||
|
PW_PORT_EVENT_NUM
|
||||||
|
|
||||||
|
PW_VERSION_PORT_EVENTS = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_PORT_METHOD_ADD_LISTENER = iota
|
||||||
|
PW_PORT_METHOD_SUBSCRIBE_PARAMS
|
||||||
|
PW_PORT_METHOD_ENUM_PARAMS
|
||||||
|
PW_PORT_METHOD_NUM
|
||||||
|
|
||||||
|
PW_VERSION_PORT_METHODS = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
/* pipewire/extensions/client-node.h */
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_TYPE_INTERFACE_ClientNode = PW_TYPE_INFO_INTERFACE_BASE + "ClientNode"
|
||||||
|
PW_VERSION_CLIENT_NODE = 6
|
||||||
|
|
||||||
|
PW_EXTENSION_MODULE_CLIENT_NODE = PIPEWIRE_MODULE_PREFIX + "module-client-node"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_CLIENT_NODE_EVENT_TRANSPORT = iota
|
||||||
|
PW_CLIENT_NODE_EVENT_SET_PARAM
|
||||||
|
PW_CLIENT_NODE_EVENT_SET_IO
|
||||||
|
PW_CLIENT_NODE_EVENT_EVENT
|
||||||
|
PW_CLIENT_NODE_EVENT_COMMAND
|
||||||
|
PW_CLIENT_NODE_EVENT_ADD_PORT
|
||||||
|
PW_CLIENT_NODE_EVENT_REMOVE_PORT
|
||||||
|
PW_CLIENT_NODE_EVENT_PORT_SET_PARAM
|
||||||
|
PW_CLIENT_NODE_EVENT_PORT_USE_BUFFERS
|
||||||
|
PW_CLIENT_NODE_EVENT_PORT_SET_IO
|
||||||
|
PW_CLIENT_NODE_EVENT_SET_ACTIVATION
|
||||||
|
PW_CLIENT_NODE_EVENT_PORT_SET_MIX_INFO
|
||||||
|
PW_CLIENT_NODE_EVENT_NUM
|
||||||
|
|
||||||
|
PW_VERSION_CLIENT_NODE_EVENTS = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_CLIENT_NODE_METHOD_ADD_LISTENER = iota
|
||||||
|
PW_CLIENT_NODE_METHOD_GET_NODE
|
||||||
|
PW_CLIENT_NODE_METHOD_UPDATE
|
||||||
|
PW_CLIENT_NODE_METHOD_PORT_UPDATE
|
||||||
|
PW_CLIENT_NODE_METHOD_SET_ACTIVE
|
||||||
|
PW_CLIENT_NODE_METHOD_EVENT
|
||||||
|
PW_CLIENT_NODE_METHOD_PORT_BUFFERS
|
||||||
|
PW_CLIENT_NODE_METHOD_NUM
|
||||||
|
|
||||||
|
PW_VERSION_CLIENT_NODE_METHODS = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_CLIENT_NODE_UPDATE_PARAMS = 1 << iota
|
||||||
|
PW_CLIENT_NODE_UPDATE_INFO
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_CLIENT_NODE_PORT_UPDATE_PARAMS = 1 << iota
|
||||||
|
PW_CLIENT_NODE_PORT_UPDATE_INFO
|
||||||
|
)
|
||||||
|
|
||||||
|
/* pipewire/extensions/metadata.h */
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_TYPE_INTERFACE_Metadata = PW_TYPE_INFO_INTERFACE_BASE + "Metadata"
|
||||||
|
PW_METADATA_PERM_MASK = PW_PERM_RWX
|
||||||
|
PW_VERSION_METADATA = 3
|
||||||
|
|
||||||
|
PW_EXTENSION_MODULE_METADATA = PIPEWIRE_MODULE_PREFIX + "module-metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_METADATA_EVENT_PROPERTY = iota
|
||||||
|
PW_METADATA_EVENT_NUM
|
||||||
|
|
||||||
|
PW_VERSION_METADATA_EVENTS = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_METADATA_METHOD_ADD_LISTENER = iota
|
||||||
|
PW_METADATA_METHOD_SET_PROPERTY
|
||||||
|
PW_METADATA_METHOD_CLEAR
|
||||||
|
PW_METADATA_METHOD_NUM
|
||||||
|
|
||||||
|
PW_VERSION_METADATA_METHODS = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_KEY_METADATA_NAME = "metadata.name"
|
||||||
|
PW_KEY_METADATA_VALUES = "metadata.values"
|
||||||
|
)
|
||||||
|
|
||||||
|
/* pipewire/extensions/profiler.h */
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_TYPE_INTERFACE_Profiler = PW_TYPE_INFO_INTERFACE_BASE + "Profiler"
|
||||||
|
PW_VERSION_PROFILER = 3
|
||||||
|
PW_PROFILER_PERM_MASK = PW_PERM_R
|
||||||
|
|
||||||
|
PW_EXTENSION_MODULE_PROFILER = PIPEWIRE_MODULE_PREFIX + "module-profiler"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_PROFILER_EVENT_PROFILE = iota
|
||||||
|
PW_PROFILER_EVENT_NUM
|
||||||
|
|
||||||
|
PW_VERSION_PROFILER_EVENTS = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_PROFILER_METHOD_ADD_LISTENER = iota
|
||||||
|
PW_PROFILER_METHOD_NUM
|
||||||
|
|
||||||
|
PW_VERSION_PROFILER_METHODS = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_KEY_PROFILER_NAME = "profiler.name"
|
||||||
|
)
|
||||||
|
|
||||||
|
/* pipewire/extensions/security-context.h */
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_TYPE_INTERFACE_SecurityContext = PW_TYPE_INFO_INTERFACE_BASE + "SecurityContext"
|
||||||
|
PW_SECURITY_CONTEXT_PERM_MASK = PW_PERM_RWX
|
||||||
|
PW_VERSION_SECURITY_CONTEXT = 3
|
||||||
|
|
||||||
|
PW_EXTENSION_MODULE_SECURITY_CONTEXT = PIPEWIRE_MODULE_PREFIX + "module-security-context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_SECURITY_CONTEXT_EVENT_NUM = iota
|
||||||
|
|
||||||
|
PW_VERSION_SECURITY_CONTEXT_EVENTS = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_SECURITY_CONTEXT_METHOD_ADD_LISTENER = iota
|
||||||
|
PW_SECURITY_CONTEXT_METHOD_CREATE
|
||||||
|
PW_SECURITY_CONTEXT_METHOD_NUM
|
||||||
|
|
||||||
|
PW_VERSION_SECURITY_CONTEXT_METHODS = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
/* pipewire/type.h */
|
||||||
|
|
||||||
|
const (
|
||||||
|
PW_TYPE_INFO_BASE = "PipeWire:"
|
||||||
|
|
||||||
|
PW_TYPE_INFO_Object = PW_TYPE_INFO_BASE + "Object"
|
||||||
|
PW_TYPE_INFO_OBJECT_BASE = PW_TYPE_INFO_Object + ":"
|
||||||
|
|
||||||
|
PW_TYPE_INFO_Interface = PW_TYPE_INFO_BASE + "Interface"
|
||||||
|
PW_TYPE_INFO_INTERFACE_BASE = PW_TYPE_INFO_Interface + ":"
|
||||||
|
)
|
||||||
59
internal/pipewire/pipewire_test.go
Normal file
59
internal/pipewire/pipewire_test.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package pipewire_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"encoding/binary"
|
||||||
|
|
||||||
|
"hakurei.app/internal/pipewire"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed testdata/pw-container-00-sendmsg
|
||||||
|
samplePWContainer00 string
|
||||||
|
//go:embed testdata/pw-container-01-recvmsg
|
||||||
|
samplePWContainer01 string
|
||||||
|
//go:embed testdata/pw-container-03-sendmsg
|
||||||
|
samplePWContainer03 string
|
||||||
|
//go:embed testdata/pw-container-04-recvmsg
|
||||||
|
samplePWContainer04 string
|
||||||
|
//go:embed testdata/pw-container-06-sendmsg
|
||||||
|
samplePWContainer06 string
|
||||||
|
//go:embed testdata/pw-container-07-recvmsg
|
||||||
|
samplePWContainer07 string
|
||||||
|
|
||||||
|
// samplePWContainer is a collection of messages from the pw-container sample.
|
||||||
|
samplePWContainer = [...][][3][]byte{
|
||||||
|
splitMessages(samplePWContainer00),
|
||||||
|
splitMessages(samplePWContainer01),
|
||||||
|
nil,
|
||||||
|
splitMessages(samplePWContainer03),
|
||||||
|
splitMessages(samplePWContainer04),
|
||||||
|
nil,
|
||||||
|
splitMessages(samplePWContainer06),
|
||||||
|
splitMessages(samplePWContainer07),
|
||||||
|
nil,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// splitMessages splits concatenated messages into groups of
|
||||||
|
// header, payload, footer of each individual message.
|
||||||
|
// splitMessages panics on any decoding error.
|
||||||
|
func splitMessages(iovec string) (messages [][3][]byte) {
|
||||||
|
data := []byte(iovec)
|
||||||
|
messages = make([][3][]byte, 0, 1<<7)
|
||||||
|
|
||||||
|
var header pipewire.Header
|
||||||
|
for len(data) != 0 {
|
||||||
|
if err := header.UnmarshalBinary(data[:pipewire.SizeHeader]); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
size := pipewire.SizePrefix + binary.NativeEndian.Uint32(data[pipewire.SizeHeader:])
|
||||||
|
messages = append(messages, [3][]byte{
|
||||||
|
data[:pipewire.SizeHeader],
|
||||||
|
data[pipewire.SizeHeader : pipewire.SizeHeader+size],
|
||||||
|
data[pipewire.SizeHeader+size : pipewire.SizeHeader+header.Size],
|
||||||
|
})
|
||||||
|
data = data[pipewire.SizeHeader+header.Size:]
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
621
internal/pipewire/pod.go
Normal file
621
internal/pipewire/pod.go
Normal file
@ -0,0 +1,621 @@
|
|||||||
|
package pipewire
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// A Word is a 32-bit unsigned integer.
|
||||||
|
//
|
||||||
|
// Values internal to a message appear to always be aligned to 32-bit boundary.
|
||||||
|
Word = uint32
|
||||||
|
|
||||||
|
// A Bool is a boolean value representing SPA_TYPE_Bool.
|
||||||
|
Bool = bool
|
||||||
|
// An Id is an enumerated value representing SPA_TYPE_Id.
|
||||||
|
Id = Word
|
||||||
|
// An Int is a signed integer value representing SPA_TYPE_Int.
|
||||||
|
Int = int32
|
||||||
|
// A Long is a signed integer value representing SPA_TYPE_Long.
|
||||||
|
Long = int64
|
||||||
|
// A Float is a floating point value representing SPA_TYPE_Float.
|
||||||
|
Float = float32
|
||||||
|
// A Double is a floating point value representing SPA_TYPE_Double.
|
||||||
|
Double = float64
|
||||||
|
// A String is a string value representing SPA_TYPE_String.
|
||||||
|
String = string
|
||||||
|
// Bytes is a byte slice representing SPA_TYPE_Bytes.
|
||||||
|
Bytes = []byte
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// SizeAlign is the boundary which POD starts are always aligned to.
|
||||||
|
SizeAlign = 8
|
||||||
|
|
||||||
|
// SizeSPrefix is the fixed, unpadded size of the fixed-size prefix encoding POD wire size.
|
||||||
|
SizeSPrefix = 4
|
||||||
|
// SizeTPrefix is the fixed, unpadded size of the fixed-size prefix encoding POD value type.
|
||||||
|
SizeTPrefix = 4
|
||||||
|
// SizePrefix is the fixed, unpadded size of the fixed-size POD prefix.
|
||||||
|
SizePrefix = SizeSPrefix + SizeTPrefix
|
||||||
|
|
||||||
|
// SizeId is the fixed, unpadded size of a [SPA_TYPE_Id] value.
|
||||||
|
SizeId Word = 4
|
||||||
|
// SizeInt is the fixed, unpadded size of a [SPA_TYPE_Int] value.
|
||||||
|
SizeInt Word = 4
|
||||||
|
// SizeLong is the fixed, unpadded size of a [SPA_TYPE_Long] value.
|
||||||
|
SizeLong Word = 8
|
||||||
|
)
|
||||||
|
|
||||||
|
/* Basic types */
|
||||||
|
const (
|
||||||
|
/* POD's can contain a number of basic SPA types: */
|
||||||
|
|
||||||
|
SPA_TYPE_START = 0x00000 + iota
|
||||||
|
SPA_TYPE_None // No value or a NULL pointer.
|
||||||
|
SPA_TYPE_Bool // A boolean value.
|
||||||
|
SPA_TYPE_Id // An enumerated value.
|
||||||
|
SPA_TYPE_Int // An integer value, 32-bit.
|
||||||
|
SPA_TYPE_Long // An integer value, 64-bit.
|
||||||
|
SPA_TYPE_Float // A floating point value, 32-bit.
|
||||||
|
SPA_TYPE_Double // A floating point value, 64-bit.
|
||||||
|
SPA_TYPE_String // A string.
|
||||||
|
SPA_TYPE_Bytes // A byte array.
|
||||||
|
SPA_TYPE_Rectangle // A rectangle with width and height.
|
||||||
|
SPA_TYPE_Fraction // A fraction with numerator and denominator.
|
||||||
|
SPA_TYPE_Bitmap // An array of bits.
|
||||||
|
|
||||||
|
/* POD's can be grouped together in these container types: */
|
||||||
|
|
||||||
|
SPA_TYPE_Array // An array of equal sized objects.
|
||||||
|
SPA_TYPE_Struct // A collection of types and objects.
|
||||||
|
SPA_TYPE_Object // An object with properties.
|
||||||
|
SPA_TYPE_Sequence // A timed sequence of POD's.
|
||||||
|
|
||||||
|
/* POD's can also contain some extra types: */
|
||||||
|
|
||||||
|
SPA_TYPE_Pointer // A typed pointer in memory.
|
||||||
|
SPA_TYPE_Fd // A file descriptor.
|
||||||
|
SPA_TYPE_Choice // A choice of values.
|
||||||
|
SPA_TYPE_Pod // A generic type for the POD itself.
|
||||||
|
|
||||||
|
_SPA_TYPE_LAST // not part of ABI
|
||||||
|
)
|
||||||
|
|
||||||
|
// A KnownSize value has known POD encoded size before serialisation.
|
||||||
|
type KnownSize interface {
|
||||||
|
// Size returns the POD encoded size of the receiver.
|
||||||
|
Size() Word
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaddingSize returns the padding size corresponding to a wire size.
|
||||||
|
func PaddingSize[W Word | int](wireSize W) W { return (SizeAlign - (wireSize)%SizeAlign) % SizeAlign }
|
||||||
|
|
||||||
|
// PaddedSize returns the padded size corresponding to a wire size.
|
||||||
|
func PaddedSize[W Word | int](wireSize W) W { return wireSize + PaddingSize(wireSize) }
|
||||||
|
|
||||||
|
// Size returns prefixed and padded size corresponding to a wire size.
|
||||||
|
func Size[W Word | int](wireSize W) W { return SizePrefix + PaddedSize(wireSize) }
|
||||||
|
|
||||||
|
// SizeString returns prefixed and padded size corresponding to a string.
|
||||||
|
func SizeString[W Word | int](s string) W { return Size(W(len(s)) + 1) }
|
||||||
|
|
||||||
|
// PODMarshaler is the interface implemented by an object that can
|
||||||
|
// marshal itself into PipeWire POD encoding.
|
||||||
|
type PODMarshaler interface {
|
||||||
|
// MarshalPOD encodes the receiver into PipeWire POD encoding,
|
||||||
|
// appends it to data, and returns the result.
|
||||||
|
MarshalPOD(data []byte) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// An UnsupportedTypeError is returned by [Marshal] when attempting
|
||||||
|
// to encode an unsupported value type.
|
||||||
|
type UnsupportedTypeError struct{ Type reflect.Type }
|
||||||
|
|
||||||
|
func (e *UnsupportedTypeError) Error() string { return "unsupported type: " + e.Type.String() }
|
||||||
|
|
||||||
|
// An UnsupportedSizeError is returned by [Marshal] when attempting
|
||||||
|
// to encode a value with its encoded size exceeding what could be
|
||||||
|
// represented by the format.
|
||||||
|
type UnsupportedSizeError int
|
||||||
|
|
||||||
|
func (e UnsupportedSizeError) Error() string { return "size out of range: " + strconv.Itoa(int(e)) }
|
||||||
|
|
||||||
|
// Marshal returns the PipeWire POD encoding of v.
|
||||||
|
func Marshal(v any) ([]byte, error) {
|
||||||
|
var data []byte
|
||||||
|
if s, ok := v.(KnownSize); ok {
|
||||||
|
data = make([]byte, 0, s.Size())
|
||||||
|
}
|
||||||
|
return MarshalAppend(data, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalAppend appends the PipeWire POD encoding of v to data.
|
||||||
|
func MarshalAppend(data []byte, v any) ([]byte, error) {
|
||||||
|
return marshalValueAppend(data, reflect.ValueOf(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// appendInner calls f and handles size prefix and padding around the appended data.
|
||||||
|
// f must only append to data.
|
||||||
|
func appendInner(data []byte, f func(data []byte) ([]byte, error)) ([]byte, error) {
|
||||||
|
data = append(data, make([]byte, SizeSPrefix)...)
|
||||||
|
|
||||||
|
rData, err := f(data)
|
||||||
|
if err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
|
||||||
|
size := len(rData) - len(data) + SizeSPrefix
|
||||||
|
// compensated for size and type prefix
|
||||||
|
wireSize := size - SizePrefix
|
||||||
|
if wireSize > math.MaxUint32 {
|
||||||
|
return data, UnsupportedSizeError(wireSize)
|
||||||
|
}
|
||||||
|
binary.NativeEndian.PutUint32(rData[len(data)-SizeSPrefix:len(data)], Word(wireSize))
|
||||||
|
rData = append(rData, make([]byte, PaddingSize(size))...)
|
||||||
|
|
||||||
|
return rData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// marshalValueAppendRaw implements [MarshalAppend] on [reflect.Value].
|
||||||
|
func marshalValueAppend(data []byte, v reflect.Value) ([]byte, error) {
|
||||||
|
if v.CanInterface() && (v.Kind() != reflect.Pointer || !v.IsNil()) {
|
||||||
|
if m, ok := v.Interface().(PODMarshaler); ok {
|
||||||
|
var err error
|
||||||
|
data, err = m.MarshalPOD(data)
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return appendInner(data, func(data []byte) ([]byte, error) { return marshalValueAppendRaw(data, v) })
|
||||||
|
}
|
||||||
|
|
||||||
|
// marshalValueAppendRaw implements [MarshalAppend] on [reflect.Value] without the size prefix.
|
||||||
|
func marshalValueAppendRaw(data []byte, v reflect.Value) ([]byte, error) {
|
||||||
|
switch v.Kind() {
|
||||||
|
case reflect.Uint32:
|
||||||
|
data = binary.NativeEndian.AppendUint32(data, SPA_TYPE_Id)
|
||||||
|
data = binary.NativeEndian.AppendUint32(data, Word(v.Uint()))
|
||||||
|
return data, nil
|
||||||
|
|
||||||
|
case reflect.Int32:
|
||||||
|
data = binary.NativeEndian.AppendUint32(data, SPA_TYPE_Int)
|
||||||
|
data = binary.NativeEndian.AppendUint32(data, Word(v.Int()))
|
||||||
|
return data, nil
|
||||||
|
|
||||||
|
case reflect.Int64:
|
||||||
|
data = binary.NativeEndian.AppendUint32(data, SPA_TYPE_Long)
|
||||||
|
data = binary.NativeEndian.AppendUint64(data, uint64(v.Int()))
|
||||||
|
return data, nil
|
||||||
|
|
||||||
|
case reflect.Struct:
|
||||||
|
data = binary.NativeEndian.AppendUint32(data, SPA_TYPE_Struct)
|
||||||
|
var err error
|
||||||
|
for i := 0; i < v.NumField(); i++ {
|
||||||
|
data, err = marshalValueAppend(data, v.Field(i))
|
||||||
|
if err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
|
||||||
|
case reflect.Pointer:
|
||||||
|
if v.IsNil() {
|
||||||
|
data = binary.NativeEndian.AppendUint32(data, SPA_TYPE_None)
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
return marshalValueAppendRaw(data, v.Elem())
|
||||||
|
|
||||||
|
case reflect.String:
|
||||||
|
data = binary.NativeEndian.AppendUint32(data, SPA_TYPE_String)
|
||||||
|
data = append(data, []byte(v.String())...)
|
||||||
|
data = append(data, 0)
|
||||||
|
return data, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return data, &UnsupportedTypeError{v.Type()}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PODUnmarshaler is the interface implemented by an object that can
|
||||||
|
// unmarshal a PipeWire POD encoding representation of itself.
|
||||||
|
type PODUnmarshaler interface {
|
||||||
|
// UnmarshalPOD must be able to decode the form generated by MarshalPOD.
|
||||||
|
// UnmarshalPOD must copy the data if it wishes to retain the data
|
||||||
|
// after returning.
|
||||||
|
UnmarshalPOD(data []byte) (Word, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// An InvalidUnmarshalError describes an invalid argument passed to [Unmarshal].
|
||||||
|
// (The argument to [Unmarshal] must be a non-nil pointer.)
|
||||||
|
type InvalidUnmarshalError struct{ Type reflect.Type }
|
||||||
|
|
||||||
|
func (e *InvalidUnmarshalError) Error() string {
|
||||||
|
if e.Type == nil {
|
||||||
|
return "attempting to unmarshal to nil"
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Type.Kind() != reflect.Pointer {
|
||||||
|
return "attempting to unmarshal to non-pointer type: " + e.Type.String()
|
||||||
|
}
|
||||||
|
return "attempting to unmarshal to nil " + e.Type.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal parses the PipeWire POD encoded data and stores the result
|
||||||
|
// in the value pointed to by v. If v is nil or not a pointer,
|
||||||
|
// Unmarshal returns an [InvalidUnmarshalError].
|
||||||
|
func Unmarshal(data []byte, v any) error {
|
||||||
|
if n, err := UnmarshalNext(data, v); err != nil {
|
||||||
|
return err
|
||||||
|
} else if len(data) > int(n) {
|
||||||
|
return &TrailingGarbageError{data[int(n):]}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalNext implements [Unmarshal] but returns the size of message decoded
|
||||||
|
// and skips the final trailing garbage check.
|
||||||
|
func UnmarshalNext(data []byte, v any) (size Word, err error) {
|
||||||
|
rv := reflect.ValueOf(v)
|
||||||
|
if rv.Kind() != reflect.Pointer || rv.IsNil() {
|
||||||
|
return 0, &InvalidUnmarshalError{reflect.TypeOf(v)}
|
||||||
|
}
|
||||||
|
err = unmarshalValue(data, rv.Elem(), &size)
|
||||||
|
// prefix and padding size
|
||||||
|
size = Size(size)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalSetError describes a value that cannot be set during [Unmarshal].
|
||||||
|
// This is likely an unexported struct field.
|
||||||
|
type UnmarshalSetError struct{ Type reflect.Type }
|
||||||
|
|
||||||
|
func (u *UnmarshalSetError) Error() string { return "cannot set: " + u.Type.String() }
|
||||||
|
|
||||||
|
// A TrailingGarbageError describes extra bytes after decoding
|
||||||
|
// has completed during [Unmarshal].
|
||||||
|
type TrailingGarbageError struct{ Data []byte }
|
||||||
|
|
||||||
|
func (e *TrailingGarbageError) Error() string {
|
||||||
|
if len(e.Data) < SizePrefix {
|
||||||
|
return "got " + strconv.Itoa(len(e.Data)) + " bytes of trailing garbage"
|
||||||
|
}
|
||||||
|
return "data has extra values starting with type " + strconv.Itoa(int(binary.NativeEndian.Uint32(e.Data[SizeSPrefix:])))
|
||||||
|
}
|
||||||
|
|
||||||
|
// A StringTerminationError describes an incorrectly terminated string
|
||||||
|
// encountered during [Unmarshal].
|
||||||
|
type StringTerminationError struct{ Value byte }
|
||||||
|
|
||||||
|
func (e StringTerminationError) Error() string {
|
||||||
|
return "got byte " + strconv.Itoa(int(e.Value)) + " instead of NUL"
|
||||||
|
}
|
||||||
|
|
||||||
|
// unmarshalValue implements [Unmarshal] on [reflect.Value] without compensating for prefix and padding size.
|
||||||
|
func unmarshalValue(data []byte, v reflect.Value, wireSizeP *Word) error {
|
||||||
|
if !v.CanSet() {
|
||||||
|
return &UnmarshalSetError{v.Type()}
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.CanInterface() {
|
||||||
|
if v.Kind() == reflect.Pointer {
|
||||||
|
v.Set(reflect.New(v.Type().Elem()))
|
||||||
|
}
|
||||||
|
|
||||||
|
if u, ok := v.Interface().(PODUnmarshaler); ok {
|
||||||
|
var err error
|
||||||
|
*wireSizeP, err = u.UnmarshalPOD(data)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v.Kind() {
|
||||||
|
case reflect.Uint32:
|
||||||
|
*wireSizeP = SizeId
|
||||||
|
if err := unmarshalCheckTypeBounds(&data, SPA_TYPE_Id, wireSizeP); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.SetUint(uint64(binary.NativeEndian.Uint32(data)))
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case reflect.Int32:
|
||||||
|
*wireSizeP = SizeInt
|
||||||
|
if err := unmarshalCheckTypeBounds(&data, SPA_TYPE_Int, wireSizeP); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.SetInt(int64(binary.NativeEndian.Uint32(data)))
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case reflect.Int64:
|
||||||
|
*wireSizeP = SizeLong
|
||||||
|
if err := unmarshalCheckTypeBounds(&data, SPA_TYPE_Long, wireSizeP); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.SetInt(int64(binary.NativeEndian.Uint64(data)))
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case reflect.Struct:
|
||||||
|
*wireSizeP = 0
|
||||||
|
if err := unmarshalCheckTypeBounds(&data, SPA_TYPE_Struct, wireSizeP); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var fieldWireSize Word
|
||||||
|
for i := 0; i < v.NumField(); i++ {
|
||||||
|
if err := unmarshalValue(data, v.Field(i), &fieldWireSize); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// bounds check completed in successful call to unmarshalValue
|
||||||
|
data = data[Size(fieldWireSize):]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) != 0 {
|
||||||
|
return &TrailingGarbageError{data}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case reflect.Pointer:
|
||||||
|
if len(data) < SizePrefix {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
switch binary.NativeEndian.Uint32(data[SizeSPrefix:]) {
|
||||||
|
case SPA_TYPE_None:
|
||||||
|
v.SetZero()
|
||||||
|
return nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
v.Set(reflect.New(v.Type().Elem()))
|
||||||
|
return unmarshalValue(data, v.Elem(), wireSizeP)
|
||||||
|
}
|
||||||
|
|
||||||
|
case reflect.String:
|
||||||
|
*wireSizeP = 0
|
||||||
|
if err := unmarshalCheckTypeBounds(&data, SPA_TYPE_String, wireSizeP); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// string size, one extra NUL byte
|
||||||
|
size := int(*wireSizeP)
|
||||||
|
if len(data) < size {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
|
||||||
|
// the serialised strings still include NUL termination
|
||||||
|
if data[size-1] != 0 {
|
||||||
|
return StringTerminationError{data[size-1]}
|
||||||
|
}
|
||||||
|
|
||||||
|
v.SetString(string(data[:size-1]))
|
||||||
|
return nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return &UnsupportedTypeError{v.Type()}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// An InconsistentSizeError describes an inconsistent size prefix encountered
|
||||||
|
// in data passed to [Unmarshal].
|
||||||
|
type InconsistentSizeError struct{ Prefix, Expect Word }
|
||||||
|
|
||||||
|
func (e *InconsistentSizeError) Error() string {
|
||||||
|
return "unexpected size prefix: " + strconv.Itoa(int(e.Prefix)) + ", want " + strconv.Itoa(int(e.Expect))
|
||||||
|
}
|
||||||
|
|
||||||
|
// An UnexpectedTypeError describes an unexpected type encountered
|
||||||
|
// in data passed to [Unmarshal].
|
||||||
|
type UnexpectedTypeError struct{ Type, Expect Word }
|
||||||
|
|
||||||
|
func (u *UnexpectedTypeError) Error() string {
|
||||||
|
return "unexpected type: " + strconv.Itoa(int(u.Type)) + ", want " + strconv.Itoa(int(u.Expect))
|
||||||
|
}
|
||||||
|
|
||||||
|
// unmarshalCheckTypeBounds performs bounds checks on data and validates the type and size prefixes.
|
||||||
|
// An expected size of zero skips further bounds checks.
|
||||||
|
func unmarshalCheckTypeBounds(data *[]byte, t Word, sizeP *Word) error {
|
||||||
|
if len(*data) < SizePrefix {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
|
||||||
|
wantSize := *sizeP
|
||||||
|
gotSize := binary.NativeEndian.Uint32(*data)
|
||||||
|
*sizeP = gotSize
|
||||||
|
|
||||||
|
if wantSize != 0 && gotSize != wantSize {
|
||||||
|
return &InconsistentSizeError{gotSize, wantSize}
|
||||||
|
}
|
||||||
|
if len(*data)-SizePrefix < int(gotSize) {
|
||||||
|
return io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
|
||||||
|
gotType := binary.NativeEndian.Uint32((*data)[SizeSPrefix:])
|
||||||
|
if gotType != t {
|
||||||
|
return &UnexpectedTypeError{gotType, t}
|
||||||
|
}
|
||||||
|
|
||||||
|
*data = (*data)[SizePrefix : gotSize+SizePrefix]
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Footer contains additional messages, not directed to
|
||||||
|
// the destination object defined by the Id field.
|
||||||
|
type Footer[T any] struct {
|
||||||
|
// The footer opcode.
|
||||||
|
Opcode Id `json:"opcode"`
|
||||||
|
// The footer payload struct.
|
||||||
|
Payload T `json:"payload"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
|
||||||
|
func (f *Footer[T]) MarshalBinary() ([]byte, error) { return Marshal(f) }
|
||||||
|
|
||||||
|
// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
|
||||||
|
func (f *Footer[T]) UnmarshalBinary(data []byte) error { return Unmarshal(data, f) }
|
||||||
|
|
||||||
|
// SPADictItem represents spa_dict_item.
|
||||||
|
type SPADictItem struct{ Key, Value string }
|
||||||
|
|
||||||
|
// SPADict represents spa_dict.
|
||||||
|
type SPADict []SPADictItem
|
||||||
|
|
||||||
|
// Size satisfies [KnownSize] with a value computed at runtime.
|
||||||
|
func (d *SPADict) Size() Word {
|
||||||
|
if d == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// struct prefix, NItems value
|
||||||
|
size := SizePrefix + int(Size(SizeInt))
|
||||||
|
for i := range *d {
|
||||||
|
size += SizeString[int]((*d)[i].Key)
|
||||||
|
size += SizeString[int]((*d)[i].Value)
|
||||||
|
}
|
||||||
|
return Word(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalPOD satisfies [PODMarshaler] as [SPADict] violates the POD type system.
|
||||||
|
func (d *SPADict) MarshalPOD(data []byte) ([]byte, error) {
|
||||||
|
return appendInner(data, func(dataPrefix []byte) (data []byte, err error) {
|
||||||
|
data = binary.NativeEndian.AppendUint32(dataPrefix, SPA_TYPE_Struct)
|
||||||
|
if data, err = MarshalAppend(data, Int(len(*d))); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := range *d {
|
||||||
|
if data, err = MarshalAppend(data, (*d)[i].Key); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if data, err = MarshalAppend(data, (*d)[i].Value); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalPOD satisfies [PODUnmarshaler] as [SPADict] violates the POD type system.
|
||||||
|
func (d *SPADict) UnmarshalPOD(data []byte) (Word, error) {
|
||||||
|
var wireSize Word
|
||||||
|
if err := unmarshalCheckTypeBounds(&data, SPA_TYPE_Struct, &wireSize); err != nil {
|
||||||
|
return wireSize, err
|
||||||
|
}
|
||||||
|
// bounds check completed in successful call to unmarshalCheckTypeBounds
|
||||||
|
data = data[:wireSize]
|
||||||
|
|
||||||
|
var count Int
|
||||||
|
if size, err := UnmarshalNext(data, &count); err != nil {
|
||||||
|
return wireSize, err
|
||||||
|
} else {
|
||||||
|
// bounds check completed in successful call to Unmarshal
|
||||||
|
data = data[size:]
|
||||||
|
}
|
||||||
|
|
||||||
|
*d = make([]SPADictItem, count)
|
||||||
|
for i := range *d {
|
||||||
|
if size, err := UnmarshalNext(data, &(*d)[i].Key); err != nil {
|
||||||
|
return wireSize, err
|
||||||
|
} else {
|
||||||
|
// bounds check completed in successful call to Unmarshal
|
||||||
|
data = data[size:]
|
||||||
|
}
|
||||||
|
if size, err := UnmarshalNext(data, &(*d)[i].Value); err != nil {
|
||||||
|
return wireSize, err
|
||||||
|
} else {
|
||||||
|
// bounds check completed in successful call to Unmarshal
|
||||||
|
data = data[size:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) != 0 {
|
||||||
|
return wireSize, &TrailingGarbageError{data}
|
||||||
|
}
|
||||||
|
return wireSize, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pointers */
|
||||||
|
const (
|
||||||
|
SPA_TYPE_POINTER_START = 0x10000 + iota
|
||||||
|
SPA_TYPE_POINTER_Buffer
|
||||||
|
SPA_TYPE_POINTER_Meta
|
||||||
|
SPA_TYPE_POINTER_Dict
|
||||||
|
|
||||||
|
_SPA_TYPE_POINTER_LAST // not part of ABI
|
||||||
|
)
|
||||||
|
|
||||||
|
/* Events */
|
||||||
|
const (
|
||||||
|
SPA_TYPE_EVENT_START = 0x20000 + iota
|
||||||
|
SPA_TYPE_EVENT_Device
|
||||||
|
SPA_TYPE_EVENT_Node
|
||||||
|
|
||||||
|
_SPA_TYPE_EVENT_LAST // not part of ABI
|
||||||
|
)
|
||||||
|
|
||||||
|
/* Commands */
|
||||||
|
const (
|
||||||
|
SPA_TYPE_COMMAND_START = 0x30000 + iota
|
||||||
|
SPA_TYPE_COMMAND_Device
|
||||||
|
SPA_TYPE_COMMAND_Node
|
||||||
|
|
||||||
|
_SPA_TYPE_COMMAND_LAST // not part of ABI
|
||||||
|
)
|
||||||
|
|
||||||
|
/* Objects */
|
||||||
|
const (
|
||||||
|
SPA_TYPE_OBJECT_START = 0x40000 + iota
|
||||||
|
SPA_TYPE_OBJECT_PropInfo
|
||||||
|
SPA_TYPE_OBJECT_Props
|
||||||
|
SPA_TYPE_OBJECT_Format
|
||||||
|
SPA_TYPE_OBJECT_ParamBuffers
|
||||||
|
SPA_TYPE_OBJECT_ParamMeta
|
||||||
|
SPA_TYPE_OBJECT_ParamIO
|
||||||
|
SPA_TYPE_OBJECT_ParamProfile
|
||||||
|
SPA_TYPE_OBJECT_ParamPortConfig
|
||||||
|
SPA_TYPE_OBJECT_ParamRoute
|
||||||
|
SPA_TYPE_OBJECT_Profiler
|
||||||
|
SPA_TYPE_OBJECT_ParamLatency
|
||||||
|
SPA_TYPE_OBJECT_ParamProcessLatency
|
||||||
|
SPA_TYPE_OBJECT_ParamTag
|
||||||
|
_SPA_TYPE_OBJECT_LAST // not part of ABI
|
||||||
|
)
|
||||||
|
|
||||||
|
/* vendor extensions */
|
||||||
|
const (
|
||||||
|
SPA_TYPE_VENDOR_PipeWire = 0x02000000
|
||||||
|
|
||||||
|
SPA_TYPE_VENDOR_Other = 0x7f000000
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SPA_TYPE_INFO_BASE = "Spa:"
|
||||||
|
|
||||||
|
SPA_TYPE_INFO_Flags = SPA_TYPE_INFO_BASE + "Flags"
|
||||||
|
SPA_TYPE_INFO_FLAGS_BASE = SPA_TYPE_INFO_Flags + ":"
|
||||||
|
|
||||||
|
SPA_TYPE_INFO_Enum = SPA_TYPE_INFO_BASE + "Enum"
|
||||||
|
SPA_TYPE_INFO_ENUM_BASE = SPA_TYPE_INFO_Enum + ":"
|
||||||
|
|
||||||
|
SPA_TYPE_INFO_Pod = SPA_TYPE_INFO_BASE + "Pod"
|
||||||
|
SPA_TYPE_INFO_POD_BASE = SPA_TYPE_INFO_Pod + ":"
|
||||||
|
|
||||||
|
SPA_TYPE_INFO_Struct = SPA_TYPE_INFO_POD_BASE + "Struct"
|
||||||
|
SPA_TYPE_INFO_STRUCT_BASE = SPA_TYPE_INFO_Struct + ":"
|
||||||
|
|
||||||
|
SPA_TYPE_INFO_Object = SPA_TYPE_INFO_POD_BASE + "Object"
|
||||||
|
SPA_TYPE_INFO_OBJECT_BASE = SPA_TYPE_INFO_Object + ":"
|
||||||
|
|
||||||
|
SPA_TYPE_INFO_Pointer = SPA_TYPE_INFO_BASE + "Pointer"
|
||||||
|
SPA_TYPE_INFO_POINTER_BASE = SPA_TYPE_INFO_Pointer + ":"
|
||||||
|
|
||||||
|
SPA_TYPE_INFO_Interface = SPA_TYPE_INFO_POINTER_BASE + "Interface"
|
||||||
|
SPA_TYPE_INFO_INTERFACE_BASE = SPA_TYPE_INFO_Interface + ":"
|
||||||
|
|
||||||
|
SPA_TYPE_INFO_Event = SPA_TYPE_INFO_OBJECT_BASE + "Event"
|
||||||
|
SPA_TYPE_INFO_EVENT_BASE = SPA_TYPE_INFO_Event + ":"
|
||||||
|
|
||||||
|
SPA_TYPE_INFO_Command = SPA_TYPE_INFO_OBJECT_BASE + "Command"
|
||||||
|
SPA_TYPE_INFO_COMMAND_BASE = SPA_TYPE_INFO_Command + ":"
|
||||||
|
)
|
||||||
76
internal/pipewire/pod_test.go
Normal file
76
internal/pipewire/pod_test.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package pipewire_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding"
|
||||||
|
"encoding/json"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hakurei.app/internal/pipewire"
|
||||||
|
)
|
||||||
|
|
||||||
|
type encodingTestCases[V any, S interface {
|
||||||
|
encoding.BinaryMarshaler
|
||||||
|
encoding.BinaryUnmarshaler
|
||||||
|
|
||||||
|
*V
|
||||||
|
}] []struct {
|
||||||
|
// Uninterpreted name of subtest.
|
||||||
|
name string
|
||||||
|
// Encoded data.
|
||||||
|
wantData []byte
|
||||||
|
// Value corresponding to wantData.
|
||||||
|
value V
|
||||||
|
// Expected decoding error. Skips encoding check if non-nil.
|
||||||
|
wantErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
// run runs all test cases as subtests of [testing.T].
|
||||||
|
func (testCases encodingTestCases[V, S]) run(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("decode", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var value V
|
||||||
|
if err := S(&value).UnmarshalBinary(tc.wantData); err != nil {
|
||||||
|
t.Fatalf("UnmarshalBinary: error = %v", err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(&value, &tc.value) {
|
||||||
|
t.Fatalf("UnmarshalBinary:\n%s\nwant\n%s", mustMarshalJSON(value), mustMarshalJSON(tc.value))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("encode", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if gotData, err := S(&tc.value).MarshalBinary(); err != nil {
|
||||||
|
t.Fatalf("MarshalBinary: error = %v", err)
|
||||||
|
} else if string(gotData) != string(tc.wantData) {
|
||||||
|
t.Fatalf("MarshalBinary: %#v, want %#v", gotData, tc.wantData)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if s, ok := any(&tc.value).(pipewire.KnownSize); ok {
|
||||||
|
t.Run("size", func(t *testing.T) {
|
||||||
|
if got := int(s.Size()); got != len(tc.wantData) {
|
||||||
|
t.Errorf("Size: %d, want %d", got, len(tc.wantData))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mustMarshalJSON calls [json.Marshal] and returns the result.
|
||||||
|
func mustMarshalJSON(v any) string {
|
||||||
|
if data, err := json.Marshal(v); err != nil {
|
||||||
|
panic(err)
|
||||||
|
} else {
|
||||||
|
return string(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
internal/pipewire/testdata/pw-container-00-sendmsg
vendored
Normal file
BIN
internal/pipewire/testdata/pw-container-00-sendmsg
vendored
Normal file
Binary file not shown.
BIN
internal/pipewire/testdata/pw-container-01-recvmsg
vendored
Normal file
BIN
internal/pipewire/testdata/pw-container-01-recvmsg
vendored
Normal file
Binary file not shown.
BIN
internal/pipewire/testdata/pw-container-03-sendmsg
vendored
Normal file
BIN
internal/pipewire/testdata/pw-container-03-sendmsg
vendored
Normal file
Binary file not shown.
BIN
internal/pipewire/testdata/pw-container-04-recvmsg
vendored
Normal file
BIN
internal/pipewire/testdata/pw-container-04-recvmsg
vendored
Normal file
Binary file not shown.
BIN
internal/pipewire/testdata/pw-container-06-sendmsg
vendored
Normal file
BIN
internal/pipewire/testdata/pw-container-06-sendmsg
vendored
Normal file
Binary file not shown.
BIN
internal/pipewire/testdata/pw-container-07-recvmsg
vendored
Normal file
BIN
internal/pipewire/testdata/pw-container-07-recvmsg
vendored
Normal file
Binary file not shown.
@ -3,7 +3,6 @@ package system
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
|
|
||||||
"hakurei.app/container/check"
|
"hakurei.app/container/check"
|
||||||
"hakurei.app/hst"
|
"hakurei.app/hst"
|
||||||
@ -63,9 +62,6 @@ func (w *waylandOp) revert(sys *I, _ *Criteria) error {
|
|||||||
if w.ctx != nil {
|
if w.ctx != nil {
|
||||||
hangupErr = w.ctx.Close()
|
hangupErr = w.ctx.Close()
|
||||||
}
|
}
|
||||||
if err := sys.remove(w.dst.String()); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
||||||
removeErr = err
|
|
||||||
}
|
|
||||||
|
|
||||||
return newOpError("wayland", errors.Join(hangupErr, removeErr), true)
|
return newOpError("wayland", errors.Join(hangupErr, removeErr), true)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,21 +36,6 @@ func TestWaylandOp(t *testing.T) {
|
|||||||
call("aclUpdate", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, stub.UniqueError(2)),
|
call("aclUpdate", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, stub.UniqueError(2)),
|
||||||
}, &OpError{Op: "wayland", Err: errors.Join(stub.UniqueError(2), os.ErrInvalid)}, nil, nil},
|
}, &OpError{Op: "wayland", Err: errors.Join(stub.UniqueError(2), os.ErrInvalid)}, nil, nil},
|
||||||
|
|
||||||
{"remove", 0xbeef, 0xff, &waylandOp{nil,
|
|
||||||
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"),
|
|
||||||
m("/run/user/1971/wayland-0"),
|
|
||||||
"org.chromium.Chromium",
|
|
||||||
"ebf083d1b175911782d413369b64ce7c",
|
|
||||||
}, []stub.Call{
|
|
||||||
call("waylandNew", stub.ExpectArgs{m("/run/user/1971/wayland-0"), m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"), "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c"}, nil, nil),
|
|
||||||
call("verbosef", stub.ExpectArgs{"wayland pathname socket on %q via %q", []any{m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"), m("/run/user/1971/wayland-0")}}, nil, nil),
|
|
||||||
call("chmod", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", os.FileMode(0)}, nil, nil),
|
|
||||||
call("aclUpdate", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil),
|
|
||||||
}, nil, []stub.Call{
|
|
||||||
call("verbosef", stub.ExpectArgs{"hanging up wayland socket on %q", []any{m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland")}}, nil, nil),
|
|
||||||
call("remove", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"}, nil, stub.UniqueError(1)),
|
|
||||||
}, &OpError{Op: "wayland", Err: errors.Join(stub.UniqueError(1)), Revert: true}},
|
|
||||||
|
|
||||||
{"success", 0xbeef, 0xff, &waylandOp{nil,
|
{"success", 0xbeef, 0xff, &waylandOp{nil,
|
||||||
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"),
|
m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"),
|
||||||
m("/run/user/1971/wayland-0"),
|
m("/run/user/1971/wayland-0"),
|
||||||
@ -63,7 +48,6 @@ func TestWaylandOp(t *testing.T) {
|
|||||||
call("aclUpdate", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil),
|
call("aclUpdate", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil),
|
||||||
}, nil, []stub.Call{
|
}, nil, []stub.Call{
|
||||||
call("verbosef", stub.ExpectArgs{"hanging up wayland socket on %q", []any{m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland")}}, nil, nil),
|
call("verbosef", stub.ExpectArgs{"hanging up wayland socket on %q", []any{m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland")}}, nil, nil),
|
||||||
call("remove", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"}, nil, nil),
|
|
||||||
}, nil},
|
}, nil},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -12,18 +12,32 @@ import (
|
|||||||
type SecurityContext struct {
|
type SecurityContext struct {
|
||||||
// Pipe with its write end passed to security-context-v1.
|
// Pipe with its write end passed to security-context-v1.
|
||||||
closeFds [2]int
|
closeFds [2]int
|
||||||
|
// Absolute pathname the socket was bound to.
|
||||||
|
bindPath *check.Absolute
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close releases any resources held by [SecurityContext], and prevents further
|
// Close releases any resources held by [SecurityContext], and prevents further
|
||||||
// connections to its associated socket.
|
// connections to its associated socket.
|
||||||
|
//
|
||||||
|
// A non-nil error has the concrete type [Error].
|
||||||
func (sc *SecurityContext) Close() error {
|
func (sc *SecurityContext) Close() error {
|
||||||
if sc == nil {
|
if sc == nil || sc.bindPath == nil {
|
||||||
return os.ErrInvalid
|
return os.ErrInvalid
|
||||||
}
|
}
|
||||||
return errors.Join(
|
|
||||||
|
e := Error{RCleanup, sc.bindPath.String(), "", errors.Join(
|
||||||
syscall.Close(sc.closeFds[1]),
|
syscall.Close(sc.closeFds[1]),
|
||||||
syscall.Close(sc.closeFds[0]),
|
syscall.Close(sc.closeFds[0]),
|
||||||
)
|
// there is still technically a TOCTOU here but this is internal
|
||||||
|
// and has access to the privileged wayland socket, so it only
|
||||||
|
// receives trusted input (e.g. from cmd/hakurei) anyway
|
||||||
|
os.Remove(sc.bindPath.String()),
|
||||||
|
)}
|
||||||
|
if e.Errno != nil {
|
||||||
|
return &e
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new security context on the Wayland display at displayPath
|
// New creates a new security context on the Wayland display at displayPath
|
||||||
@ -51,14 +65,19 @@ func New(displayPath, bindPath *check.Absolute, appID, instanceID string) (*Secu
|
|||||||
} else {
|
} else {
|
||||||
closeFds, bindErr := securityContextBindPipe(fd, bindPath, appID, instanceID)
|
closeFds, bindErr := securityContextBindPipe(fd, bindPath, appID, instanceID)
|
||||||
if bindErr != nil {
|
if bindErr != nil {
|
||||||
// do not leak the pipe and socket
|
// securityContextBindPipe does not try to remove the socket during cleanup
|
||||||
|
closeErr := os.Remove(bindPath.String())
|
||||||
|
if closeErr != nil && errors.Is(closeErr, os.ErrNotExist) {
|
||||||
|
closeErr = nil
|
||||||
|
}
|
||||||
|
|
||||||
err = errors.Join(bindErr, // already wrapped
|
err = errors.Join(bindErr, // already wrapped
|
||||||
syscall.Close(closeFds[1]),
|
closeErr,
|
||||||
syscall.Close(closeFds[0]),
|
// do not leak the socket
|
||||||
syscall.Close(fd),
|
syscall.Close(fd),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return &SecurityContext{closeFds}, err
|
return &SecurityContext{closeFds, bindPath}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package wayland
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"reflect"
|
"reflect"
|
||||||
"syscall"
|
"syscall"
|
||||||
"testing"
|
"testing"
|
||||||
@ -11,13 +12,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestSecurityContextClose(t *testing.T) {
|
func TestSecurityContextClose(t *testing.T) {
|
||||||
t.Parallel()
|
// do not parallel: fd test not thread safe
|
||||||
|
|
||||||
if err := (*SecurityContext)(nil).Close(); !reflect.DeepEqual(err, os.ErrInvalid) {
|
if err := (*SecurityContext)(nil).Close(); !reflect.DeepEqual(err, os.ErrInvalid) {
|
||||||
t.Fatalf("Close: error = %v", err)
|
t.Fatalf("Close: error = %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var ctx SecurityContext
|
var ctx SecurityContext
|
||||||
|
if f, err := os.Create(path.Join(t.TempDir(), "remove")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
ctx.bindPath = check.MustAbs(f.Name())
|
||||||
|
}
|
||||||
if err := syscall.Pipe2(ctx.closeFds[0:], syscall.O_CLOEXEC); err != nil {
|
if err := syscall.Pipe2(ctx.closeFds[0:], syscall.O_CLOEXEC); err != nil {
|
||||||
t.Fatalf("Pipe: error = %v", err)
|
t.Fatalf("Pipe: error = %v", err)
|
||||||
}
|
}
|
||||||
@ -25,9 +31,15 @@ func TestSecurityContextClose(t *testing.T) {
|
|||||||
|
|
||||||
if err := ctx.Close(); err != nil {
|
if err := ctx.Close(); err != nil {
|
||||||
t.Fatalf("Close: error = %v", err)
|
t.Fatalf("Close: error = %v", err)
|
||||||
|
} else if _, err = os.Stat(ctx.bindPath.String()); err == nil || !errors.Is(err, os.ErrNotExist) {
|
||||||
|
t.Fatalf("Did not remove %q", ctx.bindPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
wantErr := errors.Join(syscall.EBADF, syscall.EBADF)
|
wantErr := &Error{Cause: RCleanup, Path: ctx.bindPath.String(), Errno: errors.Join(syscall.EBADF, syscall.EBADF, &os.PathError{
|
||||||
|
Op: "remove",
|
||||||
|
Path: ctx.bindPath.String(),
|
||||||
|
Err: syscall.ENOENT,
|
||||||
|
})}
|
||||||
if err := ctx.Close(); !reflect.DeepEqual(err, wantErr) {
|
if err := ctx.Close(); !reflect.DeepEqual(err, wantErr) {
|
||||||
t.Fatalf("Close: error = %#v, want %#v", err, wantErr)
|
t.Fatalf("Close: error = %#v, want %#v", err, wantErr)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,8 @@ typedef enum {
|
|||||||
HAKUREI_WAYLAND_HOST_SOCKET,
|
HAKUREI_WAYLAND_HOST_SOCKET,
|
||||||
/* connect for host server failed, implemented in conn.go */
|
/* connect for host server failed, implemented in conn.go */
|
||||||
HAKUREI_WAYLAND_HOST_CONNECT,
|
HAKUREI_WAYLAND_HOST_CONNECT,
|
||||||
|
/* cleanup failed, implemented in conn.go */
|
||||||
|
HAKUREI_WAYLAND_CLEANUP,
|
||||||
} hakurei_wayland_res;
|
} hakurei_wayland_res;
|
||||||
|
|
||||||
hakurei_wayland_res hakurei_security_context_bind(
|
hakurei_wayland_res hakurei_security_context_bind(
|
||||||
|
|||||||
@ -9,14 +9,20 @@ package wayland
|
|||||||
#cgo freebsd openbsd LDFLAGS: -lwayland-client
|
#cgo freebsd openbsd LDFLAGS: -lwayland-client
|
||||||
|
|
||||||
#include "wayland-client-helper.h"
|
#include "wayland-client-helper.h"
|
||||||
|
#include <wayland-version.h>
|
||||||
*/
|
*/
|
||||||
import "C"
|
import "C"
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// Version is the value of WAYLAND_VERSION.
|
||||||
|
Version = C.WAYLAND_VERSION
|
||||||
|
|
||||||
// Display contains the name of the server socket
|
// Display contains the name of the server socket
|
||||||
// (https://gitlab.freedesktop.org/wayland/wayland/-/blob/1.23.1/src/wayland-client.c#L1147)
|
// (https://gitlab.freedesktop.org/wayland/wayland/-/blob/1.23.1/src/wayland-client.c#L1147)
|
||||||
// which is concatenated with XDG_RUNTIME_DIR
|
// which is concatenated with XDG_RUNTIME_DIR
|
||||||
@ -79,6 +85,9 @@ const (
|
|||||||
RHostSocket Res = C.HAKUREI_WAYLAND_HOST_SOCKET
|
RHostSocket Res = C.HAKUREI_WAYLAND_HOST_SOCKET
|
||||||
// RHostConnect is returned if connect failed for host server. Returned by [New].
|
// RHostConnect is returned if connect failed for host server. Returned by [New].
|
||||||
RHostConnect Res = C.HAKUREI_WAYLAND_HOST_CONNECT
|
RHostConnect Res = C.HAKUREI_WAYLAND_HOST_CONNECT
|
||||||
|
|
||||||
|
// RCleanup is returned if cleanup fails. Returned by [SecurityContext.Close].
|
||||||
|
RCleanup Res = C.HAKUREI_WAYLAND_CLEANUP
|
||||||
)
|
)
|
||||||
|
|
||||||
func (e *Error) Unwrap() error { return e.Errno }
|
func (e *Error) Unwrap() error { return e.Errno }
|
||||||
@ -120,6 +129,19 @@ func (e *Error) Error() string {
|
|||||||
case RHostConnect:
|
case RHostConnect:
|
||||||
return e.withPrefix("cannot connect to " + e.Host)
|
return e.withPrefix("cannot connect to " + e.Host)
|
||||||
|
|
||||||
|
case RCleanup:
|
||||||
|
var pathError *os.PathError
|
||||||
|
if errors.As(e.Errno, &pathError) && pathError != nil {
|
||||||
|
return pathError.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
var errno syscall.Errno
|
||||||
|
if errors.As(e.Errno, &errno) && errno != 0 {
|
||||||
|
return "cannot close wayland close_fd pipe: " + errno.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.withPrefix("cannot hang up wayland security_context")
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return e.withPrefix("impossible outcome") /* not reached */
|
return e.withPrefix("impossible outcome") /* not reached */
|
||||||
}
|
}
|
||||||
|
|||||||
@ -58,15 +58,15 @@ func TestError(t *testing.T) {
|
|||||||
|
|
||||||
{"bind", Error{
|
{"bind", Error{
|
||||||
Cause: RBind,
|
Cause: RBind,
|
||||||
Path: "/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland",
|
Path: "/tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland",
|
||||||
Errno: stub.UniqueError(5),
|
Errno: stub.UniqueError(5),
|
||||||
}, "cannot bind /hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland: unique error 5 injected by the test suite"},
|
}, "cannot bind /tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland: unique error 5 injected by the test suite"},
|
||||||
|
|
||||||
{"listen", Error{
|
{"listen", Error{
|
||||||
Cause: RListen,
|
Cause: RListen,
|
||||||
Path: "/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland",
|
Path: "/tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland",
|
||||||
Errno: stub.UniqueError(6),
|
Errno: stub.UniqueError(6),
|
||||||
}, "cannot listen on /hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland: unique error 6 injected by the test suite"},
|
}, "cannot listen on /tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland: unique error 6 injected by the test suite"},
|
||||||
|
|
||||||
{"socket invalid", Error{
|
{"socket invalid", Error{
|
||||||
Cause: RSocket,
|
Cause: RSocket,
|
||||||
@ -92,6 +92,27 @@ func TestError(t *testing.T) {
|
|||||||
Errno: stub.UniqueError(8),
|
Errno: stub.UniqueError(8),
|
||||||
}, "cannot connect to /run/user/1971/wayland-1: unique error 8 injected by the test suite"},
|
}, "cannot connect to /run/user/1971/wayland-1: unique error 8 injected by the test suite"},
|
||||||
|
|
||||||
|
{"cleanup", Error{
|
||||||
|
Cause: RCleanup,
|
||||||
|
Path: "/tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland",
|
||||||
|
}, "cannot hang up wayland security_context"},
|
||||||
|
|
||||||
|
{"cleanup PathError", Error{
|
||||||
|
Cause: RCleanup,
|
||||||
|
Path: "/tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland",
|
||||||
|
Errno: errors.Join(syscall.EINVAL, &os.PathError{
|
||||||
|
Op: "remove",
|
||||||
|
Path: "/tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland",
|
||||||
|
Err: stub.UniqueError(9),
|
||||||
|
}),
|
||||||
|
}, "remove /tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland: unique error 9 injected by the test suite"},
|
||||||
|
|
||||||
|
{"cleanup errno", Error{
|
||||||
|
Cause: RCleanup,
|
||||||
|
Path: "/tmp/hakurei.0/18783d07791f2460dbbcffb76c24c9e6/wayland",
|
||||||
|
Errno: errors.Join(syscall.EINVAL),
|
||||||
|
}, "cannot close wayland close_fd pipe: invalid argument"},
|
||||||
|
|
||||||
{"invalid", Error{
|
{"invalid", Error{
|
||||||
Cause: 0xbad,
|
Cause: 0xbad,
|
||||||
}, "impossible outcome"},
|
}, "impossible outcome"},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user