//go:build !windows

package daemon

import (
	"fmt"
	"math"
	"strconv"
	"strings"

	"github.com/moby/go-archive"
	"github.com/moby/moby/v2/daemon/container"
	"github.com/moby/moby/v2/errdefs"
	"github.com/moby/sys/user"
)

func (daemon *Daemon) applyTarCopyUserOptions(ctr *container.Container, opts *archive.TarOptions) error {
	if ctr.Config.User == "" {
		return nil
	}

	uid, gid, err := getUIDGID(ctr.Config.User)
	if err != nil {
		return errdefs.InvalidParameter(err)
	}
	uid, gid, err = daemon.idMapping.ToHost(uid, gid)
	if err != nil {
		return errdefs.InvalidParameter(err)
	}

	// ChownOpts overrides archive ownership, so avoid remapping header IDs.
	opts.IDMap = user.IdentityMapping{}
	opts.ChownOpts = &archive.ChownOpts{UID: uid, GID: gid}
	return nil
}

// getUIDGID resolves the UID and GID of a given container's Config.User,
// which can contain a user name (or ID) and, optionally, group (or ID).
//
// usergrp is a username or uid, and optional group, in the format `user[:group]`.
// Both `user` and `group` can be provided as an `uid` / `gid`, so the following
// formats are supported:
//
// - username            - valid username from /etc/passwd
// - 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 != "" {
		u, err := user.LookupUser(userName)
		if err != nil {
			return 0, 0, fmt.Errorf("failed to look up user %q in container: %w", userName, err)
		}
		return u.Uid, u.Gid, nil
	}

	u, err := user.LookupUid(userID)
	if err != nil {
		// Match behavior of "docker run": when using a UID for the
		// user, resolving the user and its primary group is best-effort.
		// If a user with the given UID is found, we use its primary
		// group, otherwise use it as-is and use the default (0) as
		//  GID.
		//
		//	docker run --rm --user 12345 ubuntu id
		//	uid=12345 gid=0(root) groups=0(root)
		//
		//	docker run --rm ubuntu cat /etc/passwd | grep www-data
		//	www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
		//
		//	docker run --rm --user 33 ubuntu id
		//	uid=33(www-data) gid=33(www-data) groups=33(www-data)
		return userID, 0, nil
	}
	return u.Uid, u.Gid, nil
}

func lookupGID(nameOrID string) (int, error) {
	groupID, groupName := getIDOrName(nameOrID)
	if groupName == "" {
		// GID is passed, no need to look up
		return groupID, nil
	}
	group, err := user.LookupGroup(groupName)
	if err != nil {
		return 0, fmt.Errorf("failed to look up group %q in container: %w", nameOrID, err)
	}
	return group.Gid, nil
}
