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