lookup_unix.mx raw
1 // Copyright 2016 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 ((unix && !android) || (js && wasm) || wasip1) && ((!cgo && !darwin) || osusergo)
6
7 package user
8
9 import (
10 "bufio"
11 "bytes"
12 "errors"
13 "io"
14 "os"
15 "strconv"
16 )
17
18 // lineFunc returns a value, an error, or (nil, nil) to skip the row.
19 type lineFunc func(line []byte) (v any, err error)
20
21 // readColonFile parses r as an /etc/group or /etc/passwd style file, running
22 // fn for each row. readColonFile returns a value, an error, or (nil, nil) if
23 // the end of the file is reached without a match.
24 //
25 // readCols is the minimum number of colon-separated fields that will be passed
26 // to fn; in a long line additional fields may be silently discarded.
27 func readColonFile(r io.Reader, fn lineFunc, readCols int) (v any, err error) {
28 rd := bufio.NewReader(r)
29
30 // Read the file line-by-line.
31 for {
32 var isPrefix bool
33 var wholeLine []byte
34
35 // Read the next line. We do so in chunks (as much as reader's
36 // buffer is able to keep), check if we read enough columns
37 // already on each step and store final result in wholeLine.
38 for {
39 var line []byte
40 line, isPrefix, err = rd.ReadLine()
41
42 if err != nil {
43 // We should return (nil, nil) if EOF is reached
44 // without a match.
45 if err == io.EOF {
46 err = nil
47 }
48 return nil, err
49 }
50
51 // Simple common case: line is short enough to fit in a
52 // single reader's buffer.
53 if !isPrefix && len(wholeLine) == 0 {
54 wholeLine = line
55 break
56 }
57
58 wholeLine = append(wholeLine, line...)
59
60 // Check if we read the whole line (or enough columns)
61 // already.
62 if !isPrefix || bytes.Count(wholeLine, []byte{':'}) >= readCols {
63 break
64 }
65 }
66
67 // There's no spec for /etc/passwd or /etc/group, but we try to follow
68 // the same rules as the glibc parser, which allows comments and blank
69 // space at the beginning of a line.
70 wholeLine = bytes.TrimSpace(wholeLine)
71 if len(wholeLine) == 0 || wholeLine[0] == '#' {
72 continue
73 }
74 v, err = fn(wholeLine)
75 if v != nil || err != nil {
76 return
77 }
78
79 // If necessary, skip the rest of the line
80 for ; isPrefix; _, isPrefix, err = rd.ReadLine() {
81 if err != nil {
82 // We should return (nil, nil) if EOF is reached without a match.
83 if err == io.EOF {
84 err = nil
85 }
86 return nil, err
87 }
88 }
89 }
90 }
91
92 func matchGroupIndexValue(value string, idx int) lineFunc {
93 var leadColon string
94 if idx > 0 {
95 leadColon = ":"
96 }
97 substr := []byte(leadColon + value + ":")
98 return func(line []byte) (v any, err error) {
99 if !bytes.Contains(line, substr) || bytes.Count(line, colon) < 3 {
100 return
101 }
102 // wheel:*:0:root
103 parts := bytes.SplitN(string(line), ":", 4)
104 if len(parts) < 4 || parts[0] == "" || parts[idx] != value ||
105 // If the file contains +foo and you search for "foo", glibc
106 // returns an "invalid argument" error. Similarly, if you search
107 // for a gid for a row where the group name starts with "+" or "-",
108 // glibc fails to find the record.
109 parts[0][0] == '+' || parts[0][0] == '-' {
110 return
111 }
112 if _, err := strconv.Atoi(parts[2]); err != nil {
113 return nil, nil
114 }
115 return &Group{Name: parts[0], Gid: parts[2]}, nil
116 }
117 }
118
119 func findGroupId(id string, r io.Reader) (*Group, error) {
120 if v, err := readColonFile(r, matchGroupIndexValue(id, 2), 3); err != nil {
121 return nil, err
122 } else if v != nil {
123 return v.(*Group), nil
124 }
125 return nil, UnknownGroupIdError(id)
126 }
127
128 func findGroupName(name string, r io.Reader) (*Group, error) {
129 if v, err := readColonFile(r, matchGroupIndexValue(name, 0), 3); err != nil {
130 return nil, err
131 } else if v != nil {
132 return v.(*Group), nil
133 }
134 return nil, UnknownGroupError(name)
135 }
136
137 // returns a *User for a row if that row's has the given value at the
138 // given index.
139 func matchUserIndexValue(value string, idx int) lineFunc {
140 var leadColon string
141 if idx > 0 {
142 leadColon = ":"
143 }
144 substr := []byte(leadColon + value + ":")
145 return func(line []byte) (v any, err error) {
146 if !bytes.Contains(line, substr) || bytes.Count(line, colon) < 6 {
147 return
148 }
149 // kevin:x:1005:1006::/home/kevin:/usr/bin/zsh
150 parts := bytes.SplitN(string(line), ":", 7)
151 if len(parts) < 6 || parts[idx] != value || parts[0] == "" ||
152 parts[0][0] == '+' || parts[0][0] == '-' {
153 return
154 }
155 if _, err := strconv.Atoi(parts[2]); err != nil {
156 return nil, nil
157 }
158 if _, err := strconv.Atoi(parts[3]); err != nil {
159 return nil, nil
160 }
161 u := &User{
162 Username: parts[0],
163 Uid: parts[2],
164 Gid: parts[3],
165 Name: parts[4],
166 HomeDir: parts[5],
167 }
168 // The pw_gecos field isn't quite standardized. Some docs
169 // say: "It is expected to be a comma separated list of
170 // personal data where the first item is the full name of the
171 // user."
172 u.Name, _, _ = bytes.Cut(u.Name, ",")
173 return u, nil
174 }
175 }
176
177 func findUserId(uid string, r io.Reader) (*User, error) {
178 i, e := strconv.Atoi(uid)
179 if e != nil {
180 return nil, errors.New("user: invalid userid " + uid)
181 }
182 if v, err := readColonFile(r, matchUserIndexValue(uid, 2), 6); err != nil {
183 return nil, err
184 } else if v != nil {
185 return v.(*User), nil
186 }
187 return nil, UnknownUserIdError(i)
188 }
189
190 func findUsername(name string, r io.Reader) (*User, error) {
191 if v, err := readColonFile(r, matchUserIndexValue(name, 0), 6); err != nil {
192 return nil, err
193 } else if v != nil {
194 return v.(*User), nil
195 }
196 return nil, UnknownUserError(name)
197 }
198
199 func lookupGroup(groupname string) (*Group, error) {
200 f, err := os.Open(groupFile)
201 if err != nil {
202 return nil, err
203 }
204 defer f.Close()
205 return findGroupName(groupname, f)
206 }
207
208 func lookupGroupId(id string) (*Group, error) {
209 f, err := os.Open(groupFile)
210 if err != nil {
211 return nil, err
212 }
213 defer f.Close()
214 return findGroupId(id, f)
215 }
216
217 func lookupUser(username string) (*User, error) {
218 f, err := os.Open(userFile)
219 if err != nil {
220 return nil, err
221 }
222 defer f.Close()
223 return findUsername(username, f)
224 }
225
226 func lookupUserId(uid string) (*User, error) {
227 f, err := os.Open(userFile)
228 if err != nil {
229 return nil, err
230 }
231 defer f.Close()
232 return findUserId(uid, f)
233 }
234