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