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