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