| Module | LDAP::LDIF |
| In: |
lib/ldap/ldif.rb
|
This module provides the ability to process LDIF entries and files.
| LINE_LENGTH | = | 77 |
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 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.
# 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.
# 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
Perform Base64 encoding of str.
# 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.
# 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.
# 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.
# 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.
# 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