hst: remove enablement json adapter

The go116 behaviour of built-in new function makes this cleaner.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
2026-04-10 20:39:06 +09:00
parent c33a6a5b7e
commit 560cb626a1
26 changed files with 149 additions and 130 deletions

View File

@@ -152,7 +152,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
} }
} }
var et hst.Enablement var et hst.Enablements
if flagWayland { if flagWayland {
et |= hst.EWayland et |= hst.EWayland
} }
@@ -170,7 +170,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
ID: flagID, ID: flagID,
Identity: flagIdentity, Identity: flagIdentity,
Groups: flagGroups, Groups: flagGroups,
Enablements: hst.NewEnablements(et), Enablements: &et,
Container: &hst.ContainerConfig{ Container: &hst.ContainerConfig{
Filesystem: []hst.FilesystemConfigJSON{ Filesystem: []hst.FilesystemConfigJSON{

View File

@@ -32,7 +32,7 @@ var (
PID: 0xbeef, PID: 0xbeef,
ShimPID: 0xcafe, ShimPID: 0xcafe,
Config: &hst.Config{ Config: &hst.Config{
Enablements: hst.NewEnablements(hst.EWayland | hst.EPipeWire), Enablements: new(hst.EWayland | hst.EPipeWire),
Identity: 1, Identity: 1,
Container: &hst.ContainerConfig{ Container: &hst.ContainerConfig{
Shell: check.MustAbs("/bin/sh"), Shell: check.MustAbs("/bin/sh"),

View File

@@ -64,44 +64,44 @@ func TestConfigValidate(t *testing.T) {
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron, }}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron,
Msg: `invalid environment variable "TERM\x00"`}}, Msg: `invalid environment variable "TERM\x00"`}},
{"insecure pulse", &hst.Config{Enablements: hst.NewEnablements(hst.EPulse), Container: &hst.ContainerConfig{ {"insecure pulse", &hst.Config{Enablements: new(hst.EPulse), Container: &hst.ContainerConfig{
Home: fhs.AbsTmp, Home: fhs.AbsTmp,
Shell: fhs.AbsTmp, Shell: fhs.AbsTmp,
Path: fhs.AbsTmp, Path: fhs.AbsTmp,
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure, }}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure,
Msg: "enablement PulseAudio is insecure and no longer supported"}}, Msg: "enablement PulseAudio is insecure and no longer supported"}},
{"direct wayland", &hst.Config{Enablements: hst.NewEnablements(hst.EWayland), DirectWayland: true, Container: &hst.ContainerConfig{ {"direct wayland", &hst.Config{Enablements: new(hst.EWayland), DirectWayland: true, Container: &hst.ContainerConfig{
Home: fhs.AbsTmp, Home: fhs.AbsTmp,
Shell: fhs.AbsTmp, Shell: fhs.AbsTmp,
Path: fhs.AbsTmp, Path: fhs.AbsTmp,
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure, }}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure,
Msg: "direct_wayland is insecure and no longer supported"}}, Msg: "direct_wayland is insecure and no longer supported"}},
{"direct wayland allow", &hst.Config{Enablements: hst.NewEnablements(hst.EWayland), DirectWayland: true, Container: &hst.ContainerConfig{ {"direct wayland allow", &hst.Config{Enablements: new(hst.EWayland), DirectWayland: true, Container: &hst.ContainerConfig{
Home: fhs.AbsTmp, Home: fhs.AbsTmp,
Shell: fhs.AbsTmp, Shell: fhs.AbsTmp,
Path: fhs.AbsTmp, Path: fhs.AbsTmp,
}}, hst.VAllowInsecure, nil}, }}, hst.VAllowInsecure, nil},
{"direct pipewire", &hst.Config{Enablements: hst.NewEnablements(hst.EPipeWire), DirectPipeWire: true, Container: &hst.ContainerConfig{ {"direct pipewire", &hst.Config{Enablements: new(hst.EPipeWire), DirectPipeWire: true, Container: &hst.ContainerConfig{
Home: fhs.AbsTmp, Home: fhs.AbsTmp,
Shell: fhs.AbsTmp, Shell: fhs.AbsTmp,
Path: fhs.AbsTmp, Path: fhs.AbsTmp,
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure, }}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure,
Msg: "direct_pipewire is insecure and no longer supported"}}, Msg: "direct_pipewire is insecure and no longer supported"}},
{"direct pipewire allow", &hst.Config{Enablements: hst.NewEnablements(hst.EPipeWire), DirectPipeWire: true, Container: &hst.ContainerConfig{ {"direct pipewire allow", &hst.Config{Enablements: new(hst.EPipeWire), DirectPipeWire: true, Container: &hst.ContainerConfig{
Home: fhs.AbsTmp, Home: fhs.AbsTmp,
Shell: fhs.AbsTmp, Shell: fhs.AbsTmp,
Path: fhs.AbsTmp, Path: fhs.AbsTmp,
}}, hst.VAllowInsecure, nil}, }}, hst.VAllowInsecure, nil},
{"direct pulse", &hst.Config{Enablements: hst.NewEnablements(hst.EPulse), DirectPulse: true, Container: &hst.ContainerConfig{ {"direct pulse", &hst.Config{Enablements: new(hst.EPulse), DirectPulse: true, Container: &hst.ContainerConfig{
Home: fhs.AbsTmp, Home: fhs.AbsTmp,
Shell: fhs.AbsTmp, Shell: fhs.AbsTmp,
Path: fhs.AbsTmp, Path: fhs.AbsTmp,
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure, }}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure,
Msg: "direct_pulse is insecure and no longer supported"}}, Msg: "direct_pulse is insecure and no longer supported"}},
{"direct pulse allow", &hst.Config{Enablements: hst.NewEnablements(hst.EPulse), DirectPulse: true, Container: &hst.ContainerConfig{ {"direct pulse allow", &hst.Config{Enablements: new(hst.EPulse), DirectPulse: true, Container: &hst.ContainerConfig{
Home: fhs.AbsTmp, Home: fhs.AbsTmp,
Shell: fhs.AbsTmp, Shell: fhs.AbsTmp,
Path: fhs.AbsTmp, Path: fhs.AbsTmp,

View File

@@ -7,12 +7,12 @@ import (
"syscall" "syscall"
) )
// Enablement represents an optional host service to export to the target user. // Enablements denotes optional host service to export to the target user.
type Enablement byte type Enablements byte
const ( const (
// EWayland exposes a Wayland pathname socket via security-context-v1. // EWayland exposes a Wayland pathname socket via security-context-v1.
EWayland Enablement = 1 << iota EWayland Enablements = 1 << iota
// EX11 adds the target user via X11 ChangeHosts and exposes the X11 // EX11 adds the target user via X11 ChangeHosts and exposes the X11
// pathname socket. // pathname socket.
EX11 EX11
@@ -28,8 +28,8 @@ const (
EM EM
) )
// String returns a string representation of the flags set on [Enablement]. // String returns a string representation of the flags set on [Enablements].
func (e Enablement) String() string { func (e Enablements) String() string {
switch e { switch e {
case 0: case 0:
return "(no enablements)" return "(no enablements)"
@@ -47,7 +47,7 @@ func (e Enablement) String() string {
buf := new(strings.Builder) buf := new(strings.Builder)
buf.Grow(32) buf.Grow(32)
for i := Enablement(1); i < EM; i <<= 1 { for i := Enablements(1); i < EM; i <<= 1 {
if e&i != 0 { if e&i != 0 {
buf.WriteString(", " + i.String()) buf.WriteString(", " + i.String())
} }
@@ -60,12 +60,6 @@ func (e Enablement) String() string {
} }
} }
// NewEnablements returns the address of [Enablement] as [Enablements].
func NewEnablements(e Enablement) *Enablements { return (*Enablements)(&e) }
// Enablements is the [json] adapter for [Enablement].
type Enablements Enablement
// enablementsJSON is the [json] representation of [Enablements]. // enablementsJSON is the [json] representation of [Enablements].
type enablementsJSON = struct { type enablementsJSON = struct {
Wayland bool `json:"wayland,omitempty"` Wayland bool `json:"wayland,omitempty"`
@@ -75,24 +69,21 @@ type enablementsJSON = struct {
Pulse bool `json:"pulse,omitempty"` Pulse bool `json:"pulse,omitempty"`
} }
// Unwrap returns the underlying [Enablement]. // Unwrap returns the value pointed to by e.
func (e *Enablements) Unwrap() Enablement { func (e *Enablements) Unwrap() Enablements {
if e == nil { if e == nil {
return 0 return 0
} }
return Enablement(*e) return *e
} }
func (e *Enablements) MarshalJSON() ([]byte, error) { func (e Enablements) MarshalJSON() ([]byte, error) {
if e == nil {
return nil, syscall.EINVAL
}
return json.Marshal(&enablementsJSON{ return json.Marshal(&enablementsJSON{
Wayland: Enablement(*e)&EWayland != 0, Wayland: e&EWayland != 0,
X11: Enablement(*e)&EX11 != 0, X11: e&EX11 != 0,
DBus: Enablement(*e)&EDBus != 0, DBus: e&EDBus != 0,
PipeWire: Enablement(*e)&EPipeWire != 0, PipeWire: e&EPipeWire != 0,
Pulse: Enablement(*e)&EPulse != 0, Pulse: e&EPulse != 0,
}) })
} }
@@ -106,22 +97,21 @@ func (e *Enablements) UnmarshalJSON(data []byte) error {
return err return err
} }
var ve Enablement *e = 0
if v.Wayland { if v.Wayland {
ve |= EWayland *e |= EWayland
} }
if v.X11 { if v.X11 {
ve |= EX11 *e |= EX11
} }
if v.DBus { if v.DBus {
ve |= EDBus *e |= EDBus
} }
if v.PipeWire { if v.PipeWire {
ve |= EPipeWire *e |= EPipeWire
} }
if v.Pulse { if v.Pulse {
ve |= EPulse *e |= EPulse
} }
*e = Enablements(ve)
return nil return nil
} }

View File

@@ -13,7 +13,7 @@ func TestEnablementString(t *testing.T) {
t.Parallel() t.Parallel()
testCases := []struct { testCases := []struct {
flags hst.Enablement flags hst.Enablements
want string want string
}{ }{
{0, "(no enablements)"}, {0, "(no enablements)"},
@@ -59,13 +59,13 @@ func TestEnablements(t *testing.T) {
sData string sData string
}{ }{
{"nil", nil, "null", `{"value":null,"magic":3236757504}`}, {"nil", nil, "null", `{"value":null,"magic":3236757504}`},
{"zero", hst.NewEnablements(0), `{}`, `{"value":{},"magic":3236757504}`}, {"zero", new(hst.Enablements(0)), `{}`, `{"value":{},"magic":3236757504}`},
{"wayland", hst.NewEnablements(hst.EWayland), `{"wayland":true}`, `{"value":{"wayland":true},"magic":3236757504}`}, {"wayland", new(hst.EWayland), `{"wayland":true}`, `{"value":{"wayland":true},"magic":3236757504}`},
{"x11", hst.NewEnablements(hst.EX11), `{"x11":true}`, `{"value":{"x11":true},"magic":3236757504}`}, {"x11", new(hst.EX11), `{"x11":true}`, `{"value":{"x11":true},"magic":3236757504}`},
{"dbus", hst.NewEnablements(hst.EDBus), `{"dbus":true}`, `{"value":{"dbus":true},"magic":3236757504}`}, {"dbus", new(hst.EDBus), `{"dbus":true}`, `{"value":{"dbus":true},"magic":3236757504}`},
{"pipewire", hst.NewEnablements(hst.EPipeWire), `{"pipewire":true}`, `{"value":{"pipewire":true},"magic":3236757504}`}, {"pipewire", new(hst.EPipeWire), `{"pipewire":true}`, `{"value":{"pipewire":true},"magic":3236757504}`},
{"pulse", hst.NewEnablements(hst.EPulse), `{"pulse":true}`, `{"value":{"pulse":true},"magic":3236757504}`}, {"pulse", new(hst.EPulse), `{"pulse":true}`, `{"value":{"pulse":true},"magic":3236757504}`},
{"all", hst.NewEnablements(hst.EM - 1), `{"wayland":true,"x11":true,"dbus":true,"pipewire":true,"pulse":true}`, `{"value":{"wayland":true,"x11":true,"dbus":true,"pipewire":true,"pulse":true},"magic":3236757504}`}, {"all", new(hst.EM - 1), `{"wayland":true,"x11":true,"dbus":true,"pipewire":true,"pulse":true}`, `{"value":{"wayland":true,"x11":true,"dbus":true,"pipewire":true,"pulse":true},"magic":3236757504}`},
} }
for _, tc := range testCases { for _, tc := range testCases {
@@ -137,7 +137,7 @@ func TestEnablements(t *testing.T) {
}) })
t.Run("val", func(t *testing.T) { t.Run("val", func(t *testing.T) {
if got := hst.NewEnablements(hst.EWayland | hst.EPulse).Unwrap(); got != hst.EWayland|hst.EPulse { if got := new(hst.EWayland | hst.EPulse).Unwrap(); got != hst.EWayland|hst.EPulse {
t.Errorf("Unwrap: %v", got) t.Errorf("Unwrap: %v", got)
} }
}) })
@@ -146,9 +146,6 @@ func TestEnablements(t *testing.T) {
t.Run("passthrough", func(t *testing.T) { t.Run("passthrough", func(t *testing.T) {
t.Parallel() t.Parallel()
if _, err := (*hst.Enablements)(nil).MarshalJSON(); !errors.Is(err, syscall.EINVAL) {
t.Errorf("MarshalJSON: error = %v", err)
}
if err := (*hst.Enablements)(nil).UnmarshalJSON(nil); !errors.Is(err, syscall.EINVAL) { if err := (*hst.Enablements)(nil).UnmarshalJSON(nil); !errors.Is(err, syscall.EINVAL) {
t.Errorf("UnmarshalJSON: error = %v", err) t.Errorf("UnmarshalJSON: error = %v", err)
} }

View File

@@ -72,7 +72,7 @@ func Template() *Config {
return &Config{ return &Config{
ID: "org.chromium.Chromium", ID: "org.chromium.Chromium",
Enablements: NewEnablements(EWayland | EDBus | EPipeWire), Enablements: new(EWayland | EDBus | EPipeWire),
SessionBus: &BusConfig{ SessionBus: &BusConfig{
See: nil, See: nil,

View File

@@ -194,7 +194,7 @@ type outcomeStateSys struct {
// Copied from [hst.Config]. Safe for read by outcomeOp.toSystem. // Copied from [hst.Config]. Safe for read by outcomeOp.toSystem.
appId string appId string
// Copied from [hst.Config]. Safe for read by outcomeOp.toSystem. // Copied from [hst.Config]. Safe for read by outcomeOp.toSystem.
et hst.Enablement et hst.Enablements
// Copied from [hst.Config]. Safe for read by spWaylandOp.toSystem only. // Copied from [hst.Config]. Safe for read by spWaylandOp.toSystem only.
directWayland bool directWayland bool

View File

@@ -297,12 +297,12 @@ func (k *outcome) main(msg message.Msg, identifierFd int) {
// accumulate enablements of remaining instances // accumulate enablements of remaining instances
var ( var (
// alive enablement bits // alive enablement bits
rt hst.Enablement rt hst.Enablements
// alive instance count // alive instance count
n int n int
) )
for eh := range entries { for eh := range entries {
var et hst.Enablement var et hst.Enablements
if et, err = eh.Load(nil); err != nil { if et, err = eh.Load(nil); err != nil {
perror(err, "read state header of instance "+eh.ID.String()) perror(err, "read state header of instance "+eh.ID.String())
} else { } else {

View File

@@ -288,7 +288,7 @@ func TestOutcomeRun(t *testing.T) {
}, },
Filter: true, Filter: true,
}, },
Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse), Enablements: new(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse),
Container: &hst.ContainerConfig{ Container: &hst.ContainerConfig{
Filesystem: []hst.FilesystemConfigJSON{ Filesystem: []hst.FilesystemConfigJSON{
@@ -427,7 +427,7 @@ func TestOutcomeRun(t *testing.T) {
DirectPipeWire: true, DirectPipeWire: true,
ID: "org.chromium.Chromium", ID: "org.chromium.Chromium",
Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse), Enablements: new(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse),
Container: &hst.ContainerConfig{ Container: &hst.ContainerConfig{
Env: nil, Env: nil,
Filesystem: []hst.FilesystemConfigJSON{ Filesystem: []hst.FilesystemConfigJSON{

View File

@@ -21,7 +21,7 @@ func TestSpPulseOp(t *testing.T) {
newConfig := func() *hst.Config { newConfig := func() *hst.Config {
config := hst.Template() config := hst.Template()
config.DirectPulse = true config.DirectPulse = true
config.Enablements = hst.NewEnablements(hst.EPulse) config.Enablements = new(hst.EPulse)
return config return config
} }

View File

@@ -24,7 +24,7 @@ func entryEncode(w io.Writer, s *hst.State) error {
} }
// entryDecodeHeader calls entryReadHeader, returning [hst.AppError] for a non-nil error. // entryDecodeHeader calls entryReadHeader, returning [hst.AppError] for a non-nil error.
func entryDecodeHeader(r io.Reader) (hst.Enablement, error) { func entryDecodeHeader(r io.Reader) (hst.Enablements, error) {
if et, err := entryReadHeader(r); err != nil { if et, err := entryReadHeader(r); err != nil {
return 0, &hst.AppError{Step: "decode state header", Err: err} return 0, &hst.AppError{Step: "decode state header", Err: err}
} else { } else {
@@ -32,11 +32,11 @@ func entryDecodeHeader(r io.Reader) (hst.Enablement, error) {
} }
} }
// entryDecode decodes [hst.State] from [io.Reader] and stores the result in the value pointed to by p. // entryDecode decodes [hst.State] from [io.Reader] and stores the result in the
// entryDecode validates the embedded [hst.Config] value. // value pointed to by p. entryDecode validates the embedded [hst.Config] value.
// //
// A non-nil error returned by entryDecode is of type [hst.AppError]. // A non-nil error returned by entryDecode is of type [hst.AppError].
func entryDecode(r io.Reader, p *hst.State) (hst.Enablement, error) { func entryDecode(r io.Reader, p *hst.State) (hst.Enablements, error) {
if et, err := entryDecodeHeader(r); err != nil { if et, err := entryDecodeHeader(r); err != nil {
return et, err return et, err
} else if err = gob.NewDecoder(r).Decode(&p); err != nil { } else if err = gob.NewDecoder(r).Decode(&p); err != nil {
@@ -45,7 +45,10 @@ func entryDecode(r io.Reader, p *hst.State) (hst.Enablement, error) {
return et, err return et, err
} else if p.Enablements.Unwrap() != et { } else if p.Enablements.Unwrap() != et {
return et, &hst.AppError{Step: "validate state enablement", Err: os.ErrInvalid, return et, &hst.AppError{Step: "validate state enablement", Err: os.ErrInvalid,
Msg: fmt.Sprintf("state entry %s has unexpected enablement byte %#x, %#x", p.ID.String(), byte(p.Enablements.Unwrap()), byte(et))} Msg: fmt.Sprintf(
"state entry %s has unexpected enablement byte %#x, %#x",
p.ID.String(), byte(p.Enablements.Unwrap()), byte(et),
)}
} else { } else {
return et, nil return et, nil
} }

View File

@@ -14,22 +14,25 @@ import (
const ( const (
// entryHeaderMagic are magic bytes at the beginning of the state entry file. // entryHeaderMagic are magic bytes at the beginning of the state entry file.
entryHeaderMagic = "\x00\xff\xca\xfe" entryHeaderMagic = "\x00\xff\xca\xfe"
// entryHeaderRevision follows entryHeaderMagic and is incremented for revisions of the format. // entryHeaderRevision follows entryHeaderMagic and is incremented for
// revisions of the format.
entryHeaderRevision = "\x00\x00" entryHeaderRevision = "\x00\x00"
// entryHeaderSize is the fixed size of the header in bytes, including the enablement byte and its complement. // entryHeaderSize is the fixed size of the header in bytes, including the
// enablement byte and its complement.
entryHeaderSize = len(entryHeaderMagic+entryHeaderRevision) + 2 entryHeaderSize = len(entryHeaderMagic+entryHeaderRevision) + 2
) )
// entryHeaderEncode encodes a state entry header for a [hst.Enablement] byte. // entryHeaderEncode encodes a state entry header for a [hst.Enablements] byte.
func entryHeaderEncode(et hst.Enablement) *[entryHeaderSize]byte { func entryHeaderEncode(et hst.Enablements) *[entryHeaderSize]byte {
data := [entryHeaderSize]byte([]byte( data := [entryHeaderSize]byte([]byte(
entryHeaderMagic + entryHeaderRevision + string([]hst.Enablement{et, ^et}), entryHeaderMagic + entryHeaderRevision + string([]hst.Enablements{et, ^et}),
)) ))
return &data return &data
} }
// entryHeaderDecode validates a state entry header and returns the [hst.Enablement] byte. // entryHeaderDecode validates a state entry header and returns the
func entryHeaderDecode(data *[entryHeaderSize]byte) (hst.Enablement, error) { // [hst.Enablements] byte.
func entryHeaderDecode(data *[entryHeaderSize]byte) (hst.Enablements, error) {
if magic := data[:len(entryHeaderMagic)]; string(magic) != entryHeaderMagic { if magic := data[:len(entryHeaderMagic)]; string(magic) != entryHeaderMagic {
return 0, errors.New("invalid header " + hex.EncodeToString(magic)) return 0, errors.New("invalid header " + hex.EncodeToString(magic))
} }
@@ -41,7 +44,7 @@ func entryHeaderDecode(data *[entryHeaderSize]byte) (hst.Enablement, error) {
if et != ^data[len(entryHeaderMagic+entryHeaderRevision)+1] { if et != ^data[len(entryHeaderMagic+entryHeaderRevision)+1] {
return 0, errors.New("header enablement value is inconsistent") return 0, errors.New("header enablement value is inconsistent")
} }
return hst.Enablement(et), nil return hst.Enablements(et), nil
} }
// EntrySizeError is returned for a file too small to hold a state entry header. // EntrySizeError is returned for a file too small to hold a state entry header.
@@ -68,8 +71,8 @@ func entryCheckFile(fi os.FileInfo) error {
return nil return nil
} }
// entryReadHeader reads [hst.Enablement] from an [io.Reader]. // entryReadHeader reads [hst.Enablements] from an [io.Reader].
func entryReadHeader(r io.Reader) (hst.Enablement, error) { func entryReadHeader(r io.Reader) (hst.Enablements, error) {
var data [entryHeaderSize]byte var data [entryHeaderSize]byte
if n, err := r.Read(data[:]); err != nil { if n, err := r.Read(data[:]); err != nil {
return 0, err return 0, err
@@ -79,8 +82,8 @@ func entryReadHeader(r io.Reader) (hst.Enablement, error) {
return entryHeaderDecode(&data) return entryHeaderDecode(&data)
} }
// entryWriteHeader writes [hst.Enablement] header to an [io.Writer]. // entryWriteHeader writes [hst.Enablements] header to an [io.Writer].
func entryWriteHeader(w io.Writer, et hst.Enablement) error { func entryWriteHeader(w io.Writer, et hst.Enablements) error {
_, err := w.Write(entryHeaderEncode(et)[:]) _, err := w.Write(entryHeaderEncode(et)[:])
return err return err
} }

View File

@@ -20,7 +20,7 @@ func TestEntryHeader(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
data [entryHeaderSize]byte data [entryHeaderSize]byte
et hst.Enablement et hst.Enablements
err error err error
}{ }{
{"complement mismatch", [entryHeaderSize]byte{0x00, 0xff, 0xca, 0xfe, 0x00, 0x00, {"complement mismatch", [entryHeaderSize]byte{0x00, 0xff, 0xca, 0xfe, 0x00, 0x00,

View File

@@ -14,6 +14,7 @@ import (
) )
// EntryHandle is a handle on a state entry retrieved from a [Handle]. // EntryHandle is a handle on a state entry retrieved from a [Handle].
//
// Must only be used while its parent [Handle.Lock] is held. // Must only be used while its parent [Handle.Lock] is held.
type EntryHandle struct { type EntryHandle struct {
// Error returned while decoding pathname. // Error returned while decoding pathname.
@@ -27,6 +28,7 @@ type EntryHandle struct {
} }
// open opens the underlying state entry file. // open opens the underlying state entry file.
//
// A non-nil error returned by open is of type [hst.AppError]. // A non-nil error returned by open is of type [hst.AppError].
func (eh *EntryHandle) open(flag int, perm os.FileMode) (*os.File, error) { func (eh *EntryHandle) open(flag int, perm os.FileMode) (*os.File, error) {
if eh.DecodeErr != nil { if eh.DecodeErr != nil {
@@ -41,6 +43,7 @@ func (eh *EntryHandle) open(flag int, perm os.FileMode) (*os.File, error) {
} }
// Destroy removes the underlying state entry. // Destroy removes the underlying state entry.
//
// A non-nil error returned by Destroy is of type [hst.AppError]. // A non-nil error returned by Destroy is of type [hst.AppError].
func (eh *EntryHandle) Destroy() error { func (eh *EntryHandle) Destroy() error {
// destroy does not go through open // destroy does not go through open
@@ -55,8 +58,10 @@ func (eh *EntryHandle) Destroy() error {
} }
// save encodes [hst.State] and writes it to the underlying file. // save encodes [hst.State] and writes it to the underlying file.
//
// An error is returned if a file already exists with the same identifier. // An error is returned if a file already exists with the same identifier.
// save does not validate the embedded [hst.Config]. // save does not validate the embedded [hst.Config].
//
// A non-nil error returned by save is of type [hst.AppError]. // A non-nil error returned by save is of type [hst.AppError].
func (eh *EntryHandle) save(state *hst.State) error { func (eh *EntryHandle) save(state *hst.State) error {
f, err := eh.open(os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600) f, err := eh.open(os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
@@ -71,17 +76,19 @@ func (eh *EntryHandle) save(state *hst.State) error {
return err return err
} }
// Load loads and validates the state entry header, and returns the [hst.Enablement] byte. // Load loads and validates the state entry header, and returns the
// for a non-nil v, the full state payload is decoded and stored in the value pointed to by v. // [hst.Enablements] byte. For a non-nil v, the full state payload is decoded
// Load validates the embedded [hst.Config] value. // and stored in the value pointed to by v.
// A non-nil error returned by Load is of type [hst.AppError]. //
func (eh *EntryHandle) Load(v *hst.State) (hst.Enablement, error) { // Load validates the embedded [hst.Config] value. A non-nil error returned by
// Load is of type [hst.AppError].
func (eh *EntryHandle) Load(v *hst.State) (hst.Enablements, error) {
f, err := eh.open(os.O_RDONLY, 0) f, err := eh.open(os.O_RDONLY, 0)
if err != nil { if err != nil {
return 0, err return 0, err
} }
var et hst.Enablement var et hst.Enablements
if v != nil { if v != nil {
et, err = entryDecode(f, v) et, err = entryDecode(f, v)
if err == nil && v.ID != eh.ID { if err == nil && v.ID != eh.ID {
@@ -99,6 +106,7 @@ func (eh *EntryHandle) Load(v *hst.State) (hst.Enablement, error) {
} }
// Handle is a handle on a [Store] segment. // Handle is a handle on a [Store] segment.
//
// Initialised by [Store.Handle]. // Initialised by [Store.Handle].
type Handle struct { type Handle struct {
// Identity of instances tracked by this segment. // Identity of instances tracked by this segment.
@@ -113,8 +121,9 @@ type Handle struct {
mu sync.Mutex mu sync.Mutex
} }
// Lock attempts to acquire a lock on [Handle]. // Lock attempts to acquire a lock on [Handle]. If successful, Lock returns a
// If successful, Lock returns a non-nil unlock function. // non-nil unlock function.
//
// A non-nil error returned by Lock is of type [hst.AppError]. // A non-nil error returned by Lock is of type [hst.AppError].
func (h *Handle) Lock() (unlock func(), err error) { func (h *Handle) Lock() (unlock func(), err error) {
if unlock, err = h.fileMu.Lock(); err != nil { if unlock, err = h.fileMu.Lock(); err != nil {
@@ -123,20 +132,24 @@ func (h *Handle) Lock() (unlock func(), err error) {
return return
} }
// Save attempts to save [hst.State] as a segment entry, and returns its [EntryHandle]. // Save attempts to save [hst.State] as a segment entry, and returns its
// Must be called while holding [Handle.Lock]. // [EntryHandle]. Must be called while holding [Handle.Lock].
//
// An error is returned if an entry already exists with the same identifier. // An error is returned if an entry already exists with the same identifier.
// Save does not validate the embedded [hst.Config]. // Save does not validate the embedded [hst.Config].
//
// A non-nil error returned by Save is of type [hst.AppError]. // A non-nil error returned by Save is of type [hst.AppError].
func (h *Handle) Save(state *hst.State) (*EntryHandle, error) { func (h *Handle) Save(state *hst.State) (*EntryHandle, error) {
eh := EntryHandle{nil, h.Path.Append(state.ID.String()), state.ID} eh := EntryHandle{nil, h.Path.Append(state.ID.String()), state.ID}
return &eh, eh.save(state) return &eh, eh.save(state)
} }
// Entries returns an iterator over all [EntryHandle] held in this segment. // Entries returns an iterator over all [EntryHandle] held in this segment. Must
// Must be called while holding [Handle.Lock]. // be called while holding [Handle.Lock].
// A non-nil error attached to a [EntryHandle] indicates a malformed identifier and is of type [hst.AppError]. //
// A non-nil error returned by Entries is of type [hst.AppError]. // A non-nil error attached to a [EntryHandle] indicates a malformed identifier
// and is of type [hst.AppError]. A non-nil error returned by Entries is of type
// [hst.AppError].
func (h *Handle) Entries() (iter.Seq[*EntryHandle], int, error) { func (h *Handle) Entries() (iter.Seq[*EntryHandle], int, error) {
// for error reporting // for error reporting
const step = "read store segment entries" const step = "read store segment entries"

View File

@@ -21,7 +21,7 @@ import (
// Made available here for direct validation of state entry files. // Made available here for direct validation of state entry files.
// //
//go:linkname entryDecode hakurei.app/internal/store.entryDecode //go:linkname entryDecode hakurei.app/internal/store.entryDecode
func entryDecode(r io.Reader, p *hst.State) (hst.Enablement, error) func entryDecode(r io.Reader, p *hst.State) (hst.Enablements, error)
// Made available here for direct access to known segment handles. // Made available here for direct access to known segment handles.
// //

View File

@@ -17,20 +17,21 @@ func (sys *I) UpdatePerm(path *check.Absolute, perms ...acl.Perm) *I {
return sys return sys
} }
// UpdatePermType maintains [acl.Perms] on a file until its [Enablement] is no longer satisfied. // UpdatePermType maintains [acl.Perms] on a file until its [hst.Enablements] is
func (sys *I) UpdatePermType(et hst.Enablement, path *check.Absolute, perms ...acl.Perm) *I { // no longer satisfied.
func (sys *I) UpdatePermType(et hst.Enablements, path *check.Absolute, perms ...acl.Perm) *I {
sys.ops = append(sys.ops, &aclUpdateOp{et, path.String(), perms}) sys.ops = append(sys.ops, &aclUpdateOp{et, path.String(), perms})
return sys return sys
} }
// aclUpdateOp implements [I.UpdatePermType]. // aclUpdateOp implements [I.UpdatePermType].
type aclUpdateOp struct { type aclUpdateOp struct {
et hst.Enablement et hst.Enablements
path string path string
perms acl.Perms perms acl.Perms
} }
func (a *aclUpdateOp) Type() hst.Enablement { return a.et } func (a *aclUpdateOp) Type() hst.Enablements { return a.et }
func (a *aclUpdateOp) apply(sys *I) error { func (a *aclUpdateOp) apply(sys *I) error {
sys.msg.Verbose("applying ACL", a) sys.msg.Verbose("applying ACL", a)

View File

@@ -31,7 +31,9 @@ func (sys *I) MustProxyDBus(
} }
} }
// ProxyDBus finalises configuration ahead of time and starts xdg-dbus-proxy via [dbus] and terminates it on revert. // ProxyDBus finalises configuration ahead of time and starts xdg-dbus-proxy via
// [dbus] and terminates it on revert.
//
// This [Op] is always [Process] scoped. // This [Op] is always [Process] scoped.
func (sys *I) ProxyDBus( func (sys *I) ProxyDBus(
session, system *hst.BusConfig, session, system *hst.BusConfig,
@@ -84,7 +86,7 @@ type dbusProxyOp struct {
system bool system bool
} }
func (d *dbusProxyOp) Type() hst.Enablement { return Process } func (d *dbusProxyOp) Type() hst.Enablements { return Process }
func (d *dbusProxyOp) apply(sys *I) error { func (d *dbusProxyOp) apply(sys *I) error {
sys.msg.Verbosef("session bus proxy on %q for upstream %q", d.final.Session[1], d.final.Session[0]) sys.msg.Verbosef("session bus proxy on %q for upstream %q", d.final.Session[1], d.final.Session[0])

View File

@@ -21,12 +21,16 @@ type osFile interface {
fs.File fs.File
} }
// syscallDispatcher provides methods that make state-dependent system calls as part of their behaviour. // syscallDispatcher provides methods that make state-dependent system calls as
// part of their behaviour.
//
// syscallDispatcher is embedded in [I], so all methods must be unexported. // syscallDispatcher is embedded in [I], so all methods must be unexported.
type syscallDispatcher interface { type syscallDispatcher interface {
// new starts a goroutine with a new instance of syscallDispatcher. // new starts a goroutine with a new instance of syscallDispatcher.
// A syscallDispatcher must never be used in any goroutine other than the one owning it, //
// just synchronising access is not enough, as this is for test instrumentation. // A syscallDispatcher must never be used in any goroutine other than the
// one owning it, just synchronising access is not enough, as this is for
// test instrumentation.
new(f func(k syscallDispatcher)) new(f func(k syscallDispatcher))
// stat provides os.Stat. // stat provides os.Stat.

View File

@@ -27,7 +27,7 @@ func call(name string, args stub.ExpectArgs, ret any, err error) stub.Call {
type opBehaviourTestCase struct { type opBehaviourTestCase struct {
name string name string
uid int uid int
ec hst.Enablement ec hst.Enablements
op Op op Op
apply []stub.Call apply []stub.Call
@@ -158,7 +158,7 @@ type opMetaTestCase struct {
name string name string
op Op op Op
wantType hst.Enablement wantType hst.Enablements
wantPath string wantPath string
wantString string wantString string
} }

View File

@@ -12,19 +12,19 @@ func (sys *I) Link(oldname, newname *check.Absolute) *I {
return sys.LinkFileType(Process, oldname, newname) return sys.LinkFileType(Process, oldname, newname)
} }
// LinkFileType maintains a hardlink until its [Enablement] is no longer satisfied. // LinkFileType maintains a hardlink until its [hst.Enablements] is no longer satisfied.
func (sys *I) LinkFileType(et hst.Enablement, oldname, newname *check.Absolute) *I { func (sys *I) LinkFileType(et hst.Enablements, oldname, newname *check.Absolute) *I {
sys.ops = append(sys.ops, &hardlinkOp{et, newname.String(), oldname.String()}) sys.ops = append(sys.ops, &hardlinkOp{et, newname.String(), oldname.String()})
return sys return sys
} }
// hardlinkOp implements [I.LinkFileType]. // hardlinkOp implements [I.LinkFileType].
type hardlinkOp struct { type hardlinkOp struct {
et hst.Enablement et hst.Enablements
dst, src string dst, src string
} }
func (l *hardlinkOp) Type() hst.Enablement { return l.et } func (l *hardlinkOp) Type() hst.Enablements { return l.et }
func (l *hardlinkOp) apply(sys *I) error { func (l *hardlinkOp) apply(sys *I) error {
sys.msg.Verbose("linking", l) sys.msg.Verbose("linking", l)

View File

@@ -15,21 +15,22 @@ func (sys *I) Ensure(name *check.Absolute, perm os.FileMode) *I {
return sys return sys
} }
// Ephemeral ensures the existence of a directory until its [Enablement] is no longer satisfied. // Ephemeral ensures the existence of a directory until its [hst.Enablements] is
func (sys *I) Ephemeral(et hst.Enablement, name *check.Absolute, perm os.FileMode) *I { // no longer satisfied.
func (sys *I) Ephemeral(et hst.Enablements, name *check.Absolute, perm os.FileMode) *I {
sys.ops = append(sys.ops, &mkdirOp{et, name.String(), perm, true}) sys.ops = append(sys.ops, &mkdirOp{et, name.String(), perm, true})
return sys return sys
} }
// mkdirOp implements [I.Ensure] and [I.Ephemeral]. // mkdirOp implements [I.Ensure] and [I.Ephemeral].
type mkdirOp struct { type mkdirOp struct {
et hst.Enablement et hst.Enablements
path string path string
perm os.FileMode perm os.FileMode
ephemeral bool ephemeral bool
} }
func (m *mkdirOp) Type() hst.Enablement { return m.et } func (m *mkdirOp) Type() hst.Enablements { return m.et }
func (m *mkdirOp) apply(sys *I) error { func (m *mkdirOp) apply(sys *I) error {
sys.msg.Verbose("ensuring directory", m) sys.msg.Verbose("ensuring directory", m)

View File

@@ -12,8 +12,9 @@ import (
) )
// PipeWire maintains a pipewire socket with SecurityContext attached via [pipewire]. // PipeWire maintains a pipewire socket with SecurityContext attached via [pipewire].
// The socket stops accepting connections once the pipe referred to by sync is closed. //
// The socket is pathname only and is destroyed on revert. // The socket stops accepting connections once the pipe referred to by sync is
// closed. The socket is pathname only and is destroyed on revert.
func (sys *I) PipeWire(dst *check.Absolute, appID, instanceID string) *I { func (sys *I) PipeWire(dst *check.Absolute, appID, instanceID string) *I {
sys.ops = append(sys.ops, &pipewireOp{nil, dst, appID, instanceID}) sys.ops = append(sys.ops, &pipewireOp{nil, dst, appID, instanceID})
return sys return sys
@@ -27,7 +28,7 @@ type pipewireOp struct {
appID, instanceID string appID, instanceID string
} }
func (p *pipewireOp) Type() hst.Enablement { return Process } func (p *pipewireOp) Type() hst.Enablements { return Process }
func (p *pipewireOp) apply(sys *I) (err error) { func (p *pipewireOp) apply(sys *I) (err error) {
var ctx *pipewire.Context var ctx *pipewire.Context

View File

@@ -20,21 +20,21 @@ const (
) )
// Criteria specifies types of Op to revert. // Criteria specifies types of Op to revert.
type Criteria hst.Enablement type Criteria hst.Enablements
func (ec *Criteria) hasType(t hst.Enablement) bool { func (ec *Criteria) hasType(t hst.Enablements) bool {
// nil criteria: revert everything except User // nil criteria: revert everything except User
if ec == nil { if ec == nil {
return t != User return t != User
} }
return hst.Enablement(*ec)&t != 0 return hst.Enablements(*ec)&t != 0
} }
// Op is a reversible system operation. // Op is a reversible system operation.
type Op interface { type Op interface {
// Type returns [Op]'s enablement type, for matching a revert criteria. // Type returns [Op]'s enablement type, for matching a revert criteria.
Type() hst.Enablement Type() hst.Enablements
apply(sys *I) error apply(sys *I) error
revert(sys *I, ec *Criteria) error revert(sys *I, ec *Criteria) error
@@ -44,8 +44,8 @@ type Op interface {
String() string String() string
} }
// TypeString extends [hst.Enablement.String] to support [User] and [Process]. // TypeString extends [hst.Enablements] to support [User] and [Process].
func TypeString(e hst.Enablement) string { func TypeString(e hst.Enablements) string {
switch e { switch e {
case User: case User:
return "user" return "user"
@@ -110,7 +110,9 @@ func (sys *I) Equal(target *I) bool {
return true return true
} }
// Commit applies all [Op] held by [I] and reverts all successful [Op] on first error encountered. // Commit applies all [Op] held by [I] and reverts all successful [Op] on first
// error encountered.
//
// Commit must not be called more than once. // Commit must not be called more than once.
func (sys *I) Commit() error { func (sys *I) Commit() error {
if sys.committed { if sys.committed {

View File

@@ -20,7 +20,7 @@ func TestCriteria(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
ec, t hst.Enablement ec, t hst.Enablements
want bool want bool
}{ }{
{"nil", 0xff, hst.EWayland, true}, {"nil", 0xff, hst.EWayland, true},
@@ -47,7 +47,7 @@ func TestTypeString(t *testing.T) {
t.Parallel() t.Parallel()
testCases := []struct { testCases := []struct {
e hst.Enablement e hst.Enablements
want string want string
}{ }{
{hst.EWayland, hst.EWayland.String()}, {hst.EWayland, hst.EWayland.String()},
@@ -190,7 +190,7 @@ func TestCommitRevert(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
f func(sys *I) f func(sys *I)
ec hst.Enablement ec hst.Enablements
commit []stub.Call commit []stub.Call
wantErrCommit error wantErrCommit error

View File

@@ -11,8 +11,9 @@ import (
) )
// Wayland maintains a wayland socket with security-context-v1 attached via [wayland]. // Wayland maintains a wayland socket with security-context-v1 attached via [wayland].
// The socket stops accepting connections once the pipe referred to by sync is closed. //
// The socket is pathname only and is destroyed on revert. // The socket stops accepting connections once the pipe referred to by sync is
// closed. The socket is pathname only and is destroyed on revert.
func (sys *I) Wayland(dst, src *check.Absolute, appID, instanceID string) *I { func (sys *I) Wayland(dst, src *check.Absolute, appID, instanceID string) *I {
sys.ops = append(sys.ops, &waylandOp{nil, sys.ops = append(sys.ops, &waylandOp{nil,
dst, src, appID, instanceID}) dst, src, appID, instanceID})
@@ -26,7 +27,7 @@ type waylandOp struct {
appID, instanceID string appID, instanceID string
} }
func (w *waylandOp) Type() hst.Enablement { return Process } func (w *waylandOp) Type() hst.Enablements { return Process }
func (w *waylandOp) apply(sys *I) (err error) { func (w *waylandOp) apply(sys *I) (err error) {
if w.ctx, err = sys.waylandNew(w.src, w.dst, w.appID, w.instanceID); err != nil { if w.ctx, err = sys.waylandNew(w.src, w.dst, w.appID, w.instanceID); err != nil {

View File

@@ -5,7 +5,8 @@ import (
"hakurei.app/internal/xcb" "hakurei.app/internal/xcb"
) )
// ChangeHosts inserts the target user into X11 hosts and deletes it once its [Enablement] is no longer satisfied. // ChangeHosts inserts the target user into X11 hosts and deletes it once its
// [hst.Enablements] is no longer satisfied.
func (sys *I) ChangeHosts(username string) *I { func (sys *I) ChangeHosts(username string) *I {
sys.ops = append(sys.ops, xhostOp(username)) sys.ops = append(sys.ops, xhostOp(username))
return sys return sys
@@ -14,7 +15,7 @@ func (sys *I) ChangeHosts(username string) *I {
// xhostOp implements [I.ChangeHosts]. // xhostOp implements [I.ChangeHosts].
type xhostOp string type xhostOp string
func (x xhostOp) Type() hst.Enablement { return hst.EX11 } func (x xhostOp) Type() hst.Enablements { return hst.EX11 }
func (x xhostOp) apply(sys *I) error { func (x xhostOp) apply(sys *I) error {
sys.msg.Verbosef("inserting entry %s to X11", x) sys.msg.Verbosef("inserting entry %s to X11", x)