| 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 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.
# 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.
# 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
Perform Base64 encoding of str.
# 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.
# 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.
# 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.
# 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.
# 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