aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/uevent/message.go
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2026-03-28 00:10:50 +0900
committerOphestra <cat@gensokyo.uk>2026-03-28 00:49:34 +0900
commit713bff3eb0a77efff016acc02b6f10913810f887 (patch)
treea648ab320007bf1aa15ab2f55e7856713cf92e88 /internal/uevent/message.go
parent30f459e690ca561d3932cb6efa0cf14ea53c25b2 (diff)
internal/uevent: decode uevent messages
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>
Diffstat (limited to 'internal/uevent/message.go')
-rw-r--r--internal/uevent/message.go137
1 files changed, 137 insertions, 0 deletions
diff --git a/internal/uevent/message.go b/internal/uevent/message.go
new file mode 100644
index 00000000..aecd5b8c
--- /dev/null
+++ b/internal/uevent/message.go
@@ -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
+}