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// +build aix darwin dragonfly freebsd hurd js,wasm !android,linux netbsd openbsd solaris
6// +build !cgo osusergo
7
8package user
9
10import (
11	"bufio"
12	"bytes"
13	"errors"
14	"io"
15	"os"
16	"strconv"
17	"strings"
18)
19
20const groupFile = "/etc/group"
21const userFile = "/etc/passwd"
22
23var colon = []byte{':'}
24
25func init() {
26	groupImplemented = false
27}
28
29// lineFunc returns a value, an error, or (nil, nil) to skip the row.
30type lineFunc func(line []byte) (v interface{}, err error)
31
32// readColonFile parses r as an /etc/group or /etc/passwd style file, running
33// fn for each row. readColonFile returns a value, an error, or (nil, nil) if
34// the end of the file is reached without a match.
35func readColonFile(r io.Reader, fn lineFunc) (v interface{}, err error) {
36	bs := bufio.NewScanner(r)
37	for bs.Scan() {
38		line := bs.Bytes()
39		// There's no spec for /etc/passwd or /etc/group, but we try to follow
40		// the same rules as the glibc parser, which allows comments and blank
41		// space at the beginning of a line.
42		line = bytes.TrimSpace(line)
43		if len(line) == 0 || line[0] == '#' {
44			continue
45		}
46		v, err = fn(line)
47		if v != nil || err != nil {
48			return
49		}
50	}
51	return nil, bs.Err()
52}
53
54func matchGroupIndexValue(value string, idx int) lineFunc {
55	var leadColon string
56	if idx > 0 {
57		leadColon = ":"
58	}
59	substr := []byte(leadColon + value + ":")
60	return func(line []byte) (v interface{}, err error) {
61		if !bytes.Contains(line, substr) || bytes.Count(line, colon) < 3 {
62			return
63		}
64		// wheel:*:0:root
65		parts := strings.SplitN(string(line), ":", 4)
66		if len(parts) < 4 || parts[0] == "" || parts[idx] != value ||
67			// If the file contains +foo and you search for "foo", glibc
68			// returns an "invalid argument" error. Similarly, if you search
69			// for a gid for a row where the group name starts with "+" or "-",
70			// glibc fails to find the record.
71			parts[0][0] == '+' || parts[0][0] == '-' {
72			return
73		}
74		if _, err := strconv.Atoi(parts[2]); err != nil {
75			return nil, nil
76		}
77		return &Group{Name: parts[0], Gid: parts[2]}, nil
78	}
79}
80
81func findGroupId(id string, r io.Reader) (*Group, error) {
82	if v, err := readColonFile(r, matchGroupIndexValue(id, 2)); err != nil {
83		return nil, err
84	} else if v != nil {
85		return v.(*Group), nil
86	}
87	return nil, UnknownGroupIdError(id)
88}
89
90func findGroupName(name string, r io.Reader) (*Group, error) {
91	if v, err := readColonFile(r, matchGroupIndexValue(name, 0)); err != nil {
92		return nil, err
93	} else if v != nil {
94		return v.(*Group), nil
95	}
96	return nil, UnknownGroupError(name)
97}
98
99// returns a *User for a row if that row's has the given value at the
100// given index.
101func matchUserIndexValue(value string, idx int) lineFunc {
102	var leadColon string
103	if idx > 0 {
104		leadColon = ":"
105	}
106	substr := []byte(leadColon + value + ":")
107	return func(line []byte) (v interface{}, err error) {
108		if !bytes.Contains(line, substr) || bytes.Count(line, colon) < 6 {
109			return
110		}
111		// kevin:x:1005:1006::/home/kevin:/usr/bin/zsh
112		parts := strings.SplitN(string(line), ":", 7)
113		if len(parts) < 6 || parts[idx] != value || parts[0] == "" ||
114			parts[0][0] == '+' || parts[0][0] == '-' {
115			return
116		}
117		if _, err := strconv.Atoi(parts[2]); err != nil {
118			return nil, nil
119		}
120		if _, err := strconv.Atoi(parts[3]); err != nil {
121			return nil, nil
122		}
123		u := &User{
124			Username: parts[0],
125			Uid:      parts[2],
126			Gid:      parts[3],
127			Name:     parts[4],
128			HomeDir:  parts[5],
129		}
130		// The pw_gecos field isn't quite standardized. Some docs
131		// say: "It is expected to be a comma separated list of
132		// personal data where the first item is the full name of the
133		// user."
134		if i := strings.Index(u.Name, ","); i >= 0 {
135			u.Name = u.Name[:i]
136		}
137		return u, nil
138	}
139}
140
141func findUserId(uid string, r io.Reader) (*User, error) {
142	i, e := strconv.Atoi(uid)
143	if e != nil {
144		return nil, errors.New("user: invalid userid " + uid)
145	}
146	if v, err := readColonFile(r, matchUserIndexValue(uid, 2)); err != nil {
147		return nil, err
148	} else if v != nil {
149		return v.(*User), nil
150	}
151	return nil, UnknownUserIdError(i)
152}
153
154func findUsername(name string, r io.Reader) (*User, error) {
155	if v, err := readColonFile(r, matchUserIndexValue(name, 0)); err != nil {
156		return nil, err
157	} else if v != nil {
158		return v.(*User), nil
159	}
160	return nil, UnknownUserError(name)
161}
162
163func lookupGroup(groupname string) (*Group, error) {
164	f, err := os.Open(groupFile)
165	if err != nil {
166		return nil, err
167	}
168	defer f.Close()
169	return findGroupName(groupname, f)
170}
171
172func lookupGroupId(id string) (*Group, error) {
173	f, err := os.Open(groupFile)
174	if err != nil {
175		return nil, err
176	}
177	defer f.Close()
178	return findGroupId(id, f)
179}
180
181func lookupUser(username string) (*User, error) {
182	f, err := os.Open(userFile)
183	if err != nil {
184		return nil, err
185	}
186	defer f.Close()
187	return findUsername(username, f)
188}
189
190func lookupUserId(uid string) (*User, error) {
191	f, err := os.Open(userFile)
192	if err != nil {
193		return nil, err
194	}
195	defer f.Close()
196	return findUserId(uid, f)
197}
198