gdbm 0.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,30 @@
1
+ ## ffi-gdbm
2
+
3
+ An attempt to make gdbm available beyond the C Ruby implementation.
4
+
5
+ ## Installing
6
+
7
+ You can download and use `gdbm.rb` anyhow you would like.
8
+
9
+ You can also install it using Ruby Gems:
10
+
11
+ `jgem install gdbm --source http://gemcutter.org`
12
+
13
+ ## Notes
14
+
15
+ * Conforms to tests for 1.8.7 and 1.9.1 and follows the C library for MRI if there are contradictions with the documentation
16
+ * Should be compatible with gdbm files created with MRI
17
+ * Does not work with JRuby 1.3, try using it JRuby 1.4 or the master from http://github.com/jruby/jruby
18
+ * Only works with JRuby, as it relies on features from JRuby's FFI that are not available in Ruby FFI
19
+
20
+ ## Status
21
+
22
+ Passes all tests from 1.8.7 and 1.9.1 except those related to [this JRuby bug](http://jira.codehaus.org/browse/JRUBY-4071), which will probably not matter to too many people. Once this is resolved, the version should go to 1.0.
23
+
24
+ ## Testing
25
+
26
+ Two sets of tests are included, copied straight from the MRI distribution. However, they do require the use of ObjectSpace, so this is how to run them:
27
+
28
+ `jruby -X+O -r lib/gdbm test/test_gdbm-1.8.7.rb`
29
+
30
+ `jruby --1.9 -X+O -r lib/gdbm test/test_gdbm-1.9.1.rb`
@@ -0,0 +1,615 @@
1
+ =begin
2
+ JRuby access to gdbm via FFI. Faithfully mimics MRI's standard library and is
3
+ compatible with gdbm files produced by that version.
4
+
5
+ Author: Justin Collins
6
+ Based on the C version by: yugui
7
+ Website: http://github.com/presidentbeef/ffi-gdbm
8
+ Documentation: http://ruby-doc.org/stdlib/libdoc/gdbm/rdoc/classes/GDBM.html
9
+ JRuby: http://www.jruby.org/
10
+ gdbm: http://directory.fsf.org/project/gdbm/
11
+
12
+ The MIT License
13
+
14
+ Copyright (c) 2009, Justin Collins
15
+
16
+ Permission is hereby granted, free of charge, to any person obtaining a copy
17
+ of this software and associated documentation files (the "Software"), to deal
18
+ in the Software without restriction, including without limitation the rights
19
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
20
+ copies of the Software, and to permit persons to whom the Software is
21
+ furnished to do so, subject to the following conditions:
22
+
23
+ The above copyright notice and this permission notice shall be included in
24
+ all copies or substantial portions of the Software.
25
+
26
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
27
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
28
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
29
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
30
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
31
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
32
+ THE SOFTWARE.
33
+ =end
34
+
35
+ require 'ffi'
36
+
37
+ module GDBM_FFI
38
+ extend FFI::Library
39
+ ffi_lib "gdbm"
40
+
41
+ #Note that MRI does not store the null byte, so neither does this version,
42
+ #even though FFI automatically appends one to the String.
43
+ class Datum < FFI::Struct
44
+ layout :dptr, :pointer, :dsize, :int
45
+
46
+ #Expects either a MemoryPointer or a String as an argument.
47
+ #If it is given a String, it will initialize the fields, including
48
+ #setting dsize.
49
+ def initialize(*args)
50
+ if args.length == 0 or (args.length == 1 and args[0].is_a? FFI::MemoryPointer)
51
+ super
52
+ elsif args.length == 1 and args[0].is_a? String
53
+ super()
54
+ self.dptr = args[0]
55
+ self[:dsize] = args[0].length
56
+ end
57
+ end
58
+
59
+ def value
60
+ if self[:dptr].nil? or self[:dptr].null?
61
+ nil
62
+ else
63
+ self[:dptr].read_string(self.size)
64
+ end
65
+ end
66
+
67
+ #_Do not use_. Creates a new MemoryPointer from the String.
68
+ def dptr=(str)
69
+ @dptr = FFI::MemoryPointer.from_string(str)
70
+ self[:dptr] = @dptr
71
+ end
72
+
73
+ #Returns the size of the stored String.
74
+ def size
75
+ self[:dsize]
76
+ end
77
+ end
78
+
79
+ callback :fatal_func, [:string], :void
80
+
81
+ #Attach gdbm functions
82
+ attach_function :gdbm_open, [ :string, :int, :int, :int, :fatal_func ], :pointer
83
+ attach_function :close, :gdbm_close, [ :pointer ], :void
84
+ attach_function :gdbm_store, [ :pointer, Datum.by_value, Datum.by_value, :int ], :int
85
+ attach_function :gdbm_fetch, [ :pointer, Datum.by_value ], Datum.by_value
86
+ attach_function :gdbm_delete, [ :pointer, Datum.by_value ], :int
87
+ attach_function :gdbm_firstkey, [ :pointer ], Datum.by_value
88
+ attach_function :gdbm_nextkey, [ :pointer, Datum.by_value ], Datum.by_value
89
+ attach_function :reorganize, :gdbm_reorganize, [ :pointer ], :int
90
+ attach_function :sync, :gdbm_sync, [ :pointer ], :void
91
+ attach_function :gdbm_exists, [ :pointer, Datum.by_value ], :int
92
+ attach_function :set_opt, :gdbm_setopt, [ :pointer, :int, :pointer, :int ], :int
93
+ attach_function :error_string, :gdbm_strerror, [ :int ], :string
94
+
95
+ READER = 0
96
+ WRITER = 1
97
+ WRCREAT = 2
98
+ NEWDB = 3
99
+ FAST = 0x10
100
+ SYNC = 0x20
101
+ NOLOCK = 0x40
102
+ REPLACE = 1
103
+ CACHE_SIZE = 1
104
+ FAST_MODE = 2
105
+ SYNC_MODE = 3
106
+ CANT_BE_READER = 9
107
+ CANT_BE_WRITER = 10
108
+ FILE_OPEN_ERROR = 3
109
+
110
+ FATAL = Proc.new { |msg| raise RuntimeError, msg }
111
+
112
+ attach_variable :error_number, :gdbm_errno, :int
113
+ attach_variable :VERSION, :gdbm_version, :string
114
+
115
+ #Store the given Strings in _file_. _file_ is always GDBM_FILE pointer in these functions.
116
+ def self.store(file, key, value)
117
+ key_datum = Datum.new key
118
+ val_datum = Datum.new value
119
+
120
+ result = gdbm_store file, key_datum, val_datum, GDBM_FFI::REPLACE
121
+ raise GDBMError, last_error if result != 0
122
+ end
123
+
124
+ #Fetch a String from the _file_ matching the given _key_. Returns _nil_ if
125
+ #there is no such key.
126
+ def self.fetch(file, key)
127
+ key_datum = Datum.new key
128
+
129
+ val_datum = gdbm_fetch file, key_datum
130
+
131
+ val_datum.value
132
+ end
133
+
134
+ #Returns the first key in the _file_.
135
+ def self.first_key(file)
136
+ key_datum = GDBM_FFI.gdbm_firstkey file
137
+ key_datum.value
138
+ end
139
+
140
+ #Deletes the _key_ from the _file_.
141
+ def self.delete(file, key)
142
+ return nil if not self.exists? file, key
143
+ key_datum = Datum.new key
144
+ result = gdbm_delete file, key_datum
145
+ raise GDBMError, last_error if result != 0
146
+ end
147
+
148
+ #Checks if the _file_ contains the given _key_.
149
+ def self.exists?(file, key)
150
+ key_datum = Datum.new key
151
+
152
+ gdbm_exists(file, key_datum) != 0
153
+ end
154
+
155
+ #Iterates over each _key_, _value_ pair in the _file_.
156
+ def self.each_pair(file)
157
+ current = self.gdbm_firstkey file
158
+ until current.value.nil?
159
+ value = gdbm_fetch file, current
160
+ yield current.value, value.value
161
+ current = self.gdbm_nextkey file, current
162
+ end
163
+ end
164
+
165
+ #Iterates over each key in the _file_.
166
+ def self.each_key(file)
167
+ current = self.gdbm_firstkey file
168
+ until current.value.nil?
169
+ yield current.value
170
+ current = self.gdbm_nextkey file, current
171
+ end
172
+ end
173
+
174
+ #Iterates over each value in the _file_.
175
+ def self.each_value(file)
176
+ current = self.gdbm_firstkey file
177
+ until current.value.nil?
178
+ value = gdbm_fetch file, current
179
+ yield value.value
180
+ current = self.gdbm_nextkey file, current
181
+ end
182
+ end
183
+
184
+ #Deletes all keys and values from the _file_.
185
+ def self.clear(file)
186
+ until (key = self.gdbm_firstkey(file)).value.nil?
187
+ until key.value.nil?
188
+ next_key = self.gdbm_nextkey(file, key)
189
+ result = self.gdbm_delete file, key
190
+ raise GDBMError, last_error if result != 0
191
+ key = next_key
192
+ end
193
+ end
194
+ end
195
+
196
+ #Returns the last error encountered from the gdbm library as a String.
197
+ def self.last_error
198
+ error_string(error_number)
199
+ end
200
+
201
+ #Opens a gdbm file. Returns the GDBM_FILE pointer, which should be treated
202
+ #as an opaque value, to be passed in to GDBM_FFI methods.
203
+ def self.open(filename, blocksize, flags, mode)
204
+ self.gdbm_open filename, blocksize, flags, mode, FATAL
205
+ end
206
+
207
+ #Sets the cache size.
208
+ def self.set_cache_size(file, size)
209
+ opt = MemoryPointer.new size
210
+ self.set_opt file, CACHE_SIZE, opt, opt.size
211
+ end
212
+
213
+ #Sets the sync mode.
214
+ def self.set_sync_mode(file, boolean)
215
+ if boolean
216
+ opt = MemoryPointer.new 1
217
+ else
218
+ opt = MemoryPointer.new 0
219
+ end
220
+
221
+ self.set_opt file, SYNC_MODE, opt, opt.size
222
+ end
223
+ end
224
+
225
+ class GDBMError < StandardError; end
226
+ class GDBMFatalError < Exception; end
227
+
228
+ class GDBM
229
+ include Enumerable
230
+
231
+ #This constant is to check if a flag is READER, WRITER, WRCREAT, or NEWDB
232
+ RUBY_GDBM_RW_BIT = 0x20000000
233
+
234
+ BLOCKSIZE = 2048
235
+ READER = GDBM_FFI::READER | RUBY_GDBM_RW_BIT
236
+ WRITER = GDBM_FFI::WRITER | RUBY_GDBM_RW_BIT
237
+ WRCREAT = GDBM_FFI::WRCREAT | RUBY_GDBM_RW_BIT
238
+ NEWDB = GDBM_FFI::NEWDB | RUBY_GDBM_RW_BIT
239
+ FAST = GDBM_FFI::FAST
240
+ SYNC = GDBM_FFI::SYNC
241
+ NOLOCK = GDBM_FFI::NOLOCK
242
+ VERSION = GDBM_FFI.VERSION
243
+
244
+ def initialize(filename, mode = 0666, flags = nil)
245
+
246
+ mode = -1 if mode.nil?
247
+ flags = 0 if flags.nil?
248
+ @file = nil
249
+
250
+ if flags & RUBY_GDBM_RW_BIT != 0 #Check if flags are appropriate
251
+ flags &= ~RUBY_GDBM_RW_BIT #Remove check to make flag match GDBM constants
252
+
253
+ @file = GDBM_FFI.open filename, BLOCKSIZE, flags, mode
254
+ else
255
+ if mode >= 0
256
+ @file = GDBM_FFI.open filename, BLOCKSIZE, WRCREAT | flags, mode
257
+ end
258
+
259
+ @file = GDBM_FFI.open filename, BLOCKSIZE, WRITER | flags, 0 if @file.nil? or @file.null?
260
+ @file = GDBM_FFI.open filename, BLOCKSIZE, READER | flags, 0 if @file.nil? or @file.null?
261
+ end
262
+
263
+ if @file.nil? or @file.null?
264
+ return if mode == -1 #C code returns Qnil, but we can't
265
+ if GDBM_FFI.error_number == GDBM_FFI::FILE_OPEN_ERROR ||
266
+ GDBM_FFI.error_number == GDBM_FFI::CANT_BE_READER ||
267
+ GDBM_FFI.error_number == GDBM_FFI::CANT_BE_WRITER
268
+
269
+ raise SystemCallError.new(GDBM_FFI.last_error, FFI.errno)
270
+ else
271
+ raise GDBMError, GDBM_FFI.last_error;
272
+ end
273
+ end
274
+ end
275
+
276
+ def self.open(filename, mode = 0666, flags = nil)
277
+ obj = self.new filename, mode, flags
278
+
279
+ if block_given?
280
+ begin
281
+ result = yield obj
282
+ ensure
283
+ obj.close unless obj.closed?
284
+ end
285
+ result
286
+ elsif obj.nil?
287
+ nil
288
+ else
289
+ obj
290
+ end
291
+ end
292
+
293
+ def [](key)
294
+ GDBM_FFI.fetch file, key
295
+ end
296
+
297
+ def []=(key, value)
298
+ modifiable?
299
+ GDBM_FFI.store file, key, value
300
+ end
301
+
302
+ alias :store :[]=
303
+
304
+ def cachesize=(size)
305
+ GDBM_FFI.set_cache_size file, size
306
+ end
307
+
308
+ def clear
309
+ modifiable?
310
+ GDBM_FFI.clear file
311
+ self
312
+ end
313
+
314
+ def close
315
+ if closed?
316
+ raise RuntimeError, "closed GDBM file"
317
+ else
318
+ GDBM_FFI.close @file
319
+ @file = nil unless frozen?
320
+ end
321
+ end
322
+
323
+ def closed?
324
+ @file.nil? or @file.null?
325
+ end
326
+
327
+ def delete(key)
328
+ modifiable?
329
+ value = self[key]
330
+ #This is bizarre and not mentioned in the docs,
331
+ #but this is what the tests expect and what the MRI
332
+ #version does.
333
+ if value.nil? and block_given?
334
+ value = yield key
335
+ end
336
+ GDBM_FFI.delete file, key
337
+ value
338
+ end
339
+
340
+ def delete_if
341
+ modifiable?
342
+ rejects = []
343
+ begin
344
+ GDBM_FFI.each_pair(file) do |k,v|
345
+ if yield k, v
346
+ rejects << k
347
+ end
348
+ end
349
+ #unsure about this, but it handles breaking during
350
+ #the iteration
351
+ ensure
352
+ rejects.each do |k|
353
+ GDBM_FFI.delete file, k
354
+ end
355
+ end
356
+
357
+ self
358
+ end
359
+
360
+ alias :reject! :delete_if
361
+
362
+ def each_key(&block)
363
+ GDBM_FFI.each_key file, &block
364
+ self
365
+ end
366
+
367
+ def each_pair(&block)
368
+ GDBM_FFI.each_pair file, &block
369
+ self
370
+ end
371
+
372
+ alias :each :each_pair
373
+
374
+ def each_value(&block)
375
+ GDBM_FFI.each_value file, &block
376
+ self
377
+ end
378
+
379
+ def empty?
380
+ key = GDBM_FFI.first_key file
381
+ key.nil?
382
+ end
383
+
384
+ def fastmode=(boolean)
385
+ GDBM_FFI.set_sync_mode file, !boolean
386
+ end
387
+
388
+ def fetch(key, default = nil)
389
+ result = GDBM_FFI.fetch file, key
390
+ if result.nil?
391
+ if default
392
+ default
393
+ elsif block_given?
394
+ yield key
395
+ else
396
+ raise IndexError, "key not found"
397
+ end
398
+ else
399
+ result
400
+ end
401
+ end
402
+
403
+ def has_key?(key)
404
+ GDBM_FFI.exists? file, key
405
+ end
406
+
407
+ alias :key? :has_key?
408
+ alias :member? :has_key?
409
+ alias :include? :has_key?
410
+
411
+ def has_value?(value)
412
+ GDBM_FFI.each_value(file) do |v|
413
+ if v == value
414
+ return true
415
+ end
416
+ end
417
+ false
418
+ end
419
+
420
+ alias :value? :has_value?
421
+
422
+ def index(value)
423
+ if RUBY_VERSION >= "1.9"
424
+ warn "GDBM#index is deprecated; use GDBM#key"
425
+ end
426
+
427
+ self.key(value)
428
+ end
429
+
430
+ def invert
431
+ result = {}
432
+
433
+ GDBM_FFI.each_pair(file) do |k,v|
434
+ result[v] = k
435
+ end
436
+
437
+ result
438
+ end
439
+
440
+ def key(value)
441
+ GDBM_FFI.each_pair(file) do |k,v|
442
+ if v == value
443
+ return k
444
+ end
445
+ end
446
+ nil
447
+ end
448
+
449
+ def keys
450
+ keys = []
451
+
452
+ GDBM_FFI.each_key(file) do |k|
453
+ keys << k
454
+ end
455
+
456
+ keys
457
+ end
458
+
459
+ def length
460
+ len = 0
461
+
462
+ GDBM_FFI.each_key(file) do |k|
463
+ len = len + 1
464
+ end
465
+
466
+ len
467
+ end
468
+
469
+ alias :size :length
470
+
471
+ def nil?
472
+ @file.nil? or @file.null?
473
+ end
474
+
475
+ def reject
476
+ result = {}
477
+
478
+ GDBM_FFI.each_pair(file) do |k,v|
479
+ if not yield k, v
480
+ result[k] = v
481
+ end
482
+ end
483
+
484
+ result
485
+ end
486
+
487
+ def reorganize
488
+ modifiable?
489
+ GDBM_FFI.reorganize file
490
+ self
491
+ end
492
+
493
+ def replace(other)
494
+ self.clear
495
+ self.update other
496
+ self
497
+ end
498
+
499
+ def select(*args)
500
+ result = []
501
+ #This method behaves completely contrary to what the docs state:
502
+ #http://ruby-doc.org/stdlib/libdoc/gdbm/rdoc/classes/GDBM.html#M000318
503
+ #Instead, it yields a pair and returns [[k1, v1], [k2, v2], ...]
504
+ #But this is how it is in 1.8.7 and 1.9.1, so...
505
+
506
+ if block_given?
507
+ if args.length > 0
508
+ raise ArgumentError, "wrong number of arguments(#{args.length} for 0)"
509
+ end
510
+
511
+ GDBM_FFI.each_pair(file) do |k, v|
512
+ if yield k, v
513
+ result << [k, v]
514
+ end
515
+ end
516
+ #This is for 1.8.7 compatibility
517
+ elsif RUBY_VERSION <= "1.8.7"
518
+ warn "GDBM#select(index..) is deprecated; use GDBM#values_at"
519
+
520
+ result = values_at(*args)
521
+ else
522
+ result = []
523
+ end
524
+
525
+ result
526
+ end
527
+
528
+ def shift
529
+ modifiable?
530
+ key = GDBM_FFI.first_key file
531
+ if key
532
+ value = GDBM_FFI.fetch file, key
533
+ GDBM_FFI.delete file, key
534
+ [key, value]
535
+ else
536
+ nil
537
+ end
538
+ end
539
+
540
+ def sync
541
+ modifiable?
542
+ GDBM_FFI.sync file
543
+ self
544
+ end
545
+
546
+ def syncmode=(boolean)
547
+ GDBM_FFI.set_sync_mode file, boolean
548
+ end
549
+
550
+ def to_a
551
+ result = []
552
+ GDBM_FFI.each_pair(file) do |k,v|
553
+ result << [k, v]
554
+ end
555
+ result
556
+ end
557
+
558
+ def to_hash
559
+ result = {}
560
+ GDBM_FFI.each_pair(file) do |k,v|
561
+ result[k] = v
562
+ end
563
+ result
564
+ end
565
+
566
+ def update(other)
567
+ other.each_pair do |k,v|
568
+ GDBM_FFI.store file, k, v
569
+ end
570
+ self
571
+ end
572
+
573
+ def values
574
+ values = []
575
+
576
+ GDBM_FFI.each_value(file) do |v|
577
+ values << v
578
+ end
579
+
580
+ values
581
+ end
582
+
583
+ def values_at(*keys)
584
+ results = []
585
+
586
+ keys.each do |k|
587
+ results << self[k]
588
+ end
589
+
590
+ results
591
+ end
592
+
593
+ private
594
+
595
+ def modifiable?
596
+ #raise SecurityError, "Insecure operation at level #$SAFE" if $SAFE >= 4 #Not currently supported in JRuby
597
+ if self.frozen?
598
+ if RUBY_VERSION > "1.8.7"
599
+ raise RuntimeError, "Can't modify frozen #{self}"
600
+ else
601
+ raise TypeError, "Can't modify frozen #{self}"
602
+ end
603
+ end
604
+ end
605
+
606
+ #Raises a RuntimeError if the file is closed.
607
+ def file
608
+ unless @file.nil? or @file.null?
609
+ @file
610
+ else
611
+ raise(RuntimeError, "closed GDBM file")
612
+ end
613
+ end
614
+ end
615
+