.. versionadded:: 2016.11.0

    Used by :py:func:`file.get_managed <salt.modules.file.get_managed>` to
    obtain the hash and hash type from the parameters specified below.

    file_name
        Optional file name being managed, for matching with
        :py:func:`file.extract_hash <salt.modules.file.extract_hash>`.

    source
        Source file, as used in :py:mod:`file <salt.states.file>` and other
        states. If ``source_hash`` refers to a file containing hashes, then
        this filename will be used to match a filename in that file. If the
        ``source_hash`` is a hash expression, then this argument will be
        ignored.

    source_hash
        Hash file/expression, as used in :py:mod:`file <salt.states.file>` and
        other states. If this value refers to a remote URL or absolute path to
        a local file, it will be cached and :py:func:`file.extract_hash
        <salt.modules.file.extract_hash>` will be used to obtain a hash from
        it.

    source_hash_name
        Specific file name to look for when ``source_hash`` refers to a remote
        file, used to disambiguate ambiguous matches.

    saltenv : base
        Salt fileserver environment from which to retrieve the source_hash. This
        value will only be used when ``source_hash`` refers to a file on the
        Salt fileserver (i.e. one beginning with ``salt://``).

    CLI Example:

    .. code-block:: bash

        salt '*' file.get_source_sum /tmp/foo.tar.gz source=http://mydomain.tld/foo.tar.gz source_hash=499ae16dcae71eeb7c3a30c75ea7a1a6
        salt '*' file.get_source_sum /tmp/foo.tar.gz source=http://mydomain.tld/foo.tar.gz source_hash=https://mydomain.tld/hashes.md5
        salt '*' file.get_source_sum /tmp/foo.tar.gz source=http://mydomain.tld/foo.tar.gz source_hash=https://mydomain.tld/hashes.md5 source_hash_name=./dir2/foo.tar.gz

def get_source_sum(file_name='',
                   source='',
                   source_hash=None,
                   source_hash_name=None,
                   saltenv='base'):
    '''
    .. versionadded:: 2016.11.0

    Used by :py:func:`file.get_managed <salt.modules.file.get_managed>` to
    obtain the hash and hash type from the parameters specified below.

    file_name
        Optional file name being managed, for matching with
        :py:func:`file.extract_hash <salt.modules.file.extract_hash>`.

    source
        Source file, as used in :py:mod:`file <salt.states.file>` and other
        states. If ``source_hash`` refers to a file containing hashes, then
        this filename will be used to match a filename in that file. If the
        ``source_hash`` is a hash expression, then this argument will be
        ignored.

    source_hash
        Hash file/expression, as used in :py:mod:`file <salt.states.file>` and
        other states. If this value refers to a remote URL or absolute path to
        a local file, it will be cached and :py:func:`file.extract_hash
        <salt.modules.file.extract_hash>` will be used to obtain a hash from
        it.

    source_hash_name
        Specific file name to look for when ``source_hash`` refers to a remote
        file, used to disambiguate ambiguous matches.

    saltenv : base
        Salt fileserver environment from which to retrieve the source_hash. This
        value will only be used when ``source_hash`` refers to a file on the
        Salt fileserver (i.e. one beginning with ``salt://``).

    CLI Example:

    .. code-block:: bash

        salt '*' file.get_source_sum /tmp/foo.tar.gz source=http://mydomain.tld/foo.tar.gz source_hash=499ae16dcae71eeb7c3a30c75ea7a1a6
        salt '*' file.get_source_sum /tmp/foo.tar.gz source=http://mydomain.tld/foo.tar.gz source_hash=https://mydomain.tld/hashes.md5
        salt '*' file.get_source_sum /tmp/foo.tar.gz source=http://mydomain.tld/foo.tar.gz source_hash=https://mydomain.tld/hashes.md5 source_hash_name=./dir2/foo.tar.gz
    '''
    def _invalid_source_hash_format():
        '''
        DRY helper for reporting invalid source_hash input
        '''
        raise CommandExecutionError(
            'Source hash {0} format is invalid. The supported formats are: '
            '1) a hash, 2) an expression in the format <hash_type>=<hash>, or '
            '3) either a path to a local file containing hashes, or a URI of '
            'a remote hash file. Supported protocols for remote hash files '
            'are: {1}. The hash may also not be of a valid length, the '
            'following are supported hash types and lengths: {2}.'.format(
                source_hash,
                ', '.join(salt.utils.files.VALID_PROTOS),
                ', '.join(
                    ['{0} ({1})'.format(HASHES_REVMAP[x], x)
                     for x in sorted(HASHES_REVMAP)]
                ),
            )
        )

    hash_fn = None
    if os.path.isabs(source_hash):
        hash_fn = source_hash
    else:
        try:
            proto = _urlparse(source_hash).scheme
            if proto in salt.utils.files.VALID_PROTOS:
                hash_fn = __salt__['cp.cache_file'](source_hash, saltenv)
                if not hash_fn:
                    raise CommandExecutionError(
                        'Source hash file {0} not found'.format(source_hash)
                    )
            else:
                if proto != '':
                    # Some unsupported protocol (e.g. foo://) is being used.
                    # We'll get into this else block if a hash expression
                    # (like md5=<md5 checksum here>), but in those cases, the
                    # protocol will be an empty string, in which case we avoid
                    # this error condition.
                    _invalid_source_hash_format()
        except (AttributeError, TypeError):
            _invalid_source_hash_format()

    if hash_fn is not None:
        ret = extract_hash(hash_fn, '', file_name, source, source_hash_name)
        if ret is None:
            _invalid_source_hash_format()
        ret['hsum'] = ret['hsum'].lower()
        return ret
    else:
        # The source_hash is a hash expression
        ret = {}
        try:
            ret['hash_type'], ret['hsum'] = \
                [x.strip() for x in source_hash.split('=', 1)]
        except AttributeError:
            _invalid_source_hash_format()
        except ValueError:
            # No hash type, try to figure out by hash length
            if not re.match('^[{0}]+$'.format(string.hexdigits), source_hash):
                _invalid_source_hash_format()
            ret['hsum'] = source_hash
            source_hash_len = len(source_hash)
            if source_hash_len in HASHES_REVMAP:
                ret['hash_type'] = HASHES_REVMAP[source_hash_len]
            else:
                _invalid_source_hash_format()

        if ret['hash_type'] not in HASHES:
            raise CommandExecutionError(
                'Invalid hash type \'{0}\'. Supported hash types are: {1}. '
                'Either remove the hash type and simply use \'{2}\' as the '
                'source_hash, or change the hash type to a supported type.'
                .format(ret['hash_type'], ', '.join(HASHES), ret['hsum'])
            )
        else:
            hsum_len = len(ret['hsum'])
            if hsum_len not in HASHES_REVMAP:
                _invalid_source_hash_format()
            elif hsum_len != HASHES[ret['hash_type']]:
                raise CommandExecutionError(
                    'Invalid length ({0}) for hash type \'{1}\'. Either '
                    'remove the hash type and simply use \'{2}\' as the '
                    'source_hash, or change the hash type to \'{3}\''.format(
                        hsum_len,
                        ret['hash_type'],
                        ret['hsum'],
                        HASHES_REVMAP[hsum_len],
                    )
                )

        ret['hsum'] = ret['hsum'].lower()
        return ret