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