#! /usr/bin/python3
#
# dput/dcut.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.

""" dcut — Debian command upload tool. """

import os
import pwd
import shutil
import string
import subprocess
import sys
import tempfile
import textwrap
import time

from . import configfile
from . import dput
from .helper import dputhelper


validcommands = ("rm", "cancel", "reschedule")


def make_usage_message():
    """ Make the program usage help message. """
    text = textwrap.dedent("""\
        Usage: {progname} [options] [host] command [, command]
         Supported options (see man page for long forms):
           -c file Config file to parse.
           -d      Enable debug messages.
           -h      Display this help message.
           -s      Simulate the commands file creation only.
           -v      Display version information.
           -m maintaineraddress
                   Use maintainer information in "Uploader:" field.
           -k keyid
                   Use this keyid for signing.
           -O file Write commands to file.
           -U file Upload specified commands file (presently no checks).
           -i changes
                   Upload a commands file to remove files listed in .changes.
         Supported commands: cancel, reschedule, rm
           (No paths or command-line options allowed on ftp-master.)
        """).format(progname=dputhelper.get_progname(sys.argv))
    return text


def get_uploader_from_system(*, environ=None, debug=False):
    """ Get the system value for the ‘uploader’ specification.

        :param environ: Mapping of environment variables for this
            process. (If ``None``, default to `os.environ`.)
        :param debug: If true, enable debugging output.
        :return: The uploader specification as a person specification
            "Full Name <localpart@example.org>", if it can be
            determined; otherwise ``None``.
        """
    uploader = None

    # check environment for maintainer
    if debug:
        sys.stdout.write(
                "D: trying to get maintainer email from environment\n")

    if environ is None:
        environ = os.environ

    if 'DEBEMAIL' in environ:
        uploader = dputhelper.make_person_text_from_fields(
                raw_text=environ['DEBEMAIL'],
                full_name=environ.get('DEBFULLNAME', ""))
        if debug:
            sys.stdout.write(
                    "D: Uploader from env: {}\n".format(uploader))
    elif 'EMAIL' in environ:
        uploader = dputhelper.make_person_text_from_fields(
                raw_text=environ['EMAIL'],
                full_name=environ.get('DEBFULLNAME', ""))
        if debug:
            sys.stdout.write(
                    "D: Uploader from env: {}\n".format(uploader))
    else:
        if debug:
            sys.stdout.write("D: Guessing uploader\n")
        pwrec = pwd.getpwuid(os.getuid())
        username = pwrec[0]
        fullname = pwrec[4].split(",")[0]
        try:
            hostname = open("/etc/mailname").read().strip()
        except IOError:
            hostname = ""
        if not hostname:
            if debug:
                sys.stdout.write(
                        "D: Guessing uploader: /etc/mailname was a failure\n")
            hostname_subprocess = subprocess.Popen(
                    "/bin/hostname --fqdn",
                    shell=True, stdout=subprocess.PIPE)
            hostname_stdout = dputhelper.make_text_stream(
                    hostname_subprocess.stdout)
            hostname = hostname_stdout.read().strip()
        if hostname:
            uploader = (
                    "{fullname} <{username}@{hostname}>".format(**vars()))
            if debug:
                sys.stdout.write(
                        "D: Guessed uploader: {}\n".format(uploader))
        else:
            if debug:
                sys.stdout.write("D: Couldn't guess uploader\n")

    return uploader


def getoptions():
    # seed some defaults
    options = {
            'debug': 0, 'simulate': 0, 'config': None, 'host': None,
            'uploader': None, 'keyid': None, 'commandline_passive': None,
            'filetocreate': None, 'filetoupload': None, 'changes': None}
    progname = dputhelper.get_progname(sys.argv)
    version = dputhelper.get_distribution_version()

    # enable debugging very early
    if ("-d" in sys.argv[1:] or "--debug" in sys.argv[1:]):
        options['debug'] = 1
        sys.stdout.write("D: {progname} {version}\n".format(**vars()))

    # parse command line arguments
    (opts, arguments) = dputhelper.getopt(
            sys.argv[1:],
            "c:dDhsvm:k:PU:O:i:", [
                "config=", "debug",
                "help", "simulate", "version", "host=",
                "maintainteraddress=", "keyid=",
                "passive", "upload=", "output=", "input=",
                ])

    for (option, arg) in opts:
        if options['debug']:
            sys.stdout.write((
                    "D: processing arg \"{option}\","
                    " option \"{arg}\"\n").format(**vars()))
        if option in ("-h", "--help"):
            sys.stdout.write(make_usage_message())
            sys.exit(os.EX_OK)
        elif option in ("-v", "--version"):
            sys.stdout.write("{progname} {version}\n".format(**vars()))
            sys.exit(os.EX_OK)
        elif option in ("-d", "--debug"):
            options['debug'] = 1
        elif option in ("-c", "--config"):
            options['config'] = arg
        elif option in ("-m", "--maintaineraddress"):
            options['uploader'] = arg
        elif option in ("-k", "--keyid"):
            options['keyid'] = arg
        elif option in ("-s", "--simulate"):
            options['simulate'] = 1
        elif option in ("-P", "--passive"):
            options['commandline_passive'] = True
        elif option in ("-U", "--upload"):
            options['filetoupload'] = arg
        elif option in ("-O", "--output"):
            options['filetocreate'] = arg
        elif option == "--host":
            options['host'] = arg
        elif option in ("-i", "--input"):
            options['changes'] = arg
        else:
            sys.stderr.write((
                    "{progname} internal error:"
                    " Option {option}, argument {arg} unknown\n"
                    ).format(**vars()))
            sys.exit(1)

    if not options['host'] and arguments and arguments[0] not in validcommands:
        options['host'] = arguments[0]
        if options['debug']:
            sys.stdout.write(
                    "D: first argument \"{}\" treated as host\n".format(
                        options['host']))
        del arguments[0]

    if not options['uploader']:
        options['uploader'] = get_uploader_from_system(debug=options['debug'])

    # we don't create command files without uploader
    if (
            not options['uploader']
            and (options['filetoupload'] or options['changes'])):
        sys.stderr.write((
                "{progname} error: command file cannot be created"
                " without maintainer email\n").format(**vars()))
        sys.stderr.write((
                "{indentation}        please set $DEBEMAIL, $EMAIL"
                " or use the \"-m\" option\n"
                ).format(indentation=(len(progname) * " ")))
        sys.exit(1)

    return options, arguments


def parse_queuecommands(arguments, options, config):
    commands = []
    # want to consume a copy of arguments
    arguments = arguments[:]
    arguments.append(0)
    curarg = []
    while arguments:
        if arguments[0] in validcommands:
            curarg = [arguments[0]]
            if arguments[0] == "rm":
                if len(arguments) > 1 and arguments[1] == "--nosearchdirs":
                    del arguments[1]
                else:
                    curarg.append("--searchdirs")
        else:
            if not curarg and arguments[0] != 0:
                sys.stderr.write(
                        "Error: Could not parse commands at \"{}\"\n".format(
                            arguments[0]))
                sys.exit(1)
            if str(arguments[0])[-1] in (",", ";", 0):
                curarg.append(arguments[0][0:-1])
                arguments[0] = ","
            if arguments[0] in (",", ";", 0) and curarg:
                # TV-TODO: syntax check for #args etc.
                if options['debug']:
                    sys.stdout.write(
                            "D: Successfully parsed command \"{}\"\n".format(
                                " ".join(curarg)))
                commands.append(" ".join(curarg))
                curarg = []
            else:
                # TV-TODO: maybe syntax check the arguments here
                curarg.append(arguments[0])
        del arguments[0]
    if not commands:
        sys.stderr.write("Error: no arguments given, see dcut -h\n")
        sys.exit(1)
    return commands


def write_commands(commands, *, options, tempdir):
    """ Write a file of commands for the upload queue daemon.

        :param commands: Commands to write, as a sequence of text
            strings.
        :param options: Program configuration, as a mapping of options
            `{name: value}`.
        :param tempdir: Filesystem path to directory for temporary files.
        :return: Filesystem path of file which was written.

        Write the specified sequence of commands to a file, in the
        format required for the Debian upload queue management daemon.

        Once writing is finished, the file is signed using the
        'debsign' command.

        If not specified in the configuration option 'filetocreate', a
        default filename is generated. In either case, the resulting
        filename is returned.
        """
    progname = dputhelper.get_progname(sys.argv)
    if options['filetocreate']:
        filename = options['filetocreate']
    else:
        translationorig = "".join("{control_chars}{letters}{digits}".format(
                control_chars="".join(map(chr, range(256))),
                letters=string.ascii_letters,
                digits=string.digits))
        translationdest = "".join("{control_chars}{letters}{digits}".format(
                control_chars=(256 * "_"),
                letters=string.ascii_letters,
                digits=string.digits))
        translationmap = str.maketrans(translationorig, translationdest)
        uploadpartforname = options['uploader'].translate(translationmap)
        filename_stem = ".".join([
                "{progname}",
                "{uploader_mapped}",
                "{time:d}",
                "{pid:d}",
                ]).format(
                    progname=progname,
                    uploader_mapped=uploadpartforname,
                    time=int(time.time()),
                    pid=os.getpid())
        filename = "{}.commands".format(filename_stem)
        if tempdir:
            filename = os.path.join(tempdir, filename)
    f = open(filename, 'w', encoding='UTF-8')
    f.write("Uploader: {}\n".format(options['uploader']))
    f.write("Commands:\n {}\n\n".format("\n ".join(commands)))
    f.close()
    debsign_cmdline = ['debsign']
    debsign_cmdline.append("-m{}".format(options['uploader']))
    if options['keyid']:
        debsign_cmdline.append("-k{}".format(options['keyid']))
    debsign_cmdline.append(filename)
    if options['debug']:
        sys.stdout.write("D: calling debsign: {}\n".format(debsign_cmdline))
    try:
        subprocess.check_call(debsign_cmdline)
    except subprocess.CalledProcessError:
        sys.stderr.write("Error: debsign failed.\n")
        sys.exit(1)
    return filename


def dcut():
    options, arguments = getoptions()
    if options['debug']:
        sys.stdout.write("D: reading program configuration\n")
    try:
        config_files = list(configfile.active_config_files(
                options['config'], options['debug']))
        config = dput.read_configs(config_files, options['debug'])
    except configfile.ConfigurationError as exc:
        sys.stderr.write("Error: {0}\n".format(str(exc)))
        sys.stderr.write("Failed to read program configuration; aborting.\n")
        sys.exit(1)

    if (
            not options['host']
            and config.has_option('DEFAULT', 'default_host_main')):
        options['host'] = config.get('DEFAULT', 'default_host_main')
        if options['debug']:
            sys.stdout.write(
                    "D: Using host \"{}\" (default_host_main)\n".format(
                        options['host']))
        if not options['host']:
            options['host'] = "ftp-master"
            if options['debug']:
                sys.stdout.write(
                        "D: Using host \"{}\" (hardcoded)\n".format(
                            options['host']))
    tempdir = None
    filename = None
    progname = dputhelper.get_progname(sys.argv)
    try:
        if not (options['filetoupload'] or options['filetocreate']):
            tempdir = tempfile.mkdtemp(prefix="{}.".format(progname))
        if not options['filetocreate']:
            if not options['host']:
                sys.stdout.write(
                        "Error: No host specified"
                        " and no default found in config\n")
                sys.exit(1)
            if not config.has_section(options['host']):
                sys.stdout.write(
                        "No host {} found in config\n".format(options['host']))
                sys.exit(1)
            else:
                if config.has_option(options['host'], 'allow_dcut'):
                    dcut_allowed = config.getboolean(
                            options['host'], 'allow_dcut')
                else:
                    dcut_allowed = config.getboolean('DEFAULT', 'allow_dcut')
                if not dcut_allowed:
                    sys.stdout.write(
                            "Error: dcut is not supported"
                            " for this upload queue.\n")
                    sys.exit(1)
        if options['filetoupload']:
            if arguments:
                sys.stdout.write(
                        "Error: cannot take commands"
                        " when uploading existing file,\n"
                        "       \"{}\" found\n".format(" ".join(arguments)))
                sys.exit(1)
            commands = None
            filename = options['filetoupload']
            if not filename.endswith(".commands"):
                sys.stdout.write(textwrap.dedent("""\
                        Error: I'm insisting on the ‘.commands’ suffix, which
                               ‘{}’ doesn't have.
                        """).format(filename))
            # TV-TODO: check file to be readable?
        elif options['changes']:
            parse_changes = dput.parse_changes
            commands = create_commands(options, parse_changes)
            filename = write_commands(
                    commands,
                    options=options,
                    tempdir=tempdir,
                    )
        else:
            commands = parse_queuecommands(arguments, options, config)
            filename = write_commands(
                    commands,
                    options=options,
                    tempdir=tempdir,
                    )
        if not options['filetocreate']:
            upload_methods = dput.import_upload_functions()
            dput.upload_files(
                    upload_methods, config, options['host'],
                    [filename],
                    simulate=options['simulate'],
                    commandline_passive=options['commandline_passive'],
                    debug=options['debug'])
    finally:
        # we use sys.exit, so we need to clean up here
        if tempdir:
            shutil.rmtree(tempdir)


def create_commands(options, parse_changes):
    """ Get the removal commands from a package changes file.

        :param options: The command-line options, as attributes in a namespace.
        :param parse_changes: Callable object that accepts the package changes
            file, and returns the mapping of fields.
        :return: A sequence of `rm` commands for the files to remove on the
            archive host.

        Parse the specified ‘foo.changes’ file and returns commands to
        remove files named in it.
        """
    changes_file_path = options['changes']
    if options['debug']:
        sys.stdout.write(
                "D: Parsing changes file ({}) for files to remove\n".format(
                    changes_file_path))
    try:
        changes_file = open(changes_file_path, 'r')
    except IOError:
        sys.stdout.write("Can't open changes file: {}\n".format(
                changes_file_path))
        sys.exit(1)
    changes = parse_changes(changes_file)
    changes_file.close
    removecommands = [
            "rm --searchdirs {}".format(os.path.basename(changes_file_path))]
    for file_spec in changes['files'].strip().split("\n"):
        # Filename only.
        file_path = file_spec.split()[4]
        rm = "rm --searchdirs {}".format(file_path)
        if options['debug']:
            sys.stdout.write(
                "D: Will remove {file_path} with '{rm}'\n".format(**vars()))
        removecommands.append(rm)
    return removecommands


# Copyright © 2015–2024 Ben Finney <bignose@debian.org>
# Copyright © 2008–2013 Y Giridhar Appaji Nag <appaji@debian.org>
# Copyright © 2004–2009 Thomas Viehmann <tv@beamnet.de>
# Copyright © 2000–2004 Christian Kurz <shorty@debian.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

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