docs: def _parse_ldap_criteria(ldap_filter, startidx=0, endidx=-1):
      # type: (str, int, int) -> LDAPCriteria
      """
      Parses an LDAP sub filter (criterion)
  
      :param ldap_filter: An LDAP filter string
      :param startidx: Sub-filter start index
      :param endidx: Sub-filter end index
      :return: The LDAP sub-filter
      :raise ValueError: Invalid sub-filter
      """
      comparators = "=<>~"
      if startidx < 0:
          raise ValueError(
              "Invalid string range start={0}, end={1}".format(startidx, endidx)
          )
  
      # Get the comparator
      escaped = False
      idx = startidx
      for char in ldap_filter[startidx:endidx]:
          if not escaped:
              if char == ESCAPE_CHARACTER:
                  # Next character escaped
                  escaped = True
              elif char in comparators:
                  # Comparator found
                  break
          else:
              # Escaped character ignored
              escaped = False
          idx += 1
      else:
          # Comparator never found
          raise ValueError(
              "Comparator not found in '{0}'".format(ldap_filter[startidx:endidx])
          )
  
      # The attribute name can be extracted directly
      attribute_name = ldap_filter[startidx:idx].strip()
      if not attribute_name:
          # Attribute name is missing
          raise ValueError(
              "Attribute name is missing in '{0}'".format(
                  ldap_filter[startidx:endidx]
              )
          )
  
      comparator = _compute_comparator(ldap_filter, idx)
      if comparator is None:
          # Unknown comparator
          raise ValueError(
              "Unknown comparator in '{0}' - {1}\nFilter : {2}".format(
                  ldap_filter[startidx:endidx], ldap_filter[idx], ldap_filter
              )
          )
  
      # Find the end of the comparator
      while ldap_filter[idx] in comparators:
          idx += 1
  
      # Skip spaces
      idx = _skip_spaces(ldap_filter, idx)
  
      # Extract the value
      value = ldap_filter[idx:endidx].strip()
  
      # Use the appropriate comparator if a joker is found in the filter value
      if value == "*":
          # Presence comparator
          comparator = _comparator_presence
      elif "*" in value:
          # Joker
          if comparator == _comparator_eq:
              comparator = _comparator_star
          elif comparator == _comparator_approximate:
              comparator = _comparator_approximate_star
  
      return LDAPCriteria(
          unescape_LDAP(attribute_name), unescape_LDAP(value), comparator
      )