lookup_unix.mx raw

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