internal/uevent: decode uevent messages
All checks were successful
Test / Create distribution (push) Successful in 1m14s
Test / Sandbox (push) Successful in 3m3s
Test / Hakurei (push) Successful in 4m13s
Test / ShareFS (push) Successful in 4m17s
Test / Sandbox (race detector) (push) Successful in 5m35s
Test / Hakurei (race detector) (push) Successful in 6m38s
Test / Flake checks (push) Successful in 1m24s
All checks were successful
Test / Create distribution (push) Successful in 1m14s
Test / Sandbox (push) Successful in 3m3s
Test / Hakurei (push) Successful in 4m13s
Test / ShareFS (push) Successful in 4m17s
Test / Sandbox (race detector) (push) Successful in 5m35s
Test / Hakurei (race detector) (push) Successful in 6m38s
Test / Flake checks (push) Successful in 1m24s
The wire format and behaviour is entirely undocumented. This is implemented by reading lib/kobject_uevent.c, with testdata collected from the internal/rosa kernel. Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
137
internal/uevent/message.go
Normal file
137
internal/uevent/message.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package uevent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A Message represents a kernel message to userspace.
|
||||
type Message struct {
|
||||
// alloc_uevent_skb: action_string
|
||||
Action KobjectAction `json:"action"`
|
||||
// alloc_uevent_skb: devpath
|
||||
DevPath string `json:"devpath"`
|
||||
// add_uevent_var: key value strings
|
||||
Env []string `json:"env"`
|
||||
}
|
||||
|
||||
// String returns a multiline user-facing string representation of [Message].
|
||||
func (msg *Message) String() string {
|
||||
var buf strings.Builder
|
||||
buf.WriteString(msg.Action.String() + " event")
|
||||
if msg.DevPath != "" {
|
||||
buf.WriteString(" on " + msg.DevPath)
|
||||
}
|
||||
buf.WriteString(":")
|
||||
for _, s := range msg.Env {
|
||||
buf.WriteString("\n" + s)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
var (
|
||||
// zero is a single pre-allocated NUL character.
|
||||
zero = []byte{0}
|
||||
|
||||
// sepHeader is the separator in a [Message] header.
|
||||
sepHeader = []byte{'@'}
|
||||
)
|
||||
|
||||
func (msg *Message) AppendBinary(b []byte) (_ []byte, err error) {
|
||||
if b, err = msg.Action.AppendText(b); err != nil {
|
||||
return
|
||||
}
|
||||
b = append(b, sepHeader...)
|
||||
b = append(b, msg.DevPath...)
|
||||
b = append(b, zero...)
|
||||
for _, s := range msg.Env {
|
||||
b = append(b, s...)
|
||||
b = append(b, zero...)
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (msg *Message) MarshalBinary() ([]byte, error) {
|
||||
return msg.AppendBinary(nil)
|
||||
}
|
||||
|
||||
// MissingHeaderError is an invalid representation of [Message] which is missing
|
||||
// its header added by alloc_uevent_skb.
|
||||
type MissingHeaderError string
|
||||
|
||||
var _ Recoverable = MissingHeaderError("")
|
||||
|
||||
func (MissingHeaderError) recoverable() {}
|
||||
func (e MissingHeaderError) Error() string {
|
||||
return "message " + strconv.Quote(string(e)) + " has no header"
|
||||
}
|
||||
|
||||
// MessageError describes a malformed representation of [Message].
|
||||
type MessageError struct {
|
||||
// Full offending data.
|
||||
Data string `json:"data"`
|
||||
// Offending section.
|
||||
Section string `json:"section"`
|
||||
// Part of header in Section.
|
||||
Kind int `json:"kind"`
|
||||
}
|
||||
|
||||
var _ Recoverable = new(MessageError)
|
||||
var _ Nontrivial = new(MessageError)
|
||||
|
||||
const (
|
||||
// MErrorKindHeaderSep denotes a message header missing its separator.
|
||||
MErrorKindHeaderSep = iota
|
||||
// MErrorKindFinalNUL denotes a message body missing its final NUL terminator.
|
||||
MErrorKindFinalNUL
|
||||
)
|
||||
|
||||
func (*MessageError) recoverable() {}
|
||||
func (*MessageError) nontrivial() {}
|
||||
func (e *MessageError) Error() string {
|
||||
switch e.Kind {
|
||||
case MErrorKindHeaderSep:
|
||||
return "header " + strconv.Quote(e.Section) + " missing separator"
|
||||
case MErrorKindFinalNUL:
|
||||
return "entry " + strconv.Quote(e.Section) + " missing NUL"
|
||||
|
||||
default:
|
||||
return "section " + strconv.Quote(e.Section) + " is invalid"
|
||||
}
|
||||
}
|
||||
|
||||
func (msg *Message) UnmarshalBinary(data []byte) error {
|
||||
header, body, ok := bytes.Cut(data, zero)
|
||||
if !ok {
|
||||
return MissingHeaderError(data)
|
||||
}
|
||||
|
||||
action_string, devpath, ok := bytes.Cut(header, sepHeader)
|
||||
if !ok {
|
||||
return &MessageError{string(data), string(header), MErrorKindHeaderSep}
|
||||
}
|
||||
|
||||
if err := msg.Action.UnmarshalText(action_string); err != nil {
|
||||
return err
|
||||
}
|
||||
msg.DevPath = string(devpath)
|
||||
|
||||
if len(body) == 0 {
|
||||
msg.Env = nil
|
||||
return nil
|
||||
}
|
||||
msg.Env = make([]string, 0, bytes.Count(body, zero))
|
||||
|
||||
var s []byte
|
||||
for len(body) != 0 {
|
||||
var r []byte
|
||||
s, r, ok = bytes.Cut(body, zero)
|
||||
if !ok {
|
||||
return &MessageError{string(data), string(body), MErrorKindFinalNUL}
|
||||
}
|
||||
body = r
|
||||
msg.Env = append(msg.Env, string(s))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
126
internal/uevent/message_test.go
Normal file
126
internal/uevent/message_test.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package uevent_test
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/internal/uevent"
|
||||
)
|
||||
|
||||
func TestMessage(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
v uevent.Message
|
||||
want string
|
||||
wantErr error
|
||||
wantErrE error
|
||||
|
||||
s string
|
||||
}{
|
||||
{"sample virtio-sound-pci add", uevent.Message{
|
||||
Action: uevent.KOBJ_ADD,
|
||||
DevPath: "/devices/pci0000:00/0000:00:04.0/virtio1",
|
||||
Env: []string{
|
||||
"ACTION=add",
|
||||
"DEVPATH=/devices/pci0000:00/0000:00:04.0/virtio1",
|
||||
"SUBSYSTEM=virtio",
|
||||
"MODALIAS=virtio:d00000019v00001AF4",
|
||||
"SEQNUM=779",
|
||||
},
|
||||
}, "add@/devices/pci0000:00/0000:00:04.0/virtio1\x00" +
|
||||
"ACTION=add\x00" +
|
||||
"DEVPATH=/devices/pci0000:00/0000:00:04.0/virtio1\x00" +
|
||||
"SUBSYSTEM=virtio\x00" +
|
||||
"MODALIAS=virtio:d00000019v00001AF4\x00" +
|
||||
"SEQNUM=779\x00",
|
||||
nil, nil, `add event on /devices/pci0000:00/0000:00:04.0/virtio1:
|
||||
ACTION=add
|
||||
DEVPATH=/devices/pci0000:00/0000:00:04.0/virtio1
|
||||
SUBSYSTEM=virtio
|
||||
MODALIAS=virtio:d00000019v00001AF4
|
||||
SEQNUM=779`},
|
||||
|
||||
{"sample virtio-sound-pci bind", uevent.Message{
|
||||
Action: uevent.KOBJ_BIND,
|
||||
DevPath: "/devices/pci0000:00/0000:00:04.0",
|
||||
Env: []string{
|
||||
"ACTION=bind",
|
||||
"DEVPATH=/devices/pci0000:00/0000:00:04.0",
|
||||
"SUBSYSTEM=pci",
|
||||
"DRIVER=virtio-pci",
|
||||
"PCI_CLASS=40100",
|
||||
"PCI_ID=1AF4:1059",
|
||||
"PCI_SUBSYS_ID=1AF4:1100",
|
||||
"PCI_SLOT_NAME=0000:00:04.0",
|
||||
"MODALIAS=pci:v00001AF4d00001059sv00001AF4sd00001100bc04sc01i00",
|
||||
"SEQNUM=780",
|
||||
},
|
||||
}, "bind@/devices/pci0000:00/0000:00:04.0\x00" +
|
||||
"ACTION=bind\x00" +
|
||||
"DEVPATH=/devices/pci0000:00/0000:00:04.0\x00" +
|
||||
"SUBSYSTEM=pci\x00" +
|
||||
"DRIVER=virtio-pci\x00" +
|
||||
"PCI_CLASS=40100\x00" +
|
||||
"PCI_ID=1AF4:1059\x00" +
|
||||
"PCI_SUBSYS_ID=1AF4:1100\x00" +
|
||||
"PCI_SLOT_NAME=0000:00:04.0\x00" +
|
||||
"MODALIAS=pci:v00001AF4d00001059sv00001AF4sd00001100bc04sc01i00\x00" +
|
||||
"SEQNUM=780\x00", nil, nil, `bind event on /devices/pci0000:00/0000:00:04.0:
|
||||
ACTION=bind
|
||||
DEVPATH=/devices/pci0000:00/0000:00:04.0
|
||||
SUBSYSTEM=pci
|
||||
DRIVER=virtio-pci
|
||||
PCI_CLASS=40100
|
||||
PCI_ID=1AF4:1059
|
||||
PCI_SUBSYS_ID=1AF4:1100
|
||||
PCI_SLOT_NAME=0000:00:04.0
|
||||
MODALIAS=pci:v00001AF4d00001059sv00001AF4sd00001100bc04sc01i00
|
||||
SEQNUM=780`},
|
||||
|
||||
{"zero devpath env", uevent.Message{
|
||||
Action: uevent.KOBJ_MOVE,
|
||||
}, "move@\x00", nil, nil, "move event:"},
|
||||
|
||||
{"d final NUL e bad action", uevent.Message{
|
||||
Action: 0xbad,
|
||||
}, "move@\x00truncated", &uevent.MessageError{
|
||||
Data: "move@\x00truncated",
|
||||
Section: "truncated",
|
||||
Kind: uevent.MErrorKindFinalNUL,
|
||||
}, syscall.EINVAL, "unsupported kobject_action 2989 event:"},
|
||||
|
||||
{"bad action", uevent.Message{
|
||||
Action: 0xbad,
|
||||
}, "nonexistent@\x00", uevent.UnsupportedActionError(
|
||||
"nonexistent",
|
||||
), syscall.EINVAL, "unsupported kobject_action 2989 event:"},
|
||||
|
||||
{"d header sep e bad action", uevent.Message{
|
||||
Action: 0xbad,
|
||||
}, "move\x00", &uevent.MessageError{
|
||||
Data: "move\x00",
|
||||
Section: "move",
|
||||
Kind: uevent.MErrorKindHeaderSep,
|
||||
}, syscall.EINVAL, "unsupported kobject_action 2989 event:"},
|
||||
|
||||
{"d missing header e bad action", uevent.Message{
|
||||
Action: 0xbad,
|
||||
}, "move", uevent.MissingHeaderError(
|
||||
"move",
|
||||
), syscall.EINVAL, "unsupported kobject_action 2989 event:"},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
adeB(t, "", tc.v, tc.want, tc.wantErr, tc.wantErrE)
|
||||
t.Run("string", func(t *testing.T) {
|
||||
if got := tc.v.String(); got != tc.s {
|
||||
t.Errorf("String: %q, want %q", got, tc.s)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package uevent_test
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
@@ -14,13 +13,14 @@ func adeT[V any, S interface {
|
||||
encoding.TextAppender
|
||||
encoding.TextMarshaler
|
||||
encoding.TextUnmarshaler
|
||||
fmt.Stringer
|
||||
|
||||
*V
|
||||
}](t *testing.T, name string, v V, want string, wantErr, wantErrE error) {
|
||||
t.Helper()
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
f := func(t *testing.T) {
|
||||
if name != "" {
|
||||
t.Parallel()
|
||||
}
|
||||
t.Helper()
|
||||
|
||||
t.Run("decode", func(t *testing.T) {
|
||||
@@ -49,15 +49,63 @@ func adeT[V any, S interface {
|
||||
} else if err == nil && string(got) != want {
|
||||
t.Errorf("MarshalText: %q, want %q", string(got), want)
|
||||
}
|
||||
if wantErrE != nil {
|
||||
})
|
||||
}
|
||||
if name != "" {
|
||||
t.Run(name, f)
|
||||
} else {
|
||||
f(t)
|
||||
}
|
||||
}
|
||||
|
||||
// adeT sets up a binary subtest for a textual appender/decoder/encoder.
|
||||
func adeB[V any, S interface {
|
||||
encoding.BinaryAppender
|
||||
encoding.BinaryMarshaler
|
||||
encoding.BinaryUnmarshaler
|
||||
|
||||
*V
|
||||
}](t *testing.T, name string, v V, want string, wantErr, wantErrE error) {
|
||||
t.Helper()
|
||||
f := func(t *testing.T) {
|
||||
if name != "" {
|
||||
t.Parallel()
|
||||
}
|
||||
t.Helper()
|
||||
|
||||
t.Run("decode", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Helper()
|
||||
|
||||
var got V
|
||||
if err := S(&got).UnmarshalBinary([]byte(want)); !reflect.DeepEqual(err, wantErr) {
|
||||
t.Fatalf("UnmarshalBinary: error = %v, want %v", err, wantErr)
|
||||
}
|
||||
if wantErr != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if got := S(&v).String(); got != want {
|
||||
t.Errorf("String: %q, want %q", got, want)
|
||||
if !reflect.DeepEqual(&got, &v) {
|
||||
t.Errorf("UnmarshalBinary: %#v, want %#v", got, v)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("encode", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Helper()
|
||||
|
||||
if got, err := S(&v).MarshalBinary(); !reflect.DeepEqual(err, wantErrE) {
|
||||
t.Fatalf("MarshalBinary: error = %v, want %v", err, wantErrE)
|
||||
} else if err == nil && string(got) != want {
|
||||
t.Errorf("MarshalBinary: %q, want %q", string(got), want)
|
||||
}
|
||||
})
|
||||
}
|
||||
if name != "" {
|
||||
t.Run(name, f)
|
||||
} else {
|
||||
f(t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
@@ -70,6 +118,26 @@ func TestErrors(t *testing.T) {
|
||||
}{
|
||||
{"UnsupportedActionError", uevent.UnsupportedActionError("explode"),
|
||||
`unsupported kobject_action "explode"`},
|
||||
|
||||
{"MissingHeaderError", uevent.MissingHeaderError("move"),
|
||||
`message "move" has no header`},
|
||||
|
||||
{"MessageError MErrorKindHeaderSep", &uevent.MessageError{
|
||||
Data: "move\x00",
|
||||
Section: "move",
|
||||
Kind: uevent.MErrorKindHeaderSep,
|
||||
}, `header "move" missing separator`},
|
||||
|
||||
{"MessageError MErrorKindFinalNUL", &uevent.MessageError{
|
||||
Data: "move\x00truncated",
|
||||
Section: "truncated",
|
||||
Kind: uevent.MErrorKindFinalNUL,
|
||||
}, `entry "truncated" missing NUL`},
|
||||
|
||||
{"MessageError bad", &uevent.MessageError{
|
||||
Data: "\x00",
|
||||
Kind: 0xbad,
|
||||
}, `section "" is invalid`},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user