From ea29e6bad114306b78820cb674863a9bf3041fa5 Mon Sep 17 00:00:00 2001 From: Ophestra Date: Tue, 27 Oct 2026 20:13:13 +0900 Subject: internal/pipewire: re-format for readability This package was the first somewhat large piece of code that was primarily written using the built-in display of my laptop. The experience had been deeply unpleasant due to the awkward 3:2 aspect ratio not working well with my setup, which almost always involved having the implementation and tests side-by-side. This experience is what provoked me to re-instate the 80-column limit that I had long resented and rejected. This package however predates the formal re-instating of this limit, and was never updated. Signed-off-by: Ophestra --- internal/pipewire/client.go | 17 +- internal/pipewire/core.go | 251 ++++++++++++++++----------- internal/pipewire/header.go | 10 +- internal/pipewire/pipewire.go | 324 +++++++++++++++++++++++------------ internal/pipewire/pod.go | 124 +++++++++----- internal/pipewire/securitycontext.go | 51 +++--- internal/pipewire/spa.go | 7 +- 7 files changed, 501 insertions(+), 283 deletions(-) diff --git a/internal/pipewire/client.go b/internal/pipewire/client.go index 96beeba3..e67cc22e 100644 --- a/internal/pipewire/client.go +++ b/internal/pipewire/client.go @@ -36,13 +36,16 @@ const ( ) // The ClientInfo event provides client information updates. -// This is emitted when binding to a client or when the client info is updated later. +// +// 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 of this object, valid when change_mask has + // PW_CLIENT_CHANGE_MASK_PROPS. Properties *SPADict `json:"props"` } @@ -87,8 +90,11 @@ func (c *ClientUpdateProperties) MarshalBinary() ([]byte, error) { return Marsha // UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal]. func (c *ClientUpdateProperties) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) } -// clientUpdateProperties queues a [ClientUpdateProperties] message for the PipeWire server. -// This method should not be called directly, the New function queues this message. +// clientUpdateProperties queues a [ClientUpdateProperties] message for the +// PipeWire server. +// +// This method should not be called directly, the New function queues this +// message. func (ctx *Context) clientUpdateProperties(props SPADict) error { return ctx.writeMessage( PW_ID_CLIENT, @@ -98,7 +104,8 @@ func (ctx *Context) clientUpdateProperties(props SPADict) error { // Client holds state of [PW_TYPE_INTERFACE_Client]. type Client struct { - // Additional information from the server, populated or updated during [Context.Roundtrip]. + // Additional information from the server, populated or updated during + // [Context.Roundtrip]. Info *ClientInfo `json:"info"` // Populated by [CoreBoundProps] events targeting [Client]. diff --git a/internal/pipewire/core.go b/internal/pipewire/core.go index 026f75bb..b0d82b44 100644 --- a/internal/pipewire/core.go +++ b/internal/pipewire/core.go @@ -84,8 +84,8 @@ const ( 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 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. @@ -99,12 +99,12 @@ func (fcg FooterCoreGeneration) Size() Word { Size(SizeLong) } -// The FooterClientGeneration indicates to the server what is the last -// registry generation number the client has processed. +// The FooterClientGeneration indicates to the server what is the last registry +// generation number the client has processed. // -// The client shall include this footer in the next message it sends, -// after it has processed an incoming message whose footer includes a -// registry generation update. +// The client shall include this footer in the next message it sends, after it +// has processed an incoming message whose footer includes a registry generation +// update. type FooterClientGeneration struct { ClientGeneration Long `json:"client_generation"` } @@ -115,8 +115,8 @@ func (fcg FooterClientGeneration) Size() Word { Size(SizeLong) } -// A CoreInfo event is emitted by the server upon connection -// with the more information about the server. +// 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"` @@ -132,7 +132,8 @@ type CoreInfo struct { 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. + // Optional key/value properties, valid when change_mask has + // PW_CORE_CHANGE_MASK_PROPS. Properties *SPADict `json:"props"` } @@ -184,12 +185,13 @@ 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 CorePing event is emitted by the server when it wants to check if a client is -// alive or ensure that it has processed the previous events. +// The CorePing event is emitted by the server when it wants to check if a +// client is alive or ensure that it has processed the previous events. type CorePing struct { // The object id to ping. ID Int `json:"id"` // Usually automatically generated. + // // The client should pass this in the Pong method reply. Sequence Int `json:"seq"` } @@ -212,13 +214,12 @@ func (c *CorePing) UnmarshalBinary(data []byte) error { return Unmarshal(data, c // The CoreError can be emitted by both the client and the server. // // When emitted by the server, the error event is sent out when a fatal -// (non-recoverable) error has occurred. The id argument is the proxy -// object where the error occurred, most often in response to a request -// to that object. The message is a brief description of the error, for -// (debugging) convenience. +// (non-recoverable) error has occurred. The id argument is the proxy object +// where the error occurred, most often in response to a request to that object. +// The message is a brief description of the error, for (debugging) convenience. // -// When emitted by the client, it indicates an error occurred in an -// object on the client. +// When emitted by the client, it indicates an error occurred in an object on +// the client. type CoreError struct { // The id of the resource (proxy if emitted by the client) that is in error. ID Int `json:"id"` @@ -270,9 +271,9 @@ func (c *CoreErrorEvent) Opcode() byte { return PW_CORE_EVENT_ERROR } // The CoreRemoveId event is used internally by the object ID management logic. // -// When a client deletes an object, the server will send this event to acknowledge -// that it has seen the delete request. When the client receives this event, it -// will know that it can safely reuse the object ID. +// When a client deletes an object, the server will send this event to +// acknowledge that it has seen the delete request. When the client receives +// this event, it will know that it can safely reuse the object ID. type CoreRemoveId struct { // A proxy id that was removed. ID Int `json:"id"` @@ -293,8 +294,8 @@ func (c *CoreRemoveId) MarshalBinary() ([]byte, error) { return Marshal(c) } // UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal]. func (c *CoreRemoveId) 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. +// 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"` @@ -334,8 +335,8 @@ type noAck struct{} // setBoundProps should never be called as this proxy should never be targeted by [CoreBoundProps]. func (noAck) setBoundProps(*CoreBoundProps) error { panic(ErrBadBoundProps) } -// ErrBadRemove is returned when a [CoreRemoveId] event targeting a proxy -// that should never be targeted is received and processed. +// ErrBadRemove is returned when a [CoreRemoveId] event targeting a proxy that +// should never be targeted is received and processed. var ErrBadRemove = errors.New("attempting to remove a proxy that should never be targeted") // noRemove is embedded by proxies that are never targeted by [CoreRemoveId]. @@ -344,11 +345,14 @@ type noRemove struct{} // remove should never be called as this proxy should never be targeted by [CoreRemoveId]. func (noRemove) remove() error { panic(ErrBadRemove) } -// ErrInvalidRemove is returned when a proxy is somehow removed twice. This is only reached for -// an implementation error as the proxy struct should no longer be reachable after the first call. +// ErrInvalidRemove is returned when a proxy is somehow removed twice. +// +// This is only reached for an implementation error as the proxy struct should +// no longer be reachable after the first call. var ErrInvalidRemove = errors.New("attempting to remove an already freed proxy") -// removable is embedded by proxies that can be targeted by [CoreRemoveId] and requires no cleanup. +// removable is embedded by proxies that can be targeted by [CoreRemoveId] and +// requires no cleanup. type removable bool // remove checks against removal of a freed proxy and marks the proxy as removed. @@ -360,21 +364,27 @@ func (s *removable) remove() error { return nil } -// ErrProxyDestroyed is returned when attempting to use a proxy method when the underlying -// proxy has already been targeted by a [CoreRemoveId] event. +// ErrProxyDestroyed is returned when attempting to use a proxy method when the +// underlying proxy has already been targeted by a [CoreRemoveId] event. var ErrProxyDestroyed = errors.New("underlying proxy has been removed") -// checkDestroy returns [ErrProxyDestroyed] if the current proxy has been destroyed. -// Must be called at the beginning of any exported method of a proxy embedding removable. +// checkDestroy returns [ErrProxyDestroyed] if the current proxy has been +// destroyed. +// +// Must be called at the beginning of any exported method of a proxy embedding +// removable. func (s *removable) checkDestroy() error { if *s { - // not fatal: the caller is allowed to recover from this and allocate a new proxy + // not fatal: the caller is allowed to recover from this and allocate a + // new proxy return ErrProxyDestroyed } return nil } -// mustCheckDestroy calls checkDestroy and panics if a non-nil error is returned. +// mustCheckDestroy calls checkDestroy and panics if a non-nil error is +// returned. +// // This is useful for non-exported methods as they should become unreachable. func (s *removable) mustCheckDestroy() { if err := s.checkDestroy(); err != nil { @@ -382,17 +392,19 @@ func (s *removable) mustCheckDestroy() { } } -// destructible is embedded by proxies that can be targeted by the [CoreRemoveId] event and the -// [CoreDestroy] method and requires no cleanup. destructible purposefully does not override -// removable.mustCheckDestroy because it is used by unexported methods called during event handling -// and are exempt from the destruction check. +// destructible is embedded by proxies that can be targeted by the +// [CoreRemoveId] event and the [CoreDestroy] method and requires no cleanup. +// destructible purposefully does not override removable.mustCheckDestroy +// because it is used by unexported methods called during event handling and are +// exempt from the destruction check. type destructible struct { destroyed bool removable } -// checkDestroy overrides removable.checkDestroy to also check the destroyed field. +// checkDestroy overrides removable.checkDestroy to also check the destroyed +// field. func (s *destructible) checkDestroy() error { if s.destroyed { return ErrProxyDestroyed @@ -403,7 +415,8 @@ func (s *destructible) checkDestroy() error { return nil } -// destroy calls removable.checkDestroy then queues a [CoreDestroy] event if it succeeds. +// destroy calls removable.checkDestroy then queues a [CoreDestroy] event if it +// succeeds. func (s *destructible) destroy(ctx *Context, id Int) error { if err := s.checkDestroy(); err != nil { return err @@ -417,8 +430,10 @@ func (s *destructible) destroy(ctx *Context, id Int) error { return ctx.GetCore().destroy(id) } -// An InconsistentIdError describes an inconsistent state where the server claims an impossible -// proxy or global id. This is only generated by the [CoreBoundProps] event. +// An InconsistentIdError describes an inconsistent state where the server claims +// an impossible proxy or global id. +// +// This is only generated by the [CoreBoundProps] event. type InconsistentIdError struct { // Whether the inconsistent id is the global resource id. Global bool @@ -434,8 +449,9 @@ func (e *InconsistentIdError) Error() string { name = "global" } - return name + " id " + strconv.Itoa(int(e.ID)) + " targeting " + e.Proxy.String() + - " inconsistent with " + strconv.Itoa(int(e.ServerID)) + " claimed by the server" + return name + " id " + strconv.Itoa(int(e.ID)) + " targeting " + + e.Proxy.String() + " inconsistent with " + + strconv.Itoa(int(e.ServerID)) + " claimed by the server" } // CoreHello is the first message sent by a client. @@ -460,7 +476,9 @@ func (c *CoreHello) MarshalBinary() ([]byte, error) { return Marshal(c) } func (c *CoreHello) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) } // hello queues a [CoreHello] message for the PipeWire server. -// This method should not be called directly, the [New] function queues this message. +// +// This method should not be called directly, the [New] function queues this +// message. func (core *Core) hello() error { return core.ctx.writeMessage( PW_ID_CORE, @@ -469,13 +487,14 @@ func (core *Core) hello() error { } const ( - // CoreSyncSequenceOffset is the offset to [Header.Sequence] to produce [CoreSync.Sequence]. + // 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. +// 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"` @@ -499,6 +518,7 @@ func (c *CoreSync) MarshalBinary() ([]byte, error) { return Marshal(c) } func (c *CoreSync) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) } // sync queues a [CoreSync] message for the PipeWire server. +// // This is not safe to use directly, callers should use Sync instead. func (core *Core) sync(id Int) error { return core.ctx.writeMessage( @@ -507,9 +527,11 @@ func (core *Core) sync(id Int) error { ) } -// ErrNotDone is returned if [Core.Sync] returns from its [Context.Roundtrip] without -// receiving a [CoreDone] event targeting the [CoreSync] event it delivered. -var ErrNotDone = errors.New("did not receive a Core::Done event targeting previously delivered Core::Sync") +// ErrNotDone is returned if [Core.Sync] returns from its [Context.Roundtrip] +// without receiving a [CoreDone] event targeting the [CoreSync] event it +// delivered. +var ErrNotDone = errors.New("did not receive a Core::Done event " + + "targeting previously delivered Core::Sync") const ( // syncTimeout is the maximum duration [Core.Sync] is allowed to take before @@ -517,7 +539,8 @@ const ( syncTimeout = RoundtripTimeout + 15*time.Second ) -// Sync queues a [CoreSync] message for the PipeWire server and initiates a Roundtrip. +// Sync queues a [CoreSync] message for the PipeWire server and initiates a +// Roundtrip. func (core *Core) Sync() error { core.done = false if err := core.sync(roundtripSyncID); err != nil { @@ -537,16 +560,31 @@ func (core *Core) Sync() error { if len(core.ctx.pendingIds) != 0 { core.ctx.closeReceivedFiles() - return &ProxyFatalError{Err: UnacknowledgedProxyError(slices.Collect(maps.Keys(core.ctx.pendingIds))), ProxyErrs: core.ctx.cloneAsProxyErrors()} + return &ProxyFatalError{ + Err: UnacknowledgedProxyError(slices.Collect( + maps.Keys( + core.ctx.pendingIds, + )), + ), + ProxyErrs: core.ctx.cloneAsProxyErrors(), + } } if len(core.ctx.pendingDestruction) != 0 { core.ctx.closeReceivedFiles() - return &ProxyFatalError{Err: UnacknowledgedProxyDestructionError(slices.Collect(maps.Keys(core.ctx.pendingDestruction))), ProxyErrs: core.ctx.cloneAsProxyErrors()} + return &ProxyFatalError{ + Err: UnacknowledgedProxyDestructionError(slices.Collect( + maps.Keys( + core.ctx.pendingDestruction, + )), + ), + ProxyErrs: core.ctx.cloneAsProxyErrors(), + } } return core.ctx.doSyncComplete() } -// The CorePong message is sent from the client to the server when the server emits the Ping event. +// The CorePong message is sent from the client to the server when the server +// emits the Ping event. type CorePong struct { // Copied from [CorePing.ID]. ID Int `json:"id"` @@ -569,18 +607,18 @@ func (c *CorePong) MarshalBinary() ([]byte, error) { return Marshal(c) } // UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal]. func (c *CorePong) 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. +// 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). +// 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. + // The id of the new proxy with the registry interface, ends up as + // [Header.ID] in future messages. NewID Int `json:"new_id"` } @@ -599,8 +637,8 @@ 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) } -// GetRegistry queues a [CoreGetRegistry] message for the PipeWire server -// and returns the address of the newly allocated [Registry]. +// GetRegistry queues a [CoreGetRegistry] message for the PipeWire server and +// returns the address of the newly allocated [Registry]. func (ctx *Context) GetRegistry() (*Registry, error) { registry := Registry{Objects: make(map[Int]RegistryGlobal), ctx: ctx} newId := ctx.newProxyId(®istry, false) @@ -611,18 +649,17 @@ func (ctx *Context) GetRegistry() (*Registry, error) { ) } -// CoreCreateObject is sent when the client requests to create a -// new object from a factory of a certain type. +// CoreCreateObject is sent when the client requests to create a new object from +// a factory of a certain type. // -// The client allocates a new_id for the proxy. The server will -// allocate a new resource with the same new_id and from then on, -// Methods and Events will be exchanged between the new object of -// the given type. +// The client allocates a new_id for the proxy. The server will allocate a new +// resource with the same new_id and from then on, Methods and Events will be +// exchanged between the new object of the given type. type CoreCreateObject struct { // The name of a server factory object to use. FactoryName String `json:"factory_name"` - // The type of the object to create, this is also the type of - // the interface of the new_id proxy. + // The type of the object to create, this is also the type of the interface + // of the new_id proxy. Type String `json:"type"` // Undocumented, assumed to be the local version of the proxy. Version Int `json:"version"` @@ -655,7 +692,9 @@ func (c *CoreCreateObject) MarshalBinary() ([]byte, error) { return Marshal(c) } func (c *CoreCreateObject) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) } // createObject queues a [CoreCreateObject] message for the PipeWire server. -// This is not safe to use directly, callers should use typed wrapper methods on [Registry] instead. +// +// This is not safe to use directly, callers should use typed wrapper methods on +// [Registry] instead. func (core *Core) createObject(factoryName, typeName String, version Int, props SPADict, newId Int) error { return core.ctx.writeMessage( PW_ID_CORE, @@ -685,8 +724,9 @@ func (c *CoreDestroy) MarshalBinary() ([]byte, error) { return Marshal(c) } func (c *CoreDestroy) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) } // destroy queues a [CoreDestroy] message for the PipeWire server. -// This is not safe to use directly, callers should use the exported method -// on the proxy implementation instead. +// +// This is not safe to use directly, callers should use the exported method on +// the proxy implementation instead. func (core *Core) destroy(id Int) error { return core.ctx.writeMessage( PW_ID_CORE, @@ -754,10 +794,9 @@ func (c *RegistryGlobalRemove) MarshalBinary() ([]byte, error) { return Marshal( // UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal]. func (c *RegistryGlobalRemove) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) } -// RegistryBind is sent when the client requests to bind to the -// global object with id and use the client proxy with new_id as -// the proxy. After this call, methods can be sent to the remote -// global object and events can be received. +// RegistryBind is sent when the client requests to bind to the global object +// with id and use the client proxy with new_id as the proxy. After this call, +// methods can be sent to the remote global object and events can be received. type RegistryBind struct { // The [RegistryGlobal.ID] to bind to. ID Int `json:"id"` @@ -790,8 +829,8 @@ func (c *RegistryBind) MarshalBinary() ([]byte, error) { return Marshal(c) } // UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal]. func (c *RegistryBind) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) } -// bind queues a [RegistryBind] message for the PipeWire server -// and returns the newly allocated proxy id. +// bind queues a [RegistryBind] message for the PipeWire server and returns the +// newly allocated proxy id. func (registry *Registry) bind(proxy eventProxy, id, version Int) (Int, error) { bind := RegistryBind{ ID: id, @@ -806,6 +845,7 @@ func (registry *Registry) bind(proxy eventProxy, id, version Int) (Int, error) { } // RegistryDestroy is sent to try to destroy the global object with id. +// // This might fail when the client does not have permission. type RegistryDestroy struct { // The global id to destroy. @@ -864,7 +904,8 @@ func (registry *Registry) Destroy(id Int) (err error) { } } -// An UnsupportedObjectTypeError is the name of a type not known by the server [Registry]. +// An UnsupportedObjectTypeError is the name of a type not known by the server +// [Registry]. type UnsupportedObjectTypeError string func (e UnsupportedObjectTypeError) Error() string { return "unsupported object type " + string(e) } @@ -872,7 +913,8 @@ func (e UnsupportedObjectTypeError) Message() string { return e.Error() } // Core holds state of [PW_TYPE_INTERFACE_Core]. type Core struct { - // Additional information from the server, populated or updated during [Context.Roundtrip]. + // Additional information from the server, populated or updated during + // [Context.Roundtrip]. Info *CoreInfo `json:"info"` // Whether a [CoreDone] event was received during Sync. @@ -887,7 +929,8 @@ type Core struct { // ErrUnexpectedDone is a [CoreDone] event with unexpected values. var ErrUnexpectedDone = errors.New("multiple Core::Done events targeting Core::Sync") -// An UnknownProxyIdError describes an event targeting a proxy id that was never allocated. +// An UnknownProxyIdError describes an event targeting a proxy id that was never +// allocated. type UnknownProxyIdError[E any] struct { // Offending id decoded from Data. Id Int @@ -899,7 +942,8 @@ func (e *UnknownProxyIdError[E]) Error() string { return "unknown proxy id " + strconv.Itoa(int(e.Id)) } -// An InvalidPingError is a [CorePing] event targeting a proxy id that was never allocated. +// An InvalidPingError is a [CorePing] event targeting a proxy id that was never +// allocated. type InvalidPingError CorePing func (e *InvalidPingError) Error() string { @@ -917,7 +961,8 @@ func (core *Core) consume(opcode byte, files []int, unmarshal func(v any)) error case PW_CORE_EVENT_DONE: var done CoreDone unmarshal(&done) - if done.ID == roundtripSyncID && done.Sequence == CoreSyncSequenceOffset+core.ctx.currentSeq() { + if done.ID == roundtripSyncID && + done.Sequence == CoreSyncSequenceOffset+core.ctx.currentSeq() { if core.done { return ErrUnexpectedDone } @@ -956,7 +1001,10 @@ func (core *Core) consume(opcode byte, files []int, unmarshal func(v any)) error unmarshal(&coreRemoveId) if proxy, ok := core.ctx.proxy[coreRemoveId.ID]; !ok { // this should never happen so is non-recoverable if it does - panic(&UnknownProxyIdError[*CoreRemoveId]{Id: coreRemoveId.ID, Event: &coreRemoveId}) + panic(&UnknownProxyIdError[*CoreRemoveId]{ + Id: coreRemoveId.ID, + Event: &coreRemoveId}, + ) } else { delete(core.ctx.proxy, coreRemoveId.ID) // not always populated so this is not checked @@ -971,7 +1019,10 @@ func (core *Core) consume(opcode byte, files []int, unmarshal func(v any)) error delete(core.ctx.pendingIds, boundProps.ID) proxy, ok := core.ctx.proxy[boundProps.ID] if !ok { - return &UnknownProxyIdError[*CoreBoundProps]{Id: boundProps.ID, Event: &boundProps} + return &UnknownProxyIdError[*CoreBoundProps]{ + Id: boundProps.ID, + Event: &boundProps, + } } return proxy.setBoundProps(&boundProps) @@ -989,8 +1040,8 @@ type Registry struct { // Global objects received via the [RegistryGlobal] event. // - // This requires more processing before it can be used, but is not implemented - // as it is not used by Hakurei. + // This requires more processing before it can be used, but is not + // implemented as it is not used by Hakurei. Objects map[Int]RegistryGlobal `json:"objects"` ctx *Context @@ -999,7 +1050,8 @@ type Registry struct { noRemove } -// A GlobalIDCollisionError describes a [RegistryGlobal] event stepping on a previous instance of itself. +// A GlobalIDCollisionError describes a [RegistryGlobal] event stepping on a +// previous instance of itself. type GlobalIDCollisionError struct { // The colliding id. ID Int @@ -1012,8 +1064,9 @@ func (e *GlobalIDCollisionError) Error() string { " stepping on previous id " + strconv.Itoa(int(e.ID)) + " for " + e.Previous.Type } -// An UnknownGlobalIDRemoveError describes a [RegistryGlobalRemove] event announcing the removal of -// a global id that is not yet known to [Registry] or was already deleted. +// An UnknownGlobalIDRemoveError describes a [RegistryGlobalRemove] event +// announcing the removal of a global id that is not yet known to [Registry] or +// was already deleted. type UnknownGlobalIDRemoveError Int func (e UnknownGlobalIDRemoveError) Error() string { @@ -1028,7 +1081,11 @@ func (registry *Registry) consume(opcode byte, files []int, unmarshal func(v any unmarshal(&global) if object, ok := registry.Objects[global.ID]; ok { // this should never happen so is non-recoverable if it does - panic(&GlobalIDCollisionError{global.ID, &object, &global}) + panic(&GlobalIDCollisionError{ + global.ID, + &object, + &global, + }) } registry.Objects[global.ID] = global return nil @@ -1036,8 +1093,8 @@ func (registry *Registry) consume(opcode byte, files []int, unmarshal func(v any case PW_REGISTRY_EVENT_GLOBAL_REMOVE: var globalRemove RegistryGlobalRemove unmarshal(&globalRemove) - // server emits PW_CORE_EVENT_REMOVE_ID events targeting - // affected proxies so they do not need to be handled here + // server emits PW_CORE_EVENT_REMOVE_ID events targeting affected + // proxies so they do not need to be handled here l := len(registry.Objects) delete(registry.Objects, globalRemove.ID) if len(registry.Objects) != l-1 { diff --git a/internal/pipewire/header.go b/internal/pipewire/header.go index 3aecfb7d..a627db24 100644 --- a/internal/pipewire/header.go +++ b/internal/pipewire/header.go @@ -8,14 +8,17 @@ import ( 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 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 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 indicates that the header slice does not have length + // [SizeHeader]. ErrBadHeader = errors.New("incorrect header size") ) @@ -26,6 +29,7 @@ type Header struct { // 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. diff --git a/internal/pipewire/pipewire.go b/internal/pipewire/pipewire.go index 3b909336..9357c565 100644 --- a/internal/pipewire/pipewire.go +++ b/internal/pipewire/pipewire.go @@ -1,17 +1,25 @@ -// Package pipewire provides a partial implementation of the PipeWire protocol native. +// Package pipewire provides a partial implementation of the PipeWire protocol +// native and the SecurityContext extension. // -// 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. +// 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. +// 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. +// +// This package does not use the network poller. It directly utilises epoll +// in a way identical to the upstream PipeWire client implementation, validated +// via syscall tracing. This is not necessary, but aims to avoid potential +// upstream PipeWire bugs that happen to not affect their own implementation. package pipewire import ( @@ -46,9 +54,12 @@ type Conn interface { Close() error } -// The kernel constant SCM_MAX_FD defines a limit on the number of file descriptors in the array. -// Attempting to send an array larger than this limit causes sendmsg(2) to fail with the error -// EINVAL. SCM_MAX_FD has the value 253 (or 255 before Linux 2.6.38). +// The kernel constant SCM_MAX_FD defines a limit on the number of file +// descriptors in the array. +// +// Attempting to send an array larger than this limit causes sendmsg(2) to fail +// with the error EINVAL. SCM_MAX_FD has the value 253 (or 255 before Linux +// 2.6.38). const _SCM_MAX_FD = 253 // A Context holds state of a connection to PipeWire. @@ -76,7 +87,8 @@ type Context struct { // Proxy for built-in client events. client Client - // Current server-side [Header.Sequence] value, incremented on every event processed. + // Current server-side [Header.Sequence] value, incremented on every event + // processed. remoteSequence Int // Files from the server. This is discarded on every Roundtrip so eventProxy // implementations must make sure to close them to avoid leaking fds. @@ -84,8 +96,9 @@ type Context struct { // These are not automatically set up as [os.File] because it is impossible // to undo the effects of os.NewFile, which can be inconvenient for some uses. receivedFiles []int - // Non-protocol errors encountered during event handling of the current Roundtrip; - // errors that prevent event processing from continuing must be panicked. + // Non-protocol errors encountered during event handling of the current + // Roundtrip; errors that prevent event processing from continuing must be + // panicked. proxyErrors ProxyConsumeError // Pending footer value for the next outgoing message. // Newer footers appear to simply replace the existing one. @@ -95,8 +108,9 @@ type Context struct { deferredPendingFooter KnownSize // Server side registry generation number. generation Long - // Deferred operations ran after a [Core.Sync] completes or Close is called. Errors - //are reported as part of [ProxyConsumeError] and is not considered fatal unless panicked. + // Deferred operations ran after a [Core.Sync] completes or Close is called. + // Errors are reported as part of [ProxyConsumeError] and is not considered + // fatal unless panicked. syncComplete []func() error // Passed to [Conn.Recvmsg]. Not copied if sufficient for all received messages. @@ -110,7 +124,9 @@ type Context struct { // cleanup arranges for f to be called after the next [CoreDone] event // or when [Context] is closed. -func (ctx *Context) cleanup(f func() error) { ctx.syncComplete = append(ctx.syncComplete, f) } +func (ctx *Context) cleanup(f func() error) { + ctx.syncComplete = append(ctx.syncComplete, f) +} // GetCore returns the address of [Core] held by this [Context]. func (ctx *Context) GetCore() *Core { return &ctx.core } @@ -118,8 +134,11 @@ func (ctx *Context) GetCore() *Core { return &ctx.core } // GetClient returns the address of [Client] held by this [Context]. func (ctx *Context) GetClient() *Client { return &ctx.client } -// New initialises [Context] for an already established connection and returns its address. -// The caller must not call any method of the underlying [Conn] after this function returns. +// New initialises [Context] for an already established connection and returns +// its address. +// +// The caller must not call any method of the underlying [Conn] after this +// function returns. func New(conn Conn, props SPADict) (*Context, error) { ctx := Context{conn: conn} ctx.core.ctx = &ctx @@ -143,14 +162,15 @@ func New(conn Conn, props SPADict) (*Context, error) { return &ctx, nil } -// unixConn is an implementation of the [Conn] interface for connections -// to Unix domain sockets. +// unixConn is an implementation of the [Conn] interface for connections to Unix +// domain sockets. type unixConn struct { fd int // Whether creation of a new epoll instance was attempted. epoll bool // File descriptor referring to the new epoll instance. + // // Valid if epoll is true and epollErr is nil. epollFd int // Error returned by syscall.EpollCreate1. @@ -164,9 +184,15 @@ type unixConn struct { // Dial connects to a Unix domain socket described by name. func Dial(name string) (Conn, error) { - if fd, err := syscall.Socket(syscall.AF_UNIX, syscall.SOCK_STREAM|syscall.SOCK_CLOEXEC|syscall.SOCK_NONBLOCK, 0); err != nil { + if fd, err := syscall.Socket( + syscall.AF_UNIX, syscall.SOCK_STREAM|syscall.SOCK_CLOEXEC|syscall.SOCK_NONBLOCK, + 0, + ); err != nil { return nil, os.NewSyscallError("socket", err) - } else if err = syscall.Connect(fd, &syscall.SockaddrUnix{Name: name}); err != nil { + } else if err = syscall.Connect( + fd, + &syscall.SockaddrUnix{Name: name}, + ); err != nil { _ = syscall.Close(fd) return nil, os.NewSyscallError("connect", err) } else { @@ -174,8 +200,8 @@ func Dial(name string) (Conn, error) { } } -// MightBlock informs the implementation that the next call -// might block for a non-zero timeout. +// MightBlock informs the implementation that the next call might block for a +// non-zero timeout. func (conn *unixConn) MightBlock(timeout time.Duration) { if timeout < 0 { timeout = 0 @@ -189,10 +215,15 @@ func (conn *unixConn) wantsEpoll() error { conn.epoll = true conn.epollFd, conn.epollErr = syscall.EpollCreate1(syscall.EPOLL_CLOEXEC) if conn.epollErr == nil { - if conn.epollErr = syscall.EpollCtl(conn.epollFd, syscall.EPOLL_CTL_ADD, conn.fd, &syscall.EpollEvent{ - Events: syscall.EPOLLERR | syscall.EPOLLHUP, - Fd: int32(conn.fd), - }); conn.epollErr != nil { + if conn.epollErr = syscall.EpollCtl( + conn.epollFd, + syscall.EPOLL_CTL_ADD, + conn.fd, + &syscall.EpollEvent{ + Events: syscall.EPOLLERR | syscall.EPOLLHUP, + Fd: int32(conn.fd), + }, + ); conn.epollErr != nil { _ = syscall.Close(conn.epollFd) } } @@ -209,16 +240,25 @@ func (conn *unixConn) wait(event uint32) (err error) { deadline := time.Now().Add(conn.timeout) conn.timeout = 0 - if err = syscall.EpollCtl(conn.epollFd, syscall.EPOLL_CTL_MOD, conn.fd, &syscall.EpollEvent{ - Events: event | syscall.EPOLLERR | syscall.EPOLLHUP, - Fd: int32(conn.fd), - }); err != nil { + if err = syscall.EpollCtl( + conn.epollFd, + syscall.EPOLL_CTL_MOD, + conn.fd, + &syscall.EpollEvent{ + Events: event | syscall.EPOLLERR | syscall.EPOLLHUP, + Fd: int32(conn.fd), + }, + ); err != nil { return } for timeout := deadline.Sub(time.Now()); timeout > 0; timeout = deadline.Sub(time.Now()) { var n int - if n, err = syscall.EpollWait(conn.epollFd, conn.epollBuf[:], int(timeout/time.Millisecond)); err != nil { + if n, err = syscall.EpollWait( + conn.epollFd, + conn.epollBuf[:], + int(timeout/time.Millisecond), + ); err != nil { return } @@ -245,7 +285,10 @@ func (conn *unixConn) wait(event uint32) (err error) { } // Recvmsg calls syscall.Recvmsg on the underlying socket. -func (conn *unixConn) Recvmsg(p, oob []byte, flags int) (n, oobn, recvflags int, err error) { +func (conn *unixConn) Recvmsg(p, oob []byte, flags int) ( + n, oobn, recvflags int, + err error, +) { if err = conn.wantsEpoll(); err != nil { return } else if err = conn.wait(syscall.EPOLLIN); err != nil { @@ -279,8 +322,8 @@ func (conn *unixConn) Close() (err error) { return conn.epollErr } -// MustNew calls [New](conn, props) and panics on error. -// It is intended for use in tests with hard-coded strings. +// MustNew calls [New](conn, props) and panics on error. It is intended for use +// in tests with hard-coded strings. func MustNew(conn Conn, props SPADict) *Context { if ctx, err := New(conn, props); err != nil { panic(err) @@ -292,20 +335,21 @@ func MustNew(conn Conn, props SPADict) *Context { // free releases the underlying storage of buf. func (ctx *Context) free() { ctx.buf = make([]byte, 0) } -// queueFiles queues some file descriptors to be sent for the next message. -// It returns the offset of their index for the syscall.SCM_RIGHTS message. +// queueFiles queues some file descriptors to be sent for the next message. It +// returns the offset of their index for the syscall.SCM_RIGHTS message. func (ctx *Context) queueFiles(fds ...int) (offset Fd) { offset = Fd(len(ctx.pendingFiles)) ctx.pendingFiles = append(ctx.pendingFiles, fds...) return } -// InconsistentFilesError describes an implementation error where an incorrect amount -// of files is queued between two messages. +// InconsistentFilesError describes an implementation error where an incorrect +// amount of files is queued between two messages. type InconsistentFilesError [2]Int func (e *InconsistentFilesError) Error() string { - return "queued " + strconv.Itoa(int(e[0])) + " files instead of the expected " + strconv.Itoa(int(e[1])) + return "queued " + strconv.Itoa(int(e[0])) + + " files instead of the expected " + strconv.Itoa(int(e[1])) } // writeMessage appends the POD representation of v and an optional footer to buf. @@ -318,7 +362,12 @@ func (ctx *Context) writeMessage(Id Int, v Message) (err error) { ctx.pendingFooter, ctx.deferredPendingFooter = ctx.deferredPendingFooter, nil } - ctx.buf, err = MessageEncoder{v}.AppendMessage(ctx.buf, Id, ctx.sequence, ctx.pendingFooter) + ctx.buf, err = MessageEncoder{v}.AppendMessage( + ctx.buf, + Id, + ctx.sequence, + ctx.pendingFooter, + ) if err == nil { ctx.headerFiles = len(ctx.pendingFiles) ctx.pendingFooter = nil @@ -327,8 +376,8 @@ func (ctx *Context) writeMessage(Id Int, v Message) (err error) { return } -// mustWriteMessage calls writeMessage and panics if a non-nil error is returned. -// This must only be called from eventProxy.consume. +// mustWriteMessage calls writeMessage and panics if a non-nil error is +// returned. This must only be called from eventProxy.consume. func (ctx *Context) mustWriteMessage(Id Int, v Message) { if err := ctx.writeMessage(Id, v); err != nil { panic(err) @@ -352,8 +401,8 @@ increment: return newId } -// closeReceivedFiles closes all receivedFiles. This is only during protocol error -// where [Context] is rendered unusable. +// closeReceivedFiles closes all receivedFiles. This is only during protocol +// error where [Context] is rendered unusable. func (ctx *Context) closeReceivedFiles() { slices.Sort(ctx.receivedFiles) ctx.receivedFiles = slices.Compact(ctx.receivedFiles) @@ -377,7 +426,11 @@ func (ctx *Context) recvmsg(remaining []byte) (payload []byte, err error) { var n, oobn, recvflags int for { - n, oobn, recvflags, err = ctx.conn.Recvmsg(ctx.iovecBuf[len(remaining):], ctx.oobBuf[:], recvmsgFlags) + n, oobn, recvflags, err = ctx.conn.Recvmsg( + ctx.iovecBuf[len(remaining):], + ctx.oobBuf[:], + recvmsgFlags, + ) if oob := ctx.oobBuf[:oobn]; len(oob) > 0 { var oobErr error @@ -412,7 +465,10 @@ func (ctx *Context) recvmsg(remaining []byte) (payload []byte, err error) { } if err != syscall.EAGAIN && err != syscall.EWOULDBLOCK { ctx.closeReceivedFiles() - return nil, &ProxyFatalError{Err: os.NewSyscallError("recvmsg", err), ProxyErrs: ctx.cloneAsProxyErrors()} + return nil, &ProxyFatalError{ + Err: os.NewSyscallError("recvmsg", err), + ProxyErrs: ctx.cloneAsProxyErrors(), + } } } @@ -449,7 +505,10 @@ func (ctx *Context) sendmsg(p []byte, fds ...int) error { } if err != nil && err != syscall.EAGAIN && err != syscall.EWOULDBLOCK { - return &ProxyFatalError{Err: os.NewSyscallError("sendmsg", err), ProxyErrs: ctx.cloneAsProxyErrors()} + return &ProxyFatalError{ + Err: os.NewSyscallError("sendmsg", err), + ProxyErrs: ctx.cloneAsProxyErrors(), + } } return err } @@ -463,7 +522,9 @@ type UnknownIdError struct { Data string } -func (e *UnknownIdError) Error() string { return "unknown proxy id " + strconv.Itoa(int(e.Id)) } +func (e *UnknownIdError) Error() string { + return "unknown proxy id " + strconv.Itoa(int(e.Id)) +} // UnsupportedOpcodeError describes a message with an unsupported opcode. type UnsupportedOpcodeError struct { @@ -484,7 +545,8 @@ func (e UnsupportedFooterOpcodeError) Error() string { return "unsupported footer opcode " + strconv.Itoa(int(e)) } -// A RoundtripUnexpectedEOFError describes an unexpected EOF encountered during [Context.Roundtrip]. +// A RoundtripUnexpectedEOFError describes an unexpected EOF encountered during +// [Context.Roundtrip]. type RoundtripUnexpectedEOFError uintptr const ( @@ -494,11 +556,11 @@ const ( // ErrRoundtripEOFBody is returned when unexpectedly encountering EOF // establishing message body bounds. ErrRoundtripEOFBody - // ErrRoundtripEOFFooter is like [ErrRoundtripEOFBody], but for when establishing - // bounds for the footer instead. + // ErrRoundtripEOFFooter is like [ErrRoundtripEOFBody], but for when + // establishing bounds for the footer instead. ErrRoundtripEOFFooter - // ErrRoundtripEOFFooterOpcode is returned when unexpectedly encountering EOF - // during the footer opcode hack. + // ErrRoundtripEOFFooterOpcode is returned when unexpectedly encountering + // EOF during the footer opcode hack. ErrRoundtripEOFFooterOpcode ) @@ -549,10 +611,10 @@ func (ctx *Context) unmarshal(header *Header, data []byte, v any) error { data = data[n:header.Size] if len(data) > 0 { - /* the footer concrete type is determined by opcode, which cannot be - decoded directly before the type is known, so this hack is required: - skip the struct prefix, then the integer prefix, and the next SizeId - bytes are the encoded opcode value */ + // the footer concrete type is determined by opcode, which cannot be + // decoded directly before the type is known, so this hack is required: + // skip the struct prefix, then the integer prefix, and the next SizeId + // bytes are the encoded opcode value if len(data) < int(SizePrefix*2+SizeId) { return ErrRoundtripEOFFooterOpcode } @@ -565,11 +627,14 @@ func (ctx *Context) unmarshal(header *Header, data []byte, v any) error { if ctx.generation != footer.Payload.RegistryGeneration { var pendingFooter = Footer[FooterClientGeneration]{ FOOTER_CORE_OPCODE_GENERATION, - FooterClientGeneration{ClientGeneration: footer.Payload.RegistryGeneration}, + FooterClientGeneration{ + ClientGeneration: footer.Payload.RegistryGeneration, + }, } - // this emulates upstream behaviour that pending footer updated on the last message - // during a roundtrip is pushed back to the first message of the next roundtrip + // this emulates upstream behaviour that pending footer updated + // on the last message during a roundtrip is pushed back to the + // first message of the next roundtrip if isLastMessage { ctx.deferredPendingFooter = &pendingFooter } else { @@ -592,38 +657,43 @@ func (ctx *Context) unmarshal(header *Header, data []byte, v any) error { // is, in fact, not the message header. type UnexpectedSequenceError Int -func (e UnexpectedSequenceError) Error() string { return "unexpected seq " + strconv.Itoa(int(e)) } +func (e UnexpectedSequenceError) Error() string { + return "unexpected seq " + strconv.Itoa(int(e)) +} -// An UnexpectedFilesError describes an inconsistent state where file count claimed by -// [Header] accumulates to a value greater than the total number of files received. +// An UnexpectedFilesError describes an inconsistent state where file count +// claimed by [Header] accumulates to a value greater than the total number of +// files received. type UnexpectedFilesError int func (e UnexpectedFilesError) Error() string { return "server message headers claim to have sent more files than actually received" } -// A DanglingFilesError holds onto files that were sent by the server but no [Header] -// accounts for. These are closed by their finalizers if discarded. +// A DanglingFilesError holds onto files that were sent by the server but no +// [Header] accounts for. These are closed by their finalizers if discarded. type DanglingFilesError []*os.File func (e DanglingFilesError) Error() string { return "received " + strconv.Itoa(len(e)) + " dangling files" } -// An UnacknowledgedProxyError holds newly allocated proxy ids that the server failed -// to acknowledge after an otherwise successful [Core.Sync]. +// An UnacknowledgedProxyError holds newly allocated proxy ids that the server +// failed to acknowledge after an otherwise successful [Core.Sync]. type UnacknowledgedProxyError []Int func (e UnacknowledgedProxyError) Error() string { - return "server did not acknowledge " + strconv.Itoa(len(e)) + " proxies" + return "server did not acknowledge " + + strconv.Itoa(len(e)) + " proxies" } -// An UnacknowledgedProxyDestructionError holds destroyed proxy ids that the server failed -// to acknowledge after an otherwise successful [Core.Sync]. +// An UnacknowledgedProxyDestructionError holds destroyed proxy ids that the +// server failed to acknowledge after an otherwise successful [Core.Sync]. type UnacknowledgedProxyDestructionError []Int func (e UnacknowledgedProxyDestructionError) Error() string { - return "server did not acknowledge " + strconv.Itoa(len(e)) + " proxy destructions" + return "server did not acknowledge " + + strconv.Itoa(len(e)) + " proxy destructions" } // A ProxyFatalError describes an error that terminates event handling during a @@ -639,14 +709,15 @@ func (e *ProxyFatalError) Unwrap() []error { return append(e.ProxyErrs, e.Err) } func (e *ProxyFatalError) Error() string { s := e.Err.Error() if len(e.ProxyErrs) > 0 { - s += "; " + strconv.Itoa(len(e.ProxyErrs)) + " additional proxy errors occurred before this point" + s += "; " + strconv.Itoa(len(e.ProxyErrs)) + + " additional proxy errors occurred before this point" } return s } -// A ProxyConsumeError is a collection of non-protocol errors returned by proxies -// during event processing. These do not prevent event handling from continuing but -// may be considered fatal to the application. +// A ProxyConsumeError is a collection of non-protocol errors returned by +// proxies during event processing. These do not prevent event handling from +// continuing but may be considered fatal to the application. type ProxyConsumeError []error func (e ProxyConsumeError) Unwrap() []error { return e } @@ -658,7 +729,8 @@ func (e ProxyConsumeError) Error() string { // first error is usually the most relevant one s := e[0].Error() if len(e) > 1 { - s += "; " + strconv.Itoa(len(e)) + " additional proxy errors occurred after this point" + s += "; " + strconv.Itoa(len(e)) + + " additional proxy errors occurred after this point" } return s } @@ -684,14 +756,15 @@ func (ctx *Context) cloneProxyErrors() (err error) { return } -// roundtripSyncID is the id passed to Context.coreSync during a [Context.Roundtrip]. +// roundtripSyncID is the id passed to Context.coreSync during a +// [Context.Roundtrip]. const roundtripSyncID = 0 // Roundtrip sends all pending messages to the server and processes events until // the server has no more messages. // -// For a non-nil error, if the error happens over the network, it has concrete type -// [os.SyscallError]. +// For a non-nil error, if the error happens over the network, it has concrete +// type [os.SyscallError]. func (ctx *Context) Roundtrip() (err error) { err = ctx.roundtrip() if err == nil { @@ -725,10 +798,12 @@ func (ctx *Context) roundtrip() (err error) { danglingFiles = make(DanglingFilesError, 0, len(ctx.receivedFiles)) for _, fd := range ctx.receivedFiles { - // hold these as *os.File so they are closed if this error never reaches the caller, - // or the caller discards or otherwise does not handle this error, to avoid leaking fds + // hold these as *os.File so they are closed if this error never + // reaches the caller, or the caller discards or otherwise does + // not handle this error, to avoid leaking fds danglingFiles = append(danglingFiles, os.NewFile(uintptr(fd), - "dangling fd "+strconv.Itoa(fd)+" received from PipeWire")) + "dangling fd "+strconv.Itoa(fd)+ + " received from PipeWire")) } ctx.receivedFiles = ctx.receivedFiles[:0] } @@ -736,7 +811,10 @@ func (ctx *Context) roundtrip() (err error) { // populated early for finalizers, but does not overwrite existing errors if len(danglingFiles) > 0 && err == nil { ctx.closeReceivedFiles() - err = &ProxyFatalError{Err: danglingFiles, ProxyErrs: ctx.cloneAsProxyErrors()} + err = &ProxyFatalError{ + Err: danglingFiles, + ProxyErrs: ctx.cloneAsProxyErrors(), + } return } }() @@ -754,9 +832,15 @@ func (ctx *Context) roundtrip() (err error) { if len(remaining) == 0 { err = nil } else if len(remaining) < SizeHeader { - err = &ProxyFatalError{Err: ErrRoundtripEOFHeader, ProxyErrs: ctx.cloneAsProxyErrors()} + err = &ProxyFatalError{ + Err: ErrRoundtripEOFHeader, + ProxyErrs: ctx.cloneAsProxyErrors(), + } } else { - err = &ProxyFatalError{Err: ErrRoundtripEOFBody, ProxyErrs: ctx.cloneAsProxyErrors()} + err = &ProxyFatalError{ + Err: ErrRoundtripEOFBody, + ProxyErrs: ctx.cloneAsProxyErrors(), + } } } return @@ -772,7 +856,10 @@ func (ctx *Context) currentSeq() Int { return ctx.sequence - 1 } func (ctx *Context) currentRemoteSeq() Int { return ctx.remoteSequence - 1 } // consume receives messages from the server and processes events. -func (ctx *Context) consume(receiveRemaining []byte) (remaining []byte, err error) { +func (ctx *Context) consume(receiveRemaining []byte) ( + remaining []byte, + err error, +) { defer func() { r := recover() if r == nil { @@ -788,7 +875,10 @@ func (ctx *Context) consume(receiveRemaining []byte) (remaining []byte, err erro panic(&runtime.PanicNilError{}) } - err = &ProxyFatalError{Err: recoveredErr, ProxyErrs: ctx.cloneAsProxyErrors()} + err = &ProxyFatalError{ + Err: recoveredErr, + ProxyErrs: ctx.cloneAsProxyErrors(), + } return }() @@ -816,7 +906,10 @@ func (ctx *Context) consume(receiveRemaining []byte) (remaining []byte, err erro proxy, ok := ctx.proxy[header.ID] if !ok { - return remaining, &UnknownIdError{header.ID, string(remaining[:SizeHeader+header.Size])} + return remaining, &UnknownIdError{ + header.ID, + string(remaining[:SizeHeader+header.Size]), + } } fileCount := int(header.FileCount) @@ -828,7 +921,11 @@ func (ctx *Context) consume(receiveRemaining []byte) (remaining []byte, err erro remaining = remaining[SizeHeader:] proxyErr := proxy.consume(header.Opcode, files, func(v any) { - if unmarshalErr := ctx.unmarshal(&header, remaining, v); unmarshalErr != nil { + if unmarshalErr := ctx.unmarshal( + &header, + remaining, + v, + ); unmarshalErr != nil { panic(unmarshalErr) } }) @@ -840,16 +937,17 @@ func (ctx *Context) consume(receiveRemaining []byte) (remaining []byte, err erro return } -// An UnexpectedFileCountError is returned as part of a [ProxyFatalError] for an event -// that received an unexpected number of files. +// An UnexpectedFileCountError is returned as part of a [ProxyFatalError] for an +// event that received an unexpected number of files. type UnexpectedFileCountError [2]int func (e *UnexpectedFileCountError) Error() string { return "received " + strconv.Itoa(e[1]) + " files instead of the expected " + strconv.Itoa(e[0]) } -// closeReceivedFiles closes all received files and panics with [UnexpectedFileCountError] -// if one or more files are passed. This is used with events that do not expect files. +// closeReceivedFiles closes all received files and panics with +// [UnexpectedFileCountError] if one or more files are passed. This is used with +// events that do not expect files. func closeReceivedFiles(fds ...int) { for _, fd := range fds { _ = syscall.Close(fd) @@ -859,8 +957,9 @@ func closeReceivedFiles(fds ...int) { } } -// doSyncComplete calls syncComplete functions and collects their errors alongside errors -// cloned from proxyErrors. A panic is translated into ProxyFatalError. +// doSyncComplete calls syncComplete functions and collects their errors +// alongside errors cloned from proxyErrors. A panic is translated into +// ProxyFatalError. func (ctx *Context) doSyncComplete() (err error) { proxyErrors := ctx.cloneAsProxyErrors() defer func() { @@ -914,17 +1013,17 @@ func (ctx *Context) Close() (err error) { } } -// expectsCoreError returns a function that inspects an error value and -// returns the address of a [CoreError] if it is the only error present -// and targets the specified proxy and sequence. +// expectsCoreError returns a function that inspects an error value and returns +// the address of a [CoreError] if it is the only error present and targets the +// specified proxy and sequence. // -// The behaviour of expectsCoreError is only correct for an empty buf -// prior to calling. If buf is not empty, [Core.Sync] is called, with -// its return value stored to the value pointed to by errP if not nil, -// and the function is not populated. +// The behaviour of expectsCoreError is only correct for an empty buf prior to +// calling. If buf is not empty, [Core.Sync] is called, with its return value +// stored to the value pointed to by errP if not nil, and the function is not +// populated. // -// The caller must queue a message and call [Core.Sync] immediately -// after calling expectsCoreError. +// The caller must queue a message and call [Core.Sync] immediately after +// calling expectsCoreError. func (ctx *Context) expectsCoreError(id Int, errP *error) (asCoreError func() (coreError *CoreError)) { if len(ctx.buf) > 0 { if err := ctx.GetCore().Sync(); err != nil { @@ -967,7 +1066,8 @@ const Remote = "PIPEWIRE_REMOTE" const DEFAULT_SYSTEM_RUNTIME_DIR = "/run/pipewire" -// connectName connects to a PipeWire remote by name and returns the resulting [Conn]. +// connectName connects to a PipeWire remote by name and returns the resulting +// [Conn]. func connectName(name string, manager bool) (conn Conn, err error) { if manager && !strings.HasSuffix(name, "-manager") { return connectName(name+"-manager", false) diff --git a/internal/pipewire/pod.go b/internal/pipewire/pod.go index e085af3d..3a03540b 100644 --- a/internal/pipewire/pod.go +++ b/internal/pipewire/pod.go @@ -12,7 +12,8 @@ import ( type ( // A Word is a 32-bit unsigned integer. // - // Values internal to a message appear to always be aligned to 32-bit boundary. + // 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. @@ -40,9 +41,11 @@ 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 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 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 @@ -76,11 +79,11 @@ 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. +// 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 encodes the receiver into PipeWire POD encoding, appends it to + // data, and returns the result. MarshalPOD(data []byte) ([]byte, error) } @@ -90,9 +93,9 @@ 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. +// 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 " + strconv.Itoa(int(e)) + " out of range" } @@ -111,8 +114,8 @@ 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. +// 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)...) @@ -146,7 +149,8 @@ func marshalValueAppend(data []byte, v reflect.Value) ([]byte, error) { return appendInner(data, func(data []byte) ([]byte, error) { return marshalValueAppendRaw(data, v) }) } -// marshalValueAppendRaw implements [MarshalAppend] on [reflect.Value] without the size prefix. +// marshalValueAppendRaw implements [MarshalAppend] on [reflect.Value] without +// the size prefix. func marshalValueAppendRaw(data []byte, v reflect.Value) ([]byte, error) { if v.CanInterface() { switch c := v.Interface().(type) { @@ -202,10 +206,11 @@ func marshalValueAppendRaw(data []byte, v reflect.Value) ([]byte, error) { } } -// PODUnmarshaler is the interface implemented by an object that can -// unmarshal a PipeWire POD encoding representation of itself. +// 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) @@ -226,15 +231,16 @@ func (e *InvalidUnmarshalError) Error() string { return "attempting to unmarshal to nil " + e.Type.String() } -// UnexpectedEOFError describes an unexpected EOF encountered in the middle of decoding POD data. +// UnexpectedEOFError describes an unexpected EOF encountered in the middle of +// decoding POD data. type UnexpectedEOFError uintptr const ( - // ErrEOFPrefix is returned when unexpectedly encountering EOF - // decoding the fixed-size POD prefix. + // ErrEOFPrefix is returned when unexpectedly encountering EOF decoding the + // fixed-size POD prefix. ErrEOFPrefix UnexpectedEOFError = iota - // ErrEOFData is returned when unexpectedly encountering EOF - // establishing POD data bounds. + // ErrEOFData is returned when unexpectedly encountering EOF establishing + // POD data bounds. ErrEOFData // ErrEOFDataString is returned when unexpectedly encountering EOF // establishing POD [String] bounds. @@ -259,9 +265,9 @@ func (e UnexpectedEOFError) Error() string { return "unexpected EOF " + suffix } -// 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]. +// 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 @@ -291,8 +297,8 @@ 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]. +// A TrailingGarbageError describes extra bytes after decoding has completed +// during [Unmarshal]. type TrailingGarbageError []byte func (e TrailingGarbageError) Error() string { @@ -310,7 +316,8 @@ func (e StringTerminationError) Error() string { return "got byte " + strconv.Itoa(int(e)) + " instead of NUL" } -// unmarshalValue implements [Unmarshal] on [reflect.Value] without compensating for prefix and padding size. +// 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()} @@ -330,7 +337,11 @@ func unmarshalValue(data []byte, v reflect.Value, wireSizeP *Word) error { switch v.Interface().(type) { case Fd: *wireSizeP = SizeFd - if err := unmarshalCheckTypeBounds(&data, SPA_TYPE_Fd, wireSizeP); err != nil { + if err := unmarshalCheckTypeBounds( + &data, + SPA_TYPE_Fd, + wireSizeP, + ); err != nil { return err } v.SetInt(int64(binary.NativeEndian.Uint64(data))) @@ -341,7 +352,11 @@ func unmarshalValue(data []byte, v reflect.Value, wireSizeP *Word) error { switch v.Kind() { case reflect.Uint32: *wireSizeP = SizeId - if err := unmarshalCheckTypeBounds(&data, SPA_TYPE_Id, wireSizeP); err != nil { + if err := unmarshalCheckTypeBounds( + &data, + SPA_TYPE_Id, + wireSizeP, + ); err != nil { return err } v.SetUint(uint64(binary.NativeEndian.Uint32(data))) @@ -349,7 +364,11 @@ func unmarshalValue(data []byte, v reflect.Value, wireSizeP *Word) error { case reflect.Int32: *wireSizeP = SizeInt - if err := unmarshalCheckTypeBounds(&data, SPA_TYPE_Int, wireSizeP); err != nil { + if err := unmarshalCheckTypeBounds( + &data, + SPA_TYPE_Int, + wireSizeP, + ); err != nil { return err } v.SetInt(int64(binary.NativeEndian.Uint32(data))) @@ -357,7 +376,11 @@ func unmarshalValue(data []byte, v reflect.Value, wireSizeP *Word) error { case reflect.Int64: *wireSizeP = SizeLong - if err := unmarshalCheckTypeBounds(&data, SPA_TYPE_Long, wireSizeP); err != nil { + if err := unmarshalCheckTypeBounds( + &data, + SPA_TYPE_Long, + wireSizeP, + ); err != nil { return err } v.SetInt(int64(binary.NativeEndian.Uint64(data))) @@ -365,7 +388,11 @@ func unmarshalValue(data []byte, v reflect.Value, wireSizeP *Word) error { case reflect.Struct: *wireSizeP = 0 - if err := unmarshalCheckTypeBounds(&data, SPA_TYPE_Struct, wireSizeP); err != nil { + if err := unmarshalCheckTypeBounds( + &data, + SPA_TYPE_Struct, + wireSizeP, + ); err != nil { return err } @@ -396,7 +423,11 @@ func unmarshalValue(data []byte, v reflect.Value, wireSizeP *Word) error { case reflect.String: *wireSizeP = 0 - if err := unmarshalCheckTypeBounds(&data, SPA_TYPE_String, wireSizeP); err != nil { + if err := unmarshalCheckTypeBounds( + &data, + SPA_TYPE_String, + wireSizeP, + ); err != nil { return err } @@ -419,8 +450,9 @@ func unmarshalValue(data []byte, v reflect.Value, wireSizeP *Word) error { } } -// unmarshalHandleNone establishes prefix bounds and, for a value of type [SPA_TYPE_None], -// validates its size and skips the header. This is for unmarshalling values that can be nil. +// unmarshalHandleNone establishes prefix bounds and, for a value of type +// [SPA_TYPE_None], validates its size and skips the header. This is for +// unmarshalling values that can be nil. func unmarshalHandleNone(data *[]byte, wireSizeP *Word) (bool, error) { if len(*data) < SizePrefix { return false, ErrEOFPrefix @@ -451,15 +483,17 @@ func (e InconsistentSizeError) Error() string { " for a " + strconv.Itoa(int(e.Expect)) + "-byte long segment" } -// An UnexpectedTypeError describes an unexpected type encountered -// in data passed to [Unmarshal]. +// An UnexpectedTypeError describes an unexpected type encountered in data +// passed to [Unmarshal]. type UnexpectedTypeError struct{ Type, Expect SPAKind } func (e UnexpectedTypeError) Error() string { return "received " + e.Type.String() + " for a value of type " + e.Expect.String() } -// unmarshalCheckTypeBounds performs bounds checks on data and validates the type and size prefixes. +// 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 SPAKind, sizeP *Word) error { if len(*data) < SizePrefix { @@ -486,8 +520,8 @@ func unmarshalCheckTypeBounds(data *[]byte, t SPAKind, sizeP *Word) error { return nil } -// The Footer contains additional messages, not directed to -// the destination object defined by the Id field. +// The Footer contains additional messages, not directed to the destination +// object defined by the Id field. type Footer[P KnownSize] struct { // The footer opcode. Opcode Id `json:"opcode"` @@ -595,7 +629,8 @@ func (d *SPADict) UnmarshalPOD(data []byte) (Word, error) { return wireSize, nil } -// A Message is a value that can be transmitted as a message over PipeWire protocol native. +// A Message is a value that can be transmitted as a message over PipeWire +// protocol native. type Message interface { // Opcode returns the opcode of this message. Opcode() byte @@ -617,8 +652,13 @@ func (m MessageEncoder) SizeMessage(footer KnownSize) (size Word) { return } -// AppendMessage appends the protocol native encoding of Message to dst and returns the appended slice. -func (m MessageEncoder) AppendMessage(dst []byte, Id, sequence Int, footer KnownSize) (data []byte, err error) { +// AppendMessage appends the protocol native encoding of Message to dst and +// returns the appended slice. +func (m MessageEncoder) AppendMessage( + dst []byte, + Id, sequence Int, + footer KnownSize, +) (data []byte, err error) { size := m.SizeMessage(footer) if size&^SizeMax != 0 { return dst, ErrSizeRange diff --git a/internal/pipewire/securitycontext.go b/internal/pipewire/securitycontext.go index ec19a64a..f277a99e 100644 --- a/internal/pipewire/securitycontext.go +++ b/internal/pipewire/securitycontext.go @@ -35,31 +35,31 @@ const ( // SecurityContextCreate is sent to create a new security context. // -// Creates a new security context with a socket listening FD. -// PipeWire will accept new client connections on listen_fd. +// Creates a new security context with a socket listening FD. PipeWire will +// accept new client connections on listen_fd. // -// listen_fd must be ready to accept new connections when this -// request is sent by the client. In other words, the client must -// call bind(2) and listen(2) before sending the FD. +// listen_fd must be ready to accept new connections when this request is sent +// by the client. In other words, the client must call bind(2) and listen(2) +// before sending the FD. // -// close_fd is a FD closed by the client when PipeWire should stop -// accepting new connections on listen_fd. +// close_fd is a FD closed by the client when PipeWire should stop accepting new +// connections on listen_fd. // -// PipeWire must continue to accept connections on listen_fd when -// the client which created the security context disconnects. +// PipeWire must continue to accept connections on listen_fd when the client +// which created the security context disconnects. // -// After sending this request, closing listen_fd and close_fd -// remains the only valid operation on them. +// After sending this request, closing listen_fd and close_fd remains the only +// valid operation on them. type SecurityContextCreate struct { - // The offset in the SCM_RIGHTS msg_control message to - // the fd to listen on for new connections. + // The offset in the SCM_RIGHTS msg_control message to the fd to listen on + // for new connections. ListenFd Fd - // The offset in the SCM_RIGHTS msg_control message to - // the fd used to stop listening. + // The offset in the SCM_RIGHTS msg_control message to the fd used to stop + // listening. CloseFd Fd - // Extra properties. These will be copied on the client - // that connects through this context. + // Extra properties. These will be copied on the client that connects + // through this context. Properties *SPADict `json:"props"` } @@ -170,7 +170,9 @@ func (scc *securityContextCloser) Close() (err error) { return } -// BindAndCreate binds a new socket to the specified pathname and pass it to Create. +// BindAndCreate binds a new socket to the specified pathname and pass it to +// Create. +// // It returns an [io.Closer] corresponding to [SecurityContextCreate.CloseFd]. func (securityContext *SecurityContext) BindAndCreate(pathname string, props SPADict) (io.Closer, error) { if err := securityContext.checkDestroy(); err != nil { @@ -231,10 +233,19 @@ func (securityContext *SecurityContext) consume(opcode byte, files []int, _ func func (securityContext *SecurityContext) setBoundProps(event *CoreBoundProps) error { securityContext.mustCheckDestroy() if securityContext.ID != event.ID { - return &InconsistentIdError{Proxy: securityContext, ID: securityContext.ID, ServerID: event.ID} + return &InconsistentIdError{ + Proxy: securityContext, + ID: securityContext.ID, + ServerID: event.ID, + } } if securityContext.GlobalID != event.GlobalID { - return &InconsistentIdError{Global: true, Proxy: securityContext, ID: securityContext.GlobalID, ServerID: event.GlobalID} + return &InconsistentIdError{ + Global: true, + Proxy: securityContext, + ID: securityContext.GlobalID, + ServerID: event.GlobalID, + } } return nil } diff --git a/internal/pipewire/spa.go b/internal/pipewire/spa.go index c44bdb83..b231cc07 100644 --- a/internal/pipewire/spa.go +++ b/internal/pipewire/spa.go @@ -7,10 +7,9 @@ import ( // A SPAKind describes the kind of data being encoded right after it. // -// These do not always follow the same rules, and encoding/decoding -// is very much context-dependent. Callers should therefore not -// attempt to use these values directly and rely on [Marshal] and -// [Unmarshal] and their variants instead. +// These do not always follow the same rules, and encoding/decoding is very much +// context-dependent. Callers should therefore not attempt to use these values +// directly and rely on [Marshal] and [Unmarshal] and their variants instead. type SPAKind Word /* Basic types */ -- cgit v1.3.1