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