// 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.

//go:build ((unix && !android) || (js && wasm) || wasip1) && ((!cgo && !darwin) || osusergo)

package user

import (
	
	
	
	
	
	
	
)

// lineFunc returns a value, an error, or (nil, nil) to skip the row.
type lineFunc func(line []byte) (v any, 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.
//
// readCols is the minimum number of colon-separated fields that will be passed
// to fn; in a long line additional fields may be silently discarded.
func readColonFile( io.Reader,  lineFunc,  int) ( any,  error) {
	 := bufio.NewReader()

	// Read the file line-by-line.
	for {
		var  bool
		var  []byte

		// Read the next line. We do so in chunks (as much as reader's
		// buffer is able to keep), check if we read enough columns
		// already on each step and store final result in wholeLine.
		for {
			var  []byte
			, ,  = .ReadLine()

			if  != nil {
				// We should return (nil, nil) if EOF is reached
				// without a match.
				if  == io.EOF {
					 = nil
				}
				return nil, 
			}

			// Simple common case: line is short enough to fit in a
			// single reader's buffer.
			if ! && len() == 0 {
				 = 
				break
			}

			 = append(, ...)

			// Check if we read the whole line (or enough columns)
			// already.
			if ! || bytes.Count(, []byte{':'}) >=  {
				break
			}
		}

		// 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.
		 = bytes.TrimSpace()
		if len() == 0 || [0] == '#' {
			continue
		}
		,  = ()
		if  != nil ||  != nil {
			return
		}

		// If necessary, skip the rest of the line
		for ; ; _, ,  = .ReadLine() {
			if  != nil {
				// We should return (nil, nil) if EOF is reached without a match.
				if  == io.EOF {
					 = nil
				}
				return nil, 
			}
		}
	}
}

func matchGroupIndexValue( string,  int) lineFunc {
	var  string
	if  > 0 {
		 = ":"
	}
	 := []byte( +  + ":")
	return func( []byte) ( any,  error) {
		if !bytes.Contains(, ) || bytes.Count(, colon) < 3 {
			return
		}
		// wheel:*:0:root
		 := strings.SplitN(string(), ":", 4)
		if len() < 4 || [0] == "" || [] !=  ||
			// 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.
			[0][0] == '+' || [0][0] == '-' {
			return
		}
		if ,  := strconv.Atoi([2]);  != nil {
			return nil, nil
		}
		return &Group{Name: [0], Gid: [2]}, nil
	}
}

func findGroupId( string,  io.Reader) (*Group, error) {
	if ,  := readColonFile(, matchGroupIndexValue(, 2), 3);  != nil {
		return nil, 
	} else if  != nil {
		return .(*Group), nil
	}
	return nil, UnknownGroupIdError()
}

func findGroupName( string,  io.Reader) (*Group, error) {
	if ,  := readColonFile(, matchGroupIndexValue(, 0), 3);  != nil {
		return nil, 
	} else if  != nil {
		return .(*Group), nil
	}
	return nil, UnknownGroupError()
}

// returns a *User for a row if that row's has the given value at the
// given index.
func matchUserIndexValue( string,  int) lineFunc {
	var  string
	if  > 0 {
		 = ":"
	}
	 := []byte( +  + ":")
	return func( []byte) ( any,  error) {
		if !bytes.Contains(, ) || bytes.Count(, colon) < 6 {
			return
		}
		// kevin:x:1005:1006::/home/kevin:/usr/bin/zsh
		 := strings.SplitN(string(), ":", 7)
		if len() < 6 || [] !=  || [0] == "" ||
			[0][0] == '+' || [0][0] == '-' {
			return
		}
		if ,  := strconv.Atoi([2]);  != nil {
			return nil, nil
		}
		if ,  := strconv.Atoi([3]);  != nil {
			return nil, nil
		}
		 := &User{
			Username: [0],
			Uid:      [2],
			Gid:      [3],
			Name:     [4],
			HomeDir:  [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."
		.Name, _, _ = strings.Cut(.Name, ",")
		return , nil
	}
}

func findUserId( string,  io.Reader) (*User, error) {
	,  := strconv.Atoi()
	if  != nil {
		return nil, errors.New("user: invalid userid " + )
	}
	if ,  := readColonFile(, matchUserIndexValue(, 2), 6);  != nil {
		return nil, 
	} else if  != nil {
		return .(*User), nil
	}
	return nil, UnknownUserIdError()
}

func findUsername( string,  io.Reader) (*User, error) {
	if ,  := readColonFile(, matchUserIndexValue(, 0), 6);  != nil {
		return nil, 
	} else if  != nil {
		return .(*User), nil
	}
	return nil, UnknownUserError()
}

func lookupGroup( string) (*Group, error) {
	,  := os.Open(groupFile)
	if  != nil {
		return nil, 
	}
	defer .Close()
	return findGroupName(, )
}

func lookupGroupId( string) (*Group, error) {
	,  := os.Open(groupFile)
	if  != nil {
		return nil, 
	}
	defer .Close()
	return findGroupId(, )
}

func lookupUser( string) (*User, error) {
	,  := os.Open(userFile)
	if  != nil {
		return nil, 
	}
	defer .Close()
	return findUsername(, )
}

func lookupUserId( string) (*User, error) {
	,  := os.Open(userFile)
	if  != nil {
		return nil, 
	}
	defer .Close()
	return findUserId(, )
}