summaryrefslogtreecommitdiffstats
path: root/src/os/user
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/os/user/cgo_listgroups_unix.go48
-rw-r--r--src/os/user/cgo_lookup_unix.go270
-rw-r--r--src/os/user/cgo_unix_test.go23
-rw-r--r--src/os/user/getgrouplist_darwin.go54
-rw-r--r--src/os/user/getgrouplist_unix.go41
-rw-r--r--src/os/user/listgroups_stub.go19
-rw-r--r--src/os/user/listgroups_unix.go113
-rw-r--r--src/os/user/listgroups_unix_test.go107
-rw-r--r--src/os/user/lookup.go63
-rw-r--r--src/os/user/lookup_android.go25
-rw-r--r--src/os/user/lookup_plan9.go63
-rw-r--r--src/os/user/lookup_stubs.go76
-rw-r--r--src/os/user/lookup_unix.go236
-rw-r--r--src/os/user/lookup_unix_test.go259
-rw-r--r--src/os/user/lookup_windows.go385
-rw-r--r--src/os/user/user.go95
-rw-r--r--src/os/user/user_test.go150
17 files changed, 2027 insertions, 0 deletions
diff --git a/src/os/user/cgo_listgroups_unix.go b/src/os/user/cgo_listgroups_unix.go
new file mode 100644
index 0000000..0d937da
--- /dev/null
+++ b/src/os/user/cgo_listgroups_unix.go
@@ -0,0 +1,48 @@
+// Copyright 2016 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build (dragonfly || darwin || freebsd || (!android && linux) || netbsd || openbsd || (solaris && !illumos)) && cgo && !osusergo
+
+package user
+
+import (
+ "fmt"
+ "strconv"
+ "unsafe"
+)
+
+/*
+#include <unistd.h>
+#include <sys/types.h>
+*/
+import "C"
+
+const maxGroups = 2048
+
+func listGroups(u *User) ([]string, error) {
+ ug, err := strconv.Atoi(u.Gid)
+ if err != nil {
+ return nil, fmt.Errorf("user: list groups for %s: invalid gid %q", u.Username, u.Gid)
+ }
+ userGID := C.gid_t(ug)
+ nameC := make([]byte, len(u.Username)+1)
+ copy(nameC, u.Username)
+
+ n := C.int(256)
+ gidsC := make([]C.gid_t, n)
+ rv := getGroupList((*C.char)(unsafe.Pointer(&nameC[0])), userGID, &gidsC[0], &n)
+ if rv == -1 {
+ // Mac is the only Unix that does not set n properly when rv == -1, so
+ // we need to use different logic for Mac vs. the other OS's.
+ if err := groupRetry(u.Username, nameC, userGID, &gidsC, &n); err != nil {
+ return nil, err
+ }
+ }
+ gidsC = gidsC[:n]
+ gids := make([]string, 0, n)
+ for _, g := range gidsC[:n] {
+ gids = append(gids, strconv.Itoa(int(g)))
+ }
+ return gids, nil
+}
diff --git a/src/os/user/cgo_lookup_unix.go b/src/os/user/cgo_lookup_unix.go
new file mode 100644
index 0000000..5232690
--- /dev/null
+++ b/src/os/user/cgo_lookup_unix.go
@@ -0,0 +1,270 @@
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build (aix || darwin || dragonfly || freebsd || (!android && linux) || netbsd || openbsd || solaris) && cgo && !osusergo
+
+package user
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+ "syscall"
+ "unsafe"
+)
+
+/*
+#cgo solaris CFLAGS: -D_POSIX_PTHREAD_SEMANTICS
+#include <unistd.h>
+#include <sys/types.h>
+#include <pwd.h>
+#include <grp.h>
+#include <stdlib.h>
+
+static int mygetpwuid_r(int uid, struct passwd *pwd,
+ char *buf, size_t buflen, struct passwd **result) {
+ return getpwuid_r(uid, pwd, buf, buflen, result);
+}
+
+static int mygetpwnam_r(const char *name, struct passwd *pwd,
+ char *buf, size_t buflen, struct passwd **result) {
+ return getpwnam_r(name, pwd, buf, buflen, result);
+}
+
+static int mygetgrgid_r(int gid, struct group *grp,
+ char *buf, size_t buflen, struct group **result) {
+ return getgrgid_r(gid, grp, buf, buflen, result);
+}
+
+static int mygetgrnam_r(const char *name, struct group *grp,
+ char *buf, size_t buflen, struct group **result) {
+ return getgrnam_r(name, grp, buf, buflen, result);
+}
+*/
+import "C"
+
+func current() (*User, error) {
+ return lookupUnixUid(syscall.Getuid())
+}
+
+func lookupUser(username string) (*User, error) {
+ var pwd C.struct_passwd
+ var result *C.struct_passwd
+ nameC := make([]byte, len(username)+1)
+ copy(nameC, username)
+
+ buf := alloc(userBuffer)
+ defer buf.free()
+
+ err := retryWithBuffer(buf, func() syscall.Errno {
+ // mygetpwnam_r is a wrapper around getpwnam_r to avoid
+ // passing a size_t to getpwnam_r, because for unknown
+ // reasons passing a size_t to getpwnam_r doesn't work on
+ // Solaris.
+ return syscall.Errno(C.mygetpwnam_r((*C.char)(unsafe.Pointer(&nameC[0])),
+ &pwd,
+ (*C.char)(buf.ptr),
+ C.size_t(buf.size),
+ &result))
+ })
+ if err != nil {
+ return nil, fmt.Errorf("user: lookup username %s: %v", username, err)
+ }
+ if result == nil {
+ return nil, UnknownUserError(username)
+ }
+ return buildUser(&pwd), err
+}
+
+func lookupUserId(uid string) (*User, error) {
+ i, e := strconv.Atoi(uid)
+ if e != nil {
+ return nil, e
+ }
+ return lookupUnixUid(i)
+}
+
+func lookupUnixUid(uid int) (*User, error) {
+ var pwd C.struct_passwd
+ var result *C.struct_passwd
+
+ buf := alloc(userBuffer)
+ defer buf.free()
+
+ err := retryWithBuffer(buf, func() syscall.Errno {
+ // mygetpwuid_r is a wrapper around getpwuid_r to avoid using uid_t
+ // because C.uid_t(uid) for unknown reasons doesn't work on linux.
+ return syscall.Errno(C.mygetpwuid_r(C.int(uid),
+ &pwd,
+ (*C.char)(buf.ptr),
+ C.size_t(buf.size),
+ &result))
+ })
+ if err != nil {
+ return nil, fmt.Errorf("user: lookup userid %d: %v", uid, err)
+ }
+ if result == nil {
+ return nil, UnknownUserIdError(uid)
+ }
+ return buildUser(&pwd), nil
+}
+
+func buildUser(pwd *C.struct_passwd) *User {
+ u := &User{
+ Uid: strconv.FormatUint(uint64(pwd.pw_uid), 10),
+ Gid: strconv.FormatUint(uint64(pwd.pw_gid), 10),
+ Username: C.GoString(pwd.pw_name),
+ Name: C.GoString(pwd.pw_gecos),
+ HomeDir: C.GoString(pwd.pw_dir),
+ }
+ // The pw_gecos field isn't quite standardized. Some docs
+ // say: "It is expected to be a comma separated list of
+ // personal data where the first item is the full name of the
+ // user."
+ u.Name, _, _ = strings.Cut(u.Name, ",")
+ return u
+}
+
+func lookupGroup(groupname string) (*Group, error) {
+ var grp C.struct_group
+ var result *C.struct_group
+
+ buf := alloc(groupBuffer)
+ defer buf.free()
+ cname := make([]byte, len(groupname)+1)
+ copy(cname, groupname)
+
+ err := retryWithBuffer(buf, func() syscall.Errno {
+ return syscall.Errno(C.mygetgrnam_r((*C.char)(unsafe.Pointer(&cname[0])),
+ &grp,
+ (*C.char)(buf.ptr),
+ C.size_t(buf.size),
+ &result))
+ })
+ if err != nil {
+ return nil, fmt.Errorf("user: lookup groupname %s: %v", groupname, err)
+ }
+ if result == nil {
+ return nil, UnknownGroupError(groupname)
+ }
+ return buildGroup(&grp), nil
+}
+
+func lookupGroupId(gid string) (*Group, error) {
+ i, e := strconv.Atoi(gid)
+ if e != nil {
+ return nil, e
+ }
+ return lookupUnixGid(i)
+}
+
+func lookupUnixGid(gid int) (*Group, error) {
+ var grp C.struct_group
+ var result *C.struct_group
+
+ buf := alloc(groupBuffer)
+ defer buf.free()
+
+ err := retryWithBuffer(buf, func() syscall.Errno {
+ // mygetgrgid_r is a wrapper around getgrgid_r to avoid using gid_t
+ // because C.gid_t(gid) for unknown reasons doesn't work on linux.
+ return syscall.Errno(C.mygetgrgid_r(C.int(gid),
+ &grp,
+ (*C.char)(buf.ptr),
+ C.size_t(buf.size),
+ &result))
+ })
+ if err != nil {
+ return nil, fmt.Errorf("user: lookup groupid %d: %v", gid, err)
+ }
+ if result == nil {
+ return nil, UnknownGroupIdError(strconv.Itoa(gid))
+ }
+ return buildGroup(&grp), nil
+}
+
+func buildGroup(grp *C.struct_group) *Group {
+ g := &Group{
+ Gid: strconv.Itoa(int(grp.gr_gid)),
+ Name: C.GoString(grp.gr_name),
+ }
+ return g
+}
+
+type bufferKind C.int
+
+const (
+ userBuffer = bufferKind(C._SC_GETPW_R_SIZE_MAX)
+ groupBuffer = bufferKind(C._SC_GETGR_R_SIZE_MAX)
+)
+
+func (k bufferKind) initialSize() C.size_t {
+ sz := C.sysconf(C.int(k))
+ if sz == -1 {
+ // DragonFly and FreeBSD do not have _SC_GETPW_R_SIZE_MAX.
+ // Additionally, not all Linux systems have it, either. For
+ // example, the musl libc returns -1.
+ return 1024
+ }
+ if !isSizeReasonable(int64(sz)) {
+ // Truncate. If this truly isn't enough, retryWithBuffer will error on the first run.
+ return maxBufferSize
+ }
+ return C.size_t(sz)
+}
+
+type memBuffer struct {
+ ptr unsafe.Pointer
+ size C.size_t
+}
+
+func alloc(kind bufferKind) *memBuffer {
+ sz := kind.initialSize()
+ return &memBuffer{
+ ptr: C.malloc(sz),
+ size: sz,
+ }
+}
+
+func (mb *memBuffer) resize(newSize C.size_t) {
+ mb.ptr = C.realloc(mb.ptr, newSize)
+ mb.size = newSize
+}
+
+func (mb *memBuffer) free() {
+ C.free(mb.ptr)
+}
+
+// retryWithBuffer repeatedly calls f(), increasing the size of the
+// buffer each time, until f succeeds, fails with a non-ERANGE error,
+// or the buffer exceeds a reasonable limit.
+func retryWithBuffer(buf *memBuffer, f func() syscall.Errno) error {
+ for {
+ errno := f()
+ if errno == 0 {
+ return nil
+ } else if errno != syscall.ERANGE {
+ return errno
+ }
+ newSize := buf.size * 2
+ if !isSizeReasonable(int64(newSize)) {
+ return fmt.Errorf("internal buffer exceeds %d bytes", maxBufferSize)
+ }
+ buf.resize(newSize)
+ }
+}
+
+const maxBufferSize = 1 << 20
+
+func isSizeReasonable(sz int64) bool {
+ return sz > 0 && sz <= maxBufferSize
+}
+
+// Because we can't use cgo in tests:
+func structPasswdForNegativeTest() C.struct_passwd {
+ sp := C.struct_passwd{}
+ sp.pw_uid = 1<<32 - 2
+ sp.pw_gid = 1<<32 - 3
+ return sp
+}
diff --git a/src/os/user/cgo_unix_test.go b/src/os/user/cgo_unix_test.go
new file mode 100644
index 0000000..6d16aa2
--- /dev/null
+++ b/src/os/user/cgo_unix_test.go
@@ -0,0 +1,23 @@
+// Copyright 2017 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build (darwin || dragonfly || freebsd || (!android && linux) || netbsd || openbsd || solaris) && cgo && !osusergo
+
+package user
+
+import (
+ "testing"
+)
+
+// Issue 22739
+func TestNegativeUid(t *testing.T) {
+ sp := structPasswdForNegativeTest()
+ u := buildUser(&sp)
+ if g, w := u.Uid, "4294967294"; g != w {
+ t.Errorf("Uid = %q; want %q", g, w)
+ }
+ if g, w := u.Gid, "4294967293"; g != w {
+ t.Errorf("Gid = %q; want %q", g, w)
+ }
+}
diff --git a/src/os/user/getgrouplist_darwin.go b/src/os/user/getgrouplist_darwin.go
new file mode 100644
index 0000000..db6fb87
--- /dev/null
+++ b/src/os/user/getgrouplist_darwin.go
@@ -0,0 +1,54 @@
+// Copyright 2016 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build cgo && !osusergo
+
+package user
+
+/*
+#include <unistd.h>
+#include <sys/types.h>
+#include <stdlib.h>
+
+static int mygetgrouplist(const char* user, gid_t group, gid_t* groups, int* ngroups) {
+ int* buf = malloc(*ngroups * sizeof(int));
+ int rv = getgrouplist(user, (int) group, buf, ngroups);
+ int i;
+ if (rv == 0) {
+ for (i = 0; i < *ngroups; i++) {
+ groups[i] = (gid_t) buf[i];
+ }
+ }
+ free(buf);
+ return rv;
+}
+*/
+import "C"
+import (
+ "fmt"
+ "unsafe"
+)
+
+func getGroupList(name *C.char, userGID C.gid_t, gids *C.gid_t, n *C.int) C.int {
+ return C.mygetgrouplist(name, userGID, gids, n)
+}
+
+// groupRetry retries getGroupList with an increasingly large size for n. The
+// result is stored in gids.
+func groupRetry(username string, name []byte, userGID C.gid_t, gids *[]C.gid_t, n *C.int) error {
+ *n = C.int(256 * 2)
+ for {
+ *gids = make([]C.gid_t, *n)
+ rv := getGroupList((*C.char)(unsafe.Pointer(&name[0])), userGID, &(*gids)[0], n)
+ if rv >= 0 {
+ // n is set correctly
+ break
+ }
+ if *n > maxGroups {
+ return fmt.Errorf("user: %q is a member of more than %d groups", username, maxGroups)
+ }
+ *n = *n * C.int(2)
+ }
+ return nil
+}
diff --git a/src/os/user/getgrouplist_unix.go b/src/os/user/getgrouplist_unix.go
new file mode 100644
index 0000000..104c224
--- /dev/null
+++ b/src/os/user/getgrouplist_unix.go
@@ -0,0 +1,41 @@
+// Copyright 2016 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build (dragonfly || freebsd || (!android && linux) || netbsd || openbsd || (solaris && !illumos)) && cgo && !osusergo
+
+package user
+
+/*
+#include <unistd.h>
+#include <sys/types.h>
+#include <grp.h>
+
+static int mygetgrouplist(const char* user, gid_t group, gid_t* groups, int* ngroups) {
+ return getgrouplist(user, group, groups, ngroups);
+}
+*/
+import "C"
+import (
+ "fmt"
+ "unsafe"
+)
+
+func getGroupList(name *C.char, userGID C.gid_t, gids *C.gid_t, n *C.int) C.int {
+ return C.mygetgrouplist(name, userGID, gids, n)
+}
+
+// groupRetry retries getGroupList with much larger size for n. The result is
+// stored in gids.
+func groupRetry(username string, name []byte, userGID C.gid_t, gids *[]C.gid_t, n *C.int) error {
+ // More than initial buffer, but now n contains the correct size.
+ if *n > maxGroups {
+ return fmt.Errorf("user: %q is a member of more than %d groups", username, maxGroups)
+ }
+ *gids = make([]C.gid_t, *n)
+ rv := getGroupList((*C.char)(unsafe.Pointer(&name[0])), userGID, &(*gids)[0], n)
+ if rv == -1 {
+ return fmt.Errorf("user: list groups for %s failed", username)
+ }
+ return nil
+}
diff --git a/src/os/user/listgroups_stub.go b/src/os/user/listgroups_stub.go
new file mode 100644
index 0000000..4cf808b
--- /dev/null
+++ b/src/os/user/listgroups_stub.go
@@ -0,0 +1,19 @@
+// Copyright 2021 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build android || (js && !wasm)
+
+package user
+
+import (
+ "errors"
+)
+
+func init() {
+ groupListImplemented = false
+}
+
+func listGroups(*User) ([]string, error) {
+ return nil, errors.New("user: list groups not implemented")
+}
diff --git a/src/os/user/listgroups_unix.go b/src/os/user/listgroups_unix.go
new file mode 100644
index 0000000..fa2df49
--- /dev/null
+++ b/src/os/user/listgroups_unix.go
@@ -0,0 +1,113 @@
+// Copyright 2021 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build ((darwin || dragonfly || freebsd || (js && wasm) || (!android && linux) || netbsd || openbsd || solaris) && (!cgo || osusergo)) || aix || illumos
+
+package user
+
+import (
+ "bufio"
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "strconv"
+)
+
+const groupFile = "/etc/group"
+
+var colon = []byte{':'}
+
+func listGroupsFromReader(u *User, r io.Reader) ([]string, error) {
+ if u.Username == "" {
+ return nil, errors.New("user: list groups: empty username")
+ }
+ primaryGid, err := strconv.Atoi(u.Gid)
+ if err != nil {
+ return nil, fmt.Errorf("user: list groups for %s: invalid gid %q", u.Username, u.Gid)
+ }
+
+ userCommas := []byte("," + u.Username + ",") // ,john,
+ userFirst := userCommas[1:] // john,
+ userLast := userCommas[:len(userCommas)-1] // ,john
+ userOnly := userCommas[1 : len(userCommas)-1] // john
+
+ // Add primary Gid first.
+ groups := []string{u.Gid}
+
+ rd := bufio.NewReader(r)
+ done := false
+ for !done {
+ line, err := rd.ReadBytes('\n')
+ if err != nil {
+ if err == io.EOF {
+ done = true
+ } else {
+ return groups, err
+ }
+ }
+
+ // Look for username in the list of users. If user is found,
+ // append the GID to the groups slice.
+
+ // There's no spec for /etc/passwd or /etc/group, but we try to follow
+ // the same rules as the glibc parser, which allows comments and blank
+ // space at the beginning of a line.
+ line = bytes.TrimSpace(line)
+ if len(line) == 0 || line[0] == '#' ||
+ // If you search for a gid in a row where the group
+ // name (the first field) starts with "+" or "-",
+ // glibc fails to find the record, and so should we.
+ line[0] == '+' || line[0] == '-' {
+ continue
+ }
+
+ // Format of /etc/group is
+ // groupname:password:GID:user_list
+ // for example
+ // wheel:x:10:john,paul,jack
+ // tcpdump:x:72:
+ listIdx := bytes.LastIndexByte(line, ':')
+ if listIdx == -1 || listIdx == len(line)-1 {
+ // No commas, or empty group list.
+ continue
+ }
+ if bytes.Count(line[:listIdx], colon) != 2 {
+ // Incorrect number of colons.
+ continue
+ }
+ list := line[listIdx+1:]
+ // Check the list for user without splitting or copying.
+ if !(bytes.Equal(list, userOnly) || bytes.HasPrefix(list, userFirst) || bytes.HasSuffix(list, userLast) || bytes.Contains(list, userCommas)) {
+ continue
+ }
+
+ // groupname:password:GID
+ parts := bytes.Split(line[:listIdx], colon)
+ if len(parts) != 3 || len(parts[0]) == 0 {
+ continue
+ }
+ gid := string(parts[2])
+ // Make sure it's numeric and not the same as primary GID.
+ numGid, err := strconv.Atoi(gid)
+ if err != nil || numGid == primaryGid {
+ continue
+ }
+
+ groups = append(groups, gid)
+ }
+
+ return groups, nil
+}
+
+func listGroups(u *User) ([]string, error) {
+ f, err := os.Open(groupFile)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+
+ return listGroupsFromReader(u, f)
+}
diff --git a/src/os/user/listgroups_unix_test.go b/src/os/user/listgroups_unix_test.go
new file mode 100644
index 0000000..a9f79ec
--- /dev/null
+++ b/src/os/user/listgroups_unix_test.go
@@ -0,0 +1,107 @@
+// Copyright 2021 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build ((darwin || dragonfly || freebsd || (js && wasm) || (!android && linux) || netbsd || openbsd || solaris) && (!cgo || osusergo)) || aix || illumos
+
+package user
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+ "testing"
+)
+
+var testGroupFile = `# See the opendirectoryd(8) man page for additional
+# information about Open Directory.
+##
+nobody:*:-2:
+nogroup:*:-1:
+wheel:*:0:root
+emptyid:*::root
+invalidgid:*:notanumber:root
++plussign:*:20:root
+-minussign:*:21:root
+# Next line is invalid (empty group name)
+:*:22:root
+
+daemon:*:1:root
+ indented:*:7:root
+# comment:*:4:found
+ # comment:*:4:found
+kmem:*:2:root
+manymembers:x:777:jill,jody,john,jack,jov,user777
+` + largeGroup()
+
+func largeGroup() (res string) {
+ var b strings.Builder
+ b.WriteString("largegroup:x:1000:user1")
+ for i := 2; i <= 7500; i++ {
+ fmt.Fprintf(&b, ",user%d", i)
+ }
+ return b.String()
+}
+
+var listGroupsTests = []struct {
+ // input
+ in string
+ user string
+ gid string
+ // output
+ gids []string
+ err bool
+}{
+ {in: testGroupFile, user: "root", gid: "0", gids: []string{"0", "1", "2", "7"}},
+ {in: testGroupFile, user: "jill", gid: "33", gids: []string{"33", "777"}},
+ {in: testGroupFile, user: "jody", gid: "34", gids: []string{"34", "777"}},
+ {in: testGroupFile, user: "john", gid: "35", gids: []string{"35", "777"}},
+ {in: testGroupFile, user: "jov", gid: "37", gids: []string{"37", "777"}},
+ {in: testGroupFile, user: "user777", gid: "7", gids: []string{"7", "777", "1000"}},
+ {in: testGroupFile, user: "user1111", gid: "1111", gids: []string{"1111", "1000"}},
+ {in: testGroupFile, user: "user1000", gid: "1000", gids: []string{"1000"}},
+ {in: testGroupFile, user: "user7500", gid: "7500", gids: []string{"1000", "7500"}},
+ {in: testGroupFile, user: "no-such-user", gid: "2345", gids: []string{"2345"}},
+ {in: "", user: "no-such-user", gid: "2345", gids: []string{"2345"}},
+ // Error cases.
+ {in: "", user: "", gid: "2345", err: true},
+ {in: "", user: "joanna", gid: "bad", err: true},
+}
+
+func TestListGroups(t *testing.T) {
+ for _, tc := range listGroupsTests {
+ u := &User{Username: tc.user, Gid: tc.gid}
+ got, err := listGroupsFromReader(u, strings.NewReader(tc.in))
+ if tc.err {
+ if err == nil {
+ t.Errorf("listGroups(%q): got nil; want error", tc.user)
+ }
+ continue // no more checks
+ }
+ if err != nil {
+ t.Errorf("listGroups(%q): got %v error, want nil", tc.user, err)
+ continue // no more checks
+ }
+ checkSameIDs(t, got, tc.gids)
+ }
+}
+
+func checkSameIDs(t *testing.T, got, want []string) {
+ t.Helper()
+ if len(got) != len(want) {
+ t.Errorf("ID list mismatch: got %v; want %v", got, want)
+ return
+ }
+ sort.Strings(got)
+ sort.Strings(want)
+ mismatch := -1
+ for i, g := range want {
+ if got[i] != g {
+ mismatch = i
+ break
+ }
+ }
+ if mismatch != -1 {
+ t.Errorf("ID list mismatch (at index %d): got %v; want %v", mismatch, got, want)
+ }
+}
diff --git a/src/os/user/lookup.go b/src/os/user/lookup.go
new file mode 100644
index 0000000..b36b7c0
--- /dev/null
+++ b/src/os/user/lookup.go
@@ -0,0 +1,63 @@
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package user
+
+import "sync"
+
+// Current returns the current user.
+//
+// The first call will cache the current user information.
+// Subsequent calls will return the cached value and will not reflect
+// changes to the current user.
+func Current() (*User, error) {
+ cache.Do(func() { cache.u, cache.err = current() })
+ if cache.err != nil {
+ return nil, cache.err
+ }
+ u := *cache.u // copy
+ return &u, nil
+}
+
+// cache of the current user
+var cache struct {
+ sync.Once
+ u *User
+ err error
+}
+
+// Lookup looks up a user by username. If the user cannot be found, the
+// returned error is of type UnknownUserError.
+func Lookup(username string) (*User, error) {
+ if u, err := Current(); err == nil && u.Username == username {
+ return u, err
+ }
+ return lookupUser(username)
+}
+
+// LookupId looks up a user by userid. If the user cannot be found, the
+// returned error is of type UnknownUserIdError.
+func LookupId(uid string) (*User, error) {
+ if u, err := Current(); err == nil && u.Uid == uid {
+ return u, err
+ }
+ return lookupUserId(uid)
+}
+
+// LookupGroup looks up a group by name. If the group cannot be found, the
+// returned error is of type UnknownGroupError.
+func LookupGroup(name string) (*Group, error) {
+ return lookupGroup(name)
+}
+
+// LookupGroupId looks up a group by groupid. If the group cannot be found, the
+// returned error is of type UnknownGroupIdError.
+func LookupGroupId(gid string) (*Group, error) {
+ return lookupGroupId(gid)
+}
+
+// GroupIds returns the list of group IDs that the user is a member of.
+func (u *User) GroupIds() ([]string, error) {
+ return listGroups(u)
+}
diff --git a/src/os/user/lookup_android.go b/src/os/user/lookup_android.go
new file mode 100644
index 0000000..0ae31fd
--- /dev/null
+++ b/src/os/user/lookup_android.go
@@ -0,0 +1,25 @@
+// Copyright 2016 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build android
+
+package user
+
+import "errors"
+
+func lookupUser(string) (*User, error) {
+ return nil, errors.New("user: Lookup not implemented on android")
+}
+
+func lookupUserId(string) (*User, error) {
+ return nil, errors.New("user: LookupId not implemented on android")
+}
+
+func lookupGroup(string) (*Group, error) {
+ return nil, errors.New("user: LookupGroup not implemented on android")
+}
+
+func lookupGroupId(string) (*Group, error) {
+ return nil, errors.New("user: LookupGroupId not implemented on android")
+}
diff --git a/src/os/user/lookup_plan9.go b/src/os/user/lookup_plan9.go
new file mode 100644
index 0000000..0793936
--- /dev/null
+++ b/src/os/user/lookup_plan9.go
@@ -0,0 +1,63 @@
+// Copyright 2013 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package user
+
+import (
+ "fmt"
+ "os"
+ "syscall"
+)
+
+// Partial os/user support on Plan 9.
+// Supports Current(), but not Lookup()/LookupId().
+// The latter two would require parsing /adm/users.
+const (
+ userFile = "/dev/user"
+)
+
+func init() {
+ userImplemented = false
+ groupImplemented = false
+ groupListImplemented = false
+}
+
+func current() (*User, error) {
+ ubytes, err := os.ReadFile(userFile)
+ if err != nil {
+ return nil, fmt.Errorf("user: %s", err)
+ }
+
+ uname := string(ubytes)
+
+ u := &User{
+ Uid: uname,
+ Gid: uname,
+ Username: uname,
+ Name: uname,
+ HomeDir: os.Getenv("home"),
+ }
+
+ return u, nil
+}
+
+func lookupUser(username string) (*User, error) {
+ return nil, syscall.EPLAN9
+}
+
+func lookupUserId(uid string) (*User, error) {
+ return nil, syscall.EPLAN9
+}
+
+func lookupGroup(groupname string) (*Group, error) {
+ return nil, syscall.EPLAN9
+}
+
+func lookupGroupId(string) (*Group, error) {
+ return nil, syscall.EPLAN9
+}
+
+func listGroups(*User) ([]string, error) {
+ return nil, syscall.EPLAN9
+}
diff --git a/src/os/user/lookup_stubs.go b/src/os/user/lookup_stubs.go
new file mode 100644
index 0000000..ce1617d
--- /dev/null
+++ b/src/os/user/lookup_stubs.go
@@ -0,0 +1,76 @@
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build (!cgo && !windows && !plan9) || android || (osusergo && !windows && !plan9)
+
+package user
+
+import (
+ "fmt"
+ "os"
+ "runtime"
+ "strconv"
+)
+
+func current() (*User, error) {
+ uid := currentUID()
+ // $USER and /etc/passwd may disagree; prefer the latter if we can get it.
+ // See issue 27524 for more information.
+ u, err := lookupUserId(uid)
+ if err == nil {
+ return u, nil
+ }
+
+ homeDir, _ := os.UserHomeDir()
+ u = &User{
+ Uid: uid,
+ Gid: currentGID(),
+ Username: os.Getenv("USER"),
+ Name: "", // ignored
+ HomeDir: homeDir,
+ }
+ // On Android, return a dummy user instead of failing.
+ switch runtime.GOOS {
+ case "android":
+ if u.Uid == "" {
+ u.Uid = "1"
+ }
+ if u.Username == "" {
+ u.Username = "android"
+ }
+ }
+ // cgo isn't available, but if we found the minimum information
+ // without it, use it:
+ if u.Uid != "" && u.Username != "" && u.HomeDir != "" {
+ return u, nil
+ }
+ var missing string
+ if u.Username == "" {
+ missing = "$USER"
+ }
+ if u.HomeDir == "" {
+ if missing != "" {
+ missing += ", "
+ }
+ missing += "$HOME"
+ }
+ return u, fmt.Errorf("user: Current requires cgo or %s set in environment", missing)
+}
+
+func currentUID() string {
+ if id := os.Getuid(); id >= 0 {
+ return strconv.Itoa(id)
+ }
+ // Note: Windows returns -1, but this file isn't used on
+ // Windows anyway, so this empty return path shouldn't be
+ // used.
+ return ""
+}
+
+func currentGID() string {
+ if id := os.Getgid(); id >= 0 {
+ return strconv.Itoa(id)
+ }
+ return ""
+}
diff --git a/src/os/user/lookup_unix.go b/src/os/user/lookup_unix.go
new file mode 100644
index 0000000..058dab1
--- /dev/null
+++ b/src/os/user/lookup_unix.go
@@ -0,0 +1,236 @@
+// Copyright 2016 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build (aix || darwin || dragonfly || freebsd || (js && wasm) || (!android && linux) || netbsd || openbsd || solaris) && (!cgo || osusergo)
+
+package user
+
+import (
+ "bufio"
+ "bytes"
+ "errors"
+ "io"
+ "os"
+ "strconv"
+ "strings"
+)
+
+const userFile = "/etc/passwd"
+
+// lineFunc returns a value, an error, or (nil, nil) to skip the row.
+type lineFunc func(line []byte) (v any, err error)
+
+// readColonFile parses r as an /etc/group or /etc/passwd style file, running
+// fn for each row. readColonFile returns a value, an error, or (nil, nil) if
+// the end of the file is reached without a match.
+//
+// readCols is the minimum number of colon-separated fields that will be passed
+// to fn; in a long line additional fields may be silently discarded.
+func readColonFile(r io.Reader, fn lineFunc, readCols int) (v any, err error) {
+ rd := bufio.NewReader(r)
+
+ // Read the file line-by-line.
+ for {
+ var isPrefix bool
+ var wholeLine []byte
+
+ // Read the next line. We do so in chunks (as much as reader's
+ // buffer is able to keep), check if we read enough columns
+ // already on each step and store final result in wholeLine.
+ for {
+ var line []byte
+ line, isPrefix, err = rd.ReadLine()
+
+ if err != nil {
+ // We should return (nil, nil) if EOF is reached
+ // without a match.
+ if err == io.EOF {
+ err = nil
+ }
+ return nil, err
+ }
+
+ // Simple common case: line is short enough to fit in a
+ // single reader's buffer.
+ if !isPrefix && len(wholeLine) == 0 {
+ wholeLine = line
+ break
+ }
+
+ wholeLine = append(wholeLine, line...)
+
+ // Check if we read the whole line (or enough columns)
+ // already.
+ if !isPrefix || bytes.Count(wholeLine, []byte{':'}) >= readCols {
+ break
+ }
+ }
+
+ // There's no spec for /etc/passwd or /etc/group, but we try to follow
+ // the same rules as the glibc parser, which allows comments and blank
+ // space at the beginning of a line.
+ wholeLine = bytes.TrimSpace(wholeLine)
+ if len(wholeLine) == 0 || wholeLine[0] == '#' {
+ continue
+ }
+ v, err = fn(wholeLine)
+ if v != nil || err != nil {
+ return
+ }
+
+ // If necessary, skip the rest of the line
+ for ; isPrefix; _, isPrefix, err = rd.ReadLine() {
+ if err != nil {
+ // We should return (nil, nil) if EOF is reached without a match.
+ if err == io.EOF {
+ err = nil
+ }
+ return nil, err
+ }
+ }
+ }
+}
+
+func matchGroupIndexValue(value string, idx int) lineFunc {
+ var leadColon string
+ if idx > 0 {
+ leadColon = ":"
+ }
+ substr := []byte(leadColon + value + ":")
+ return func(line []byte) (v any, err error) {
+ if !bytes.Contains(line, substr) || bytes.Count(line, colon) < 3 {
+ return
+ }
+ // wheel:*:0:root
+ parts := strings.SplitN(string(line), ":", 4)
+ if len(parts) < 4 || parts[0] == "" || parts[idx] != value ||
+ // If the file contains +foo and you search for "foo", glibc
+ // returns an "invalid argument" error. Similarly, if you search
+ // for a gid for a row where the group name starts with "+" or "-",
+ // glibc fails to find the record.
+ parts[0][0] == '+' || parts[0][0] == '-' {
+ return
+ }
+ if _, err := strconv.Atoi(parts[2]); err != nil {
+ return nil, nil
+ }
+ return &Group{Name: parts[0], Gid: parts[2]}, nil
+ }
+}
+
+func findGroupId(id string, r io.Reader) (*Group, error) {
+ if v, err := readColonFile(r, matchGroupIndexValue(id, 2), 3); err != nil {
+ return nil, err
+ } else if v != nil {
+ return v.(*Group), nil
+ }
+ return nil, UnknownGroupIdError(id)
+}
+
+func findGroupName(name string, r io.Reader) (*Group, error) {
+ if v, err := readColonFile(r, matchGroupIndexValue(name, 0), 3); err != nil {
+ return nil, err
+ } else if v != nil {
+ return v.(*Group), nil
+ }
+ return nil, UnknownGroupError(name)
+}
+
+// returns a *User for a row if that row's has the given value at the
+// given index.
+func matchUserIndexValue(value string, idx int) lineFunc {
+ var leadColon string
+ if idx > 0 {
+ leadColon = ":"
+ }
+ substr := []byte(leadColon + value + ":")
+ return func(line []byte) (v any, err error) {
+ if !bytes.Contains(line, substr) || bytes.Count(line, colon) < 6 {
+ return
+ }
+ // kevin:x:1005:1006::/home/kevin:/usr/bin/zsh
+ parts := strings.SplitN(string(line), ":", 7)
+ if len(parts) < 6 || parts[idx] != value || parts[0] == "" ||
+ parts[0][0] == '+' || parts[0][0] == '-' {
+ return
+ }
+ if _, err := strconv.Atoi(parts[2]); err != nil {
+ return nil, nil
+ }
+ if _, err := strconv.Atoi(parts[3]); err != nil {
+ return nil, nil
+ }
+ u := &User{
+ Username: parts[0],
+ Uid: parts[2],
+ Gid: parts[3],
+ Name: parts[4],
+ HomeDir: parts[5],
+ }
+ // The pw_gecos field isn't quite standardized. Some docs
+ // say: "It is expected to be a comma separated list of
+ // personal data where the first item is the full name of the
+ // user."
+ u.Name, _, _ = strings.Cut(u.Name, ",")
+ return u, nil
+ }
+}
+
+func findUserId(uid string, r io.Reader) (*User, error) {
+ i, e := strconv.Atoi(uid)
+ if e != nil {
+ return nil, errors.New("user: invalid userid " + uid)
+ }
+ if v, err := readColonFile(r, matchUserIndexValue(uid, 2), 6); err != nil {
+ return nil, err
+ } else if v != nil {
+ return v.(*User), nil
+ }
+ return nil, UnknownUserIdError(i)
+}
+
+func findUsername(name string, r io.Reader) (*User, error) {
+ if v, err := readColonFile(r, matchUserIndexValue(name, 0), 6); err != nil {
+ return nil, err
+ } else if v != nil {
+ return v.(*User), nil
+ }
+ return nil, UnknownUserError(name)
+}
+
+func lookupGroup(groupname string) (*Group, error) {
+ f, err := os.Open(groupFile)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+ return findGroupName(groupname, f)
+}
+
+func lookupGroupId(id string) (*Group, error) {
+ f, err := os.Open(groupFile)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+ return findGroupId(id, f)
+}
+
+func lookupUser(username string) (*User, error) {
+ f, err := os.Open(userFile)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+ return findUsername(username, f)
+}
+
+func lookupUserId(uid string) (*User, error) {
+ f, err := os.Open(userFile)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+ return findUserId(uid, f)
+}
diff --git a/src/os/user/lookup_unix_test.go b/src/os/user/lookup_unix_test.go
new file mode 100644
index 0000000..7791767
--- /dev/null
+++ b/src/os/user/lookup_unix_test.go
@@ -0,0 +1,259 @@
+// Copyright 2016 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build (aix || darwin || dragonfly || freebsd || (!android && linux) || netbsd || openbsd || solaris) && !cgo
+
+package user
+
+import (
+ "reflect"
+ "strings"
+ "testing"
+)
+
+var groupTests = []struct {
+ in string
+ name string
+ gid string
+}{
+ {testGroupFile, "nobody", "-2"},
+ {testGroupFile, "kmem", "2"},
+ {testGroupFile, "notinthefile", ""},
+ {testGroupFile, "comment", ""},
+ {testGroupFile, "plussign", ""},
+ {testGroupFile, "+plussign", ""},
+ {testGroupFile, "-minussign", ""},
+ {testGroupFile, "minussign", ""},
+ {testGroupFile, "emptyid", ""},
+ {testGroupFile, "invalidgid", ""},
+ {testGroupFile, "indented", "7"},
+ {testGroupFile, "# comment", ""},
+ {testGroupFile, "largegroup", "1000"},
+ {testGroupFile, "manymembers", "777"},
+ {"", "emptyfile", ""},
+}
+
+func TestFindGroupName(t *testing.T) {
+ for _, tt := range groupTests {
+ got, err := findGroupName(tt.name, strings.NewReader(tt.in))
+ if tt.gid == "" {
+ if err == nil {
+ t.Errorf("findGroupName(%s): got nil error, expected err", tt.name)
+ continue
+ }
+ switch terr := err.(type) {
+ case UnknownGroupError:
+ if terr.Error() != "group: unknown group "+tt.name {
+ t.Errorf("findGroupName(%s): got %v, want %v", tt.name, terr, tt.name)
+ }
+ default:
+ t.Errorf("findGroupName(%s): got unexpected error %v", tt.name, terr)
+ }
+ } else {
+ if err != nil {
+ t.Fatalf("findGroupName(%s): got unexpected error %v", tt.name, err)
+ }
+ if got.Gid != tt.gid {
+ t.Errorf("findGroupName(%s): got gid %v, want %s", tt.name, got.Gid, tt.gid)
+ }
+ if got.Name != tt.name {
+ t.Errorf("findGroupName(%s): got name %s, want %s", tt.name, got.Name, tt.name)
+ }
+ }
+ }
+}
+
+var groupIdTests = []struct {
+ in string
+ gid string
+ name string
+}{
+ {testGroupFile, "-2", "nobody"},
+ {testGroupFile, "2", "kmem"},
+ {testGroupFile, "notinthefile", ""},
+ {testGroupFile, "comment", ""},
+ {testGroupFile, "7", "indented"},
+ {testGroupFile, "4", ""},
+ {testGroupFile, "20", ""}, // row starts with a plus
+ {testGroupFile, "21", ""}, // row starts with a minus
+ {"", "emptyfile", ""},
+}
+
+func TestFindGroupId(t *testing.T) {
+ for _, tt := range groupIdTests {
+ got, err := findGroupId(tt.gid, strings.NewReader(tt.in))
+ if tt.name == "" {
+ if err == nil {
+ t.Errorf("findGroupId(%s): got nil error, expected err", tt.gid)
+ continue
+ }
+ switch terr := err.(type) {
+ case UnknownGroupIdError:
+ if terr.Error() != "group: unknown groupid "+tt.gid {
+ t.Errorf("findGroupId(%s): got %v, want %v", tt.name, terr, tt.name)
+ }
+ default:
+ t.Errorf("findGroupId(%s): got unexpected error %v", tt.name, terr)
+ }
+ } else {
+ if err != nil {
+ t.Fatalf("findGroupId(%s): got unexpected error %v", tt.name, err)
+ }
+ if got.Gid != tt.gid {
+ t.Errorf("findGroupId(%s): got gid %v, want %s", tt.name, got.Gid, tt.gid)
+ }
+ if got.Name != tt.name {
+ t.Errorf("findGroupId(%s): got name %s, want %s", tt.name, got.Name, tt.name)
+ }
+ }
+ }
+}
+
+const testUserFile = ` # Example user file
+root:x:0:0:root:/root:/bin/bash
+daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
+bin:x:2:3:bin:/bin:/usr/sbin/nologin
+ indented:x:3:3:indented:/dev:/usr/sbin/nologin
+sync:x:4:65534:sync:/bin:/bin/sync
+negative:x:-5:60:games:/usr/games:/usr/sbin/nologin
+man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
+allfields:x:6:12:mansplit,man2,man3,man4:/home/allfields:/usr/sbin/nologin
++plussign:x:8:10:man:/var/cache/man:/usr/sbin/nologin
+-minussign:x:9:10:man:/var/cache/man:/usr/sbin/nologin
+
+malformed:x:27:12 # more:colons:after:comment
+
+struid:x:notanumber:12 # more:colons:after:comment
+
+# commented:x:28:12:commented:/var/cache/man:/usr/sbin/nologin
+ # commentindented:x:29:12:commentindented:/var/cache/man:/usr/sbin/nologin
+
+struid2:x:30:badgid:struid2name:/home/struid:/usr/sbin/nologin
+`
+
+var userIdTests = []struct {
+ in string
+ uid string
+ name string
+}{
+ {testUserFile, "-5", "negative"},
+ {testUserFile, "2", "bin"},
+ {testUserFile, "100", ""}, // not in the file
+ {testUserFile, "8", ""}, // plus sign, glibc doesn't find it
+ {testUserFile, "9", ""}, // minus sign, glibc doesn't find it
+ {testUserFile, "27", ""}, // malformed
+ {testUserFile, "28", ""}, // commented out
+ {testUserFile, "29", ""}, // commented out, indented
+ {testUserFile, "3", "indented"},
+ {testUserFile, "30", ""}, // the Gid is not valid, shouldn't match
+ {"", "1", ""},
+}
+
+func TestInvalidUserId(t *testing.T) {
+ _, err := findUserId("notanumber", strings.NewReader(""))
+ if err == nil {
+ t.Fatalf("findUserId('notanumber'): got nil error")
+ }
+ if want := "user: invalid userid notanumber"; err.Error() != want {
+ t.Errorf("findUserId('notanumber'): got %v, want %s", err, want)
+ }
+}
+
+func TestLookupUserId(t *testing.T) {
+ for _, tt := range userIdTests {
+ got, err := findUserId(tt.uid, strings.NewReader(tt.in))
+ if tt.name == "" {
+ if err == nil {
+ t.Errorf("findUserId(%s): got nil error, expected err", tt.uid)
+ continue
+ }
+ switch terr := err.(type) {
+ case UnknownUserIdError:
+ if want := "user: unknown userid " + tt.uid; terr.Error() != want {
+ t.Errorf("findUserId(%s): got %v, want %v", tt.name, terr, want)
+ }
+ default:
+ t.Errorf("findUserId(%s): got unexpected error %v", tt.name, terr)
+ }
+ } else {
+ if err != nil {
+ t.Fatalf("findUserId(%s): got unexpected error %v", tt.name, err)
+ }
+ if got.Uid != tt.uid {
+ t.Errorf("findUserId(%s): got uid %v, want %s", tt.name, got.Uid, tt.uid)
+ }
+ if got.Username != tt.name {
+ t.Errorf("findUserId(%s): got name %s, want %s", tt.name, got.Username, tt.name)
+ }
+ }
+ }
+}
+
+func TestLookupUserPopulatesAllFields(t *testing.T) {
+ u, err := findUsername("allfields", strings.NewReader(testUserFile))
+ if err != nil {
+ t.Fatal(err)
+ }
+ want := &User{
+ Username: "allfields",
+ Uid: "6",
+ Gid: "12",
+ Name: "mansplit",
+ HomeDir: "/home/allfields",
+ }
+ if !reflect.DeepEqual(u, want) {
+ t.Errorf("findUsername: got %#v, want %#v", u, want)
+ }
+}
+
+var userTests = []struct {
+ in string
+ name string
+ uid string
+}{
+ {testUserFile, "negative", "-5"},
+ {testUserFile, "bin", "2"},
+ {testUserFile, "notinthefile", ""},
+ {testUserFile, "indented", "3"},
+ {testUserFile, "plussign", ""},
+ {testUserFile, "+plussign", ""},
+ {testUserFile, "minussign", ""},
+ {testUserFile, "-minussign", ""},
+ {testUserFile, " indented", ""},
+ {testUserFile, "commented", ""},
+ {testUserFile, "commentindented", ""},
+ {testUserFile, "malformed", ""},
+ {testUserFile, "# commented", ""},
+ {"", "emptyfile", ""},
+}
+
+func TestLookupUser(t *testing.T) {
+ for _, tt := range userTests {
+ got, err := findUsername(tt.name, strings.NewReader(tt.in))
+ if tt.uid == "" {
+ if err == nil {
+ t.Errorf("lookupUser(%s): got nil error, expected err", tt.uid)
+ continue
+ }
+ switch terr := err.(type) {
+ case UnknownUserError:
+ if want := "user: unknown user " + tt.name; terr.Error() != want {
+ t.Errorf("lookupUser(%s): got %v, want %v", tt.name, terr, want)
+ }
+ default:
+ t.Errorf("lookupUser(%s): got unexpected error %v", tt.name, terr)
+ }
+ } else {
+ if err != nil {
+ t.Fatalf("lookupUser(%s): got unexpected error %v", tt.name, err)
+ }
+ if got.Uid != tt.uid {
+ t.Errorf("lookupUser(%s): got uid %v, want %s", tt.name, got.Uid, tt.uid)
+ }
+ if got.Username != tt.name {
+ t.Errorf("lookupUser(%s): got name %s, want %s", tt.name, got.Username, tt.name)
+ }
+ }
+ }
+}
diff --git a/src/os/user/lookup_windows.go b/src/os/user/lookup_windows.go
new file mode 100644
index 0000000..f65773c
--- /dev/null
+++ b/src/os/user/lookup_windows.go
@@ -0,0 +1,385 @@
+// Copyright 2012 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package user
+
+import (
+ "fmt"
+ "internal/syscall/windows"
+ "internal/syscall/windows/registry"
+ "syscall"
+ "unsafe"
+)
+
+func isDomainJoined() (bool, error) {
+ var domain *uint16
+ var status uint32
+ err := syscall.NetGetJoinInformation(nil, &domain, &status)
+ if err != nil {
+ return false, err
+ }
+ syscall.NetApiBufferFree((*byte)(unsafe.Pointer(domain)))
+ return status == syscall.NetSetupDomainName, nil
+}
+
+func lookupFullNameDomain(domainAndUser string) (string, error) {
+ return syscall.TranslateAccountName(domainAndUser,
+ syscall.NameSamCompatible, syscall.NameDisplay, 50)
+}
+
+func lookupFullNameServer(servername, username string) (string, error) {
+ s, e := syscall.UTF16PtrFromString(servername)
+ if e != nil {
+ return "", e
+ }
+ u, e := syscall.UTF16PtrFromString(username)
+ if e != nil {
+ return "", e
+ }
+ var p *byte
+ e = syscall.NetUserGetInfo(s, u, 10, &p)
+ if e != nil {
+ return "", e
+ }
+ defer syscall.NetApiBufferFree(p)
+ i := (*syscall.UserInfo10)(unsafe.Pointer(p))
+ return windows.UTF16PtrToString(i.FullName), nil
+}
+
+func lookupFullName(domain, username, domainAndUser string) (string, error) {
+ joined, err := isDomainJoined()
+ if err == nil && joined {
+ name, err := lookupFullNameDomain(domainAndUser)
+ if err == nil {
+ return name, nil
+ }
+ }
+ name, err := lookupFullNameServer(domain, username)
+ if err == nil {
+ return name, nil
+ }
+ // domain worked neither as a domain nor as a server
+ // could be domain server unavailable
+ // pretend username is fullname
+ return username, nil
+}
+
+// getProfilesDirectory retrieves the path to the root directory
+// where user profiles are stored.
+func getProfilesDirectory() (string, error) {
+ n := uint32(100)
+ for {
+ b := make([]uint16, n)
+ e := windows.GetProfilesDirectory(&b[0], &n)
+ if e == nil {
+ return syscall.UTF16ToString(b), nil
+ }
+ if e != syscall.ERROR_INSUFFICIENT_BUFFER {
+ return "", e
+ }
+ if n <= uint32(len(b)) {
+ return "", e
+ }
+ }
+}
+
+// lookupUsernameAndDomain obtains the username and domain for usid.
+func lookupUsernameAndDomain(usid *syscall.SID) (username, domain string, e error) {
+ username, domain, t, e := usid.LookupAccount("")
+ if e != nil {
+ return "", "", e
+ }
+ if t != syscall.SidTypeUser {
+ return "", "", fmt.Errorf("user: should be user account type, not %d", t)
+ }
+ return username, domain, nil
+}
+
+// findHomeDirInRegistry finds the user home path based on the uid.
+func findHomeDirInRegistry(uid string) (dir string, e error) {
+ k, e := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\`+uid, registry.QUERY_VALUE)
+ if e != nil {
+ return "", e
+ }
+ defer k.Close()
+ dir, _, e = k.GetStringValue("ProfileImagePath")
+ if e != nil {
+ return "", e
+ }
+ return dir, nil
+}
+
+// lookupGroupName accepts the name of a group and retrieves the group SID.
+func lookupGroupName(groupname string) (string, error) {
+ sid, _, t, e := syscall.LookupSID("", groupname)
+ if e != nil {
+ return "", e
+ }
+ // https://msdn.microsoft.com/en-us/library/cc245478.aspx#gt_0387e636-5654-4910-9519-1f8326cf5ec0
+ // SidTypeAlias should also be treated as a group type next to SidTypeGroup
+ // and SidTypeWellKnownGroup:
+ // "alias object -> resource group: A group object..."
+ //
+ // Tests show that "Administrators" can be considered of type SidTypeAlias.
+ if t != syscall.SidTypeGroup && t != syscall.SidTypeWellKnownGroup && t != syscall.SidTypeAlias {
+ return "", fmt.Errorf("lookupGroupName: should be group account type, not %d", t)
+ }
+ return sid.String()
+}
+
+// listGroupsForUsernameAndDomain accepts username and domain and retrieves
+// a SID list of the local groups where this user is a member.
+func listGroupsForUsernameAndDomain(username, domain string) ([]string, error) {
+ // Check if both the domain name and user should be used.
+ var query string
+ joined, err := isDomainJoined()
+ if err == nil && joined && len(domain) != 0 {
+ query = domain + `\` + username
+ } else {
+ query = username
+ }
+ q, err := syscall.UTF16PtrFromString(query)
+ if err != nil {
+ return nil, err
+ }
+ var p0 *byte
+ var entriesRead, totalEntries uint32
+ // https://msdn.microsoft.com/en-us/library/windows/desktop/aa370655(v=vs.85).aspx
+ // NetUserGetLocalGroups() would return a list of LocalGroupUserInfo0
+ // elements which hold the names of local groups where the user participates.
+ // The list does not follow any sorting order.
+ //
+ // If no groups can be found for this user, NetUserGetLocalGroups() should
+ // always return the SID of a single group called "None", which
+ // also happens to be the primary group for the local user.
+ err = windows.NetUserGetLocalGroups(nil, q, 0, windows.LG_INCLUDE_INDIRECT, &p0, windows.MAX_PREFERRED_LENGTH, &entriesRead, &totalEntries)
+ if err != nil {
+ return nil, err
+ }
+ defer syscall.NetApiBufferFree(p0)
+ if entriesRead == 0 {
+ return nil, fmt.Errorf("listGroupsForUsernameAndDomain: NetUserGetLocalGroups() returned an empty list for domain: %s, username: %s", domain, username)
+ }
+ entries := (*[1024]windows.LocalGroupUserInfo0)(unsafe.Pointer(p0))[:entriesRead:entriesRead]
+ var sids []string
+ for _, entry := range entries {
+ if entry.Name == nil {
+ continue
+ }
+ sid, err := lookupGroupName(windows.UTF16PtrToString(entry.Name))
+ if err != nil {
+ return nil, err
+ }
+ sids = append(sids, sid)
+ }
+ return sids, nil
+}
+
+func newUser(uid, gid, dir, username, domain string) (*User, error) {
+ domainAndUser := domain + `\` + username
+ name, e := lookupFullName(domain, username, domainAndUser)
+ if e != nil {
+ return nil, e
+ }
+ u := &User{
+ Uid: uid,
+ Gid: gid,
+ Username: domainAndUser,
+ Name: name,
+ HomeDir: dir,
+ }
+ return u, nil
+}
+
+func current() (*User, error) {
+ t, e := syscall.OpenCurrentProcessToken()
+ if e != nil {
+ return nil, e
+ }
+ defer t.Close()
+ u, e := t.GetTokenUser()
+ if e != nil {
+ return nil, e
+ }
+ pg, e := t.GetTokenPrimaryGroup()
+ if e != nil {
+ return nil, e
+ }
+ uid, e := u.User.Sid.String()
+ if e != nil {
+ return nil, e
+ }
+ gid, e := pg.PrimaryGroup.String()
+ if e != nil {
+ return nil, e
+ }
+ dir, e := t.GetUserProfileDirectory()
+ if e != nil {
+ return nil, e
+ }
+ username, domain, e := lookupUsernameAndDomain(u.User.Sid)
+ if e != nil {
+ return nil, e
+ }
+ return newUser(uid, gid, dir, username, domain)
+}
+
+// lookupUserPrimaryGroup obtains the primary group SID for a user using this method:
+// https://support.microsoft.com/en-us/help/297951/how-to-use-the-primarygroupid-attribute-to-find-the-primary-group-for
+// The method follows this formula: domainRID + "-" + primaryGroupRID
+func lookupUserPrimaryGroup(username, domain string) (string, error) {
+ // get the domain RID
+ sid, _, t, e := syscall.LookupSID("", domain)
+ if e != nil {
+ return "", e
+ }
+ if t != syscall.SidTypeDomain {
+ return "", fmt.Errorf("lookupUserPrimaryGroup: should be domain account type, not %d", t)
+ }
+ domainRID, e := sid.String()
+ if e != nil {
+ return "", e
+ }
+ // If the user has joined a domain use the RID of the default primary group
+ // called "Domain Users":
+ // https://support.microsoft.com/en-us/help/243330/well-known-security-identifiers-in-windows-operating-systems
+ // SID: S-1-5-21domain-513
+ //
+ // The correct way to obtain the primary group of a domain user is
+ // probing the user primaryGroupID attribute in the server Active Directory:
+ // https://msdn.microsoft.com/en-us/library/ms679375(v=vs.85).aspx
+ //
+ // Note that the primary group of domain users should not be modified
+ // on Windows for performance reasons, even if it's possible to do that.
+ // The .NET Developer's Guide to Directory Services Programming - Page 409
+ // https://books.google.bg/books?id=kGApqjobEfsC&lpg=PA410&ots=p7oo-eOQL7&dq=primary%20group%20RID&hl=bg&pg=PA409#v=onepage&q&f=false
+ joined, err := isDomainJoined()
+ if err == nil && joined {
+ return domainRID + "-513", nil
+ }
+ // For non-domain users call NetUserGetInfo() with level 4, which
+ // in this case would not have any network overhead.
+ // The primary group should not change from RID 513 here either
+ // but the group will be called "None" instead:
+ // https://www.adampalmer.me/iodigitalsec/2013/08/10/windows-null-session-enumeration/
+ // "Group 'None' (RID: 513)"
+ u, e := syscall.UTF16PtrFromString(username)
+ if e != nil {
+ return "", e
+ }
+ d, e := syscall.UTF16PtrFromString(domain)
+ if e != nil {
+ return "", e
+ }
+ var p *byte
+ e = syscall.NetUserGetInfo(d, u, 4, &p)
+ if e != nil {
+ return "", e
+ }
+ defer syscall.NetApiBufferFree(p)
+ i := (*windows.UserInfo4)(unsafe.Pointer(p))
+ return fmt.Sprintf("%s-%d", domainRID, i.PrimaryGroupID), nil
+}
+
+func newUserFromSid(usid *syscall.SID) (*User, error) {
+ username, domain, e := lookupUsernameAndDomain(usid)
+ if e != nil {
+ return nil, e
+ }
+ gid, e := lookupUserPrimaryGroup(username, domain)
+ if e != nil {
+ return nil, e
+ }
+ uid, e := usid.String()
+ if e != nil {
+ return nil, e
+ }
+ // If this user has logged in at least once their home path should be stored
+ // in the registry under the specified SID. References:
+ // https://social.technet.microsoft.com/wiki/contents/articles/13895.how-to-remove-a-corrupted-user-profile-from-the-registry.aspx
+ // https://support.asperasoft.com/hc/en-us/articles/216127438-How-to-delete-Windows-user-profiles
+ //
+ // The registry is the most reliable way to find the home path as the user
+ // might have decided to move it outside of the default location,
+ // (e.g. C:\users). Reference:
+ // https://answers.microsoft.com/en-us/windows/forum/windows_7-security/how-do-i-set-a-home-directory-outside-cusers-for-a/aed68262-1bf4-4a4d-93dc-7495193a440f
+ dir, e := findHomeDirInRegistry(uid)
+ if e != nil {
+ // If the home path does not exist in the registry, the user might
+ // have not logged in yet; fall back to using getProfilesDirectory().
+ // Find the username based on a SID and append that to the result of
+ // getProfilesDirectory(). The domain is not relevant here.
+ dir, e = getProfilesDirectory()
+ if e != nil {
+ return nil, e
+ }
+ dir += `\` + username
+ }
+ return newUser(uid, gid, dir, username, domain)
+}
+
+func lookupUser(username string) (*User, error) {
+ sid, _, t, e := syscall.LookupSID("", username)
+ if e != nil {
+ return nil, e
+ }
+ if t != syscall.SidTypeUser {
+ return nil, fmt.Errorf("user: should be user account type, not %d", t)
+ }
+ return newUserFromSid(sid)
+}
+
+func lookupUserId(uid string) (*User, error) {
+ sid, e := syscall.StringToSid(uid)
+ if e != nil {
+ return nil, e
+ }
+ return newUserFromSid(sid)
+}
+
+func lookupGroup(groupname string) (*Group, error) {
+ sid, err := lookupGroupName(groupname)
+ if err != nil {
+ return nil, err
+ }
+ return &Group{Name: groupname, Gid: sid}, nil
+}
+
+func lookupGroupId(gid string) (*Group, error) {
+ sid, err := syscall.StringToSid(gid)
+ if err != nil {
+ return nil, err
+ }
+ groupname, _, t, err := sid.LookupAccount("")
+ if err != nil {
+ return nil, err
+ }
+ if t != syscall.SidTypeGroup && t != syscall.SidTypeWellKnownGroup && t != syscall.SidTypeAlias {
+ return nil, fmt.Errorf("lookupGroupId: should be group account type, not %d", t)
+ }
+ return &Group{Name: groupname, Gid: gid}, nil
+}
+
+func listGroups(user *User) ([]string, error) {
+ sid, err := syscall.StringToSid(user.Uid)
+ if err != nil {
+ return nil, err
+ }
+ username, domain, err := lookupUsernameAndDomain(sid)
+ if err != nil {
+ return nil, err
+ }
+ sids, err := listGroupsForUsernameAndDomain(username, domain)
+ if err != nil {
+ return nil, err
+ }
+ // Add the primary group of the user to the list if it is not already there.
+ // This is done only to comply with the POSIX concept of a primary group.
+ for _, sid := range sids {
+ if sid == user.Gid {
+ return sids, nil
+ }
+ }
+ return append(sids, user.Gid), nil
+}
diff --git a/src/os/user/user.go b/src/os/user/user.go
new file mode 100644
index 0000000..0307d2a
--- /dev/null
+++ b/src/os/user/user.go
@@ -0,0 +1,95 @@
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+/*
+Package user allows user account lookups by name or id.
+
+For most Unix systems, this package has two internal implementations of
+resolving user and group ids to names, and listing supplementary group IDs.
+One is written in pure Go and parses /etc/passwd and /etc/group. The other
+is cgo-based and relies on the standard C library (libc) routines such as
+getpwuid_r, getgrnam_r, and getgrouplist.
+
+When cgo is available, and the required routines are implemented in libc
+for a particular platform, cgo-based (libc-backed) code is used.
+This can be overridden by using osusergo build tag, which enforces
+the pure Go implementation.
+*/
+package user
+
+import (
+ "strconv"
+)
+
+// These may be set to false in init() for a particular platform and/or
+// build flags to let the tests know to skip tests of some features.
+var (
+ userImplemented = true
+ groupImplemented = true
+ groupListImplemented = true
+)
+
+// User represents a user account.
+type User struct {
+ // Uid is the user ID.
+ // On POSIX systems, this is a decimal number representing the uid.
+ // On Windows, this is a security identifier (SID) in a string format.
+ // On Plan 9, this is the contents of /dev/user.
+ Uid string
+ // Gid is the primary group ID.
+ // On POSIX systems, this is a decimal number representing the gid.
+ // On Windows, this is a SID in a string format.
+ // On Plan 9, this is the contents of /dev/user.
+ Gid string
+ // Username is the login name.
+ Username string
+ // Name is the user's real or display name.
+ // It might be blank.
+ // On POSIX systems, this is the first (or only) entry in the GECOS field
+ // list.
+ // On Windows, this is the user's display name.
+ // On Plan 9, this is the contents of /dev/user.
+ Name string
+ // HomeDir is the path to the user's home directory (if they have one).
+ HomeDir string
+}
+
+// Group represents a grouping of users.
+//
+// On POSIX systems Gid contains a decimal number representing the group ID.
+type Group struct {
+ Gid string // group ID
+ Name string // group name
+}
+
+// UnknownUserIdError is returned by LookupId when a user cannot be found.
+type UnknownUserIdError int
+
+func (e UnknownUserIdError) Error() string {
+ return "user: unknown userid " + strconv.Itoa(int(e))
+}
+
+// UnknownUserError is returned by Lookup when
+// a user cannot be found.
+type UnknownUserError string
+
+func (e UnknownUserError) Error() string {
+ return "user: unknown user " + string(e)
+}
+
+// UnknownGroupIdError is returned by LookupGroupId when
+// a group cannot be found.
+type UnknownGroupIdError string
+
+func (e UnknownGroupIdError) Error() string {
+ return "group: unknown groupid " + string(e)
+}
+
+// UnknownGroupError is returned by LookupGroup when
+// a group cannot be found.
+type UnknownGroupError string
+
+func (e UnknownGroupError) Error() string {
+ return "group: unknown group " + string(e)
+}
diff --git a/src/os/user/user_test.go b/src/os/user/user_test.go
new file mode 100644
index 0000000..8025174
--- /dev/null
+++ b/src/os/user/user_test.go
@@ -0,0 +1,150 @@
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package user
+
+import (
+ "testing"
+)
+
+func checkUser(t *testing.T) {
+ t.Helper()
+ if !userImplemented {
+ t.Skip("user: not implemented; skipping tests")
+ }
+}
+
+func TestCurrent(t *testing.T) {
+ u, err := Current()
+ if err != nil {
+ t.Fatalf("Current: %v (got %#v)", err, u)
+ }
+ if u.HomeDir == "" {
+ t.Errorf("didn't get a HomeDir")
+ }
+ if u.Username == "" {
+ t.Errorf("didn't get a username")
+ }
+}
+
+func BenchmarkCurrent(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ Current()
+ }
+}
+
+func compare(t *testing.T, want, got *User) {
+ if want.Uid != got.Uid {
+ t.Errorf("got Uid=%q; want %q", got.Uid, want.Uid)
+ }
+ if want.Username != got.Username {
+ t.Errorf("got Username=%q; want %q", got.Username, want.Username)
+ }
+ if want.Name != got.Name {
+ t.Errorf("got Name=%q; want %q", got.Name, want.Name)
+ }
+ if want.HomeDir != got.HomeDir {
+ t.Errorf("got HomeDir=%q; want %q", got.HomeDir, want.HomeDir)
+ }
+ if want.Gid != got.Gid {
+ t.Errorf("got Gid=%q; want %q", got.Gid, want.Gid)
+ }
+}
+
+func TestLookup(t *testing.T) {
+ checkUser(t)
+
+ want, err := Current()
+ if err != nil {
+ t.Fatalf("Current: %v", err)
+ }
+ // TODO: Lookup() has a fast path that calls Current() and returns if the
+ // usernames match, so this test does not exercise very much. It would be
+ // good to try and test finding a different user than the current user.
+ got, err := Lookup(want.Username)
+ if err != nil {
+ t.Fatalf("Lookup: %v", err)
+ }
+ compare(t, want, got)
+}
+
+func TestLookupId(t *testing.T) {
+ checkUser(t)
+
+ want, err := Current()
+ if err != nil {
+ t.Fatalf("Current: %v", err)
+ }
+ got, err := LookupId(want.Uid)
+ if err != nil {
+ t.Fatalf("LookupId: %v", err)
+ }
+ compare(t, want, got)
+}
+
+func checkGroup(t *testing.T) {
+ t.Helper()
+ if !groupImplemented {
+ t.Skip("user: group not implemented; skipping test")
+ }
+}
+
+func TestLookupGroup(t *testing.T) {
+ checkGroup(t)
+ user, err := Current()
+ if err != nil {
+ t.Fatalf("Current(): %v", err)
+ }
+
+ g1, err := LookupGroupId(user.Gid)
+ if err != nil {
+ // NOTE(rsc): Maybe the group isn't defined. That's fine.
+ // On my OS X laptop, rsc logs in with group 5000 even
+ // though there's no name for group 5000. Such is Unix.
+ t.Logf("LookupGroupId(%q): %v", user.Gid, err)
+ return
+ }
+ if g1.Gid != user.Gid {
+ t.Errorf("LookupGroupId(%q).Gid = %s; want %s", user.Gid, g1.Gid, user.Gid)
+ }
+
+ g2, err := LookupGroup(g1.Name)
+ if err != nil {
+ t.Fatalf("LookupGroup(%q): %v", g1.Name, err)
+ }
+ if g1.Gid != g2.Gid || g1.Name != g2.Name {
+ t.Errorf("LookupGroup(%q) = %+v; want %+v", g1.Name, g2, g1)
+ }
+}
+
+func checkGroupList(t *testing.T) {
+ t.Helper()
+ if !groupListImplemented {
+ t.Skip("user: group list not implemented; skipping test")
+ }
+}
+
+func TestGroupIds(t *testing.T) {
+ checkGroupList(t)
+ user, err := Current()
+ if err != nil {
+ t.Fatalf("Current(): %v", err)
+ }
+ gids, err := user.GroupIds()
+ if err != nil {
+ t.Fatalf("%+v.GroupIds(): %v", user, err)
+ }
+ if !containsID(gids, user.Gid) {
+ t.Errorf("%+v.GroupIds() = %v; does not contain user GID %s", user, gids, user.Gid)
+ }
+}
+
+func containsID(ids []string, id string) bool {
+ for _, x := range ids {
+ if x == id {
+ return true
+ }
+ }
+ return false
+}