Moved find_bucket_list, traverse_index, and get_next_key to Engine
[dbsrgits/DBM-Deep.git] / lib / DBM / Deep / Engine.pm
1 package DBM::Deep::Engine;
2
3 use strict;
4
5 use Fcntl qw( :DEFAULT :flock :seek );
6
7 sub open {
8     ##
9     # Open a fh to the database, create if nonexistent.
10     # Make sure file signature matches DBM::Deep spec.
11     ##
12     my $self = shift;
13     my $obj = shift;
14
15     if (defined($obj->_fh)) { $self->close( $obj ); }
16
17     eval {
18         local $SIG{'__DIE__'};
19         # Theoretically, adding O_BINARY should remove the need for the binmode
20         # Of course, testing it is going to be ... interesting.
21         my $flags = O_RDWR | O_CREAT | O_BINARY;
22
23         my $fh;
24         sysopen( $fh, $obj->_root->{file}, $flags )
25             or $fh = undef;
26         $obj->_root->{fh} = $fh;
27     }; if ($@ ) { $obj->_throw_error( "Received error: $@\n" ); }
28     if (! defined($obj->_fh)) {
29         return $obj->_throw_error("Cannot sysopen file: " . $obj->_root->{file} . ": $!");
30     }
31
32     my $fh = $obj->_fh;
33
34     #XXX Can we remove this by using the right sysopen() flags?
35     # Maybe ... q.v. above
36     binmode $fh; # for win32
37
38     if ($obj->_root->{autoflush}) {
39         my $old = select $fh;
40         $|=1;
41         select $old;
42     }
43
44     seek($fh, 0 + $obj->_root->{file_offset}, SEEK_SET);
45
46     my $signature;
47     my $bytes_read = read( $fh, $signature, length(DBM::Deep->SIG_FILE));
48
49     ##
50     # File is empty -- write signature and master index
51     ##
52     if (!$bytes_read) {
53         seek($fh, 0 + $obj->_root->{file_offset}, SEEK_SET);
54         print( $fh DBM::Deep->SIG_FILE);
55         $self->create_tag($obj, $obj->_base_offset, $obj->_type, chr(0) x $DBM::Deep::INDEX_SIZE);
56
57         my $plain_key = "[base]";
58         print( $fh pack($DBM::Deep::DATA_LENGTH_PACK, length($plain_key)) . $plain_key );
59
60         # Flush the filehandle
61         my $old_fh = select $fh;
62         my $old_af = $|; $| = 1; $| = $old_af;
63         select $old_fh;
64
65         my @stats = stat($fh);
66         $obj->_root->{inode} = $stats[1];
67         $obj->_root->{end} = $stats[7];
68
69         return 1;
70     }
71
72     ##
73     # Check signature was valid
74     ##
75     unless ($signature eq DBM::Deep->SIG_FILE) {
76         $self->close( $obj );
77         return $obj->_throw_error("Signature not found -- file is not a Deep DB");
78     }
79
80     my @stats = stat($fh);
81     $obj->_root->{inode} = $stats[1];
82     $obj->_root->{end} = $stats[7];
83
84     ##
85     # Get our type from master index signature
86     ##
87     my $tag = $self->load_tag($obj, $obj->_base_offset);
88
89 #XXX We probably also want to store the hash algorithm name and not assume anything
90 #XXX The cool thing would be to allow a different hashing algorithm at every level
91
92     if (!$tag) {
93         return $obj->_throw_error("Corrupted file, no master index record");
94     }
95     if ($obj->{type} ne $tag->{signature}) {
96         return $obj->_throw_error("File type mismatch");
97     }
98
99     return 1;
100 }
101
102 sub close {
103     my $self = shift;
104     my $obj = shift;
105
106     if ( my $fh = $obj->_root->{fh} ) {
107         close $fh;
108     }
109     $obj->_root->{fh} = undef;
110
111     return 1;
112 }
113
114 sub create_tag {
115     ##
116     # Given offset, signature and content, create tag and write to disk
117     ##
118     my $self = shift;
119     my ($obj, $offset, $sig, $content) = @_;
120     my $size = length($content);
121
122     my $fh = $obj->_fh;
123
124     seek($fh, $offset + $obj->_root->{file_offset}, SEEK_SET);
125     print( $fh $sig . pack($DBM::Deep::DATA_LENGTH_PACK, $size) . $content );
126
127     if ($offset == $obj->_root->{end}) {
128         $obj->_root->{end} += DBM::Deep->SIG_SIZE + $DBM::Deep::DATA_LENGTH_SIZE + $size;
129     }
130
131     return {
132         signature => $sig,
133         size => $size,
134         offset => $offset + DBM::Deep->SIG_SIZE + $DBM::Deep::DATA_LENGTH_SIZE,
135         content => $content
136     };
137 }
138
139 sub load_tag {
140     ##
141     # Given offset, load single tag and return signature, size and data
142     ##
143     my $self = shift;
144     my ($obj, $offset) = @_;
145
146     my $fh = $obj->_fh;
147
148     seek($fh, $offset + $obj->_root->{file_offset}, SEEK_SET);
149     if (eof $fh) { return undef; }
150
151     my $b;
152     read( $fh, $b, DBM::Deep->SIG_SIZE + $DBM::Deep::DATA_LENGTH_SIZE );
153     my ($sig, $size) = unpack( "A $DBM::Deep::DATA_LENGTH_PACK", $b );
154
155     my $buffer;
156     read( $fh, $buffer, $size);
157
158     return {
159         signature => $sig,
160         size => $size,
161         offset => $offset + DBM::Deep->SIG_SIZE + $DBM::Deep::DATA_LENGTH_SIZE,
162         content => $buffer
163     };
164 }
165
166 sub index_lookup {
167     ##
168     # Given index tag, lookup single entry in index and return .
169     ##
170     my $self = shift;
171     my ($obj, $tag, $index) = @_;
172
173     my $location = unpack($DBM::Deep::LONG_PACK, substr($tag->{content}, $index * $DBM::Deep::LONG_SIZE, $DBM::Deep::LONG_SIZE) );
174     if (!$location) { return; }
175
176     return $self->load_tag( $obj, $location );
177 }
178
179 sub add_bucket {
180     ##
181     # Adds one key/value pair to bucket list, given offset, MD5 digest of key,
182     # plain (undigested) key and value.
183     ##
184     my $self = shift;
185     my ($obj, $tag, $md5, $plain_key, $value) = @_;
186     my $keys = $tag->{content};
187     my $location = 0;
188     my $result = 2;
189
190     my $root = $obj->_root;
191
192     my $is_dbm_deep = eval { local $SIG{'__DIE__'}; $value->isa( 'DBM::Deep' ) };
193     my $internal_ref = $is_dbm_deep && ($value->_root eq $root);
194
195     my $fh = $obj->_fh;
196
197     ##
198     # Iterate through buckets, seeing if this is a new entry or a replace.
199     ##
200     for (my $i=0; $i<$DBM::Deep::MAX_BUCKETS; $i++) {
201         my $subloc = unpack($DBM::Deep::LONG_PACK, substr($keys, ($i * $DBM::Deep::BUCKET_SIZE) + $DBM::Deep::HASH_SIZE, $DBM::Deep::LONG_SIZE));
202         if (!$subloc) {
203             ##
204             # Found empty bucket (end of list).  Populate and exit loop.
205             ##
206             $result = 2;
207
208             $location = $internal_ref
209                 ? $value->_base_offset
210                 : $root->{end};
211
212             seek($fh, $tag->{offset} + ($i * $DBM::Deep::BUCKET_SIZE) + $root->{file_offset}, SEEK_SET);
213             print( $fh $md5 . pack($DBM::Deep::LONG_PACK, $location) );
214             last;
215         }
216
217         my $key = substr($keys, $i * $DBM::Deep::BUCKET_SIZE, $DBM::Deep::HASH_SIZE);
218         if ($md5 eq $key) {
219             ##
220             # Found existing bucket with same key.  Replace with new value.
221             ##
222             $result = 1;
223
224             if ($internal_ref) {
225                 $location = $value->_base_offset;
226                 seek($fh, $tag->{offset} + ($i * $DBM::Deep::BUCKET_SIZE) + $root->{file_offset}, SEEK_SET);
227                 print( $fh $md5 . pack($DBM::Deep::LONG_PACK, $location) );
228                 return $result;
229             }
230
231             seek($fh, $subloc + DBM::Deep->SIG_SIZE + $root->{file_offset}, SEEK_SET);
232             my $size;
233             read( $fh, $size, $DBM::Deep::DATA_LENGTH_SIZE); $size = unpack($DBM::Deep::DATA_LENGTH_PACK, $size);
234
235             ##
236             # If value is a hash, array, or raw value with equal or less size, we can
237             # reuse the same content area of the database.  Otherwise, we have to create
238             # a new content area at the EOF.
239             ##
240             my $actual_length;
241             my $r = Scalar::Util::reftype( $value ) || '';
242             if ( $r eq 'HASH' || $r eq 'ARRAY' ) {
243                 $actual_length = $DBM::Deep::INDEX_SIZE;
244
245                 # if autobless is enabled, must also take into consideration
246                 # the class name, as it is stored along with key/value.
247                 if ( $root->{autobless} ) {
248                     my $value_class = Scalar::Util::blessed($value);
249                     if ( defined $value_class && !$value->isa('DBM::Deep') ) {
250                         $actual_length += length($value_class);
251                     }
252                 }
253             }
254             else { $actual_length = length($value); }
255
256             if ($actual_length <= $size) {
257                 $location = $subloc;
258             }
259             else {
260                 $location = $root->{end};
261                 seek($fh, $tag->{offset} + ($i * $DBM::Deep::BUCKET_SIZE) + $DBM::Deep::HASH_SIZE + $root->{file_offset}, SEEK_SET);
262                 print( $fh pack($DBM::Deep::LONG_PACK, $location) );
263             }
264
265             last;
266         }
267     }
268
269     ##
270     # If this is an internal reference, return now.
271     # No need to write value or plain key
272     ##
273     if ($internal_ref) {
274         return $result;
275     }
276
277     ##
278     # If bucket didn't fit into list, split into a new index level
279     ##
280     if (!$location) {
281         seek($fh, $tag->{ref_loc} + $root->{file_offset}, SEEK_SET);
282         print( $fh pack($DBM::Deep::LONG_PACK, $root->{end}) );
283
284         my $index_tag = $self->create_tag($obj, $root->{end}, DBM::Deep->SIG_INDEX, chr(0) x $DBM::Deep::INDEX_SIZE);
285         my @offsets = ();
286
287         $keys .= $md5 . pack($DBM::Deep::LONG_PACK, 0);
288
289         for (my $i=0; $i<=$DBM::Deep::MAX_BUCKETS; $i++) {
290             my $key = substr($keys, $i * $DBM::Deep::BUCKET_SIZE, $DBM::Deep::HASH_SIZE);
291             if ($key) {
292                 my $old_subloc = unpack($DBM::Deep::LONG_PACK, substr($keys, ($i * $DBM::Deep::BUCKET_SIZE) +
293                         $DBM::Deep::HASH_SIZE, $DBM::Deep::LONG_SIZE));
294                 my $num = ord(substr($key, $tag->{ch} + 1, 1));
295
296                 if ($offsets[$num]) {
297                     my $offset = $offsets[$num] + DBM::Deep->SIG_SIZE + $DBM::Deep::DATA_LENGTH_SIZE;
298                     seek($fh, $offset + $root->{file_offset}, SEEK_SET);
299                     my $subkeys;
300                     read( $fh, $subkeys, $DBM::Deep::BUCKET_LIST_SIZE);
301
302                     for (my $k=0; $k<$DBM::Deep::MAX_BUCKETS; $k++) {
303                         my $subloc = unpack($DBM::Deep::LONG_PACK, substr($subkeys, ($k * $DBM::Deep::BUCKET_SIZE) +
304                                 $DBM::Deep::HASH_SIZE, $DBM::Deep::LONG_SIZE));
305                         if (!$subloc) {
306                             seek($fh, $offset + ($k * $DBM::Deep::BUCKET_SIZE) + $root->{file_offset}, SEEK_SET);
307                             print( $fh $key . pack($DBM::Deep::LONG_PACK, $old_subloc || $root->{end}) );
308                             last;
309                         }
310                     } # k loop
311                 }
312                 else {
313                     $offsets[$num] = $root->{end};
314                     seek($fh, $index_tag->{offset} + ($num * $DBM::Deep::LONG_SIZE) + $root->{file_offset}, SEEK_SET);
315                     print( $fh pack($DBM::Deep::LONG_PACK, $root->{end}) );
316
317                     my $blist_tag = $self->create_tag($obj, $root->{end}, DBM::Deep->SIG_BLIST, chr(0) x $DBM::Deep::BUCKET_LIST_SIZE);
318
319                     seek($fh, $blist_tag->{offset} + $root->{file_offset}, SEEK_SET);
320                     print( $fh $key . pack($DBM::Deep::LONG_PACK, $old_subloc || $root->{end}) );
321                 }
322             } # key is real
323         } # i loop
324
325         $location ||= $root->{end};
326     } # re-index bucket list
327
328     ##
329     # Seek to content area and store signature, value and plaintext key
330     ##
331     if ($location) {
332         my $content_length;
333         seek($fh, $location + $root->{file_offset}, SEEK_SET);
334
335         ##
336         # Write signature based on content type, set content length and write actual value.
337         ##
338         my $r = Scalar::Util::reftype($value) || '';
339         if ($r eq 'HASH') {
340             print( $fh DBM::Deep->TYPE_HASH );
341             print( $fh pack($DBM::Deep::DATA_LENGTH_PACK, $DBM::Deep::INDEX_SIZE) . chr(0) x $DBM::Deep::INDEX_SIZE );
342             $content_length = $DBM::Deep::INDEX_SIZE;
343         }
344         elsif ($r eq 'ARRAY') {
345             print( $fh DBM::Deep->TYPE_ARRAY );
346             print( $fh pack($DBM::Deep::DATA_LENGTH_PACK, $DBM::Deep::INDEX_SIZE) . chr(0) x $DBM::Deep::INDEX_SIZE );
347             $content_length = $DBM::Deep::INDEX_SIZE;
348         }
349         elsif (!defined($value)) {
350             print( $fh DBM::Deep->SIG_NULL );
351             print( $fh pack($DBM::Deep::DATA_LENGTH_PACK, 0) );
352             $content_length = 0;
353         }
354         else {
355             print( $fh DBM::Deep->SIG_DATA );
356             print( $fh pack($DBM::Deep::DATA_LENGTH_PACK, length($value)) . $value );
357             $content_length = length($value);
358         }
359
360         ##
361         # Plain key is stored AFTER value, as keys are typically fetched less often.
362         ##
363         print( $fh pack($DBM::Deep::DATA_LENGTH_PACK, length($plain_key)) . $plain_key );
364
365         ##
366         # If value is blessed, preserve class name
367         ##
368         if ( $root->{autobless} ) {
369             my $value_class = Scalar::Util::blessed($value);
370             if ( defined $value_class && $value_class ne 'DBM::Deep' ) {
371                 ##
372                 # Blessed ref -- will restore later
373                 ##
374                 print( $fh chr(1) );
375                 print( $fh pack($DBM::Deep::DATA_LENGTH_PACK, length($value_class)) . $value_class );
376                 $content_length += 1;
377                 $content_length += $DBM::Deep::DATA_LENGTH_SIZE + length($value_class);
378             }
379             else {
380                 print( $fh chr(0) );
381                 $content_length += 1;
382             }
383         }
384
385         ##
386         # If this is a new content area, advance EOF counter
387         ##
388         if ($location == $root->{end}) {
389             $root->{end} += DBM::Deep->SIG_SIZE;
390             $root->{end} += $DBM::Deep::DATA_LENGTH_SIZE + $content_length;
391             $root->{end} += $DBM::Deep::DATA_LENGTH_SIZE + length($plain_key);
392         }
393
394         ##
395         # If content is a hash or array, create new child DBM::Deep object and
396         # pass each key or element to it.
397         ##
398         if ($r eq 'HASH') {
399             my $branch = DBM::Deep->new(
400                 type => DBM::Deep->TYPE_HASH,
401                 base_offset => $location,
402                 root => $root,
403             );
404             foreach my $key (keys %{$value}) {
405                 $branch->STORE( $key, $value->{$key} );
406             }
407         }
408         elsif ($r eq 'ARRAY') {
409             my $branch = DBM::Deep->new(
410                 type => DBM::Deep->TYPE_ARRAY,
411                 base_offset => $location,
412                 root => $root,
413             );
414             my $index = 0;
415             foreach my $element (@{$value}) {
416                 $branch->STORE( $index, $element );
417                 $index++;
418             }
419         }
420
421         return $result;
422     }
423
424     return $obj->_throw_error("Fatal error: indexing failed -- possibly due to corruption in file");
425 }
426
427 sub get_bucket_value {
428         ##
429         # Fetch single value given tag and MD5 digested key.
430         ##
431         my $self = shift;
432         my ($obj, $tag, $md5) = @_;
433         my $keys = $tag->{content};
434
435     my $fh = $obj->_fh;
436
437         ##
438         # Iterate through buckets, looking for a key match
439         ##
440     BUCKET:
441         for (my $i=0; $i<$DBM::Deep::MAX_BUCKETS; $i++) {
442                 my $key = substr($keys, $i * $DBM::Deep::BUCKET_SIZE, $DBM::Deep::HASH_SIZE);
443                 my $subloc = unpack($DBM::Deep::LONG_PACK, substr($keys, ($i * $DBM::Deep::BUCKET_SIZE) + $DBM::Deep::HASH_SIZE, $DBM::Deep::LONG_SIZE));
444
445                 if (!$subloc) {
446                         ##
447                         # Hit end of list, no match
448                         ##
449                         return;
450                 }
451
452         if ( $md5 ne $key ) {
453             next BUCKET;
454         }
455
456         ##
457         # Found match -- seek to offset and read signature
458         ##
459         my $signature;
460         seek($fh, $subloc + $obj->_root->{file_offset}, SEEK_SET);
461         read( $fh, $signature, DBM::Deep->SIG_SIZE);
462         
463         ##
464         # If value is a hash or array, return new DBM::Deep object with correct offset
465         ##
466         if (($signature eq DBM::Deep->TYPE_HASH) || ($signature eq DBM::Deep->TYPE_ARRAY)) {
467             my $obj = DBM::Deep->new(
468                 type => $signature,
469                 base_offset => $subloc,
470                 root => $obj->_root,
471             );
472             
473             if ($obj->_root->{autobless}) {
474                 ##
475                 # Skip over value and plain key to see if object needs
476                 # to be re-blessed
477                 ##
478                 seek($fh, $DBM::Deep::DATA_LENGTH_SIZE + $DBM::Deep::INDEX_SIZE, SEEK_CUR);
479                 
480                 my $size;
481                 read( $fh, $size, $DBM::Deep::DATA_LENGTH_SIZE); $size = unpack($DBM::Deep::DATA_LENGTH_PACK, $size);
482                 if ($size) { seek($fh, $size, SEEK_CUR); }
483                 
484                 my $bless_bit;
485                 read( $fh, $bless_bit, 1);
486                 if (ord($bless_bit)) {
487                     ##
488                     # Yes, object needs to be re-blessed
489                     ##
490                     my $class_name;
491                     read( $fh, $size, $DBM::Deep::DATA_LENGTH_SIZE); $size = unpack($DBM::Deep::DATA_LENGTH_PACK, $size);
492                     if ($size) { read( $fh, $class_name, $size); }
493                     if ($class_name) { $obj = bless( $obj, $class_name ); }
494                 }
495             }
496             
497             return $obj;
498         }
499         
500         ##
501         # Otherwise return actual value
502         ##
503         elsif ($signature eq DBM::Deep->SIG_DATA) {
504             my $size;
505             my $value = '';
506             read( $fh, $size, $DBM::Deep::DATA_LENGTH_SIZE); $size = unpack($DBM::Deep::DATA_LENGTH_PACK, $size);
507             if ($size) { read( $fh, $value, $size); }
508             return $value;
509         }
510         
511         ##
512         # Key exists, but content is null
513         ##
514         else { return; }
515         } # i loop
516
517         return;
518 }
519
520 sub delete_bucket {
521         ##
522         # Delete single key/value pair given tag and MD5 digested key.
523         ##
524         my $self = shift;
525         my ($obj, $tag, $md5) = @_;
526         my $keys = $tag->{content};
527
528     my $fh = $obj->_fh;
529         
530         ##
531         # Iterate through buckets, looking for a key match
532         ##
533     BUCKET:
534         for (my $i=0; $i<$DBM::Deep::MAX_BUCKETS; $i++) {
535                 my $key = substr($keys, $i * $DBM::Deep::BUCKET_SIZE, $DBM::Deep::HASH_SIZE);
536                 my $subloc = unpack($DBM::Deep::LONG_PACK, substr($keys, ($i * $DBM::Deep::BUCKET_SIZE) + $DBM::Deep::HASH_SIZE, $DBM::Deep::LONG_SIZE));
537
538                 if (!$subloc) {
539                         ##
540                         # Hit end of list, no match
541                         ##
542                         return;
543                 }
544
545         if ( $md5 ne $key ) {
546             next BUCKET;
547         }
548
549         ##
550         # Matched key -- delete bucket and return
551         ##
552         seek($fh, $tag->{offset} + ($i * $DBM::Deep::BUCKET_SIZE) + $obj->_root->{file_offset}, SEEK_SET);
553         print( $fh substr($keys, ($i+1) * $DBM::Deep::BUCKET_SIZE ) );
554         print( $fh chr(0) x $DBM::Deep::BUCKET_SIZE );
555         
556         return 1;
557         } # i loop
558
559         return;
560 }
561
562 sub bucket_exists {
563         ##
564         # Check existence of single key given tag and MD5 digested key.
565         ##
566         my $self = shift;
567         my ($obj, $tag, $md5) = @_;
568         my $keys = $tag->{content};
569         
570         ##
571         # Iterate through buckets, looking for a key match
572         ##
573     BUCKET:
574         for (my $i=0; $i<$DBM::Deep::MAX_BUCKETS; $i++) {
575                 my $key = substr($keys, $i * $DBM::Deep::BUCKET_SIZE, $DBM::Deep::HASH_SIZE);
576                 my $subloc = unpack($DBM::Deep::LONG_PACK, substr($keys, ($i * $DBM::Deep::BUCKET_SIZE) + $DBM::Deep::HASH_SIZE, $DBM::Deep::LONG_SIZE));
577
578                 if (!$subloc) {
579                         ##
580                         # Hit end of list, no match
581                         ##
582                         return;
583                 }
584
585         if ( $md5 ne $key ) {
586             next BUCKET;
587         }
588
589         ##
590         # Matched key -- return true
591         ##
592         return 1;
593         } # i loop
594
595         return;
596 }
597
598 sub find_bucket_list {
599         ##
600         # Locate offset for bucket list, given digested key
601         ##
602         my $self = shift;
603         my ($obj, $md5) = @_;
604         
605         ##
606         # Locate offset for bucket list using digest index system
607         ##
608         my $ch = 0;
609         my $tag = $self->load_tag($obj, $obj->_base_offset);
610         if (!$tag) { return; }
611         
612         while ($tag->{signature} ne DBM::Deep->SIG_BLIST) {
613                 $tag = $self->index_lookup($obj, $tag, ord(substr($md5, $ch, 1)));
614                 if (!$tag) { return; }
615                 $ch++;
616         }
617         
618         return $tag;
619 }
620
621 sub traverse_index {
622         ##
623         # Scan index and recursively step into deeper levels, looking for next key.
624         ##
625     my $self = shift;
626     my ($obj, $offset, $ch, $force_return_next) = @_;
627     $force_return_next = undef unless $force_return_next;
628         
629         my $tag = $self->load_tag($obj, $offset );
630
631     my $fh = $obj->_fh;
632         
633         if ($tag->{signature} ne DBM::Deep->SIG_BLIST) {
634                 my $content = $tag->{content};
635                 my $start;
636                 if ($obj->{return_next}) { $start = 0; }
637                 else { $start = ord(substr($obj->{prev_md5}, $ch, 1)); }
638                 
639                 for (my $index = $start; $index < 256; $index++) {
640                         my $subloc = unpack($DBM::Deep::LONG_PACK, substr($content, $index * $DBM::Deep::LONG_SIZE, $DBM::Deep::LONG_SIZE) );
641                         if ($subloc) {
642                                 my $result = $self->traverse_index( $obj, $subloc, $ch + 1, $force_return_next );
643                                 if (defined($result)) { return $result; }
644                         }
645                 } # index loop
646                 
647                 $obj->{return_next} = 1;
648         } # tag is an index
649         
650         elsif ($tag->{signature} eq DBM::Deep->SIG_BLIST) {
651                 my $keys = $tag->{content};
652                 if ($force_return_next) { $obj->{return_next} = 1; }
653                 
654                 ##
655                 # Iterate through buckets, looking for a key match
656                 ##
657                 for (my $i=0; $i<$DBM::Deep::MAX_BUCKETS; $i++) {
658                         my $key = substr($keys, $i * $DBM::Deep::BUCKET_SIZE, $DBM::Deep::HASH_SIZE);
659                         my $subloc = unpack($DBM::Deep::LONG_PACK, substr($keys, ($i * $DBM::Deep::BUCKET_SIZE) + $DBM::Deep::HASH_SIZE, $DBM::Deep::LONG_SIZE));
660         
661                         if (!$subloc) {
662                                 ##
663                                 # End of bucket list -- return to outer loop
664                                 ##
665                                 $obj->{return_next} = 1;
666                                 last;
667                         }
668                         elsif ($key eq $obj->{prev_md5}) {
669                                 ##
670                                 # Located previous key -- return next one found
671                                 ##
672                                 $obj->{return_next} = 1;
673                                 next;
674                         }
675                         elsif ($obj->{return_next}) {
676                                 ##
677                                 # Seek to bucket location and skip over signature
678                                 ##
679                                 seek($fh, $subloc + DBM::Deep->SIG_SIZE + $obj->_root->{file_offset}, SEEK_SET);
680                                 
681                                 ##
682                                 # Skip over value to get to plain key
683                                 ##
684                                 my $size;
685                                 read( $fh, $size, $DBM::Deep::DATA_LENGTH_SIZE); $size = unpack($DBM::Deep::DATA_LENGTH_PACK, $size);
686                                 if ($size) { seek($fh, $size, SEEK_CUR); }
687                                 
688                                 ##
689                                 # Read in plain key and return as scalar
690                                 ##
691                                 my $plain_key;
692                                 read( $fh, $size, $DBM::Deep::DATA_LENGTH_SIZE); $size = unpack($DBM::Deep::DATA_LENGTH_PACK, $size);
693                                 if ($size) { read( $fh, $plain_key, $size); }
694                                 
695                                 return $plain_key;
696                         }
697                 } # bucket loop
698                 
699                 $obj->{return_next} = 1;
700         } # tag is a bucket list
701         
702         return;
703 }
704
705 sub get_next_key {
706         ##
707         # Locate next key, given digested previous one
708         ##
709     my $self = shift;
710     my ($obj) = @_;
711         
712         $obj->{prev_md5} = $_[1] ? $_[1] : undef;
713         $obj->{return_next} = 0;
714         
715         ##
716         # If the previous key was not specifed, start at the top and
717         # return the first one found.
718         ##
719         if (!$obj->{prev_md5}) {
720                 $obj->{prev_md5} = chr(0) x $DBM::Deep::HASH_SIZE;
721                 $obj->{return_next} = 1;
722         }
723         
724         return $self->traverse_index( $obj, $obj->_base_offset, 0 );
725 }
726
727 1;
728 __END__