File Coverage

blib/lib/XBase/Index.pm
Criterion Covered Total %
statement 559 914 61.1
branch 175 380 46.0
condition 42 80 52.5
subroutine 51 63 80.9
pod 1 13 7.6
total 828 1450 57.1


line stmt bran cond sub pod time code
1              
2             =head1 NAME
3              
4             XBase::Index - base class for the index files for dbf
5              
6             =cut
7              
8             package XBase::Index;
9 6     6   2190 use strict;
  6         11  
  6         351  
10 6     6   32 use vars qw( @ISA $DEBUG $VERSION $VERBOSE $BIGEND );
  6         10  
  6         718  
11 6     6   32 use XBase::Base;
  6         12  
  6         14646  
12             @ISA = qw( XBase::Base );
13              
14             $VERSION = '1.05';
15              
16             $DEBUG = 0;
17              
18             $VERBOSE = 0 unless defined $VERBOSE;
19              
20             # We will setup global variable to denote the byte order (endian)
21             my $packed = pack('d', 1);
22             if ($packed eq "\077\360\000\000\000\000\000\000") {
23             $BIGEND = 1;
24             } elsif ($packed eq "\000\000\000\000\000\000\360\077") {
25             $BIGEND = 0;
26             } else {
27             die "XBase::Index: your architecture is not supported.\n";
28             }
29              
30             # Open appropriate index file and create object according to suffix
31             sub new {
32 16     16 1 40 my ($class, $file) = (shift, shift);
33 16         42 my @opts = @_;
34 16 50       51 print "XBase::Index::new($class, $file, @_)\n" if $XBase::Index::VERBOSE;
35 16 100       51 if (ref $class) { @opts = ('dbf', $class, @opts); }
  6         16  
36 16         100 my ($ext) = ($file =~ /\.(...)$/);
37 16         36 $ext = lc $ext;
38              
39 16 100 66     161 if ($ext eq 'sdbm' or $ext eq 'pag' or $ext eq 'dir') {
      66        
40 1         5 require XBase::SDBM;
41 1         2 $ext = 'SDBM';
42             }
43              
44 16         1598 my $object = eval "new XBase::$ext \$file, \@opts";
45 16 50       167 return $object if defined $object;
46              
47 0 0       0 __PACKAGE__->Error("Error loading index: unknown extension\n") if $@;
48 0         0 return;
49             }
50              
51             # For XBase::*x object, a record is one page, object XBase::*x::Page here
52             sub get_record {
53 227     227 0 302 my ($self, $num) = @_;
54 227 100       1645 return $self->{'pages_cache'}{$num}
55             if defined $self->{'pages_cache'}{$num};
56              
57 45         97 my $newpage = (ref $self) . '::Page::new';
58 45         190 my $page = $self->$newpage($num);
59              
60 45 50       123 if (defined $page) {
61 45         214 $self->{'pages_cache'}{$num} = $page;
62              
63 45         150 local $^W = 0;
64 45 50       112 print "Page $page->{'num'}:\tkeys: @{[ map { s/\s+$//; $_; } @{$page->{'keys'}}]}\n\tvalues: @{$page->{'values'}}\n" if $DEBUG;
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
65 45 50 66     271 print "\tlefts: @{$page->{'lefts'}}\n" if defined $page->{'lefts'} and $DEBUG;
  0         0  
66             }
67 45         99 $page;
68             }
69              
70             # Get next (value, record number in dbf) pair
71             # The important values of the index object are 'level' holding the
72             # current level of the "cursor", 'pages' holding an array of pages
73             # currently open for each level and 'rows' with an array of current row
74             # in each level
75             sub fetch {
76 1366     1366 0 1784 my $self = shift;
77 1366         1445 my ($level, $page, $row, $key, $val, $left);
78            
79             # cycle while we get to the leaf record or otherwise get
80             # a real value, not a pointer to lower page
81 1366         2509 while (not defined $val)
82             {
83 1534         2218 $level = $self->{'level'};
84              
85             # if we do not have level, let's start from zero
86 1534 100       2850 if (not defined $level) {
87 14         31 $level = $self->{'level'} = 0;
88 14         67 $page = $self->get_record($self->{'start_page'});
89 14 50       116 if (not defined $page) {
90 0         0 $self->Error("Index corrupt: $self: no root page $self->{'start_page'}\n");
91 0         0 return;
92             }
93             # and initialize 'pages' and 'rows'
94 14         38 $self->{'pages'} = [ $page ];
95 14         50 $self->{'rows'} = [];
96             }
97              
98             # get current page for this level
99 1534         3014 $page = $self->{'pages'}[$level];
100 1534 50       2586 if (not defined $page) {
101 0         0 $self->Error("Index corrupt: $self: page for level $level lost in normal course\n");
102 0         0 return;
103             }
104              
105             # get current row for current level and increase it
106             # (or setup to zero)
107 1534         2035 my $row = $self->{'rows'}[$level];
108 1534 100       2510 if (not defined $row) {
109 142         235 $row = $self->{'rows'}[$level] = 0;
110             } else {
111 1392         2153 $self->{'rows'}[$level] = ++$row;
112             }
113              
114             # get the (key, value, pointer) from the page
115 1534         3041 ($key, $val, $left) = $page->get_key_val_left($row);
116              
117             # there is another page to walk
118 1534 100       3721 if (defined $left) {
119             # go deeper
120 118         134 $level++;
121 118         137 my $oldpage = $page;
122             # load the next page
123 118         229 $page = $self->get_record($left);
124 118 50       242 if (not defined $page) {
125 0         0 $self->Error("Index corrupt: $self: no page $left, ref'd from $oldpage, row $row, level $level\n");
126 0         0 return;
127             }
128             # and put it into the structure
129 118         194 $self->{'pages'}[$level] = $page;
130 118         166 $self->{'rows'}[$level] = undef;
131 118         152 $self->{'level'} = $level;
132             # and even if some index structures allow the
133             # value in the same row as record, we want to
134             # skip it when going down
135 118         121 $val = undef;
136 118         271 next;
137             }
138             # if we're lucky and got the value, return it
139 1416 100       2198 if (defined $val) {
140 1212         10457 return ($key, $val);
141             }
142             # we neither got link to lower page, nor the value
143             # so it means we are backtracking the structure one
144             # (or more) levels back
145             else {
146 204         295 $self->{'level'} = --$level; # go up the levels
147 204 100       596 return if $level < 0; # do not fall over
148 153         238 $page = $self->{'pages'}[$level];
149 153 50       253 if (not defined $page)
150             {
151 0         0 $self->Error("Index corrupt: $self: page for level $level lost when backtracking\n");
152 0         0 return;
153             }
154             ### next unless defined $page;
155 153         206 $row = $self->{'rows'}[$level];
156 153         293 my ($backkey, $backval, $backleft) = $page->get_key_val_left($row);
157             # this is a hook for ntx files where we do not
158             # want to miss a values that are stored inside
159             # the structure, not only in leaves.
160 153 100 100     1016 if (not defined $page->{'last_key_is_just_overflow'} and defined $backleft and defined $backval) {
      100        
161 103         417 return ($backkey, $backval);
162             }
163             }
164             }
165 0         0 return;
166             }
167              
168             # Get list of tags in the indexfile (an indexfile may not have any)
169             sub tags {
170 1     1 0 1 my $self = shift;
171 1 50       6 @{$self->{'tags'}} if defined $self->{'tags'};
  1         8  
172             }
173              
174             # Method allowing to refetch the active values (key, val) without
175             # rolling forward
176             sub fetch_current {
177 20     20 0 23 my $self = shift;
178 20         22 my $level = $self->{'level'};
179 20         25 my $page = $self->{'pages'}[$level];
180 20         23 my $row = $self->{'rows'}[$level];
181 20         33 my ($key, $val, $left) = $page->get_key_val_left($row);
182 20         45 return ($key, $val);
183             }
184              
185             # Rewind the index to start
186             # the easiest way to do this is to cancel the 'level' -- this way we
187             # do not know where we are and we have to start anew
188             sub prepare_select {
189 77     77 0 523 my $self = shift;
190 77         562 delete $self->{'level'};
191 77         204 delete $self->{'pages'};
192 77         133 delete $self->{'rows'};
193 77         156 1;
194             }
195              
196             # Position index to a value (or behind it, if nothing found), so that
197             # next fetch fetches the correct value
198             sub prepare_select_eq {
199 59     59 0 114 my ($self, $eq, $recno) = @_;
200 59         147 $self->prepare_select(); # start from scratch
201              
202             ### { local $^W = 0; print STDERR "Will look for $eq $recno\n"; }
203              
204 59         122 my $left = $self->{'start_page'};
205 59         77 my $level = 0;
206 59         80 my $parent = undef;
207            
208             # we'll need to know if we want numeric or string compares
209 59 100       164 my $numdate = ($self->{'key_type'} ? 1 : 0);
210              
211 59         70 while (1) {
212 95         213 my $page = $self->get_record($left); # get page
213 95 50       229 if (not defined $page) {
214 0         0 $self->Error("Index corrupt: $self: no page $left for level $level\n");
215 0         0 return;
216             }
217 95         223 my $row = 0;
218 95         104 my ($key, $val);
219 95         190 my $empty = 1;
220 95         255 while (($key, $val, my $newleft) = $page->get_key_val_left($row)) {
221             ### { local $^W = 0; print "Got: $key, $val, $newleft ($numdate)\n"; }
222              
223 584         845 $empty = 0; # There is at least 1 key
224 584         669 $left = $newleft;
225             # Joe Campbell says:
226             # Compound char keys have two parts preceded by white space
227             # get rid of the white space so that I can do a matching....
228             # and suggests
229             # $key =~ s/^\s*//g;
230              
231              
232             # finish if we are at the end of the page or
233             # behind the correct value
234 584 50       1162 if (not defined $key)
235 0         0 { last; }
236 584 100       1726 if ($numdate == 1 ? $key >= $eq : $key ge $eq)
    100          
237 85         112 { last; }
238 499         2825 $row++;
239             }
240            
241             # we know where we are positioned on the page now
242 95         305 $self->{'pages'}[$level] = $page;
243 95         190 $self->{'rows'}[$level] = $row;
244              
245             # if there is no lower level
246 95 100 66     424 if ($empty or not defined $left) {
247 59 100       190 $self->{'rows'}[$level] = ( $row ? $row - 1: undef);
248 59         96 $self->{'level'} = $level;
249 59         102 last;
250             }
251 36 100       97 $page->{'parent'} = $parent->{'num'} if defined $parent;
252 36         44 $parent = $page;
253 36         49 $level++;
254             }
255 59 100       131 if (defined $recno) { # exact match requested
256             # get current values
257 10         31 my ($key, $val) = $self->fetch_current;
258 10         20 while (defined $val) {
259 20 50       47 last if ($numdate ? $key > $eq : $key gt $eq);
    100          
260              
261             # if we're here, we still have exact match
262 10 50       19 last if $val == $recno;
263              
264             # move forward
265 10         45 ($key, $val) = $self->fetch;
266             }
267             }
268 59         170 1;
269             }
270              
271             # Get (key, dbf record number, lower page index) from the index page
272             sub get_key_val_left {
273 2301     2301 0 2903 my ($self, $num) = @_;
274             {
275 2301         2261 local $^W = 0;
  2301         5071  
276 2301         3824 my $printkey = $self->{'keys'}[$num];
277 2301         6231 $printkey =~ s/\s+$//;
278 2301         4094 $printkey =~ s/\000/\\0/g;
279 2301 50       4593 print "Getkeyval: Page $self->{'num'}, row $num: $printkey, $self->{'values'}[$num], $self->{'lefts'}[$num]\n"
280             if $DEBUG > 5;
281 2301         13188 return ($self->{'keys'}[$num], $self->{'values'}[$num], $self->{'lefts'}[$num])
282 2301 100       2351 if $num <= $#{$self->{'keys'}};
283             }
284 222         470 return;
285             }
286              
287             sub num_keys {
288 0     0 0 0 $#{shift->{'keys'}};
  0         0  
289             }
290              
291             sub delete {
292 5     5 0 13 my ($self, $key, $value) = @_;
293 5 50       13 print "XBase::Index::delete($key, $value) called ($self->{'tag'} -> $self->{'key_string'}/$self->{'for_string'})\n" if $XBase::Index::VERBOSE;
294 5 50       13 $self->prepare_select_eq($key, $value) or return;
295 5         12 my ($foundkey, $foundvalue) = $self->fetch_current;
296              
297 5 50 33     28 if (defined $foundvalue
      33        
298             and $foundkey eq $key and $foundvalue == $value) {
299 0         0 $self->delete_current;
300 0         0 return 1;
301             }
302 5 50       22 print "$key/$value is not in the index (wanted to delete)\n" if $XBase::Index::VERBOSE;
303 5         15 undef;
304             }
305             sub insert {
306 5     5 0 10 my ($self, $key, $value) = @_;
307 5 50       14 print "XBase::Index::insert($key, $value) called\n" if $XBase::Index::VERBOSE;
308              
309 5 50       10 $self->prepare_select_eq($key, $value) or return;
310 5         9 my ($foundkey, $foundvalue) = $self->fetch_current;
311              
312 5 50 33     28 if (defined $foundvalue
      33        
313             and $foundkey eq $key and $foundvalue == $value) {
314 0         0 print STDERR "Already found, strange.\n";
315 0         0 return;
316             }
317              
318 5         25 $self->insert_before_current($key, $value);
319             }
320              
321             sub delete_current {
322 0     0 0 0 my $self = shift;
323 0 0       0 print "Delete_current called\n" if $XBase::Index::VERBOSE;
324 0         0 my $level = $self->{'level'};
325 0         0 my $page = $self->{'pages'}[$level];
326 0         0 my $row = $self->{'rows'}[$level];
327              
328 0         0 splice @{$page->{'values'}}, $row, 1;
  0         0  
329 0         0 splice @{$page->{'keys'}}, $row, 1;
  0         0  
330 0         0 splice @{$page->{'lefts'}}, $row, 1;
  0         0  
331              
332 0         0 $self->{'rows'}[$level]--;
333 0 0       0 if ($self->{'rows'}[$level] < 0) {
334 0         0 $self->{'rows'}[$level] = undef;
335             }
336              
337 0         0 $page->write_with_context;
338              
339 0         0 delete $self->{'pages_cache'};
340              
341 0 0       0 print STDERR "Delete_current returning\n" if $DEBUG;
342             }
343              
344             sub insert_before_current {
345 5     5 0 6 my ($self, $key, $value) = @_;
346 5 50       16 print "Insert_current called ($key $value)\n" if $XBase::Index::VERBOSE;
347 5         8 my $level = $self->{'level'};
348 5         7 my $page = $self->{'pages'}[$level];
349 5         8 my $row = $self->{'rows'}[$level];
350 5 50       12 $row = 0 unless defined $row;
351              
352             # update keys and values and then call save
353 5         6 splice @{$page->{'keys'}}, $row, 0, $key;
  5         13  
354 5         11 splice @{$page->{'values'}}, $row, 0, $value;
  5         9  
355 5 50       11 splice @{$page->{'lefts'}}, $row, 0, undef if defined $page->{'lefts'};
  5         11  
356              
357 5         11 $page->write_with_context;
358              
359 5         45 delete $self->{'pages_cache'};
360              
361 5 50       32 print STDERR "Insert_current returning\n" if $DEBUG;
362             }
363              
364             # #############
365             # dBase III NDX
366              
367             package XBase::ndx;
368 6     6   37 use strict;
  6         12  
  6         194  
369 6     6   27 use vars qw( @ISA $DEBUG );
  6         8  
  6         1626  
370             @ISA = qw( XBase::Base XBase::Index );
371              
372             *DEBUG = \$XBase::Index::DEBUG;
373              
374             sub read_header {
375 3     3   6 my $self = shift;
376 3         7 my %opts = @_;
377 3         7 my $header;
378 3         8 $self->{'dbf'} = $opts{'dbf'};
379             $self->{'fh'}->read($header, 512) == 512 or do
380 3 50       12 { __PACKAGE__->Error("Error reading header of $self->{'filename'}: $!\n"); return; };
  0         0  
  0         0  
381 3         111 @{$self}{ qw( start_page total_pages key_length keys_per_page
  3         32  
382             key_type key_record_length unique key_string ) }
383             = unpack 'VV @12vvvv @23c a*', $header;
384            
385 3         21 $self->{'key_string'} =~ s/[\000 ].*$//s;
386 3         7 $self->{'record_len'} = 512;
387 3         4 $self->{'header_len'} = 0;
388              
389 3         22 $self;
390             }
391              
392             sub last_record {
393 30     30   142 shift->{'total_pages'};
394             }
395              
396             package XBase::ndx::Page;
397 6     6   38 use strict;
  6         21  
  6         192  
398 6     6   29 use vars qw( @ISA $DEBUG );
  6         10  
  6         2942  
399             @ISA = qw( XBase::ndx );
400              
401             *DEBUG = \$XBase::Index::DEBUG;
402              
403             # Constructor for the ndx page
404             sub new {
405 15     15   27 my ($indexfile, $num) = @_;
406 15         18 my $parent;
407             # we can be called from parent page
408 15 50       42 if ((ref $indexfile) =~ /::Page$/) {
409 0         0 $parent = $indexfile;
410 0         0 $indexfile = $parent->{'indexfile'};
411             }
412            
413 15 50       50 my $data = $indexfile->read_record($num) or return; # get 512 bytes
414 15         34 my $noentries = unpack 'V', $data; # num of entries
415            
416 15         23 my $keylength = $indexfile->{'key_length'};
417 15         21 my $keyreclength = $indexfile->{'key_record_length'}; # length
418              
419 15 50       32 print "page $num, noentries $noentries, keylength $keylength\n" if $DEBUG;
420 15         22 my $numdate = $indexfile->{'key_type'}; # numeric or string?
421            
422 15         17 my $offset = 4;
423 15         27 my $i = 0;
424 15         34 my ($keys, $values, $lefts) = ([], [], []); # three arrays
425              
426             # walk the page
427 15         39 while ($i < $noentries) {
428             # get the values for entry
429 147         575 my ($left, $recno, $key)
430             = unpack 'VVa*', substr($data, $offset, $keylength + 8);
431 147 100       367 if ($numdate) { # some decoding for numbers
432 110 50       217 $key = reverse $key if $XBase::Index::BIGEND;
433 110         181 $key = unpack 'd', $key;
434             }
435 147 50       387 print "$i: \@$offset VVa$keylength -> ($left, $recno, $key)\n" if $DEBUG > 1;
436 147         310 push @$keys, $key;
437 147 100       296 push @$values, ($recno ? $recno : undef);
438 147 100       628 $left = ($left ? $left : undef);
439 147         197 push @$lefts, $left;
440            
441 147 100 100     414 if ($i == 0 and defined $left)
442 5         10 { $noentries++; } # fixup for nonleaf page
443             ### shouldn't this be for last page only?
444             }
445             continue {
446 147         157 $i++;
447 147         394 $offset += $keyreclength;
448             }
449              
450 15         102 my $self = bless { 'keys' => $keys, 'values' => $values,
451             'num' => $num, 'keylength' => $keylength,
452             'lefts' => $lefts, 'indexfile' => $indexfile }, __PACKAGE__;
453            
454 15 100 33     89 if ($num == $indexfile->{'start_page'}
  0   66     0  
455             or (defined
456             $parent->{'last_key_is_just_overflow'} and
457             $parent->{'lefts'}[$#{$parent->{'lefts'}}] == $num)) {
458 3         18 $self->{'last_key_is_just_overflow'} = 1;
459             }
460              
461 15         40 $self;
462             }
463              
464             # ###########
465             # Clipper NTX
466              
467             package XBase::ntx;
468 6     6   38 use strict;
  6         15  
  6         219  
469 6     6   30 use vars qw( @ISA $DEBUG );
  6         10  
  6         2949  
470             @ISA = qw( XBase::Base XBase::Index );
471              
472             sub read_header {
473 1     1   2 my $self = shift;
474 1         2 my %opts = @_;
475 1         2 my $header;
476 1         2 $self->{'dbf'} = $opts{'dbf'};
477             $self->{'fh'}->read($header, 1024) == 1024 or do
478 1 50       5 { __PACKAGE__->Error("Error reading header of $self->{'filename'}: $!\n"); return; };
  0         0  
  0         0  
479            
480 1         41 @{$self}{ qw( signature compiler_version start_offset first_unused
  1         18  
481             key_record_length key_length decimals max_item
482             half_page key_string unique ) }
483             = unpack 'vvVVvvvvvA256c', $header;
484              
485 1         4 my $key_string = uc $self->{'key_string'};
486 1         5 $key_string =~ s/^.*?->//;
487 1         2 $self->{'key_string'} = $key_string;
488              
489 1 50 33     8 if ($self->{'signature'} != 3 and $self->{'signature'} != 6) {
490 0         0 __PACKAGE__->Error("$self: bad signature value `$self->{'signature'}' found\n");
491 0         0 return;
492             }
493 1         4 $self->{'key_string'} =~ s/[\000 ].*$//s;
494 1         3 $self->{'record_len'} = 1024;
495 1         3 $self->{'header_len'} = 0;
496            
497 1         5 $self->{'start_page'} = int($self->{'start_offset'} / $self->{'record_len'});
498 1         2 my $field_type;
499 1 50       5 if (defined $opts{'type'}) {
    50          
500 0         0 $field_type = $opts{'type'};
501             } elsif (defined $self->{'dbf'}) {
502 1         6 $field_type = $self->{'dbf'}->field_type($key_string);
503 1 50       4 if (not defined $field_type) {
504 0         0 __PACKAGE__->Error("Couldn't find key string `$key_string' in dbf file, can't determine field type\n");
505 0         0 return;
506             }
507             } else {
508 0         0 __PACKAGE__->Error("Index type (char/numeric) unknown for $self\n");
509 0         0 return;
510             }
511 1 50       3 $self->{'key_type'} = ($field_type =~ /^[NDIF]$/ ? 1 : 0);
512              
513 1         7 $self;
514             }
515             sub last_record {
516 9     9   30 -1;
517             }
518              
519              
520             package XBase::ntx::Page;
521 6     6   40 use strict;
  6         12  
  6         257  
522 6     6   33 use vars qw( @ISA $DEBUG );
  6         11  
  6         2828  
523             @ISA = qw( XBase::ntx );
524              
525             *DEBUG = \$XBase::Index::DEBUG;
526              
527             # Constructor for the ntx page
528             sub new {
529 9     9   12 my ($indexfile, $num) = @_;
530 9         8 my $parent;
531             # we could be called from parent page
532 9 50       21 if ((ref $indexfile) =~ /::Page$/) {
533 0         0 $parent = $indexfile;
534 0         0 $indexfile = $parent->{'indexfile'};
535             }
536 9 50       31 my $data = $indexfile->read_record($num) or return; # get data
537 9         19 my $maxnumitem = $indexfile->{'max_item'} + 1; # limit from header
538 9         11 my $keylength = $indexfile->{'key_length'};
539 9         11 my $record_len = $indexfile->{'record_len'}; # length
540              
541 9         13 my $numdate = $indexfile->{'key_type'}; # numeric or string?
542              
543 9         53 my ($noentries, @pointers) = unpack "vv$maxnumitem", $data;
544             # get pointers where the entries are
545            
546 9 50       20 print "page $num, noentries $noentries, keylength $keylength; pointers @pointers\n" if $DEBUG;
547            
548 9         19 my ($keys, $values, $lefts) = ([], [], []);
549             # walk the pointers
550 9         23 for (my $i = 0; $i < $noentries; $i++) {
551 69         74 my $offset = $pointers[$i];
552 69         209 my ($left, $recno, $key)
553             = unpack 'VVa*', substr($data, $offset, $keylength + 8);
554              
555 69 50       125 if ($numdate) {
556             ### if looks like with ntx the numbers are
557             ### stored as ASCII strings or something
558             ### To Be Done
559 0 0       0 if ($key =~ tr!,+*)('&%$#"!0123456789!) { $key = '-' . $key; }
  0         0  
560 0         0 $key += 0;
561             }
562              
563 69 50       121 print "$i: \@$offset VVa$keylength -> ($left, $recno, $key)\n" if $DEBUG > 1;
564 69         100 push @$keys, $key;
565 69 100       128 push @$values, ($recno ? $recno : undef);
566 69 100       115 $left = ($left ? ($left / $record_len) : undef);
567 69         82 push @$lefts, $left;
568              
569             ### if ($i == 0 and defined $left and (not defined $parent or $num == $parent->{'lefts'}[-1]))
570 69 100 100     248 if ($i == 0 and defined $left)
571 1         80 { $noentries++; }
572             ### shouldn't this be for last page only?
573             }
574              
575 9         61 my $self = bless { 'num' => $num, 'indexfile' => $indexfile,
576             'keys' => $keys, 'values' => $values, 'lefts' => $lefts, },
577             __PACKAGE__;
578 9         25 $self;
579             }
580              
581             # ###########
582             # FoxBase IDX
583              
584             package XBase::idx;
585 6     6   40 use strict;
  6         18  
  6         225  
586 6     6   32 use vars qw( @ISA $DEBUG );
  6         15  
  6         7068  
587             @ISA = qw( XBase::Base XBase::Index );
588              
589             *DEBUG = \$XBase::Index::DEBUG;
590              
591             sub read_header {
592 2     2   4 my $self = shift;
593 2         6 my %opts = @_;
594 2         3 my $header;
595 2         7 $self->{'dbf'} = $opts{'dbf'};
596             $self->{'fh'}->read($header, 512) == 512 or do
597 2 50       15 { __PACKAGE__->Error("Error reading header of $self->{'filename'}: $!\n"); return; };
  0         0  
  0         0  
598 2         65 @{$self}{ qw( start_page start_free_list total_pages
  2         16  
599             key_length index_options index_signature
600             key_string for_expression
601             ) }
602             = unpack 'VVVv CC a220 a276', $header;
603            
604 2         7 $self->{'key_record_length'} = $self->{'key_length'} + 4;
605 2         11 $self->{'key_string'} =~ s/[\000 ].*$//s;
606 2         4 $self->{'record_len'} = 512;
607 2         5 $self->{'start_page'} /= $self->{'record_len'};
608 2         4 $self->{'start_free_list'} /= $self->{'record_len'};
609 2         6 $self->{'header_len'} = 0;
610              
611 2 100       8 if ($opts{'type'} eq 'N') {
612 1         2 $self->{'key_type'} = 1;
613             }
614              
615 2         14 $self;
616             }
617              
618             sub last_record {
619 8     8   38 shift->{'total_pages'};
620             }
621              
622             sub create {
623 2     2   227 my ($class, $table, $filename, $column) = @_;
624 2         9 my $type = $table->field_type($column);
625 2 50       9 if (not defined $type) {
626 0         0 die "XBase::idx: could determine index type for `$column'\n";
627             }
628 2         2 my $numdate = 0;
629 2 100 66     17 $numdate = 1 if $type eq 'N' or $type eq 'D';
630              
631 2         6 my $self = bless {}, $class;
632 2 50       13 $self->create_file($filename) or die "Error creating `$filename'\n";
633 2         15 $self->write_to(0, "\000" x 512);
634 2         10 my $key_length = $table->field_length($column);
635 2 100       6 $key_length = 8 if $numdate;
636              
637 2         7 my $count = int((512 - 12) / ($key_length + 4));
638             ### warn "Key length $key_length, per page $count.\n";
639              
640 2         3 my $encode_function;
641 2 100       6 if ($numdate) {
642             $encode_function = sub {
643 8     8   17 my $key = pack 'd', shift;
644 8 50       19 $key = reverse $key unless $XBase::Index::BIGEND;
645 8 100       23 if ((substr($key, 0, 1) & "\200") eq "\200") {
646 2         3 $key ^= "\377\377\377\377\377\377\377\377";
647             } else {
648 6         10 $key ^= "\200";
649             }
650 8         37 return $key;
651 1         11 };
652             } else {
653             $encode_function = sub {
654 8     8   54 return sprintf "%-${key_length}s", shift;
655 1         7 };
656             }
657              
658 2         5 my @data;
659 2         6 my $last_record = $table->last_record;
660 2         11 for (my $i = 0; $i <= $last_record; $i++) {
661 16         49 my ($deleted, $data) = $table->get_record($i, $column);
662 16         35 push @data, [ $encode_function->($data), $i + 1 ];
663             }
664 2         12 @data = sort { $a->[0] cmp $b->[0] } @data;
  28         46  
665              
666 2         5 $self->{'header_len'} = 0; # it is 512 really, but we
667             # count from 1, not from 0
668 2         3 $self->{'record_len'} = 512;
669              
670 2         3 my $pageno = 1;
671 2         3 my $level = 1;
672 2         3 my @newdata;
673 2   100     14 while ($level == 1 or @data > 1) {
674 3 50       13 last if $pageno > 5;
675 3         4 my $attributes = 0;
676 3 100       7 $attributes = 2 if $level == 1;
677 3 100       9 if (scalar(@data) < $count) {
678             # we have less than one page, so it's root.
679 2         2 $attributes++;
680             }
681              
682 3         4 my $left_page = 0xFFFFFFFF;
683 3         3 my $current_count = 0;
684 3         4 my $out = '';
685 3         5 @newdata = ();
686 3         9 for (my $i = 0; $i < @data; $i++) {
687 18         27 my $key = $data[$i][0];
688             ### print STDERR "Page $pageno: $i: @{$data[$i]}\n";
689 18         66 $out .= pack "a$key_length N", $key, $data[$i][1];
690 18         25 $current_count++;
691              
692 18 100 100     93 if ($current_count == $count or $i == $#data) {
693             ### print STDERR "Dumping $pageno.\n";
694             # time to close this page and move on
695 4         5 my $right_page = 0xFFFFFFFF;
696 4 100       11 if ($i < $#data) {
697 1         2 $right_page = $pageno + 1;
698             }
699 4         30 $self->write_record($pageno,
700             pack 'a512',
701             pack('vvVV', $attributes, $current_count,
702             $left_page, $right_page)
703             . $out);
704 4         14 push @newdata, [$data[$i][0], $pageno * 512];
705 4         5 $left_page = $pageno;
706 4         4 $current_count = 0;
707 4         5 $pageno++;
708 4         13 $out = '';
709             }
710             }
711              
712 3         11 @data = @newdata;
713 3         16 $level++;
714             }
715              
716 2         11 my $header = pack 'VVVv CC a220 a276',
717             ($pageno - 1) * 512, 0xFFFFFFFF, $pageno * 512,
718             $key_length, 0, 0, $column, '';
719 2         8 $self->write_to(0, $header);
720 2         12 $self->close;
721              
722 2         13 return new XBase::Index($filename, 'type' => $type);
723             }
724              
725             package XBase::idx::Page;
726 6     6   55 use strict;
  6         16  
  6         213  
727 6     6   40 use vars qw( @ISA $DEBUG );
  6         11  
  6         3599  
728             @ISA = qw( XBase::idx );
729              
730             *DEBUG = \$XBase::Index::DEBUG;
731              
732             ### $DEBUG = 1;
733             # Constructor for the idx page
734             sub new {
735 4     4   13 local $^W = 0;
736 4         6 my ($indexfile, $num) = @_;
737 4         5 my $parent;
738             # we can be called from parent page
739 4 50       13 if ((ref $indexfile) =~ /::Page$/) {
740 0         0 $parent = $indexfile;
741 0         0 $indexfile = $parent->{'indexfile'};
742             }
743 4 50       16 my $data = $indexfile->read_record($num) or return; # get 512 bytes
744 4         12 my ($attributes, $noentries, $left_brother, $right_brother)
745             = unpack 'vvVV', $data; # parse header of the page
746 4         8 my $keylength = $indexfile->{'key_length'};
747 4         7 my $keyreclength = $indexfile->{'key_record_length'}; # length
748              
749 4 50       10 print "page $num, noentries $noentries, keylength $keylength\n" if $DEBUG;
750 4         5 my $numdate = $indexfile->{'key_type'}; # numeric or string?
751            
752 4         5 my $offset = 12;
753 4         4 my $i = 0;
754 4         9 my ($keys, $values, $lefts) = ([], [], []); # three arrays
755              
756             # walk the page
757 4         11 while ($i < $noentries) {
758             # get the values for entry
759 19         64 my ($key, $recno) = unpack "\@$offset a$keylength N", $data;
760 19         22 my $left;
761 19 100       42 unless ($attributes & 2) {
762 3         5 $left = $recno / 512;
763 3         4 $recno = undef;
764             }
765 19 50       36 print "$i: \@$offset a$keylength N -> ($left, $recno, $key)\n" if $DEBUG > 1;
766             ### use Data::Dumper; print Dumper $indexfile;
767             # some decoding for numbers
768 19 100       33 if ($numdate) {
769 8 100       21 if ((substr($key, 0, 1) & "\200") ne "\200") {
770 2         3 $key ^= "\377\377\377\377\377\377\377\377";
771             } else {
772 6         8 $key ^= "\200";
773             }
774 8 50       15 if (not $XBase::Index::BIGEND) { $key = reverse $key; }
  8         13  
775 8         13 $key = unpack 'd', $key;
776             }
777 19 50       39 print "$i: \@$offset a$keylength N -> ($left, $recno, $key)\n" if $DEBUG > 1;
778 19         31 push @$keys, $key;
779 19 100       37 push @$values, ($recno ? $recno : undef);
780 19 100       27 $left = ($left ? $left : undef);
781 19         25 push @$lefts, $left;
782            
783 19 100 100     61 if ($i == 0 and defined $left)
784 1         2 { $noentries++; } # fixup for nonleaf page
785             ### shouldn't this be for last page only?
786             }
787             continue {
788 19         19 $i++;
789 19         39 $offset += $keyreclength;
790             }
791              
792 4         39 my $self = bless { 'keys' => $keys, 'values' => $values,
793             'num' => $num, 'keylength' => $keylength,
794             'lefts' => $lefts, 'indexfile' => $indexfile,
795             'attributes' => $attributes,
796             'left_brother' => $left_brother,
797             'right_brother' => $right_brother }, __PACKAGE__;
798 4         12 $self;
799             }
800              
801             # ############
802             # dBase IV MDX
803              
804             package XBase::mdx;
805 6     6   34 use strict;
  6         12  
  6         205  
806 6     6   29 use vars qw( @ISA $DEBUG );
  6         11  
  6         4558  
807             @ISA = qw( XBase::Base XBase::Index );
808              
809             sub read_header {
810 0     0   0 my $self = shift;
811 0         0 my %opts = @_;
812 0         0 my $expr_name = $opts{'tag'};
813              
814 0         0 my $header;
815 0         0 $self->{'dbf'} = $opts{'dbf'};
816             $self->{'fh'}->read($header, 544) == 544 or do
817 0 0       0 { __PACKAGE__->Error("Error reading header of $self->{'filename'}: $!\n"); return; };
  0         0  
  0         0  
818              
819 0         0 @{$self}{ qw( version created dbf_filename block_size
  0         0  
820             block_size_adder production noentries tag_length res
821             tags_used res nopages first_free noavail last_update ) }
822             = unpack 'Ca3A16vvccccvvVVVa3', $header;
823            
824 0         0 $self->{'record_len'} = 512;
825 0         0 $self->{'header_len'} = 0;
826              
827 0         0 for my $i (1 .. $self->{'tags_used'}) {
828 0         0 my $len = $self->{'tag_length'};
829            
830             $self->seek_to(544 + ($i - 1) * $len) or do
831 0 0       0 { __PACKAGE__->Error($self->errstr); return; };
  0         0  
  0         0  
832              
833             $self->{'fh'}->read($header, $len) == $len or do
834 0 0       0 { __PACKAGE__->Error("Error reading tag header $i in $self->{'filename'}: $!\n"); return; };
  0         0  
  0         0  
835            
836 0         0 my $tag;
837 0         0 @{$tag}{ qw( header_page tag_name key_format fwd_low
  0         0  
838             fwd_high backward res key_type ) }
839             = unpack 'VA11ccccca1', $header;
840              
841 0         0 $self->{'tags'}{$tag->{'tag_name'}} = $tag;
842 0   0     0 $expr_name ||= $tag->{'tag_name'}; # Default to first tag
843              
844             $self->seek_to($tag->{'header_page'} * 512) or do
845 0 0       0 { __PACKAGE__->Error($self->errstr); return; };
  0         0  
  0         0  
846              
847             $self->{'fh'}->read($header, 24) == 24 or do
848 0 0       0 { __PACKAGE__->Error("Error reading tag definition in $self->{'filename'}: $!\n"); return; };
  0         0  
  0         0  
849            
850 0         0 @{$tag}{ qw( start_page file_size key_format_1
  0         0  
851             key_type_1 res key_length max_no_keys_per_page
852             second_key_type key_record_length res unique) }
853             = unpack 'VVca1vvvvva3c', $header;
854             }
855              
856             ### use Data::Dumper; print Dumper $self;
857              
858 0 0       0 if (defined $expr_name) {
859 0 0       0 if (defined $self->{'tags'}{$expr_name}) {
860 0         0 $self->{'active'} = $self->{'tags'}{$expr_name};
861 0         0 $self->{'start_page'} = $self->{'active'}{'start_page'};
862             } else {
863 0         0 __PACKAGE__->Error("No tag $expr_name found in index file $self->{'filename'}.\n"); return;
  0         0  
864             }
865             }
866              
867 0         0 $self;
868             }
869              
870             sub last_record {
871 0     0   0 -1;
872             }
873              
874             sub tags {
875 0     0   0 my $self = shift;
876 0 0       0 return sort keys %{$self->{'tags'}} if defined $self->{'tags'};
  0         0  
877             }
878              
879             package XBase::mdx::Page;
880 6     6   36 use strict;
  6         10  
  6         194  
881 6     6   38 use vars qw( @ISA $DEBUG );
  6         16  
  6         2930  
882             @ISA = qw( XBase::mdx );
883              
884             *DEBUG = \$XBase::Index::DEBUG;
885              
886             # Constructor for the mdx page
887             sub new {
888 0     0   0 my ($indexfile, $num) = @_;
889              
890 0         0 my $parent;
891             ### parent page
892 0 0       0 if ((ref $indexfile) =~ /::Page$/) {
893 0         0 $parent = $indexfile;
894 0         0 $indexfile = $parent->{'indexfile'};
895             }
896 0 0       0 $indexfile->seek_to_record($num) or return;
897 0         0 my $data;
898 0 0       0 $indexfile->{'fh'}->read($data, 1024) == 1024 or return;
899              
900 0         0 my $keylength = $indexfile->{'active'}{'key_length'};
901 0         0 my $keyreclength = $indexfile->{'active'}{'key_record_length'};
902 0         0 my $offset = 8;
903              
904 0         0 my ($noentries, $noleaf) = unpack 'VV', $data;
905              
906 0 0       0 print "page $num, noentries $noentries, keylength $keylength; noleaf: $noleaf\n" if $DEBUG;
907              
908 0         0 my ($keys, $values, $lefts, $refs) = ([], [], [], []);
909              
910 0         0 for (my $i = 0; $i < $noentries; $i++) {
911 0         0 my ($left, $key)
912             = unpack "\@${offset}Va${keylength}", $data;
913              
914 0         0 push @$keys, $key;
915              
916 0         0 push @$refs, $left;
917              
918 0         0 $offset += $keyreclength;
919             }
920              
921 0         0 my $right;
922              
923 0 0       0 $right = unpack "\@${offset}V", $data if $offset <= (1024-4);
924              
925 0 0       0 if ($right) {
926             # It's a branch page and the next ref is for values > last key
927 0         0 push @$keys, "";
928 0         0 push @$refs, $right;
929 0         0 $lefts = $refs;
930             } else {
931             # It's a leaf page
932 0         0 $values = $refs;
933             }
934              
935 0         0 my $self = bless { 'num' => $num, 'indexfile' => $indexfile,
936             'keys' => $keys, 'values' => $values, 'lefts' => $lefts, },
937             __PACKAGE__;
938 0         0 $self;
939             }
940              
941             # ###########
942             # FoxBase CDX
943              
944             package XBase::cdx;
945 6     6   32 use strict;
  6         10  
  6         184  
946 6     6   25 use vars qw( @ISA $DEBUG );
  6         14  
  6         5826  
947             @ISA = qw( XBase::Base XBase::Index );
948              
949             *DEBUG = \$XBase::Index::DEBUG;
950              
951             sub prepare_write_header {
952 0     0   0 my $self = shift;
953 0         0 my $data = pack 'VVNv CC @502 vvv @510 v @512 a512',
954             $self->{'start_page'} * 512,
955             $self->{'start_free_list'} * 512,
956 0         0 @{$self}{ qw( total_pages
957             key_length index_options index_signature
958             sort_order total_expr_length for_expression_length
959             key_expression_length
960             key_string
961             ) };
962 0         0 $data;
963             }
964             sub write_header {
965 0     0   0 my $self = shift;
966 0         0 my $data = $self->prepare_write_header;
967 0   0     0 $self->{'fh'}->seek($self->{'adjusted_offset'} || 0, 0);
968 0         0 $self->{'fh'}->print($data);
969             }
970             sub read_header {
971 18     18   44 my ($self, %opts) = @_;
972 18 100       61 $self->{'dbf'} = $opts{'dbf'} if not exists $self->{'dbf'};
973              
974 18         23 my $header;
975             $self->{'fh'}->read($header, 1024) == 1024 or do
976 18 50       64 { __PACKAGE__->Error("Error reading header of $self->{'filename'}: $!\n"); return; };
  0         0  
  0         0  
977              
978 18         485 @{$self}{ qw( start_page start_free_list total_pages
  18         131  
979             key_length index_options index_signature
980             sort_order total_expr_length for_expression_length
981             key_expression_length
982             key_string
983             ) }
984             = unpack 'VVNv CC @502 vvv @510 v @512 a512', $header;
985              
986 18         41 $self->{'total_pages'} = -1; ### the total_pages value 11
987             ### that found in rooms.cdx is not correct, so we invalidate it
988              
989 18         201 ($self->{'key_string'}, $self->{'for_string'}) =
990             ($self->{'key_string'} =~ /^([^\000]*)\000([^\000]*)/);
991              
992 18         49 $self->{'key_record_length'} = $self->{'key_length'} + 4;
993 18         29 $self->{'record_len'} = 512;
994 18         67 $self->{'start_page'} /= $self->{'record_len'};
995 18         29 $self->{'start_free_list'} /= $self->{'record_len'};
996 18         25 $self->{'header_len'} = 0;
997 18         27 $self->{'key_type'} = 0;
998              
999             ## my $out = $self->prepare_write_header;
1000             ## if ($out ne $header) {
1001             ## print STDERR "I won't be able to write the header back\n",
1002             ## unpack("H*", $out), "\n ++\n",
1003             ## unpack("H*", $header), "\n";
1004             ## }
1005              
1006 18 100       47 if (not defined $self->{'tag'}) { # top level
1007 9         37 $self->prepare_select;
1008 9         40 while (my ($tag) = $self->fetch) {
1009 45         49 push @{$self->{'tags'}}, $tag;
  45         94  
1010 45   66     171 $opts{'tag'} ||= $tag; # Default to first tag
1011             }
1012             }
1013             ### use Data::Dumper; print Dumper \%opts;
1014              
1015 18 100       44 if (defined $opts{'tag'}) {
1016 9         42 $self->prepare_select_eq($opts{'tag'});
1017 9         21 my ($foundkey, $value) = $self->fetch;
1018              
1019 9 50 33     60 if (not defined $foundkey or $opts{'tag'} ne $foundkey) {
1020 0         0 __PACKAGE__->Error("No tag $opts{'tag'} found in index file $self->{'filename'}.\n"); return; };
  0         0  
1021              
1022 9         213 my $subidx = bless { %$self }, ref $self;
1023 9 50       41 print "Adjusting start_page value by $value for $opts{'tag'}\n" if $DEBUG;
1024 9         43 $subidx->{'fh'}->seek($value, 0);
1025 9         121 $subidx->{'adjusted_offset'} = $value;
1026 9         30 $subidx->{'tag'} = $opts{'tag'};
1027 9         53 $subidx->read_header;
1028              
1029 9         18 my $key_string = $subidx->{'key_string'};
1030 9         12 my $field_type;
1031 9 50       34 if (defined $opts{'type'}) {
    50          
1032 0         0 $field_type = $opts{'type'};
1033             }
1034             elsif (defined $subidx->{'dbf'}) {
1035 9         44 $field_type = $subidx->{'dbf'}->field_type($key_string);
1036 9 50       29 if (not defined $field_type) {
1037 0         0 __PACKAGE__->Error("Couldn't find key string `$key_string' in dbf file, can't determine field type\n");
1038 0         0 return;
1039             }
1040             }
1041             else {
1042 0         0 __PACKAGE__->Error("Index type (char/numeric) unknown for $subidx\n");
1043 0         0 return;
1044             }
1045 9 50       34 $subidx->{'key_type'} = ($field_type =~ /^[NDIF]$/ ? 1 : 0);
1046 9 50       32 if ($field_type eq 'D') {
1047 0         0 $subidx->{'key_type'} = 2;
1048 0         0 require Time::JulianDay;
1049             }
1050              
1051 9         57 for (keys %$self) { delete $self->{$_} }
  234         317  
1052 9         63 for (keys %$subidx) { $self->{$_} = $subidx->{$_} }
  252         375  
1053 9         31 $self = $subidx;
1054             ### use Data::Dumper; print Dumper $self;
1055             }
1056 18         95 $self;
1057             }
1058              
1059             sub last_record {
1060 17     17   85 shift->{'total_pages'};
1061             }
1062              
1063             package XBase::cdx::Page;
1064 6     6   35 use strict;
  6         12  
  6         248  
1065 6     6   31 use vars qw( @ISA $DEBUG );
  6         11  
  6         27827  
1066             @ISA = qw( XBase::cdx );
1067              
1068             *DEBUG = \$XBase::Index::DEBUG;
1069              
1070             # Constructor for the cdx page
1071             sub new {
1072 17     17   39 my ($indexfile, $num) = @_;
1073             my $data = $indexfile->read_record($num)
1074 17 50       66 or do { print $indexfile->errstr; return; }; # get 512 bytes
  0         0  
  0         0  
1075              
1076 17         31 my $origdata = $data;
1077              
1078 17         52 my ($attributes, $noentries, $left_brother, $right_brother)
1079             = unpack 'vvVV', $data; # parse header of the page
1080 17         53 my $keylength = $indexfile->{'key_length'};
1081 17         27 my $keyreclength = $indexfile->{'key_record_length'}; # length
1082              
1083 17 50       61 print "page $num, attr $attributes, noentries $noentries, keylength $keylength (bro $left_brother, $right_brother)\n" if $DEBUG;
1084 17         93 my $numdate = $indexfile->{'key_type'}; # numeric or string?
1085              
1086 17         64 my ($keys, $values, $lefts) = ([], [], undef);
1087              
1088 17         34 my %opts = ();
1089              
1090 17 50       41 if ($attributes & 2) {
1091 17 50       37 print "leaf page, compressed\n" if $DEBUG;
1092 17         62 my ($free_space, $recno_mask, $duplicate_count_mask,
1093             $trailing_count_mask, $recno_count, $duplicate_count,
1094             $trailing_count, $holding_recno) = unpack '@12 vVCCCCCC', $data;
1095 17 50       55 print '$free_space, $recno_mask, $duplicate_count_mask, $trailing_count_mask, $recno_count, $duplicate_count, $trailing_count, $holding_recno) = ',
1096             "$free_space, $recno_mask, $duplicate_count_mask, $trailing_count_mask, $recno_count, $duplicate_count, $trailing_count, $holding_recno)\n" if $DEBUG > 2;
1097              
1098 17         162 @opts{ qw! recno_count duplicate_count trailing_count
1099             holding_recno ! } =
1100             ( $recno_count, $duplicate_count, $trailing_count,
1101             $holding_recno);
1102              
1103 17         25 my $prevkeyval = '';
1104 17         46 for (my $i = 0; $i < $noentries; $i++) {
1105 295         594 my $one_item = substr($data, 24 + $i * $holding_recno, $holding_recno) . "\0" x 4;
1106 295         490 my $numeric_one_item = unpack 'V', $one_item;
1107            
1108 295 50       10125 print "one_item: 0x", unpack('H*', $one_item), " ($numeric_one_item)\n" if $DEBUG > 3;
1109              
1110 295         620 my $recno = $numeric_one_item & $recno_mask;
1111 295         408 my $bytes_of_recno = int($recno_count / 8);
1112 295         382 $one_item = substr($one_item, $bytes_of_recno);
1113              
1114 295         704 $numeric_one_item = unpack 'V', $one_item;
1115 295         525 $numeric_one_item >>= $recno_count - (8 * $bytes_of_recno);
1116            
1117 295         302 my $dupl = $numeric_one_item & $duplicate_count_mask;
1118 295         288 $numeric_one_item >>= $duplicate_count;
1119 295         293 my $trail = $numeric_one_item & $trailing_count_mask;
1120             ### $numeric_one_item >>= $trailing_count;
1121              
1122 295 50       537 print "Item $i: trail $trail, dupl $dupl, recno $recno\n" if $DEBUG > 6;
1123              
1124 295         416 my $getlength = $keylength - $trail - $dupl;
1125 295         596 my $key = substr($prevkeyval, 0, $dupl);
1126 295 100       609 $key .= substr($data, -$getlength) if $getlength;
1127 295         396 $key .= "\000" x $trail;
1128 295 100       610 substr($data, -$getlength) = '' if $getlength;
1129 295         328 $prevkeyval = $key;
1130              
1131             ### print "Numdate $numdate\n";
1132 295 50       437 if ($numdate) { # some decoding for numbers
1133             ### print " *** In: ", unpack("H*", $key), "\n";
1134 0 0       0 if (0x80 & unpack('C', $key)) {
1135 0         0 substr($key, 0, 1) &= "\177";
1136             }
1137 0         0 else { $key = ~$key; }
1138 0 0       0 if ($keylength == 8) {
1139 0 0       0 $key = reverse $key unless $XBase::Index::BIGEND;
1140 0         0 $key = unpack 'd', $key;
1141             } else {
1142 0         0 $key = unpack 'N', $key;
1143             }
1144 0 0 0     0 if ($numdate == 2 and $key) { # date
1145 0         0 $key = sprintf "%04d%02d%02d",
1146             Time::JulianDay::inverse_julian_day($key);
1147             }
1148             } else {
1149 295 100       632 substr($key, -$trail) = '' if $trail;
1150             }
1151              
1152 295 50       526 print "$key -> $recno\n" if $DEBUG > 4;
1153 295         570 push @$keys, $key;
1154 295         870 push @$values, $recno;
1155             }
1156             } else {
1157 0         0 for (my $i = 0; $i < $noentries; $i++) {
1158 0         0 my $offset = 12 + $i * ($keylength + 8);
1159 0         0 my ($key, $recno, $page)
1160             = unpack "\@$offset a$keylength NN", $data;
1161             # some decoding for numbers
1162 0 0       0 if ($numdate) {
1163 0 0       0 if (0x80 & unpack('C', $key)) {
1164             ### if ("\200" & substr($key, 0, 1)) {
1165             ### print STDERR "Declean\n";
1166             ### print STDERR unpack("H*", $key), ' -> ';
1167 0         0 substr($key, 0, 1) &= "\177";
1168             ### print STDERR unpack("H*", $key), "\n";
1169             }
1170 0         0 else { $key = ~$key; }
1171 0 0       0 if ($keylength == 8) {
1172 0 0       0 $key = reverse $key unless $XBase::Index::BIGEND;
1173 0         0 $key = unpack 'd', $key;
1174             } else {
1175 0         0 $key = unpack 'N', $key;
1176             }
1177 0 0 0     0 if ($numdate == 2 and $key) { # date
1178 0         0 $key = sprintf "%04d%02d%02d",
1179             Time::JulianDay::inverse_julian_day($key);
1180             }
1181             } else {
1182 0         0 $key =~ s/\000+$//;
1183             }
1184 0 0       0 print "item: $key -> $recno via $page\n" if $DEBUG > 4;
1185 0         0 push @$keys, $key;
1186 0         0 push @$values, $recno;
1187 0 0       0 $lefts = [] unless defined $lefts;
1188 0         0 push @$lefts, $page / 512;
1189             }
1190 0         0 $opts{'last_key_is_just_overflow'} = 1;
1191             }
1192              
1193 17         261 my $self = bless { 'keys' => $keys, 'values' => $values,
1194             'num' => $num, 'keylength' => $keylength,
1195             'lefts' => $lefts, 'indexfile' => $indexfile,
1196             'attributes' => $attributes,
1197             'left_brother' => $left_brother,
1198             'right_brother' => $right_brother, %opts,
1199             }, __PACKAGE__;
1200              
1201 17         67 my $outdata = $self->prepare_scalar_for_write;
1202 17         32 if (0 and $outdata ne $origdata) {
1203             print "I won't be able to write this page back.\n",
1204             unpack("H*", $outdata), "\n ++\n",
1205             unpack("H*", $origdata), "\n";
1206             } else {
1207             ### print STDERR " ** Bingo: I will be able to write this page back ($num).\n";
1208             }
1209              
1210 17         64 $self;
1211             }
1212              
1213             # Create "new" page -- allocates memory in the file and returns
1214             # structure that can reasonably used as XBase::cdx::Page
1215             sub create {
1216 0     0   0 my ($class, $indexfile) = @_;
1217 0 0 0     0 if (not defined $indexfile and ref $class) {
1218 0         0 $indexfile = $class->{'indexfile'};
1219             }
1220 0         0 my $fh = $indexfile->{'fh'};
1221 0         0 $fh->seek(0, 2); # seek to the end;
1222 0         0 my $position = $fh->tell; # get the length of the file
1223 0 0       0 if ($position % 512) {
1224 0         0 $fh->print("\000" x (512 - ($position % 512)));
1225             # pad the file to multiply of 512
1226 0         0 $position = $fh->tell; # get the length of the file
1227             }
1228 0         0 $fh->print("\000" x 512);
1229 0         0 return bless { 'num' => $position / 512,
1230             'keylength' => $indexfile->{'key_length'},
1231             'indexfile' => $indexfile }, $class;
1232             }
1233              
1234             sub prepare_scalar_for_write {
1235 22     22   30 my $self = shift;
1236              
1237 22         67 my ($attributes, $noentries, $left_brother, $right_brother)
1238 22         62 = ($self->{'attributes'}, scalar(@{$self->{'keys'}}),
1239             $self->{'left_brother'}, $self->{'right_brother'});
1240            
1241 22         80 my $data = pack 'vvVV', $attributes, $noentries, $left_brother,
1242             $right_brother;
1243            
1244 22         93 my $indexfile = $self->{'indexfile'};
1245 22         34 my $numdate = $indexfile->{'key_type'}; # numeric or string?
1246 22         32 my $record_len = $indexfile->{'record_len'};
1247 22         32 my $keylength = $self->{'keylength'};
1248              
1249 22 50       46 if ($attributes & 2) {
1250              
1251 22         39 my ($recno_count, $duplicate_count, $trailing_count,
1252             $holding_recno) = (16, 4, 4, 3);
1253 22 50       54 if (defined $self->{'recno_count'}) {
1254 22         59 ($recno_count, $duplicate_count, $trailing_count,
1255             $holding_recno) =
1256 22         28 @{$self}{ qw! recno_count duplicate_count trailing_count
1257             holding_recno ! };
1258             }
1259              
1260             ### print STDERR "Hmmm. We are setting hardcoded values for bitmasks, not good. Write to adelton.\n";
1261 22         75 my ($recno_mask, $duplicate_mask, $trailing_mask)
1262             = ( 2**$recno_count - 1, 2**$duplicate_count - 1,
1263             2**$trailing_count - 1);
1264              
1265              
1266 22         30 my $recno_data = '';
1267              
1268 22         36 my $keys_string = '';
1269 22         24 my $prevkey = '';
1270              
1271 22         24 my $row = 0;
1272 22         25 for my $key (@{$self->{'keys'}}) {
  22         52  
1273 424         474 my $dupl = 0;
1274              
1275 424         543 my $out = $key;
1276             # some encoding for numbers
1277 424 50       750 if ($numdate) {
1278 0 0       0 if ($keylength == 8) {
1279 0         0 $out = pack 'd', $out;
1280 0 0       0 $out = reverse $out unless $XBase::Index::BIGEND;
1281             } else {
1282 0         0 $out = pack 'N', $out;
1283             }
1284              
1285              
1286 0 0       0 unless (0x80 & unpack('C', $out)) {
1287 0         0 substr($out, 0, 1) |= "\200";
1288             }
1289 0         0 else { $out = ~$out; }
1290             }
1291              
1292 424         652 for my $i (0 .. length($out) - 1) {
1293 1522 100       2922 unless (substr($out, $i, 1) eq substr($prevkey, $i, 1)) {
1294 265         315 last;
1295             }
1296 1257         2091 $dupl++;
1297             }
1298              
1299 424         579 my $trail = $keylength - length $out;
1300 424         937 while (substr($out, -1) eq "\000") {
1301 0         0 $out = substr($out, 0, length($out) - 1);
1302 0         0 $trail++;
1303             }
1304 424         1107 $keys_string = substr($out, $dupl) . $keys_string;
1305              
1306              
1307 424         974 my $numdata =
1308             (((($trail & $trailing_mask) << $duplicate_count)
1309             | ($dupl & $duplicate_mask)) << $recno_count)
1310             | ($self->{'values'}[$row] & $recno_mask);
1311              
1312 424         748 $recno_data .= substr(pack('V', $numdata), 0, $holding_recno);
1313              
1314             ### print unpack("H*", substr($out, $dupl)), ": trail $trail, dupl $dupl\n";
1315              
1316 424         428 $prevkey = $out;
1317 424         568 $row++;
1318             }
1319             ### print $keys_string, "\n";
1320              
1321             ### print STDERR "Hmmm. The \$numdata is really just a hack -- the shifts have to be made 64 bit clean.\n";
1322 22         155 $data .= pack 'vVCCCCCC',
1323             ($record_len - length($recno_data) - length($keys_string)
1324             - 24), $recno_mask, $duplicate_mask,
1325             $trailing_mask, $recno_count, $duplicate_count,
1326             $trailing_count, $holding_recno;
1327              
1328 22         31 $data .= $recno_data;
1329 22         58 $data .= "\000" x ($record_len - length($data) - length($keys_string));
1330 22         39 $data .= $keys_string;
1331             } else {
1332 0         0 my $row = 0;
1333 0         0 for my $key (@{$self->{'keys'}}) {
  0         0  
1334 0         0 my $out = $key;
1335             # some encoding for numbers
1336 0 0       0 if ($numdate) {
1337 0 0       0 if ($keylength == 8) {
1338 0         0 $out = pack 'd', $out;
1339 0 0       0 $out = reverse $out unless $XBase::Index::BIGEND;
1340             } else {
1341 0         0 $out = pack 'N', $out;
1342             }
1343              
1344              
1345 0 0       0 unless (0x80 & unpack('C', $out)) {
1346 0         0 substr($out, 0, 1) |= "\200";
1347             }
1348 0         0 else { $out = ~$out; }
1349             ### print " *** Out2: ", unpack("H*", $out), "\n";
1350             }
1351 0         0 $data .= pack "a$keylength NN", $out,
1352             $self->{'values'}[$row],
1353             $self->{'lefts'}[$row] * 512;
1354 0         0 $row++;
1355             }
1356 0         0 $data .= "\000" x ($record_len - length($data));
1357             }
1358 22         89 $data;
1359             }
1360              
1361             sub write_page {
1362 0     0   0 my $self = shift;
1363 0         0 my $indexfile = $self->{'indexfile'};
1364              
1365 0         0 my $data = $self->prepare_scalar_for_write;
1366 0 0       0 die "Data is too long in cdx::write_page for $self->{'num'}\n"
1367             if length $data > 512;
1368 0         0 $indexfile->write_record($self->{'num'}, $data);
1369             }
1370              
1371             # Saves current page, taking into account all neighbour and parent
1372             # pages. We can safely assume that this method is called for pages
1373             # that have been loaded using prepare_select_eq and fetch, so they
1374             # have the parent pointers set correctly.
1375             sub write_with_context {
1376 5     5   7 my $self = shift; # page to save
1377 5 50       9 print STDERR "XBase::cdx::Page::write_with_context called ($self->{'num'})\n" if $DEBUG;
1378              
1379 5         7 my $indexfile = $self->{'indexfile'};
1380              
1381 5         7 my $self_num = $self->{'num'};
1382              
1383             # get the current page as data to be written
1384 5         7 my $data = $self->prepare_scalar_for_write;
1385              
1386 5 50       7 if (not @{$self->{'keys'}}) {
  5         15  
1387 0         0 $indexfile->write_record($self_num, $data);
1388              
1389             # empty root page means no more work, just save
1390 0 0       0 return if $self_num == $indexfile->{'start_page'};
1391              
1392 0         0 print STDERR "The page $self_num is empty, releasing from the chain\n";
1393            
1394             # first we update the brothers
1395 0         0 my $right_brother_num = $self->{'right_brother'};
1396 0         0 my $left_brother_num = $self->{'left_brother'};
1397 0 0       0 if ($right_brother_num != 0xFFFFFFFF) {
1398 0         0 my $fix_brother = $indexfile->get_record($right_brother_num / 512);
1399 0         0 $fix_brother->{'left_brother'} = $left_brother_num;
1400 0         0 $fix_brother->write_page;
1401             }
1402 0 0       0 if ($left_brother_num != 0xFFFFFFFF) {
1403 0         0 my $fix_brother = $indexfile->get_record($left_brother_num / 512);
1404 0         0 $fix_brother->{'right_brother'} = $right_brother_num;
1405 0         0 $fix_brother->write_page;
1406             }
1407              
1408             # now we need to release ourselves from parent as well
1409 0 0       0 my $parent = $self->get_parent_page or die "Index corrupt: no parent for page $self ($self_num)\n";
1410              
1411 0         0 my $maxindex = $#{$parent->{'lefts'}};
  0         0  
1412 0         0 my $i;
1413 0         0 for ($i = 0; $i <= $maxindex; $i++) {
1414 0 0       0 if ($parent->{'lefts'}[$i] == $self_num) {
1415 0         0 splice @{$parent->{'keys'}}, $i, 1;
  0         0  
1416 0         0 splice @{$parent->{'values'}}, $i, 1;
  0         0  
1417 0         0 splice @{$parent->{'lefts'}}, $i, 1;
  0         0  
1418 0         0 last;
1419             }
1420             }
1421 0 0       0 if ($i > $maxindex) {
1422 0         0 die "Index corrupt: parent doesn't point to us in write_with_context $self ($self_num)\n";
1423             }
1424 0         0 $parent->write_with_context;
1425 0         0 return;
1426             }
1427              
1428              
1429 5 50       17 if (length $data > 512) { # we need to split the page
    50          
1430 0         0 print STDERR "Splitting full page $self ($self_num)\n";
1431              
1432             # create will give us brand new empty page
1433            
1434 0         0 my $new_page = __PACKAGE__->create($indexfile);
1435 0         0 $self->{'attributes'} &= 0xfffe;
1436 0         0 $new_page->{'attributes'} = $self->{'attributes'};
1437              
1438 0         0 my $total_rows = scalar(@{$self->{'keys'}});
  0         0  
1439 0         0 my $half_rows = int($total_rows / 2);
1440              
1441             # primary split
1442 0 0       0 if ($half_rows == 0) { $half_rows++; }
  0         0  
1443 0 0       0 if ($half_rows == $total_rows) {
1444 0         0 die "Fatal trouble: page $self ($self_num) is full but I'm not able to split it\n";
1445             }
1446              
1447             # new page is right brother (will get bigger values)
1448 0         0 $new_page->{'right_brother'} = $self->{'right_brother'};
1449 0         0 $new_page->{'left_brother'} = $self_num * 512;
1450 0         0 $self->{'right_brother'} = $new_page->{'num'} * 512;
1451              
1452 0 0       0 if ($new_page->{'right_brother'} != 0xFFFFFFFF) {
1453 0         0 my $fix_brother = $indexfile->get_record($new_page->{'right_brother'} / 512);
1454 0         0 $fix_brother->{'left_brother'} = $new_page->{'num'} * 512;
1455 0         0 $fix_brother->write_page;
1456             }
1457              
1458             # we'll split keys and values
1459 0         0 $new_page->{'keys'} = [ @{$self->{'keys'}}[$half_rows .. $total_rows - 1] ];
  0         0  
1460 0         0 splice @{$self->{'keys'}}, $half_rows, $total_rows - $half_rows;
  0         0  
1461 0         0 $new_page->{'values'} = [ @{$self->{'values'}}[$half_rows .. $total_rows - 1] ];
  0         0  
1462 0         0 splice @{$self->{'values'}}, $half_rows, $total_rows - $half_rows;
  0         0  
1463              
1464             # and we'll split pointers to lower levels, if there are any
1465 0 0       0 if (defined $self->{'lefts'}) {
1466 0         0 $new_page->{'lefts'} = [ @{$self->{'lefts'}}[$half_rows .. $total_rows - 1] ];
  0         0  
1467 0         0 my $new_page_num = $new_page->{'num'};
1468 0         0 for my $q (@{$new_page->{'lefts'}}) {
  0         0  
1469 0 0 0     0 if (defined $q and defined $indexfile->{'pages_cache'}{$q}) {
1470 0         0 $indexfile->{'pages_cache'}{$q}{'parent'} = $new_page_num;
1471             }
1472             }
1473 0         0 splice @{$self->{'lefts'}}, $half_rows, $total_rows - $half_rows - 1;
  0         0  
1474             }
1475              
1476 0         0 my $parent;
1477 0 0       0 if ($self_num == $indexfile->{'start_page'}) {
1478             # we're splitting the root page, so we will
1479             # create new one
1480 0         0 $parent = __PACKAGE__->create($indexfile);
1481              
1482 0         0 $indexfile->{'start_page'} = $parent->{'num'};
1483 0         0 $indexfile->write_header;
1484              
1485             ### xxxxxxxxxxxxxxxxxxx
1486             ### And here we should write the header so that
1487             ### the new root page is saved to disk. Not
1488             ### tested yet.
1489             ### xxxxxxxxxxxxxxxxxxx
1490              
1491 0         0 $parent->{'attributes'} = 1; # root page
1492              
1493 0         0 $parent->{'keys'} = [ $self->{'keys'}[-1],
1494             $new_page->{'keys'}[-1] ];
1495 0         0 $parent->{'values'} = [ $self->{'values'}[-1],
1496             $new_page->{'values'}[-1] ];
1497 0         0 $parent->{'lefts'} = [ $self_num, $new_page->{'num'} ];
1498             } else { # update pointers in parent page
1499 0 0       0 $parent = $self->get_parent_page or die "Index corrupt: no parent for page $self ($self_num)\n";
1500 0         0 my $maxindex = $#{$parent->{'lefts'}};
  0         0  
1501 0         0 my $i = 0;
1502              
1503             # find pointer to ourselves in the parent
1504 0         0 while ($i <= $maxindex) {
1505 0 0       0 last if $parent->{'lefts'}[$i] == $self_num;
1506 0         0 $i++;
1507             }
1508            
1509 0 0       0 if ($i > $maxindex) {
1510 0         0 die "Index corrupt: parent doesn't point to us in write_with_context $self ($self_num)\n";
1511             }
1512              
1513             # now $i is index in parent of the record pointing to us
1514              
1515 0         0 splice @{$parent->{'keys'}}, $i, 1,
  0         0  
1516             $self->{'keys'}[-1], $new_page->{'keys'}[-1];
1517 0         0 splice @{$parent->{'values'}}, $i, 1,
  0         0  
1518             $self->{'values'}[-1], $new_page->{'values'}[-1];
1519 0         0 splice @{$parent->{'lefts'}}, $i, 1,
  0         0  
1520             $self_num, $new_page->{'num'};
1521             }
1522              
1523 0         0 $self->write_page;
1524              
1525 0         0 $new_page->{'parent'} = $self->{'parent'};
1526 0         0 $new_page->write_page;
1527              
1528 0         0 $parent->write_with_context;
1529             }
1530             elsif ($self_num != $indexfile->{'start_page'}) {
1531             # the output data is OK, write is out
1532             # but this is not root page, so we need to make sure the
1533             # parent is updated as well
1534 0         0 $indexfile->write_record($self_num, $data);
1535              
1536             # now we need to check if the parent page still points
1537             # correctly to us (the last value might have changed)
1538 0 0       0 my $parent = $self->get_parent_page or die "Index corrupt: no parent for page $self ($self_num)\n";
1539              
1540 0         0 my $maxindex = $#{$parent->{'lefts'}};
  0         0  
1541 0         0 my $i = 0;
1542              
1543             # find pointer to ourselves in the parent
1544 0         0 while ($i <= $maxindex) {
1545 0 0       0 last if $parent->{'lefts'}[$i] == $self_num;
1546 0         0 $i++;
1547             }
1548            
1549 0 0       0 if ($i > $maxindex) {
1550 0         0 die "Index corrupt: parent doesn't point to us in write_with_context $self ($self_num)\n";
1551             }
1552              
1553             # now $i is index in parent of the record pointing to us
1554              
1555 0 0       0 if ($parent->{'values'}[$i] != $self->{'values'}[-1]) {
1556 0         0 print STDERR "Will need to update the parent -- last value in myself changed ($self_num)\n";
1557 0         0 $parent->{'values'}[$i] = $self->{'values'}[-1];
1558 0         0 $parent->{'keys'}[$i] = $self->{'keys'}[-1];
1559 0         0 $parent->write_with_context;
1560             }
1561            
1562             } else { # write out root page
1563 5         24 $indexfile->write_record($self_num, $data);
1564             }
1565              
1566 5 50       14 print STDERR "XBase::cdx::Page::write_with_context finished ($self->{'num'})\n" if $DEBUG;
1567             }
1568              
1569             # finds parent page for the object
1570             sub get_parent_page_num {
1571 0     0     my $self = shift;
1572 0 0         return $self->{'parent'} if defined $self->{'parent'};
1573              
1574 0           my $indexfile = $self->{'indexfile'};
1575              
1576 0 0         return if $self->{'num'} == $indexfile->{'start_page'};
1577              
1578             # this should search to this page, effectivelly setting the
1579             # level array in such a way that the parent page is there
1580 0           $indexfile->prepare_select_eq($self->{'keys'}[0], $self->{'values'}[0]);
1581              
1582             ### print STDERR "self($self->{'num'}): $self, pages: @{$indexfile->{'pages'}}\n";
1583             ### use Data::Dumper; print Dumper $indexfile;
1584 0           my $pageindex = $#{$indexfile->{'pages'}};
  0            
1585 0           while ($pageindex >= 0) {
1586 0 0         if ("$self" eq "$indexfile->{'pages'}[$pageindex]") {
1587 0           print STDERR "Parent page for $self->{'num'} is $indexfile->{'pages'}[$pageindex - 1]{'num'}.\n";
1588 0           return $indexfile->{'pages'}[$pageindex - 1]->{'num'};
1589             }
1590 0           $pageindex--;
1591             }
1592 0           return undef;
1593             }
1594             sub get_parent_page {
1595 0     0     my $self = shift;
1596 0 0         my $parent_num = $self->get_parent_page_num or return;
1597 0           my $indexfile = $self->{'indexfile'};
1598 0           return $indexfile->get_record($parent_num);
1599             }
1600              
1601             1;
1602              
1603             __END__