Module LDAP::LDIF
In: lib/ldap/ldif.rb

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

Methods

Classes and Modules

Class LDAP::LDIF::Entry
Class LDAP::LDIF::LDIFError
Class LDAP::LDIF::Mod

Constants

LINE_LENGTH = 77

Public Class methods

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

[Source]

     # File lib/ldap/ldif.rb, line 498
498:     def LDIF.mods_to_ldif( dn, *mods )
499:       ldif = "dn: %s\nchangetype: modify\n" % dn
500:       plural = false
501: 
502:       mods.flatten.each do |mod|
503:         # TODO: Need to dynamically assemble this case statement to add
504:         # OpenLDAP's increment change type, etc.
505:         change_type = case mod.mod_op & ~LDAP_MOD_BVALUES
506:                         when LDAP_MOD_ADD     then 'add'
507:                         when LDAP_MOD_DELETE  then 'delete'
508:                         when LDAP_MOD_REPLACE then 'replace'
509:                       end
510: 
511:         ldif << "-\n" if plural
512:         ldif << LDIF.to_ldif( change_type, mod.mod_type )
513:         ldif << LDIF.to_ldif( mod.mod_type, mod.mod_vals )
514: 
515:         plural = true
516:       end
517: 
518:       LDIF::Mod.new( ldif )
519:     end

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.

[Source]

     # File lib/ldap/ldif.rb, line 174
174:     def LDIF.parse_entry( lines )
175:       header = true
176:       comment = false
177:       change_type = nil
178:       sep = nil
179:       attr = nil
180:       bvalues = []
181:       controls = nil
182:       hash = {}
183:       mods = {}
184:       mod_type = nil
185:       
186:       lines.each do |line|
187:         # Skip (continued) comments.
188:         if line =~ /^#/ || ( comment && line[0..0] == ' ' )
189:           comment = true
190:           next
191:         end
192: 
193:         # Skip blank lines.
194:         next if line =~ /^$/
195: 
196:         # Reset mod type if this entry has more than one mod to make.
197:         # A '-' continuation is only valid if we've already had a
198:         # 'changetype: modify' line.
199:         if line =~ /^-$/ && change_type == LDAP_MOD_REPLACE
200:           next
201:         end
202: 
203:         line.chomp!
204: 
205:         # N.B. Attributes and values can be separated by one or two colons,
206:         # or one colon and a '<'. Either of these is then followed by zero
207:         # or one spaces.
208:         if md = line.match( /^[^ ].*?((:[:<]?) ?)/ )
209: 
210:           # If previous value was Base64-encoded and is not continued,
211:           # we need to decode it now.
212:           if sep == '::'
213:             if mod_type
214:               mods[mod_type][attr][-1] =
215:                 base64_decode( mods[mod_type][attr][-1] )
216:                 bvalues << attr if unsafe_char?( mods[mod_type][attr][-1] )
217:             else
218:               hash[attr][-1] = base64_decode( hash[attr][-1] )
219:               bvalues << attr if unsafe_char?( hash[attr][-1] )
220:             end
221: 
222:           end
223: 
224:           # Found a attr/value line.
225:           attr, val = line.split( md[1], 2 )
226:           attr.downcase!
227: 
228:           # Attribute must be ldap-oid / (ALPHA *(attr-type-chars))
229:           if attr !~ /^(?:(?:\d+\.)*\d+|[[:alnum:]-]+)(?:;[[:alnum:]-]+)*$/
230:             raise LDIFError, "Invalid attribute: #{attr}"
231:           end
232: 
233:           if attr == 'dn'
234:             header = false
235:             change_type = nil
236:             controls = []
237:           end
238:           sep = md[2]
239: 
240:           val = read_file( val ) if sep == ':<'
241: 
242:           case attr
243:           when 'version'
244:             # Check the LDIF version.
245:             if header
246:               if val != '1'
247:                 raise LDIFError, "Unsupported LDIF version: #{val}"
248:               else
249:                 header = false
250:                 next
251:               end
252:             end
253: 
254:           when 'changetype'
255:             change_type = case val
256:                             when 'add'            then LDAP_MOD_ADD
257:                             when 'delete'    then LDAP_MOD_DELETE
258:                             when 'modify'    then LDAP_MOD_REPLACE
259:                             when /^modr?dn$/ then :MODRDN
260:                           end
261: 
262:             raise LDIFError, "Invalid change type: #{attr}" unless change_type
263: 
264:           when 'add', 'delete', 'replace'
265:             unless change_type == LDAP_MOD_REPLACE
266:               raise LDIFError, "Cannot #{attr} here."
267:             end
268: 
269:             mod_type = case attr
270:                          when 'add'   then LDAP_MOD_ADD
271:                          when 'delete'        then LDAP_MOD_DELETE
272:                          when 'replace'       then LDAP_MOD_REPLACE
273:                        end
274: 
275:             mods[mod_type] ||= {}
276:             mods[mod_type][val] ||= []
277: 
278:           when 'control'
279: 
280:             oid, criticality = val.split( / /, 2 )
281: 
282:             unless oid =~ /(?:\d+\.)*\d+/
283:               raise LDIFError, "Bad control OID: #{oid}" 
284:             end
285: 
286:             if criticality
287:               md = criticality.match( /(:[:<]?) ?/ )
288:               ctl_sep = md[1] if md
289:               criticality, value = criticality.split( /:[:<]? ?/, 2 )
290: 
291:               if criticality !~ /^(?:true|false)$/
292:                 raise LDIFError, "Bad control criticality: #{criticality}"
293:               end
294: 
295:               # Convert 'true' or 'false'. to_boolean would be nice. :-)
296:               criticality = eval( criticality )
297:             end
298: 
299:             if value
300:               value = base64_decode( value ) if ctl_sep == '::'
301:               value = read_file( value ) if ctl_sep == ':<'
302:               value = Control.encode( value )
303:             end
304: 
305:             controls << Control.new( oid, value, criticality )
306:           else
307: 
308:             # Convert modrdn's deleteoldrdn from '1' to true, anything else
309:             # to false. Should probably raise an exception if not '0' or '1'.
310:             #
311:             if change_type == :MODRDN && attr == 'deleteoldrdn'
312:               val = val == '1' ? true : false
313:             end
314: 
315:             if change_type == LDAP_MOD_REPLACE
316:               mods[mod_type][attr] << val
317:             else
318:               hash[attr] ||= []
319:               hash[attr] << val
320:             end
321: 
322:             comment = false
323: 
324:             # Make a note of this attribute if value is binary.
325:             bvalues << attr if unsafe_char?( val )
326:           end
327: 
328:         else
329: 
330:           # Check last line's separator: if not a binary value, the
331:           # continuation line must be indented. If a comment makes it this
332:           # far, that's also an error.
333:           #
334:           if sep == ':' && line[0..0] != ' ' || comment
335:             raise LDIFError, "Improperly continued line: #{line}"
336:           end
337: 
338:           # OK; this is a valid continuation line.
339: 
340:           # Append line except for initial space.
341:           line[0] = '' if line[0..0] == ' '
342: 
343:           if change_type == LDAP_MOD_REPLACE
344:             # Append to last value of current mod type.
345:             mods[mod_type][attr][-1] << line
346:           else
347:             # Append to last value.
348:             hash[attr][-1] << line
349:           end
350:         end
351:         
352:       end
353: 
354:       # If last value in LDIF entry was Base64-encoded, we need to decode
355:       # it now.
356:       if sep == '::'
357:         if mod_type
358:           mods[mod_type][attr][-1] =
359:             base64_decode( mods[mod_type][attr][-1] )
360:           bvalues << attr if unsafe_char?( mods[mod_type][attr][-1] )
361:         else
362:           hash[attr][-1] = base64_decode( hash[attr][-1] )
363:           bvalues << attr if unsafe_char?( hash[attr][-1] )
364:         end
365:       end
366: 
367:       # Remove and remember DN.
368:       dn = hash.delete( 'dn' )[0]
369: 
370:       # This doesn't really matter, but let's be anal about it, because it's
371:       # not an attribute and doesn't belong here.
372:       bvalues.delete( 'dn' )
373: 
374:       # If there's no change type, it's just plain LDIF data, so we'll treat
375:       # it like an addition.
376:       change_type ||= LDAP_MOD_ADD
377: 
378:       case change_type
379:       when LDAP_MOD_ADD
380: 
381:         mods[LDAP_MOD_ADD] = []
382: 
383:         hash.each do |attr_local, val|
384:           if bvalues.include?( attr_local )
385:             ct = LDAP_MOD_ADD | LDAP_MOD_BVALUES
386:           else
387:             ct = LDAP_MOD_ADD
388:           end
389: 
390:           mods[LDAP_MOD_ADD] << LDAP.mod( ct, attr_local, val )
391:         end
392: 
393:       when LDAP_MOD_DELETE
394: 
395:         # Nothing to do.
396: 
397:       when LDAP_MOD_REPLACE
398: 
399:         raise LDIFError, "mods should not be empty" if mods == {}
400: 
401:         new_mods = {}
402: 
403:         mods.each do |mod_type_local,attrs|
404:           attrs.each_key do |attr_local|
405:             if bvalues.include?( attr_local )
406:               mt = mod_type_local | LDAP_MOD_BVALUES
407:             else
408:               mt = mod_type_local
409:             end
410: 
411:             new_mods[mt] ||= {}
412:             new_mods[mt][attr_local] = mods[mod_type_local][attr_local]
413:           end
414:         end
415: 
416:         mods = new_mods
417: 
418:       when :MODRDN
419: 
420:         # Nothing to do.
421: 
422:       end
423: 
424:       Record.new( dn, change_type, hash, mods, controls )
425:     end

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.

[Source]

     # File lib/ldap/ldif.rb, line 439
439:     def LDIF.parse_file( file, sort=false ) # :yield: record
440: 
441:       File.open( file ) do |f|
442:         entries = []
443:         entry = false
444:         header = true
445:         version = false
446: 
447:         while line = f.gets
448: 
449:           if line =~ /^dn:/
450:             header = false
451: 
452:             if entry && ! version
453:               if block_given?
454:                 yield parse_entry( entry )
455:               else
456:                 entries << parse_entry( entry )
457:               end
458:             end
459: 
460:             if version
461:               entry << line
462:               version = false
463:             else
464:               entry = [ line ]
465:             end
466: 
467:             next
468:           end
469: 
470:           if header && line.downcase =~ /^version/
471:             entry = [ line ]
472:             version = true
473:             next
474:           end
475:         
476:           entry << line
477:         end
478: 
479:         if block_given?
480:           yield parse_entry( entry )
481:           nil
482:         else
483:           entries << parse_entry( entry )
484: 
485:           # Sort entries if sorting has been requested.
486:           entries.sort! { |x,y| x.dn.length <=> y.dn.length } if sort
487:           entries
488:         end
489: 
490:       end
491: 
492:     end

Private Class methods

Perform Base64 encoding of str.

[Source]

     # File lib/ldap/ldif.rb, line 123
123:     def LDIF.base64_decode( str )
124:       str.unpack( 'm*' )[0]
125:     end

Perform Base64 decoding of str. If concat is true, LF characters are stripped.

[Source]

     # File lib/ldap/ldif.rb, line 114
114:     def LDIF.base64_encode( str, concat=false )
115:       str = [ str ].pack( 'm' )
116:       str.gsub!( /\n/, '' ) if concat
117:       str
118:     end

Read a file from the URL url. At this time, the only type of URL supported is the +file://+ URL.

[Source]

     # File lib/ldap/ldif.rb, line 131
131:     def LDIF.read_file( url )
132:       unless url.sub!( %r(^file://), '' )
133:         raise ArgumentError, "Bad external file reference: #{url}"
134:       end
135:   
136:       # Slurp an external file.
137:       # TODO: Support other URL types in the future.
138:       File.open( url ).readlines( nil )[0]
139:     end

This converts an attribute and array of values to LDIF.

[Source]

     # File lib/ldap/ldif.rb, line 144
144:     def LDIF.to_ldif( attr, vals )
145:       ldif = ''
146: 
147:       vals.each do |val|
148:         sep = ':'
149:         if unsafe_char?( val )
150:           sep = '::'
151:           val = base64_encode( val, true )
152:         end
153:       
154:         firstline_len = LINE_LENGTH - ( "%s%s " % [ attr, sep ] ).length
155:         ldif << "%s%s %s\n" % [ attr, sep, val.slice!( 0..firstline_len ) ]
156:       
157:         while val.length > 0
158:           ldif << " %s\n" % val.slice!( 0..LINE_LENGTH - 1 )
159:         end
160:       end
161: 
162:       ldif
163: 
164:     end

return true if str contains a character with an ASCII value > 127 or a NUL, LF or CR. Otherwise, false is returned.

[Source]

     # File lib/ldap/ldif.rb, line 105
105:     def LDIF.unsafe_char?( str )
106:       # This could be written as a single regex, but this is faster.
107:       str =~ /^[ :]/ || str =~ /[\x00-\x1f\x7f-\xff]/
108:     end

[Validate]