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 496
496:     def LDIF.mods_to_ldif( dn, *mods )
497:       ldif = "dn: %s\nchangetype: modify\n" % dn
498:       plural = false
499: 
500:       mods.flatten.each do |mod|
501:         # TODO: Need to dynamically assemble this case statement to add
502:         # OpenLDAP's increment change type, etc.
503:         change_type = case mod.mod_op & ~LDAP_MOD_BVALUES
504:                         when LDAP_MOD_ADD     then 'add'
505:                         when LDAP_MOD_DELETE  then 'delete'
506:                         when LDAP_MOD_REPLACE then 'replace'
507:                       end
508: 
509:         ldif << "-\n" if plural
510:         ldif << LDIF.to_ldif( change_type, mod.mod_type )
511:         ldif << LDIF.to_ldif( mod.mod_type, mod.mod_vals )
512: 
513:         plural = true
514:       end
515: 
516:       LDIF::Mod.new( ldif )
517:     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 172
172:     def LDIF.parse_entry( lines )
173:       header = true
174:       comment = false
175:       change_type = nil
176:       sep = nil
177:       attr = nil
178:       bvalues = []
179:       controls = nil
180:       hash = {}
181:       mods = {}
182:       mod_type = nil
183:       
184:       lines.each do |line|
185:         # Skip (continued) comments.
186:         if line =~ /^#/ || ( comment && line[0..0] == ' ' )
187:           comment = true
188:           next
189:         end
190: 
191:         # Skip blank lines.
192:         next if line =~ /^$/
193: 
194:         # Reset mod type if this entry has more than one mod to make.
195:         # A '-' continuation is only valid if we've already had a
196:         # 'changetype: modify' line.
197:         if line =~ /^-$/ && change_type == LDAP_MOD_REPLACE
198:           next
199:         end
200: 
201:         line.chomp!
202: 
203:         # N.B. Attributes and values can be separated by one or two colons,
204:         # or one colon and a '<'. Either of these is then followed by zero
205:         # or one spaces.
206:         if md = line.match( /^[^ ].*?((:[:<]?) ?)/ )
207: 
208:           # If previous value was Base64-encoded and is not continued,
209:           # we need to decode it now.
210:           if sep == '::'
211:             if mod_type
212:               mods[mod_type][attr][-1] =
213:                 base64_decode( mods[mod_type][attr][-1] )
214:                 bvalues << attr if unsafe_char?( mods[mod_type][attr][-1] )
215:             else
216:               hash[attr][-1] = base64_decode( hash[attr][-1] )
217:               bvalues << attr if unsafe_char?( hash[attr][-1] )
218:             end
219: 
220:           end
221: 
222:           # Found a attr/value line.
223:           attr, val = line.split( md[1], 2 )
224:           attr.downcase!
225: 
226:           # Attribute must be ldap-oid / (ALPHA *(attr-type-chars))
227:           if attr !~ /^(?:(?:\d+\.)*\d+|[[:alnum:]-]+)(?:;[[:alnum:]-]+)*$/
228:             raise LDIFError, "Invalid attribute: #{attr}"
229:           end
230: 
231:           if attr == 'dn'
232:             header = false
233:             change_type = nil
234:             controls = []
235:           end
236:           sep = md[2]
237: 
238:           val = read_file( val ) if sep == ':<'
239: 
240:           case attr
241:           when 'version'
242:             # Check the LDIF version.
243:             if header
244:               if val != '1'
245:                 raise LDIFError, "Unsupported LDIF version: #{val}"
246:               else
247:                 header = false
248:                 next
249:               end
250:             end
251: 
252:           when 'changetype'
253:             change_type = case val
254:                             when 'add'            then LDAP_MOD_ADD
255:                             when 'delete'    then LDAP_MOD_DELETE
256:                             when 'modify'    then LDAP_MOD_REPLACE
257:                             when /^modr?dn$/ then :MODRDN
258:                           end
259: 
260:             raise LDIFError, "Invalid change type: #{attr}" unless change_type
261: 
262:           when 'add', 'delete', 'replace'
263:             unless change_type == LDAP_MOD_REPLACE
264:               raise LDIFError, "Cannot #{attr} here."
265:             end
266: 
267:             mod_type = case attr
268:                          when 'add'   then LDAP_MOD_ADD
269:                          when 'delete'        then LDAP_MOD_DELETE
270:                          when 'replace'       then LDAP_MOD_REPLACE
271:                        end
272: 
273:             mods[mod_type] ||= {}
274:             mods[mod_type][val] ||= []
275: 
276:           when 'control'
277: 
278:             oid, criticality = val.split( / /, 2 )
279: 
280:             unless oid =~ /(?:\d+\.)*\d+/
281:               raise LDIFError, "Bad control OID: #{oid}" 
282:             end
283: 
284:             if criticality
285:               md = criticality.match( /(:[:<]?) ?/ )
286:               ctl_sep = md[1] if md
287:               criticality, value = criticality.split( /:[:<]? ?/, 2 )
288: 
289:               if criticality !~ /^(?:true|false)$/
290:                 raise LDIFError, "Bad control criticality: #{criticality}"
291:               end
292: 
293:               # Convert 'true' or 'false'. to_boolean would be nice. :-)
294:               criticality = eval( criticality )
295:             end
296: 
297:             if value
298:               value = base64_decode( value ) if ctl_sep == '::'
299:               value = read_file( value ) if ctl_sep == ':<'
300:               value = Control.encode( value )
301:             end
302: 
303:             controls << Control.new( oid, value, criticality )
304:           else
305: 
306:             # Convert modrdn's deleteoldrdn from '1' to true, anything else
307:             # to false. Should probably raise an exception if not '0' or '1'.
308:             #
309:             if change_type == :MODRDN && attr == 'deleteoldrdn'
310:               val = val == '1' ? true : false
311:             end
312: 
313:             if change_type == LDAP_MOD_REPLACE
314:               mods[mod_type][attr] << val
315:             else
316:               hash[attr] ||= []
317:               hash[attr] << val
318:             end
319: 
320:             comment = false
321: 
322:             # Make a note of this attribute if value is binary.
323:             bvalues << attr if unsafe_char?( val )
324:           end
325: 
326:         else
327: 
328:           # Check last line's separator: if not a binary value, the
329:           # continuation line must be indented. If a comment makes it this
330:           # far, that's also an error.
331:           #
332:           if sep == ':' && line[0..0] != ' ' || comment
333:             raise LDIFError, "Improperly continued line: #{line}"
334:           end
335: 
336:           # OK; this is a valid continuation line.
337: 
338:           # Append line except for initial space.
339:           line[0] = '' if line[0..0] == ' '
340: 
341:           if change_type == LDAP_MOD_REPLACE
342:             # Append to last value of current mod type.
343:             mods[mod_type][attr][-1] << line
344:           else
345:             # Append to last value.
346:             hash[attr][-1] << line
347:           end
348:         end
349:         
350:       end
351: 
352:       # If last value in LDIF entry was Base64-encoded, we need to decode
353:       # it now.
354:       if sep == '::'
355:         if mod_type
356:           mods[mod_type][attr][-1] =
357:             base64_decode( mods[mod_type][attr][-1] )
358:           bvalues << attr if unsafe_char?( mods[mod_type][attr][-1] )
359:         else
360:           hash[attr][-1] = base64_decode( hash[attr][-1] )
361:           bvalues << attr if unsafe_char?( hash[attr][-1] )
362:         end
363:       end
364: 
365:       # Remove and remember DN.
366:       dn = hash.delete( 'dn' )[0]
367: 
368:       # This doesn't really matter, but let's be anal about it, because it's
369:       # not an attribute and doesn't belong here.
370:       bvalues.delete( 'dn' )
371: 
372:       # If there's no change type, it's just plain LDIF data, so we'll treat
373:       # it like an addition.
374:       change_type ||= LDAP_MOD_ADD
375: 
376:       case change_type
377:       when LDAP_MOD_ADD
378: 
379:         mods[LDAP_MOD_ADD] = []
380: 
381:         hash.each do |attr,val|
382:           if bvalues.include?( attr )
383:             ct = LDAP_MOD_ADD | LDAP_MOD_BVALUES
384:           else
385:             ct = LDAP_MOD_ADD
386:           end
387: 
388:           mods[LDAP_MOD_ADD] << LDAP.mod( ct, attr, val )
389:         end
390: 
391:       when LDAP_MOD_DELETE
392: 
393:         # Nothing to do.
394: 
395:       when LDAP_MOD_REPLACE
396: 
397:         raise LDIFError, "mods should not be empty" if mods == {}
398: 
399:         new_mods = {}
400: 
401:         mods.each do |mod_type,attrs|
402:           attrs.each_key do |attr|
403:             if bvalues.include?( attr )
404:               mt = mod_type | LDAP_MOD_BVALUES
405:             else
406:               mt = mod_type
407:             end
408: 
409:             new_mods[mt] ||= {}
410:             new_mods[mt][attr] = mods[mod_type][attr]
411:           end
412:         end
413: 
414:         mods = new_mods
415: 
416:       when :MODRDN
417: 
418:         # Nothing to do.
419: 
420:       end
421: 
422:       Record.new( dn, change_type, hash, mods, controls )
423:     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 437
437:     def LDIF.parse_file( file, sort=false ) # :yield: record
438: 
439:       File.open( file ) do |f|
440:         entries = []
441:         entry = false
442:         header = true
443:         version = false
444: 
445:         while line = f.gets
446: 
447:           if line =~ /^dn:/
448:             header = false
449: 
450:             if entry && ! version
451:               if block_given?
452:                 yield parse_entry( entry )
453:               else
454:                 entries << parse_entry( entry )
455:               end
456:             end
457: 
458:             if version
459:               entry << line
460:               version = false
461:             else
462:               entry = [ line ]
463:             end
464: 
465:             next
466:           end
467: 
468:           if header && line.downcase =~ /^version/
469:             entry = [ line ]
470:             version = true
471:             next
472:           end
473:         
474:           entry << line
475:         end
476: 
477:         if block_given?
478:           yield parse_entry( entry )
479:           nil
480:         else
481:           entries << parse_entry( entry )
482: 
483:           # Sort entries if sorting has been requested.
484:           entries.sort! { |x,y| x.dn.length <=> y.dn.length } if sort
485:           entries
486:         end
487: 
488:       end
489: 
490:     end

Private Class methods

Perform Base64 encoding of str.

[Source]

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

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

[Source]

     # File lib/ldap/ldif.rb, line 112
112:     def LDIF.base64_encode( str, concat=false )
113:       str = [ str ].pack( 'm' )
114:       str.gsub!( /\n/, '' ) if concat
115:       str
116:     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 129
129:     def LDIF.read_file( url )
130:       unless url.sub!( %r(^file://), '' )
131:         raise ArgumentError, "Bad external file reference: #{url}"
132:       end
133:   
134:       # Slurp an external file.
135:       # TODO: Support other URL types in the future.
136:       File.open( url ).readlines( nil )[0]
137:     end

This converts an attribute and array of values to LDIF.

[Source]

     # File lib/ldap/ldif.rb, line 142
142:     def LDIF.to_ldif( attr, vals )
143:       ldif = ''
144: 
145:       vals.each do |val|
146:         sep = ':'
147:         if unsafe_char?( val )
148:           sep = '::'
149:           val = base64_encode( val, true )
150:         end
151:       
152:         firstline_len = LINE_LENGTH - ( "%s%s " % [ attr, sep ] ).length
153:         ldif << "%s%s %s\n" % [ attr, sep, val.slice!( 0..firstline_len ) ]
154:       
155:         while val.length > 0
156:           ldif << " %s\n" % val.slice!( 0..LINE_LENGTH - 1 )
157:         end
158:       end
159: 
160:       ldif
161: 
162:     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 103
103:     def LDIF.unsafe_char?( str )
104:       # This could be written as a single regex, but this is faster.
105:       str =~ /^[ :]/ || str =~ /[\x00-\x1f\x7f-\xff]/
106:     end

[Validate]