module LDAP::LDIF

This module provides the ability to process LDIF entries and files.

Constants

LINE_LENGTH

Public Class Methods

mods_to_ldif( dn, *mods ) click to toggle source

Given the DN, dn, convert a single LDAP::Mod or an array of LDAP::Mod objects, given in mods, to LDIF.

# File lib/ldap/ldif.rb, line 493
def LDIF.mods_to_ldif( dn, *mods )
  ldif = "dn: %s\nchangetype: modify\n" % dn
  plural = false

  mods.flatten.each do |mod|
    # TODO: Need to dynamically assemble this case statement to add
    # OpenLDAP's increment change type, etc.
    change_type = case mod.mod_op & ~LDAP_MOD_BVALUES
      when LDAP_MOD_ADD     then 'add'
      when LDAP_MOD_DELETE  then 'delete'
      when LDAP_MOD_REPLACE then 'replace'
    end

    ldif << "-\n" if plural
    ldif << LDIF.to_ldif( change_type, [mod.mod_type] )
    ldif << LDIF.to_ldif( mod.mod_type, mod.mod_vals )

    plural = true
  end

  LDIF::Mod.new( ldif )
end
parse_entry( lines ) click to toggle source

Parse the LDIF entry contained in lines and return an LDAP::Record object. lines should be an object that responds to each, such as a string or an array of lines, separated by n characters.

# File lib/ldap/ldif.rb, line 174
def LDIF.parse_entry( lines )
  header = true
  comment = false
  change_type = nil
  sep = nil
  attr = nil
  bvalues = []
  controls = nil
  hash = {}
  mods = {}
  mod_type = nil

  lines.each do |line|
    # Skip (continued) comments.
    if line =~ /^#/ || ( comment && line[0..0] == ' ' )
      comment = true
      next
    end

    # Skip blank lines.
    next if line =~ /^$/

    # Reset mod type if this entry has more than one mod to make.
    # A '-' continuation is only valid if we've already had a
    # 'changetype: modify' line.
    if line =~ /^-$/ && change_type == LDAP_MOD_REPLACE
      next
    end

    line.chomp!

    # N.B. Attributes and values can be separated by one or two colons,
    # or one colon and a '<'. Either of these is then followed by zero
    # or one spaces.
    if md = line.match( /^[^ ].*?((:[:<]?) ?)/ )

      # If previous value was Base64-encoded and is not continued,
      # we need to decode it now.
      if sep == '::'
        if mod_type
          mods[mod_type][attr][-1] =
            base64_decode( mods[mod_type][attr][-1] )
          bvalues << attr if unsafe_char?( mods[mod_type][attr][-1] )
        else
          hash[attr][-1] = base64_decode( hash[attr][-1] )
          bvalues << attr if unsafe_char?( hash[attr][-1] )
        end
      end

      # Found a attr/value line.
      attr, val = line.split( md[1], 2 )
      attr.downcase!

      # Attribute must be ldap-oid / (ALPHA *(attr-type-chars))
      if attr !~ /^(?:(?:\d+\.)*\d+|[[:alnum:]-]+)(?:;[[:alnum:]-]+)*$/
        raise LDIFError, "Invalid attribute: #{attr}"
      end

      if attr == 'dn'
        header = false
        change_type = nil
        controls = []
      end
      sep = md[2]

      val = read_file( val ) if sep == ':<'

      case attr
      when 'version'
        # Check the LDIF version.
        if header
          if val != '1'
            raise LDIFError, "Unsupported LDIF version: #{val}"
          else
            header = false
            next
          end
        end

      when 'changetype'
        change_type = case val
          when 'add'       then LDAP_MOD_ADD
          when 'delete'    then LDAP_MOD_DELETE
          when 'modify'    then LDAP_MOD_REPLACE
          when /^modr?dn$/ then :MODRDN
        end

        raise LDIFError, "Invalid change type: #{attr}" unless change_type

      when 'add', 'delete', 'replace'
        unless change_type == LDAP_MOD_REPLACE
          raise LDIFError, "Cannot #{attr} here."
        end

        mod_type = case attr
          when 'add'     then LDAP_MOD_ADD
          when 'delete'  then LDAP_MOD_DELETE
          when 'replace' then LDAP_MOD_REPLACE
        end

        # In this case val is actually an attribute and should be lowercased. 
        mods[mod_type] ||= {}
        mods[mod_type][val.downcase] ||= []

      when 'control'
        oid, criticality = val.split( / /, 2 )

        unless oid =~ /(?:\d+\.)*\d+/
          raise LDIFError, "Bad control OID: #{oid}" 
        end

        if criticality
          md = criticality.match( /(:[:<]?) ?/ )
          ctl_sep = md[1] if md
          criticality, value = criticality.split( /:[:<]? ?/, 2 )

          if criticality !~ /^(?:true|false)$/
            raise LDIFError, "Bad control criticality: #{criticality}"
          end

          # Convert 'true' or 'false'. to_boolean would be nice. :-)
          criticality = eval( criticality )
        end

        if value
          value = base64_decode( value ) if ctl_sep == '::'
          value = read_file( value ) if ctl_sep == ':<'
          value = Control.encode( value )
        end

        controls << Control.new( oid, value, criticality )

      else
        # Convert modrdn's deleteoldrdn from '1' to true, anything else
        # to false. Should probably raise an exception if not '0' or '1'.
        #
        if change_type == :MODRDN && attr == 'deleteoldrdn'
          val = val == '1' ? true : false
        end

        if change_type == LDAP_MOD_REPLACE
          mods[mod_type][attr] << val
        else
          hash[attr] ||= []
          hash[attr] << val
        end

        comment = false

        # Make a note of this attribute if value is binary.
        bvalues << attr if unsafe_char?( val )

      end

    else
      # Check last line's separator: if not a binary value, the
      # continuation line must be indented. If a comment makes it this
      # far, that's also an error.
      #
      if sep == ':' && line[0..0] != ' ' || comment
        raise LDIFError, "Improperly continued line: #{line}"
      end

      # OK; this is a valid continuation line.

      # Append line except for initial space.
      line[0] = '' if line[0..0] == ' '

      if change_type == LDAP_MOD_REPLACE
        # Append to last value of current mod type.
        mods[mod_type][attr][-1] << line
      else
        # Append to last value.
        hash[attr][-1] << line
      end
    end

  end

  # If last value in LDIF entry was Base64-encoded, we need to decode
  # it now.
  if sep == '::'
    if mod_type
      mods[mod_type][attr][-1] =
        base64_decode( mods[mod_type][attr][-1] )
      bvalues << attr if unsafe_char?( mods[mod_type][attr][-1] )
    else
      hash[attr][-1] = base64_decode( hash[attr][-1] )
      bvalues << attr if unsafe_char?( hash[attr][-1] )
    end
  end

  # Remove and remember DN.
  dn = hash.delete( 'dn' )[0]

  # This doesn't really matter, but let's be anal about it, because it's
  # not an attribute and doesn't belong here.
  bvalues.delete( 'dn' )

  # If there's no change type, it's just plain LDIF data, so we'll treat
  # it like an addition.
  change_type ||= LDAP_MOD_ADD

  case change_type
  when LDAP_MOD_ADD
    mods[LDAP_MOD_ADD] = []

    hash.each do |attr_local, val|
      if bvalues.include?( attr_local )
        ct = LDAP_MOD_ADD | LDAP_MOD_BVALUES
      else
        ct = LDAP_MOD_ADD
      end

      mods[LDAP_MOD_ADD] << LDAP.mod( ct, attr_local, val )
    end

  when LDAP_MOD_DELETE
    # Nothing to do.

  when LDAP_MOD_REPLACE
    raise LDIFError, "mods should not be empty" if mods == {}

    new_mods = {}

    mods.each do |mod_type_local,attrs|
      attrs.each_key do |attr_local|
        if bvalues.include?( attr_local )
          mt = mod_type_local | LDAP_MOD_BVALUES
        else
          mt = mod_type_local
        end

        new_mods[mt] ||= {}
        new_mods[mt][attr_local] = mods[mod_type_local][attr_local]
      end
    end

    mods = new_mods

  when :MODRDN
    # Nothing to do.

  end

  Record.new( dn, change_type, hash, mods, controls )
end
parse_file( file, sort=false ) { |record| ... } click to toggle source

Open and parse a file containing LDIF entries. file should be a string containing the path to the file. If sort is true, the resulting array of LDAP::Record objects will be sorted on DN length, which can be useful to avoid a later attempt to process an entry whose parent does not yet exist. This can easily happen if your LDIF file is unordered, which is likely if it was produced with a tool such as slapcat(8).

If a block is given, each LDAP::Record object will be yielded to the block and nil will be returned instead of the array. This is much less memory-intensive when parsing a large LDIF file.

# File lib/ldap/ldif.rb, line 434
def LDIF.parse_file( file, sort=false ) # :yield: record

  File.open( file ) do |f|
    entries = []
    entry = false
    header = true
    version = false

    while line = f.gets

      if line =~ /^dn:/
        header = false

        if entry && ! version
          if block_given?
            yield parse_entry( entry )
          else
            entries << parse_entry( entry )
          end
        end

        if version
          entry << line
          version = false
        else
          entry = [ line ]
        end

        next
      end

      if header && line.downcase =~ /^version/
        entry = [ line ]
        version = true
        next
      end

      entry << line
    end

    if block_given?
      yield parse_entry( entry )
      nil
    else
      entries << parse_entry( entry )

      # Sort entries if sorting has been requested.
      entries.sort! { |x,y| x.dn.length <=> y.dn.length } if sort
      entries
    end

  end

end