# dput/helper/dputhelper.py
# Part of ‘dput’, a Debian package upload toolkit.
#
# This is free software, and you are welcome to redistribute it under
# certain conditions; see the end of this file for copyright
# information, grant of license, and disclaimer of warranty.

""" Helper code for Dput. """

import importlib.metadata
import io
import locale
import os
import pwd
import subprocess
import sys


class DputException(Exception):
    pass


class DputUploadFatalException(DputException):
    pass


class UploadChangesInvalidVersionDputError(DputException):
    """
    Raised when an invalid ‘version’ field from upload changes control data.
    """


EXIT_STATUS_SUCCESS = 0
EXIT_STATUS_FAILURE = 1
EXIT_STATUS_COMMAND_NOT_FOUND = 127


# This wrapper is intended as a migration target for the prior
# `spawnv` wrapper, and will produce the same output for now.
def check_call(args, *posargs, **kwargs):
    """ Wrap `subprocess.check_call` with error output. """
    command_file_path = args[0]
    try:
        subprocess.check_call(args, *posargs, **kwargs)
        exit_status = EXIT_STATUS_SUCCESS
    except subprocess.CalledProcessError as exc:
        exit_status = exc.returncode
        if exit_status == EXIT_STATUS_COMMAND_NOT_FOUND:
            sys.stderr.write(
                    "Error: Failed to execute '{path}'.\n"
                    "       "
                    "The file may not exist or not be executable.\n".format(
                        path=command_file_path))
        else:
            sys.stderr.write(
                    "Warning: The execution of '{path}' as\n"
                    "  '{command}'\n"
                    "  returned a nonzero exit code.\n".format(
                        path=command_file_path, command=" ".join(args)))
    return exit_status


class FileWithProgress:
    """ Mimics a file (passed as f, an open file), but with progress.

        FileWithProgress(f, args)

        args:
        * ptype = 1,2 is the type ("|/-\" or numeric), default 0 (no progress)
        * progressf = file to output progress to (default sys.stdout)
        * size = size of file (or -1, the default, to ignore)
                 for numeric output
        * step = stepsize (default 1024)
        """

    def __init__(self, f, ptype=0, progressf=sys.stdout, size=-1, step=1024):
        self.f = f
        self.count = 0
        self.lastupdate = 0
        self.ptype = ptype
        self.ppos = 0
        self.progresschars = ["|", "/", "-", "\\"]
        self.progressf = progressf
        self.size = size
        self.step = step
        self.closed = 0

    def __getattr__(self, name):
        return getattr(self.f, name)

    def read(self, size=-1):
        a = self.f.read(size)
        self.count = self.count + len(a)
        if (self.count - self.lastupdate) > 1024:
            if self.ptype == 1:
                self.ppos = (self.ppos + 1) % len(self.progresschars)
                self.progressf.write("{backspace}{spinner}".format(
                        backspace=("\b" if self.lastupdate else ""),
                        spinner=self.progresschars[self.ppos]))
                self.progressf.flush()
                self.lastupdate = self.count
            elif self.ptype == 2:
                s = "{size:d}k".format(size=(self.count // self.step))
                if self.size >= 0:
                    s = "{text}/{size:d}k".format(
                            text=s,
                            size=((self.size + self.step - 1) // self.step))
                s = "{text}{spaces}".format(
                        text=s,
                        spaces=(min(self.ppos - len(s), 0) * " "))
                self.progressf.write("{backspaces}{text}".format(
                        backspaces=(self.ppos * "\b"),
                        text=s))
                self.progressf.flush()
                self.ppos = len(s)
        return a

    def close(self):
        if not self.closed:
            self.f.close()
            self.closed = 1
            if self.ptype == 1:
                if self.lastupdate:
                    self.progressf.write("\b \b")
                    self.progressf.flush()
            elif self.ptype == 2:
                self.progressf.write(
                        "{backspaces}{spaces}{backspaces}".format(
                            backspaces=(self.ppos * "\b"),
                            spaces=(self.ppos * " ")))
                self.progressf.flush()

    def __del__(self):
        self.close()


def make_text_stream(stream):
    """ Make a text stream from the specified stream.

        :param stream: An open file-like object.
        :return: A stream object providing text I/O.

        In the normal case, the specified stream is a stream providing
        bytes I/O. We create an `io.TextIOWrapper` with the
        appropriate encoding for the byte stream, and return that
        wrapper stream.

        The text encoding is determined by interrogating the file
        object. If the file object has no declared encoding, the
        default `locale.getpreferredencoding(False)` is used.

        If the stream is a `io.TextIOBase` instance, it is already
        providing text I/O. In this case, the stream is returned as
        is.
        """
    result = None

    if hasattr(stream, 'encoding'):
        encoding = stream.encoding
    else:
        encoding = locale.getpreferredencoding(False)

    if isinstance(stream, io.TextIOBase):
        result = stream
    else:
        result = io.TextIOWrapper(stream, encoding=encoding)

    return result


def get_progname(argv=None):
    """ Get the program name from the command line arguments.

        :param argv: Sequence of command-line arguments.
            Defaults to `sys.argv`.
        :return: The program name used to invoke this program.
        """
    if argv is None:
        argv = sys.argv
    progname = os.path.basename(argv[0])
    return progname


def get_distribution_version():
    """ Get the version string for this distribution. """
    distribution = importlib.metadata.distribution("dput")
    return distribution.version


def getopt(args, shortopts, longopts):
    args = args[:]
    optlist = []
    while args and args[0].startswith("-"):
        if args[0] == "--":
            args = args[1:]
            break
        if args[0] == "-":
            break
        if args[0].startswith("--"):
            opt = args.pop(0)[2:]
            if "=" in opt:
                opt, optarg = opt.split("=", 1)
            else:
                optarg = None
            prefixmatch = [x for x in longopts if x.startswith(opt)]
            if len(prefixmatch) == 0:
                raise DputException("unknown option --{}".format(opt))
            elif len(prefixmatch) > 1:
                raise DputException("non-unique prefix --{}".format(opt))
            opt = prefixmatch[0]
            if opt.endswith("=="):
                opt = opt[:-2]
                optarg = optarg or ""
            elif opt.endswith("="):
                opt = opt[:-1]
                if not optarg:
                    if not args:
                        raise DputException(
                                "option --{} requires argument".format(opt))
                    optarg = args.pop(0)
            else:
                if optarg is not None:
                    raise DputException(
                            "option --{} does not take arguments".format(opt))
                optarg = ""
            optlist.append(("--{}".format(opt), optarg))
        else:
            s = args.pop(0)[1:]
            while s:
                pos = shortopts.find(s[0])
                if pos == -1:
                    raise DputException("option -{} unknown".format(s[0]))
                if pos + 1 >= len(shortopts) or shortopts[pos + 1] != ":":
                    optlist.append(("-{}".format(s[0]), ""))
                    s = s[1:]
                elif len(s) > 1:
                    optlist.append(("-{}".format(s[0]), s[1:]))
                    s = ""
                elif args:
                    optlist.append(("-{}".format(s), args.pop(0)))
                    s = ""
                else:
                    raise DputException(
                            "option -{} requires argument".format(s))
    return optlist, args


def make_person_text_from_fields(*, raw_text, full_name=""):
    """ Make a text representation of a person's `full_name` and `email_addr`.

        :param raw_text: The existing raw text of the field.
        :param full_name: The full name of the person.
        :return: The normalised text representation of the field values.
        """
    if ("<" in raw_text):
        # The raw text already looks like a person text representation.
        person_text = raw_text
    else:
        fields = (
            value for value in [full_name, "<{}>".format(raw_text)]
            if value)
        person_text = " ".join(fields)
    return person_text


def get_username_from_system(debug=False):
    """ Get the system username for the current process.

        :param debug: If true, enable debugging output.
        :return: The username determined from the current process.
        """
    login = None
    if 'USER' in os.environ:
        login = os.environ['USER']
    else:
        sys.stdout.write("$USER not set, will use login information.\n")
        login = pwd.getpwuid(os.getuid())[0]
        if debug:
            sys.stderr.write("D: User-ID: {:d}\n".format(os.getuid()))
    if debug:
        sys.stderr.write("D: Login: {}\n".format(login))

    return login


# Copyright © 2015–2024 Ben Finney <bignose@debian.org>
# Copyright © 2009–2010 Y Giridhar Appaji Nag <appaji@debian.org>
# Copyright © 2007–2008 Thomas Viehmann <tv@beamnet.de>
#
# This is free software: you may copy, modify, and/or distribute this work
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; version 3 of that license or any later version.
# No warranty expressed or implied. See the file ‘LICENSE.GPL-3’ for details.


# Local variables:
# coding: utf-8
# mode: python
# End:
# vim: fileencoding=utf-8 filetype=python :
