diff --git a/src/internal/syscall/windows/registry/key.go b/src/internal/syscall/windows/registry/key.go index ebe73a2e024683..45b10c64ac2541 100644 --- a/src/internal/syscall/windows/registry/key.go +++ b/src/internal/syscall/windows/registry/key.go @@ -87,9 +87,9 @@ func OpenKey(k Key, path string, access uint32) (Key, error) { return Key(subkey), nil } -// ReadSubKeyNames returns the names of subkeys of key k. -func (k Key) ReadSubKeyNames() ([]string, error) { - names := make([]string, 0) +// ReadSubKeyNames iterates over the names of subkeys of key k. Callback function fn receives each iterated subkey name. +// If fn returns non-nil error, iteration is terminated and that error is returned. +func (k Key) ReadSubKeyNames(fn func(string) error) error { // Registry key size limit is 255 bytes and described there: // https://msdn.microsoft.com/library/windows/desktop/ms724872.aspx buf := make([]uint16, 256) //plus extra room for terminating zero byte @@ -110,11 +110,15 @@ loopItems: if err == _ERROR_NO_MORE_ITEMS { break loopItems } - return names, err + return err + } + + // Callback with key name string + if err := fn(syscall.UTF16ToString(buf[:l])); err != nil{ + return err } - names = append(names, syscall.UTF16ToString(buf[:l])) } - return names, nil + return nil } // CreateKey creates a key named path under open key k. diff --git a/src/internal/syscall/windows/registry/registry_test.go b/src/internal/syscall/windows/registry/registry_test.go index 69b84e1c4c5f99..37bf0ebb354716 100644 --- a/src/internal/syscall/windows/registry/registry_test.go +++ b/src/internal/syscall/windows/registry/registry_test.go @@ -35,7 +35,11 @@ func TestReadSubKeyNames(t *testing.T) { } defer k.Close() - names, err := k.ReadSubKeyNames() + var names []string + err = k.ReadSubKeyNames(func(s string) error { + names = append(names, s) + return nil + }) if err != nil { t.Fatal(err) } diff --git a/src/mime/type_windows.go b/src/mime/type_windows.go index cee9c9db041b2b..cbd8a068292393 100644 --- a/src/mime/type_windows.go +++ b/src/mime/type_windows.go @@ -13,10 +13,15 @@ func init() { } func initMimeWindows() { - names, err := registry.CLASSES_ROOT.ReadSubKeyNames() + var names []string + err := registry.CLASSES_ROOT.ReadSubKeyNames(func(s string) error { + names = append(names, s) + return nil + }) if err != nil { return } + for _, name := range names { if len(name) < 2 || name[0] != '.' { // looking for extensions only continue diff --git a/src/os/user/iterate.go b/src/os/user/iterate.go new file mode 100644 index 00000000000000..59a98fc1d84ddd --- /dev/null +++ b/src/os/user/iterate.go @@ -0,0 +1,25 @@ +package user + +// NextUserFunc is used in users iteration process. It receives *User for each user record. +// If non-nil error is returned from NextUserFunc - iteration process is terminated. +type NextUserFunc func(*User) error + +// NextGroupFunc is used in groups iteration process. It receives *Group for each group record. +// If non-nil error is returned from NextGroupFunc - iteration process is terminated. +type NextGroupFunc func(*Group) error + +// IterateUsers iterates over user entries. For each retrieved *User entry provided NextUserFunc is called. +// +// On UNIX, if CGO is enabled, getpwent(3) is used in the underlying implementation. Since getpwent(3) is not thread-safe, +// locking is strongly advised. +func IterateUsers(n NextUserFunc) error { + return iterateUsers(n) +} + +// IterateGroups iterates over group entries. For each retrieved *Group entry provided NextGroupFunc is called. +// +// On UNIX, if CGO is enabled, getgrent(3) is used in the underlying implementation. Since getgrent(3) is not thread-safe, +// locking is strongly advised. +func IterateGroups(n NextGroupFunc) error { + return iterateGroups(n) +} diff --git a/src/os/user/iterate_cgo.go b/src/os/user/iterate_cgo.go new file mode 100644 index 00000000000000..14601ee7cd76bf --- /dev/null +++ b/src/os/user/iterate_cgo.go @@ -0,0 +1,161 @@ +//go:build (aix || dragonfly || freebsd || (!android && linux) || netbsd || openbsd || solaris) && cgo && !osusergo +// +build aix dragonfly freebsd !android,linux netbsd openbsd solaris +// +build cgo +// +build !osusergo + +package user + +// On darwin, there seems to be some issues when using getpwent(3) +// and getgrent(3). Until the issues are fixed, it is not recommended +// relying on these libc library calls. As such, cgo version of +// users and groups iterators should be disabled on darwin. +// https://developer.apple.com/forums/thread/689613 + +/* +#include +#include +#include +#include +#include +#include +#include + +static void resetErrno(){ + errno = 0; +} +*/ +import "C" + +// usersHelper defines the methods used in users iteration process within +// iterateUsers. This interface allows testing iterateUsers functionality. +// iterate_test_fgetent.go file defines test related struct that implements +// usersHelper. +type usersHelper interface { + // set sets up internal state before iteration + set() + + // get sequentially returns a passwd structure which is later processed into *User entry + get() (*C.struct_passwd, error) + + // end cleans up internal state after iteration is done + end() +} + +type iterateUsersHelper struct{} + +func (i iterateUsersHelper) set() { + C.setpwent() +} + +func (i iterateUsersHelper) get() (*C.struct_passwd, error) { + var result *C.struct_passwd + result, err := C.getpwent() + return result, err +} + +func (i iterateUsersHelper) end() { + C.endpwent() +} + +// This helper is used to retrieve users via c library call. A global +// variable which implements usersHelper interface is needed in order to +// separate testing logic from production. Since cgo can not be used directly +// in tests, iterate_test_fgetent.go file provides iterateUsersHelperTest +// structure which implements usersHelper interface and can substitute +// default userIterator value. +var userIterator usersHelper = iterateUsersHelper{} + +// iterateUsers iterates over users database via getpwent(3). If fn returns non +// nil error, then iteration is terminated. A nil result from getpwent means +// there were no more entries, or an error occurred, as such, iteration is +// terminated, and if error was encountered it is returned. +// +// Since iterateUsers uses getpwent(3), which is not thread safe, iterateUsers +// can not bet used concurrently. If concurrent usage is required, it is +// recommended to use locking mechanism such as sync.Mutex when calling +// iterateUsers from multiple goroutines. +func iterateUsers(fn NextUserFunc) error { + userIterator.set() + defer userIterator.end() + for { + var result *C.struct_passwd + C.resetErrno() + result, err := userIterator.get() + + // If result is nil - getpwent iterated through entire users database or there was an error + if result == nil { + return err + } + + if err = fn(buildUser(result)); err != nil { + // User provided non-nil error means that iteration should be terminated + return err + } + } +} + +// groupsHelper defines the methods used in groups iteration process within iterateGroups. This interface allows testing +// iterateGroups functionality. iterate_test_fgetent.go file defines test related struct that implements groupsHelper. +type groupsHelper interface { + // set sets up internal state before iteration + set() + + // get sequentially returns a group structure which is later processed into *Group entry + get() (*C.struct_group, error) + + // end cleans up internal state after iteration is done + end() +} + +type iterateGroupsHelper struct{} + +func (i iterateGroupsHelper) set() { + C.setgrent() +} + +func (i iterateGroupsHelper) get() (*C.struct_group, error) { + var result *C.struct_group + result, err := C.getgrent() + return result, err +} + +func (i iterateGroupsHelper) end() { + C.endgrent() +} + +// This helper is used to retrieve groups via c library call. A global +// variable which implements groupsHelper interface is needed in order to +// separate testing logic from production. Since cgo can not be used directly +// in tests, iterate_test_fgetent.go file provides iterateGroupsHelperTest +// structure which implements groupsHelper interface and can substitute +// default groupIterator value. +var groupIterator groupsHelper = iterateGroupsHelper{} + +// iterateGroups iterates over groups database via getgrent(3). If fn returns +// non nil error, then iteration is terminated. A nil result from getgrent means +// there were no more entries, or an error occurred, as such, iteration is +// terminated, and if error was encountered it is returned. +// +// Since iterateGroups uses getgrent(3), which is not thread safe, iterateGroups +// can not bet used concurrently. If concurrent usage is required, it is +// recommended to use locking mechanism such as sync.Mutex when calling +// iterateGroups from multiple goroutines. +func iterateGroups(fn NextGroupFunc) error { + groupIterator.set() + defer groupIterator.end() + for { + var result *C.struct_group + C.resetErrno() + result, err := groupIterator.get() + + // If result is nil - getgrent iterated through entire groups database or there was an error + if result == nil { + return err + } + + if err = fn(buildGroup(result)); err != nil { + // User provided non-nil error means that iteration should be terminated + return err + } + } +} diff --git a/src/os/user/iterate_cgo_bsd_windows_test.go b/src/os/user/iterate_cgo_bsd_windows_test.go new file mode 100644 index 00000000000000..a59fb8881751e4 --- /dev/null +++ b/src/os/user/iterate_cgo_bsd_windows_test.go @@ -0,0 +1,53 @@ +//go:build ((darwin || freebsd || openbsd || netbsd) && cgo && !osusergo) || windows + +package user + +import ( + "errors" + "testing" +) + +// As BSDs (including darwin) do not support fgetpwent(3)/fgetgrent(3), attempt +// to check if at least 1 user/group record can be retrieved. +// On Windows, it is not possible to easily mock registry. Checking if at +// least one user and group can be retrieved via iteration will suffice. + +var _stopErr = errors.New("terminate iteration") + +func TestIterateUser(t *testing.T) { + gotAtLeastOne := false + err := iterateUsers(func(user *User) error { + if *user == (User{}) { + t.Errorf("parsed user is empty: %+v", user) + } + gotAtLeastOne = true + return _stopErr + }) + + if err != _stopErr { + t.Errorf("iterating users: %w", err) + } + + if !gotAtLeastOne { + t.Errorf("no users were iterated") + } +} + +func TestIterateGroup(t *testing.T) { + gotAtLeastOne := false + err := iterateGroups(func(group *Group) error { + if *group == (Group{}) { + t.Errorf("parsed group is empty: %+v", group) + } + gotAtLeastOne = true + return _stopErr + }) + + if err != _stopErr { + t.Errorf("iterating groups: %w", err) + } + + if !gotAtLeastOne { + t.Errorf("no groups were iterated") + } +} diff --git a/src/os/user/iterate_cgo_test.go b/src/os/user/iterate_cgo_test.go new file mode 100644 index 00000000000000..54618e2aeee30b --- /dev/null +++ b/src/os/user/iterate_cgo_test.go @@ -0,0 +1,81 @@ +//go:build (aix || dragonfly || (!android && linux) || solaris) && cgo && !osusergo +// +build aix dragonfly !android,linux solaris +// +build cgo +// +build !osusergo + +package user + +import ( + "reflect" + "syscall" + "testing" +) + +// This file is used for testing cgo based unix implementation of users and +// groups iterators. Only unix based systems which support fgetpwent(3) and +// fgetgrent(3) can run tests from this file. + +func TestIterateUser(t *testing.T) { + var wantsUsers = []*User{ + {Uid: "0", Gid: "0", Username: "root", Name: "System Administrator", HomeDir: "/var/root"}, + {Uid: "1", Gid: "1", Username: "daemon", Name: "System Services", HomeDir: "/var/root"}, + {Uid: "4", Gid: "4", Username: "_uucp", Name: "Unix to Unix Copy Protocol", HomeDir: "/var/spool/uucp"}, + {Uid: "13", Gid: "13", Username: "_taskgated", Name: "Task Gate Daemon", HomeDir: "/var/empty"}, + {Uid: "24", Gid: "24", Username: "_networkd", Name: "Network Services", HomeDir: "/var/networkd"}, + {Uid: "25", Gid: "25", Username: "_installassistant", Name: "Install Assistant", HomeDir: "/var/empty"}, + {Uid: "26", Gid: "26", Username: "_lp", Name: "Printing Services", HomeDir: "/var/spool/cups"}, + {Uid: "27", Gid: "27", Username: "_postfix", Name: "Postfix Mail Server", HomeDir: "/var/spool/postfix"}, + } + + userIterator = &iterateUsersHelperTest{} + + // Test that users are retrieved in same order as defined + gotUsers := make([]*User, 0, len(wantsUsers)) + err := iterateUsers(func(user *User) error { + gotUsers = append(gotUsers, user) + return nil + }) + + if err != syscall.ENOENT { + t.Errorf("iterating users: %v", err) + } + + if !reflect.DeepEqual(wantsUsers, gotUsers) { + t.Errorf("iterate users result is incorrect: got: %+v, want: %+v", gotUsers, wantsUsers) + } +} + +func TestIterateGroup(t *testing.T) { + var wantsGroups = []*Group{ + {Gid: "0", Name: "wheel"}, + {Gid: "1", Name: "daemon"}, + {Gid: "2", Name: "kmem"}, + {Gid: "3", Name: "sys"}, + {Gid: "5", Name: "operator"}, + {Gid: "6", Name: "mail"}, + {Gid: "4", Name: "tty"}, + {Gid: "7", Name: "bin"}, + {Gid: "8", Name: "procview"}, + {Gid: "9", Name: "procmod"}, + {Gid: "10", Name: "owner"}, + {Gid: "12", Name: "everyone"}, + } + + // Use testdata fixture + groupIterator = &iterateGroupsHelperTest{} + + // Test that groups are retrieved in same order as defined + gotGroups := make([]*Group, 0, len(wantsGroups)) + err := iterateGroups(func(g *Group) error { + gotGroups = append(gotGroups, g) + return nil + }) + + if err != syscall.ENOENT { + t.Errorf("iterating groups: %v", err) + } + + if !reflect.DeepEqual(wantsGroups, gotGroups) { + t.Errorf("iterate groups result is incorrect: got: %+v, want: %+v", gotGroups, wantsGroups) + } +} diff --git a/src/os/user/iterate_example_test.go b/src/os/user/iterate_example_test.go new file mode 100644 index 00000000000000..1667ffceb43ee5 --- /dev/null +++ b/src/os/user/iterate_example_test.go @@ -0,0 +1,57 @@ +package user_test + +import ( + "errors" + "fmt" + "os/user" +) + +func ExampleIterateUsers() { + stopErr := errors.New("stop iterating") + // Get first 20 users + users := make([]*user.User, 0, 20) + i := 0 + err := user.IterateUsers(func(user *user.User) error { + users = append(users, user) + i++ + + // Once we return non-nil error - iteration process stops + if i >= 20 { + return stopErr + } + + // As long as error is nil, IterateUsers will iterate over users database + return nil + }) + + if err != stopErr && err != nil { + fmt.Printf("error encountered while iterating users database: %v", err) + } + + // Here users slice can be used to do something with collected users. +} + +func ExampleIterateGroups() { + stopErr := errors.New("stop iterating") + // Get first 20 groups + groups := make([]*user.Group, 0, 20) + i := 0 + err := user.IterateGroups(func(group *user.Group) error { + groups = append(groups, group) + i++ + + // Once we return non-nil error - iteration process stops + if i >= 20 { + return stopErr + } + + // As long as error is nil, IterateGroups will iterate over groups database + return nil + }) + + if err != stopErr && err != nil { + fmt.Printf("error encountered while iterating groups database: %v", err) + } + + // Here groups slice can be used to do something with collected groups. +} diff --git a/src/os/user/iterate_plan9.go b/src/os/user/iterate_plan9.go new file mode 100644 index 00000000000000..ebaf37dc12bdfa --- /dev/null +++ b/src/os/user/iterate_plan9.go @@ -0,0 +1,97 @@ +package user + +import ( + "bytes" + "fmt" + "os" + "strings" +) + +// Users and groups file location in plan9. Since, this value is mutated during +// testing, to a path to testdata, it is on purpose not a constant. +var usersFile = "/adm/users" + +// userGroupIterator is a helper iterator function, which parses /adm/users +func userGroupIterator(lineFn lineFunc) (err error) { + f, err := os.Open(usersFile) + if err != nil { + return fmt.Errorf("open users file: %w", err) + } + defer f.Close() + _, err = readColonFile(f, lineFn, 3) + return +} + +// parsePlan9UserGroup matches valid /adm/users line and provides colon split +// string slice to returnFn which can parse either *User or *Group. +// On plan9 /adm/users lines are both users and groups. +// +// Plan9 /adm/user line structure looks like this: +// id:name:leader:members +// sys:sys::glenda,mj <-- user/group without a leader, with 2 members glenda and mj +// mj:mj:: <-- user/group without a leader, without members +// +// According to plan 9 users(6): ids are arbitrary text strings, typically the same as name. +// In older Plan 9 file servers, ids are small decimal numbers. +func parsePlan9UserGroup(returnFn func([]string) interface{}) lineFunc { + return func(line []byte) (v interface{}, err error) { + if bytes.Count(line, []byte{':'}) < 3 { + return + } + // id:name:leader:members + // id can be negative (start with a "-" symbol) in plan 9. + parts := strings.SplitN(string(line), ":", 4) + if len(parts) < 4 || parts[0] == "" || + parts[0][0] == '+' { + return + } + + return returnFn(parts), nil + } +} + +// userReturnFn builds *User struct from provided parts +func userReturnFn(parts []string) interface{} { + return &User{ + Uid: parts[0], + Gid: parts[0], + Username: parts[1], + Name: parts[1], + + // There is no clear documentation which directory is set to homedir for user. + // However, when a new user is created, when user logs in, $HOME environment + // variable is set to /usr/ and this is also the login directory. + HomeDir: "/usr/" + parts[1], + } +} + +// groupReturnFn builds *Group struct from provided parts +func groupReturnFn(parts []string) interface{} { + return &Group{Name: parts[1], Gid: parts[0]} +} + +func iterateUsers(fn NextUserFunc) error { + return userGroupIterator(func(line []byte) (interface{}, error) { + v, _ := parsePlan9UserGroup(userReturnFn)(line) + if user, ok := v.(*User); ok { + err := fn(user) + if err != nil { + return nil, err + } + } + return nil, nil + }) +} + +func iterateGroups(fn NextGroupFunc) error { + return userGroupIterator(func(line []byte) (interface{}, error) { + v, _ := parsePlan9UserGroup(groupReturnFn)(line) + if group, ok := v.(*Group); ok { + err := fn(group) + if err != nil { + return nil, err + } + } + return nil, nil + }) +} diff --git a/src/os/user/iterate_plan9_test.go b/src/os/user/iterate_plan9_test.go new file mode 100644 index 00000000000000..3f43224a9c6686 --- /dev/null +++ b/src/os/user/iterate_plan9_test.go @@ -0,0 +1,134 @@ +package user + +import ( + "reflect" + "testing" +) + +var wantUsers = []*User{ + { + Uid: "-1", + Gid: "-1", + Username: "adm", + Name: "adm", + HomeDir: "/usr/adm", + }, + { + Uid: "0", + Gid: "0", + Username: "none", + Name: "none", + HomeDir: "/usr/none", + }, + { + Uid: "1", + Gid: "1", + Username: "tor", + Name: "tor", + HomeDir: "/usr/tor", + }, + { + Uid: "2", + Gid: "2", + Username: "glenda", + Name: "glenda", + HomeDir: "/usr/glenda", + }, + { + Uid: "9999", + Gid: "9999", + Username: "noworld", + Name: "noworld", + HomeDir: "/usr/noworld", + }, + { + Uid: "10000", + Gid: "10000", + Username: "sys", + Name: "sys", + HomeDir: "/usr/sys", + }, + { + Uid: "10001", + Gid: "10001", + Username: "upas", + Name: "upas", + HomeDir: "/usr/upas", + }, + { + Uid: "10002", + Gid: "10002", + Username: "bootes", + Name: "bootes", + HomeDir: "/usr/bootes", + }, + { + Uid: "test", + Gid: "test", + Username: "test", + Name: "test", + HomeDir: "/usr/test", + }, +} + +var wantGroups = []*Group{ + {Name: "adm", Gid: "-1"}, + {Name: "none", Gid: "0"}, + {Name: "tor", Gid: "1"}, + {Name: "glenda", Gid: "2"}, + {Name: "noworld", Gid: "9999"}, + {Name: "sys", Gid: "10000"}, + {Name: "upas", Gid: "10001"}, + {Name: "bootes", Gid: "10002"}, + {Name: "test", Gid: "test"}, +} + +const testUsersFile = "./testdata/plan9/users.txt" + +func TestIterateUsers(t *testing.T) { + saveTestUsersFile := usersFile + defer func() { + usersFile = saveTestUsersFile + }() + + usersFile = testUsersFile + + gotUsers := make([]*User, 0, len(wantUsers)) + + err := iterateUsers(func(user *User) error { + gotUsers = append(gotUsers, user) + return nil + }) + + if err != nil { + t.Error(err) + } + + if !reflect.DeepEqual(wantUsers, gotUsers) { + t.Errorf("iterate users result is incorrect: got: %+v, want: %+v", gotUsers, wantUsers) + } +} + +func TestIterateGroups(t *testing.T) { + saveTestUsersFile := usersFile + defer func() { + usersFile = saveTestUsersFile + }() + + usersFile = testUsersFile + + gotGroups := make([]*Group, 0, len(wantGroups)) + + err := iterateGroups(func(groups *Group) error { + gotGroups = append(gotGroups, groups) + return nil + }) + + if err != nil { + t.Error(err) + } + + if !reflect.DeepEqual(wantGroups, gotGroups) { + t.Errorf("iterate groups result is incorrect: got: %+v, want: %+v", gotGroups, wantGroups) + } +} diff --git a/src/os/user/iterate_test_fgetent.go b/src/os/user/iterate_test_fgetent.go new file mode 100644 index 00000000000000..1bd0bcda99426e --- /dev/null +++ b/src/os/user/iterate_test_fgetent.go @@ -0,0 +1,102 @@ +//go:build (aix || dragonfly || (!android && linux) || solaris) && cgo && !osusergo +// +build aix dragonfly !android,linux solaris +// +build cgo +// +build !osusergo + +package user + +/* +#include +#include +#include +#include +#include +#include +#include + +static void resetErrno(){ + errno = 0; +} + +static FILE* openUsersFile(){ + FILE* fp; + fp = fopen("./testdata/users.txt", "r"); + return fp; +} + +static FILE* openGroupsFile(){ + FILE* fp; + fp = fopen("./testdata/groups.txt", "r"); + return fp; +} +*/ +import "C" + +import ( + "os" +) + +// iterateUsersHelperTest implements usersHelper interface and is used for testing +// users iteration functionality with fgetpwent(3). +type iterateUsersHelperTest struct { + fp *C.FILE +} + +func (i *iterateUsersHelperTest) set() { + var fp *C.FILE + C.resetErrno() + fp, err := C.openUsersFile() + if err != nil { + panic(err) + } + i.fp = fp +} + +func (i *iterateUsersHelperTest) get() (*C.struct_passwd, error) { + var result *C.struct_passwd + // fgetpwent(3) returns ENOENT when there are no more records. This is + // undocumented in fgetgrent documentation, however, underlying + // implementation of fgetpwent uses fgetpwent_r(3), which in turn returns + // ENOENT when there are no more records. + result, err := C.fgetpwent(i.fp) + return result, err +} + +func (i *iterateUsersHelperTest) end() { + if i.fp != nil { + C.fclose(i.fp) + } +} + +// iterateGroupsHelperTest implements groupsHelper interface and is used for testing +// users iteration functionality with fgetgrent(3). +type iterateGroupsHelperTest struct { + f *os.File + fp *C.FILE +} + +func (i *iterateGroupsHelperTest) set() { + var fp *C.FILE + C.resetErrno() + fp, err := C.openGroupsFile() + if err != nil { + panic(err) + } + i.fp = fp +} + +func (i *iterateGroupsHelperTest) get() (*C.struct_group, error) { + var result *C.struct_group + result, err := C.fgetgrent(i.fp) + // fgetgrent(3) returns ENOENT when there are no more records. This is + // undocumented in fgetgrent documentation, however, underlying + // implementation of fgetgrent uses fgetgrent_r(3), which in turn returns + // ENOENT when there are no more records. + return result, err +} + +func (i *iterateGroupsHelperTest) end() { + if i.fp != nil { + C.fclose(i.fp) + } +} diff --git a/src/os/user/iterate_unix.go b/src/os/user/iterate_unix.go new file mode 100644 index 00000000000000..8e99384d3f58b6 --- /dev/null +++ b/src/os/user/iterate_unix.go @@ -0,0 +1,125 @@ +//go:build ((aix || dragonfly || freebsd || (js && wasm) || (!android && linux) || netbsd || openbsd || solaris) && (!cgo || osusergo)) || darwin + +package user + +// See iterate_cgo.go for explanation why this is used on darwin +// regardless of whether CGO is enabled or not. + +import ( + "bytes" + "os" + "strconv" + "strings" +) + +// Redefining constants, because darwin can use cgo for lookup, but not for +// iterating. +const _groupFile = "/etc/group" +const _userFile = "/etc/passwd" + +var _colon = []byte{':'} + +func iterateUsers(fn NextUserFunc) error { + f, err := os.Open(_userFile) + if err != nil { + return err + } + defer f.Close() + _, err = readColonFile(f, usersIterator(fn), 6) + return err +} + +func iterateGroups(fn NextGroupFunc) error { + f, err := os.Open(_groupFile) + if err != nil { + return err + } + defer f.Close() + _, err = readColonFile(f, groupsIterator(fn), 3) + return err +} + +// parseGroupLine is lineFunc to parse a valid group line for iteration. +func parseGroupLine(line []byte) (v interface{}, err error) { + if bytes.Count(line, _colon) < 3 { + return + } + // wheel:*:0:root + parts := strings.SplitN(string(line), ":", 4) + if len(parts) < 4 || parts[0] == "" || + // 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 +} + +// parseUserLine is lineFunc to parse a valid user line for iteration. +func parseUserLine(line []byte) (v interface{}, err error) { + if 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[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." + if i := strings.Index(u.Name, ","); i >= 0 { + u.Name = u.Name[:i] + } + return u, nil +} + +// usersIterator parses *User and passes it to fn for each given valid line +// read by readColonFile. If non-nil error is returned from fn, iteration +// is terminated. +func usersIterator(fn NextUserFunc) lineFunc { + return func(line []byte) (interface{}, error) { + v, _ := parseUserLine(line) + if u, ok := v.(*User); ok { + if err := fn(u); err != nil { + return nil, err + } + } + return nil, nil + } +} + +// groupsIterator parses *Group and passes it to fn for each given valid line +// read by readColonFile. If non-nil error is returned from fn, iteration +// is terminated. +func groupsIterator(fn NextGroupFunc) lineFunc { + return func(line []byte) (interface{}, error) { + v, _ := parseGroupLine(line) + if g, ok := v.(*Group); ok { + if err := fn(g); err != nil { + return nil, err + } + } + return nil, nil + } +} diff --git a/src/os/user/iterate_unix_test.go b/src/os/user/iterate_unix_test.go new file mode 100644 index 00000000000000..3999602edd4a8d --- /dev/null +++ b/src/os/user/iterate_unix_test.go @@ -0,0 +1,100 @@ +//go:build (aix || darwin || dragonfly || freebsd || (js && wasm) || (!android && linux) || netbsd || openbsd || solaris) && (!cgo || osusergo) +// +build aix darwin dragonfly freebsd js,wasm !android,linux netbsd openbsd solaris +// +build !cgo osusergo + +package user + +import ( + "strings" + "testing" +) + +func TestIterateGroups(t *testing.T) { + const testGroupFile = `# See the opendirectoryd(8) man page for additional +# information about Open Directory. +## +nobody:*:-2: +nogroup:*:-1: +invalidgid:*:notanumber:root ++plussign:*:20:root + indented:*:7: +# comment:*:4:found + # comment:*:4:found +kmem:*:2:root +` + // Ordered list of correctly parsed wantGroups from testGroupFile + var wantGroups = []*Group{ + {Gid: "-2", Name: "nobody"}, + {Gid: "-1", Name: "nogroup"}, + {Gid: "7", Name: "indented"}, + {Gid: "2", Name: "kmem"}, + } + + r := strings.NewReader(testGroupFile) + + gotGroups := make([]*Group, 0, len(wantGroups)) + _, err := readColonFile(r, groupsIterator(func(g *Group) error { + gotGroups = append(gotGroups, g) + return nil + }), 3) + + if len(gotGroups) != len(wantGroups) { + t.Errorf("wantGroups could not be retrieved correctly: parsed %d/%d", len(gotGroups), len(wantGroups)) + } + + for i, g := range gotGroups { + if *g != *wantGroups[i] { + t.Errorf("iterate wantGroups result is incorrect: got: %+v, want: %+v", g, wantGroups[i]) + } + } + + if err != nil { + t.Errorf("readEtcFile error: %v", err) + } +} + +func TestIterateUsers(t *testing.T) { + const testUserFile = ` # Example user file +root:x:0:0:root:/root:/bin/bash + indented:x:3:3:indented with a name:/dev:/usr/sbin/nologin +negative:x:-5:60:games:/usr/games:/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 + +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 wantUsers = []*User{ + {Username: "root", Name: "root", Uid: "0", Gid: "0", HomeDir: "/root"}, + {Username: "indented", Name: "indented with a name", Uid: "3", Gid: "3", HomeDir: "/dev"}, + {Username: "negative", Name: "games", Uid: "-5", Gid: "60", HomeDir: "/usr/games"}, + {Username: "allfields", Name: "mansplit", Uid: "6", Gid: "12", HomeDir: "/home/allfields"}, + } + + gotUsers := make([]*User, 0, len(wantUsers)) + r := strings.NewReader(testUserFile) + _, err := readColonFile(r, usersIterator(func(u *User) error { + gotUsers = append(gotUsers, u) + return nil + }), 6) + + if len(gotUsers) != len(wantUsers) { + t.Errorf("wantUsers could not be parsed correctly: parsed %d/%d", len(gotUsers), len(wantUsers)) + } + + for i, u := range gotUsers { + if *u != *wantUsers[i] { + t.Errorf("iterate wantUsers result is incorrect: got: %+v, want: %+v", u, wantUsers[i]) + } + } + + if err != nil { + t.Errorf("readEtcFile error: %v", err) + } +} diff --git a/src/os/user/iterate_windows.go b/src/os/user/iterate_windows.go new file mode 100644 index 00000000000000..56db7e15a18dad --- /dev/null +++ b/src/os/user/iterate_windows.go @@ -0,0 +1,74 @@ +package user + +import ( + "internal/syscall/windows/registry" + "syscall" +) + +// _profileListKey registry key contains all local user/group SIDs +// (Security Identifiers are Windows version of user/group ids on unix systems) +// as sub keys. It is a sub key of HKEY_LOCAL_MACHINE. Since +// HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList +// registry key does not contain SIDs of Administrator, Guest, or other default +// system users, some users or groups might not be provided when using +// iterateUsers or iterateGroups +const _profileListKey = `SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList` + +// iterateSIDS iterates through _profileListKey sub keys and calls provided +// fn with enumerated sub key name as parameter. If fn returns non-nil error, +// iteration is terminated. +func iterateSIDS(fn func(string) error) error { + k, err := registry.OpenKey(registry.LOCAL_MACHINE, _profileListKey, registry.QUERY_VALUE|registry.ENUMERATE_SUB_KEYS) + if err != nil { + return err + } + return k.ReadSubKeyNames(fn) +} + +// iterateUsers iterates through _profileListKey SIDs, looks up for user +// with each given SID and calls user provided fn with each *User entry. Each iterated SID can be either user or group. Only user SIDs are processed. +func iterateUsers(fn NextUserFunc) error { + return iterateSIDS(func(sid string) error { + SID, err := syscall.StringToSid(sid) + if err != nil { + return err + } + + // Skip non-user SID + if _, _, accType, _ := SID.LookupAccount(""); accType != syscall.SidTypeUser { + return nil + } + u, err := newUserFromSid(SID) + if err != nil { + return err + } + + // Callback to user supplied fn, with user + return fn(u) + }) +} + +// iterateGroups iterates through _profileListKey SIDs, looks up for group with +// each given SID and calls user provided fn with each *Group entry. Each +// iterated SID can be either user or group. Only group SIDs are processed. +func iterateGroups(fn NextGroupFunc) error { + return iterateSIDS(func(sid string) error { + SID, err := syscall.StringToSid(sid) + if err != nil { + return err + } + + groupname, _, t, err := SID.LookupAccount("") + if err != nil { + return err + } + // Skip non-group SID + if isNotGroup(t) { + return nil + } + g := &Group{Name: groupname, Gid: sid} + + // Callback to user supplied fn, with group + return fn(g) + }) +} diff --git a/src/os/user/lookup_unix.go b/src/os/user/lookup_unix.go index 97c611fad42fdf..885558dbfa9dfd 100644 --- a/src/os/user/lookup_unix.go +++ b/src/os/user/lookup_unix.go @@ -9,7 +9,6 @@ package user import ( - "bufio" "bytes" "errors" "io" @@ -27,80 +26,6 @@ func init() { groupImplemented = false } -// lineFunc returns a value, an error, or (nil, nil) to skip the row. -type lineFunc func(line []byte) (v interface{}, 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 interface{}, 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 { diff --git a/src/os/user/lookup_windows.go b/src/os/user/lookup_windows.go index f65773ced3a36d..49b9c2c579ea77 100644 --- a/src/os/user/lookup_windows.go +++ b/src/os/user/lookup_windows.go @@ -346,6 +346,10 @@ func lookupGroup(groupname string) (*Group, error) { return &Group{Name: groupname, Gid: sid}, nil } +func isNotGroup(t uint32) bool { + return t != syscall.SidTypeGroup && t != syscall.SidTypeWellKnownGroup && t != syscall.SidTypeAlias +} + func lookupGroupId(gid string) (*Group, error) { sid, err := syscall.StringToSid(gid) if err != nil { @@ -355,7 +359,7 @@ func lookupGroupId(gid string) (*Group, error) { if err != nil { return nil, err } - if t != syscall.SidTypeGroup && t != syscall.SidTypeWellKnownGroup && t != syscall.SidTypeAlias { + if isNotGroup(t) { return nil, fmt.Errorf("lookupGroupId: should be group account type, not %d", t) } return &Group{Name: groupname, Gid: gid}, nil diff --git a/src/os/user/read_colon_file.go b/src/os/user/read_colon_file.go new file mode 100644 index 00000000000000..4cfffe5d01e209 --- /dev/null +++ b/src/os/user/read_colon_file.go @@ -0,0 +1,86 @@ +//go:build !windows +// +build !windows + +package user + +import ( + "bufio" + "bytes" + "io" +) + +// lineFunc returns a value, an error, or (nil, nil) to skip the row. +type lineFunc func(line []byte) (v interface{}, 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. +// +// readColonFile can also be used to read /adm/users on plan9. +func readColonFile(r io.Reader, fn lineFunc, readCols int) (v interface{}, 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 + } + } + } +} diff --git a/src/os/user/testdata/groups.txt b/src/os/user/testdata/groups.txt new file mode 100644 index 00000000000000..466303cbc1c5fc --- /dev/null +++ b/src/os/user/testdata/groups.txt @@ -0,0 +1,24 @@ +## +# Group Database +# +# Note that this file is consulted directly only when the system is running +# in single-user mode. At other times this information is provided by +# Open Directory. +# +# See the opendirectoryd(8) man page for additional information about +# Open Directory. +## +nobody:x:-2: +nogroup:x:-1: +wheel:x:0:root +daemon:x:1:root +kmem:x:2:root +sys:x:3:root +operator:x:5:root +mail:x:6:_teamsserver +tty:x:4:root +bin:x:7: +procview:x:8:root +procmod:x:9:root +owner:x:10: +everyone:x:12: diff --git a/src/os/user/testdata/plan9/users.txt b/src/os/user/testdata/plan9/users.txt new file mode 100644 index 00000000000000..bf674c076d6fbf --- /dev/null +++ b/src/os/user/testdata/plan9/users.txt @@ -0,0 +1,9 @@ +-1:adm:adm:bootes +0:none:adm: +1:tor:tor: +2:glenda:glenda: +9999:noworld:: +10000:sys:: +10001:upas:upas: +10002:bootes:bootes: +test:test:: \ No newline at end of file diff --git a/src/os/user/testdata/users.txt b/src/os/user/testdata/users.txt new file mode 100644 index 00000000000000..0fe0fb94ba3f7f --- /dev/null +++ b/src/os/user/testdata/users.txt @@ -0,0 +1,19 @@ +## +# User Database +# +# Note that this file is consulted directly only when the system is running +# in single-user mode. At other times this information is provided by +# Open Directory. +# +# See the opendirectoryd(8) man page for additional information about +# Open Directory. +## +nobody:x:-2:-2:Unprivileged User:/var/empty:/usr/bin/false +root:x:0:0:System Administrator:/var/root:/bin/sh +daemon:x:1:1:System Services:/var/root:/usr/bin/false +_uucp:x:4:4:Unix to Unix Copy Protocol:/var/spool/uucp:/usr/sbin/uucico +_taskgated:x:13:13:Task Gate Daemon:/var/empty:/usr/bin/false +_networkd:x:24:24:Network Services:/var/networkd:/usr/bin/false +_installassistant:x:25:25:Install Assistant:/var/empty:/usr/bin/false +_lp:x:26:26:Printing Services:/var/spool/cups:/usr/bin/false +_postfix:x:27:27:Postfix Mail Server:/var/spool/postfix:/usr/bin/false \ No newline at end of file diff --git a/src/os/user/user.go b/src/os/user/user.go index c1b8101c8629cb..cf57bc791fabc4 100644 --- a/src/os/user/user.go +++ b/src/os/user/user.go @@ -3,16 +3,24 @@ // license that can be found in the LICENSE file. /* -Package user allows user account lookups by name or id. +Package user allows user and group iteration and account lookups by name or id. For most Unix systems, this package has two internal implementations of resolving user and group ids to names. 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 and getgrnam_r. +the standard C library (libc) routines such as getpwuid_r(3) and getgrnam_r(3). +Iteration functionality, when use with cgo, relies on getpwent(3) and +getgrent(3) for users and groups respectively. When cgo is available, cgo-based (libc-backed) code is used by default. This can be overridden by using osusergo build tag, which enforces the pure Go implementation. + +On Windows, iterateUsers and iterateGroups iterates over +HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList +registry key to get SIDs (Security Identifiers) of users and groups. Some +system users like Administrator or Guest might not be available in +ProfileList registry key. */ package user diff --git a/src/time/zoneinfo_windows.go b/src/time/zoneinfo_windows.go index ba66f90ffeaf20..cc3eddd66591de 100644 --- a/src/time/zoneinfo_windows.go +++ b/src/time/zoneinfo_windows.go @@ -67,7 +67,11 @@ func toEnglishName(stdname, dstname string) (string, error) { } defer k.Close() - names, err := k.ReadSubKeyNames() + var names []string + err = k.ReadSubKeyNames(func(s string) error { + names = append(names, s) + return nil + }) if err != nil { return "", err }