package acl_test import ( "bufio" "bytes" "errors" "fmt" "io" "os" "os/exec" "path/filepath" "reflect" "strconv" "syscall" "testing" "hakurei.app/internal/acl" "hakurei.app/internal/info" ) var ( uid = os.Geteuid() cred = int32(uid) ) // testFile returns the pathname of a newly created test file. func testFile(t *testing.T) string { name := filepath.Join(t.TempDir(), "f") f, err := os.Create(name) if err != nil { t.Fatal(err) } else if err = f.Close(); err != nil { t.Fatal(err) } return name } func TestUpdate(t *testing.T) { if info.CanDegrade { name := filepath.Join(t.TempDir(), "check-degrade") if err := os.WriteFile(name, nil, 0); err != nil { t.Fatal(err) } if err := acl.Update(name, uid, 0); errors.Is(err, syscall.ENOTSUP) { t.Skip(err) } } t.Parallel() pathname := testFile(t) cur := getfacl(t, pathname) if len(cur) != 3 { t.Fatalf("unexpected test file acl length %d", len(cur)) } if err := acl.Update(pathname, uid, 0); err != nil { t.Fatalf("(clear) Update: error = %v", err) } if cur = getfacl(t, pathname); len(cur) != 4 { t.Fatalf("(clear) Update: %v", cur) } if err := acl.Update(pathname, uid, 0); err != nil { t.Fatalf("(clear*) Update: error = %v", err) } if val := getfacl(t, pathname); !reflect.DeepEqual(val, cur) { t.Fatalf("(clear*) Update: %v, want %v", val, cur) } testUpdate(t, "r--", "", cur, acl.Read, true) testUpdate(t, "-w-", "", cur, acl.Write, true) testUpdate(t, "--x", "", cur, acl.Execute, true) testUpdate( t, "-wx", "", cur, acl.Write|acl.Execute, true, ) testUpdate( t, "r-x", "", cur, acl.Read|acl.Execute, true, ) testUpdate( t, "rw-", "", cur, acl.Read|acl.Write, true, ) testUpdate( t, "rwx", "", cur, acl.Read|acl.Write|acl.Execute, true, ) for i := range 16 { testUpdate( t, "r-x", " "+strconv.Itoa(i), cur, acl.Read|acl.Execute, false, ) } testUpdate( t, "rwx", " cleanup", cur, acl.Read|acl.Write|acl.Execute, true, ) } func testUpdate( t *testing.T, name, suffix string, cur []*getFACLResp, perms acl.Perm, cleanup bool, ) { t.Run(name+suffix, func(t *testing.T) { t.Parallel() pathname := testFile(t) if cleanup { t.Cleanup(func() { if err := acl.Update(pathname, uid, 0); err != nil { t.Fatalf("Update: error = %v", err) } if v := getfacl(t, pathname); !reflect.DeepEqual(v, cur) { t.Fatalf("Update: %v, want %v", v, cur) } }) } if err := acl.Update(pathname, uid, perms); err != nil { t.Fatalf("Update: error = %v", err) } r := respByCred(getfacl(t, pathname), fAclTypeUser, cred) if r == nil { t.Fatalf("Update did not add an ACL entry") } if !r.equals(&getFACLResp{ typ: fAclTypeUser, cred: cred, val: perms, }) { t.Fatalf("Update(%s) = %s", name, r) } }) } type ( getFAclInvocation struct { cmd *exec.Cmd val []*getFACLResp pe []error } getFACLResp struct { typ fAclType cred int32 val acl.Perm raw []byte } fAclType uint8 ) const fAclBufSize = 16 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, ) c.cmd.Stderr = os.Stderr 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(r io.Reader, scanErr chan error) { c.val = make([]*getFACLResp, 0, 4+fAclBufSize) s := bufio.NewScanner(r) for s.Scan() { fields := bytes.SplitN(s.Bytes(), []byte{':'}, 3) if len(fields) != 3 { continue } var 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 |= acl.Read case '-': default: c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][0])) continue } switch fields[2][1] { case 'w': resp.val |= acl.Write case '-': default: c.pe = append(c.pe, fmt.Errorf("invalid perm %v", fields[2][1])) continue } switch fields[2][2] { case 'x': resp.val |= acl.Execute 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(v *getFACLResp) bool { return r.typ == v.typ && r.cred == v.cred && r.val == v.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...)) } 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] }