lib/net/ber/ber_parser.rb in net-ldap-0.1.1 vs lib/net/ber/ber_parser.rb in net-ldap-0.2

- old
+ new

@@ -1,112 +1,168 @@ +# -*- ruby encoding: utf-8 -*- require 'stringio' -module Net - module BER - module BERParser - VERSION = '0.1.0' +# Implements Basic Encoding Rules parsing to be mixed into types as needed. +module Net::BER::BERParser + primitive = { + 1 => :boolean, + 2 => :integer, + 4 => :string, + 5 => :null, + 6 => :oid, + 10 => :integer, + 13 => :string # (relative OID) + } + constructed = { + 16 => :array, + 17 => :array + } + universal = { :primitive => primitive, :constructed => constructed } - # The order of these follows the class-codes in BER. - # Maybe this should have been a hash. - TagClasses = [:universal, :application, :context_specific, :private] + primitive = { 10 => :integer } + context = { :primitive => primitive } - BuiltinSyntax = Net::BER.compile_syntax( { - :universal => { - :primitive => { - 1 => :boolean, - 2 => :integer, - 4 => :string, - 5 => :null, - 6 => :oid, - 10 => :integer, - 13 => :string # (relative OID) - }, - :constructed => { - 16 => :array, - 17 => :array - } - }, - :context_specific => { - :primitive => { - 10 => :integer - } - } - }) + # The universal, built-in ASN.1 BER syntax. + BuiltinSyntax = Net::BER.compile_syntax(:universal => universal, + :context_specific => context) - def read_ber syntax=nil - # TODO: clean this up so it works properly with partial - # packets coming from streams that don't block when - # we ask for more data (like StringIOs). At it is, - # this can throw TypeErrors and other nasties. - - id = getbyte or return nil # don't trash this value, we'll use it later + ## + # This is an extract of our BER object parsing to simplify our + # understanding of how we parse basic BER object types. + def parse_ber_object(syntax, id, data) + # Find the object type from either the provided syntax lookup table or + # the built-in syntax lookup table. + # + # This exceptionally clever bit of code is verrrry slow. + object_type = (syntax && syntax[id]) || BuiltinSyntax[id] - n = getbyte - lengthlength,contentlength = if n <= 127 - [1,n] - else - # Replaced the inject because it profiles hot. - # j = (0...(n & 127)).inject(0) {|mem,x| mem = (mem << 8) + getc} - j = 0 - read( n & 127 ).each_byte {|n1| j = (j << 8) + n1} - [1 + (n & 127), j] - end - - newobj = read contentlength - - # This exceptionally clever and clear bit of code is verrrry slow. - objtype = (syntax && syntax[id]) || BuiltinSyntax[id] - - # == is expensive so sort this if/else so the common cases are at the top. - obj = if objtype == :string - #(newobj || "").dup - s = BerIdentifiedString.new( newobj || "" ) - s.ber_identifier = id - s - elsif objtype == :integer - j = 0 - newobj.each_byte {|b| j = (j << 8) + b} - j - elsif objtype == :oid - # cf X.690 pgh 8.19 for an explanation of this algorithm. - # Potentially not good enough. We may need a BerIdentifiedOid - # as a subclass of BerIdentifiedArray, to get the ber identifier - # and also a to_s method that produces the familiar dotted notation. - oid = newobj.unpack("w*") - f = oid.shift - g = if f < 40 + # == is expensive so sort this so the common cases are at the top. + if object_type == :string + s = Net::BER::BerIdentifiedString.new(data || "") + s.ber_identifier = id + s + elsif object_type == :integer + j = 0 + data.each_byte { |b| j = (j << 8) + b } + j + elsif object_type == :oid + # See X.690 pgh 8.19 for an explanation of this algorithm. + # This is potentially not good enough. We may need a + # BerIdentifiedOid as a subclass of BerIdentifiedArray, to + # get the ber identifier and also a to_s method that produces + # the familiar dotted notation. + oid = data.unpack("w*") + f = oid.shift + g = if f < 40 [0, f] elsif f < 80 - [1, f-40] + [1, f - 40] else - [2, f-80] # f-80 can easily be > 80. What a weird optimization. + # f - 80 can easily be > 80. What a weird optimization. + [2, f - 80] end - oid.unshift g.last - oid.unshift g.first - oid - elsif objtype == :array - #seq = [] - seq = BerIdentifiedArray.new - seq.ber_identifier = id - sio = StringIO.new( newobj || "" ) - # Interpret the subobject, but note how the loop - # is built: nil ends the loop, but false (a valid - # BER value) does not! - while (e = sio.read_ber(syntax)) != nil - seq << e - end - seq - elsif objtype == :boolean - newobj != "\000" - elsif objtype == :null - n = BerIdentifiedNull.new - n.ber_identifier = id - n - else - raise BerError.new( "unsupported object type: id=#{id}" ) - end + oid.unshift g.last + oid.unshift g.first + # Net::BER::BerIdentifiedOid.new(oid) + oid + elsif object_type == :array + seq = Net::BER::BerIdentifiedArray.new + seq.ber_identifier = id + sio = StringIO.new(data || "") + # Interpret the subobject, but note how the loop is built: + # nil ends the loop, but false (a valid BER value) does not! + while (e = sio.read_ber(syntax)) != nil + seq << e + end + seq + elsif object_type == :boolean + data != "\000" + elsif object_type == :null + n = Net::BER::BerIdentifiedNull.new + n.ber_identifier = id + n + else + raise Net::BER::BerError, "Unsupported object type: id=#{id}" + end + end + private :parse_ber_object - obj + ## + # This is an extract of how our BER object length parsing is done to + # simplify the primary call. This is defined in X.690 section 8.1.3. + # + # The BER length will either be a single byte or up to 126 bytes in + # length. There is a special case of a BER length indicating that the + # content-length is undefined and will be identified by the presence of + # two null values (0x00 0x00). + # + # <table> + # <tr> + # <th>Range</th> + # <th>Length</th> + # </tr> + # <tr> + # <th>0x00 -- 0x7f<br />0b00000000 -- 0b01111111</th> + # <td>0 - 127 bytes</td> + # </tr> + # <tr> + # <th>0x80<br />0b10000000</th> + # <td>Indeterminate (end-of-content marker required)</td> + # </tr> + # <tr> + # <th>0x81 -- 0xfe<br />0b10000001 -- 0b11111110</th> + # <td>1 - 126 bytes of length as an integer value</td> + # </tr> + # <tr> + # <th>0xff<br />0b11111111</th> + # <td>Illegal (reserved for future expansion)</td> + # </tr> + # </table> + # + #-- + # This has been modified from the version that was previously inside + # #read_ber to handle both the indeterminate terminator case and the + # invalid BER length case. Because the "lengthlength" value was not used + # inside of #read_ber, we no longer return it. + def read_ber_length + n = getbyte + + if n <= 0x7f + n + elsif n == 0x80 + -1 + elsif n == 0xff + raise Net::BER::BerError, "Invalid BER length 0xFF detected." + else + v = 0 + read(n & 0x7f).each_byte do |b| + v = (v << 8) + b end + + v end + end + private :read_ber_length + + ## + # Reads a BER object from the including object. Requires that #getbyte is + # implemented on the including object and that it returns a Fixnum value. + # Also requires #read(bytes) to work. + # + # This does not work with non-blocking I/O. + def read_ber(syntax = nil) + # TODO: clean this up so it works properly with partial packets coming + # from streams that don't block when we ask for more data (like + # StringIOs). At it is, this can throw TypeErrors and other nasties. + + id = getbyte or return nil # don't trash this value, we'll use it later + content_length = read_ber_length + + if -1 == content_length + raise Net::BER::BerError, "Indeterminite BER content length not implemented." + else + data = read(content_length) + end + + parse_ber_object(syntax, id, data) end end