From ee22847dde5c02027a4f180f1f32a69adc3aaf70 Mon Sep 17 00:00:00 2001 From: Ophestra Date: Fri, 27 Mar 2026 22:37:36 +0900 Subject: [PATCH] internal/uevent: kobject_action lookup This is encoded as part of kobject uevent message headers. Signed-off-by: Ophestra --- internal/uevent/action.go | 74 ++++++++++++++++++++++++++++++ internal/uevent/action_test.go | 32 +++++++++++++ internal/uevent/uevent_test.go | 83 ++++++++++++++++++++++++++++++++++ 3 files changed, 189 insertions(+) create mode 100644 internal/uevent/action.go create mode 100644 internal/uevent/action_test.go create mode 100644 internal/uevent/uevent_test.go 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) + } + }) + } +}