listgroups_unix.mx raw
1 // Copyright 2021 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
4
5 //go:build ((darwin || dragonfly || freebsd || (js && wasm) || wasip1 || (!android && linux) || netbsd || openbsd || solaris) && ((!cgo && !darwin) || osusergo)) || aix || illumos
6
7 package user
8
9 import (
10 "bufio"
11 "bytes"
12 "errors"
13 "fmt"
14 "io"
15 "os"
16 "strconv"
17 )
18
19 func listGroupsFromReader(u *User, r io.Reader) ([]string, error) {
20 if u.Username == "" {
21 return nil, errors.New("user: list groups: empty username")
22 }
23 primaryGid, err := strconv.Atoi(u.Gid)
24 if err != nil {
25 return nil, fmt.Errorf("user: list groups for %s: invalid gid %q", u.Username, u.Gid)
26 }
27
28 userCommas := []byte("," + u.Username + ",") // ,john,
29 userFirst := userCommas[1:] // john,
30 userLast := userCommas[:len(userCommas)-1] // ,john
31 userOnly := userCommas[1 : len(userCommas)-1] // john
32
33 // Add primary Gid first.
34 groups := []string{u.Gid}
35
36 rd := bufio.NewReader(r)
37 done := false
38 for !done {
39 line, err := rd.ReadBytes('\n')
40 if err != nil {
41 if err == io.EOF {
42 done = true
43 } else {
44 return groups, err
45 }
46 }
47
48 // Look for username in the list of users. If user is found,
49 // append the GID to the groups slice.
50
51 // There's no spec for /etc/passwd or /etc/group, but we try to follow
52 // the same rules as the glibc parser, which allows comments and blank
53 // space at the beginning of a line.
54 line = bytes.TrimSpace(line)
55 if len(line) == 0 || line[0] == '#' ||
56 // If you search for a gid in a row where the group
57 // name (the first field) starts with "+" or "-",
58 // glibc fails to find the record, and so should we.
59 line[0] == '+' || line[0] == '-' {
60 continue
61 }
62
63 // Format of /etc/group is
64 // groupname:password:GID:user_list
65 // for example
66 // wheel:x:10:john,paul,jack
67 // tcpdump:x:72:
68 listIdx := bytes.LastIndexByte(line, ':')
69 if listIdx == -1 || listIdx == len(line)-1 {
70 // No commas, or empty group list.
71 continue
72 }
73 if bytes.Count(line[:listIdx], colon) != 2 {
74 // Incorrect number of colons.
75 continue
76 }
77 list := line[listIdx+1:]
78 // Check the list for user without splitting or copying.
79 if !(bytes.Equal(list, userOnly) || bytes.HasPrefix(list, userFirst) || bytes.HasSuffix(list, userLast) || bytes.Contains(list, userCommas)) {
80 continue
81 }
82
83 // groupname:password:GID
84 parts := bytes.Split(line[:listIdx], colon)
85 if len(parts) != 3 || len(parts[0]) == 0 {
86 continue
87 }
88 gid := string(parts[2])
89 // Make sure it's numeric and not the same as primary GID.
90 numGid, err := strconv.Atoi(gid)
91 if err != nil || numGid == primaryGid {
92 continue
93 }
94
95 groups = append(groups, gid)
96 }
97
98 return groups, nil
99 }
100
101 func listGroups(u *User) ([]string, error) {
102 f, err := os.Open(groupFile)
103 if err != nil {
104 return nil, err
105 }
106 defer f.Close()
107
108 return listGroupsFromReader(u, f)
109 }
110