Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

os/user: add users groups iteration functionality #1

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions src/internal/syscall/windows/registry/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
mjonaitis1 marked this conversation as resolved.
Show resolved Hide resolved
// 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
Expand All @@ -110,11 +110,15 @@ loopItems:
if err == _ERROR_NO_MORE_ITEMS {
break loopItems
}
return names, err
return err
}

// Callback with key name string
mjonaitis1 marked this conversation as resolved.
Show resolved Hide resolved
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.
Expand Down
6 changes: 5 additions & 1 deletion src/internal/syscall/windows/registry/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
7 changes: 6 additions & 1 deletion src/mime/type_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions src/os/user/iterate.go
Original file line number Diff line number Diff line change
@@ -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.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What needs locking in this case? Maybe provide some examples or checks to make sure it is locked?

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)
}
161 changes: 161 additions & 0 deletions src/os/user/iterate_cgo.go
Original file line number Diff line number Diff line change
@@ -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 <unistd.h>
#include <sys/types.h>
#include <pwd.h>
#include <grp.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>

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 {
mjonaitis1 marked this conversation as resolved.
Show resolved Hide resolved
// 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
}
Comment on lines +83 to +88
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return of err should be based off of what err actually is instead of the value, result.

		result, err := userIterator.get()

		// If result is nil - getpwent iterated through entire users database or there was an error
		if err != nil {
			return err
		}
		
		...
		if result == nil {
		        ...
		}


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
}
}
}
53 changes: 53 additions & 0 deletions src/os/user/iterate_cgo_bsd_windows_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
81 changes: 81 additions & 0 deletions src/os/user/iterate_cgo_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//go:build (aix || dragonfly || (!android && linux) || solaris) && cgo && !osusergo
// +build aix dragonfly !android,linux solaris
// +build cgo
// +build !osusergo
mjonaitis1 marked this conversation as resolved.
Show resolved Hide resolved

package user

mjonaitis1 marked this conversation as resolved.
Show resolved Hide resolved
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)
}
}
Loading