internal: relocate packages
All checks were successful
Test / Create distribution (push) Successful in 36s
Test / Sandbox (push) Successful in 2m20s
Test / Hakurei (push) Successful in 3m19s
Test / Hpkg (push) Successful in 4m12s
Test / Sandbox (race detector) (push) Successful in 4m31s
Test / Hakurei (race detector) (push) Successful in 5m12s
Test / Flake checks (push) Successful in 1m32s

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
2025-11-15 13:47:09 +09:00
parent 16e674782a
commit a91920310d
63 changed files with 61 additions and 61 deletions

33
internal/acl/acl.go Normal file
View File

@@ -0,0 +1,33 @@
// 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)
}

276
internal/acl/acl_test.go Normal file
View File

@@ -0,0 +1,276 @@
package acl_test
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path"
"reflect"
"strconv"
"testing"
"hakurei.app/internal/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]
}

View File

@@ -0,0 +1,90 @@
#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;
}

View File

@@ -0,0 +1,40 @@
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
}

View File

@@ -0,0 +1,4 @@
#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);

View File

@@ -0,0 +1,60 @@
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)
}
})
}
}

18
internal/acl/perms.go Normal file
View File

@@ -0,0 +1,18 @@
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)
}

View File

@@ -0,0 +1,30 @@
package acl_test
import (
"testing"
"hakurei.app/internal/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)
}
})
}
}