blob: 05f39be40b31c3d438a30985b907a23e44378959 [file] [log] [blame]
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build darwin dragonfly freebsd !android,linux nacl netbsd openbsd solaris
// +build !cgo osusergo
package user
import (
"bufio"
"bytes"
"errors"
"io"
"os"
"strconv"
"strings"
)
const groupFile = "/etc/group"
const userFile = "/etc/passwd"
var colon = []byte{':'}
func init() {
groupImplemented = false
}
// lineFunc returns a value, an error, or (nil, nil) to skip the row.
type lineFunc func(line []byte) (v interface{}, err error)
// readColonFile parses r as an /etc/group or /etc/passwd style file, running
// fn for each row. readColonFile returns a value, an error, or (nil, nil) if
// the end of the file is reached without a match.
func readColonFile(r io.Reader, fn lineFunc) (v interface{}, err error) {
bs := bufio.NewScanner(r)
for bs.Scan() {
line := bs.Bytes()
// There's no spec for /etc/passwd or /etc/group, but we try to follow
// the same rules as the glibc parser, which allows comments and blank
// space at the beginning of a line.
line = bytes.TrimSpace(line)
if len(line) == 0 || line[0] == '#' {
continue
}
v, err = fn(line)
if v != nil || err != nil {
return
}
}
return nil, bs.Err()
}
func matchGroupIndexValue(value string, idx int) lineFunc {
var leadColon string
if idx > 0 {
leadColon = ":"
}
substr := []byte(leadColon + value + ":")
return func(line []byte) (v interface{}, err error) {
if !bytes.Contains(line, substr) || bytes.Count(line, colon) < 3 {
return
}
// wheel:*:0:root
parts := strings.SplitN(string(line), ":", 4)
if len(parts) < 4 || parts[0] == "" || parts[idx] != value ||
// If the file contains +foo and you search for "foo", glibc
// returns an "invalid argument" error. Similarly, if you search
// for a gid for a row where the group name starts with "+" or "-",
// glibc fails to find the record.
parts[0][0] == '+' || parts[0][0] == '-' {
return
}
if _, err := strconv.Atoi(parts[2]); err != nil {
return nil, nil
}
return &Group{Name: parts[0], Gid: parts[2]}, nil
}
}
func findGroupId(id string, r io.Reader) (*Group, error) {
if v, err := readColonFile(r, matchGroupIndexValue(id, 2)); err != nil {
return nil, err
} else if v != nil {
return v.(*Group), nil
}
return nil, UnknownGroupIdError(id)
}
func findGroupName(name string, r io.Reader) (*Group, error) {
if v, err := readColonFile(r, matchGroupIndexValue(name, 0)); err != nil {
return nil, err
} else if v != nil {
return v.(*Group), nil
}
return nil, UnknownGroupError(name)
}
// returns a *User for a row if that row's has the given value at the
// given index.
func matchUserIndexValue(value string, idx int) lineFunc {
var leadColon string
if idx > 0 {
leadColon = ":"
}
substr := []byte(leadColon + value + ":")
return func(line []byte) (v interface{}, err error) {
if !bytes.Contains(line, substr) || bytes.Count(line, colon) < 6 {
return
}
// kevin:x:1005:1006::/home/kevin:/usr/bin/zsh
parts := strings.SplitN(string(line), ":", 7)
if len(parts) < 6 || parts[idx] != value || parts[0] == "" ||
parts[0][0] == '+' || parts[0][0] == '-' {
return
}
if _, err := strconv.Atoi(parts[2]); err != nil {
return nil, nil
}
if _, err := strconv.Atoi(parts[3]); err != nil {
return nil, nil
}
u := &User{
Username: parts[0],
Uid: parts[2],
Gid: parts[3],
Name: parts[4],
HomeDir: parts[5],
}
// The pw_gecos field isn't quite standardized. Some docs
// say: "It is expected to be a comma separated list of
// personal data where the first item is the full name of the
// user."
if i := strings.Index(u.Name, ","); i >= 0 {
u.Name = u.Name[:i]
}
return u, nil
}
}
func findUserId(uid string, r io.Reader) (*User, error) {
i, e := strconv.Atoi(uid)
if e != nil {
return nil, errors.New("user: invalid userid " + uid)
}
if v, err := readColonFile(r, matchUserIndexValue(uid, 2)); err != nil {
return nil, err
} else if v != nil {
return v.(*User), nil
}
return nil, UnknownUserIdError(i)
}
func findUsername(name string, r io.Reader) (*User, error) {
if v, err := readColonFile(r, matchUserIndexValue(name, 0)); err != nil {
return nil, err
} else if v != nil {
return v.(*User), nil
}
return nil, UnknownUserError(name)
}
func lookupGroup(groupname string) (*Group, error) {
f, err := os.Open(groupFile)
if err != nil {
return nil, err
}
defer f.Close()
return findGroupName(groupname, f)
}
func lookupGroupId(id string) (*Group, error) {
f, err := os.Open(groupFile)
if err != nil {
return nil, err
}
defer f.Close()
return findGroupId(id, f)
}
func lookupUser(username string) (*User, error) {
f, err := os.Open(userFile)
if err != nil {
return nil, err
}
defer f.Close()
return findUsername(username, f)
}
func lookupUserId(uid string) (*User, error) {
f, err := os.Open(userFile)
if err != nil {
return nil, err
}
defer f.Close()
return findUserId(uid, f)
}