aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2026-03-27 22:37:36 +0900
committerOphestra <cat@gensokyo.uk>2026-03-27 22:39:43 +0900
commitee22847dde5c02027a4f180f1f32a69adc3aaf70 (patch)
treec1cdd5a0b546cfe2903e197c4658098960a67254 /internal
parentc61188649b757ba81c813ff2f7351a3d393cdce1 (diff)
internal/uevent: kobject_action lookup
This is encoded as part of kobject uevent message headers. Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'internal')
-rw-r--r--internal/uevent/action.go74
-rw-r--r--internal/uevent/action_test.go32
-rw-r--r--internal/uevent/uevent_test.go83
3 files changed, 189 insertions, 0 deletions
diff --git a/internal/uevent/action.go b/internal/uevent/action.go
new file mode 100644
index 00000000..4f1f4f79
--- /dev/null
+++ b/internal/uevent/action.go
@@ -0,0 +1,74 @@
+package uevent
+
+import (
+ "strconv"
+ "syscall"
+)
+
+// KobjectAction represents enum kobject_action found in include/linux/kobject.h
+// and their corresponding string representations in lib/kobject_uevent.c.
+type KobjectAction uint32
+
+// include/linux/kobject.h
+const (
+ KOBJ_ADD KobjectAction = iota
+ KOBJ_REMOVE
+ KOBJ_CHANGE
+ KOBJ_MOVE
+ KOBJ_ONLINE
+ KOBJ_OFFLINE
+ KOBJ_BIND
+ KOBJ_UNBIND
+)
+
+// lib/kobject_uevent.c
+var kobject_actions = [...]string{
+ KOBJ_ADD: "add",
+ KOBJ_REMOVE: "remove",
+ KOBJ_CHANGE: "change",
+ KOBJ_MOVE: "move",
+ KOBJ_ONLINE: "online",
+ KOBJ_OFFLINE: "offline",
+ KOBJ_BIND: "bind",
+ KOBJ_UNBIND: "unbind",
+}
+
+// Valid returns whether the value of act is defined.
+func (act KobjectAction) Valid() bool { return int(act) < len(kobject_actions) }
+
+// String returns the corresponding string sent over netlink.
+func (act KobjectAction) String() string {
+ if !act.Valid() {
+ return "unsupported kobject_action " + strconv.Itoa(int(act))
+ }
+ return kobject_actions[act]
+}
+
+func (act KobjectAction) AppendText(b []byte) ([]byte, error) {
+ if !act.Valid() {
+ return b, syscall.EINVAL
+ }
+ return append(b, act.String()...), nil
+}
+
+func (act KobjectAction) MarshalText() ([]byte, error) {
+ return act.AppendText(nil)
+}
+
+// An UnsupportedActionError describes a string representation of [KobjectAction]
+// not yet supported by this package.
+type UnsupportedActionError string
+
+func (e UnsupportedActionError) Error() string {
+ return "unsupported kobject_action " + strconv.Quote(string(e))
+}
+
+func (act *KobjectAction) UnmarshalText(data []byte) error {
+ for v, s := range kobject_actions {
+ if string(data) == s {
+ *act = KobjectAction(v)
+ return nil
+ }
+ }
+ return UnsupportedActionError(data)
+}
diff --git a/internal/uevent/action_test.go b/internal/uevent/action_test.go
new file mode 100644
index 00000000..1a552f15
--- /dev/null
+++ b/internal/uevent/action_test.go
@@ -0,0 +1,32 @@
+package uevent_test
+
+import (
+ "syscall"
+ "testing"
+
+ "hakurei.app/internal/uevent"
+)
+
+func TestKobjectAction(t *testing.T) {
+ t.Parallel()
+
+ adeT(t, "add", uevent.KOBJ_ADD, "add", nil, nil)
+ adeT(t, "remove", uevent.KOBJ_REMOVE, "remove", nil, nil)
+ adeT(t, "change", uevent.KOBJ_CHANGE, "change", nil, nil)
+ adeT(t, "move", uevent.KOBJ_MOVE, "move", nil, nil)
+ adeT(t, "online", uevent.KOBJ_ONLINE, "online", nil, nil)
+ adeT(t, "offline", uevent.KOBJ_OFFLINE, "offline", nil, nil)
+ adeT(t, "bind", uevent.KOBJ_BIND, "bind", nil, nil)
+ adeT(t, "unbind", uevent.KOBJ_UNBIND, "unbind", nil, nil)
+
+ adeT(t, "unsupported", uevent.KobjectAction(0xbad), "explode",
+ uevent.UnsupportedActionError("explode"), syscall.EINVAL)
+ t.Run("oob string", func(t *testing.T) {
+ t.Parallel()
+
+ const want = "unsupported kobject_action 2989"
+ if got := uevent.KobjectAction(0xbad).String(); got != want {
+ t.Errorf("String: %q, want %q", got, want)
+ }
+ })
+}
diff --git a/internal/uevent/uevent_test.go b/internal/uevent/uevent_test.go
new file mode 100644
index 00000000..9af3dcdd
--- /dev/null
+++ b/internal/uevent/uevent_test.go
@@ -0,0 +1,83 @@
+package uevent_test
+
+import (
+ "encoding"
+ "fmt"
+ "reflect"
+ "testing"
+
+ "hakurei.app/internal/uevent"
+)
+
+// adeT sets up a parallel subtest for a textual appender/decoder/encoder.
+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()
+ t.Helper()
+
+ t.Run("decode", func(t *testing.T) {
+ t.Parallel()
+ t.Helper()
+
+ var got V
+ if err := S(&got).UnmarshalText([]byte(want)); !reflect.DeepEqual(err, wantErr) {
+ t.Fatalf("UnmarshalText: error = %v, want %v", err, wantErr)
+ }
+ if wantErr != nil {
+ return
+ }
+
+ if !reflect.DeepEqual(&got, &v) {
+ t.Errorf("UnmarshalText: %#v, want %#v", got, v)
+ }
+ })
+
+ t.Run("encode", func(t *testing.T) {
+ t.Parallel()
+ t.Helper()
+
+ if got, err := S(&v).MarshalText(); !reflect.DeepEqual(err, wantErrE) {
+ t.Fatalf("MarshalText: error = %v, want %v", err, wantErrE)
+ } else if err == nil && string(got) != want {
+ t.Errorf("MarshalText: %q, want %q", string(got), want)
+ }
+ if wantErrE != nil {
+ return
+ }
+
+ if got := S(&v).String(); got != want {
+ t.Errorf("String: %q, want %q", got, want)
+ }
+ })
+ })
+}
+
+func TestErrors(t *testing.T) {
+ t.Parallel()
+
+ testCases := []struct {
+ name string
+ err error
+ want string
+ }{
+ {"UnsupportedActionError", uevent.UnsupportedActionError("explode"),
+ `unsupported kobject_action "explode"`},
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ if got := tc.err.Error(); got != tc.want {
+ t.Errorf("Error: %q, want %q", got, tc.want)
+ }
+ })
+ }
+}