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 ((unix && !android) || (js && wasm) || wasip1) && ((!cgo && !darwin) || 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  // lineFunc returns a value, an error, or (nil, nil) to skip the row.
    20  type 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.
    28  func 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  
    93  func 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  
   120  func 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  
   129  func 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.
   140  func 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  
   178  func 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  
   191  func 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  
   200  func 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  
   209  func 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  
   218  func 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  
   227  func 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  

View as plain text