diff options
Diffstat (limited to 'system')
54 files changed, 204 insertions, 5992 deletions
diff --git a/system/acl.go b/system/acl.go deleted file mode 100644 index c89a4780..00000000 --- a/system/acl.go +++ /dev/null @@ -1,69 +0,0 @@ -package system - -import ( - "errors" - "fmt" - "os" - "slices" - - "hakurei.app/container/check" - "hakurei.app/hst" - "hakurei.app/system/acl" -) - -// UpdatePerm calls UpdatePermType with the [Process] criteria. -func (sys *I) UpdatePerm(path *check.Absolute, perms ...acl.Perm) *I { - sys.UpdatePermType(Process, path, perms...) - return sys -} - -// UpdatePermType maintains [acl.Perms] on a file until its [Enablement] is no longer satisfied. -func (sys *I) UpdatePermType(et hst.Enablement, path *check.Absolute, perms ...acl.Perm) *I { - sys.ops = append(sys.ops, &aclUpdateOp{et, path.String(), perms}) - return sys -} - -// aclUpdateOp implements [I.UpdatePermType]. -type aclUpdateOp struct { - et hst.Enablement - path string - perms acl.Perms -} - -func (a *aclUpdateOp) Type() hst.Enablement { return a.et } - -func (a *aclUpdateOp) apply(sys *I) error { - sys.msg.Verbose("applying ACL", a) - return newOpError("acl", sys.aclUpdate(a.path, sys.uid, a.perms...), false) -} - -func (a *aclUpdateOp) revert(sys *I, ec *Criteria) error { - if ec.hasType(a.Type()) { - sys.msg.Verbose("stripping ACL", a) - err := sys.aclUpdate(a.path, sys.uid) - if errors.Is(err, os.ErrNotExist) { - // the ACL is effectively stripped if the file no longer exists - sys.msg.Verbosef("target of ACL %s no longer exists", a) - err = nil - } - return newOpError("acl", err, true) - } else { - sys.msg.Verbose("skipping ACL", a) - return nil - } -} - -func (a *aclUpdateOp) Is(o Op) bool { - target, ok := o.(*aclUpdateOp) - return ok && a != nil && target != nil && - a.et == target.et && - a.path == target.path && - slices.Equal(a.perms, target.perms) -} - -func (a *aclUpdateOp) Path() string { return a.path } - -func (a *aclUpdateOp) String() string { - return fmt.Sprintf("%s type: %s path: %q", - a.perms, TypeString(a.et), a.path) -} diff --git a/system/acl/acl.go b/system/acl/acl.go deleted file mode 100644 index 366ad533..00000000 --- a/system/acl/acl.go +++ /dev/null @@ -1,33 +0,0 @@ -// Package acl implements simple ACL manipulation via libacl. -package acl - -/* -#cgo linux pkg-config: --static libacl - -#include "libacl-helper.h" -*/ -import "C" - -type Perm C.acl_perm_t - -const ( - Read Perm = C.ACL_READ - Write Perm = C.ACL_WRITE - Execute Perm = C.ACL_EXECUTE -) - -// Update replaces ACL_USER entry with qualifier uid. -func Update(name string, uid int, perms ...Perm) error { - var p *Perm - if len(perms) > 0 { - p = &perms[0] - } - - r, err := C.hakurei_acl_update_file_by_uid( - C.CString(name), - C.uid_t(uid), - (*C.acl_perm_t)(p), - C.size_t(len(perms)), - ) - return newAclPathError(name, int(r), err) -} diff --git a/system/acl/acl_test.go b/system/acl/acl_test.go deleted file mode 100644 index c6c3e562..00000000 --- a/system/acl/acl_test.go +++ /dev/null @@ -1,276 +0,0 @@ -package acl_test - -import ( - "bufio" - "bytes" - "errors" - "fmt" - "io" - "os" - "os/exec" - "path" - "reflect" - "strconv" - "testing" - - "hakurei.app/system/acl" -) - -const testFileName = "acl.test" - -var ( - uid = os.Geteuid() - cred = int32(os.Geteuid()) -) - -func TestUpdate(t *testing.T) { - if os.Getenv("GO_TEST_SKIP_ACL") == "1" { - t.Log("acl test skipped") - t.SkipNow() - } - - testFilePath := path.Join(t.TempDir(), testFileName) - - if f, err := os.Create(testFilePath); err != nil { - t.Fatalf("Create: error = %v", err) - } else { - if err = f.Close(); err != nil { - t.Fatalf("Close: error = %v", err) - } - } - defer func() { - if err := os.Remove(testFilePath); err != nil { - t.Fatalf("Remove: error = %v", err) - } - }() - - cur := getfacl(t, testFilePath) - - t.Run("default entry count", func(t *testing.T) { - if len(cur) != 3 { - t.Fatalf("unexpected test file acl length %d", len(cur)) - } - }) - - t.Run("default clear mask", func(t *testing.T) { - if err := acl.Update(testFilePath, uid); err != nil { - t.Fatalf("Update: error = %v", err) - } - if cur = getfacl(t, testFilePath); len(cur) != 4 { - t.Fatalf("Update: %v", cur) - } - }) - - t.Run("default clear consistency", func(t *testing.T) { - if err := acl.Update(testFilePath, uid); err != nil { - t.Fatalf("Update: error = %v", err) - } - if val := getfacl(t, testFilePath); !reflect.DeepEqual(val, cur) { - t.Fatalf("Update: %v, want %v", val, cur) - } - }) - - testUpdate(t, testFilePath, "r--", cur, fAclPermRead, acl.Read) - testUpdate(t, testFilePath, "-w-", cur, fAclPermWrite, acl.Write) - testUpdate(t, testFilePath, "--x", cur, fAclPermExecute, acl.Execute) - testUpdate(t, testFilePath, "-wx", cur, fAclPermWrite|fAclPermExecute, acl.Write, acl.Execute) - testUpdate(t, testFilePath, "r-x", cur, fAclPermRead|fAclPermExecute, acl.Read, acl.Execute) - testUpdate(t, testFilePath, "rw-", cur, fAclPermRead|fAclPermWrite, acl.Read, acl.Write) - testUpdate(t, testFilePath, "rwx", cur, fAclPermRead|fAclPermWrite|fAclPermExecute, acl.Read, acl.Write, acl.Execute) -} - -func testUpdate(t *testing.T, testFilePath, name string, cur []*getFAclResp, val fAclPerm, perms ...acl.Perm) { - t.Run(name, func(t *testing.T) { - t.Cleanup(func() { - if err := acl.Update(testFilePath, uid); err != nil { - t.Fatalf("Update: error = %v", err) - } - if v := getfacl(t, testFilePath); !reflect.DeepEqual(v, cur) { - t.Fatalf("Update: %v, want %v", v, cur) - } - }) - - if err := acl.Update(testFilePath, uid, perms...); err != nil { - t.Fatalf("Update: error = %v", err) - } - r := respByCred(getfacl(t, testFilePath), fAclTypeUser, cred) - if r == nil { - t.Fatalf("Update did not add an ACL entry") - } - if !r.equals(fAclTypeUser, cred, val) { - t.Fatalf("Update(%s) = %s", name, r) - } - }) -} - -type ( - getFAclInvocation struct { - cmd *exec.Cmd - val []*getFAclResp - pe []error - } - - getFAclResp struct { - typ fAclType - cred int32 - val fAclPerm - - raw []byte - } - - fAclPerm uintptr - fAclType uint8 -) - -const fAclBufSize = 16 - -const ( - fAclPermRead fAclPerm = 1 << iota - fAclPermWrite - fAclPermExecute -) - -const ( - fAclTypeUser fAclType = iota - fAclTypeGroup - fAclTypeMask - fAclTypeOther -) - -func (c *getFAclInvocation) run(name string) error { - if c.cmd != nil { - panic("attempted to run twice") - } - - c.cmd = exec.Command("getfacl", "--omit-header", "--absolute-names", "--numeric", name) - - scanErr := make(chan error, 1) - if p, err := c.cmd.StdoutPipe(); err != nil { - return err - } else { - go c.parse(p, scanErr) - } - - if err := c.cmd.Start(); err != nil { - return err - } - - return errors.Join(<-scanErr, c.cmd.Wait()) -} - -func (c *getFAclInvocation) parse(pipe io.Reader, scanErr chan error) { - c.val = make([]*getFAclResp, 0, 4+fAclBufSize) - - s := bufio.NewScanner(pipe) - for s.Scan() { - fields := bytes.SplitN(s.Bytes(), []byte{':'}, 3) - if len(fields) != 3 { - continue - } - - resp := getFAclResp{} - - switch string(fields[0]) { - case "user": - resp.typ = fAclTypeUser - case "group": - resp.typ = fAclTypeGroup - case "mask": - resp.typ = fAclTypeMask - case "other": - resp.typ = fAclTypeOther - default: - c.pe = append(c.pe, fmt.Errorf("unknown type %s", string(fields[0]))) - continue - } - - if len(fields[1]) == 0 { - resp.cred = -1 - } else { - if cred, err := strconv.Atoi(string(fields[1])); err != nil { - c.pe = append(c.pe, err) - continue - } else { - resp.cred = int32(cred) - if resp.cred < 0 { - c.pe = append(c.pe, fmt.Errorf("credential %d out of range", resp.cred)) - continue - } - } - } - - if len(fields[2]) != 3 { - c.pe = append(c.pe, fmt.Errorf("invalid perm length %d", len(fields[2]))) - continue - } else { - switch fields[2][0] { - case 'r': - resp.val |= fAclPermRead - case '-': - default: - c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][0])) - continue - } - switch fields[2][1] { - case 'w': - resp.val |= fAclPermWrite - case '-': - default: - c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][1])) - continue - } - switch fields[2][2] { - case 'x': - resp.val |= fAclPermExecute - case '-': - default: - c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][2])) - continue - } - } - - resp.raw = make([]byte, len(s.Bytes())) - copy(resp.raw, s.Bytes()) - c.val = append(c.val, &resp) - } - scanErr <- s.Err() -} - -func (r *getFAclResp) String() string { - if r.raw != nil && len(r.raw) > 0 { - return string(r.raw) - } - - return "(user-initialised resp value)" -} - -func (r *getFAclResp) equals(typ fAclType, cred int32, val fAclPerm) bool { - return r.typ == typ && r.cred == cred && r.val == val -} - -func getfacl(t *testing.T, name string) []*getFAclResp { - c := new(getFAclInvocation) - if err := c.run(name); err != nil { - t.Fatalf("getfacl: error = %v", err) - } - if len(c.pe) != 0 { - t.Errorf("errors encountered parsing getfacl output\n%s", errors.Join(c.pe...).Error()) - } - return c.val -} - -func respByCred(v []*getFAclResp, typ fAclType, cred int32) *getFAclResp { - j := -1 - for i, r := range v { - if r.typ == typ && r.cred == cred { - if j != -1 { - panic("invalid acl") - } - j = i - } - } - if j == -1 { - return nil - } - return v[j] -} diff --git a/system/acl/deprecated.go b/system/acl/deprecated.go new file mode 100644 index 00000000..4119057b --- /dev/null +++ b/system/acl/deprecated.go @@ -0,0 +1,25 @@ +// Package acl exposes the internal/system/acl package. +// +// Deprecated: This package will be removed in 0.4. +package acl + +import ( + _ "unsafe" // for go:linkname + + "hakurei.app/internal/system/acl" +) + +type Perm = acl.Perm + +const ( + Read = acl.Read + Write = acl.Write + Execute = acl.Execute +) + +// Update replaces ACL_USER entry with qualifier uid. +// +//go:linkname Update hakurei.app/internal/system/acl.Update +func Update(name string, uid int, perms ...Perm) error + +type Perms = acl.Perms diff --git a/system/acl/libacl-helper.c b/system/acl/libacl-helper.c deleted file mode 100644 index d1df8167..00000000 --- a/system/acl/libacl-helper.c +++ /dev/null @@ -1,90 +0,0 @@ -#include "libacl-helper.h" -#include <acl/libacl.h> -#include <stdbool.h> -#include <stdlib.h> -#include <sys/acl.h> - -int hakurei_acl_update_file_by_uid(const char *path_p, uid_t uid, - acl_perm_t *perms, size_t plen) { - int ret; - bool v; - int i; - acl_t acl; - acl_entry_t entry; - acl_tag_t tag_type; - void *qualifier_p; - acl_permset_t permset; - - ret = -1; /* acl_get_file */ - acl = acl_get_file(path_p, ACL_TYPE_ACCESS); - if (acl == NULL) - goto out; - - /* prune entries by uid */ - for (i = acl_get_entry(acl, ACL_FIRST_ENTRY, &entry); i == 1; - i = acl_get_entry(acl, ACL_NEXT_ENTRY, &entry)) { - ret = -2; /* acl_get_tag_type */ - if (acl_get_tag_type(entry, &tag_type) != 0) - goto out; - if (tag_type != ACL_USER) - continue; - - ret = -3; /* acl_get_qualifier */ - qualifier_p = acl_get_qualifier(entry); - if (qualifier_p == NULL) - goto out; - v = *(uid_t *)qualifier_p == uid; - acl_free(qualifier_p); - - if (!v) - continue; - - ret = -4; /* acl_delete_entry */ - if (acl_delete_entry(acl, entry) != 0) - goto out; - } - - if (plen == 0) - goto set; - - ret = -5; /* acl_create_entry */ - if (acl_create_entry(&acl, &entry) != 0) - goto out; - - ret = -6; /* acl_get_permset */ - if (acl_get_permset(entry, &permset) != 0) - goto out; - - ret = -7; /* acl_add_perm */ - for (i = 0; i < plen; i++) { - if (acl_add_perm(permset, perms[i]) != 0) - goto out; - } - - ret = -8; /* acl_set_tag_type */ - if (acl_set_tag_type(entry, ACL_USER) != 0) - goto out; - - ret = -9; /* acl_set_qualifier */ - if (acl_set_qualifier(entry, (void *)&uid) != 0) - goto out; - -set: - ret = -10; /* acl_calc_mask */ - if (acl_calc_mask(&acl) != 0) - goto out; - - ret = -11; /* acl_valid */ - if (acl_valid(acl) != 0) - goto out; - - ret = -12; /* acl_set_file */ - if (acl_set_file(path_p, ACL_TYPE_ACCESS, acl) == 0) - ret = 0; - -out: - free((void *)path_p); - if (acl != NULL) - acl_free((void *)acl); - return ret; -} diff --git a/system/acl/libacl-helper.go b/system/acl/libacl-helper.go deleted file mode 100644 index 22575296..00000000 --- a/system/acl/libacl-helper.go +++ /dev/null @@ -1,40 +0,0 @@ -package acl - -import "os" - -func newAclPathError(name string, r int, err error) error { - pathError := &os.PathError{Path: name, Err: err} - switch r { - case 0: - return nil - - case -1: - pathError.Op = "acl_get_file" - case -2: - pathError.Op = "acl_get_tag_type" - case -3: - pathError.Op = "acl_get_qualifier" - case -4: - pathError.Op = "acl_delete_entry" - case -5: - pathError.Op = "acl_create_entry" - case -6: - pathError.Op = "acl_get_permset" - case -7: - pathError.Op = "acl_add_perm" - case -8: - pathError.Op = "acl_set_tag_type" - case -9: - pathError.Op = "acl_set_qualifier" - case -10: - pathError.Op = "acl_calc_mask" - case -11: - pathError.Op = "acl_valid" - case -12: - pathError.Op = "acl_set_file" - - default: // unreachable - pathError.Op = "setfacl" - } - return pathError -} diff --git a/system/acl/libacl-helper.h b/system/acl/libacl-helper.h deleted file mode 100644 index b86eb170..00000000 --- a/system/acl/libacl-helper.h +++ /dev/null @@ -1,4 +0,0 @@ -#include <sys/acl.h> - -int hakurei_acl_update_file_by_uid(const char *path_p, uid_t uid, - acl_perm_t *perms, size_t plen); diff --git a/system/acl/libacl-helper_test.go b/system/acl/libacl-helper_test.go deleted file mode 100644 index 13d23a3c..00000000 --- a/system/acl/libacl-helper_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package acl - -import ( - "os" - "reflect" - "syscall" - "testing" - - "hakurei.app/container" -) - -func TestNewAclPathError(t *testing.T) { - testCases := []struct { - name string - path string - r int - err error - want error - }{ - {"nil", container.Nonexistent, 0, syscall.ENOTRECOVERABLE, nil}, - - {"acl_get_file", container.Nonexistent, -1, syscall.ENOTRECOVERABLE, - &os.PathError{Op: "acl_get_file", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - {"acl_get_tag_type", container.Nonexistent, -2, syscall.ENOTRECOVERABLE, - &os.PathError{Op: "acl_get_tag_type", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - {"acl_get_qualifier", container.Nonexistent, -3, syscall.ENOTRECOVERABLE, - &os.PathError{Op: "acl_get_qualifier", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - {"acl_delete_entry", container.Nonexistent, -4, syscall.ENOTRECOVERABLE, - &os.PathError{Op: "acl_delete_entry", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - {"acl_create_entry", container.Nonexistent, -5, syscall.ENOTRECOVERABLE, - &os.PathError{Op: "acl_create_entry", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - {"acl_get_permset", container.Nonexistent, -6, syscall.ENOTRECOVERABLE, - &os.PathError{Op: "acl_get_permset", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - {"acl_add_perm", container.Nonexistent, -7, syscall.ENOTRECOVERABLE, - &os.PathError{Op: "acl_add_perm", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - {"acl_set_tag_type", container.Nonexistent, -8, syscall.ENOTRECOVERABLE, - &os.PathError{Op: "acl_set_tag_type", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - {"acl_set_qualifier", container.Nonexistent, -9, syscall.ENOTRECOVERABLE, - &os.PathError{Op: "acl_set_qualifier", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - {"acl_calc_mask", container.Nonexistent, -10, syscall.ENOTRECOVERABLE, - &os.PathError{Op: "acl_calc_mask", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - {"acl_valid", container.Nonexistent, -11, syscall.ENOTRECOVERABLE, - &os.PathError{Op: "acl_valid", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - {"acl_set_file", container.Nonexistent, -12, syscall.ENOTRECOVERABLE, - &os.PathError{Op: "acl_set_file", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - - {"acl", container.Nonexistent, -13, syscall.ENOTRECOVERABLE, - &os.PathError{Op: "setfacl", Path: container.Nonexistent, Err: syscall.ENOTRECOVERABLE}}, - {"invalid", container.Nonexistent, -0xdead, nil, - &os.PathError{Op: "setfacl", Path: container.Nonexistent}}, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - err := newAclPathError(tc.path, tc.r, tc.err) - if !reflect.DeepEqual(err, tc.want) { - t.Errorf("newAclPathError: %v, want %v", err, tc.want) - } - }) - } -} diff --git a/system/acl/perms.go b/system/acl/perms.go deleted file mode 100644 index fbc99e82..00000000 --- a/system/acl/perms.go +++ /dev/null @@ -1,18 +0,0 @@ -package acl - -type Perms []Perm - -func (ps Perms) String() string { - var s = []byte("---") - for _, p := range ps { - switch p { - case Read: - s[0] = 'r' - case Write: - s[1] = 'w' - case Execute: - s[2] = 'x' - } - } - return string(s) -} diff --git a/system/acl/perms_test.go b/system/acl/perms_test.go deleted file mode 100644 index 16b282a7..00000000 --- a/system/acl/perms_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package acl_test - -import ( - "testing" - - "hakurei.app/system/acl" -) - -func TestPerms(t *testing.T) { - testCases := []struct { - name string - perms acl.Perms - }{ - {"---", acl.Perms{}}, - {"r--", acl.Perms{acl.Read}}, - {"-w-", acl.Perms{acl.Write}}, - {"--x", acl.Perms{acl.Execute}}, - {"rw-", acl.Perms{acl.Read, acl.Read, acl.Write}}, - {"r-x", acl.Perms{acl.Read, acl.Execute, acl.Execute}}, - {"-wx", acl.Perms{acl.Write, acl.Write, acl.Execute, acl.Execute}}, - {"rwx", acl.Perms{acl.Read, acl.Write, acl.Execute}}, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - if got := tc.perms.String(); got != tc.name { - t.Errorf("String: %q, want %q", got, tc.name) - } - }) - } -} diff --git a/system/acl_test.go b/system/acl_test.go deleted file mode 100644 index 2202d957..00000000 --- a/system/acl_test.go +++ /dev/null @@ -1,185 +0,0 @@ -package system - -import ( - "os" - "syscall" - "testing" - - "hakurei.app/container/stub" - "hakurei.app/hst" - "hakurei.app/system/acl" -) - -func TestACLUpdateOp(t *testing.T) { - t.Parallel() - - checkOpBehaviour(t, []opBehaviourTestCase{ - {"apply aclUpdate", 0xbeef, 0xff, - &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"applying ACL", &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil), - call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, stub.UniqueError(1)), - }, &OpError{Op: "acl", Err: stub.UniqueError(1)}, nil, nil}, - - {"revert aclUpdate", 0xbeef, 0xff, - &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"applying ACL", &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil), - call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil), - }, nil, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"stripping ACL", &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil), - call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, ([]acl.Perm)(nil)}, nil, stub.UniqueError(0)), - }, &OpError{Op: "acl", Err: stub.UniqueError(0), Revert: true}}, - - {"success revert skip", 0xbeef, Process, - &aclUpdateOp{User, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"applying ACL", &aclUpdateOp{User, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil), - call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil), - }, nil, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"skipping ACL", &aclUpdateOp{User, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil), - }, nil}, - - {"success revert aclUpdate ENOENT", 0xbeef, 0xff, - &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"applying ACL", &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil), - call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil), - }, nil, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"stripping ACL", &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil), - call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, ([]acl.Perm)(nil)}, nil, &os.PathError{Op: "acl_get_file", Path: "/proc/nonexistent", Err: syscall.ENOENT}), - call("verbosef", stub.ExpectArgs{"target of ACL %s no longer exists", []any{&aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil), - }, nil}, - - {"success", 0xbeef, 0xff, - &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"applying ACL", &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil), - call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil), - }, nil, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"stripping ACL", &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{acl.Read, acl.Write, acl.Execute}}}}, nil, nil), - call("aclUpdate", stub.ExpectArgs{"/proc/nonexistent", 0xbeef, ([]acl.Perm)(nil)}, nil, nil), - }, nil}, - }) - - checkOpsBuilder(t, "UpdatePermType", []opsBuilderTestCase{ - {"simple", - 0xbeef, - func(_ *testing.T, sys *I) { - sys. - UpdatePerm(m("/run/user/1971/hakurei"), acl.Execute). - UpdatePerm(m("/tmp/hakurei.0/tmpdir/150"), acl.Read, acl.Write, acl.Execute) - }, []Op{ - &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{}}, - - {"tmpdirp", 0xbeef, func(_ *testing.T, sys *I) { - sys.UpdatePermType(User, m("/tmp/hakurei.0/tmpdir"), acl.Execute) - }, []Op{ - &aclUpdateOp{User, "/tmp/hakurei.0/tmpdir", []acl.Perm{acl.Execute}}, - }, stub.Expect{}}, - - {"tmpdir", 0xbeef, func(_ *testing.T, sys *I) { - sys.UpdatePermType(User, m("/tmp/hakurei.0/tmpdir/150"), acl.Read, acl.Write, acl.Execute) - }, []Op{ - &aclUpdateOp{User, "/tmp/hakurei.0/tmpdir/150", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, - }, stub.Expect{}}, - - {"share", 0xbeef, func(_ *testing.T, sys *I) { - sys.UpdatePermType(Process, m("/run/user/1971/hakurei/fcb8a12f7c482d183ade8288c3de78b5"), acl.Execute) - }, []Op{ - &aclUpdateOp{Process, "/run/user/1971/hakurei/fcb8a12f7c482d183ade8288c3de78b5", []acl.Perm{acl.Execute}}, - }, stub.Expect{}}, - - {"passwd", 0xbeef, func(_ *testing.T, sys *I) { - sys. - UpdatePermType(Process, m("/tmp/hakurei.0/fcb8a12f7c482d183ade8288c3de78b5/passwd"), acl.Read). - UpdatePermType(Process, m("/tmp/hakurei.0/fcb8a12f7c482d183ade8288c3de78b5/group"), acl.Read) - }, []Op{ - &aclUpdateOp{Process, "/tmp/hakurei.0/fcb8a12f7c482d183ade8288c3de78b5/passwd", []acl.Perm{acl.Read}}, - &aclUpdateOp{Process, "/tmp/hakurei.0/fcb8a12f7c482d183ade8288c3de78b5/group", []acl.Perm{acl.Read}}, - }, stub.Expect{}}, - - {"wayland", 0xbeef, func(_ *testing.T, sys *I) { - sys.UpdatePermType(hst.EWayland, m("/run/user/1971/wayland-0"), acl.Read, acl.Write, acl.Execute) - }, []Op{ - &aclUpdateOp{hst.EWayland, "/run/user/1971/wayland-0", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, - }, stub.Expect{}}, - }) - - checkOpIs(t, []opIsTestCase{ - {"nil", (*aclUpdateOp)(nil), (*aclUpdateOp)(nil), false}, - {"zero", new(aclUpdateOp), new(aclUpdateOp), true}, - - {"et differs", - &aclUpdateOp{ - hst.EWayland, "/run/user/1971/wayland-0", - []acl.Perm{acl.Read, acl.Write, acl.Execute}, - }, &aclUpdateOp{ - hst.EX11, "/run/user/1971/wayland-0", - []acl.Perm{acl.Read, acl.Write, acl.Execute}, - }, false}, - - {"path differs", &aclUpdateOp{ - hst.EWayland, "/run/user/1971/wayland-0", - []acl.Perm{acl.Read, acl.Write, acl.Execute}, - }, &aclUpdateOp{ - hst.EWayland, "/run/user/1971/wayland-1", - []acl.Perm{acl.Read, acl.Write, acl.Execute}, - }, false}, - - {"perms differs", &aclUpdateOp{ - hst.EWayland, "/run/user/1971/wayland-0", - []acl.Perm{acl.Read, acl.Write, acl.Execute}, - }, &aclUpdateOp{ - hst.EWayland, "/run/user/1971/wayland-0", - []acl.Perm{acl.Read, acl.Write}, - }, false}, - - {"equals", &aclUpdateOp{ - hst.EWayland, "/run/user/1971/wayland-0", - []acl.Perm{acl.Read, acl.Write, acl.Execute}, - }, &aclUpdateOp{ - hst.EWayland, "/run/user/1971/wayland-0", - []acl.Perm{acl.Read, acl.Write, acl.Execute}, - }, true}, - }) - - checkOpMeta(t, []opMetaTestCase{ - {"clear", - &aclUpdateOp{Process, "/proc/nonexistent", []acl.Perm{}}, - Process, "/proc/nonexistent", - `--- type: process path: "/proc/nonexistent"`}, - - {"read", - &aclUpdateOp{User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/0", []acl.Perm{acl.Read}}, - User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/0", - `r-- type: user path: "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/0"`}, - - {"write", - &aclUpdateOp{User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/1", []acl.Perm{acl.Write}}, - User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/1", - `-w- type: user path: "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/1"`}, - - {"execute", - &aclUpdateOp{User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/2", []acl.Perm{acl.Execute}}, - User, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/2", - `--x type: user path: "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/2"`}, - - {"wayland", - &aclUpdateOp{hst.EWayland, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/wayland", []acl.Perm{acl.Read, acl.Write}}, - hst.EWayland, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/wayland", - `rw- type: wayland path: "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/wayland"`}, - - {"x11", - &aclUpdateOp{hst.EX11, "/tmp/.X11-unix/X0", []acl.Perm{acl.Read, acl.Execute}}, - hst.EX11, "/tmp/.X11-unix/X0", - `r-x type: x11 path: "/tmp/.X11-unix/X0"`}, - - {"dbus", - &aclUpdateOp{hst.EDBus, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/bus", []acl.Perm{acl.Write, acl.Execute}}, - hst.EDBus, "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/bus", - `-wx type: dbus path: "/tmp/hakurei.0/27d81d567f8fae7f33278eec45da9446/bus"`}, - - {"pulseaudio", - &aclUpdateOp{hst.EPulse, "/run/user/1971/hakurei/27d81d567f8fae7f33278eec45da9446/pulse", []acl.Perm{acl.Read, acl.Write, acl.Execute}}, - hst.EPulse, "/run/user/1971/hakurei/27d81d567f8fae7f33278eec45da9446/pulse", - `rwx type: pulseaudio path: "/run/user/1971/hakurei/27d81d567f8fae7f33278eec45da9446/pulse"`}, - }) -} diff --git a/system/dbus.go b/system/dbus.go deleted file mode 100644 index 424f9dfa..00000000 --- a/system/dbus.go +++ /dev/null @@ -1,207 +0,0 @@ -package system - -import ( - "bytes" - "context" - "errors" - "fmt" - "log" - "reflect" - "strings" - "sync" - "syscall" - - "hakurei.app/container" - "hakurei.app/hst" - "hakurei.app/system/dbus" -) - -// ErrDBusConfig is returned when a required [hst.BusConfig] argument is nil. -var ErrDBusConfig = errors.New("dbus config not supplied") - -// MustProxyDBus calls ProxyDBus and panics if an error is returned. -func (sys *I) MustProxyDBus( - session, system *hst.BusConfig, - sessionBus, systemBus dbus.ProxyPair, -) *I { - if err := sys.ProxyDBus(session, system, sessionBus, systemBus); err != nil { - panic(err.Error()) - } else { - return sys - } -} - -// ProxyDBus finalises configuration ahead of time and starts xdg-dbus-proxy via [dbus] and terminates it on revert. -// This [Op] is always [Process] scoped. -func (sys *I) ProxyDBus( - session, system *hst.BusConfig, - sessionBus, systemBus dbus.ProxyPair, -) error { - d := new(dbusProxyOp) - - // session bus is required as otherwise this is effectively a very expensive noop - if session == nil { - return newOpErrorMessage("dbus", ErrDBusConfig, - "attempted to create message bus proxy args without session bus config", false) - } - - // system bus is optional - d.system = system != nil - - d.out = &linePrefixWriter{println: log.Println, prefix: "(dbus) ", buf: new(strings.Builder)} - if final, err := sys.dbusFinalise(sessionBus, systemBus, session, system); err != nil { - if errors.Is(err, syscall.EINVAL) { - return newOpErrorMessage("dbus", err, - "message bus proxy configuration contains NUL byte", false) - } - return newOpErrorMessage("dbus", err, - fmt.Sprintf("cannot finalise message bus proxy: %v", err), false) - } else { - if sys.msg.IsVerbose() { - sys.msg.Verbose("session bus proxy:", dbus.Args(session, sessionBus)) - if system != nil { - sys.msg.Verbose("system bus proxy:", dbus.Args(system, systemBus)) - } - - // this calls the argsWt String method - sys.msg.Verbose("message bus proxy final args:", final.WriterTo) - } - - d.final = final - } - - sys.ops = append(sys.ops, d) - return nil -} - -// dbusProxyOp implements [I.ProxyDBus]. -type dbusProxyOp struct { - proxy *dbus.Proxy // populated during apply - - final *dbus.Final - out *linePrefixWriter - // whether system bus proxy is enabled - system bool -} - -func (d *dbusProxyOp) Type() hst.Enablement { return Process } - -func (d *dbusProxyOp) apply(sys *I) error { - sys.msg.Verbosef("session bus proxy on %q for upstream %q", d.final.Session[1], d.final.Session[0]) - if d.system { - sys.msg.Verbosef("system bus proxy on %q for upstream %q", d.final.System[1], d.final.System[0]) - } - - d.proxy = dbus.New(sys.ctx, sys.msg, d.final, d.out) - if err := sys.dbusProxyStart(d.proxy); err != nil { - d.out.Dump() - return newOpErrorMessage("dbus", err, - fmt.Sprintf("cannot start message bus proxy: %v", err), false) - } - sys.msg.Verbose("starting message bus proxy", d.proxy) - return nil -} - -func (d *dbusProxyOp) revert(sys *I, _ *Criteria) error { - // criteria ignored here since dbus is always process-scoped - sys.msg.Verbose("terminating message bus proxy") - sys.dbusProxyClose(d.proxy) - - exitMessage := "message bus proxy exit" - defer func() { sys.msg.Verbose(exitMessage) }() - - if d.out != nil { - d.out.Dump() - } - - err := sys.dbusProxyWait(d.proxy) - if errors.Is(err, context.Canceled) { - exitMessage = "message bus proxy canceled upstream" - err = nil - } - return newOpErrorMessage("dbus", err, - fmt.Sprintf("message bus proxy error: %v", err), true) -} - -func (d *dbusProxyOp) Is(o Op) bool { - target, ok := o.(*dbusProxyOp) - return ok && d != nil && target != nil && - d.system == target.system && - d.final != nil && target.final != nil && - d.final.Session == target.final.Session && - d.final.System == target.final.System && - dbus.EqualAddrEntries(d.final.SessionUpstream, target.final.SessionUpstream) && - dbus.EqualAddrEntries(d.final.SystemUpstream, target.final.SystemUpstream) && - reflect.DeepEqual(d.final.WriterTo, target.final.WriterTo) -} - -func (d *dbusProxyOp) Path() string { return container.Nonexistent } -func (d *dbusProxyOp) String() string { return d.proxy.String() } - -const ( - // lpwSizeThreshold is the threshold of bytes written to linePrefixWriter which, - // if reached or exceeded, causes linePrefixWriter to drop all future writes. - lpwSizeThreshold = 1 << 24 -) - -// linePrefixWriter calls println with a prefix for every line written. -type linePrefixWriter struct { - prefix string - println func(v ...any) - - n int - msg []string - buf *strings.Builder - - mu sync.RWMutex -} - -func (s *linePrefixWriter) Write(p []byte) (n int, err error) { - s.mu.Lock() - defer s.mu.Unlock() - return s.write(p, 0) -} - -func (s *linePrefixWriter) write(p []byte, a int) (int, error) { - if s.n >= lpwSizeThreshold { - if len(p) == 0 { - return a, nil - } - return a, syscall.ENOMEM - } - - if i := bytes.IndexByte(p, '\n'); i == -1 { - n, _ := s.buf.Write(p) - s.n += n - return a + n, nil - } else { - n, _ := s.buf.Write(p[:i]) - s.n += n + 1 - - v := s.buf.String() - if strings.HasPrefix(v, "init: ") { - s.n -= len(v) + 1 - // pass through container init messages - s.println(s.prefix + v) - } else { - s.msg = append(s.msg, v) - } - - s.buf.Reset() - return s.write(p[i+1:], a+n+1) - } -} - -func (s *linePrefixWriter) Dump() { - s.mu.RLock() - for _, m := range s.msg { - s.println(s.prefix + m) - } - if s.buf != nil && s.buf.Len() != 0 { - s.println("*" + s.prefix + s.buf.String()) - } - if s.n >= lpwSizeThreshold { - s.println("+" + s.prefix + "write threshold reached, output may be incomplete") - } - s.mu.RUnlock() -} diff --git a/system/dbus/address.go b/system/dbus/address.go deleted file mode 100644 index edee7e20..00000000 --- a/system/dbus/address.go +++ /dev/null @@ -1,193 +0,0 @@ -package dbus - -import ( - "bytes" - "encoding/hex" - "errors" - "fmt" - "slices" -) - -type AddrEntry struct { - Method string `json:"method"` - Values [][2]string `json:"values"` -} - -// EqualAddrEntries returns whether two slices of [AddrEntry] are equal. -func EqualAddrEntries(entries, target []AddrEntry) bool { - return slices.EqualFunc(entries, target, func(a AddrEntry, b AddrEntry) bool { - return a.Method == b.Method && slices.Equal(a.Values, b.Values) - }) -} - -// Parse parses D-Bus address according to -// https://dbus.freedesktop.org/doc/dbus-specification.html#addresses -func Parse(addr []byte) ([]AddrEntry, error) { - // Look for a semicolon - address := bytes.Split(bytes.TrimSuffix(addr, []byte{';'}), []byte{';'}) - - // Allocate for entries - v := make([]AddrEntry, len(address)) - - for i, s := range address { - var pairs [][]byte - - // Look for the colon : - if method, list, ok := bytes.Cut(s, []byte{':'}); !ok { - return v, &BadAddressError{ErrNoColon, i, s, -1, nil} - } else { - pairs = bytes.Split(list, []byte{','}) - v[i].Method = string(method) - v[i].Values = make([][2]string, len(pairs)) - } - - for j, pair := range pairs { - key, value, ok := bytes.Cut(pair, []byte{'='}) - if !ok { - return v, &BadAddressError{ErrBadPairSep, i, s, j, pair} - } - if len(key) == 0 { - return v, &BadAddressError{ErrBadPairKey, i, s, j, pair} - } - if len(value) == 0 { - return v, &BadAddressError{ErrBadPairVal, i, s, j, pair} - } - v[i].Values[j][0] = string(key) - - if val, errno := unescapeValue(value); errno != errSuccess { - return v, &BadAddressError{errno, i, s, j, pair} - } else { - v[i].Values[j][1] = string(val) - } - } - } - - return v, nil -} - -func unescapeValue(v []byte) (val []byte, errno ParseError) { - if l := len(v) - (bytes.Count(v, []byte{'%'}) * 2); l < 0 { - errno = ErrBadValLength - return - } else { - val = make([]byte, l) - } - - var i, skip int - for iu, b := range v { - if skip > 0 { - skip-- - continue - } - - if ib := bytes.IndexByte([]byte("-_/.\\*"), b); ib != -1 { // - // _/.\* - goto opt - } else if b >= '0' && b <= '9' { // 0-9 - goto opt - } else if b >= 'A' && b <= 'Z' { // A-Z - goto opt - } else if b >= 'a' && b <= 'z' { // a-z - goto opt - } - - if b != '%' { - errno = ErrBadValByte - break - } - - skip += 2 - if iu+2 >= len(v) { - errno = ErrBadValHexLength - break - } - if c, err := hex.Decode(val[i:i+1], v[iu+1:iu+3]); err != nil { - if errors.As(err, new(hex.InvalidByteError)) { - errno = ErrBadValHexByte - break - } - // unreachable - panic(err.Error()) - } else if c != 1 { - // unreachable - panic(fmt.Sprintf("invalid decode length %d", c)) - } - i++ - continue - - opt: - val[i] = b - i++ - } - - return -} - -type ParseError uint8 - -func (e ParseError) Error() string { - switch e { - case errSuccess: - panic("attempted to return success as error") - case ErrNoColon: - return "address does not contain a colon" - case ErrBadPairSep: - return "'=' character not found" - case ErrBadPairKey: - return "'=' character has no key preceding it" - case ErrBadPairVal: - return "'=' character has no value following it" - case ErrBadValLength: - return "unescaped value has impossible length" - case ErrBadValByte: - return "in D-Bus address, characters other than [-0-9A-Za-z_/.\\*] should have been escaped" - case ErrBadValHexLength: - return "in D-Bus address, percent character was not followed by two hex digits" - case ErrBadValHexByte: - return "in D-Bus address, percent character was followed by characters other than hex digits" - - default: - return fmt.Sprintf("parse error %d", e) - } -} - -const ( - errSuccess ParseError = iota - ErrNoColon - ErrBadPairSep - ErrBadPairKey - ErrBadPairVal - ErrBadValLength - ErrBadValByte - ErrBadValHexLength - ErrBadValHexByte -) - -type BadAddressError struct { - // error type - Type ParseError - - // bad entry position - EntryPos int - // bad entry value - EntryVal []byte - - // bad pair position - PairPos int - // bad pair value - PairVal []byte -} - -func (a *BadAddressError) Is(err error) bool { - var b *BadAddressError - return errors.As(err, &b) && a.Type == b.Type && - a.EntryPos == b.EntryPos && slices.Equal(a.EntryVal, b.EntryVal) && - a.PairPos == b.PairPos && slices.Equal(a.PairVal, b.PairVal) -} - -func (a *BadAddressError) Error() string { - return a.Type.Error() -} - -func (a *BadAddressError) Unwrap() error { - return a.Type -} diff --git a/system/dbus/address_escape_test.go b/system/dbus/address_escape_test.go deleted file mode 100644 index 4ef48f8b..00000000 --- a/system/dbus/address_escape_test.go +++ /dev/null @@ -1,59 +0,0 @@ -package dbus - -import ( - "testing" -) - -func TestUnescapeValue(t *testing.T) { - t.Parallel() - - testCases := []struct { - value string - want string - wantErr ParseError - }{ - // upstream test cases - {value: "abcde", want: "abcde"}, - {value: "", want: ""}, - {value: "%20%20", want: " "}, - {value: "%24", want: "$"}, - {value: "%25", want: "%"}, - {value: "abc%24", want: "abc$"}, - {value: "%24abc", want: "$abc"}, - {value: "abc%24abc", want: "abc$abc"}, - {value: "/", want: "/"}, - {value: "-", want: "-"}, - {value: "_", want: "_"}, - {value: "A", want: "A"}, - {value: "I", want: "I"}, - {value: "Z", want: "Z"}, - {value: "a", want: "a"}, - {value: "i", want: "i"}, - {value: "z", want: "z"}, - /* Bug: https://bugs.freedesktop.org/show_bug.cgi?id=53499 */ - {value: "%c3%b6", want: "\xc3\xb6"}, - - {value: "%a", wantErr: ErrBadValHexLength}, - {value: "%q", wantErr: ErrBadValHexLength}, - {value: "%az", wantErr: ErrBadValHexByte}, - {value: "%%", wantErr: ErrBadValLength}, - {value: "%$$", wantErr: ErrBadValHexByte}, - {value: "abc%a", wantErr: ErrBadValHexLength}, - {value: "%axyz", wantErr: ErrBadValHexByte}, - {value: "%", wantErr: ErrBadValLength}, - {value: "$", wantErr: ErrBadValByte}, - {value: " ", wantErr: ErrBadValByte}, - } - - for _, tc := range testCases { - t.Run("unescape "+tc.value, func(t *testing.T) { - t.Parallel() - - if got, errno := unescapeValue([]byte(tc.value)); errno != tc.wantErr { - t.Errorf("unescapeValue() errno = %v, wantErr %v", errno, tc.wantErr) - } else if tc.wantErr == errSuccess && string(got) != tc.want { - t.Errorf("unescapeValue() = %q, want %q", got, tc.want) - } - }) - } -} diff --git a/system/dbus/address_test.go b/system/dbus/address_test.go deleted file mode 100644 index 5de90535..00000000 --- a/system/dbus/address_test.go +++ /dev/null @@ -1,123 +0,0 @@ -package dbus_test - -import ( - "errors" - "reflect" - "testing" - - "hakurei.app/system/dbus" -) - -func TestParse(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - addr string - want []dbus.AddrEntry - wantErr error - }{ - { - name: "simple session unix", - addr: "unix:path=/run/user/1971/bus", - want: []dbus.AddrEntry{{ - Method: "unix", - Values: [][2]string{{"path", "/run/user/1971/bus"}}, - }}, - }, - { - name: "simple upper escape", - addr: "debug:name=Test,cat=cute,escaped=%c3%b6", - want: []dbus.AddrEntry{{ - Method: "debug", - Values: [][2]string{ - {"name", "Test"}, - {"cat", "cute"}, - {"escaped", "\xc3\xb6"}, - }, - }}, - }, - { - name: "simple bad escape", - addr: "debug:name=%", - wantErr: &dbus.BadAddressError{Type: dbus.ErrBadValLength, - EntryPos: 0, EntryVal: []byte("debug:name=%"), PairPos: 0, PairVal: []byte("name=%")}, - }, - - // upstream test cases - { - name: "full address success", - addr: "unix:path=/tmp/foo;debug:name=test,sliff=sloff;", - want: []dbus.AddrEntry{ - {Method: "unix", Values: [][2]string{{"path", "/tmp/foo"}}}, - {Method: "debug", Values: [][2]string{{"name", "test"}, {"sliff", "sloff"}}}, - }, - }, - { - name: "empty address", - addr: "", - wantErr: &dbus.BadAddressError{Type: dbus.ErrNoColon, - EntryVal: []byte{}, PairPos: -1}, - }, - { - name: "no body", - addr: "foo", - wantErr: &dbus.BadAddressError{Type: dbus.ErrNoColon, - EntryPos: 0, EntryVal: []byte("foo"), PairPos: -1}, - }, - { - name: "no pair separator", - addr: "foo:bar", - wantErr: &dbus.BadAddressError{Type: dbus.ErrBadPairSep, - EntryPos: 0, EntryVal: []byte("foo:bar"), PairPos: 0, PairVal: []byte("bar")}, - }, - { - name: "no pair separator multi pair", - addr: "foo:bar,baz", - wantErr: &dbus.BadAddressError{Type: dbus.ErrBadPairSep, - EntryPos: 0, EntryVal: []byte("foo:bar,baz"), PairPos: 0, PairVal: []byte("bar")}, - }, - { - name: "no pair separator single valid pair", - addr: "foo:bar=foo,baz", - wantErr: &dbus.BadAddressError{Type: dbus.ErrBadPairSep, - EntryPos: 0, EntryVal: []byte("foo:bar=foo,baz"), PairPos: 1, PairVal: []byte("baz")}, - }, - { - name: "no body single valid address", - addr: "foo:bar=foo;baz", - wantErr: &dbus.BadAddressError{Type: dbus.ErrNoColon, - EntryPos: 1, EntryVal: []byte("baz"), PairPos: -1}, - }, - { - name: "no key", - addr: "foo:=foo", - wantErr: &dbus.BadAddressError{Type: dbus.ErrBadPairKey, - EntryPos: 0, EntryVal: []byte("foo:=foo"), PairPos: 0, PairVal: []byte("=foo")}, - }, - { - name: "no value", - addr: "foo:foo=", - wantErr: &dbus.BadAddressError{Type: dbus.ErrBadPairVal, - EntryPos: 0, EntryVal: []byte("foo:foo="), PairPos: 0, PairVal: []byte("foo=")}, - }, - { - name: "no pair separator single valid pair trailing", - addr: "foo:foo,bar=baz", - wantErr: &dbus.BadAddressError{Type: dbus.ErrBadPairSep, - EntryPos: 0, EntryVal: []byte("foo:foo,bar=baz"), PairPos: 0, PairVal: []byte("foo")}, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - if got, err := dbus.Parse([]byte(tc.addr)); !errors.Is(err, tc.wantErr) { - t.Errorf("Parse() error = %v, wantErr %v", err, tc.wantErr) - } else if tc.wantErr == nil && !reflect.DeepEqual(got, tc.want) { - t.Errorf("Parse() = %#v, want %#v", got, tc.want) - } - }) - } -} diff --git a/system/dbus/config.go b/system/dbus/config.go deleted file mode 100644 index 2a09ee90..00000000 --- a/system/dbus/config.go +++ /dev/null @@ -1,71 +0,0 @@ -package dbus - -import ( - "hakurei.app/hst" -) - -// ProxyPair is an upstream dbus address and a downstream socket path. -type ProxyPair [2]string - -// Args returns the xdg-dbus-proxy arguments equivalent of [hst.BusConfig]. -func Args(c *hst.BusConfig, bus ProxyPair) (args []string) { - argc := 2 + len(c.See) + len(c.Talk) + len(c.Own) + len(c.Call) + len(c.Broadcast) - if c.Log { - argc++ - } - if c.Filter { - argc++ - } - - args = make([]string, 0, argc) - args = append(args, bus[0], bus[1]) - if c.Filter { - args = append(args, "--filter") - } - for _, name := range c.See { - args = append(args, "--see="+name) - } - for _, name := range c.Talk { - args = append(args, "--talk="+name) - } - for _, name := range c.Own { - args = append(args, "--own="+name) - } - for name, rule := range c.Call { - args = append(args, "--call="+name+"="+rule) - } - for name, rule := range c.Broadcast { - args = append(args, "--broadcast="+name+"="+rule) - } - if c.Log { - args = append(args, "--log") - } - - return -} - -// NewConfig returns the address of a new [hst.BusConfig] with optional defaults. -func NewConfig(id string, defaults, mpris bool) *hst.BusConfig { - c := hst.BusConfig{ - Call: make(map[string]string), - Broadcast: make(map[string]string), - - Filter: true, - } - - if defaults { - c.Talk = []string{"org.freedesktop.DBus", "org.freedesktop.Notifications"} - - c.Call["org.freedesktop.portal.*"] = "*" - c.Broadcast["org.freedesktop.portal.*"] = "@/org/freedesktop/portal/*" - - if id != "" { - c.Own = []string{id + ".*"} - if mpris { - c.Own = append(c.Own, "org.mpris.MediaPlayer2."+id+".*") - } - } - } - - return &c -} diff --git a/system/dbus/config_test.go b/system/dbus/config_test.go deleted file mode 100644 index 644c766f..00000000 --- a/system/dbus/config_test.go +++ /dev/null @@ -1,124 +0,0 @@ -package dbus_test - -import ( - "reflect" - "slices" - "strings" - "testing" - - "hakurei.app/hst" - "hakurei.app/system/dbus" -) - -func TestConfigArgs(t *testing.T) { - t.Parallel() - - for _, tc := range testCasesExt { - if tc.wantErr { - // args does not check for nulls - continue - } - - t.Run("build arguments for "+tc.id, func(t *testing.T) { - t.Parallel() - - if got := dbus.Args(tc.c, tc.bus); !slices.Equal(got, tc.want) { - t.Errorf("Args: %v, want %v", got, tc.want) - } - }) - } -} - -func TestNewConfig(t *testing.T) { - t.Parallel() - ids := [...]string{"org.chromium.Chromium", "dev.vencord.Vesktop"} - - type newTestCase struct { - id string - args [2]bool - want *hst.BusConfig - } - - // populate tests from IDs in generic tests - tcs := make([]newTestCase, 0, (len(ids)+1)*4) - // tests for defaults without id - tcs = append(tcs, - newTestCase{"", [2]bool{false, false}, &hst.BusConfig{ - Call: make(map[string]string), - Broadcast: make(map[string]string), - Filter: true, - }}, - newTestCase{"", [2]bool{false, true}, &hst.BusConfig{ - Call: make(map[string]string), - Broadcast: make(map[string]string), - Filter: true, - }}, - newTestCase{"", [2]bool{true, false}, &hst.BusConfig{ - Talk: []string{"org.freedesktop.DBus", "org.freedesktop.Notifications"}, - Call: map[string]string{"org.freedesktop.portal.*": "*"}, - Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"}, - Filter: true, - }}, - newTestCase{"", [2]bool{true, true}, &hst.BusConfig{ - Talk: []string{"org.freedesktop.DBus", "org.freedesktop.Notifications"}, - Call: map[string]string{"org.freedesktop.portal.*": "*"}, - Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"}, - Filter: true, - }}, - ) - for _, id := range ids { - tcs = append(tcs, - newTestCase{id, [2]bool{false, false}, &hst.BusConfig{ - Call: make(map[string]string), - Broadcast: make(map[string]string), - Filter: true, - }}, - newTestCase{id, [2]bool{false, true}, &hst.BusConfig{ - Call: make(map[string]string), - Broadcast: make(map[string]string), - Filter: true, - }}, - newTestCase{id, [2]bool{true, false}, &hst.BusConfig{ - Talk: []string{"org.freedesktop.DBus", "org.freedesktop.Notifications"}, - Own: []string{id + ".*"}, - Call: map[string]string{"org.freedesktop.portal.*": "*"}, - Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"}, - Filter: true, - }}, - newTestCase{id, [2]bool{true, true}, &hst.BusConfig{ - Talk: []string{"org.freedesktop.DBus", "org.freedesktop.Notifications"}, - Own: []string{id + ".*", "org.mpris.MediaPlayer2." + id + ".*"}, - Call: map[string]string{"org.freedesktop.portal.*": "*"}, - Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"}, - Filter: true, - }}, - ) - } - - for _, tc := range tcs { - name := new(strings.Builder) - name.WriteString("create new configuration struct") - - if tc.args[0] { - name.WriteString(" with builtin defaults") - if tc.args[1] { - name.WriteString(" (mpris)") - } - } - - if tc.id != "" { - name.WriteString(" for application ID ") - name.WriteString(tc.id) - } - - t.Run(name.String(), func(t *testing.T) { - t.Parallel() - - if gotC := dbus.NewConfig(tc.id, tc.args[0], tc.args[1]); !reflect.DeepEqual(gotC, tc.want) { - t.Errorf("NewConfig(%q, %t, %t) = %v, want %v", - tc.id, tc.args[0], tc.args[1], - gotC, tc.want) - } - }) - } -} diff --git a/system/dbus/dbus.go b/system/dbus/dbus.go deleted file mode 100644 index 9ea89f51..00000000 --- a/system/dbus/dbus.go +++ /dev/null @@ -1,69 +0,0 @@ -// Package dbus wraps xdg-dbus-proxy and implements configuration and sandboxing of the underlying helper process. -package dbus - -import ( - "fmt" - "os" - "sync" -) - -const ( - /* - SessionBusAddress is the name of the environment variable where the address of the login session message bus is given in. - - If that variable is not set, applications may also try to read the address from the X Window System root window property _DBUS_SESSION_BUS_ADDRESS. - The root window property must have type STRING. The environment variable should have precedence over the root window property. - - The address of the login session message bus is given in the DBUS_SESSION_BUS_ADDRESS environment variable. - If DBUS_SESSION_BUS_ADDRESS is not set, or if it's set to the string "autolaunch:", - the system should use platform-specific methods of locating a running D-Bus session server, - or starting one if a running instance cannot be found. - Note that this mechanism is not recommended for attempting to determine if a daemon is running. - It is inherently racy to attempt to make this determination, since the bus daemon may be started just before or just after the determination is made. - Therefore, it is recommended that applications do not try to make this determination for their functionality purposes, and instead they should attempt to start the server. - - This package diverges from the specification, as the caller is unlikely to be an X client, or be in a position to autolaunch a dbus server. - So a fallback address with a socket located in the well-known default XDG_RUNTIME_DIR formatting is used. - */ - SessionBusAddress = "DBUS_SESSION_BUS_ADDRESS" - - /* - SystemBusAddress is the name of the environment variable where the address of the system message bus is given in. - - If that variable is not set, applications should try to connect to the well-known address unix:path=/var/run/dbus/system_bus_socket. - Implementations of the well-known system bus should listen on an address that will result in that connection being successful. - */ - SystemBusAddress = "DBUS_SYSTEM_BUS_ADDRESS" - - // FallbackSystemBusAddress is used when [SystemBusAddress] is not set. - FallbackSystemBusAddress = "unix:path=/var/run/dbus/system_bus_socket" -) - -var ( - address [2]string - addressOnce sync.Once -) - -// Address returns the session and system bus addresses copied from environment, -// or appropriate fallback values if they are not set. -func Address() (session, system string) { - addressOnce.Do(func() { - // resolve upstream session bus address - if addr, ok := os.LookupEnv(SessionBusAddress); !ok { - // fall back to default format - address[0] = fmt.Sprintf("unix:path=/run/user/%d/bus", os.Getuid()) - } else { - address[0] = addr - } - - // resolve upstream system bus address - if addr, ok := os.LookupEnv(SystemBusAddress); !ok { - // fall back to default hardcoded value - address[1] = FallbackSystemBusAddress - } else { - address[1] = addr - } - }) - - return address[0], address[1] -} diff --git a/system/dbus/dbus_test.go b/system/dbus/dbus_test.go deleted file mode 100644 index 3a9af578..00000000 --- a/system/dbus/dbus_test.go +++ /dev/null @@ -1,170 +0,0 @@ -package dbus_test - -import ( - "context" - "errors" - "fmt" - "io" - "os" - "strings" - "syscall" - "testing" - "time" - - "hakurei.app/internal/helper" - "hakurei.app/message" - "hakurei.app/system/dbus" -) - -func TestFinalise(t *testing.T) { - if _, err := dbus.Finalise(dbus.ProxyPair{}, dbus.ProxyPair{}, nil, nil); !errors.Is(err, syscall.EBADE) { - t.Errorf("Finalise: error = %v, want %v", - err, syscall.EBADE) - } - - for id, tc := range testCasePairs { - t.Run("create final for "+id, func(t *testing.T) { - var wt io.WriterTo - if v, err := dbus.Finalise(tc[0].bus, tc[1].bus, tc[0].c, tc[1].c); (errors.Is(err, syscall.EINVAL)) != tc[0].wantErr { - t.Errorf("Finalise: error = %v, wantErr %v", - err, tc[0].wantErr) - return - } else { - wt = v - } - - // rest of the tests happen for sealed instances - if tc[0].wantErr { - return - } - - // build null-terminated string from wanted args - want := new(strings.Builder) - args := append(tc[0].want, tc[1].want...) - for _, arg := range args { - want.WriteString(arg) - want.WriteByte(0) - } - - got := new(strings.Builder) - if _, err := wt.WriteTo(got); err != nil { - t.Errorf("WriteTo: error = %v", err) - } - - if want.String() != got.String() { - t.Errorf("Seal: %q, want %q", - got.String(), want.String()) - } - }) - } -} - -func TestProxyStartWaitCloseString(t *testing.T) { - t.Run("sandbox", func(t *testing.T) { testProxyFinaliseStartWaitCloseString(t, true) }) - t.Run("direct", func(t *testing.T) { testProxyFinaliseStartWaitCloseString(t, false) }) -} - -const ( - stubProxyTimeout = 5 * time.Second -) - -func testProxyFinaliseStartWaitCloseString(t *testing.T, useSandbox bool) { - { - oldWaitDelay := helper.WaitDelay - helper.WaitDelay = 16 * time.Second - t.Cleanup(func() { helper.WaitDelay = oldWaitDelay }) - } - - { - proxyName := dbus.ProxyName - dbus.ProxyName = os.Args[0] - t.Cleanup(func() { dbus.ProxyName = proxyName }) - } - - var p *dbus.Proxy - - t.Run("string for nil proxy", func(t *testing.T) { - want := "(invalid dbus proxy)" - if got := p.String(); got != want { - t.Errorf("String: %q, want %q", - got, want) - } - }) - - t.Run("invalid start", func(t *testing.T) { - if !useSandbox { - p = dbus.NewDirect(t.Context(), message.New(nil), nil, nil) - } else { - p = dbus.New(t.Context(), message.New(nil), nil, nil) - } - - if err := p.Start(); !errors.Is(err, syscall.ENOTRECOVERABLE) { - t.Errorf("Start: error = %q, wantErr %q", err, syscall.ENOTRECOVERABLE) - return - } - }) - - for id, tc := range testCasePairs { - // this test does not test errors - if tc[0].wantErr { - continue - } - - t.Run("proxy for "+id, func(t *testing.T) { - var final *dbus.Final - t.Run("finalise", func(t *testing.T) { - if v, err := dbus.Finalise(tc[0].bus, tc[1].bus, tc[0].c, tc[1].c); err != nil { - t.Errorf("Finalise: error = %v, wantErr %v", err, tc[0].wantErr) - return - } else { - final = v - } - }) - - ctx, cancel := context.WithTimeout(t.Context(), stubProxyTimeout) - defer cancel() - output := new(strings.Builder) - if !useSandbox { - p = dbus.NewDirect(ctx, message.New(nil), final, output) - } else { - p = dbus.New(ctx, message.New(nil), final, output) - } - - { // check invalid wait behaviour - wantErr := "dbus: not started" - if err := p.Wait(); err == nil || err.Error() != wantErr { - t.Errorf("Wait: error = %v, wantErr %v", err, wantErr) - } - } - - { // check string behaviour - want := "(unused dbus proxy)" - if got := p.String(); got != want { - t.Errorf("String: %q, want %q", got, want) - return - } - } - - if err := p.Start(); err != nil { - t.Fatalf("Start: error = %v", err) - } - - { // check running string behaviour - wantSubstr := fmt.Sprintf("%s --args=3 --fd=4", os.Args[0]) - if useSandbox { - wantSubstr = `argv: ["xdg-dbus-proxy" "--args=3" "--fd=4"], filter: true, rules: 0, flags: 0x1, presets: 0xf` - } - if got := p.String(); !strings.Contains(got, wantSubstr) { - t.Errorf("String: %q, want %q", - got, wantSubstr) - return - } - } - - p.Close() - if err := p.Wait(); err != nil { - t.Errorf("Wait: error = %v\noutput: %s", err, output.String()) - } - }) - } -} diff --git a/system/dbus/deprecated.go b/system/dbus/deprecated.go new file mode 100644 index 00000000..67a309c2 --- /dev/null +++ b/system/dbus/deprecated.go @@ -0,0 +1,115 @@ +// Package dbus exposes the internal/system/dbus package. +// +// Deprecated: This package will be removed in 0.4. +package dbus + +import ( + "context" + "io" + _ "unsafe" // for go:linkname + + "hakurei.app/hst" + "hakurei.app/internal/system/dbus" + "hakurei.app/message" +) + +type AddrEntry = dbus.AddrEntry + +// EqualAddrEntries returns whether two slices of [AddrEntry] are equal. +// +//go:linkname EqualAddrEntries hakurei.app/internal/system/dbus.EqualAddrEntries +func EqualAddrEntries(entries, target []AddrEntry) bool + +// Parse parses D-Bus address according to +// https://dbus.freedesktop.org/doc/dbus-specification.html#addresses +// +//go:linkname Parse hakurei.app/internal/system/dbus.Parse +func Parse(addr []byte) ([]AddrEntry, error) + +type ParseError = dbus.ParseError + +const ( + ErrNoColon = dbus.ErrNoColon + ErrBadPairSep = dbus.ErrBadPairSep + ErrBadPairKey = dbus.ErrBadPairKey + ErrBadPairVal = dbus.ErrBadPairVal + ErrBadValLength = dbus.ErrBadValLength + ErrBadValByte = dbus.ErrBadValByte + ErrBadValHexLength = dbus.ErrBadValHexLength + ErrBadValHexByte = dbus.ErrBadValHexByte +) + +type BadAddressError = dbus.BadAddressError + +// ProxyPair is an upstream dbus address and a downstream socket path. +type ProxyPair = dbus.ProxyPair + +// Args returns the xdg-dbus-proxy arguments equivalent of [hst.BusConfig]. +// +//go:linkname Args hakurei.app/internal/system/dbus.Args +func Args(c *hst.BusConfig, bus ProxyPair) (args []string) + +// NewConfig returns the address of a new [hst.BusConfig] with optional defaults. +// +//go:linkname NewConfig hakurei.app/internal/system/dbus.NewConfig +func NewConfig(id string, defaults, mpris bool) *hst.BusConfig + +const ( + /* + SessionBusAddress is the name of the environment variable where the address of the login session message bus is given in. + + If that variable is not set, applications may also try to read the address from the X Window System root window property _DBUS_SESSION_BUS_ADDRESS. + The root window property must have type STRING. The environment variable should have precedence over the root window property. + + The address of the login session message bus is given in the DBUS_SESSION_BUS_ADDRESS environment variable. + If DBUS_SESSION_BUS_ADDRESS is not set, or if it's set to the string "autolaunch:", + the system should use platform-specific methods of locating a running D-Bus session server, + or starting one if a running instance cannot be found. + Note that this mechanism is not recommended for attempting to determine if a daemon is running. + It is inherently racy to attempt to make this determination, since the bus daemon may be started just before or just after the determination is made. + Therefore, it is recommended that applications do not try to make this determination for their functionality purposes, and instead they should attempt to start the server. + + This package diverges from the specification, as the caller is unlikely to be an X client, or be in a position to autolaunch a dbus server. + So a fallback address with a socket located in the well-known default XDG_RUNTIME_DIR formatting is used. + */ + SessionBusAddress = dbus.SessionBusAddress + + /* + SystemBusAddress is the name of the environment variable where the address of the system message bus is given in. + + If that variable is not set, applications should try to connect to the well-known address unix:path=/var/run/dbus/system_bus_socket. + Implementations of the well-known system bus should listen on an address that will result in that connection being successful. + */ + SystemBusAddress = dbus.SystemBusAddress + + // FallbackSystemBusAddress is used when [SystemBusAddress] is not set. + FallbackSystemBusAddress = dbus.FallbackSystemBusAddress +) + +// Address returns the session and system bus addresses copied from environment, +// or appropriate fallback values if they are not set. +// +//go:linkname Address hakurei.app/internal/system/dbus.Address +func Address() (session, system string) + +// ProxyName is the file name or path to the proxy program. +// Overriding ProxyName will only affect Proxy instance created after the change. +// +//go:linkname ProxyName hakurei.app/internal/system/dbus.ProxyName +var ProxyName string + +// Proxy holds the state of a xdg-dbus-proxy process, and should never be copied. +type Proxy = dbus.Proxy + +// Final describes the outcome of a proxy configuration. +type Final = dbus.Final + +// Finalise creates a checked argument writer for [Proxy]. +// +//go:linkname Finalise hakurei.app/internal/system/dbus.Finalise +func Finalise(sessionBus, systemBus ProxyPair, session, system *hst.BusConfig) (final *Final, err error) + +// New returns a new instance of [Proxy]. +// +//go:linkname New hakurei.app/internal/system/dbus.New +func New(ctx context.Context, msg message.Msg, final *Final, output io.Writer) *Proxy diff --git a/system/dbus/export_test.go b/system/dbus/export_test.go deleted file mode 100644 index bc24f3f3..00000000 --- a/system/dbus/export_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package dbus - -import ( - "context" - "io" - - "hakurei.app/message" -) - -// NewDirect returns a new instance of [Proxy] with its sandbox disabled. -func NewDirect(ctx context.Context, msg message.Msg, final *Final, output io.Writer) *Proxy { - p := New(ctx, msg, final, output) - p.useSandbox = false - return p -} diff --git a/system/dbus/proc.go b/system/dbus/proc.go deleted file mode 100644 index 0bd9c6b3..00000000 --- a/system/dbus/proc.go +++ /dev/null @@ -1,188 +0,0 @@ -package dbus - -import ( - "context" - "errors" - "os" - "os/exec" - "strconv" - "syscall" - - "hakurei.app/container" - "hakurei.app/container/check" - "hakurei.app/container/seccomp" - "hakurei.app/container/std" - "hakurei.app/internal/helper" - "hakurei.app/ldd" -) - -// Start starts and configures a D-Bus proxy process. -func (p *Proxy) Start() error { - if p.final == nil || p.final.WriterTo == nil { - return syscall.ENOTRECOVERABLE - } - - p.mu.Lock() - defer p.mu.Unlock() - p.pmu.Lock() - defer p.pmu.Unlock() - - if p.cancel != nil || p.cause != nil { - return errors.New("dbus: already started") - } - - ctx, cancel := context.WithCancelCause(p.ctx) - - if !p.useSandbox { - p.helper = helper.NewDirect(ctx, p.name, p.final, true, argF, func(cmd *exec.Cmd) { - if p.output != nil { - cmd.Stdout, cmd.Stderr = p.output, p.output - } - cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} - cmd.Env = make([]string, 0) - }, nil) - } else { - var toolPath *check.Absolute - if a, err := check.NewAbs(p.name); err != nil { - if p.name, err = exec.LookPath(p.name); err != nil { - return err - } else if toolPath, err = check.NewAbs(p.name); err != nil { - return err - } - } else { - toolPath = a - } - - var libPaths []*check.Absolute - if entries, err := ldd.Exec(ctx, p.msg, toolPath.String()); err != nil { - return err - } else { - libPaths = ldd.Path(entries) - } - - p.helper = helper.New( - ctx, p.msg, toolPath, "xdg-dbus-proxy", - p.final, true, - argF, func(z *container.Container) { - z.SeccompFlags |= seccomp.AllowMultiarch - z.SeccompPresets |= std.PresetStrict - z.Hostname = "hakurei-dbus" - if p.output != nil { - z.Stdout, z.Stderr = p.output, p.output - } - - // these lib paths are unpredictable, so mount them first so they cannot cover anything - for _, name := range libPaths { - z.Bind(name, name, 0) - } - - // upstream bus directories - upstreamPaths := make([]*check.Absolute, 0, 2) - for _, addr := range [][]AddrEntry{p.final.SessionUpstream, p.final.SystemUpstream} { - for _, ent := range addr { - if ent.Method != "unix" { - continue - } - for _, pair := range ent.Values { - if pair[0] != "path" { - continue - } - if a, err := check.NewAbs(pair[1]); err != nil { - continue - } else { - upstreamPaths = append(upstreamPaths, a.Dir()) - } - } - } - } - check.SortAbs(upstreamPaths) - upstreamPaths = check.CompactAbs(upstreamPaths) - for _, name := range upstreamPaths { - z.Bind(name, name, 0) - } - z.HostNet = len(upstreamPaths) == 0 - z.HostAbstract = z.HostNet - - // parent directories of bind paths - sockDirPaths := make([]*check.Absolute, 0, 2) - if a, err := check.NewAbs(p.final.Session[1]); err == nil { - sockDirPaths = append(sockDirPaths, a.Dir()) - } - if a, err := check.NewAbs(p.final.System[1]); err == nil { - sockDirPaths = append(sockDirPaths, a.Dir()) - } - check.SortAbs(sockDirPaths) - sockDirPaths = check.CompactAbs(sockDirPaths) - for _, name := range sockDirPaths { - z.Bind(name, name, std.BindWritable) - } - - // xdg-dbus-proxy bin path - binPath := toolPath.Dir() - z.Bind(binPath, binPath, 0) - }, nil) - } - - if err := p.helper.Start(); err != nil { - cancel(err) - p.helper = nil - return err - } - - p.cancel, p.cause = cancel, func() error { return context.Cause(ctx) } - return nil -} - -var proxyClosed = errors.New("proxy closed") - -// Wait blocks until xdg-dbus-proxy exits and releases resources. -func (p *Proxy) Wait() error { - p.mu.RLock() - defer p.mu.RUnlock() - - p.pmu.RLock() - if p.helper == nil || p.cancel == nil || p.cause == nil { - p.pmu.RUnlock() - return errors.New("dbus: not started") - } - - var errs [3]error - - errs[0] = p.helper.Wait() - if errors.Is(errs[0], context.Canceled) && - errors.Is(p.cause(), proxyClosed) { - errs[0] = nil - } - p.pmu.RUnlock() - - // ensure socket removal so ephemeral directory is empty at revert - if err := os.Remove(p.final.Session[1]); err != nil && !errors.Is(err, os.ErrNotExist) { - errs[1] = err - } - if p.final.System[1] != "" { - if err := os.Remove(p.final.System[1]); err != nil && !errors.Is(err, os.ErrNotExist) { - errs[2] = err - } - } - - return errors.Join(errs[:]...) -} - -// Close cancels the context passed to the helper instance attached to xdg-dbus-proxy. -func (p *Proxy) Close() { - p.pmu.Lock() - defer p.pmu.Unlock() - - if p.cancel == nil { - panic("dbus: not started") - } - p.cancel(proxyClosed) -} - -func argF(argsFd, statFd int) []string { - if statFd == -1 { - return []string{"--args=" + strconv.Itoa(argsFd)} - } else { - return []string{"--args=" + strconv.Itoa(argsFd), "--fd=" + strconv.Itoa(statFd)} - } -} diff --git a/system/dbus/proc_test.go b/system/dbus/proc_test.go deleted file mode 100644 index 544856ed..00000000 --- a/system/dbus/proc_test.go +++ /dev/null @@ -1,11 +0,0 @@ -package dbus_test - -import ( - "os" - "testing" - - "hakurei.app/container" - "hakurei.app/internal/helper" -) - -func TestMain(m *testing.M) { container.TryArgv0(nil); helper.InternalHelperStub(); os.Exit(m.Run()) } diff --git a/system/dbus/proxy.go b/system/dbus/proxy.go deleted file mode 100644 index 9de49c41..00000000 --- a/system/dbus/proxy.go +++ /dev/null @@ -1,105 +0,0 @@ -package dbus - -import ( - "context" - "io" - "sync" - "syscall" - - "hakurei.app/hst" - "hakurei.app/internal/helper" - "hakurei.app/message" -) - -// ProxyName is the file name or path to the proxy program. -// Overriding ProxyName will only affect Proxy instance created after the change. -var ProxyName = "xdg-dbus-proxy" - -// Proxy holds the state of a xdg-dbus-proxy process, and should never be copied. -type Proxy struct { - helper helper.Helper - ctx context.Context - msg message.Msg - - cancel context.CancelCauseFunc - cause func() error - - final *Final - output io.Writer - useSandbox bool - - name string - - mu, pmu sync.RWMutex -} - -func (p *Proxy) String() string { - if p == nil { - return "(invalid dbus proxy)" - } - - p.mu.RLock() - defer p.mu.RUnlock() - - if p.helper != nil { - return p.helper.String() - } - - return "(unused dbus proxy)" -} - -// Final describes the outcome of a proxy configuration. -type Final struct { - Session, System ProxyPair - // parsed upstream address - SessionUpstream, SystemUpstream []AddrEntry - io.WriterTo -} - -// Finalise creates a checked argument writer for [Proxy]. -func Finalise(sessionBus, systemBus ProxyPair, session, system *hst.BusConfig) (final *Final, err error) { - if session == nil && system == nil { - return nil, syscall.EBADE - } - - var args []string - if session != nil { - if err = session.CheckInterfaces("session"); err != nil { - return - } - args = append(args, Args(session, sessionBus)...) - } - if system != nil { - if err = system.CheckInterfaces("system"); err != nil { - return - } - args = append(args, Args(system, systemBus)...) - } - - final = &Final{Session: sessionBus, System: systemBus} - - final.WriterTo, err = helper.NewCheckedArgs(args...) - if err != nil { - return - } - - if session != nil { - final.SessionUpstream, err = Parse([]byte(final.Session[0])) - if err != nil { - return - } - } - if system != nil { - final.SystemUpstream, err = Parse([]byte(final.System[0])) - if err != nil { - return - } - } - - return -} - -// New returns a new instance of [Proxy]. -func New(ctx context.Context, msg message.Msg, final *Final, output io.Writer) *Proxy { - return &Proxy{name: ProxyName, ctx: ctx, msg: msg, final: final, output: output, useSandbox: true} -} diff --git a/system/dbus/samples_test.go b/system/dbus/samples_test.go deleted file mode 100644 index 7db3d6b9..00000000 --- a/system/dbus/samples_test.go +++ /dev/null @@ -1,214 +0,0 @@ -package dbus_test - -import ( - "hakurei.app/hst" -) - -const ( - sampleHostPath = "/tmp/bus" - sampleHostAddr = "unix:path=" + sampleHostPath - sampleBindPath = "/tmp/proxied_bus" -) - -var samples = []dbusTestCase{ - { - "org.chromium.Chromium", &hst.BusConfig{ - See: nil, - Talk: []string{"org.freedesktop.Notifications", "org.freedesktop.FileManager1", "org.freedesktop.ScreenSaver", - "org.freedesktop.secrets", "org.kde.kwalletd5", "org.kde.kwalletd6", "org.gnome.SessionManager"}, - Own: []string{"org.chromium.Chromium.*", "org.mpris.MediaPlayer2.org.chromium.Chromium.*", - "org.mpris.MediaPlayer2.chromium.*"}, - Call: map[string]string{"org.freedesktop.portal.*": "*"}, - Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"}, - Log: false, - Filter: true, - }, false, false, - [2]string{sampleHostAddr, sampleBindPath}, - []string{ - sampleHostAddr, - sampleBindPath, - "--filter", - "--talk=org.freedesktop.Notifications", - "--talk=org.freedesktop.FileManager1", - "--talk=org.freedesktop.ScreenSaver", - "--talk=org.freedesktop.secrets", - "--talk=org.kde.kwalletd5", - "--talk=org.kde.kwalletd6", - "--talk=org.gnome.SessionManager", - "--own=org.chromium.Chromium.*", - "--own=org.mpris.MediaPlayer2.org.chromium.Chromium.*", - "--own=org.mpris.MediaPlayer2.chromium.*", - "--call=org.freedesktop.portal.*=*", - "--broadcast=org.freedesktop.portal.*=@/org/freedesktop/portal/*", - }, - }, - { - "org.chromium.Chromium+", &hst.BusConfig{ - See: nil, - Talk: []string{"org.bluez", "org.freedesktop.Avahi", "org.freedesktop.UPower"}, - Own: nil, - Call: nil, - Broadcast: nil, - Log: false, - Filter: true, - }, false, false, - [2]string{sampleHostAddr, sampleBindPath}, - []string{ - sampleHostAddr, - sampleBindPath, - "--filter", - "--talk=org.bluez", - "--talk=org.freedesktop.Avahi", - "--talk=org.freedesktop.UPower", - }, - }, - - { - "dev.vencord.Vesktop", &hst.BusConfig{ - See: nil, - Talk: []string{"org.freedesktop.Notifications", "org.kde.StatusNotifierWatcher"}, - Own: []string{"dev.vencord.Vesktop.*", "org.mpris.MediaPlayer2.dev.vencord.Vesktop.*"}, - Call: map[string]string{"org.freedesktop.portal.*": "*"}, - Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"}, - Log: false, - Filter: true, - }, false, false, - [2]string{sampleHostAddr, sampleBindPath}, - []string{ - sampleHostAddr, - sampleBindPath, - "--filter", - "--talk=org.freedesktop.Notifications", - "--talk=org.kde.StatusNotifierWatcher", - "--own=dev.vencord.Vesktop.*", - "--own=org.mpris.MediaPlayer2.dev.vencord.Vesktop.*", - "--call=org.freedesktop.portal.*=*", - "--broadcast=org.freedesktop.portal.*=@/org/freedesktop/portal/*"}, - }, - - { - "uk.gensokyo.CrashTestDummy", &hst.BusConfig{ - See: []string{"uk.gensokyo.CrashTestDummy1"}, - Talk: []string{"org.freedesktop.Notifications"}, - Own: []string{"uk.gensokyo.CrashTestDummy.*", "org.mpris.MediaPlayer2.uk.gensokyo.CrashTestDummy.*"}, - Call: map[string]string{"org.freedesktop.portal.*": "*"}, - Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"}, - Log: true, - Filter: true, - }, false, false, - [2]string{sampleHostAddr, sampleBindPath}, - []string{ - sampleHostAddr, - sampleBindPath, - "--filter", - "--see=uk.gensokyo.CrashTestDummy1", - "--talk=org.freedesktop.Notifications", - "--own=uk.gensokyo.CrashTestDummy.*", - "--own=org.mpris.MediaPlayer2.uk.gensokyo.CrashTestDummy.*", - "--call=org.freedesktop.portal.*=*", - "--broadcast=org.freedesktop.portal.*=@/org/freedesktop/portal/*", - "--log"}, - }, - { - "uk.gensokyo.CrashTestDummy1", &hst.BusConfig{ - See: []string{"uk.gensokyo.CrashTestDummy"}, - Talk: []string{"org.freedesktop.Notifications"}, - Own: []string{"uk.gensokyo.CrashTestDummy1.*", "org.mpris.MediaPlayer2.uk.gensokyo.CrashTestDummy1.*"}, - Call: map[string]string{"org.freedesktop.portal.*": "*"}, - Broadcast: map[string]string{"org.freedesktop.portal.*": "@/org/freedesktop/portal/*"}, - Log: true, - Filter: true, - }, false, true, - [2]string{sampleHostAddr, sampleBindPath}, - []string{ - sampleHostAddr, - sampleBindPath, - "--filter", - "--see=uk.gensokyo.CrashTestDummy", - "--talk=org.freedesktop.Notifications", - "--own=uk.gensokyo.CrashTestDummy1.*", - "--own=org.mpris.MediaPlayer2.uk.gensokyo.CrashTestDummy1.*", - "--call=org.freedesktop.portal.*=*", - "--broadcast=org.freedesktop.portal.*=@/org/freedesktop/portal/*", - "--log"}, - }, -} - -type dbusTestCase struct { - id string - c *hst.BusConfig - wantErr bool - wantErrF bool - bus [2]string - want []string -} - -var ( - testCasesExt = func() []dbusTestCase { - testCases := make([]dbusTestCase, len(samples)*2) - for i := range samples { - testCases[i] = samples[i] - - fi := &testCases[len(samples)+i] - *fi = samples[i] - - // create null-injected test cases - fi.wantErr = true - injectNulls := func(t *[]string) { - f := make([]string, len(*t)) - for i := range f { - f[i] = "\x00" + (*t)[i] + "\x00" - } - *t = f - } - - fi.c = new(hst.BusConfig) - *fi.c = *samples[i].c - injectNulls(&fi.c.See) - injectNulls(&fi.c.Talk) - injectNulls(&fi.c.Own) - } - return testCases - }() - - testCasePairs = func() map[string][2]dbusTestCase { - // enumerate test case pairs - var pc int - for _, tc := range samples { - if tc.id != "" { - pc++ - } - } - pairs := make(map[string][2]dbusTestCase, pc) - for i, tc := range testCasesExt { - if tc.id == "" { - continue - } - - // skip already enumerated system bus test - if tc.id[len(tc.id)-1] == '+' { - continue - } - - ftp := [2]dbusTestCase{tc} - - // system proxy tests always place directly after its user counterpart with id ending in + - if i+1 < len(testCasesExt) && testCasesExt[i+1].id[len(testCasesExt[i+1].id)-1] == '+' { - // attach system bus config - ftp[1] = testCasesExt[i+1] - - // check for misplaced/mismatching tests - if ftp[0].wantErr != ftp[1].wantErr || ftp[0].id+"+" != ftp[1].id { - panic("mismatching session/system pairing") - } - } - - k := tc.id - if tc.wantErr { - k = "malformed_" + k - } - pairs[k] = ftp - } - return pairs - }() -) diff --git a/system/dbus/testdata/dev.vencord.Vesktop.json b/system/dbus/testdata/dev.vencord.Vesktop.json deleted file mode 100644 index 05259774..00000000 --- a/system/dbus/testdata/dev.vencord.Vesktop.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "talk":[ - "org.freedesktop.Notifications", - "org.kde.StatusNotifierWatcher" - ], - "own":[ - "dev.vencord.Vesktop.*", - "org.mpris.MediaPlayer2.dev.vencord.Vesktop.*" - ], - "call":{ - "org.freedesktop.portal.*":"*" - }, - "broadcast":{ - "org.freedesktop.portal.*":"@/org/freedesktop/portal/*" - }, - - "filter":true -} diff --git a/system/dbus/testdata/org.chromium.Chromium+.json b/system/dbus/testdata/org.chromium.Chromium+.json deleted file mode 100644 index 0a27d719..00000000 --- a/system/dbus/testdata/org.chromium.Chromium+.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "talk":[ - "org.bluez", - "org.freedesktop.Avahi", - "org.freedesktop.UPower" - ], - - "filter":true -}
\ No newline at end of file diff --git a/system/dbus/testdata/org.chromium.Chromium.json b/system/dbus/testdata/org.chromium.Chromium.json deleted file mode 100644 index be027f39..00000000 --- a/system/dbus/testdata/org.chromium.Chromium.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "talk":[ - "org.freedesktop.Notifications", - "org.freedesktop.FileManager1", - "org.freedesktop.ScreenSaver", - "org.freedesktop.secrets", - "org.kde.kwalletd5", - "org.kde.kwalletd6", - "org.gnome.SessionManager" - ], - "own":[ - "org.chromium.Chromium.*", - "org.mpris.MediaPlayer2.org.chromium.Chromium.*", - "org.mpris.MediaPlayer2.chromium.*" - ], - "call":{ - "org.freedesktop.portal.*":"*" - }, - "broadcast":{ - "org.freedesktop.portal.*":"@/org/freedesktop/portal/*" - }, - - "filter":true -} diff --git a/system/dbus/testdata/uk.gensokyo.CrashTestDummy.json b/system/dbus/testdata/uk.gensokyo.CrashTestDummy.json deleted file mode 100644 index 43272f67..00000000 --- a/system/dbus/testdata/uk.gensokyo.CrashTestDummy.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "see": [ - "uk.gensokyo.CrashTestDummy1" - ], - "talk":[ - "org.freedesktop.Notifications" - ], - "own":[ - "uk.gensokyo.CrashTestDummy.*", - "org.mpris.MediaPlayer2.uk.gensokyo.CrashTestDummy.*" - ], - "call":{ - "org.freedesktop.portal.*":"*" - }, - "broadcast":{ - "org.freedesktop.portal.*":"@/org/freedesktop/portal/*" - }, - - "log": true, - "filter":true -} diff --git a/system/dbus_test.go b/system/dbus_test.go deleted file mode 100644 index eedb2335..00000000 --- a/system/dbus_test.go +++ /dev/null @@ -1,674 +0,0 @@ -package system - -import ( - "context" - "reflect" - "slices" - "strconv" - "strings" - "syscall" - "testing" - - "hakurei.app/container/stub" - "hakurei.app/hst" - "hakurei.app/internal/helper" - "hakurei.app/system/dbus" -) - -func TestDBusProxyOp(t *testing.T) { - t.Parallel() - - checkOpBehaviour(t, []opBehaviourTestCase{ - {"dbusProxyStart", 0xdead, 0xff, &dbusProxyOp{ - final: dbusNewFinalSample(4), - out: new(linePrefixWriter), // panics on write - system: true, - }, []stub.Call{ - call("verbosef", stub.ExpectArgs{"session bus proxy on %q for upstream %q", []any{"/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus", "unix:path=/run/user/1000/bus"}}, nil, nil), - call("verbosef", stub.ExpectArgs{"system bus proxy on %q for upstream %q", []any{"/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket", "unix:path=/run/dbus/system_bus_socket"}}, nil, nil), - call("dbusProxyStart", stub.ExpectArgs{dbusNewFinalSample(4)}, nil, stub.UniqueError(2)), - }, &OpError{ - Op: "dbus", Err: stub.UniqueError(2), - Msg: "cannot start message bus proxy: unique error 2 injected by the test suite", - }, nil, nil}, - - {"dbusProxyWait", 0xdead, 0xff, &dbusProxyOp{ - final: dbusNewFinalSample(3), - }, []stub.Call{ - call("verbosef", stub.ExpectArgs{"session bus proxy on %q for upstream %q", []any{"/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus", "unix:path=/run/user/1000/bus"}}, nil, nil), - call("dbusProxyStart", stub.ExpectArgs{dbusNewFinalSample(3)}, nil, nil), - call("verbose", stub.ExpectArgs{[]any{"starting message bus proxy", ignoreValue{}}}, nil, nil), - }, nil, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"terminating message bus proxy"}}, nil, nil), - call("dbusProxyClose", stub.ExpectArgs{dbusNewFinalSample(3)}, nil, nil), - call("dbusProxyWait", stub.ExpectArgs{dbusNewFinalSample(3)}, nil, stub.UniqueError(1)), - call("verbose", stub.ExpectArgs{[]any{"message bus proxy exit"}}, nil, nil), - }, &OpError{ - Op: "dbus", Err: stub.UniqueError(1), Revert: true, - Msg: "message bus proxy error: unique error 1 injected by the test suite", - }}, - - {"success dbusProxyWait cancel", 0xdead, 0xff, &dbusProxyOp{ - final: dbusNewFinalSample(2), - system: true, - }, []stub.Call{ - call("verbosef", stub.ExpectArgs{"session bus proxy on %q for upstream %q", []any{"/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus", "unix:path=/run/user/1000/bus"}}, nil, nil), - call("verbosef", stub.ExpectArgs{"system bus proxy on %q for upstream %q", []any{"/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket", "unix:path=/run/dbus/system_bus_socket"}}, nil, nil), - call("dbusProxyStart", stub.ExpectArgs{dbusNewFinalSample(2)}, nil, nil), - call("verbose", stub.ExpectArgs{[]any{"starting message bus proxy", ignoreValue{}}}, nil, nil), - }, nil, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"terminating message bus proxy"}}, nil, nil), - call("dbusProxyClose", stub.ExpectArgs{dbusNewFinalSample(2)}, nil, nil), - call("dbusProxyWait", stub.ExpectArgs{dbusNewFinalSample(2)}, nil, context.Canceled), - call("verbose", stub.ExpectArgs{[]any{"message bus proxy canceled upstream"}}, nil, nil), - }, nil}, - - {"success", 0xdead, 0xff, &dbusProxyOp{ - final: dbusNewFinalSample(1), - system: true, - }, []stub.Call{ - call("verbosef", stub.ExpectArgs{"session bus proxy on %q for upstream %q", []any{"/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus", "unix:path=/run/user/1000/bus"}}, nil, nil), - call("verbosef", stub.ExpectArgs{"system bus proxy on %q for upstream %q", []any{"/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket", "unix:path=/run/dbus/system_bus_socket"}}, nil, nil), - call("dbusProxyStart", stub.ExpectArgs{dbusNewFinalSample(1)}, nil, nil), - call("verbose", stub.ExpectArgs{[]any{"starting message bus proxy", ignoreValue{}}}, nil, nil), - }, nil, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"terminating message bus proxy"}}, nil, nil), - call("dbusProxyClose", stub.ExpectArgs{dbusNewFinalSample(1)}, nil, nil), - call("dbusProxyWait", stub.ExpectArgs{dbusNewFinalSample(1)}, nil, nil), - call("verbose", stub.ExpectArgs{[]any{"message bus proxy exit"}}, nil, nil), - }, nil}, - }) - - checkOpsBuilder(t, "ProxyDBus", []opsBuilderTestCase{ - {"nil session", 0xcafe, func(t *testing.T, sys *I) { - wantErr := &OpError{ - Op: "dbus", Err: ErrDBusConfig, - Msg: "attempted to create message bus proxy args without session bus config", - } - if err := sys.ProxyDBus(nil, new(hst.BusConfig), dbus.ProxyPair{}, dbus.ProxyPair{}); !reflect.DeepEqual(err, wantErr) { - t.Errorf("ProxyDBus: error = %v, want %v", err, wantErr) - } - }, nil, stub.Expect{}}, - - {"dbusFinalise NUL", 0xcafe, func(_ *testing.T, sys *I) { - defer func() { - want := "message bus proxy configuration contains NUL byte" - if r := recover(); r != want { - t.Errorf("MustProxyDBus: panic = %v, want %v", r, want) - } - }() - - sys.MustProxyDBus( - &hst.BusConfig{ - // use impossible value here as an implicit assert that it goes through the stub - Talk: []string{"session\x00"}, Filter: true, - }, &hst.BusConfig{ - // use impossible value here as an implicit assert that it goes through the stub - Talk: []string{"system\x00"}, Filter: true, - }, dbus.ProxyPair{ - "unix:path=/run/user/1000/bus", - "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus", - }, dbus.ProxyPair{ - "unix:path=/run/dbus/system_bus_socket", - "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket", - }) - }, nil, stub.Expect{Calls: []stub.Call{ - call("dbusFinalise", stub.ExpectArgs{ - dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus"}, - dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket"}, - &hst.BusConfig{Talk: []string{"session\x00"}, Filter: true}, - &hst.BusConfig{Talk: []string{"system\x00"}, Filter: true}, - }, (*dbus.Final)(nil), syscall.EINVAL), - }}}, - - {"dbusFinalise", 0xcafe, func(_ *testing.T, sys *I) { - wantErr := &OpError{ - Op: "dbus", Err: stub.UniqueError(0), - Msg: "cannot finalise message bus proxy: unique error 0 injected by the test suite", - } - if err := sys.ProxyDBus( - &hst.BusConfig{ - // use impossible value here as an implicit assert that it goes through the stub - Talk: []string{"session\x00"}, Filter: true, - }, &hst.BusConfig{ - // use impossible value here as an implicit assert that it goes through the stub - Talk: []string{"system\x00"}, Filter: true, - }, dbus.ProxyPair{ - "unix:path=/run/user/1000/bus", - "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus", - }, dbus.ProxyPair{ - "unix:path=/run/dbus/system_bus_socket", - "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket", - }); !reflect.DeepEqual(err, wantErr) { - t.Errorf("ProxyDBus: error = %v", err) - } - }, nil, stub.Expect{Calls: []stub.Call{ - call("dbusFinalise", stub.ExpectArgs{ - dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus"}, - dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket"}, - &hst.BusConfig{Talk: []string{"session\x00"}, Filter: true}, - &hst.BusConfig{Talk: []string{"system\x00"}, Filter: true}, - }, (*dbus.Final)(nil), stub.UniqueError(0)), - }}}, - - {"full", 0xcafe, func(_ *testing.T, sys *I) { - sys.MustProxyDBus( - &hst.BusConfig{ - // use impossible value here as an implicit assert that it goes through the stub - Talk: []string{"session\x00"}, Filter: true, - }, &hst.BusConfig{ - // use impossible value here as an implicit assert that it goes through the stub - Talk: []string{"system\x00"}, Filter: true, - }, dbus.ProxyPair{ - "unix:path=/run/user/1000/bus", - "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus", - }, dbus.ProxyPair{ - "unix:path=/run/dbus/system_bus_socket", - "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket", - }) - }, []Op{ - &dbusProxyOp{ - final: dbusNewFinalSample(0), - system: true, - }, - }, stub.Expect{Calls: []stub.Call{ - call("dbusFinalise", stub.ExpectArgs{ - dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus"}, - dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket"}, - &hst.BusConfig{Talk: []string{"session\x00"}, Filter: true}, - &hst.BusConfig{Talk: []string{"system\x00"}, Filter: true}, - }, dbusNewFinalSample(0), nil), - call("isVerbose", stub.ExpectArgs{}, true, nil), - call("verbose", stub.ExpectArgs{[]any{"session bus proxy:", []string{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus", "--filter", "--talk=session\x00"}}}, nil, nil), - call("verbose", stub.ExpectArgs{[]any{"system bus proxy:", []string{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket", "--filter", "--talk=system\x00"}}}, nil, nil), - call("verbose", stub.ExpectArgs{[]any{"message bus proxy final args:", helper.MustNewCheckedArgs("unique", "value", "0", "injected", "by", "the", "test", "suite")}}, nil, nil), - }}}, - }) - - checkOpIs(t, []opIsTestCase{ - {"nil", (*dbusProxyOp)(nil), (*dbusProxyOp)(nil), false}, - {"zero", new(dbusProxyOp), new(dbusProxyOp), false}, - - {"system differs", &dbusProxyOp{final: &dbus.Final{ - Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"}, - System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"}, - - SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}}, - SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}}, - - WriterTo: helper.MustNewCheckedArgs( - "--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus", - "--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket", - ), - }, system: false, - }, &dbusProxyOp{final: &dbus.Final{ - Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"}, - System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"}, - - SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}}, - SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}}, - - WriterTo: helper.MustNewCheckedArgs( - "--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus", - "--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket", - ), - }, system: true, - }, false}, - - {"wt differs", &dbusProxyOp{final: &dbus.Final{ - Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"}, - System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"}, - - SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}}, - SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}}, - - WriterTo: helper.MustNewCheckedArgs( - "--filter", "unix:path=/run/user/1001/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus", - "--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket", - ), - }, system: true, - }, &dbusProxyOp{final: &dbus.Final{ - Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"}, - System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"}, - - SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}}, - SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}}, - - WriterTo: helper.MustNewCheckedArgs( - "--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus", - "--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket", - ), - }, system: true, - }, false}, - - {"final system upstream differs", &dbusProxyOp{final: &dbus.Final{ - Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"}, - System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"}, - - SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}}, - SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket\x00"}}}}, - - WriterTo: helper.MustNewCheckedArgs( - "--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus", - "--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket", - ), - }, system: true, - }, &dbusProxyOp{final: &dbus.Final{ - Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"}, - System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"}, - - SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}}, - SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}}, - - WriterTo: helper.MustNewCheckedArgs( - "--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus", - "--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket", - ), - }, system: true, - }, false}, - - {"final session upstream differs", &dbusProxyOp{final: &dbus.Final{ - Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"}, - System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"}, - - SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1001/bus"}}}}, - SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}}, - - WriterTo: helper.MustNewCheckedArgs( - "--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus", - "--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket", - ), - }, system: true, - }, &dbusProxyOp{final: &dbus.Final{ - Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"}, - System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"}, - - SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}}, - SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}}, - - WriterTo: helper.MustNewCheckedArgs( - "--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus", - "--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket", - ), - }, system: true, - }, false}, - - {"final system differs", &dbusProxyOp{final: &dbus.Final{ - Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"}, - System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.1/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"}, - - SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}}, - SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}}, - - WriterTo: helper.MustNewCheckedArgs( - "--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus", - "--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket", - ), - }, system: true, - }, &dbusProxyOp{final: &dbus.Final{ - Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"}, - System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"}, - - SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}}, - SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}}, - - WriterTo: helper.MustNewCheckedArgs( - "--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus", - "--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket", - ), - }, system: true, - }, false}, - - {"final session differs", &dbusProxyOp{final: &dbus.Final{ - Session: dbus.ProxyPair{"unix:path=/run/user/1001/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"}, - System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"}, - - SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}}, - SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}}, - - WriterTo: helper.MustNewCheckedArgs( - "--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus", - "--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket", - ), - }, system: true, - }, &dbusProxyOp{final: &dbus.Final{ - Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"}, - System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"}, - - SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}}, - SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}}, - - WriterTo: helper.MustNewCheckedArgs( - "--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus", - "--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket", - ), - }, system: true, - }, false}, - - {"equals", &dbusProxyOp{final: &dbus.Final{ - Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"}, - System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"}, - - SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}}, - SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}}, - - WriterTo: helper.MustNewCheckedArgs( - "--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus", - "--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket", - ), - }, system: true, - }, &dbusProxyOp{final: &dbus.Final{ - Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus"}, - System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket"}, - - SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}}, - SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"unix", "/run/dbus/system_bus_socket"}}}}, - - WriterTo: helper.MustNewCheckedArgs( - "--filter", "unix:path=/run/user/1000/bus", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/bus", - "--filter", "unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/b186c281d9e83a39afdc66d964ef99c6/system_bus_socket", - ), - }, system: true, - }, true}, - }) - - checkOpMeta(t, []opMetaTestCase{ - {"dbus", new(dbusProxyOp), - Process, "/proc/nonexistent", - "(invalid dbus proxy)"}, - }) -} - -func dbusNewFinalSample(v int) *dbus.Final { - return &dbus.Final{ - Session: dbus.ProxyPair{"unix:path=/run/user/1000/bus", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/bus"}, - System: dbus.ProxyPair{"unix:path=/run/dbus/system_bus_socket", "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f/system_bus_socket"}, - - SessionUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/user/1000/bus"}}}}, - SystemUpstream: []dbus.AddrEntry{{Method: "unix", Values: [][2]string{{"path", "/run/dbus/system_bus_socket"}}}}, - - WriterTo: helper.MustNewCheckedArgs("unique", "value", strconv.Itoa(v), "injected", "by", "the", "test", "suite"), - } -} - -func TestLinePrefixWriter(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - prefix string - f func(w func(s string)) - wantErr []error - wantPt []string - want []string - wantExt []string - wantBuf string - }{ - {"nop", "(nop) ", func(func(string)) {}, nil, nil, nil, nil, ""}, - - {"partial", "(partial) ", func(w func(string)) { - w("C-65533: -> ") - }, nil, nil, nil, []string{ - "*(partial) C-65533: -> ", - }, "C-65533: -> "}, - - {"break", "(break) ", func(w func(string)) { - w("C-65533: -> ") - w("org.freedesktop.DBus fake ListNames\n") - }, nil, nil, []string{ - "C-65533: -> org.freedesktop.DBus fake ListNames", - }, nil, ""}, - - {"break pt", "(break pt) ", func(w func(string)) { - w("init: ") - w("received setup parameters\n") - }, nil, []string{ - "init: received setup parameters", - }, nil, nil, ""}, - - {"threshold", "(threshold) ", func(w func(s string)) { - w(string(make([]byte, lpwSizeThreshold))) - w("\n") - }, []error{nil, syscall.ENOMEM}, nil, nil, []string{ - "*(threshold) " + string(make([]byte, lpwSizeThreshold)), - "+(threshold) write threshold reached, output may be incomplete", - }, string(make([]byte, lpwSizeThreshold))}, - - {"threshold multi", "(threshold multi) ", func(w func(s string)) { - w(":3\n") - w(string(make([]byte, lpwSizeThreshold-3))) - w("\n") - }, []error{nil, nil, syscall.ENOMEM}, nil, []string{ - ":3", - }, []string{ - "*(threshold multi) " + string(make([]byte, lpwSizeThreshold-3)), - "+(threshold multi) write threshold reached, output may be incomplete", - }, string(make([]byte, lpwSizeThreshold-3))}, - - {"threshold multi partial", "(threshold multi partial) ", func(w func(s string)) { - w(":3\n") - w(string(make([]byte, lpwSizeThreshold-2))) - w("dropped\n") - }, []error{nil, nil, syscall.ENOMEM}, nil, []string{ - ":3", - }, []string{ - "*(threshold multi partial) " + string(make([]byte, lpwSizeThreshold-2)), - "+(threshold multi partial) write threshold reached, output may be incomplete", - }, string(make([]byte, lpwSizeThreshold-2))}, - - {"threshold exact", "(threshold exact) ", func(w func(s string)) { - w(string(make([]byte, lpwSizeThreshold-1))) - w("\n") - }, nil, nil, []string{ - string(make([]byte, lpwSizeThreshold-1)), - }, []string{ - "+(threshold exact) write threshold reached, output may be incomplete", - }, ""}, - - {"sample", "(dbus) ", func(w func(s string)) { - w("init: received setup parameters\n") - w(`init: mounting "/nix/store/5gml2l2cj28yvyfyzblzjy1laqpxmyzd-libselinux-3.8.1/lib" flags 0x0` + "\n") - w(`init: resolved "/host/nix/store/5gml2l2cj28yvyfyzblzjy1laqpxmyzd-libselinux-3.8.1/lib" on "/sysroot/nix/store/5gml2l2cj28yvyfyzblzjy1laqpxmyzd-libselinux-3.8.1/lib" flags 0x4005` + "\n") - w(`init: mounting "/nix/store/bcs094l67dlbqf7idxxbljp293zms9mh-util-linux-minimal-2.41-lib/lib" flags 0x0` + "\n") - w(`init: resolved "/host/nix/store/bcs094l67dlbqf7idxxbljp293zms9mh-util-linux-minimal-2.41-lib/lib" on "/sysroot/nix/store/bcs094l67dlbqf7idxxbljp293zms9mh-util-linux-minimal-2.41-lib/lib" flags 0x4005` + "\n") - w(`init: mounting "/nix/store/jl19fdc7gdxqz9a1s368r9d15vpirnqy-zlib-1.3.1/lib" flags 0x0` + "\n") - w(`init: resolved "/host/nix/store/jl19fdc7gdxqz9a1s368r9d15vpirnqy-zlib-1.3.1/lib" on "/sysroot/nix/store/jl19fdc7gdxqz9a1s368r9d15vpirnqy-zlib-1.3.1/lib" flags 0x4005` + "\n") - w(`init: mounting "/nix/store/rnn29mhynsa4ncmk0fkcrdr29n0j20l4-libffi-3.4.8/lib" flags 0x0` + "\n") - w(`init: resolved "/host/nix/store/rnn29mhynsa4ncmk0fkcrdr29n0j20l4-libffi-3.4.8/lib" on "/sysroot/nix/store/rnn29mhynsa4ncmk0fkcrdr29n0j20l4-libffi-3.4.8/lib" flags 0x4005` + "\n") - w(`init: mounting "/nix/store/vvp8hlss3d5q6hn0cifq04jrpnp6bini-pcre2-10.44/lib" flags 0x0` + "\n") - w(`init: resolved "/host/nix/store/vvp8hlss3d5q6hn0cifq04jrpnp6bini-pcre2-10.44/lib" on "/sysroot/nix/store/vvp8hlss3d5q6hn0cifq04jrpnp6bini-pcre2-10.44/lib" flags 0x4005` + "\n") - w(`init: mounting "/nix/store/y3nxdc2x8hwivppzgx5hkrhacsh87l21-glib-2.84.3/lib" flags 0x0` + "\n") - w(`init: resolved "/host/nix/store/y3nxdc2x8hwivppzgx5hkrhacsh87l21-glib-2.84.3/lib" on "/sysroot/nix/store/y3nxdc2x8hwivppzgx5hkrhacsh87l21-glib-2.84.3/lib" flags 0x4005` + "\n") - w(`init: mounting "/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib" flags 0x0` + "\n") - w(`init: resolved "/host/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib" on "/sysroot/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib" flags 0x4005` + "\n") - w(`init: mounting "/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib64" flags 0x0` + "\n") - w(`init: resolved "/host/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib" on "/sysroot/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib64" flags 0x4005` + "\n") - w(`init: mounting "/run/user/1000" flags 0x0` + "\n") - w(`init: resolved "/host/run/user/1000" on "/sysroot/run/user/1000" flags 0x4005` + "\n") - w(`init: mounting "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f" flags 0x2` + "\n") - w(`init: resolved "/host/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f" on "/sysroot/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f" flags 0x4004` + "\n") - w(`init: mounting "/nix/store/d2divmq2d897amikcwpdx7zrbpddxxcl-xdg-dbus-proxy-0.1.6/bin" flags 0x0` + "\n") - w(`init: resolved "/host/nix/store/d2divmq2d897amikcwpdx7zrbpddxxcl-xdg-dbus-proxy-0.1.6/bin" on "/sysroot/nix/store/d2divmq2d897amikcwpdx7zrbpddxxcl-xdg-dbus-proxy-0.1.6/bin" flags 0x4005` + "\n") - w("init: resolving presets 0xf\n") - w("init: 68 filter rules loaded\n") - w("init: starting initial program /nix/store/d2divmq2d897amikcwpdx7zrbpddxxcl-xdg-dbus-proxy-0.1.6/bin/xdg-dbus-proxy\n") - w("C1: -> org.freedesktop.DBus call org.freedesktop.DBus.Hello at /org/freedesktop/DBus\n") - w("C-65536: -> org.freedesktop.DBus fake wildcarded AddMatch for org.freedesktop.portal\n") - w("C-65535: -> org.freedesktop.DBus fake AddMatch for org.freedesktop.Notifications\n") - w("C-65534: -> org.freedesktop.DBus fake GetNameOwner for org.freedesktop.Notifications\n") - w("C-65533: -> org.freedesktop.DBus fake ListNames\n") - w("B1: <- org.freedesktop.DBus return from C1\n") - w("B2: <- org.freedesktop.DBus signal org.freedesktop.DBus.NameAcquired at /org/freedesktop/DBus\n") - w("B3: <- org.freedesktop.DBus return from C-65536\n") - w("*SKIPPED*\n") - w("B4: <- org.freedesktop.DBus return from C-65535\n") - w("*SKIPPED*\n") - w("B5: <- org.freedesktop.DBus return error org.freedesktop.DBus.Error.NameHasNoOwner from C-65534\n") - w("*SKIPPED*\n") - w("B6: <- org.freedesktop.DBus return from C-65533\n") - w("C-65532: -> org.freedesktop.DBus fake GetNameOwner for org.freedesktop.DBus\n") - w("*SKIPPED*\n") - w("B7: <- org.freedesktop.DBus return from C-65532\n") - w("*SKIPPED*\n") - w("C2: -> org.freedesktop.DBus call org.freedesktop.DBus.AddMatch at /org/freedesktop/DBus\n") - w("C3: -> org.freedesktop.DBus call org.freedesktop.DBus.GetNameOwner at /org/freedesktop/DBus\n") - w("C4: -> org.freedesktop.DBus call org.freedesktop.DBus.AddMatch at /org/freedesktop/DBus\n") - w("C5: -> org.freedesktop.DBus call org.freedesktop.DBus.StartServiceByName at /org/freedesktop/DBus\n") - w("B8: <- org.freedesktop.DBus return from C2\n") - w("B9: <- org.freedesktop.DBus return error org.freedesktop.DBus.Error.NameHasNoOwner from C3\n") - w("B10: <- org.freedesktop.DBus return from C4\n") - w("B12: <- org.freedesktop.DBus signal org.freedesktop.DBus.NameOwnerChanged at /org/freedesktop/DBus\n") - w("B11: <- org.freedesktop.DBus return from C5\n") - w("C6: -> org.freedesktop.DBus call org.freedesktop.DBus.GetNameOwner at /org/freedesktop/DBus\n") - w("B13: <- org.freedesktop.DBus return from C6\n") - w("C7: -> :1.4 call org.freedesktop.Notifications.GetServerInformation at /org/freedesktop/Notifications\n") - w("B4: <- :1.4 return from C7\n") - w("C8: -> :1.4 call org.freedesktop.Notifications.GetServerInformation at /org/freedesktop/Notifications\n") - w("B5: <- :1.4 return from C8\n") - w("C9: -> :1.4 call org.freedesktop.Notifications.Notify at /org/freedesktop/Notifications\n") - w("B6: <- :1.4 return from C9\n") - w("C10: -> org.freedesktop.DBus call org.freedesktop.DBus.RemoveMatch at /org/freedesktop/DBus\n") - w("C11: -> org.freedesktop.DBus call org.freedesktop.DBus.RemoveMatch at /org/freedesktop/DBus\n") - w("B14: <- org.freedesktop.DBus return from C10\n") - w("B15: <- org.freedesktop.DBus return from C11\n") - w("init: initial process exited with code 0\n") - }, nil, []string{ - "init: received setup parameters", - `init: mounting "/nix/store/5gml2l2cj28yvyfyzblzjy1laqpxmyzd-libselinux-3.8.1/lib" flags 0x0`, - `init: resolved "/host/nix/store/5gml2l2cj28yvyfyzblzjy1laqpxmyzd-libselinux-3.8.1/lib" on "/sysroot/nix/store/5gml2l2cj28yvyfyzblzjy1laqpxmyzd-libselinux-3.8.1/lib" flags 0x4005`, - `init: mounting "/nix/store/bcs094l67dlbqf7idxxbljp293zms9mh-util-linux-minimal-2.41-lib/lib" flags 0x0`, - `init: resolved "/host/nix/store/bcs094l67dlbqf7idxxbljp293zms9mh-util-linux-minimal-2.41-lib/lib" on "/sysroot/nix/store/bcs094l67dlbqf7idxxbljp293zms9mh-util-linux-minimal-2.41-lib/lib" flags 0x4005`, - `init: mounting "/nix/store/jl19fdc7gdxqz9a1s368r9d15vpirnqy-zlib-1.3.1/lib" flags 0x0`, - `init: resolved "/host/nix/store/jl19fdc7gdxqz9a1s368r9d15vpirnqy-zlib-1.3.1/lib" on "/sysroot/nix/store/jl19fdc7gdxqz9a1s368r9d15vpirnqy-zlib-1.3.1/lib" flags 0x4005`, - `init: mounting "/nix/store/rnn29mhynsa4ncmk0fkcrdr29n0j20l4-libffi-3.4.8/lib" flags 0x0`, - `init: resolved "/host/nix/store/rnn29mhynsa4ncmk0fkcrdr29n0j20l4-libffi-3.4.8/lib" on "/sysroot/nix/store/rnn29mhynsa4ncmk0fkcrdr29n0j20l4-libffi-3.4.8/lib" flags 0x4005`, - `init: mounting "/nix/store/vvp8hlss3d5q6hn0cifq04jrpnp6bini-pcre2-10.44/lib" flags 0x0`, - `init: resolved "/host/nix/store/vvp8hlss3d5q6hn0cifq04jrpnp6bini-pcre2-10.44/lib" on "/sysroot/nix/store/vvp8hlss3d5q6hn0cifq04jrpnp6bini-pcre2-10.44/lib" flags 0x4005`, - `init: mounting "/nix/store/y3nxdc2x8hwivppzgx5hkrhacsh87l21-glib-2.84.3/lib" flags 0x0`, - `init: resolved "/host/nix/store/y3nxdc2x8hwivppzgx5hkrhacsh87l21-glib-2.84.3/lib" on "/sysroot/nix/store/y3nxdc2x8hwivppzgx5hkrhacsh87l21-glib-2.84.3/lib" flags 0x4005`, - `init: mounting "/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib" flags 0x0`, - `init: resolved "/host/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib" on "/sysroot/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib" flags 0x4005`, - `init: mounting "/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib64" flags 0x0`, - `init: resolved "/host/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib" on "/sysroot/nix/store/zdpby3l6azi78sl83cpad2qjpfj25aqx-glibc-2.40-66/lib64" flags 0x4005`, - `init: mounting "/run/user/1000" flags 0x0`, - `init: resolved "/host/run/user/1000" on "/sysroot/run/user/1000" flags 0x4005`, - `init: mounting "/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f" flags 0x2`, - `init: resolved "/host/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f" on "/sysroot/tmp/hakurei.0/99dd71ee2146369514e0d10783368f8f" flags 0x4004`, - `init: mounting "/nix/store/d2divmq2d897amikcwpdx7zrbpddxxcl-xdg-dbus-proxy-0.1.6/bin" flags 0x0`, - `init: resolved "/host/nix/store/d2divmq2d897amikcwpdx7zrbpddxxcl-xdg-dbus-proxy-0.1.6/bin" on "/sysroot/nix/store/d2divmq2d897amikcwpdx7zrbpddxxcl-xdg-dbus-proxy-0.1.6/bin" flags 0x4005`, - "init: resolving presets 0xf", - "init: 68 filter rules loaded", - "init: starting initial program /nix/store/d2divmq2d897amikcwpdx7zrbpddxxcl-xdg-dbus-proxy-0.1.6/bin/xdg-dbus-proxy", - - "init: initial process exited with code 0", - }, []string{ - "C1: -> org.freedesktop.DBus call org.freedesktop.DBus.Hello at /org/freedesktop/DBus", - "C-65536: -> org.freedesktop.DBus fake wildcarded AddMatch for org.freedesktop.portal", - "C-65535: -> org.freedesktop.DBus fake AddMatch for org.freedesktop.Notifications", - "C-65534: -> org.freedesktop.DBus fake GetNameOwner for org.freedesktop.Notifications", - "C-65533: -> org.freedesktop.DBus fake ListNames", - "B1: <- org.freedesktop.DBus return from C1", - "B2: <- org.freedesktop.DBus signal org.freedesktop.DBus.NameAcquired at /org/freedesktop/DBus", - "B3: <- org.freedesktop.DBus return from C-65536", - "*SKIPPED*", - "B4: <- org.freedesktop.DBus return from C-65535", - "*SKIPPED*", - "B5: <- org.freedesktop.DBus return error org.freedesktop.DBus.Error.NameHasNoOwner from C-65534", - "*SKIPPED*", - "B6: <- org.freedesktop.DBus return from C-65533", - "C-65532: -> org.freedesktop.DBus fake GetNameOwner for org.freedesktop.DBus", - "*SKIPPED*", - "B7: <- org.freedesktop.DBus return from C-65532", - "*SKIPPED*", - "C2: -> org.freedesktop.DBus call org.freedesktop.DBus.AddMatch at /org/freedesktop/DBus", - "C3: -> org.freedesktop.DBus call org.freedesktop.DBus.GetNameOwner at /org/freedesktop/DBus", - "C4: -> org.freedesktop.DBus call org.freedesktop.DBus.AddMatch at /org/freedesktop/DBus", - "C5: -> org.freedesktop.DBus call org.freedesktop.DBus.StartServiceByName at /org/freedesktop/DBus", - "B8: <- org.freedesktop.DBus return from C2", - "B9: <- org.freedesktop.DBus return error org.freedesktop.DBus.Error.NameHasNoOwner from C3", - "B10: <- org.freedesktop.DBus return from C4", - "B12: <- org.freedesktop.DBus signal org.freedesktop.DBus.NameOwnerChanged at /org/freedesktop/DBus", - "B11: <- org.freedesktop.DBus return from C5", - "C6: -> org.freedesktop.DBus call org.freedesktop.DBus.GetNameOwner at /org/freedesktop/DBus", - "B13: <- org.freedesktop.DBus return from C6", - "C7: -> :1.4 call org.freedesktop.Notifications.GetServerInformation at /org/freedesktop/Notifications", - "B4: <- :1.4 return from C7", - "C8: -> :1.4 call org.freedesktop.Notifications.GetServerInformation at /org/freedesktop/Notifications", - "B5: <- :1.4 return from C8", - "C9: -> :1.4 call org.freedesktop.Notifications.Notify at /org/freedesktop/Notifications", - "B6: <- :1.4 return from C9", - "C10: -> org.freedesktop.DBus call org.freedesktop.DBus.RemoveMatch at /org/freedesktop/DBus", - "C11: -> org.freedesktop.DBus call org.freedesktop.DBus.RemoveMatch at /org/freedesktop/DBus", - "B14: <- org.freedesktop.DBus return from C10", - "B15: <- org.freedesktop.DBus return from C11", - }, nil, ""}, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - gotPt := make([]string, 0, len(tc.wantPt)) - out := &linePrefixWriter{ - prefix: tc.prefix, - println: func(v ...any) { - if len(v) != 1 { - t.Fatalf("invalid call to println: %#v", v) - } - gotPt = append(gotPt, v[0].(string)) - }, - buf: new(strings.Builder), - } - - var pos int - tc.f(func(s string) { - _, err := out.Write([]byte(s)) - if tc.wantErr != nil { - if !reflect.DeepEqual(err, tc.wantErr[pos]) { - t.Fatalf("Write: error = %v, want %v", err, tc.wantErr[pos]) - } - } else if err != nil { - t.Fatalf("Write: unexpected error: %v", err) - return - } - pos++ - }) - - if !slices.Equal(out.msg, tc.want) { - t.Errorf("msg: %#v, want %#v", out.msg, tc.want) - } - - if out.buf.String() != tc.wantBuf { - t.Errorf("buf: %q, want %q", out.buf, tc.wantBuf) - } - - wantPt := make([]string, len(tc.wantPt)) - for i, m := range tc.wantPt { - wantPt[i] = tc.prefix + m - } - if !slices.Equal(gotPt, wantPt) { - t.Errorf("passthrough: %#v, want %#v", gotPt, wantPt) - } - - wantDump := make([]string, len(tc.want)+len(tc.wantExt)) - for i, want := range tc.want { - wantDump[i] = tc.prefix + want - } - for i, want := range tc.wantExt { - wantDump[len(tc.want)+i] = want - } - t.Run("dump", func(t *testing.T) { - got := make([]string, 0, len(wantDump)) - out.println = func(v ...any) { - if len(v) != 1 { - t.Fatalf("Dump: invalid call to println: %#v", v) - } - got = append(got, v[0].(string)) - } - out.Dump() - - if !slices.Equal(got, wantDump) { - t.Errorf("Dump: %#v, want %#v", got, wantDump) - } - }) - }) - } -} diff --git a/system/deprecated.go b/system/deprecated.go new file mode 100644 index 00000000..3f0a423d --- /dev/null +++ b/system/deprecated.go @@ -0,0 +1,50 @@ +// Package system exposes the internal/system package. +// +// Deprecated: This package will be removed in 0.4. +package system + +import ( + "context" + _ "unsafe" // for go:linkname + + "hakurei.app/hst" + "hakurei.app/internal/system" + "hakurei.app/message" +) + +// ErrDBusConfig is returned when a required hst.BusConfig argument is nil. +// +//go:linkname ErrDBusConfig hakurei.app/internal/system.ErrDBusConfig +var ErrDBusConfig error + +// OpError is returned by [I.Commit] and [I.Revert]. +type OpError = system.OpError + +const ( + // User type is reverted at final instance exit. + User = system.User + // Process type is unconditionally reverted on exit. + Process = system.Process + + CM = system.CM +) + +// Criteria specifies types of Op to revert. +type Criteria = system.Criteria + +// Op is a reversible system operation. +type Op = system.Op + +// TypeString extends [Enablement.String] to support [User] and [Process]. +// +//go:linkname TypeString hakurei.app/internal/system.TypeString +func TypeString(e hst.Enablement) string + +// New returns the address of a new [I] targeting uid. +// +//go:linkname New hakurei.app/internal/system.New +func New(ctx context.Context, msg message.Msg, uid int) (sys *I) + +// An I provides deferred operating system interaction. [I] must not be copied. +// Methods of [I] must not be used concurrently. +type I = system.I diff --git a/system/dispatcher.go b/system/dispatcher.go deleted file mode 100644 index cdc95072..00000000 --- a/system/dispatcher.go +++ /dev/null @@ -1,89 +0,0 @@ -package system - -import ( - "io" - "io/fs" - "log" - "os" - - "hakurei.app/hst" - "hakurei.app/system/acl" - "hakurei.app/system/dbus" - "hakurei.app/system/internal/xcb" -) - -type osFile interface { - Name() string - io.Writer - fs.File -} - -// syscallDispatcher provides methods that make state-dependent system calls as part of their behaviour. -// syscallDispatcher is embedded in [I], so all methods must be unexported. -type syscallDispatcher interface { - // new starts a goroutine with a new instance of syscallDispatcher. - // A syscallDispatcher must never be used in any goroutine other than the one owning it, - // just synchronising access is not enough, as this is for test instrumentation. - new(f func(k syscallDispatcher)) - - // stat provides os.Stat. - stat(name string) (os.FileInfo, error) - // open provides [os.Open]. - open(name string) (osFile, error) - // mkdir provides os.Mkdir. - mkdir(name string, perm os.FileMode) error - // chmod provides os.Chmod. - chmod(name string, mode os.FileMode) error - // link provides os.Link. - link(oldname, newname string) error - // remove provides os.Remove. - remove(name string) error - - // println provides [log.Println]. - println(v ...any) - - // aclUpdate provides [acl.Update]. - aclUpdate(name string, uid int, perms ...acl.Perm) error - - // xcbChangeHosts provides [xcb.ChangeHosts]. - xcbChangeHosts(mode xcb.HostMode, family xcb.Family, address string) error - - // dbusFinalise provides [dbus.Finalise]. - dbusFinalise(sessionBus, systemBus dbus.ProxyPair, session, system *hst.BusConfig) (final *dbus.Final, err error) - // dbusProxyStart provides the Start method of [dbus.Proxy]. - dbusProxyStart(proxy *dbus.Proxy) error - // dbusProxyClose provides the Close method of [dbus.Proxy]. - dbusProxyClose(proxy *dbus.Proxy) - // dbusProxyWait provides the Wait method of [dbus.Proxy]. - dbusProxyWait(proxy *dbus.Proxy) error -} - -// direct implements syscallDispatcher on the current kernel. -type direct struct{} - -func (k direct) new(f func(k syscallDispatcher)) { go f(k) } - -func (k direct) stat(name string) (os.FileInfo, error) { return os.Stat(name) } -func (k direct) open(name string) (osFile, error) { return os.Open(name) } -func (k direct) mkdir(name string, perm os.FileMode) error { return os.Mkdir(name, perm) } -func (k direct) chmod(name string, mode os.FileMode) error { return os.Chmod(name, mode) } -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) println(v ...any) { log.Println(v...) } - -func (k direct) aclUpdate(name string, uid int, perms ...acl.Perm) error { - return acl.Update(name, uid, perms...) -} - -func (k direct) xcbChangeHosts(mode xcb.HostMode, family xcb.Family, address string) error { - return xcb.ChangeHosts(mode, family, address) -} - -func (k direct) dbusFinalise(sessionBus, systemBus dbus.ProxyPair, session, system *hst.BusConfig) (final *dbus.Final, err error) { - return dbus.Finalise(sessionBus, systemBus, session, system) -} - -func (k direct) dbusProxyStart(proxy *dbus.Proxy) error { return proxy.Start() } -func (k direct) dbusProxyClose(proxy *dbus.Proxy) { proxy.Close() } -func (k direct) dbusProxyWait(proxy *dbus.Proxy) error { return proxy.Wait() } diff --git a/system/dispatcher_test.go b/system/dispatcher_test.go deleted file mode 100644 index fd6e5d09..00000000 --- a/system/dispatcher_test.go +++ /dev/null @@ -1,379 +0,0 @@ -package system - -import ( - "log" - "os" - "reflect" - "slices" - "testing" - "unsafe" - - "hakurei.app/container/stub" - "hakurei.app/hst" - "hakurei.app/system/acl" - "hakurei.app/system/dbus" - "hakurei.app/system/internal/xcb" -) - -// call initialises a [stub.Call]. -// This keeps composites analysis happy without making the test cases too bloated. -func call(name string, args stub.ExpectArgs, ret any, err error) stub.Call { - return stub.Call{Name: name, Args: args, Ret: ret, Err: err} -} - -type opBehaviourTestCase struct { - name string - uid int - ec hst.Enablement - op Op - - apply []stub.Call - wantErrApply error - - revert []stub.Call - wantErrRevert error -} - -func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) { - t.Helper() - - t.Run("behaviour", func(t *testing.T) { - t.Helper() - t.Parallel() - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - t.Helper() - t.Parallel() - - var ec *Criteria - if tc.ec != 0xff { - ec = (*Criteria)(&tc.ec) - } - - sys, s := InternalNew(t, stub.Expect{Calls: slices.Concat(tc.apply, []stub.Call{{Name: stub.CallSeparator}}, tc.revert)}, tc.uid) - defer stub.HandleExit(t) - errApply := tc.op.apply(sys) - s.Expects(stub.CallSeparator) - if !reflect.DeepEqual(errApply, tc.wantErrApply) { - t.Errorf("apply: error = %v, want %v", errApply, tc.wantErrApply) - } - if errApply != nil { - goto out - } - - if err := tc.op.revert(sys, ec); !reflect.DeepEqual(err, tc.wantErrRevert) { - t.Errorf("revert: error = %v, want %v", err, tc.wantErrRevert) - } - - out: - s.VisitIncomplete(func(s *stub.Stub[syscallDispatcher]) { - count := s.Pos() - 1 // separator - if count < len(tc.apply) { - t.Errorf("apply: %d calls, want %d", count, len(tc.apply)) - } else { - t.Errorf("revert: %d calls, want %d", count-len(tc.apply), len(tc.revert)) - } - }) - }) - } - }) -} - -type opsBuilderTestCase struct { - name string - uid int - f func(t *testing.T, sys *I) - want []Op - exp stub.Expect -} - -func checkOpsBuilder(t *testing.T, fname string, testCases []opsBuilderTestCase) { - t.Helper() - - t.Run("build", func(t *testing.T) { - t.Helper() - t.Parallel() - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - t.Helper() - t.Parallel() - - sys, s := InternalNew(t, tc.exp, tc.uid) - defer stub.HandleExit(t) - tc.f(t, sys) - s.VisitIncomplete(func(s *stub.Stub[syscallDispatcher]) { - t.Helper() - - t.Errorf("%s: %d calls, want %d", fname, s.Pos(), s.Len()) - }) - if !slices.EqualFunc(sys.ops, tc.want, func(op Op, v Op) bool { return op.Is(v) }) { - t.Errorf("ops: %#v, want %#v", sys.ops, tc.want) - } - }) - } - }) -} - -type opIsTestCase struct { - name string - op, v Op - want bool -} - -func checkOpIs(t *testing.T, testCases []opIsTestCase) { - t.Helper() - - t.Run("is", func(t *testing.T) { - t.Helper() - t.Parallel() - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - t.Helper() - t.Parallel() - - if got := tc.op.Is(tc.v); got != tc.want { - t.Errorf("Is: %v, want %v", got, tc.want) - } - }) - } - }) -} - -type opMetaTestCase struct { - name string - op Op - - wantType hst.Enablement - wantPath string - wantString string -} - -func checkOpMeta(t *testing.T, testCases []opMetaTestCase) { - t.Helper() - - t.Run("meta", func(t *testing.T) { - t.Helper() - t.Parallel() - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - t.Helper() - t.Parallel() - - t.Run("type", func(t *testing.T) { - t.Helper() - - if got := tc.op.Type(); got != tc.wantType { - t.Errorf("Type: %q, want %q", got, tc.wantType) - } - }) - - t.Run("path", func(t *testing.T) { - t.Helper() - - if got := tc.op.Path(); got != tc.wantPath { - t.Errorf("Path: %q, want %q", got, tc.wantPath) - } - }) - - t.Run("string", func(t *testing.T) { - t.Helper() - - if got := tc.op.String(); got != tc.wantString { - t.Errorf("String: %s, want %s", got, tc.wantString) - } - }) - }) - } - }) -} - -// InternalNew initialises [I] with a stub syscallDispatcher. -func InternalNew(t *testing.T, want stub.Expect, uid int) (*I, *stub.Stub[syscallDispatcher]) { - k := &kstub{stub.New(t, func(s *stub.Stub[syscallDispatcher]) syscallDispatcher { return &kstub{s} }, want)} - sys := New(t.Context(), k, uid) - sys.syscallDispatcher = k - return sys, k.Stub -} - -type kstub struct{ *stub.Stub[syscallDispatcher] } - -func (k *kstub) new(f func(k syscallDispatcher)) { k.Helper(); k.New(f) } - -func (k *kstub) stat(name string) (fi os.FileInfo, err error) { - k.Helper() - expect := k.Expects("stat") - err = expect.Error( - stub.CheckArg(k.Stub, "name", name, 0)) - if err == nil { - fi = expect.Ret.(os.FileInfo) - } - return -} - -func (k *kstub) open(name string) (f osFile, err error) { - k.Helper() - expect := k.Expects("open") - err = expect.Error( - stub.CheckArg(k.Stub, "name", name, 0)) - if err == nil { - f = expect.Ret.(osFile) - } - return -} - -func (k *kstub) mkdir(name string, perm os.FileMode) error { - k.Helper() - return k.Expects("mkdir").Error( - stub.CheckArg(k.Stub, "name", name, 0), - stub.CheckArg(k.Stub, "perm", perm, 1)) -} - -func (k *kstub) chmod(name string, mode os.FileMode) error { - k.Helper() - return k.Expects("chmod").Error( - stub.CheckArg(k.Stub, "name", name, 0), - stub.CheckArg(k.Stub, "mode", mode, 1)) -} - -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) println(v ...any) { - k.Helper() - k.Expects("println") - if !stub.CheckArgReflect(k.Stub, "v", v, 0) { - k.FailNow() - } -} - -func (k *kstub) aclUpdate(name string, uid int, perms ...acl.Perm) error { - k.Helper() - return k.Expects("aclUpdate").Error( - stub.CheckArg(k.Stub, "name", name, 0), - stub.CheckArg(k.Stub, "uid", uid, 1), - stub.CheckArgReflect(k.Stub, "perms", perms, 2)) -} - -func (k *kstub) xcbChangeHosts(mode xcb.HostMode, family xcb.Family, address string) error { - k.Helper() - return k.Expects("xcbChangeHosts").Error( - stub.CheckArg(k.Stub, "mode", mode, 0), - stub.CheckArg(k.Stub, "family", family, 1), - stub.CheckArg(k.Stub, "address", address, 2)) -} - -func (k *kstub) dbusFinalise(sessionBus, systemBus dbus.ProxyPair, session, system *hst.BusConfig) (final *dbus.Final, err error) { - k.Helper() - expect := k.Expects("dbusFinalise") - - final = expect.Ret.(*dbus.Final) - err = expect.Error( - stub.CheckArg(k.Stub, "sessionBus", sessionBus, 0), - stub.CheckArg(k.Stub, "systemBus", systemBus, 1), - stub.CheckArgReflect(k.Stub, "session", session, 2), - stub.CheckArgReflect(k.Stub, "system", system, 3)) - if err != nil { - final = nil - } - return -} - -func (k *kstub) dbusProxyStart(proxy *dbus.Proxy) error { - k.Helper() - return k.dbusProxySCW(k.Expects("dbusProxyStart"), proxy) -} -func (k *kstub) dbusProxyClose(proxy *dbus.Proxy) { - k.Helper() - if k.dbusProxySCW(k.Expects("dbusProxyClose"), proxy) != nil { - k.Fail() - } -} -func (k *kstub) dbusProxyWait(proxy *dbus.Proxy) error { - k.Helper() - return k.dbusProxySCW(k.Expects("dbusProxyWait"), proxy) -} -func (k *kstub) dbusProxySCW(expect *stub.Call, proxy *dbus.Proxy) error { - k.Helper() - v := reflect.ValueOf(proxy).Elem() - - if ctxV := v.FieldByName("ctx"); ctxV.IsNil() { - k.Errorf("proxy: ctx = %s", ctxV.String()) - return os.ErrInvalid - } - - finalV := v.FieldByName("final") - if gotFinal := reflect.NewAt(finalV.Type(), unsafe.Pointer(finalV.UnsafeAddr())).Elem().Interface().(*dbus.Final); !reflect.DeepEqual(gotFinal, expect.Args[0]) { - k.Errorf("proxy: final = %#v, want %#v", gotFinal, expect.Args[0]) - return os.ErrInvalid - } - - outputV := v.FieldByName("output") - if _, ok := reflect.NewAt(outputV.Type(), unsafe.Pointer(outputV.UnsafeAddr())).Elem().Interface().(*linePrefixWriter); !ok { - k.Errorf("proxy: output = %s", outputV.String()) - return os.ErrInvalid - } - - return expect.Err -} - -func (k *kstub) GetLogger() *log.Logger { panic("unreachable") } - -func (k *kstub) IsVerbose() bool { k.Helper(); return k.Expects("isVerbose").Ret.(bool) } -func (k *kstub) SwapVerbose(verbose bool) bool { - k.Helper() - expect := k.Expects("swapVerbose") - if expect.Error( - stub.CheckArg(k.Stub, "verbose", verbose, 0)) != nil { - k.FailNow() - } - return expect.Ret.(bool) -} - -// ignoreValue marks a value to be ignored by the test suite. -type ignoreValue struct{} - -func (k *kstub) Verbose(v ...any) { - k.Helper() - expect := k.Expects("verbose") - - // translate ignores in v - if want, ok := expect.Args[0].([]any); ok && len(v) == len(want) { - for i, a := range want { - if _, ok = a.(ignoreValue); ok { - v[i] = ignoreValue{} - } - } - } - - if expect.Error( - stub.CheckArgReflect(k.Stub, "v", v, 0)) != nil { - k.FailNow() - } -} - -func (k *kstub) Verbosef(format string, v ...any) { - k.Helper() - if k.Expects("verbosef").Error( - stub.CheckArg(k.Stub, "format", format, 0), - stub.CheckArgReflect(k.Stub, "v", v, 1)) != nil { - k.FailNow() - } -} - -func (k *kstub) Suspend() bool { k.Helper(); return k.Expects("suspend").Ret.(bool) } -func (k *kstub) Resume() bool { k.Helper(); return k.Expects("resume").Ret.(bool) } -func (k *kstub) BeforeExit() { k.Helper(); k.Expects("beforeExit") } diff --git a/system/internal/xcb/changehosts.go b/system/internal/xcb/changehosts.go deleted file mode 100644 index 8b05614b..00000000 --- a/system/internal/xcb/changehosts.go +++ /dev/null @@ -1,17 +0,0 @@ -package xcb - -import "errors" - -var ErrChangeHosts = errors.New("xcb_change_hosts() failed") - -func ChangeHosts(mode HostMode, family Family, address string) error { - conn := new(connection) - if err := conn.connect(); err != nil { - conn.disconnect() - return err - } else { - defer conn.disconnect() - } - - return conn.changeHostsChecked(mode, family, address) -} diff --git a/system/internal/xcb/xcb.go b/system/internal/xcb/xcb.go deleted file mode 100644 index 3dc62c3f..00000000 --- a/system/internal/xcb/xcb.go +++ /dev/null @@ -1,127 +0,0 @@ -// Package xcb implements X11 ChangeHosts via libxcb. -package xcb - -import ( - "runtime" - "unsafe" -) - -/* -#cgo linux pkg-config: --static xcb - -#include <stdlib.h> -#include <xcb/xcb.h> - -static int hakurei_xcb_change_hosts_checked(xcb_connection_t *c, - uint8_t mode, uint8_t family, - uint16_t address_len, const uint8_t *address) { - int ret; - xcb_generic_error_t *e; - xcb_void_cookie_t cookie; - - cookie = xcb_change_hosts_checked(c, mode, family, address_len, address); - free((void *)address); - - ret = xcb_connection_has_error(c); - if (ret != 0) - return ret; - - e = xcb_request_check(c, cookie); - if (e != NULL) { - // don't want to deal with xcb errors - free((void *)e); - ret = -1; - } - - return ret; -} -*/ -import "C" - -const ( - HostModeInsert = C.XCB_HOST_MODE_INSERT - HostModeDelete = C.XCB_HOST_MODE_DELETE - - FamilyInternet = C.XCB_FAMILY_INTERNET - FamilyDecnet = C.XCB_FAMILY_DECNET - FamilyChaos = C.XCB_FAMILY_CHAOS - FamilyServerInterpreted = C.XCB_FAMILY_SERVER_INTERPRETED - FamilyInternet6 = C.XCB_FAMILY_INTERNET_6 -) - -type ( - HostMode = C.xcb_host_mode_t - Family = C.xcb_family_t -) - -func (conn *connection) changeHostsChecked(mode HostMode, family Family, address string) error { - ret := C.hakurei_xcb_change_hosts_checked( - conn.c, - C.uint8_t(mode), - C.uint8_t(family), - C.uint16_t(len(address)), - (*C.uint8_t)(unsafe.Pointer(C.CString(address))), - ) - switch ret { - case 0: - return nil - case -1: - return ErrChangeHosts - default: - return ConnectionError(ret) - } -} - -type connection struct{ c *C.xcb_connection_t } - -func (conn *connection) connect() error { - conn.c = C.xcb_connect(nil, nil) - runtime.SetFinalizer(conn, (*connection).disconnect) - return conn.hasError() -} - -func (conn *connection) hasError() error { - ret := C.xcb_connection_has_error(conn.c) - if ret == 0 { - return nil - } - return ConnectionError(ret) -} - -func (conn *connection) disconnect() { - C.xcb_disconnect(conn.c) - - // no need for a finalizer anymore - runtime.SetFinalizer(conn, nil) -} - -const ( - ConnError ConnectionError = C.XCB_CONN_ERROR - ConnClosedExtNotSupported ConnectionError = C.XCB_CONN_CLOSED_EXT_NOTSUPPORTED - ConnClosedMemInsufficient ConnectionError = C.XCB_CONN_CLOSED_MEM_INSUFFICIENT - ConnClosedReqLenExceed ConnectionError = C.XCB_CONN_CLOSED_REQ_LEN_EXCEED - ConnClosedParseErr ConnectionError = C.XCB_CONN_CLOSED_PARSE_ERR - ConnClosedInvalidScreen ConnectionError = C.XCB_CONN_CLOSED_INVALID_SCREEN -) - -// ConnectionError represents an error returned by xcb_connection_has_error. -type ConnectionError int - -func (ce ConnectionError) Error() string { - switch ce { - case ConnError: - return "connection error" - case ConnClosedExtNotSupported: - return "extension not supported" - case ConnClosedMemInsufficient: - return "memory not available" - case ConnClosedReqLenExceed: - return "request length exceeded" - case ConnClosedParseErr: - return "invalid display string" - case ConnClosedInvalidScreen: - return "server has no screen matching display" - default: - return "generic X11 failure" - } -} diff --git a/system/link.go b/system/link.go deleted file mode 100644 index a13adaae..00000000 --- a/system/link.go +++ /dev/null @@ -1,50 +0,0 @@ -package system - -import ( - "fmt" - - "hakurei.app/container/check" - "hakurei.app/hst" -) - -// Link calls LinkFileType with the [Process] criteria. -func (sys *I) Link(oldname, newname *check.Absolute) *I { - return sys.LinkFileType(Process, oldname, newname) -} - -// LinkFileType maintains a hardlink until its [Enablement] is no longer satisfied. -func (sys *I) LinkFileType(et hst.Enablement, oldname, newname *check.Absolute) *I { - sys.ops = append(sys.ops, &hardlinkOp{et, newname.String(), oldname.String()}) - return sys -} - -// hardlinkOp implements [I.LinkFileType]. -type hardlinkOp struct { - et hst.Enablement - dst, src string -} - -func (l *hardlinkOp) Type() hst.Enablement { return l.et } - -func (l *hardlinkOp) apply(sys *I) error { - sys.msg.Verbose("linking", l) - return newOpError("hardlink", sys.link(l.src, l.dst), false) -} - -func (l *hardlinkOp) revert(sys *I, ec *Criteria) error { - if ec.hasType(l.Type()) { - sys.msg.Verbosef("removing hard link %q", l.dst) - return newOpError("hardlink", sys.remove(l.dst), true) - } else { - sys.msg.Verbosef("skipping hard link %q", l.dst) - return nil - } -} - -func (l *hardlinkOp) Is(o Op) bool { - target, ok := o.(*hardlinkOp) - return ok && l != nil && target != nil && *l == *target -} - -func (l *hardlinkOp) Path() string { return l.src } -func (l *hardlinkOp) String() string { return fmt.Sprintf("%q from %q", l.dst, l.src) } diff --git a/system/link_test.go b/system/link_test.go deleted file mode 100644 index d719931f..00000000 --- a/system/link_test.go +++ /dev/null @@ -1,87 +0,0 @@ -package system - -import ( - "testing" - - "hakurei.app/container/stub" - "hakurei.app/hst" -) - -func TestHardlinkOp(t *testing.T) { - t.Parallel() - - checkOpBehaviour(t, []opBehaviourTestCase{ - {"link", 0xbeef, 0xff, &hardlinkOp{hst.EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"linking", &hardlinkOp{hst.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", 0xbeef, 0xff, &hardlinkOp{hst.EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"linking", &hardlinkOp{hst.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", 0xbeef, hst.EWayland | hst.EX11, &hardlinkOp{hst.EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"linking", &hardlinkOp{hst.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", 0xbeef, 0xff, &hardlinkOp{hst.EPulse, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"linking", &hardlinkOp{hst.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", 0xcafe, func(_ *testing.T, sys *I) { - sys.LinkFileType(User, m("/run/user/1000/pulse/native"), m("/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse")) - }, []Op{ - &hardlinkOp{User, "/run/user/1000/hakurei/9663730666a44cfc2a81610379e02ed6/pulse", "/run/user/1000/pulse/native"}, - }, stub.Expect{}}, - - {"link", 0xcafe, func(_ *testing.T, sys *I) { - sys.Link(m("/run/user/1000/pulse/native"), m("/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/mkdir.go b/system/mkdir.go deleted file mode 100644 index f375579c..00000000 --- a/system/mkdir.go +++ /dev/null @@ -1,76 +0,0 @@ -package system - -import ( - "errors" - "fmt" - "os" - - "hakurei.app/container/check" - "hakurei.app/hst" -) - -// Ensure ensures the existence of a directory. -func (sys *I) Ensure(name *check.Absolute, perm os.FileMode) *I { - sys.ops = append(sys.ops, &mkdirOp{User, name.String(), perm, false}) - return sys -} - -// Ephemeral ensures the existence of a directory until its [Enablement] is no longer satisfied. -func (sys *I) Ephemeral(et hst.Enablement, name *check.Absolute, perm os.FileMode) *I { - sys.ops = append(sys.ops, &mkdirOp{et, name.String(), perm, true}) - return sys -} - -// mkdirOp implements [I.Ensure] and [I.Ephemeral]. -type mkdirOp struct { - et hst.Enablement - path string - perm os.FileMode - ephemeral bool -} - -func (m *mkdirOp) Type() hst.Enablement { return m.et } - -func (m *mkdirOp) apply(sys *I) error { - sys.msg.Verbose("ensuring directory", m) - - if err := sys.mkdir(m.path, m.perm); err != nil { - if !errors.Is(err, os.ErrExist) { - return newOpError("mkdir", err, false) - } - // directory exists, ensure mode - return newOpError("mkdir", sys.chmod(m.path, m.perm), false) - } else { - return nil - } -} - -func (m *mkdirOp) revert(sys *I, ec *Criteria) error { - if !m.ephemeral { - // skip non-ephemeral dir and do not log anything - return nil - } - - if ec.hasType(m.Type()) { - sys.msg.Verbose("destroying ephemeral directory", m) - return newOpError("mkdir", sys.remove(m.path), true) - } else { - sys.msg.Verbose("skipping ephemeral directory", m) - return nil - } -} - -func (m *mkdirOp) Is(o Op) bool { - target, ok := o.(*mkdirOp) - return ok && m != nil && target != nil && *m == *target -} - -func (m *mkdirOp) Path() string { return m.path } - -func (m *mkdirOp) String() string { - t := "ensure" - if m.ephemeral { - t = TypeString(m.Type()) - } - return fmt.Sprintf("mode: %s type: %s path: %q", m.perm.String(), t, m.path) -} diff --git a/system/mkdir_test.go b/system/mkdir_test.go deleted file mode 100644 index e8ffde2a..00000000 --- a/system/mkdir_test.go +++ /dev/null @@ -1,113 +0,0 @@ -package system - -import ( - "os" - "testing" - - "hakurei.app/container/stub" -) - -func TestMkdirOp(t *testing.T) { - t.Parallel() - - checkOpBehaviour(t, []opBehaviourTestCase{ - {"mkdir", 0xbeef, 0xff, &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false}, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false}}}, nil, nil), - call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, stub.UniqueError(2)), - }, &OpError{Op: "mkdir", Err: stub.UniqueError(2)}, nil, nil}, - - {"chmod", 0xbeef, 0xff, &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false}, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false}}}, nil, nil), - call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, os.ErrExist), - call("chmod", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, stub.UniqueError(1)), - }, &OpError{Op: "mkdir", Err: stub.UniqueError(1)}, nil, nil}, - - {"remove", 0xbeef, 0xff, &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil), - call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, nil), - }, nil, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"destroying ephemeral directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil), - call("remove", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, stub.UniqueError(0)), - }, &OpError{Op: "mkdir", Err: stub.UniqueError(0), Revert: true}}, - - {"success exist chmod", 0xbeef, 0xff, &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false}, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false}}}, nil, nil), - call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, os.ErrExist), - call("chmod", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, nil), - }, nil, nil, nil}, - - {"success ensure", 0xbeef, 0xff, &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false}, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, false}}}, nil, nil), - call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, nil), - }, nil, nil, nil}, - - {"success skip", 0xbeef, 0xff, &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil), - call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, nil), - }, nil, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"skipping ephemeral directory", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil), - }, nil}, - - {"success", 0xbeef, 0xff, &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil), - call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, nil), - }, nil, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"destroying ephemeral directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil), - call("remove", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, nil), - }, nil}, - }) - - checkOpsBuilder(t, "EnsureEphemeral", []opsBuilderTestCase{ - {"ensure", 0xcafe, func(_ *testing.T, sys *I) { - sys.Ensure(m("/tmp/hakurei.0"), 0700) - }, []Op{ - &mkdirOp{User, "/tmp/hakurei.0", 0700, false}, - }, stub.Expect{}}, - - {"ephemeral", 0xcafe, func(_ *testing.T, sys *I) { - sys.Ephemeral(Process, m("/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"), 0711) - }, []Op{ - &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}, - }, stub.Expect{}}, - }) - - checkOpIs(t, []opIsTestCase{ - {"nil", (*mkdirOp)(nil), (*mkdirOp)(nil), false}, - {"zero", new(mkdirOp), new(mkdirOp), true}, - - {"ephemeral differs", - &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, false}, - &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true}, - false}, - - {"perm differs", - &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0701, true}, - &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true}, - false}, - - {"path differs", - &mkdirOp{Process, "/tmp/hakurei.1/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true}, - &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true}, - false}, - - {"et differs", - &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true}, - &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true}, - false}, - - {"equals", - &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true}, - &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true}, - true}, - }) - - checkOpMeta(t, []opMetaTestCase{ - {"ensure", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, false}, - User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", - `mode: -rwx------ type: ensure path: "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"`}, - - {"ephemeral", &mkdirOp{User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0700, true}, - User, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", - `mode: -rwx------ type: user path: "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"`}, - }) -} diff --git a/system/output.go b/system/output.go deleted file mode 100644 index 71acace4..00000000 --- a/system/output.go +++ /dev/null @@ -1,88 +0,0 @@ -package system - -import ( - "errors" - "net" - "os" - - "hakurei.app/container" - "hakurei.app/message" -) - -// OpError is returned by [I.Commit] and [I.Revert]. -type OpError struct { - Op string - Err error - Msg string - Revert bool -} - -func (e *OpError) Unwrap() error { return e.Err } -func (e *OpError) Error() string { - if e.Msg != "" { - return e.Msg - } - - switch { - case errors.As(e.Err, new(*os.PathError)), - errors.As(e.Err, new(*os.LinkError)), - errors.As(e.Err, new(*net.OpError)), - errors.As(e.Err, new(*container.StartError)): - return e.Err.Error() - - default: - if !e.Revert { - return "apply " + e.Op + ": " + e.Err.Error() - } else { - return "revert " + e.Op + ": " + e.Err.Error() - } - } -} - -func (e *OpError) Message() string { - switch { - case e.Msg != "": - return e.Error() - - default: - return "cannot " + e.Error() - } -} - -// newOpError returns an [OpError] without a message string. -func newOpError(op string, err error, revert bool) error { - if err == nil { - return nil - } - return &OpError{op, err, "", revert} -} - -// newOpErrorMessage returns an [OpError] with an overriding message string. -func newOpErrorMessage(op string, err error, message string, revert bool) error { - if err == nil { - return nil - } - return &OpError{op, err, message, revert} -} - -func printJoinedError(println func(v ...any), fallback string, err error) { - var joinErr interface { - Unwrap() []error - error - } - if !errors.As(err, &joinErr) { - if m, ok := message.GetMessage(err); ok { - println(m) - } else { - println(fallback, err) - } - } else { - for _, err = range joinErr.Unwrap() { - if m, ok := message.GetMessage(err); ok { - println(m) - } else { - println(err.Error()) - } - } - } -} diff --git a/system/output_test.go b/system/output_test.go deleted file mode 100644 index c0cbfdfe..00000000 --- a/system/output_test.go +++ /dev/null @@ -1,140 +0,0 @@ -package system - -import ( - "errors" - "net" - "os" - "reflect" - "syscall" - "testing" - - "hakurei.app/container" - "hakurei.app/message" -) - -func TestOpError(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - err error - s string - is error - isF error - msg string - }{ - {"message", newOpErrorMessage("dbus", ErrDBusConfig, - "attempted to create message bus proxy args without session bus config", false), - "attempted to create message bus proxy args without session bus config", - ErrDBusConfig, syscall.ENOTRECOVERABLE, - "attempted to create message bus proxy args without session bus config"}, - - {"apply", newOpError("tmpfile", syscall.EBADE, false), - "apply tmpfile: invalid exchange", - syscall.EBADE, syscall.EBADF, - "cannot apply tmpfile: invalid exchange"}, - - {"revert", newOpError("wayland", syscall.EBADF, true), - "revert wayland: bad file descriptor", - syscall.EBADF, syscall.EBADE, - "cannot revert wayland: bad file descriptor"}, - - {"path", newOpError("tmpfile", &os.PathError{Op: "stat", Path: "/run/dbus", Err: syscall.EISDIR}, false), - "stat /run/dbus: is a directory", - syscall.EISDIR, syscall.ENOTDIR, - "cannot stat /run/dbus: is a directory"}, - - {"net", newOpError("wayland", &net.OpError{Op: "dial", Net: "unix", Addr: &net.UnixAddr{Name: "/run/user/1000/wayland-1", Net: "unix"}, Err: syscall.ENOENT}, false), - "dial unix /run/user/1000/wayland-1: no such file or directory", - syscall.ENOENT, syscall.EPERM, - "cannot dial unix /run/user/1000/wayland-1: no such file or directory"}, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - t.Run("error", func(t *testing.T) { - if got := tc.err.Error(); got != tc.s { - t.Errorf("Error: %q, want %q", got, tc.s) - } - }) - - t.Run("is", func(t *testing.T) { - if !errors.Is(tc.err, tc.is) { - t.Error("Is: unexpected false") - } - if errors.Is(tc.err, tc.isF) { - t.Error("Is: unexpected true") - } - }) - - t.Run("msg", func(t *testing.T) { - if got, ok := message.GetMessage(tc.err); !ok { - if tc.msg != "" { - t.Errorf("GetMessage: err does not implement MessageError") - } - return - } else if got != tc.msg { - t.Errorf("GetMessage: %q, want %q", got, tc.msg) - } - }) - }) - } - - t.Run("new", func(t *testing.T) { - if err := newOpError("check", nil, false); err != nil { - t.Errorf("newOpError: %v", err) - } - if err := newOpErrorMessage("check", nil, "", false); err != nil { - t.Errorf("newOpErrorMessage: %v", err) - } - }) -} - -func TestPrintJoinedError(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - err error - want [][]any - }{ - {"nil", nil, [][]any{{"not a joined error:", nil}}}, - {"single", errors.Join(syscall.EINVAL), [][]any{{"invalid argument"}}}, - - {"unwrapped", syscall.EINVAL, [][]any{{"not a joined error:", syscall.EINVAL}}}, - {"unwrapped message", &OpError{ - Op: "meow", - Err: syscall.EBADFD, - }, [][]any{ - {"cannot apply meow: file descriptor in bad state"}, - }}, - - {"many", errors.Join(syscall.ENOTRECOVERABLE, syscall.ETIMEDOUT, syscall.EBADFD), [][]any{ - {"state not recoverable"}, - {"connection timed out"}, - {"file descriptor in bad state"}, - }}, - {"many message", errors.Join( - &container.StartError{Step: "meow", Err: syscall.ENOMEM}, - &os.PathError{Op: "meow", Path: "/proc/nonexistent", Err: syscall.ENOSYS}, - &os.LinkError{Op: "link", Old: "/etc", New: "/proc/nonexistent", Err: syscall.ENOENT}, - &OpError{Op: "meow", Err: syscall.ENODEV, Revert: true}, - ), [][]any{ - {"cannot meow: cannot allocate memory"}, - {"meow /proc/nonexistent: function not implemented"}, - {"link /etc /proc/nonexistent: no such file or directory"}, - {"cannot revert meow: no such device"}, - }}, - } - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - var got [][]any - printJoinedError(func(v ...any) { got = append(got, v) }, "not a joined error:", tc.err) - if !reflect.DeepEqual(got, tc.want) { - t.Errorf("printJoinedError: %#v, want %#v", got, tc.want) - } - }) - } -} diff --git a/system/system.go b/system/system.go deleted file mode 100644 index cadaab04..00000000 --- a/system/system.go +++ /dev/null @@ -1,177 +0,0 @@ -// Package system provides helpers to apply and revert groups of operations to the system. -package system - -import ( - "context" - "errors" - "strings" - - "hakurei.app/hst" - "hakurei.app/message" -) - -const ( - // User type is reverted at final instance exit. - User = hst.EM << iota - // Process type is unconditionally reverted on exit. - Process - - CM -) - -// Criteria specifies types of Op to revert. -type Criteria hst.Enablement - -func (ec *Criteria) hasType(t hst.Enablement) bool { - // nil criteria: revert everything except User - if ec == nil { - return t != User - } - - return hst.Enablement(*ec)&t != 0 -} - -// Op is a reversible system operation. -type Op interface { - // Type returns [Op]'s enablement type, for matching a revert criteria. - Type() hst.Enablement - - apply(sys *I) error - revert(sys *I, ec *Criteria) error - - Is(o Op) bool - Path() string - String() string -} - -// TypeString extends [Enablement.String] to support [User] and [Process]. -func TypeString(e hst.Enablement) string { - switch e { - case User: - return "user" - case Process: - return "process" - default: - buf := new(strings.Builder) - buf.Grow(48) - if v := e &^ User &^ Process; v != 0 { - buf.WriteString(v.String()) - } - - for i := User; i < CM; i <<= 1 { - if e&i != 0 { - buf.WriteString(", " + TypeString(i)) - } - } - return strings.TrimPrefix(buf.String(), ", ") - } -} - -// New returns the address of a new [I] targeting uid. -func New(ctx context.Context, msg message.Msg, uid int) (sys *I) { - if ctx == nil || msg == nil || uid < 0 { - panic("invalid call to New") - } - return &I{ctx: ctx, msg: msg, uid: uid, syscallDispatcher: direct{}} -} - -// An I provides deferred operating system interaction. [I] must not be copied. -// Methods of [I] must not be used concurrently. -type I struct { - _ noCopy - - uid int - ops []Op - ctx context.Context - - // the behaviour of Commit is only defined for up to one call - committed bool - // the behaviour of Revert is only defined for up to one call - reverted bool - - msg message.Msg - syscallDispatcher -} - -func (sys *I) UID() int { return sys.uid } - -// Equal returns whether all [Op] instances held by sys matches that of target. -func (sys *I) Equal(target *I) bool { - if sys == nil || target == nil || sys.uid != target.uid || len(sys.ops) != len(target.ops) { - return false - } - - for i, o := range sys.ops { - if !o.Is(target.ops[i]) { - return false - } - } - - return true -} - -// Commit applies all [Op] held by [I] and reverts all successful [Op] on first error encountered. -// Commit must not be called more than once. -func (sys *I) Commit() error { - if sys.committed { - panic("attempting to commit twice") - } - sys.committed = true - - sp := New(sys.ctx, sys.msg, sys.uid) - sp.syscallDispatcher = sys.syscallDispatcher - sp.ops = make([]Op, 0, len(sys.ops)) // prevent copies during commits - defer func() { - // sp is set to nil when all ops are applied - if sp != nil { - // rollback partial commit - sys.msg.Verbosef("commit faulted after %d ops, rolling back partial commit", len(sp.ops)) - if err := sp.Revert(nil); err != nil { - printJoinedError(sys.println, "cannot revert partial commit:", err) - } - } - }() - - for _, o := range sys.ops { - if err := o.apply(sys); err != nil { - return err - } else { - // register partial commit - sp.ops = append(sp.ops, o) - } - } - - // disarm partial commit rollback - sp = nil - return nil -} - -// Revert reverts all [Op] meeting [Criteria] held by [I]. -func (sys *I) Revert(ec *Criteria) error { - if sys.reverted { - panic("attempting to revert twice") - } - sys.reverted = true - - // collect errors - errs := make([]error, len(sys.ops)) - for i := range sys.ops { - errs[i] = sys.ops[len(sys.ops)-i-1].revert(sys, ec) - } - - // errors.Join filters nils - return errors.Join(errs...) -} - -// noCopy may be added to structs which must not be copied -// after the first use. -// -// See https://golang.org/issues/8005#issuecomment-190753527 -// for details. -// -// Note that it must not be embedded, due to the Lock and Unlock methods. -type noCopy struct{} - -// Lock is a no-op used by -copylocks checker from `go vet`. -func (*noCopy) Lock() {} -func (*noCopy) Unlock() {} diff --git a/system/system_test.go b/system/system_test.go deleted file mode 100644 index 83df9dee..00000000 --- a/system/system_test.go +++ /dev/null @@ -1,331 +0,0 @@ -package system - -import ( - "errors" - "os" - "reflect" - "slices" - "strconv" - "testing" - - "hakurei.app/container/check" - "hakurei.app/container/stub" - "hakurei.app/hst" - "hakurei.app/message" - "hakurei.app/system/internal/xcb" -) - -func TestCriteria(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - ec, t hst.Enablement - want bool - }{ - {"nil", 0xff, hst.EWayland, true}, - {"nil user", 0xff, User, false}, - {"all", hst.EWayland | hst.EX11 | hst.EDBus | hst.EPulse | User | Process, Process, true}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - var criteria *Criteria - if tc.ec != 0xff { - criteria = (*Criteria)(&tc.ec) - } - if got := criteria.hasType(tc.t); got != tc.want { - t.Errorf("hasType: got %v, want %v", - got, tc.want) - } - }) - } -} - -func TestTypeString(t *testing.T) { - t.Parallel() - - testCases := []struct { - e hst.Enablement - want string - }{ - {hst.EWayland, hst.EWayland.String()}, - {hst.EX11, hst.EX11.String()}, - {hst.EDBus, hst.EDBus.String()}, - {hst.EPulse, hst.EPulse.String()}, - {User, "user"}, - {Process, "process"}, - {User | Process, "user, process"}, - {hst.EWayland | User | Process, "wayland, user, process"}, - {hst.EX11 | Process, "x11, process"}, - } - - for _, tc := range testCases { - t.Run("label type string "+strconv.Itoa(int(tc.e)), func(t *testing.T) { - t.Parallel() - if got := TypeString(tc.e); got != tc.want { - t.Errorf("TypeString: %q, want %q", got, tc.want) - } - }) - } -} - -func TestNew(t *testing.T) { - t.Parallel() - - t.Run("panic", func(t *testing.T) { - t.Run("ctx", func(t *testing.T) { - defer func() { - want := "invalid call to New" - if r := recover(); r != want { - t.Errorf("recover: %v, want %v", r, want) - } - }() - New(nil, message.New(nil), 0) - }) - - t.Run("msg", func(t *testing.T) { - defer func() { - want := "invalid call to New" - if r := recover(); r != want { - t.Errorf("recover: %v, want %v", r, want) - } - }() - New(t.Context(), nil, 0) - }) - - t.Run("uid", func(t *testing.T) { - defer func() { - want := "invalid call to New" - if r := recover(); r != want { - t.Errorf("recover: %v, want %v", r, want) - } - }() - New(t.Context(), message.New(nil), -1) - }) - }) - - sys := New(t.Context(), message.New(nil), 0xbeef) - if sys.ctx == nil { - t.Error("New: ctx = nil") - } - if got := sys.UID(); got != 0xbeef { - t.Errorf("UID: %d", got) - } -} - -func TestEqual(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - sys *I - v *I - want bool - }{ - {"simple UID", - New(t.Context(), message.New(nil), 150), - New(t.Context(), message.New(nil), 150), - true}, - - {"simple UID differ", - New(t.Context(), message.New(nil), 150), - New(t.Context(), message.New(nil), 151), - false}, - - {"simple UID nil", - New(t.Context(), message.New(nil), 150), - nil, - false}, - - {"op length mismatch", - New(t.Context(), message.New(nil), 150). - ChangeHosts("chronos"), - New(t.Context(), message.New(nil), 150). - ChangeHosts("chronos"). - Ensure(m("/run"), 0755), - false}, - - {"op value mismatch", - New(t.Context(), message.New(nil), 150). - ChangeHosts("chronos"). - Ensure(m("/run"), 0644), - New(t.Context(), message.New(nil), 150). - ChangeHosts("chronos"). - Ensure(m("/run"), 0755), - false}, - - {"op type mismatch", - New(t.Context(), message.New(nil), 150). - ChangeHosts("chronos"). - Wayland(m("/proc/nonexistent/dst"), m("/proc/nonexistent/src"), "\x00", "\x00"), - New(t.Context(), message.New(nil), 150). - ChangeHosts("chronos"). - Ensure(m("/run"), 0755), - false}, - - {"op equals", - New(t.Context(), message.New(nil), 150). - ChangeHosts("chronos"). - Ensure(m("/run"), 0755), - New(t.Context(), message.New(nil), 150). - ChangeHosts("chronos"). - Ensure(m("/run"), 0755), - true}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - if tc.sys.Equal(tc.v) != tc.want { - t.Errorf("Equal: %v, want %v", !tc.want, tc.want) - } - }) - } -} - -func TestCommitRevert(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - f func(sys *I) - ec hst.Enablement - - commit []stub.Call - wantErrCommit error - - revert []stub.Call - wantErrRevert error - }{ - {"apply xhost partial mkdir", func(sys *I) { - sys. - Ephemeral(Process, m("/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"), 0711). - ChangeHosts("chronos") - }, 0xff, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil), - call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, nil), - call("verbosef", stub.ExpectArgs{"inserting entry %s to X11", []any{xhostOp("chronos")}}, nil, nil), - call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeInsert), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, stub.UniqueError(2)), - call("verbosef", stub.ExpectArgs{"commit faulted after %d ops, rolling back partial commit", []any{1}}, nil, nil), - call("verbose", stub.ExpectArgs{[]any{"destroying ephemeral directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil), - call("remove", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, stub.UniqueError(3)), - call("println", stub.ExpectArgs{[]any{"cannot revert mkdir: unique error 3 injected by the test suite"}}, nil, nil), - }, &OpError{Op: "xhost", Err: stub.UniqueError(2)}, nil, nil}, - - {"apply xhost", func(sys *I) { - sys. - Ephemeral(Process, m("/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"), 0711). - ChangeHosts("chronos") - }, 0xff, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil), - call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, nil), - call("verbosef", stub.ExpectArgs{"inserting entry %s to X11", []any{xhostOp("chronos")}}, nil, nil), - call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeInsert), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, stub.UniqueError(2)), - call("verbosef", stub.ExpectArgs{"commit faulted after %d ops, rolling back partial commit", []any{1}}, nil, nil), - call("verbose", stub.ExpectArgs{[]any{"destroying ephemeral directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil), - call("remove", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, nil), - }, &OpError{Op: "xhost", Err: stub.UniqueError(2)}, nil, nil}, - - {"revert multi", func(sys *I) { - sys. - Ephemeral(Process, m("/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"), 0711). - ChangeHosts("chronos") - }, 0xff, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil), - call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, nil), - call("verbosef", stub.ExpectArgs{"inserting entry %s to X11", []any{xhostOp("chronos")}}, nil, nil), - call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeInsert), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, nil), - }, nil, []stub.Call{ - call("verbosef", stub.ExpectArgs{"deleting entry %s from X11", []any{xhostOp("chronos")}}, nil, nil), - call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeDelete), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, stub.UniqueError(1)), - call("verbose", stub.ExpectArgs{[]any{"destroying ephemeral directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil), - call("remove", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, stub.UniqueError(0)), - }, errors.Join( - &OpError{Op: "xhost", Err: stub.UniqueError(1), Revert: true}, - &OpError{Op: "mkdir", Err: stub.UniqueError(0), Revert: true})}, - - {"success", func(sys *I) { - sys. - Ephemeral(Process, m("/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"), 0711). - ChangeHosts("chronos") - }, 0xff, []stub.Call{ - call("verbose", stub.ExpectArgs{[]any{"ensuring directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil), - call("mkdir", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", os.FileMode(0711)}, nil, nil), - call("verbosef", stub.ExpectArgs{"inserting entry %s to X11", []any{xhostOp("chronos")}}, nil, nil), - call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeInsert), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, nil), - }, nil, []stub.Call{ - call("verbosef", stub.ExpectArgs{"deleting entry %s from X11", []any{xhostOp("chronos")}}, nil, nil), - call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeDelete), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, nil), - call("verbose", stub.ExpectArgs{[]any{"destroying ephemeral directory", &mkdirOp{Process, "/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9", 0711, true}}}, nil, nil), - call("remove", stub.ExpectArgs{"/tmp/hakurei.0/f2f3bcd492d0266438fa9bf164fe90d9"}, nil, nil), - }, nil}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - var ec *Criteria - if tc.ec != 0xff { - ec = (*Criteria)(&tc.ec) - } - - sys, s := InternalNew(t, stub.Expect{Calls: slices.Concat(tc.commit, []stub.Call{{Name: stub.CallSeparator}}, tc.revert)}, 0xbad) - defer stub.HandleExit(t) - tc.f(sys) - errCommit := sys.Commit() - s.Expects(stub.CallSeparator) - if !reflect.DeepEqual(errCommit, tc.wantErrCommit) { - t.Errorf("Commit: error = %v, want %v", errCommit, tc.wantErrCommit) - } - if errCommit != nil { - goto out - } - - if err := sys.Revert(ec); !reflect.DeepEqual(err, tc.wantErrRevert) { - t.Errorf("Revert: error = %v, want %v", err, tc.wantErrRevert) - } - - out: - s.VisitIncomplete(func(s *stub.Stub[syscallDispatcher]) { - count := s.Pos() - 1 // separator - if count < len(tc.commit) { - t.Errorf("Commit: %d calls, want %d", count, len(tc.commit)) - } else { - t.Errorf("Revert: %d calls, want %d", count-len(tc.commit), len(tc.revert)) - } - }) - }) - } - - t.Run("panic", func(t *testing.T) { - t.Run("committed", func(t *testing.T) { - defer func() { - want := "attempting to commit twice" - if r := recover(); r != want { - t.Errorf("Commit: panic = %v, want %v", r, want) - } - }() - _ = (&I{committed: true}).Commit() - }) - - t.Run("reverted", func(t *testing.T) { - defer func() { - want := "attempting to revert twice" - if r := recover(); r != want { - t.Errorf("Revert: panic = %v, want %v", r, want) - } - }() - _ = (&I{reverted: true}).Revert(nil) - }) - }) -} - -func TestNop(t *testing.T) { - // these do nothing - new(noCopy).Unlock() - new(noCopy).Lock() -} - -func m(pathname string) *check.Absolute { return check.MustAbs(pathname) } diff --git a/system/wayland.go b/system/wayland.go deleted file mode 100644 index bb144d8c..00000000 --- a/system/wayland.go +++ /dev/null @@ -1,90 +0,0 @@ -package system - -import ( - "errors" - "fmt" - "os" - - "hakurei.app/container/check" - "hakurei.app/hst" - "hakurei.app/system/acl" - "hakurei.app/system/wayland" -) - -type waylandConn interface { - Attach(p string) (err error) - Bind(pathname, appID, instanceID string) (*os.File, error) - Close() error -} - -// Wayland maintains a wayland socket with security-context-v1 attached via [wayland]. -// The socket stops accepting connections once the pipe referred to by sync is closed. -// The socket is pathname only and is destroyed on revert. -func (sys *I) Wayland(dst, src *check.Absolute, appID, instanceID string) *I { - sys.ops = append(sys.ops, &waylandOp{nil, - dst.String(), src.String(), - appID, instanceID, - new(wayland.Conn)}) - return sys -} - -// waylandOp implements [I.Wayland]. -type waylandOp struct { - sync *os.File - dst, src string - appID, instanceID string - - conn waylandConn -} - -func (w *waylandOp) Type() hst.Enablement { return Process } - -func (w *waylandOp) apply(sys *I) error { - if err := w.conn.Attach(w.src); err != nil { - return newOpError("wayland", err, false) - } else { - sys.msg.Verbosef("wayland attached on %q", w.src) - } - - if sp, err := w.conn.Bind(w.dst, w.appID, w.instanceID); err != nil { - return newOpError("wayland", err, false) - } else { - w.sync = sp - sys.msg.Verbosef("wayland listening on %q", w.dst) - if err = sys.chmod(w.dst, 0); err != nil { - return newOpError("wayland", err, false) - } - return newOpError("wayland", sys.aclUpdate(w.dst, sys.uid, acl.Read, acl.Write, acl.Execute), false) - } -} - -func (w *waylandOp) revert(sys *I, _ *Criteria) error { - var ( - hangupErr error - closeErr error - removeErr error - ) - - sys.msg.Verbosef("detaching from wayland on %q", w.src) - if w.sync != nil { - hangupErr = w.sync.Close() - } - closeErr = w.conn.Close() - - sys.msg.Verbosef("removing wayland socket on %q", w.dst) - if err := sys.remove(w.dst); err != nil && !errors.Is(err, os.ErrNotExist) { - removeErr = err - } - - return newOpError("wayland", errors.Join(hangupErr, closeErr, removeErr), true) -} - -func (w *waylandOp) Is(o Op) bool { - target, ok := o.(*waylandOp) - return ok && w != nil && target != nil && - w.dst == target.dst && w.src == target.src && - w.appID == target.appID && w.instanceID == target.instanceID -} - -func (w *waylandOp) Path() string { return w.dst } -func (w *waylandOp) String() string { return fmt.Sprintf("wayland socket at %q", w.dst) } diff --git a/system/wayland/conn.go b/system/wayland/conn.go deleted file mode 100644 index 86e0a288..00000000 --- a/system/wayland/conn.go +++ /dev/null @@ -1,123 +0,0 @@ -// Package wayland implements Wayland security_context_v1 protocol. -package wayland - -import ( - "errors" - "net" - "os" - "runtime" - "sync" - "syscall" -) - -// Conn represents a connection to the wayland display server. -type Conn struct { - conn *net.UnixConn - - done chan struct{} - doneOnce sync.Once - - mu sync.Mutex -} - -// Attach connects Conn to a wayland socket. -func (c *Conn) Attach(p string) (err error) { - c.mu.Lock() - defer c.mu.Unlock() - - if c.conn != nil { - return errors.New("socket already attached") - } - - c.conn, err = net.DialUnix("unix", nil, &net.UnixAddr{Name: p, Net: "unix"}) - return -} - -// Close releases resources and closes the connection to the wayland compositor. -func (c *Conn) Close() error { - c.mu.Lock() - defer c.mu.Unlock() - - if c.done == nil { - return errors.New("no socket bound") - } - - c.doneOnce.Do(func() { - c.done <- struct{}{} - <-c.done - }) - - // closed by wayland - runtime.SetFinalizer(c.conn, nil) - return nil -} - -// Bind binds the new socket to pathname. -func (c *Conn) Bind(pathname, appID, instanceID string) (*os.File, error) { - c.mu.Lock() - defer c.mu.Unlock() - - if c.conn == nil { - return nil, errors.New("socket not attached") - } - if c.done != nil { - return nil, errors.New("socket already bound") - } - - if rc, err := c.conn.SyscallConn(); err != nil { - // unreachable - return nil, err - } else { - c.done = make(chan struct{}) - return bindRawConn(c.done, rc, pathname, appID, instanceID) - } -} - -func bindRawConn(done chan struct{}, rc syscall.RawConn, p, appID, instanceID string) (*os.File, error) { - var syncPipe [2]*os.File - - if r, w, err := os.Pipe(); err != nil { - return nil, err - } else { - syncPipe[0] = r - syncPipe[1] = w - } - - setupDone := make(chan error, 1) // does not block with c.done - - go func() { - if err := rc.Control(func(fd uintptr) { - // prevent runtime from closing the read end of sync fd - runtime.SetFinalizer(syncPipe[0], nil) - - // allow the Bind method to return after setup - setupDone <- bind(fd, p, appID, instanceID, syncPipe[0].Fd()) - close(setupDone) - - // keep socket alive until done is requested - <-done - runtime.KeepAlive(syncPipe[1]) - }); err != nil { - setupDone <- err - } - - // notify Close that rc.Control has returned - close(done) - }() - - // return write end of the pipe - return syncPipe[1], <-setupDone -} - -func bind(fd uintptr, p, appID, instanceID string, syncFd uintptr) error { - // ensure p is available - if f, err := os.Create(p); err != nil { - return err - } else if err = f.Close(); err != nil { - return err - } else if err = os.Remove(p); err != nil { - return err - } - - return bindWaylandFd(p, fd, appID, instanceID, syncFd) -} diff --git a/system/wayland/consts.go b/system/wayland/deprecated.go index 672e6392..bf364d9a 100644 --- a/system/wayland/consts.go +++ b/system/wayland/deprecated.go @@ -1,5 +1,17 @@ +// Package wayland exposes the internal/system/wayland package. +// +// Deprecated: This package will be removed in 0.4. package wayland +import ( + _ "unsafe" // for go:linkname + + "hakurei.app/internal/system/wayland" +) + +// Conn represents a connection to the wayland display server. +type Conn = wayland.Conn + const ( // WaylandDisplay contains the name of the server socket // (https://gitlab.freedesktop.org/wayland/wayland/-/blob/1.23.1/src/wayland-client.c#L1147) @@ -7,9 +19,9 @@ const ( // (https://gitlab.freedesktop.org/wayland/wayland/-/blob/1.23.1/src/wayland-client.c#L1171) // or used as-is if absolute // (https://gitlab.freedesktop.org/wayland/wayland/-/blob/1.23.1/src/wayland-client.c#L1176). - WaylandDisplay = "WAYLAND_DISPLAY" + WaylandDisplay = wayland.Display // FallbackName is used as the wayland socket name if WAYLAND_DISPLAY is unset // (https://gitlab.freedesktop.org/wayland/wayland/-/blob/1.23.1/src/wayland-client.c#L1149). - FallbackName = "wayland-0" + FallbackName = wayland.FallbackName ) diff --git a/system/wayland/security-context-v1-protocol.c b/system/wayland/security-context-v1-protocol.c deleted file mode 100644 index 5b15b1b4..00000000 --- a/system/wayland/security-context-v1-protocol.c +++ /dev/null @@ -1,74 +0,0 @@ -/* Generated by wayland-scanner 1.23.1 */ - -/* - * Copyright © 2021 Simon Ser - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice (including the next - * paragraph) shall be included in all copies or substantial portions of the - * Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - */ - -#include <stdbool.h> -#include <stdlib.h> -#include <stdint.h> -#include "wayland-util.h" - -#ifndef __has_attribute -# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ -#endif - -#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) -#define WL_PRIVATE __attribute__ ((visibility("hidden"))) -#else -#define WL_PRIVATE -#endif - -extern const struct wl_interface wp_security_context_v1_interface; - -static const struct wl_interface *security_context_v1_types[] = { - NULL, - &wp_security_context_v1_interface, - NULL, - NULL, -}; - -static const struct wl_message wp_security_context_manager_v1_requests[] = { - { "destroy", "", security_context_v1_types + 0 }, - { "create_listener", "nhh", security_context_v1_types + 1 }, -}; - -WL_PRIVATE const struct wl_interface wp_security_context_manager_v1_interface = { - "wp_security_context_manager_v1", 1, - 2, wp_security_context_manager_v1_requests, - 0, NULL, -}; - -static const struct wl_message wp_security_context_v1_requests[] = { - { "destroy", "", security_context_v1_types + 0 }, - { "set_sandbox_engine", "s", security_context_v1_types + 0 }, - { "set_app_id", "s", security_context_v1_types + 0 }, - { "set_instance_id", "s", security_context_v1_types + 0 }, - { "commit", "", security_context_v1_types + 0 }, -}; - -WL_PRIVATE const struct wl_interface wp_security_context_v1_interface = { - "wp_security_context_v1", 1, - 5, wp_security_context_v1_requests, - 0, NULL, -}; - diff --git a/system/wayland/security-context-v1-protocol.h b/system/wayland/security-context-v1-protocol.h deleted file mode 100644 index f973619e..00000000 --- a/system/wayland/security-context-v1-protocol.h +++ /dev/null @@ -1,392 +0,0 @@ -/* Generated by wayland-scanner 1.23.1 */ - -#ifndef SECURITY_CONTEXT_V1_CLIENT_PROTOCOL_H -#define SECURITY_CONTEXT_V1_CLIENT_PROTOCOL_H - -#include <stdint.h> -#include <stddef.h> -#include "wayland-client.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @page page_security_context_v1 The security_context_v1 protocol - * @section page_ifaces_security_context_v1 Interfaces - * - @subpage page_iface_wp_security_context_manager_v1 - client security context manager - * - @subpage page_iface_wp_security_context_v1 - client security context - * @section page_copyright_security_context_v1 Copyright - * <pre> - * - * Copyright © 2021 Simon Ser - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice (including the next - * paragraph) shall be included in all copies or substantial portions of the - * Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - * </pre> - */ -struct wp_security_context_manager_v1; -struct wp_security_context_v1; - -#ifndef WP_SECURITY_CONTEXT_MANAGER_V1_INTERFACE -#define WP_SECURITY_CONTEXT_MANAGER_V1_INTERFACE -/** - * @page page_iface_wp_security_context_manager_v1 wp_security_context_manager_v1 - * @section page_iface_wp_security_context_manager_v1_desc Description - * - * This interface allows a client to register a new Wayland connection to - * the compositor and attach a security context to it. - * - * This is intended to be used by sandboxes. Sandbox engines attach a - * security context to all connections coming from inside the sandbox. The - * compositor can then restrict the features that the sandboxed connections - * can use. - * - * Compositors should forbid nesting multiple security contexts by not - * exposing wp_security_context_manager_v1 global to clients with a security - * context attached, or by sending the nested protocol error. Nested - * security contexts are dangerous because they can potentially allow - * privilege escalation of a sandboxed client. - * - * Warning! The protocol described in this file is currently in the testing - * phase. Backward compatible changes may be added together with the - * corresponding interface version bump. Backward incompatible changes can - * only be done by creating a new major version of the extension. - * @section page_iface_wp_security_context_manager_v1_api API - * See @ref iface_wp_security_context_manager_v1. - */ -/** - * @defgroup iface_wp_security_context_manager_v1 The wp_security_context_manager_v1 interface - * - * This interface allows a client to register a new Wayland connection to - * the compositor and attach a security context to it. - * - * This is intended to be used by sandboxes. Sandbox engines attach a - * security context to all connections coming from inside the sandbox. The - * compositor can then restrict the features that the sandboxed connections - * can use. - * - * Compositors should forbid nesting multiple security contexts by not - * exposing wp_security_context_manager_v1 global to clients with a security - * context attached, or by sending the nested protocol error. Nested - * security contexts are dangerous because they can potentially allow - * privilege escalation of a sandboxed client. - * - * Warning! The protocol described in this file is currently in the testing - * phase. Backward compatible changes may be added together with the - * corresponding interface version bump. Backward incompatible changes can - * only be done by creating a new major version of the extension. - */ -extern const struct wl_interface wp_security_context_manager_v1_interface; -#endif -#ifndef WP_SECURITY_CONTEXT_V1_INTERFACE -#define WP_SECURITY_CONTEXT_V1_INTERFACE -/** - * @page page_iface_wp_security_context_v1 wp_security_context_v1 - * @section page_iface_wp_security_context_v1_desc Description - * - * The security context allows a client to register a new client and attach - * security context metadata to the connections. - * - * When both are set, the combination of the application ID and the sandbox - * engine must uniquely identify an application. The same application ID - * will be used across instances (e.g. if the application is restarted, or - * if the application is started multiple times). - * - * When both are set, the combination of the instance ID and the sandbox - * engine must uniquely identify a running instance of an application. - * @section page_iface_wp_security_context_v1_api API - * See @ref iface_wp_security_context_v1. - */ -/** - * @defgroup iface_wp_security_context_v1 The wp_security_context_v1 interface - * - * The security context allows a client to register a new client and attach - * security context metadata to the connections. - * - * When both are set, the combination of the application ID and the sandbox - * engine must uniquely identify an application. The same application ID - * will be used across instances (e.g. if the application is restarted, or - * if the application is started multiple times). - * - * When both are set, the combination of the instance ID and the sandbox - * engine must uniquely identify a running instance of an application. - */ -extern const struct wl_interface wp_security_context_v1_interface; -#endif - -#ifndef WP_SECURITY_CONTEXT_MANAGER_V1_ERROR_ENUM -#define WP_SECURITY_CONTEXT_MANAGER_V1_ERROR_ENUM -enum wp_security_context_manager_v1_error { - /** - * listening socket FD is invalid - */ - WP_SECURITY_CONTEXT_MANAGER_V1_ERROR_INVALID_LISTEN_FD = 1, - /** - * nested security contexts are forbidden - */ - WP_SECURITY_CONTEXT_MANAGER_V1_ERROR_NESTED = 2, -}; -#endif /* WP_SECURITY_CONTEXT_MANAGER_V1_ERROR_ENUM */ - -#define WP_SECURITY_CONTEXT_MANAGER_V1_DESTROY 0 -#define WP_SECURITY_CONTEXT_MANAGER_V1_CREATE_LISTENER 1 - - -/** - * @ingroup iface_wp_security_context_manager_v1 - */ -#define WP_SECURITY_CONTEXT_MANAGER_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wp_security_context_manager_v1 - */ -#define WP_SECURITY_CONTEXT_MANAGER_V1_CREATE_LISTENER_SINCE_VERSION 1 - -/** @ingroup iface_wp_security_context_manager_v1 */ -static inline void -wp_security_context_manager_v1_set_user_data(struct wp_security_context_manager_v1 *wp_security_context_manager_v1, void *user_data) -{ - wl_proxy_set_user_data((struct wl_proxy *) wp_security_context_manager_v1, user_data); -} - -/** @ingroup iface_wp_security_context_manager_v1 */ -static inline void * -wp_security_context_manager_v1_get_user_data(struct wp_security_context_manager_v1 *wp_security_context_manager_v1) -{ - return wl_proxy_get_user_data((struct wl_proxy *) wp_security_context_manager_v1); -} - -static inline uint32_t -wp_security_context_manager_v1_get_version(struct wp_security_context_manager_v1 *wp_security_context_manager_v1) -{ - return wl_proxy_get_version((struct wl_proxy *) wp_security_context_manager_v1); -} - -/** - * @ingroup iface_wp_security_context_manager_v1 - * - * Destroy the manager. This doesn't destroy objects created with the - * manager. - */ -static inline void -wp_security_context_manager_v1_destroy(struct wp_security_context_manager_v1 *wp_security_context_manager_v1) -{ - wl_proxy_marshal_flags((struct wl_proxy *) wp_security_context_manager_v1, - WP_SECURITY_CONTEXT_MANAGER_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) wp_security_context_manager_v1), WL_MARSHAL_FLAG_DESTROY); -} - -/** - * @ingroup iface_wp_security_context_manager_v1 - * - * Creates a new security context with a socket listening FD. - * - * The compositor will accept new client connections on listen_fd. - * listen_fd must be ready to accept new connections when this request is - * sent by the client. In other words, the client must call bind(2) and - * listen(2) before sending the FD. - * - * close_fd is a FD that will signal hangup when the compositor should stop - * accepting new connections on listen_fd. - * - * The compositor must continue to accept connections on listen_fd when - * the Wayland client which created the security context disconnects. - * - * After sending this request, closing listen_fd and close_fd remains the - * only valid operation on them. - */ -static inline struct wp_security_context_v1 * -wp_security_context_manager_v1_create_listener(struct wp_security_context_manager_v1 *wp_security_context_manager_v1, int32_t listen_fd, int32_t close_fd) -{ - struct wl_proxy *id; - - id = wl_proxy_marshal_flags((struct wl_proxy *) wp_security_context_manager_v1, - WP_SECURITY_CONTEXT_MANAGER_V1_CREATE_LISTENER, &wp_security_context_v1_interface, wl_proxy_get_version((struct wl_proxy *) wp_security_context_manager_v1), 0, NULL, listen_fd, close_fd); - - return (struct wp_security_context_v1 *) id; -} - -#ifndef WP_SECURITY_CONTEXT_V1_ERROR_ENUM -#define WP_SECURITY_CONTEXT_V1_ERROR_ENUM -enum wp_security_context_v1_error { - /** - * security context has already been committed - */ - WP_SECURITY_CONTEXT_V1_ERROR_ALREADY_USED = 1, - /** - * metadata has already been set - */ - WP_SECURITY_CONTEXT_V1_ERROR_ALREADY_SET = 2, - /** - * metadata is invalid - */ - WP_SECURITY_CONTEXT_V1_ERROR_INVALID_METADATA = 3, -}; -#endif /* WP_SECURITY_CONTEXT_V1_ERROR_ENUM */ - -#define WP_SECURITY_CONTEXT_V1_DESTROY 0 -#define WP_SECURITY_CONTEXT_V1_SET_SANDBOX_ENGINE 1 -#define WP_SECURITY_CONTEXT_V1_SET_APP_ID 2 -#define WP_SECURITY_CONTEXT_V1_SET_INSTANCE_ID 3 -#define WP_SECURITY_CONTEXT_V1_COMMIT 4 - - -/** - * @ingroup iface_wp_security_context_v1 - */ -#define WP_SECURITY_CONTEXT_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_wp_security_context_v1 - */ -#define WP_SECURITY_CONTEXT_V1_SET_SANDBOX_ENGINE_SINCE_VERSION 1 -/** - * @ingroup iface_wp_security_context_v1 - */ -#define WP_SECURITY_CONTEXT_V1_SET_APP_ID_SINCE_VERSION 1 -/** - * @ingroup iface_wp_security_context_v1 - */ -#define WP_SECURITY_CONTEXT_V1_SET_INSTANCE_ID_SINCE_VERSION 1 -/** - * @ingroup iface_wp_security_context_v1 - */ -#define WP_SECURITY_CONTEXT_V1_COMMIT_SINCE_VERSION 1 - -/** @ingroup iface_wp_security_context_v1 */ -static inline void -wp_security_context_v1_set_user_data(struct wp_security_context_v1 *wp_security_context_v1, void *user_data) -{ - wl_proxy_set_user_data((struct wl_proxy *) wp_security_context_v1, user_data); -} - -/** @ingroup iface_wp_security_context_v1 */ -static inline void * -wp_security_context_v1_get_user_data(struct wp_security_context_v1 *wp_security_context_v1) -{ - return wl_proxy_get_user_data((struct wl_proxy *) wp_security_context_v1); -} - -static inline uint32_t -wp_security_context_v1_get_version(struct wp_security_context_v1 *wp_security_context_v1) -{ - return wl_proxy_get_version((struct wl_proxy *) wp_security_context_v1); -} - -/** - * @ingroup iface_wp_security_context_v1 - * - * Destroy the security context object. - */ -static inline void -wp_security_context_v1_destroy(struct wp_security_context_v1 *wp_security_context_v1) -{ - wl_proxy_marshal_flags((struct wl_proxy *) wp_security_context_v1, - WP_SECURITY_CONTEXT_V1_DESTROY, NULL, wl_proxy_get_version((struct wl_proxy *) wp_security_context_v1), WL_MARSHAL_FLAG_DESTROY); -} - -/** - * @ingroup iface_wp_security_context_v1 - * - * Attach a unique sandbox engine name to the security context. The name - * should follow the reverse-DNS style (e.g. "org.flatpak"). - * - * A list of well-known engines is maintained at: - * https://gitlab.freedesktop.org/wayland/wayland-protocols/-/blob/main/staging/security-context/engines.md - * - * It is a protocol error to call this request twice. The already_set - * error is sent in this case. - */ -static inline void -wp_security_context_v1_set_sandbox_engine(struct wp_security_context_v1 *wp_security_context_v1, const char *name) -{ - wl_proxy_marshal_flags((struct wl_proxy *) wp_security_context_v1, - WP_SECURITY_CONTEXT_V1_SET_SANDBOX_ENGINE, NULL, wl_proxy_get_version((struct wl_proxy *) wp_security_context_v1), 0, name); -} - -/** - * @ingroup iface_wp_security_context_v1 - * - * Attach an application ID to the security context. - * - * The application ID is an opaque, sandbox-specific identifier for an - * application. See the well-known engines document for more details: - * https://gitlab.freedesktop.org/wayland/wayland-protocols/-/blob/main/staging/security-context/engines.md - * - * The compositor may use the application ID to group clients belonging to - * the same security context application. - * - * Whether this request is optional or not depends on the sandbox engine used. - * - * It is a protocol error to call this request twice. The already_set - * error is sent in this case. - */ -static inline void -wp_security_context_v1_set_app_id(struct wp_security_context_v1 *wp_security_context_v1, const char *app_id) -{ - wl_proxy_marshal_flags((struct wl_proxy *) wp_security_context_v1, - WP_SECURITY_CONTEXT_V1_SET_APP_ID, NULL, wl_proxy_get_version((struct wl_proxy *) wp_security_context_v1), 0, app_id); -} - -/** - * @ingroup iface_wp_security_context_v1 - * - * Attach an instance ID to the security context. - * - * The instance ID is an opaque, sandbox-specific identifier for a running - * instance of an application. See the well-known engines document for - * more details: - * https://gitlab.freedesktop.org/wayland/wayland-protocols/-/blob/main/staging/security-context/engines.md - * - * Whether this request is optional or not depends on the sandbox engine used. - * - * It is a protocol error to call this request twice. The already_set - * error is sent in this case. - */ -static inline void -wp_security_context_v1_set_instance_id(struct wp_security_context_v1 *wp_security_context_v1, const char *instance_id) -{ - wl_proxy_marshal_flags((struct wl_proxy *) wp_security_context_v1, - WP_SECURITY_CONTEXT_V1_SET_INSTANCE_ID, NULL, wl_proxy_get_version((struct wl_proxy *) wp_security_context_v1), 0, instance_id); -} - -/** - * @ingroup iface_wp_security_context_v1 - * - * Atomically register the new client and attach the security context - * metadata. - * - * If the provided metadata is inconsistent or does not match with out of - * band metadata (see - * https://gitlab.freedesktop.org/wayland/wayland-protocols/-/blob/main/staging/security-context/engines.md), - * the invalid_metadata error may be sent eventually. - * - * It's a protocol error to send any request other than "destroy" after - * this request. In this case, the already_used error is sent. - */ -static inline void -wp_security_context_v1_commit(struct wp_security_context_v1 *wp_security_context_v1) -{ - wl_proxy_marshal_flags((struct wl_proxy *) wp_security_context_v1, - WP_SECURITY_CONTEXT_V1_COMMIT, NULL, wl_proxy_get_version((struct wl_proxy *) wp_security_context_v1), 0); -} - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/system/wayland/wayland-client-helper.c b/system/wayland/wayland-client-helper.c deleted file mode 100644 index 5a2c9a01..00000000 --- a/system/wayland/wayland-client-helper.c +++ /dev/null @@ -1,96 +0,0 @@ -#include "wayland-client-helper.h" -#include <stdio.h> -#include <stdlib.h> -#include <string.h> -#include <sys/socket.h> -#include <sys/un.h> -#include <unistd.h> - -#include "security-context-v1-protocol.h" -#include <wayland-client.h> - -static void registry_handle_global(void *data, struct wl_registry *registry, - uint32_t name, const char *interface, - uint32_t version) { - struct wp_security_context_manager_v1 **out = data; - - if (strcmp(interface, wp_security_context_manager_v1_interface.name) == 0) - *out = wl_registry_bind(registry, name, - &wp_security_context_manager_v1_interface, 1); -} - -static void registry_handle_global_remove(void *data, - struct wl_registry *registry, - uint32_t name) {} /* no-op */ - -static const struct wl_registry_listener registry_listener = { - .global = registry_handle_global, - .global_remove = registry_handle_global_remove, -}; - -int32_t hakurei_bind_wayland_fd(char *socket_path, int fd, const char *app_id, - const char *instance_id, int sync_fd) { - int32_t res = 0; /* refer to resErr for corresponding Go error */ - - struct wl_display *display; - display = wl_display_connect_to_fd(fd); - if (!display) { - res = 1; - goto out; - }; - - struct wl_registry *registry; - registry = wl_display_get_registry(display); - - struct wp_security_context_manager_v1 *security_context_manager = NULL; - wl_registry_add_listener(registry, ®istry_listener, - &security_context_manager); - int ret; - ret = wl_display_roundtrip(display); - wl_registry_destroy(registry); - if (ret < 0) - goto out; - - if (!security_context_manager) { - res = 2; - goto out; - } - - int listen_fd = -1; - listen_fd = socket(AF_UNIX, SOCK_STREAM, 0); - if (listen_fd < 0) - goto out; - - struct sockaddr_un sockaddr = {0}; - sockaddr.sun_family = AF_UNIX; - snprintf(sockaddr.sun_path, sizeof(sockaddr.sun_path), "%s", socket_path); - if (bind(listen_fd, (struct sockaddr *)&sockaddr, sizeof(sockaddr)) != 0) - goto out; - - if (listen(listen_fd, 0) != 0) - goto out; - - struct wp_security_context_v1 *security_context; - security_context = wp_security_context_manager_v1_create_listener( - security_context_manager, listen_fd, sync_fd); - wp_security_context_v1_set_sandbox_engine(security_context, "app.hakurei"); - wp_security_context_v1_set_app_id(security_context, app_id); - wp_security_context_v1_set_instance_id(security_context, instance_id); - wp_security_context_v1_commit(security_context); - wp_security_context_v1_destroy(security_context); - if (wl_display_roundtrip(display) < 0) - goto out; - -out: - if (listen_fd >= 0) - close(listen_fd); - if (security_context_manager) - wp_security_context_manager_v1_destroy(security_context_manager); - if (display) - wl_display_disconnect(display); - - free((void *)socket_path); - free((void *)app_id); - free((void *)instance_id); - return res; -} diff --git a/system/wayland/wayland-client-helper.h b/system/wayland/wayland-client-helper.h deleted file mode 100644 index eae8936d..00000000 --- a/system/wayland/wayland-client-helper.h +++ /dev/null @@ -1,4 +0,0 @@ -#include <stdint.h> - -int32_t hakurei_bind_wayland_fd(char *socket_path, int fd, const char *app_id, - const char *instance_id, int sync_fd);
\ No newline at end of file diff --git a/system/wayland/wayland.go b/system/wayland/wayland.go deleted file mode 100644 index 2aa1fe15..00000000 --- a/system/wayland/wayland.go +++ /dev/null @@ -1,36 +0,0 @@ -package wayland - -//go:generate sh -c "wayland-scanner client-header `pkg-config --variable=datarootdir wayland-protocols`/wayland-protocols/staging/security-context/security-context-v1.xml security-context-v1-protocol.h" -//go:generate sh -c "wayland-scanner private-code `pkg-config --variable=datarootdir wayland-protocols`/wayland-protocols/staging/security-context/security-context-v1.xml security-context-v1-protocol.c" - -/* -#cgo linux pkg-config: --static wayland-client -#cgo freebsd openbsd LDFLAGS: -lwayland-client - -#include "wayland-client-helper.h" -*/ -import "C" -import ( - "errors" - "strings" -) - -var ( - ErrContainsNull = errors.New("string contains null character") -) - -var resErr = [...]error{ - 0: nil, - 1: errors.New("wl_display_connect_to_fd() failed"), - 2: errors.New("wp_security_context_v1 not available"), -} - -func bindWaylandFd(socketPath string, fd uintptr, appID, instanceID string, syncFd uintptr) error { - if hasNull(appID) || hasNull(instanceID) { - return ErrContainsNull - } - res := C.hakurei_bind_wayland_fd(C.CString(socketPath), C.int(fd), C.CString(appID), C.CString(instanceID), C.int(syncFd)) - return resErr[int32(res)] -} - -func hasNull(s string) bool { return strings.IndexByte(s, '\x00') > -1 } diff --git a/system/wayland_test.go b/system/wayland_test.go deleted file mode 100644 index 6805b0c2..00000000 --- a/system/wayland_test.go +++ /dev/null @@ -1,303 +0,0 @@ -package system - -import ( - "errors" - "os" - "testing" - - "hakurei.app/container/stub" - "hakurei.app/system/acl" - "hakurei.app/system/wayland" -) - -type stubWaylandConn struct { - t *testing.T - - wantAttach string - attachErr error - attached bool - - wantBind [3]string - bindErr error - bound bool - - closeErr error - closed bool -} - -func (conn *stubWaylandConn) Attach(p string) (err error) { - conn.t.Helper() - - if conn.attached { - conn.t.Fatal("Attach called twice") - } - conn.attached = true - - err = conn.attachErr - if p != conn.wantAttach { - conn.t.Errorf("Attach: p = %q, want %q", p, conn.wantAttach) - err = stub.ErrCheck - } - return -} - -func (conn *stubWaylandConn) Bind(pathname, appID, instanceID string) (*os.File, error) { - conn.t.Helper() - - if !conn.attached { - conn.t.Fatal("Bind called before Attach") - } - - if conn.bound { - conn.t.Fatal("Bind called twice") - } - conn.bound = true - - if pathname != conn.wantBind[0] { - conn.t.Errorf("Attach: pathname = %q, want %q", pathname, conn.wantBind[0]) - return nil, stub.ErrCheck - } - if appID != conn.wantBind[1] { - conn.t.Errorf("Attach: appID = %q, want %q", appID, conn.wantBind[1]) - return nil, stub.ErrCheck - } - if instanceID != conn.wantBind[2] { - conn.t.Errorf("Attach: instanceID = %q, want %q", instanceID, conn.wantBind[2]) - return nil, stub.ErrCheck - } - return nil, conn.bindErr -} - -func (conn *stubWaylandConn) Close() error { - conn.t.Helper() - - if !conn.attached { - conn.t.Fatal("Close called before Attach") - } - if !conn.bound { - conn.t.Fatal("Close called before Bind") - } - - if conn.closed { - conn.t.Fatal("Close called twice") - } - conn.closed = true - return conn.closeErr -} - -func TestWaylandOp(t *testing.T) { - t.Parallel() - - checkOpBehaviour(t, []opBehaviourTestCase{ - {"attach", 0xbeef, 0xff, &waylandOp{nil, - "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", - "/run/user/1971/wayland-0", - "org.chromium.Chromium", - "ebf083d1b175911782d413369b64ce7c", - &stubWaylandConn{t: t, wantAttach: "/run/user/1971/wayland-0", wantBind: [3]string{ - "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", - "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c"}, - attachErr: stub.UniqueError(5)}, - }, nil, &OpError{Op: "wayland", Err: stub.UniqueError(5)}, nil, nil}, - - {"bind", 0xbeef, 0xff, &waylandOp{nil, - "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", - "/run/user/1971/wayland-0", - "org.chromium.Chromium", - "ebf083d1b175911782d413369b64ce7c", - &stubWaylandConn{t: t, wantAttach: "/run/user/1971/wayland-0", wantBind: [3]string{ - "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", - "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c"}, - bindErr: stub.UniqueError(4)}, - }, []stub.Call{ - call("verbosef", stub.ExpectArgs{"wayland attached on %q", []any{"/run/user/1971/wayland-0"}}, nil, nil), - }, &OpError{Op: "wayland", Err: stub.UniqueError(4)}, nil, nil}, - - {"chmod", 0xbeef, 0xff, &waylandOp{nil, - "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", - "/run/user/1971/wayland-0", - "org.chromium.Chromium", - "ebf083d1b175911782d413369b64ce7c", - &stubWaylandConn{t: t, wantAttach: "/run/user/1971/wayland-0", wantBind: [3]string{ - "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", - "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c"}}, - }, []stub.Call{ - call("verbosef", stub.ExpectArgs{"wayland attached on %q", []any{"/run/user/1971/wayland-0"}}, nil, nil), - call("verbosef", stub.ExpectArgs{"wayland listening on %q", []any{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"}}, nil, nil), - call("chmod", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", os.FileMode(0)}, nil, stub.UniqueError(3)), - }, &OpError{Op: "wayland", Err: stub.UniqueError(3)}, nil, nil}, - - {"aclUpdate", 0xbeef, 0xff, &waylandOp{nil, - "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", - "/run/user/1971/wayland-0", - "org.chromium.Chromium", - "ebf083d1b175911782d413369b64ce7c", - &stubWaylandConn{t: t, wantAttach: "/run/user/1971/wayland-0", wantBind: [3]string{ - "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", - "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c"}}, - }, []stub.Call{ - call("verbosef", stub.ExpectArgs{"wayland attached on %q", []any{"/run/user/1971/wayland-0"}}, nil, nil), - call("verbosef", stub.ExpectArgs{"wayland listening on %q", []any{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"}}, nil, nil), - call("chmod", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", os.FileMode(0)}, nil, nil), - call("aclUpdate", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, stub.UniqueError(2)), - }, &OpError{Op: "wayland", Err: stub.UniqueError(2)}, nil, nil}, - - {"remove", 0xbeef, 0xff, &waylandOp{nil, - "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", - "/run/user/1971/wayland-0", - "org.chromium.Chromium", - "ebf083d1b175911782d413369b64ce7c", - &stubWaylandConn{t: t, wantAttach: "/run/user/1971/wayland-0", wantBind: [3]string{ - "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", - "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c"}}, - }, []stub.Call{ - call("verbosef", stub.ExpectArgs{"wayland attached on %q", []any{"/run/user/1971/wayland-0"}}, nil, nil), - call("verbosef", stub.ExpectArgs{"wayland listening on %q", []any{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"}}, nil, nil), - call("chmod", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", os.FileMode(0)}, nil, nil), - call("aclUpdate", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil), - }, nil, []stub.Call{ - call("verbosef", stub.ExpectArgs{"detaching from wayland on %q", []any{"/run/user/1971/wayland-0"}}, nil, nil), - call("verbosef", stub.ExpectArgs{"removing wayland socket on %q", []any{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"}}, nil, nil), - call("remove", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"}, nil, stub.UniqueError(1)), - }, &OpError{Op: "wayland", Err: errors.Join(stub.UniqueError(1)), Revert: true}}, - - {"close", 0xbeef, 0xff, &waylandOp{nil, - "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", - "/run/user/1971/wayland-0", - "org.chromium.Chromium", - "ebf083d1b175911782d413369b64ce7c", - &stubWaylandConn{t: t, wantAttach: "/run/user/1971/wayland-0", wantBind: [3]string{ - "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", - "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c"}, - closeErr: stub.UniqueError(0)}, - }, []stub.Call{ - call("verbosef", stub.ExpectArgs{"wayland attached on %q", []any{"/run/user/1971/wayland-0"}}, nil, nil), - call("verbosef", stub.ExpectArgs{"wayland listening on %q", []any{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"}}, nil, nil), - call("chmod", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", os.FileMode(0)}, nil, nil), - call("aclUpdate", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil), - }, nil, []stub.Call{ - call("verbosef", stub.ExpectArgs{"detaching from wayland on %q", []any{"/run/user/1971/wayland-0"}}, nil, nil), - call("verbosef", stub.ExpectArgs{"removing wayland socket on %q", []any{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"}}, nil, nil), - call("remove", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"}, nil, nil), - }, &OpError{Op: "wayland", Err: errors.Join(stub.UniqueError(0)), Revert: true}}, - - {"success", 0xbeef, 0xff, &waylandOp{nil, - "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", - "/run/user/1971/wayland-0", - "org.chromium.Chromium", - "ebf083d1b175911782d413369b64ce7c", - &stubWaylandConn{t: t, wantAttach: "/run/user/1971/wayland-0", wantBind: [3]string{ - "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", - "org.chromium.Chromium", "ebf083d1b175911782d413369b64ce7c"}}, - }, []stub.Call{ - call("verbosef", stub.ExpectArgs{"wayland attached on %q", []any{"/run/user/1971/wayland-0"}}, nil, nil), - call("verbosef", stub.ExpectArgs{"wayland listening on %q", []any{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"}}, nil, nil), - call("chmod", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", os.FileMode(0)}, nil, nil), - call("aclUpdate", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", 0xbeef, []acl.Perm{acl.Read, acl.Write, acl.Execute}}, nil, nil), - }, nil, []stub.Call{ - call("verbosef", stub.ExpectArgs{"detaching from wayland on %q", []any{"/run/user/1971/wayland-0"}}, nil, nil), - call("verbosef", stub.ExpectArgs{"removing wayland socket on %q", []any{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"}}, nil, nil), - call("remove", stub.ExpectArgs{"/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"}, nil, nil), - }, nil}, - }) - - checkOpsBuilder(t, "Wayland", []opsBuilderTestCase{ - {"chromium", 0xcafe, func(_ *testing.T, sys *I) { - sys.Wayland( - m("/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"), - m("/run/user/1971/wayland-0"), - "org.chromium.Chromium", - "ebf083d1b175911782d413369b64ce7c", - ) - }, []Op{&waylandOp{nil, - "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", - "/run/user/1971/wayland-0", - "org.chromium.Chromium", - "ebf083d1b175911782d413369b64ce7c", - new(wayland.Conn), - }}, stub.Expect{}}, - }) - - checkOpIs(t, []opIsTestCase{ - {"dst differs", &waylandOp{nil, - "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7d/wayland", - "/run/user/1971/wayland-0", - "org.chromium.Chromium", - "ebf083d1b175911782d413369b64ce7c", - new(wayland.Conn), - }, &waylandOp{nil, - "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", - "/run/user/1971/wayland-0", - "org.chromium.Chromium", - "ebf083d1b175911782d413369b64ce7c", - new(wayland.Conn), - }, false}, - - {"src differs", &waylandOp{nil, - "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", - "/run/user/1971/wayland-1", - "org.chromium.Chromium", - "ebf083d1b175911782d413369b64ce7c", - new(wayland.Conn), - }, &waylandOp{nil, - "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", - "/run/user/1971/wayland-0", - "org.chromium.Chromium", - "ebf083d1b175911782d413369b64ce7c", - new(wayland.Conn), - }, false}, - - {"appID differs", &waylandOp{nil, - "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", - "/run/user/1971/wayland-0", - "org.chromium", - "ebf083d1b175911782d413369b64ce7c", - new(wayland.Conn), - }, &waylandOp{nil, - "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", - "/run/user/1971/wayland-0", - "org.chromium.Chromium", - "ebf083d1b175911782d413369b64ce7c", - new(wayland.Conn), - }, false}, - - {"instanceID differs", &waylandOp{nil, - "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", - "/run/user/1971/wayland-0", - "org.chromium.Chromium", - "ebf083d1b175911782d413369b64ce7d", - new(wayland.Conn), - }, &waylandOp{nil, - "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", - "/run/user/1971/wayland-0", - "org.chromium.Chromium", - "ebf083d1b175911782d413369b64ce7c", - new(wayland.Conn), - }, false}, - - {"equals", &waylandOp{nil, - "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", - "/run/user/1971/wayland-0", - "org.chromium.Chromium", - "ebf083d1b175911782d413369b64ce7c", - new(wayland.Conn), - }, &waylandOp{nil, - "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", - "/run/user/1971/wayland-0", - "org.chromium.Chromium", - "ebf083d1b175911782d413369b64ce7c", - new(wayland.Conn), - }, true}, - }) - - checkOpMeta(t, []opMetaTestCase{ - {"chromium", &waylandOp{nil, - "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", - "/run/user/1971/wayland-0", - "org.chromium.Chromium", - "ebf083d1b175911782d413369b64ce7c", - new(wayland.Conn), - }, Process, "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland", - `wayland socket at "/tmp/hakurei.1971/ebf083d1b175911782d413369b64ce7c/wayland"`}, - }) -} diff --git a/system/xhost.go b/system/xhost.go deleted file mode 100644 index 9a547f43..00000000 --- a/system/xhost.go +++ /dev/null @@ -1,38 +0,0 @@ -package system - -import ( - "hakurei.app/hst" - "hakurei.app/system/internal/xcb" -) - -// ChangeHosts inserts the target user into X11 hosts and deletes it once its [Enablement] is no longer satisfied. -func (sys *I) ChangeHosts(username string) *I { - sys.ops = append(sys.ops, xhostOp(username)) - return sys -} - -// xhostOp implements [I.ChangeHosts]. -type xhostOp string - -func (x xhostOp) Type() hst.Enablement { return hst.EX11 } - -func (x xhostOp) apply(sys *I) error { - sys.msg.Verbosef("inserting entry %s to X11", x) - return newOpError("xhost", - sys.xcbChangeHosts(xcb.HostModeInsert, xcb.FamilyServerInterpreted, "localuser\x00"+string(x)), false) -} - -func (x xhostOp) revert(sys *I, ec *Criteria) error { - if ec.hasType(x.Type()) { - sys.msg.Verbosef("deleting entry %s from X11", x) - return newOpError("xhost", - sys.xcbChangeHosts(xcb.HostModeDelete, xcb.FamilyServerInterpreted, "localuser\x00"+string(x)), true) - } else { - sys.msg.Verbosef("skipping entry %s in X11", x) - return nil - } -} - -func (x xhostOp) Is(o Op) bool { target, ok := o.(xhostOp); return ok && x == target } -func (x xhostOp) Path() string { return "/tmp/.X11-unix" } -func (x xhostOp) String() string { return string("SI:localuser:" + x) } diff --git a/system/xhost_test.go b/system/xhost_test.go deleted file mode 100644 index 759f7536..00000000 --- a/system/xhost_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package system - -import ( - "testing" - - "hakurei.app/container/stub" - "hakurei.app/hst" - "hakurei.app/system/internal/xcb" -) - -func TestXHostOp(t *testing.T) { - t.Parallel() - - checkOpBehaviour(t, []opBehaviourTestCase{ - {"xcbChangeHosts revert", 0xbeef, hst.EX11, xhostOp("chronos"), []stub.Call{ - call("verbosef", stub.ExpectArgs{"inserting entry %s to X11", []any{xhostOp("chronos")}}, nil, nil), - call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeInsert), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, stub.UniqueError(1)), - }, &OpError{Op: "xhost", Err: stub.UniqueError(1)}, nil, nil}, - - {"xcbChangeHosts revert", 0xbeef, hst.EX11, xhostOp("chronos"), []stub.Call{ - call("verbosef", stub.ExpectArgs{"inserting entry %s to X11", []any{xhostOp("chronos")}}, nil, nil), - call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeInsert), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, nil), - }, nil, []stub.Call{ - call("verbosef", stub.ExpectArgs{"deleting entry %s from X11", []any{xhostOp("chronos")}}, nil, nil), - call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeDelete), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, stub.UniqueError(0)), - }, &OpError{Op: "xhost", Err: stub.UniqueError(0), Revert: true}}, - - {"success skip", 0xbeef, 0, xhostOp("chronos"), []stub.Call{ - call("verbosef", stub.ExpectArgs{"inserting entry %s to X11", []any{xhostOp("chronos")}}, nil, nil), - call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeInsert), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, nil), - }, nil, []stub.Call{ - call("verbosef", stub.ExpectArgs{"skipping entry %s in X11", []any{xhostOp("chronos")}}, nil, nil), - }, nil}, - - {"success", 0xbeef, hst.EX11, xhostOp("chronos"), []stub.Call{ - call("verbosef", stub.ExpectArgs{"inserting entry %s to X11", []any{xhostOp("chronos")}}, nil, nil), - call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeInsert), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, nil), - }, nil, []stub.Call{ - call("verbosef", stub.ExpectArgs{"deleting entry %s from X11", []any{xhostOp("chronos")}}, nil, nil), - call("xcbChangeHosts", stub.ExpectArgs{xcb.HostMode(xcb.HostModeDelete), xcb.Family(xcb.FamilyServerInterpreted), "localuser\x00chronos"}, nil, nil), - }, nil}, - }) - - checkOpsBuilder(t, "ChangeHosts", []opsBuilderTestCase{ - {"xhost", 0xcafe, func(_ *testing.T, sys *I) { - sys.ChangeHosts("chronos") - }, []Op{ - xhostOp("chronos"), - }, stub.Expect{}}, - }) - - checkOpIs(t, []opIsTestCase{ - {"differs", xhostOp("kbd"), xhostOp("chronos"), false}, - {"equals", xhostOp("chronos"), xhostOp("chronos"), true}, - }) - - checkOpMeta(t, []opMetaTestCase{ - {"xhost", xhostOp("chronos"), hst.EX11, "/tmp/.X11-unix", "SI:localuser:chronos"}, - }) -} |
