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