Repository: moby/moby
File: daemon/archive_tarcopyoptions_unix.go (Go)

Code before:
// - username:groupname  - valid username; valid groupname from /etc/passwd
// - uid                 - 32-bit unsigned int valid Linux UID value
// - uid:gid             - uid value; 32-bit unsigned int Linux GID value
// - username:gid        - valid username from getent(1), gid value; 32-bit unsigned int Linux GID value
// - uid:groupname       - 32-bit unsigned int valid Linux UID value, valid groupname from /etc/group
//
// If only a username (or uid) is provided, an attempt is made to lookup the gid
// for that username using /etc/passwd
func getUIDGID(ctr *container.Container) (uid int, gid int, _ error) {
	userNameOrID, groupNameOrID, hasGroup := strings.Cut(ctr.Config.User, ":")

	var (
		userID, userName   = getIDOrName(userNameOrID)
		groupID, groupName = getIDOrName(groupNameOrID)
	)
	if userName == "" && hasGroup && groupName == "" {
		// UID/GID passed; nothing to look up.
		return userID, groupID, nil
	}

	usr, err := lookupUser(ctr, userNameOrID)
	if err != nil {
		return 0, 0, errors.Wrapf(err, "failed to look up user %q in container", userNameOrID)
	}
	if !hasGroup || groupNameOrID == "" {
		// Use user's primary group
		return usr.Uid, usr.Gid, nil
	}

	groupID, err = lookupGID(ctr, groupNameOrID)
	if err != nil {
		return 0, 0, errors.Wrapf(err, "failed to look up group %q in container", groupNameOrID)
	}
	return usr.Uid, groupID, nil
}

// getIDOrName checks whether nameOrID is a ID (integer) or a Name.
// It assumes nameOrID is a name when failing to parse as integer,
// in which case a non-empty name is returned.
func getIDOrName(nameOrID string) (id int, name string) {
	if uid, err := strconv.ParseUint(nameOrID, 10, 32); err == nil && uid <= math.MaxInt32 {
		// uid provided
		return int(uid), ""
	}
	// not am id, assume name
	return 0, nameOrID
}

func lookupUser(ctr *container.Container, nameOrID string) (user.User, error) {
	passwdPath, err := resourcePath(ctr, user.GetPasswdPath)
	if err != nil {

Review comment (suggestion):
That said.. with the logic in this PR, we'd be able to skip lookup if a numeric UID/GID is passed;  that  means we're  also  able to work  with a container that doesn't have `/etc/passwd` / `etc/group`.

Code after:
// - username:groupname  - valid username; valid groupname from /etc/passwd, /etc/group
// - uid                 - 32-bit unsigned int valid Linux UID value
// - uid:gid             - uid value; 32-bit unsigned int Linux GID value
// - username:gid        - valid username from getent(1), gid value; 32-bit unsigned int Linux GID value
// - uid:groupname       - 32-bit unsigned int valid Linux UID value, valid groupname from /etc/group
//
// If only a username (or uid) is provided, an attempt is made to look up the gid
// for that username using /etc/passwd
func getUIDGID(ctrUser string) (uid int, gid int, _ error) {
	userNameOrID, groupNameOrID, _ := strings.Cut(ctrUser, ":")

	// Align with behavior of docker run, which treats an empty username
	// or groupname as default (0 (root)).
	//
	//	docker run --rm --user ":33" alpine id
	//	uid=0(root) gid=33 groups=33
	//
	//	docker run --rm --user "33:" alpine id
	//	uid=33 gid=0(root) groups=0(root)
	if userNameOrID != "" {
		var err error
		uid, gid, err = lookupUser(userNameOrID)
		if err != nil {
			return 0, 0, err
		}
	}
	if groupNameOrID != "" {
		var err error
		gid, err = lookupGID(groupNameOrID)
		if err != nil {
			return 0, 0, err
		}
	}
	return uid, gid, nil
}

// getIDOrName checks whether nameOrID is a ID (integer) or a Name.
// It assumes nameOrID is a name when failing to parse as integer,
// in which case a non-empty name is returned.
func getIDOrName(nameOrID string) (id int, name string) {
	if uid, err := strconv.ParseUint(nameOrID, 10, 32); err == nil && uid <= math.MaxInt32 {
		// uid provided
		return int(uid), ""
	}
	// not an id, assume name
	return 0, nameOrID
}

func lookupUser(nameOrID string) (uid, gid int, _ error) {
	userID, userName := getIDOrName(nameOrID)
	if userName != "" {