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