From 6cc2b406a45dee7c3fcb686036efdccc1776368d Mon Sep 17 00:00:00 2001 From: Ophestra Date: Sat, 6 Sep 2025 19:47:58 +0900 Subject: [PATCH] system/link: use syscall dispatcher This enables hardlink op methods to be instrumented. Signed-off-by: Ophestra --- system/acl_test.go | 5 +-- system/dispatcher.go | 10 +++++ system/dispatcher_test.go | 13 ++++++ system/link.go | 15 ++++--- system/link_test.go | 84 +++++++++++++++++++++++++++++++++++++++ system/tmpfiles_test.go | 37 ----------------- 6 files changed, 116 insertions(+), 48 deletions(-) create mode 100644 system/link_test.go diff --git a/system/acl_test.go b/system/acl_test.go index 910e5a1..5228b90 100644 --- a/system/acl_test.go +++ b/system/acl_test.go @@ -54,7 +54,7 @@ func TestACLUpdateOp(t *testing.T) { }, nil}, }) - checkOpsBuilder(t, "UpdatePerm", []opsBuilderTestCase{ + checkOpsBuilder(t, "UpdatePermType", []opsBuilderTestCase{ {"simple", 0xdeadbeef, func(_ *testing.T, sys *I) { @@ -65,8 +65,7 @@ func TestACLUpdateOp(t *testing.T) { &aclUpdateOp{Process, "/run/user/1971/hakurei", []acl.Perm{acl.Execute}}, &aclUpdateOp{Process, "/tmp/hakurei.0/tmpdir/150", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, }, stub.Expect{}}, - }) - checkOpsBuilder(t, "UpdatePermType", []opsBuilderTestCase{ + {"tmpdirp", 0xdeadbeef, func(_ *testing.T, sys *I) { sys.UpdatePermType(User, "/tmp/hakurei.0/tmpdir", acl.Execute) }, []Op{ diff --git a/system/dispatcher.go b/system/dispatcher.go index c896de5..5d5c82b 100644 --- a/system/dispatcher.go +++ b/system/dispatcher.go @@ -1,6 +1,8 @@ package system import ( + "os" + "hakurei.app/system/acl" "hakurei.app/system/dbus" ) @@ -13,6 +15,11 @@ type syscallDispatcher interface { // just synchronising access is not enough, as this is for test instrumentation. new(f func(k syscallDispatcher)) + // link provides os.Link. + link(oldname, newname string) error + // remove provides os.Remove. + remove(name string) error + // aclUpdate provides [acl.Update]. aclUpdate(name string, uid int, perms ...acl.Perm) error @@ -37,6 +44,9 @@ type direct struct{} func (k direct) new(f func(k syscallDispatcher)) { go f(k) } +func (k direct) link(oldname, newname string) error { return os.Link(oldname, newname) } +func (k direct) remove(name string) error { return os.Remove(name) } + func (k direct) aclUpdate(name string, uid int, perms ...acl.Perm) error { return acl.Update(name, uid, perms...) } diff --git a/system/dispatcher_test.go b/system/dispatcher_test.go index c23c6c5..c459de4 100644 --- a/system/dispatcher_test.go +++ b/system/dispatcher_test.go @@ -192,6 +192,19 @@ type kstub struct{ *stub.Stub[syscallDispatcher] } func (k *kstub) new(f func(k syscallDispatcher)) { k.Helper(); k.New(f) } +func (k *kstub) link(oldname, newname string) error { + k.Helper() + return k.Expects("link").Error( + stub.CheckArg(k.Stub, "oldname", oldname, 0), + stub.CheckArg(k.Stub, "newname", newname, 1)) +} + +func (k *kstub) remove(name string) error { + k.Helper() + return k.Expects("remove").Error( + stub.CheckArg(k.Stub, "name", name, 0)) +} + func (k *kstub) aclUpdate(name string, uid int, perms ...acl.Perm) error { k.Helper() return k.Expects("aclUpdate").Error( diff --git a/system/link.go b/system/link.go index 8df85fc..2eb658a 100644 --- a/system/link.go +++ b/system/link.go @@ -2,7 +2,6 @@ package system import ( "fmt" - "os" ) // Link calls LinkFileType with the [Process] criteria. @@ -22,17 +21,17 @@ type hardlinkOp struct { func (l *hardlinkOp) Type() Enablement { return l.et } -func (l *hardlinkOp) apply(*I) error { - msg.Verbose("linking", l) - return newOpError("hardlink", os.Link(l.src, l.dst), false) +func (l *hardlinkOp) apply(sys *I) error { + sys.verbose("linking", l) + return newOpError("hardlink", sys.link(l.src, l.dst), false) } -func (l *hardlinkOp) revert(_ *I, ec *Criteria) error { +func (l *hardlinkOp) revert(sys *I, ec *Criteria) error { if ec.hasType(l.Type()) { - msg.Verbosef("removing hard link %q", l.dst) - return newOpError("hardlink", os.Remove(l.dst), true) + sys.verbosef("removing hard link %q", l.dst) + return newOpError("hardlink", sys.remove(l.dst), true) } else { - msg.Verbosef("skipping hard link %q", l.dst) + sys.verbosef("skipping hard link %q", l.dst) return nil } } diff --git a/system/link_test.go b/system/link_test.go new file mode 100644 index 0000000..b962db1 --- /dev/null +++ b/system/link_test.go @@ -0,0 +1,84 @@ +package system + +import ( + "testing" + + "hakurei.app/container/stub" +) + +func TestHardlinkOp(t *testing.T) { + checkOpBehaviour(t, []opBehaviourTestCase{ + {"link", 0xdeadbeef, 0xff, &hardlinkOp{EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}, []stub.Call{ + call("verbose", stub.ExpectArgs{[]any{"linking", &hardlinkOp{EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}}}, nil, nil), + call("link", stub.ExpectArgs{"/run/user/1000/pulse/native", "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse"}, nil, stub.UniqueError(1)), + }, &OpError{Op: "hardlink", Err: stub.UniqueError(1)}, nil, nil}, + + {"remove", 0xdeadbeef, 0xff, &hardlinkOp{EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}, []stub.Call{ + call("verbose", stub.ExpectArgs{[]any{"linking", &hardlinkOp{EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}}}, nil, nil), + call("link", stub.ExpectArgs{"/run/user/1000/pulse/native", "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse"}, nil, nil), + }, nil, []stub.Call{ + call("verbosef", stub.ExpectArgs{"removing hard link %q", []any{"/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse"}}, nil, nil), + call("remove", stub.ExpectArgs{"/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse"}, nil, stub.UniqueError(0)), + }, &OpError{Op: "hardlink", Err: stub.UniqueError(0), Revert: true}}, + + {"success skip", 0xdeadbeef, EWayland | EX11, &hardlinkOp{EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}, []stub.Call{ + call("verbose", stub.ExpectArgs{[]any{"linking", &hardlinkOp{EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}}}, nil, nil), + call("link", stub.ExpectArgs{"/run/user/1000/pulse/native", "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse"}, nil, nil), + }, nil, []stub.Call{ + call("verbosef", stub.ExpectArgs{"skipping hard link %q", []any{"/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse"}}, nil, nil), + }, nil}, + + {"success", 0xdeadbeef, 0xff, &hardlinkOp{EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}, []stub.Call{ + call("verbose", stub.ExpectArgs{[]any{"linking", &hardlinkOp{EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}}}, nil, nil), + call("link", stub.ExpectArgs{"/run/user/1000/pulse/native", "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse"}, nil, nil), + }, nil, []stub.Call{ + call("verbosef", stub.ExpectArgs{"removing hard link %q", []any{"/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse"}}, nil, nil), + call("remove", stub.ExpectArgs{"/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse"}, nil, nil), + }, nil}, + }) + + checkOpsBuilder(t, "LinkFileType", []opsBuilderTestCase{ + {"type", 0xcafebabe, func(_ *testing.T, sys *I) { + sys.LinkFileType(User, "/run/user/1000/pulse/native", "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse") + }, []Op{ + &hardlinkOp{User, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}, + }, stub.Expect{}}, + + {"link", 0xcafebabe, func(_ *testing.T, sys *I) { + sys.Link("/run/user/1000/pulse/native", "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse") + }, []Op{ + &hardlinkOp{Process, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}, + }, stub.Expect{}}, + }) + + checkOpIs(t, []opIsTestCase{ + {"nil", (*hardlinkOp)(nil), (*hardlinkOp)(nil), false}, + {"zero", new(hardlinkOp), new(hardlinkOp), true}, + + {"src differs", + &hardlinkOp{Process, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse"}, + &hardlinkOp{Process, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}, + false}, + + {"dst differs", + &hardlinkOp{Process, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6", "/run/user/1000/pulse/native"}, + &hardlinkOp{Process, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}, + false}, + + {"et differs", + &hardlinkOp{User, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}, + &hardlinkOp{Process, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}, + false}, + + {"equals", + &hardlinkOp{Process, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}, + &hardlinkOp{Process, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}, + true}, + }) + + checkOpMeta(t, []opMetaTestCase{ + {"link", &hardlinkOp{Process, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}, + Process, "/run/user/1000/pulse/native", + `"/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse" from "/run/user/1000/pulse/native"`}, + }) +} diff --git a/system/tmpfiles_test.go b/system/tmpfiles_test.go index 6410a9a..a06242e 100644 --- a/system/tmpfiles_test.go +++ b/system/tmpfiles_test.go @@ -24,43 +24,6 @@ func TestCopyFile(t *testing.T) { } } -func TestLink(t *testing.T) { - testCases := []struct { - dst, src string - }{ - {"/tmp/hakurei.1971/f587afe9fce3c8e1ad5b64deb6c41ad5/pulse-cookie", "/home/ophestra/xdg/config/pulse/cookie"}, - {"/tmp/hakurei.1971/62154f708b5184ab01f9dcc2bbe7a33b/pulse-cookie", "/home/ophestra/xdg/config/pulse/cookie"}, - } - for _, tc := range testCases { - t.Run("link file "+tc.dst+" from "+tc.src, func(t *testing.T) { - sys := New(t.Context(), 150) - sys.Link(tc.src, tc.dst) - (&tcOp{Process, tc.src}).test(t, sys.ops, []Op{ - &hardlinkOp{Process, tc.dst, tc.src}, - }, "Link") - }) - } -} - -func TestLinkFileType(t *testing.T) { - testCases := []struct { - tcOp - dst string - }{ - {tcOp{User, "/tmp/hakurei.1971/f587afe9fce3c8e1ad5b64deb6c41ad5/pulse-cookie"}, "/home/ophestra/xdg/config/pulse/cookie"}, - {tcOp{Process, "/tmp/hakurei.1971/62154f708b5184ab01f9dcc2bbe7a33b/pulse-cookie"}, "/home/ophestra/xdg/config/pulse/cookie"}, - } - for _, tc := range testCases { - t.Run("link file "+tc.dst+" from "+tc.path+" with type "+TypeString(tc.et), func(t *testing.T) { - sys := New(t.Context(), 150) - sys.LinkFileType(tc.et, tc.path, tc.dst) - tc.test(t, sys.ops, []Op{ - &hardlinkOp{tc.et, tc.dst, tc.path}, - }, "LinkFileType") - }) - } -} - func TestTmpfile_String(t *testing.T) { testCases := []struct { src string