class Net::LDAP::Filter

See the individual class and instance methods below for more examples.
:filter parameter of Net::LDAP#search.
The object returned by this code can be passed directly to the
f = Net::LDAP::Filter.present(“objectclass”)
Here’s how to code the familiar “objectclass is present” filter:
++
filter strings (RFC 4515).
NOTE: This wording needs to change as we will be supporting LDAPv3 search

specifying LDAP search filters.
This class supplants the (infamous) RFC 2254 standard notation for
LDAP, including conjunction, disjunction and negation (AND, OR, and NOT).
Net::LDAP::Filter supports the complete set of search filters available in
this class is passed to Net::LDAP#search in the parameter :filter.
Class Net::LDAP::Filter is used to constrain LDAP searches. An object of
#

def &(filter)

z = x & y
# Selects only entries that meet both conditions above.
y = Net::LDAP::Filter.eq("mail", "George*")
# with "George".
# Selects only entries that have a mail attribute that begins
x = Net::LDAP::Filter.present("objectclass")
# Selects only entries that have an objectclass attribute.

Joins two or more filters so that all conditions must be true.
#
def &(filter)
  self.class.join(self, filter)
end

def ==(filter)

Equality operator for filters, useful primarily for constructing unit tests.
#
def ==(filter)
  # 20100320 AZ: We need to come up with a better way of doing this. This
  # is just nasty.
  str = "[@op,@left,@right]"
  self.instance_eval(str) == filter.instance_eval(str)
end

def begins(attribute, value)

escaped, so the "*" character is interpreted literally.
attribute must begin with a particular string. The attribute value is
Creates a Filter object indicating that the value of a particular
#
def begins(attribute, value)
  new(:eq, attribute, escape(value) + "*")
end

def bineq(attribute, value)

This filter does not perform any escaping.

f = Net::LDAP::Filter.bineq("objectGUID", guid_bin)
guid_bin = [guid].pack("H*")
guid = "6a31b4a12aa27a41aca9603f27dd5116"
# for guid represented as hex charecters

GUID values.
This is primarily used for Microsoft Active Directory to compare

this prevents the search data from being forced into a UTF-8 string.
Creates a Filter object indicating a binary comparison.
#
def bineq(attribute, value)
  new(:bineq, attribute, value)
end

def coalesce(operator) #:nodoc:

:nodoc:
only of self.
coalesce recursively). If they're not, then return an array consisting
type of joining operator, then return both of them as an array (calling
that are longer than two. If BOTH of our branches are of the specified
This is a private helper method for dealing with chains of ANDs and ORs
#
def coalesce(operator) #:nodoc:
  if @op == operator
    [@left.coalesce(operator), @right.coalesce(operator)]
  else
    [self]
  end
end

def construct(ldap_filter_string)

to a Net::LDAP::Filter.
Converts an LDAP filter-string (in the prefix syntax specified in RFC-2254)
#
def construct(ldap_filter_string)
  FilterParser.parse(ldap_filter_string)
end

def contains(attribute, value)

escaped, so the "*" character is interpreted literally.
attribute must contain a particular string. The attribute value is
Creates a Filter object indicating that the value of a particular
#
def contains(attribute, value)
  new(:eq, attribute, "*" + escape(value) + "*")
end

def ends(attribute, value)

escaped, so the "*" character is interpreted literally.
attribute must end with a particular string. The attribute value is
Creates a Filter object indicating that the value of a particular
#
def ends(attribute, value)
  new(:eq, attribute, "*" + escape(value))
end

def eq(attribute, value)

This filter does not perform any escaping

f = Net::LDAP::Filter.eq("mail", "*anderson*")

mail value containing the substring "anderson":
underlying LDAP protocol. This example selects any entry with a
regular-expressions are not supported due to limitations in the
wildcards: these match zero or more occurrences of any character. Full
parameter to #eq. The string may contain one or more "*" characters as
To match a particular range of values, pass a string as the second

f = Net::LDAP::Filter.eq("sAMAccountName", "*")

one or more values for sAMAccountName:
second parameter to #eq. This example selects only entries that have
mail. Presence is indicated by giving the value "*" in the
the filter. This is useful in case of optional attributes such as
which contain a value for the particular attribute will be selected by
Specifying that an attribute is 'present' means only directory entries

attribute must either be present or match a particular string.
Creates a Filter object indicating that the value of a particular
#
def eq(attribute, value)
  new(:eq, attribute, value)
end

def equals(attribute, value)

escaped, so the "*" character is interpreted literally.
attribute must match a particular string. The attribute value is
Creates a Filter object indicating that the value of a particular
#
def equals(attribute, value)
  new(:eq, attribute, escape(value))
end

def escape(string)

Escape a string for use in an LDAP filter
#
def escape(string)
  string.gsub(ESCAPE_RE) { |char| "\\" + ESCAPES[char] }
end

def ex(attribute, value)

++
* https://docs.opends.org/2.0/page/SearchingUsingInternationalCollationRules
* http://www.novell.com/documentation/edir88/edir88/?page=/documentation/edir88/edir88/data/agazepd.html
* http://www.ietf.org/rfc/rfc2251.txt
=Refs:

uid: 1
givenName:: YsO0Yg==
sn:: YsO0YiByw7Riw6lydHM=
cn:: csO0YsOpcnRz
objectClass: top
objectClass: inetOrgPerson
objectClass: organizationalPerson
objectClass: person
dn: uid=1,ou=People,dc=example,dc=com

ou: People
objectClass: top
objectClass: organizationalUnit
dn: ou=People,dc=example,dc=com

dc: example
objectClass: top
objectClass: domain
dn: dc=example,dc=com

version: 1

server:
The LDIF required to support the above examples on the OpenDS LDAP
--
Net::LDAP.search(:filter => filter)
# Perform a search with the Extensible Match Filter

filter = Net::LDAP::Filter.ex(attr, value) # Net::LDAP::Filter
filter = "#{attr}:=#{value}" # Basic String Filter

value = 'roberts'
attr = sample_attributes.first # Pick an extensible attribute
'cn:1.3.6.1.4.1.42.2.27.9.4.49.1.3', 'cn:dn:fr', 'cn:dn:fr.eq']
sample_attributes = ['cn:fr', 'cn:fr.eq',

object is currently considered EXPERIMENTAL.
Creates a Filter object indicating extensible comparison. This Filter
#
def ex(attribute, value)
  new(:ex, attribute, value)
end

def execute(&block)

call to #execute with the the same block.
* :not (one argument, which is an object returned from a recursive
a recursive call to #execute, with the same block; and
* :or (two or more arguments, each of which is an object returned from
from a recursive call to #execute, with the same block;
* :and (two or more arguments, each of which is an object returned
compared against);
* :lessOrEqual (two arguments: an attribute name and a value to be
compared against);
* :greaterOrEqual (two arguments: an attribute name and a value to be
* :present (one argument: an attribute name);
one or more "*" characters);
* :substrings (two arguments: an attribute name and a value containing
to be matched);
* :equalityMatch (the arguments will be an attribute name and a value
block:
These are the possible values that may be passed to the user-supplied

Net::LDAP::Filter objects.
A typical object to return from the user-supplied block is an array of

:or and :not operations detailed below.
locally-meaningful object that will appear as a parameter in the :and,
some desired application-defined processing, and may return a
operation. The user-supplied block (which is MANDATORY) should perform
the filter; and second, an array consisting of arguments to the
called with two arguments: first, a symbol denoting the "operation" of
when implementing an LDAP directory server. The caller's block will be
Perform filter operations against a user-supplied block. This is useful
#
def execute(&block)
  case @op
  when :eq
    if @right == "*"
      yield :present, @left
    elsif @right.index '*'
      yield :substrings, @left, @right
    else
      yield :equalityMatch, @left, @right
    end
  when :ge
    yield :greaterOrEqual, @left, @right
  when :le
    yield :lessOrEqual, @left, @right
  when :or, :and
    yield @op, (@left.execute(&block)), (@right.execute(&block))
  when :not
    yield @op, (@left.execute(&block))
  end || []
end

def ge(attribute, value)

is greater than or equal to the specified value.
Creates a Filter object indicating that a particular attribute value
#
def ge(attribute, value)
  new(:ge, attribute, value)
end

def initialize(op, left, right) #:nodoc:

:nodoc:
def initialize(op, left, right) #:nodoc:
  unless FilterTypes.include?(op)
    raise Net::LDAP::OperatorError, "Invalid or unsupported operator #{op.inspect} in LDAP Filter."
  end
  @op = op
  @left = left
  @right = right
end

def intersect(left, right)

z = x | y
# Selects only entries that meet either condition above.
y = Net::LDAP::Filter.eq("mail", "George*")
# with "George".
# Selects only entries that have a mail attribute that begins
x = Net::LDAP::Filter.present("objectclass")
# Selects only entries that have an objectclass attribute.

right.
Filter.intersect(left, right) is the same as left |
entries where either the left or right side are true. Calling
Creates a disjoint comparison between two or more filters. Selects
#
def intersect(left, right)
  new(:or, left, right)
end

def join(left, right)

z = Net::LDAP::Filter.join(x, y)
# Selects only entries that meet both conditions above.
y = Net::LDAP::Filter.eq("mail", "George*")
# with "George".
# Selects only entries that have a mail attribute that begins
x = Net::LDAP::Filter.present("objectclass")
# Selects only entries that have an objectclass attribute.

right
.
Filter.join(left, right) is the same as left &
Joins two or more filters so that all conditions must be true. Calling
#
def join(left, right)
  new(:and, left, right)
end

def le(attribute, value)

is less than or equal to the specified value.
Creates a Filter object indicating that a particular attribute value
#
def le(attribute, value)
  new(:le, attribute, value)
end

def match(entry)

++
Return T/F, and call match recursively as necessary.
Do we match the attributes?
We got a hash of attribute values.
--
#
def match(entry)
  case @op
  when :eq
    if @right == "*"
      l = entry[@left] and l.length > 0
    else
      l = entry[@left] and l = Array(l) and l.index(@right)
    end
  else
    raise Net::LDAP::FilterTypeUnknownError, "Unknown filter type in match: #{@op}"
  end
end

def ne(attribute, value)

This filter does not perform any escaping

Filter::eq for more information.
is either not present or does not match a particular string; see
Creates a Filter object indicating that a particular attribute value
#
def ne(attribute, value)
  new(:ne, attribute, value)
end

def negate(filter)

x = ~Net::LDAP::Filter.present("objectclass")
# attribute.
# Selects only entries that do not have an objectclass

as ~filter.
Negates a filter. Calling Fitler.negate(filter) i s the same
#
def negate(filter)
  new(:not, filter, nil)
end

def parse_ber(ber)

broken out insto constants.
We're hardcoding the BER constants from the RFC. These should be
--
the grammar snippet from the RFC.
LDAP searchRequest PDU. See also the comments under #to_ber, including
object. The incoming BER object most likely came to us by parsing an
Converts an LDAP search filter in BER format to an Net::LDAP::Filter
#
def parse_ber(ber)
  case ber.ber_identifier
  when 0xa0 # context-specific constructed 0, "and"
    ber.map { |b| parse_ber(b) }.inject { |memo, obj| memo & obj }
  when 0xa1 # context-specific constructed 1, "or"
    ber.map { |b| parse_ber(b) }.inject { |memo, obj| memo | obj }
  when 0xa2 # context-specific constructed 2, "not"
    ~parse_ber(ber.first)
  when 0xa3 # context-specific constructed 3, "equalityMatch"
    if ber.last == "*"
    else
      eq(ber.first, ber.last)
    end
  when 0xa4 # context-specific constructed 4, "substring"
    str = ""
    final = false
    ber.last.each do |b|
      case b.ber_identifier
      when 0x80 # context-specific primitive 0, SubstringFilter "initial"
        raise Net::LDAP::SubstringFilterError, "Unrecognized substring filter; bad initial value." if str.length > 0
        str += escape(b)
      when 0x81 # context-specific primitive 0, SubstringFilter "any"
        str += "*#{escape(b)}"
      when 0x82 # context-specific primitive 0, SubstringFilter "final"
        str += "*#{escape(b)}"
        final = true
      end
    end
    str += "*" unless final
    eq(ber.first.to_s, str)
  when 0xa5 # context-specific constructed 5, "greaterOrEqual"
    ge(ber.first.to_s, ber.last.to_s)
  when 0xa6 # context-specific constructed 6, "lessOrEqual"
    le(ber.first.to_s, ber.last.to_s)
  when 0x87 # context-specific primitive 7, "present"
    # call to_s to get rid of the BER-identifiedness of the incoming string.
    present?(ber.to_s)
  when 0xa9 # context-specific constructed 9, "extensible comparison"
    raise Net::LDAP::SearchFilterError, "Invalid extensible search filter, should be at least two elements" if ber.size < 2
    # Reassembles the extensible filter parts
    # (["sn", "2.4.6.8.10", "Barbara Jones", '1'])
    type = value = dn = rule = nil
    ber.each do |element|
      case element.ber_identifier
        when 0x81 then rule=element
        when 0x82 then type=element
        when 0x83 then value=element
        when 0x84 then dn='dn'
      end
    end
    attribute = ''
    attribute << type if type
    attribute << ":#{dn}" if dn
    attribute << ":#{rule}" if rule
    ex(attribute, value)
  else
    raise Net::LDAP::BERInvalidError, "Invalid BER tag-value (#{ber.ber_identifier}) in search filter."
  end
end

def parse_ldap_filter(obj)

++
filter types. Could pull them out into a constant.
TODO, we're hardcoding the RFC-1777 BER-encodings of the various
--
object.
Convert an RFC-1777 LDAP/BER "Filter" object to a Net::LDAP::Filter
#
def parse_ldap_filter(obj)
  case obj.ber_identifier
  when 0x87 # present. context-specific primitive 7.
    eq(obj.to_s, "*")
  when 0xa3 # equalityMatch. context-specific constructed 3.
    eq(obj[0], obj[1])
  else
    raise Net::LDAP::SearchFilterTypeUnknownError, "Unknown LDAP search-filter type: #{obj.ber_identifier}"
  end
end

def present?(attribute)

#pres.
This is a synonym for #eq(attribute, "*"). Also known as #present and
#
def present?(attribute)
  eq(attribute, "*")
end

def to_ber

++

Substring [.6] or [.sub]
Greater than [.5] or [.gt]
Greater than or equal to [.4] or [.gte]
Equality [.3] or [.eq] (default)
Less than or equal to [.2] or [.lte]
Less than [.1] or .[lt]
Matching Rule Suffixes

}
dnAttributes [4] BOOLEAN DEFAULT FALSE
matchValue [3] AssertionValue,
type [2] AttributeDescription OPTIONAL,
matchingRule [1] MatchingRuleId OPTIONAL,
SEQUENCE {
MatchingRuleAssertion ::=

}
}
final [2] LDAPString
any [1] LDAPString,
initial [0] LDAPString,
SEQUENCE OF CHOICE {
type AttributeType,
SEQUENCE {
SubstringFilter ::=

}
extensibleMatch [9] MatchingRuleAssertion
approxMatch [8] AttributeValueAssertion,
present [7] AttributeType,
lessOrEqual [6] AttributeValueAssertion,
greaterOrEqual [5] AttributeValueAssertion,
substrings [4] SubstringFilter,
equalityMatch [3] AttributeValueAssertion,
not [2] Filter,
or [1] SET OF Filter,
and [0] SET OF Filter,
CHOICE {
Filter ::=
--
Converts the filter to BER format.
#
def to_ber
  case @op
  when :eq
    if @right == "*" # presence test
      @left.to_s.to_ber_contextspecific(7)
    elsif @right.to_s =~ /[*]/ # substring
      # Parsing substrings is a little tricky. We use String#split to
      # break a string into substrings delimited by the * (star)
      # character. But we also need to know whether there is a star at the
      # head and tail of the string, so we use a limit parameter value of
      # -1: "If negative, there is no limit to the number of fields
      # returned, and trailing null fields are not suppressed."
      #
      # 20100320 AZ: This is much simpler than the previous verison. Also,
      # unnecessary regex escaping has been removed.
      ary = @right.split(/[*]+/, -1)
      if ary.first.empty?
        first = nil
        ary.shift
      else
        first = unescape(ary.shift).to_ber_contextspecific(0)
      end
      if ary.last.empty?
        last = nil
        ary.pop
      else
        last = unescape(ary.pop).to_ber_contextspecific(2)
      end
      seq = ary.map { |e| unescape(e).to_ber_contextspecific(1) }
      seq.unshift first if first
      seq.push last if last
      [@left.to_s.to_ber, seq.to_ber].to_ber_contextspecific(4)
    else # equality
      [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(3)
    end
  when :bineq
    # make sure data is not forced to UTF-8
    [@left.to_s.to_ber, unescape(@right).to_ber_bin].to_ber_contextspecific(3)
  when :ex
    seq = []
    unless @left =~ /^([-;\w]*)(:dn)?(:(\w+|[.\w]+))?$/
      raise Net::LDAP::BadAttributeError, "Bad attribute #{@left}"
    end
    type, dn, rule = $1, $2, $4
    seq << rule.to_ber_contextspecific(1) unless rule.to_s.empty? # matchingRule
    seq << type.to_ber_contextspecific(2) unless type.to_s.empty? # type
    seq << unescape(@right).to_ber_contextspecific(3) # matchingValue
    seq << "1".to_ber_contextspecific(4) unless dn.to_s.empty? # dnAttributes
    seq.to_ber_contextspecific(9)
  when :ge
    [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(5)
  when :le
    [@left.to_s.to_ber, unescape(@right).to_ber].to_ber_contextspecific(6)
  when :ne
    [self.class.eq(@left, @right).to_ber].to_ber_contextspecific(2)
  when :and
    ary = [@left.coalesce(:and), @right.coalesce(:and)].flatten
    ary.map(&:to_ber).to_ber_contextspecific(0)
  when :or
    ary = [@left.coalesce(:or), @right.coalesce(:or)].flatten
    ary.map(&:to_ber).to_ber_contextspecific(1)
  when :not
    [@left.to_ber].to_ber_contextspecific(2)
  end
end

def to_raw_rfc2254

def to_raw_rfc2254
  case @op
  when :ne
    "!(#{@left}=#{@right})"
  when :eq, :bineq
    "#{@left}=#{@right}"
  when :ex
    "#{@left}:=#{@right}"
  when :ge
    "#{@left}>=#{@right}"
  when :le
    "#{@left}<=#{@right}"
  when :and
    "&(#{@left.to_raw_rfc2254})(#{@right.to_raw_rfc2254})"
  when :or
    "|(#{@left.to_raw_rfc2254})(#{@right.to_raw_rfc2254})"
  when :not
    "!(#{@left.to_raw_rfc2254})"
  end
end

def to_rfc2254

Converts the Filter object to an RFC 2254-compatible text format.
#
def to_rfc2254
  "(#{to_raw_rfc2254})"
end

def to_s

def to_s
  to_rfc2254
end

def unescape(right)

Other tags:
    Note: - slawson20170317: Don't attempt to unescape 16 byte binary data which we assume are objectGUIDs
def unescape(right)
  right = right.to_s
  if right.length == 16 && right.encoding == Encoding::BINARY
    right
  else
    right.to_s.gsub(/\\([a-fA-F\d]{2})/) { [$1.hex].pack("U") }
  end
end

def |(filter)

z = x | y
# Selects only entries that meet either condition above.
y = Net::LDAP::Filter.eq("mail", "George*")
# with "George".
# Selects only entries that have a mail attribute that begins
x = Net::LDAP::Filter.present("objectclass")
# Selects only entries that have an objectclass attribute.

entries where either the left or right side are true.
Creates a disjoint comparison between two or more filters. Selects
#
def |(filter)
  self.class.intersect(self, filter)
end

def ~@

x = ~Net::LDAP::Filter.present("objectclass")
# attribute.
# Selects only entries that do not have an objectclass

Negates a filter.
#
def ~@
  self.class.negate(self)
end