# data.py -- Calculated/Discovered Data Values
# Copyright 1998-2014 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2

import grp
import os
import platform
import pwd

import portage
from portage.localization import _

ostype = platform.system()
userland = "GNU"
if ostype == "DragonFly" or ostype.endswith("BSD"):
    userland = "BSD"

lchown = getattr(os, "lchown", None)

if not lchown:
    if ostype == "Darwin":

        def lchown(*_args, **_kwargs):
            pass

    else:

        def lchown(*_args, **_kwargs):
            from portage.output import colorize
            from portage.util import writemsg

            writemsg(
                colorize("BAD", "!!!")
                + _(
                    " It seems that os.lchown does not"
                    " exist.  Please rebuild python.\n"
                ),
                noiselevel=-1,
            )

        lchown()


def _target_eprefix():
    """
    Calculate the target EPREFIX, which may be different from
    portage.const.EPREFIX due to cross-prefix support. The result
    is equivalent to portage.settings["EPREFIX"], but the calculation
    is done without the expense of instantiating portage.settings.
    @rtype: str
    @return: the target EPREFIX
    """
    eprefix = os.environ.get("EPREFIX", portage.const.EPREFIX)
    if eprefix:
        eprefix = portage.util.normalize_path(eprefix)
    return eprefix


def _target_root():
    """
    Calculate the target ROOT. The result is equivalent to
    portage.settings["ROOT"], but the calculation
    is done without the expense of instantiating portage.settings.
    @rtype: str
    @return: the target ROOT (always ends with a slash)
    """
    root = os.environ.get("ROOT")
    if not root:
        # Handle either empty or unset ROOT.
        root = os.sep
    root = portage.util.normalize_path(root)
    return f"{root.rstrip(os.sep)}{os.sep}"


def portage_group_warning():
    from portage.output import colorize
    from portage.util import writemsg

    warn_prefix = colorize("BAD", "*** WARNING ***  ")
    mylines = (
        "For security reasons, only system administrators should be",
        "allowed in the portage group.  Untrusted users or processes",
        "can potentially exploit the portage group for attacks such as",
        "local privilege escalation.",
    )
    for x in mylines:
        writemsg(warn_prefix, noiselevel=-1)
        writemsg(x, noiselevel=-1)
        writemsg("\n", noiselevel=-1)
    writemsg("\n", noiselevel=-1)


# Portage has 3 security levels that depend on the uid and gid of the main
# process and are assigned according to the following table:
#
# Privileges  secpass  uid    gid
# normal      0        any    any
# group       1        any    portage_gid
# super       2        0      any
#
# If the "wheel" group does not exist then wheelgid falls back to 0.
# If the "portage" group does not exist then portage_uid falls back to wheelgid.

# If the current user is not root, but has write access to the
# EROOT directory (not due to the 0002 bit), then use "unprivileged"
# mode which sets secpass = 2 and uses the UID and GID of the EROOT
# directory to generate default PORTAGE_INST_GID, PORTAGE_INST_UID,
# PORTAGE_USERNAME, and PORTAGE_GRPNAME settings.


def _unprivileged_mode(eroot, eroot_st):
    return (
        os.getuid() != 0 and os.access(eroot, os.W_OK) and not eroot_st.st_mode & 0o0002
    )


uid = os.getuid()
wheelgid = 0
try:
    wheelgid = grp.getgrnam("wheel")[2]
except KeyError:
    pass

# The portage_uid and portage_gid global constants, and others that
# depend on them are initialized lazily, in order to allow configuration
# via make.conf. Eventually, these constants may be deprecated in favor
# of config attributes, since it's conceivable that multiple
# configurations with different constants could be used simultaneously.
_initialized_globals = set()


def _get_global(k):
    import subprocess

    from portage.output import colorize
    from portage.util import writemsg
    from portage.util.path import first_existing

    if k in _initialized_globals:
        return globals()[k]

    if k == "secpass":
        unprivileged = False
        if hasattr(portage, "settings"):
            unprivileged = "unprivileged" in portage.settings.features
        else:
            # The config class has equivalent code, but we also need to
            # do it here if _disable_legacy_globals() has been called.
            eroot_or_parent = first_existing(
                os.path.join(_target_root(), _target_eprefix().lstrip(os.sep))
            )
            try:
                eroot_st = os.stat(eroot_or_parent)
            except OSError:
                pass
            else:
                unprivileged = _unprivileged_mode(eroot_or_parent, eroot_st)

        v = 0
        if uid == 0 or unprivileged:
            v = 2
        elif _get_global("portage_gid") in os.getgroups():
            v = 1

    elif k in ("portage_gid", "portage_uid"):
        # Discover the uid and gid of the portage user/group
        keyerror = False
        try:
            portage_uid = pwd.getpwnam(_get_global("_portage_username")).pw_uid
        except KeyError:
            keyerror = True
            portage_uid = 0

        try:
            portage_gid = grp.getgrnam(_get_global("_portage_grpname")).gr_gid
        except KeyError:
            keyerror = True
            portage_gid = 0

        # Suppress this error message if both PORTAGE_GRPNAME and
        # PORTAGE_USERNAME are set to "root", for things like
        # Android (see bug #454060).
        if keyerror and not (
            _get_global("_portage_username") == "root"
            and _get_global("_portage_grpname") == "root"
        ):
            writemsg(
                colorize("BAD", _("portage: 'portage' user or group missing.")) + "\n",
                noiselevel=-1,
            )
            writemsg(
                _(
                    "         For the defaults, line 1 goes into passwd, "
                    "and 2 into group.\n"
                ),
                noiselevel=-1,
            )
            writemsg(
                colorize(
                    "GOOD",
                    "         portage:x:250:250:portage:/var/tmp/portage:/bin/false",
                )
                + "\n",
                noiselevel=-1,
            )
            writemsg(
                colorize("GOOD", "         portage::250:portage") + "\n", noiselevel=-1
            )
            portage_group_warning()

        globals()["portage_gid"] = portage_gid
        _initialized_globals.add("portage_gid")
        globals()["portage_uid"] = portage_uid
        _initialized_globals.add("portage_uid")

        if k == "portage_gid":
            return portage_gid
        if k == "portage_uid":
            return portage_uid
        raise AssertionError(f"unknown name: {k}")

    elif k == "userpriv_groups":
        v = [_get_global("portage_gid")]
        if secpass >= 2:
            # Get a list of group IDs for the portage user. Do not use
            # grp.getgrall() since it is known to trigger spurious
            # SIGPIPE problems with nss_ldap.
            cmd = (x for x in ("id", "-G", _portage_username))
            proc = subprocess.Popen(
                cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
            )
            myoutput = proc.communicate()[0]
            status = proc.wait()
            if os.WIFEXITED(status) and os.WEXITSTATUS(status) == os.EX_OK:

                def check(x):
                    try:
                        return int(x)
                    except ValueError:
                        return None

                unicode_decode = myoutput.decode("utf-8", "strict")
                checked_v = (check(x) for x in unicode_decode.split())
                filtered_v = (x for x in checked_v if x)
                v = sorted(set(filtered_v))

    # Avoid instantiating portage.settings when the desired
    # variable is set in os.environ.
    elif k in ("_portage_grpname", "_portage_username"):
        v = "portage"
        env_key = "PORTAGE_USERNAME"
        if k == "_portage_grpname":
            env_key = "PORTAGE_GRPNAME"

        if env_key in os.environ:
            v = os.environ[env_key]
        elif hasattr(portage, "settings"):
            v = portage.settings.get(env_key, v)
        else:
            # The config class has equivalent code, but we also need to
            # do it here if _disable_legacy_globals() has been called.
            eroot_or_parent = first_existing(
                os.path.join(_target_root(), _target_eprefix().lstrip(os.sep))
            )
            try:
                eroot_st = os.stat(eroot_or_parent)
            except OSError:
                pass
            else:
                if _unprivileged_mode(eroot_or_parent, eroot_st):
                    if k == "_portage_grpname":
                        try:
                            grp_struct = grp.getgrgid(eroot_st.st_gid)
                        except KeyError:
                            pass
                        else:
                            v = grp_struct.gr_name
                    else:
                        try:
                            pwd_struct = pwd.getpwuid(eroot_st.st_uid)
                        except KeyError:
                            pass
                        else:
                            v = pwd_struct.pw_name
    else:
        raise AssertionError(f"unknown name: {k}")

    globals()[k] = v
    _initialized_globals.add(k)
    return v


class _GlobalProxy(portage.proxy.objectproxy.ObjectProxy):
    __slots__ = ("_name",)

    def __init__(self, name):
        portage.proxy.objectproxy.ObjectProxy.__init__(self)
        object.__setattr__(self, "_name", name)

    def _get_target(self):
        return _get_global(object.__getattribute__(self, "_name"))


for k in (
    "portage_gid",
    "portage_uid",
    "secpass",
    "userpriv_groups",
    "_portage_grpname",
    "_portage_username",
):
    globals()[k] = _GlobalProxy(k)
del k


def _init(settings):
    """
    Use config variables like PORTAGE_GRPNAME and PORTAGE_USERNAME to
    initialize global variables. This allows settings to come from make.conf
    instead of requiring them to be set in the calling environment.
    """
    if (
        "_portage_grpname" not in _initialized_globals
        and "_portage_username" not in _initialized_globals
    ):
        # Prevents "TypeError: expected string" errors
        # from grp.getgrnam() with PyPy
        native_string = platform.python_implementation() == "PyPy"

        v = settings.get("PORTAGE_GRPNAME", "portage")
        if native_string:
            v = v
        globals()["_portage_grpname"] = v
        _initialized_globals.add("_portage_grpname")

        v = settings.get("PORTAGE_USERNAME", "portage")
        if native_string:
            v = v
        globals()["_portage_username"] = v
        _initialized_globals.add("_portage_username")

    if "secpass" not in _initialized_globals:
        v = 0
        if uid == 0 or "unprivileged" in settings.features:
            v = 2
        elif portage_gid in os.getgroups():
            v = 1
        globals()["secpass"] = v
        _initialized_globals.add("secpass")
