# Buggy code
def subdict_match(
    data, expr, delimiter=DEFAULT_TARGET_DELIM, regex_match=False, exact_match=False
):
    """
    Check for a match in a dictionary using a delimiter character to denote
    levels of subdicts, and also allowing the delimiter character to be
    matched. Thus, 'foo:bar:baz' will match data['foo'] == 'bar:baz' and
    data['foo']['bar'] == 'baz'. The former would take priority over the
    latter.
    """

    def _match(target, pattern, regex_match=False, exact_match=False):
        if regex_match:
            try:
                return re.match(pattern.lower(), six.text_type(target).lower())
            except Exception:
                log.error("Invalid regex '%s' in match", pattern)
                return False
        elif exact_match:
            return six.text_type(target).lower() == pattern.lower()
        else:
            return fnmatch.fnmatch(six.text_type(target).lower(), pattern.lower())

    def _dict_match(target, pattern, regex_match=False, exact_match=False):
        wildcard = pattern.startswith("*:")
        if wildcard:
            pattern = pattern[2:]

        if pattern == "*":
            # We are just checking that the key exists
            return True
        elif pattern in target:
            # We might want to search for a key
            return True
        elif subdict_match(
            target, pattern, regex_match=regex_match, exact_match=exact_match
        ):
            return True
        if wildcard:
            for key in target:
                if _match(
                    key, pattern, regex_match=regex_match, exact_match=exact_match
                ):
                    return True
                if isinstance(target[key], dict):
                    if _dict_match(
                        target[key],
                        pattern,
                        regex_match=regex_match,
                        exact_match=exact_match,
                    ):
                        return True
                elif isinstance(target[key], list):
                    for item in target[key]:
                        if _match(
                            item,
                            pattern,
                            regex_match=regex_match,
                            exact_match=exact_match,
                        ):
                            return True
        return False

    for idx in range(1, expr.count(delimiter) + 1):
        splits = expr.split(delimiter)
        key = delimiter.join(splits[:idx])
        matchstr = delimiter.join(splits[idx:])
        log.debug(
            "Attempting to match '%s' in '%s' using delimiter '%s'",
            matchstr,
            key,
            delimiter,
        )
        match = traverse_dict_and_list(data, key, {}, delimiter=delimiter)
        if match == {}:
            continue
        if isinstance(match, dict):
            if _dict_match(
                match, matchstr, regex_match=regex_match, exact_match=exact_match
            ):
                return True
            continue
        if isinstance(match, (list, tuple)):
            # We are matching a single component to a single list member
            for member in match:
                if isinstance(member, dict):
                    if _dict_match(
                        member,
                        matchstr,
                        regex_match=regex_match,
                        exact_match=exact_match,
                    ):
                        return True
                if _match(
                    member, matchstr, regex_match=regex_match, exact_match=exact_match
                ):
                    return True
            continue
        if _match(match, matchstr, regex_match=regex_match, exact_match=exact_match):
            return True
    return False


# Traceback (UnicodeDecodeError)
2018-09-12 14:09:01,744 [salt.utils.minions                                    :696 ][ERROR   ][5648] Failed matching available minions with compound pattern: G@ip_interfaces:*:10.0.1.*
Traceback (most recent call last):
File "/usr/lib/python2.7/site-packages/salt/utils/minions.py", line 683, in check_minions
_res = check_func(expr, delimiter, greedy)
File "/usr/lib/python2.7/site-packages/salt/utils/minions.py", line 578, in _check_compound_minions
_results = engine(*engine_args)
File "/usr/lib/python2.7/site-packages/salt/utils/minions.py", line 325, in _check_grain_minions
return self._check_cache_minions(expr, delimiter, greedy, 'grains')
File "/usr/lib/python2.7/site-packages/salt/utils/minions.py", line 315, in _check_cache_minions
exact_match=exact_match):
File "/usr/lib/python2.7/site-packages/salt/utils/data.py", line 579, in subdict_match
exact_match=exact_match):
File "/usr/lib/python2.7/site-packages/salt/utils/data.py", line 549, in _dict_match
exact_match=exact_match):
File "/usr/lib/python2.7/site-packages/salt/utils/data.py", line 526, in _match
return fnmatch.fnmatch(six.text_type(target).lower(), pattern.lower())
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 6: ordinal not in range(128)

# Fixed code
def subdict_match(
    data, expr, delimiter=DEFAULT_TARGET_DELIM, regex_match=False, exact_match=False
):
    """
    Check for a match in a dictionary using a delimiter character to denote
    levels of subdicts, and also allowing the delimiter character to be
    matched. Thus, 'foo:bar:baz' will match data['foo'] == 'bar:baz' and
    data['foo']['bar'] == 'baz'. The former would take priority over the
    latter.
    """

    def _match(target, pattern, regex_match=False, exact_match=False):
        # The reason for using six.text_type first and _then_ using
        # to_unicode as a fallback is because we want to eventually have
        # unicode types for comparison below. If either value is numeric then
        # six.text_type will turn it into a unicode string. However, if the
        # value is a PY2 str type with non-ascii chars, then the result will be
        # a UnicodeDecodeError. In those cases, we simply use to_unicode to
        # decode it to unicode. The reason we can't simply use to_unicode to
        # begin with is that (by design) to_unicode will raise a TypeError if a
        # non-string/bytestring/bytearray value is passed.
        try:
            target = six.text_type(target).lower()
        except UnicodeDecodeError:
            target = salt.utils.stringutils.to_unicode(target).lower()
        try:
            pattern = six.text_type(pattern).lower()
        except UnicodeDecodeError:
            pattern = salt.utils.stringutils.to_unicode(pattern).lower()

        if regex_match:
            try:
                return re.match(pattern, target)
            except Exception:
                log.error("Invalid regex '%s' in match", pattern)
                return False
        else:
            return (
                target == pattern if exact_match else fnmatch.fnmatch(target, pattern)
            )

    def _dict_match(target, pattern, regex_match=False, exact_match=False):
        wildcard = pattern.startswith("*:")
        if wildcard:
            pattern = pattern[2:]

        if pattern == "*":
            # We are just checking that the key exists
            return True
        elif pattern in target:
            # We might want to search for a key
            return True
        elif subdict_match(
            target, pattern, regex_match=regex_match, exact_match=exact_match
        ):
            return True
        if wildcard:
            for key in target:
                if isinstance(target[key], dict):
                    if _dict_match(
                        target[key],
                        pattern,
                        regex_match=regex_match,
                        exact_match=exact_match,
                    ):
                        return True
                elif isinstance(target[key], list):
                    for item in target[key]:
                        if _match(
                            item,
                            pattern,
                            regex_match=regex_match,
                            exact_match=exact_match,
                        ):
                            return True
                elif _match(
                    target[key],
                    pattern,
                    regex_match=regex_match,
                    exact_match=exact_match,
                ):
                    return True
        return False

    splits = expr.split(delimiter)
    num_splits = len(splits)
    if num_splits == 1:
        # Delimiter not present, this can't possibly be a match
        return False

    for idx in range(1, expr.count(delimiter) + 1):
        key = delimiter.join(splits[:idx])
        if key == "*":
            # We are matching on everything under the top level, so we need to
            # treat the match as the entire data being passed in
            matchstr = expr
            match = data
        else:
            matchstr = delimiter.join(splits[idx:])
            match = traverse_dict_and_list(data, key, {}, delimiter=delimiter)
        log.debug(
            "Attempting to match '%s' in '%s' using delimiter '%s'",
            matchstr,
            key,
            delimiter,
        )
        if match == {}:
            continue
        if isinstance(match, dict):
            if _dict_match(
                match, matchstr, regex_match=regex_match, exact_match=exact_match
            ):
                return True
            continue
        if isinstance(match, (list, tuple)):
            # We are matching a single component to a single list member
            for member in match:
                if isinstance(member, dict):
                    if _dict_match(
                        member,
                        matchstr,
                        regex_match=regex_match,
                        exact_match=exact_match,
                    ):
                        return True
                if _match(
                    member, matchstr, regex_match=regex_match, exact_match=exact_match
                ):
                    return True
            continue
        if _match(match, matchstr, regex_match=regex_match, exact_match=exact_match):
            return True
    return False