ruby-ldap3 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/ldap/ldif.rb ADDED
@@ -0,0 +1,564 @@
1
+ # encoding: US-ASCII
2
+ # Manipulation of LDIF data.
3
+ #
4
+ #--
5
+ # $Id: ldif.rb,v 1.11 2005/03/03 01:32:07 ianmacd Exp $
6
+ #++
7
+ #
8
+ # Copyright (C) 2005 Ian Macdonald <ian@caliban.org>
9
+ #
10
+
11
+ module LDAP
12
+
13
+ # Record objects are embodiments of LDAP operations. They possess a DN,
14
+ # a change type (*LDAP_MOD_ADD*, *LDAP_MOD_DELETE* or *LDAP_MOD_REPLACE*
15
+ # [any of which can be logically AND'ed with *LDAP_MOD_BVALUES*]), a hash of
16
+ # attributes and value arrays, a hash of modification operations (useful
17
+ # only when the change type is *LDAP_MOD_REPLACE*) and an array of
18
+ # LDAP controls.
19
+ #
20
+ # The Record class's primary use is as a transitional medium for LDIF
21
+ # operations parsed by the LDAP::LDIF module. You are unlikely to want to
22
+ # use it in application code.
23
+ #
24
+ class Record
25
+ attr_reader :dn, :change_type, :attrs, :mods, :controls
26
+
27
+ def initialize(dn, change_type, attrs, mods=nil, ctls=nil)
28
+ @dn = dn
29
+ @change_type = change_type
30
+ @attrs = attrs
31
+ @mods = mods
32
+ @controls = ctls
33
+ end
34
+
35
+
36
+ # Send the operation embodied in the Record object to the LDAP::Conn
37
+ # object specified in +conn+.
38
+ #
39
+ def send( conn )
40
+ if @change_type == :MODRDN
41
+ # TODO: How do we deal with 'newsuperior'?
42
+ # The LDAP API's ldap_modrdn2_s() function doesn't seem to use it.
43
+ return conn.modrdn( @dn, @attrs['newrdn'], @attrs['deleteoldrdn'] )
44
+ end
45
+
46
+ # Mask out the LDAP_MOD_BVALUES bit, as it's irrelevant here.
47
+ case @change_type & ~LDAP_MOD_BVALUES
48
+ when LDAP_MOD_ADD
49
+ @controls == [] ? conn.add( @dn, @attrs ) :
50
+ conn.add_ext( @dn, @attrs, @controls, [] )
51
+ when LDAP_MOD_DELETE
52
+ @controls == [] ? conn.delete( @dn ) :
53
+ conn.delete_ext( @dn, @controls, [] )
54
+ when LDAP_MOD_REPLACE
55
+ @controls == [] ? conn.modify( @dn, @mods ) :
56
+ conn.modify_ext( @dn, @mods, @controls, [] )
57
+ end
58
+
59
+ self
60
+ end
61
+
62
+
63
+ # Remove common operational attributes from a Record object. This is
64
+ # useful if you have Record objects formed from LDIF data that contained
65
+ # operational attributes. Using LDAP::Record#send to send such an object
66
+ # to an LDAP server is likely to meet with an exception unless the data is
67
+ # first cleaned.
68
+ #
69
+ # In addition, attributes with duplicate values are pruned, as this can
70
+ # also result in an exception.
71
+ #
72
+ def clean
73
+
74
+ # TODO: These operational attributes are those commonly used by
75
+ # OpenLDAP 2.2. Others should probably be supported.
76
+ #
77
+ %w[ creatorsname createtimestamp modifiersname modifytimestamp
78
+ entrycsn entryuuid structuralobjectclass ].each do |attr|
79
+ @attrs.delete( attr )
80
+ end
81
+
82
+ # Clean out duplicate attribute values.
83
+ @attrs.each_key { |k| @attrs[k].uniq! }
84
+
85
+ self
86
+ end
87
+
88
+ end
89
+
90
+
91
+ # This module provides the ability to process LDIF entries and files.
92
+ #
93
+ module LDIF
94
+ LINE_LENGTH = 77
95
+
96
+ private
97
+
98
+ class Entry < String; end
99
+ class Mod < String; end
100
+ class LDIFError < LDAP::Error; end
101
+
102
+
103
+ # return *true* if +str+ contains a character with an ASCII value > 127 or
104
+ # a NUL, LF or CR. Otherwise, *false* is returned.
105
+ #
106
+ def LDIF.unsafe_char?( str )
107
+ # This could be written as a single regex, but this is faster.
108
+ str =~ /^[ :]/ || str =~ /[\x00-\x1f\x7f-\xff]/
109
+ end
110
+
111
+
112
+ # Perform Base64 decoding of +str+. If +concat+ is *true*, LF characters
113
+ # are stripped.
114
+ #
115
+ def LDIF.base64_encode( str, concat=false )
116
+ str = [ str ].pack( 'm' )
117
+ str.gsub!( /\n/, '' ) if concat
118
+ str
119
+ end
120
+
121
+
122
+ # Perform Base64 encoding of +str+.
123
+ #
124
+ def LDIF.base64_decode( str )
125
+ str.unpack( 'm*' )[0]
126
+ end
127
+
128
+
129
+ # Read a file from the URL +url+. At this time, the only type of URL
130
+ # supported is the +file://+ URL.
131
+ #
132
+ def LDIF.read_file( url )
133
+ unless url.sub!( %r(^file://), '' )
134
+ raise ArgumentError, "Bad external file reference: #{url}"
135
+ end
136
+
137
+ # Slurp an external file.
138
+ # TODO: Support other URL types in the future.
139
+ File.open( url ).readlines( nil )[0]
140
+ end
141
+
142
+
143
+ # This converts an attribute and array of values to LDIF.
144
+ #
145
+ def LDIF.to_ldif( attr, vals )
146
+ ldif = ''
147
+
148
+ vals.each do |val|
149
+ sep = ':'
150
+ if unsafe_char?( val )
151
+ sep = '::'
152
+ val = base64_encode( val, true )
153
+ end
154
+
155
+ firstline_len = LINE_LENGTH - ( "%s%s " % [ attr, sep ] ).length
156
+ ldif << "%s%s %s\n" % [ attr, sep, val.slice!( 0..firstline_len ) ]
157
+
158
+ while val.length > 0
159
+ ldif << " %s\n" % val.slice!( 0..LINE_LENGTH - 1 )
160
+ end
161
+ end
162
+
163
+ ldif
164
+
165
+ end
166
+
167
+
168
+ public
169
+
170
+
171
+ # Parse the LDIF entry contained in +lines+ and return an LDAP::Record
172
+ # object. +lines+ should be an object that responds to each, such as a
173
+ # string or an array of lines, separated by \n characters.
174
+ #
175
+ def LDIF.parse_entry( lines )
176
+ header = true
177
+ comment = false
178
+ change_type = nil
179
+ sep = nil
180
+ attr = nil
181
+ bvalues = []
182
+ controls = nil
183
+ hash = {}
184
+ mods = {}
185
+ mod_type = nil
186
+
187
+ lines.each do |line|
188
+ _line = line.chomp
189
+ # Skip (continued) comments.
190
+ if _line =~ /^#/ || ( comment && _line[0..0] == ' ' )
191
+ comment = true
192
+ next
193
+ end
194
+
195
+ # Skip blank lines.
196
+ next if _line =~ /^$/
197
+
198
+ # Reset mod type if this entry has more than one mod to make.
199
+ # A '-' continuation is only valid if we've already had a
200
+ # 'changetype: modify' line.
201
+ if _line =~ /^-$/ && change_type == LDAP_MOD_REPLACE
202
+ next
203
+ end
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
+ end
222
+
223
+ # Found a attr/value line.
224
+ attr, val = _line.split( md[1], 2 )
225
+ attr.downcase!
226
+
227
+ # Attribute must be ldap-oid / (ALPHA *(attr-type-chars))
228
+ if attr !~ /^(?:(?:\d+\.)*\d+|[[:alnum:]-]+)(?:;[[:alnum:]-]+)*$/
229
+ raise LDIFError, "Invalid attribute: #{attr}"
230
+ end
231
+
232
+ if attr == 'dn'
233
+ header = false
234
+ change_type = nil
235
+ controls = []
236
+ end
237
+ sep = md[2]
238
+
239
+ val = read_file( val ) if sep == ':<'
240
+
241
+ case attr
242
+ when 'version'
243
+ # Check the LDIF version.
244
+ if header
245
+ if val != '1'
246
+ raise LDIFError, "Unsupported LDIF version: #{val}"
247
+ else
248
+ header = false
249
+ next
250
+ end
251
+ end
252
+
253
+ when 'changetype'
254
+ change_type = case val
255
+ when 'add' then LDAP_MOD_ADD
256
+ when 'delete' then LDAP_MOD_DELETE
257
+ when 'modify' then LDAP_MOD_REPLACE
258
+ when /^modr?dn$/ then :MODRDN
259
+ end
260
+
261
+ raise LDIFError, "Invalid change type: #{attr}" unless change_type
262
+
263
+ when 'add', 'delete', 'replace'
264
+ unless change_type == LDAP_MOD_REPLACE
265
+ raise LDIFError, "Cannot #{attr} here."
266
+ end
267
+
268
+ mod_type = case attr
269
+ when 'add' then LDAP_MOD_ADD
270
+ when 'delete' then LDAP_MOD_DELETE
271
+ when 'replace' then LDAP_MOD_REPLACE
272
+ end
273
+
274
+ # In this case val is actually an attribute and should be lowercased.
275
+ mods[mod_type] ||= {}
276
+ mods[mod_type][val.downcase] ||= []
277
+
278
+ when 'control'
279
+ oid, criticality = val.split( / /, 2 )
280
+
281
+ unless oid =~ /(?:\d+\.)*\d+/
282
+ raise LDIFError, "Bad control OID: #{oid}"
283
+ end
284
+
285
+ if criticality
286
+ md = criticality.match( /(:[:<]?) ?/ )
287
+ ctl_sep = md[1] if md
288
+ criticality, value = criticality.split( /:[:<]? ?/, 2 )
289
+
290
+ if criticality !~ /^(?:true|false)$/
291
+ raise LDIFError, "Bad control criticality: #{criticality}"
292
+ end
293
+
294
+ # Convert 'true' or 'false'. to_boolean would be nice. :-)
295
+ criticality = eval( criticality )
296
+ end
297
+
298
+ if value
299
+ value = base64_decode( value ) if ctl_sep == '::'
300
+ value = read_file( value ) if ctl_sep == ':<'
301
+ value = Control.encode( value )
302
+ end
303
+
304
+ controls << Control.new( oid, value, criticality )
305
+
306
+ else
307
+ # Convert modrdn's deleteoldrdn from '1' to true, anything else
308
+ # to false. Should probably raise an exception if not '0' or '1'.
309
+ #
310
+ if change_type == :MODRDN && attr == 'deleteoldrdn'
311
+ val = val == '1' ? true : false
312
+ end
313
+
314
+ if change_type == LDAP_MOD_REPLACE
315
+ mods[mod_type][attr] << val
316
+ else
317
+ hash[attr] ||= []
318
+ hash[attr] << val
319
+ end
320
+
321
+ comment = false
322
+
323
+ # Make a note of this attribute if value is binary.
324
+ bvalues << attr if unsafe_char?( val )
325
+
326
+ end
327
+
328
+ else
329
+ # Check last line's separator: if not a binary value, the
330
+ # continuation line must be indented. If a comment makes it this
331
+ # far, that's also an error.
332
+ #
333
+ if sep == ':' && _line[0..0] != ' ' || comment
334
+ raise LDIFError, "Improperly continued line: #{_line}"
335
+ end
336
+
337
+ # OK; this is a valid continuation line.
338
+
339
+ # Append line except for initial space.
340
+ _line[0] = '' if _line[0..0] == ' '
341
+
342
+ if change_type == LDAP_MOD_REPLACE
343
+ # Append to last value of current mod type.
344
+ mods[mod_type][attr][-1] << _line
345
+ else
346
+ # Append to last value.
347
+ hash[attr][-1] << _line
348
+ end
349
+ end
350
+
351
+ end
352
+
353
+ # If last value in LDIF entry was Base64-encoded, we need to decode
354
+ # it now.
355
+ if sep == '::'
356
+ if mod_type
357
+ mods[mod_type][attr][-1] =
358
+ base64_decode( mods[mod_type][attr][-1] )
359
+ bvalues << attr if unsafe_char?( mods[mod_type][attr][-1] )
360
+ else
361
+ hash[attr][-1] = base64_decode( hash[attr][-1] )
362
+ bvalues << attr if unsafe_char?( hash[attr][-1] )
363
+ end
364
+ end
365
+
366
+ # Remove and remember DN.
367
+ dn = hash.delete( 'dn' )[0]
368
+
369
+ # This doesn't really matter, but let's be anal about it, because it's
370
+ # not an attribute and doesn't belong here.
371
+ bvalues.delete( 'dn' )
372
+
373
+ # If there's no change type, it's just plain LDIF data, so we'll treat
374
+ # it like an addition.
375
+ change_type ||= LDAP_MOD_ADD
376
+
377
+ case change_type
378
+ when LDAP_MOD_ADD
379
+ mods[LDAP_MOD_ADD] = []
380
+
381
+ hash.each do |attr_local, val|
382
+ if bvalues.include?( attr_local )
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_local, val )
389
+ end
390
+
391
+ when LDAP_MOD_DELETE
392
+ # Nothing to do.
393
+
394
+ when LDAP_MOD_REPLACE
395
+ raise LDIFError, "mods should not be empty" if mods == {}
396
+
397
+ new_mods = {}
398
+
399
+ mods.each do |mod_type_local,attrs|
400
+ attrs.each_key do |attr_local|
401
+ if bvalues.include?( attr_local )
402
+ mt = mod_type_local | LDAP_MOD_BVALUES
403
+ else
404
+ mt = mod_type_local
405
+ end
406
+
407
+ new_mods[mt] ||= {}
408
+ new_mods[mt][attr_local] = mods[mod_type_local][attr_local]
409
+ end
410
+ end
411
+
412
+ mods = new_mods
413
+
414
+ when :MODRDN
415
+ # Nothing to do.
416
+
417
+ end
418
+
419
+ Record.new( dn, change_type, hash, mods, controls )
420
+ end
421
+
422
+
423
+ # Open and parse a file containing LDIF entries. +file+ should be a string
424
+ # containing the path to the file. If +sort+ is true, the resulting array
425
+ # of LDAP::Record objects will be sorted on DN length, which can be useful
426
+ # to avoid a later attempt to process an entry whose parent does not yet
427
+ # exist. This can easily happen if your LDIF file is unordered, which is
428
+ # likely if it was produced with a tool such as <em>slapcat(8)</em>.
429
+ #
430
+ # If a block is given, each LDAP::Record object will be yielded to the
431
+ # block and *nil* will be returned instead of the array. This is much less
432
+ # memory-intensive when parsing a large LDIF file.
433
+ #
434
+ def LDIF.parse_file( file, sort=false ) # :yield: record
435
+
436
+ File.open( file ) do |f|
437
+ entries = []
438
+ entry = false
439
+ header = true
440
+ version = false
441
+
442
+ while line = f.gets
443
+
444
+ if line =~ /^dn:/
445
+ header = false
446
+
447
+ if entry && ! version
448
+ if block_given?
449
+ yield parse_entry( entry )
450
+ else
451
+ entries << parse_entry( entry )
452
+ end
453
+ end
454
+
455
+ if version
456
+ entry << line
457
+ version = false
458
+ else
459
+ entry = [ line ]
460
+ end
461
+
462
+ next
463
+ end
464
+
465
+ if header && line.downcase =~ /^version/
466
+ entry = [ line ]
467
+ version = true
468
+ next
469
+ end
470
+
471
+ entry << line
472
+ end
473
+
474
+ if block_given?
475
+ yield parse_entry( entry )
476
+ nil
477
+ else
478
+ entries << parse_entry( entry )
479
+
480
+ # Sort entries if sorting has been requested.
481
+ entries.sort! { |x,y| x.dn.length <=> y.dn.length } if sort
482
+ entries
483
+ end
484
+
485
+ end
486
+
487
+ end
488
+
489
+
490
+ # Given the DN, +dn+, convert a single LDAP::Mod or an array of
491
+ # LDAP::Mod objects, given in +mods+, to LDIF.
492
+ #
493
+ def LDIF.mods_to_ldif( dn, *mods )
494
+ ldif = "dn: %s\nchangetype: modify\n" % dn
495
+ plural = false
496
+
497
+ mods.flatten.each do |mod|
498
+ # TODO: Need to dynamically assemble this case statement to add
499
+ # OpenLDAP's increment change type, etc.
500
+ change_type = case mod.mod_op & ~LDAP_MOD_BVALUES
501
+ when LDAP_MOD_ADD then 'add'
502
+ when LDAP_MOD_DELETE then 'delete'
503
+ when LDAP_MOD_REPLACE then 'replace'
504
+ end
505
+
506
+ ldif << "-\n" if plural
507
+ ldif << LDIF.to_ldif( change_type, [mod.mod_type] )
508
+ ldif << LDIF.to_ldif( mod.mod_type, mod.mod_vals )
509
+
510
+ plural = true
511
+ end
512
+
513
+ LDIF::Mod.new( ldif )
514
+ end
515
+
516
+ end
517
+
518
+
519
+ class Entry
520
+
521
+ # Convert an LDAP::Entry to LDIF.
522
+ #
523
+ def to_ldif
524
+ ldif = "dn: %s\n" % get_dn
525
+
526
+ get_attributes.each do |attr|
527
+ get_values( attr ).each do |val|
528
+ ldif << LDIF.to_ldif( attr, [ val ] )
529
+ end
530
+ end
531
+
532
+ LDIF::Entry.new( ldif )
533
+ end
534
+
535
+ alias_method :to_s, :to_ldif
536
+ end
537
+
538
+
539
+ class Mod
540
+
541
+ # Convert an LDAP::Mod with the DN given in +dn+ to LDIF.
542
+ #
543
+ def to_ldif( dn )
544
+ ldif = "dn: %s\n" % dn
545
+
546
+ # TODO: Need to dynamically assemble this case statement to add
547
+ # OpenLDAP's increment change type, etc.
548
+ case mod_op & ~LDAP_MOD_BVALUES
549
+ when LDAP_MOD_ADD
550
+ ldif << "changetype: add\n"
551
+ when LDAP_MOD_DELETE
552
+ ldif << "changetype: delete\n"
553
+ when LDAP_MOD_REPLACE
554
+ return LDIF.mods_to_ldif( dn, self )
555
+ end
556
+
557
+ ldif << LDIF.to_ldif( mod_type, mod_vals )
558
+ LDIF::Mod.new( ldif )
559
+ end
560
+
561
+ alias_method :to_s, :to_ldif
562
+ end
563
+
564
+ end
@@ -0,0 +1,135 @@
1
+ # Manipulation of LDAP schema data.
2
+ #
3
+ #--
4
+ # $Id: schema.rb,v 1.9 2006/02/08 23:15:17 ianmacd Exp $
5
+ #++
6
+
7
+ # The LDAP module encapsulates the various LDAP-related classes in their own
8
+ # namespace.
9
+ #
10
+ module LDAP
11
+
12
+ # Retrieve and process information pertaining to LDAP schemas.
13
+ #
14
+ class Schema < Hash
15
+
16
+ def initialize(entry)
17
+ if( entry )
18
+ entry.each do |key, vals|
19
+ self[key] = vals
20
+ end
21
+ end
22
+ end
23
+
24
+ # Return the list of values related to the schema component given in
25
+ # +key+. See LDAP::Conn#schema for common values of +key+.
26
+ #
27
+ def names(key)
28
+ self[key].collect{|val| val =~ /NAME\s+'([\w\d_\-]+)'/; $1}
29
+ end
30
+
31
+ # Return the list of attributes in object class +oc+ that are of category
32
+ # +at+. +at+ may be the string *MUST*, *MAY* or *SUP*.
33
+ #
34
+ def attr(oc,at)
35
+ self['objectClasses'].each do |s|
36
+ if( s =~ /NAME\s+'#{oc}'/ )
37
+ case s
38
+ when /#{at}\s+\(([\w\d_\-\s\$]+)\)/i then return $1.split("$").collect{|attr| attr.strip}
39
+ when /#{at}\s+([\w\d_\-]+)/i then return $1.split("$").collect{|attr| attr.strip}
40
+ end
41
+ end
42
+ end
43
+
44
+ return nil
45
+ end
46
+
47
+ # Return the list of attributes that an entry with object class +oc+
48
+ # _must_ possess.
49
+ #
50
+ def must(oc)
51
+ attr(oc, "MUST")
52
+ end
53
+
54
+ # Return the list of attributes that an entry with object class +oc+
55
+ # _may_ possess.
56
+ #
57
+ def may(oc)
58
+ attr(oc, "MAY")
59
+ end
60
+
61
+ # Return the superior object class of object class +oc+.
62
+ #
63
+ def sup(oc)
64
+ attr(oc, "SUP")
65
+ end
66
+
67
+ end
68
+
69
+ class Conn
70
+
71
+ # Fetch the schema data for the connection.
72
+ #
73
+ # If +base+ is given, it gives the base DN for the search. +attrs+, if
74
+ # given, is an array of attributes that should be returned from the
75
+ # server. The default list is *objectClasses*, *attributeTypes*,
76
+ # *matchingRules*, *matchingRuleUse*, *dITStructureRules*,
77
+ # *dITContentRules*, *nameForms* and *ldapSyntaxes*.
78
+ #
79
+ # +sec+ and +usec+ can be used to specify a time-out for the search in
80
+ # seconds and microseconds, respectively.
81
+ #
82
+ def schema(base = nil, attrs = nil, sec = 0, usec = 0)
83
+ attrs ||= [
84
+ 'objectClasses',
85
+ 'attributeTypes',
86
+ 'matchingRules',
87
+ 'matchingRuleUse',
88
+ 'dITStructureRules',
89
+ 'dITContentRules',
90
+ 'nameForms',
91
+ 'ldapSyntaxes',
92
+ ]
93
+ base ||= root_dse(['subschemaSubentry'], sec, usec)[0]['subschemaSubentry'][0]
94
+ base ||= 'cn=schema'
95
+ entries = search2(base, LDAP_SCOPE_BASE, '(objectClass=subschema)',
96
+ attrs, false, sec, usec)
97
+
98
+ return Schema.new(entries[0])
99
+ end
100
+
101
+ # Fetch the root DSE (DSA-specific Entry) for the connection. DSA stands
102
+ # for Directory System Agent and simply refers to the LDAP server you are
103
+ # using.
104
+ #
105
+ # +attrs+, if given, is an array of attributes that should be returned
106
+ # from the server. The default list is *subschemaSubentry*,
107
+ # *namingContexts*, *monitorContext*, *altServer*, *supportedControl*,
108
+ # *supportedExtension*, *supportedFeatures*, *supportedSASLMechanisms*
109
+ # and *supportedLDAPVersion*.
110
+ #
111
+ # +sec+ and +usec+ can be used to specify a time-out for the search in
112
+ # seconds and microseconds, respectively.
113
+ #
114
+ def root_dse(attrs = nil, sec = 0, usec = 0)
115
+ attrs ||= [
116
+ 'subschemaSubentry',
117
+ 'namingContexts',
118
+ 'monitorContext',
119
+ 'altServer',
120
+ 'supportedControl',
121
+ 'supportedExtension',
122
+ 'supportedFeatures',
123
+ 'supportedSASLMechanisms',
124
+ 'supportedLDAPVersion',
125
+ ]
126
+
127
+ entries = search2('', LDAP_SCOPE_BASE, '(objectClass=*)',
128
+ attrs, false, sec, usec)
129
+
130
+ return entries
131
+ end
132
+
133
+ end
134
+
135
+ end