.. versionchanged:: 2016.3.5
        Prior to this version, only the ``file_name`` argument was considered
        for filename matches in the hash file. This would be problematic for
        cases in which the user was relying on a remote checksum file that they
        do not control, and they wished to use a different name for that file
        on the minion from the filename on the remote server (and in the
        checksum file). For example, managing ``/tmp/myfile.tar.gz`` when the
        remote file was at ``https://mydomain.tld/different_name.tar.gz``. The
        :py:func:`file.managed <salt.states.file.managed>` state now also
        passes this function the source URI as well as the ``source_hash_name``
        (if specified). In cases where ``source_hash_name`` is specified, it
        takes precedence over both the ``file_name`` and ``source``. When it is
        not specified, ``file_name`` takes precedence over ``source``. This
        allows for better capability for matching hashes.
    .. versionchanged:: 2016.11.0
        File name and source URI matches are no longer disregarded when
        ``source_hash_name`` is specified. They will be used as fallback
        matches if there is no match to the ``source_hash_name`` value.

    This routine is called from the :mod:`file.managed
    <salt.states.file.managed>` state to pull a hash from a remote file.
    Regular expressions are used line by line on the ``source_hash`` file, to
    find a potential candidate of the indicated hash type. This avoids many
    problems of arbitrary file layout rules. It specifically permits pulling
    hash codes from debian ``*.dsc`` files.

    If no exact match of a hash and filename are found, then the first hash
    found (if any) will be returned. If no hashes at all are found, then
    ``None`` will be returned.

    For example:

    .. code-block:: yaml

        openerp_7.0-latest-1.tar.gz:
          file.managed:
            - name: /tmp/openerp_7.0-20121227-075624-1_all.deb
            - source: http://nightly.openerp.com/7.0/nightly/deb/openerp_7.0-20121227-075624-1.tar.gz
            - source_hash: http://nightly.openerp.com/7.0/nightly/deb/openerp_7.0-20121227-075624-1.dsc

    CLI Example:

    .. code-block:: bash

        salt '*' file.extract_hash /path/to/hash/file sha512 /etc/foo

def extract_hash(hash_fn,
                 hash_type='sha256',
                 file_name='',
                 source='',
                 source_hash_name=None):
    '''
    .. versionchanged:: 2016.3.5
        Prior to this version, only the ``file_name`` argument was considered
        for filename matches in the hash file. This would be problematic for
        cases in which the user was relying on a remote checksum file that they
        do not control, and they wished to use a different name for that file
        on the minion from the filename on the remote server (and in the
        checksum file). For example, managing ``/tmp/myfile.tar.gz`` when the
        remote file was at ``https://mydomain.tld/different_name.tar.gz``. The
        :py:func:`file.managed <salt.states.file.managed>` state now also
        passes this function the source URI as well as the ``source_hash_name``
        (if specified). In cases where ``source_hash_name`` is specified, it
        takes precedence over both the ``file_name`` and ``source``. When it is
        not specified, ``file_name`` takes precedence over ``source``. This
        allows for better capability for matching hashes.
    .. versionchanged:: 2016.11.0
        File name and source URI matches are no longer disregarded when
        ``source_hash_name`` is specified. They will be used as fallback
        matches if there is no match to the ``source_hash_name`` value.

    This routine is called from the :mod:`file.managed
    <salt.states.file.managed>` state to pull a hash from a remote file.
    Regular expressions are used line by line on the ``source_hash`` file, to
    find a potential candidate of the indicated hash type. This avoids many
    problems of arbitrary file layout rules. It specifically permits pulling
    hash codes from debian ``*.dsc`` files.

    If no exact match of a hash and filename are found, then the first hash
    found (if any) will be returned. If no hashes at all are found, then
    ``None`` will be returned.

    For example:

    .. code-block:: yaml

        openerp_7.0-latest-1.tar.gz:
          file.managed:
            - name: /tmp/openerp_7.0-20121227-075624-1_all.deb
            - source: http://nightly.openerp.com/7.0/nightly/deb/openerp_7.0-20121227-075624-1.tar.gz
            - source_hash: http://nightly.openerp.com/7.0/nightly/deb/openerp_7.0-20121227-075624-1.dsc

    CLI Example:

    .. code-block:: bash

        salt '*' file.extract_hash /path/to/hash/file sha512 /etc/foo
    '''
    hash_len = HASHES.get(hash_type)
    if hash_len is None:
        if hash_type:
            log.warning(
                'file.extract_hash: Unsupported hash_type \'%s\', falling '
                'back to matching any supported hash_type', hash_type
            )
            hash_type = ''
        hash_len_expr = '{0},{1}'.format(min(HASHES_REVMAP), max(HASHES_REVMAP))
    else:
        hash_len_expr = six.text_type(hash_len)

    filename_separators = string.whitespace + r'\/'

    if source_hash_name:
        if not isinstance(source_hash_name, six.string_types):
            source_hash_name = six.text_type(source_hash_name)
        source_hash_name_idx = (len(source_hash_name) + 1) * -1
        log.debug(
            'file.extract_hash: Extracting %s hash for file matching '
            'source_hash_name \'%s\'',
            'any supported' if not hash_type else hash_type,
            source_hash_name
        )
    if file_name:
        if not isinstance(file_name, six.string_types):
            file_name = six.text_type(file_name)
        file_name_basename = os.path.basename(file_name)
        file_name_idx = (len(file_name_basename) + 1) * -1
    if source:
        if not isinstance(source, six.string_types):
            source = six.text_type(source)
        urlparsed_source = _urlparse(source)
        source_basename = os.path.basename(
            urlparsed_source.path or urlparsed_source.netloc
        )
        source_idx = (len(source_basename) + 1) * -1

    basename_searches = [x for x in (file_name, source) if x]
    if basename_searches:
        log.debug(
            'file.extract_hash: %s %s hash for file matching%s: %s',
            'If no source_hash_name match found, will extract'
                if source_hash_name
                else 'Extracting',
            'any supported' if not hash_type else hash_type,
            '' if len(basename_searches) == 1 else ' either of the following',
            ', '.join(basename_searches)
        )

    partial = None
    found = {}

    with salt.utils.files.fopen(hash_fn, 'r') as fp_:
        for line in fp_:
            line = salt.utils.stringutils.to_unicode(line.strip())
            hash_re = r'(?i)(?<![a-z0-9])([a-f0-9]{' + hash_len_expr + '})(?![a-z0-9])'
            hash_match = re.search(hash_re, line)
            matched = None
            if hash_match:
                matched_hsum = hash_match.group(1)
                if matched_hsum is not None:
                    matched_type = HASHES_REVMAP.get(len(matched_hsum))
                    if matched_type is None:
                        # There was a match, but it's not of the correct length
                        # to match one of the supported hash types.
                        matched = None
                    else:
                        matched = {'hsum': matched_hsum,
                                   'hash_type': matched_type}

            if matched is None:
                log.debug(
                    'file.extract_hash: In line \'%s\', no %shash found',
                    line,
                    '' if not hash_type else hash_type + ' '
                )
                continue

            if partial is None:
                partial = matched

            def _add_to_matches(found, line, match_type, value, matched):
                log.debug(
                    'file.extract_hash: Line \'%s\' matches %s \'%s\'',
                    line, match_type, value
                )
                found.setdefault(match_type, []).append(matched)

            hash_matched = False
            if source_hash_name:
                if line.endswith(source_hash_name):
                    # Checking the character before where the basename
                    # should start for either whitespace or a path
                    # separator. We can't just rsplit on spaces/whitespace,
                    # because the filename may contain spaces.
                    try:
                        if line[source_hash_name_idx] in string.whitespace:
                            _add_to_matches(found, line, 'source_hash_name',
                                            source_hash_name, matched)
                            hash_matched = True
                    except IndexError:
                        pass
                elif re.match(re.escape(source_hash_name) + r'\s+',
                              line):
                    _add_to_matches(found, line, 'source_hash_name',
                                    source_hash_name, matched)
                    hash_matched = True
            if file_name:
                if line.endswith(file_name_basename):
                    # Checking the character before where the basename
                    # should start for either whitespace or a path
                    # separator. We can't just rsplit on spaces/whitespace,
                    # because the filename may contain spaces.
                    try:
                        if line[file_name_idx] in filename_separators:
                            _add_to_matches(found, line, 'file_name',
                                            file_name, matched)
                            hash_matched = True
                    except IndexError:
                        pass
                elif re.match(re.escape(file_name) + r'\s+', line):
                    _add_to_matches(found, line, 'file_name',
                                    file_name, matched)
                    hash_matched = True
            if source:
                if line.endswith(source_basename):
                    # Same as above, we can't just do an rsplit here.
                    try:
                        if line[source_idx] in filename_separators:
                            _add_to_matches(found, line, 'source',
                                            source, matched)
                            hash_matched = True
                    except IndexError:
                        pass
                elif re.match(re.escape(source) + r'\s+', line):
                    _add_to_matches(found, line, 'source', source, matched)
                    hash_matched = True

            if not hash_matched:
                log.debug(
                    'file.extract_hash: Line \'%s\' contains %s hash '
                    '\'%s\', but line did not meet the search criteria',
                    line, matched['hash_type'], matched['hsum']
                )

    for found_type, found_str in (('source_hash_name', source_hash_name),
                                  ('file_name', file_name),
                                  ('source', source)):
        if found_type in found:
            if len(found[found_type]) > 1:
                log.debug(
                    'file.extract_hash: Multiple %s matches for %s: %s',
                    found_type,
                    found_str,
                    ', '.join(
                        ['{0} ({1})'.format(x['hsum'], x['hash_type'])
                         for x in found[found_type]]
                    )
                )
            ret = found[found_type][0]
            log.debug(
                'file.extract_hash: Returning %s hash \'%s\' as a match of %s',
                ret['hash_type'], ret['hsum'], found_str
            )
            return ret

    if partial:
        log.debug(
            'file.extract_hash: Returning the partially identified %s hash '
            '\'%s\'', partial['hash_type'], partial['hsum']
        )
        return partial

    log.debug('file.extract_hash: No matches, returning None')
    return None