Source file src/os/user/lookup_unix.go

     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 (aix || darwin || dragonfly || freebsd || (js && wasm) || (!android && linux) || netbsd || openbsd || solaris) && (!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  }
   237  

View as plain text