docs: def _parse_ldap(ldap_filter):
      # type: (str) -> Optional[LDAPFilter]
      """
      Parses the given LDAP filter string
  
      :param ldap_filter: An LDAP filter string
      :return: An LDAPFilter object, None if the filter was empty
      :raise ValueError: The LDAP filter string is invalid
      """
      if ldap_filter is None:
          # Nothing to do
          return None
  
      assert is_string(ldap_filter)
  
      # Remove surrounding spaces
      ldap_filter = ldap_filter.strip()
      if not ldap_filter:
          # Empty string
          return None
  
      escaped = False
      filter_len = len(ldap_filter)
      root = None
      stack = []
      subfilter_stack = []
  
      idx = 0
      while idx < filter_len:
          if not escaped:
              if ldap_filter[idx] == "(":
                  # Opening filter : get the operator
                  idx = _skip_spaces(ldap_filter, idx + 1)
                  if idx == -1:
                      raise ValueError(
                          "Missing filter operator: {0}".format(ldap_filter)
                      )
  
                  operator = _compute_operation(ldap_filter, idx)
                  if operator is not None:
                      # New sub-filter
                      stack.append(LDAPFilter(operator))
                  else:
                      # Sub-filter content
                      subfilter_stack.append(idx)
  
              elif ldap_filter[idx] == ")":
                  # Ending filter : store it in its parent
                  if subfilter_stack:
                      # criterion finished
                      start_idx = subfilter_stack.pop()
                      criterion = _parse_ldap_criteria(
                          ldap_filter, start_idx, idx
                      )
  
                      if stack:
                          top = stack.pop()
                          top.append(criterion)
                          stack.append(top)
                      else:
                          # No parent : filter contains only one criterion
                          # Make a parent to stay homogeneous
                          root = LDAPFilter(AND)
                          root.append(criterion)
                  elif stack:
                      # Sub filter finished
                      ended_filter = stack.pop()
                      if stack:
                          top = stack.pop()
                          top.append(ended_filter)
                          stack.append(top)
                      else:
                          # End of the parse
                          root = ended_filter
                  else:
                      raise ValueError(
                          "Too many end of parenthesis:{0}: {1}".format(
                              idx, ldap_filter[idx:]
                          )
                      )
              elif ldap_filter[idx] == "\\":
                  # Next character must be ignored
                  escaped = True
          else:
              # Escaped character ignored
              escaped = False
  
          # Don't forget to increment...
          idx += 1
  
      # No root : invalid content
      if root is None:
          raise ValueError("Invalid filter string: {0}".format(ldap_filter))
  
      # Return the root of the filter
      return root.normalize()