File Coverage

blib/lib/Tie/File/Indexed.pm
Criterion Covered Total %
statement 299 337 88.7
branch 142 284 50.0
condition 48 101 47.5
subroutine 53 64 82.8
pod 23 39 58.9
total 565 825 68.4


line stmt bran cond sub pod time code
1             ##-*- Mode: CPerl -*-
2             ##
3             ## File: Tie/File/Indexed.pm
4             ## Author: Bryan Jurish
5             ## Description: tied array access to indexed data files
6              
7             package Tie::File::Indexed;
8 5     5   34520 use 5.10.0; ##-- for // operator
  5         11  
9 5     5   2169 use Tie::Array;
  5         4397  
  5         106  
10 5     5   3126 use JSON qw();
  5         53665  
  5         124  
11 5     5   30 use Fcntl qw(:DEFAULT :seek :flock);
  5         6  
  5         1754  
12 5     5   2277 use File::Copy qw();
  5         10866  
  5         100  
13 5     5   2246 use IO::File;
  5         33384  
  5         943  
14 5     5   26 use Carp qw(confess);
  5         6  
  5         167  
15 5     5   19 use strict;
  5         5  
  5         4233  
16              
17             ##======================================================================
18             ## Globals
19              
20             our @ISA = qw(Tie::Array);
21             our $VERSION = '0.09';
22              
23             ##======================================================================
24             ## Constructors etc.
25              
26             ## $tied = CLASS->new(%opts)
27             ## $tied = CLASS->new($file,%opts)
28             ## + %opts, object structure:
29             ## (
30             ## file => $file, ##-- file basename; uses files "${file}", "${file}.idx", "${file}.hdr"
31             ## mode => $mode, ##-- open mode (fcntl flags or perl-style; default='rwa')
32             ## perms => $perms, ##-- default: 0666 & ~umask
33             ## pack_o => $pack_o, ##-- file offset pack template (default='J')
34             ## pack_l => $pack_l, ##-- string-length pack template (default='J')
35             ## bsize => $bsize, ##-- block-size in bytes for index batch-operations (default=2**21 = 2MB)
36             ## temp => $bool, ##-- if true, call unlink() on object destruction (default=false)
37             ## ##
38             ## ##-- pack lengths (after open())
39             ## len_o => $len_o, ##-- packsize($pack_o)
40             ## len_l => $len_l, ##-- packsize($pack_l)
41             ## len_ix => $len_ix, ##-- packsize($pack_ix) == $len_o + $len_l
42             ## pack_ix=> $pack_ix, ##-- "${pack_o}${pack_l}"
43             ## ##
44             ## ##-- guts (after open())
45             ## idxfh => $idxfh, ##-- $file.idx : [$i] => pack("${pack_o}${pack_l}", $offset_in_datfh_of_item_i, $len_in_datfh_of_item_i)
46             ## datfh => $datfh, ##-- $file : raw data (concatenated)
47             ## #size => $nrecords, ##-- cached number of records for faster FETCHSIZE() ##-- potentially UNSAFE for concurrent access: DON'T USE
48             ## )
49             sub new {
50 17     17 1 393 my $that = shift;
51 17 50       42 my $file = (@_ % 2)==0 ? undef : shift;
52 17         36 my %opts = @_;
53 17   66     45 my $tied = bless({
54             $that->defaults(),
55             file => $file,
56             @_,
57             }, ref($that)||$that);
58 17 50       88 return $tied->open() if (defined($tied->{file}));
59 0         0 return $tied;
60             }
61              
62             ## %defaults = CLASS_OR_OBJECT->defaults()
63             ## + default attributes for constructor
64             sub defaults {
65             return (
66             #file => $file,
67 17     17 1 171 perms => (0666 & ~umask),
68             mode => 'rwa',
69             pack_o => 'J',
70             pack_l => 'J',
71             block => 2**21,
72             );
73             }
74              
75             ## undef = $tied->DESTROY()
76             ## + implicitly calls unlink() if 'temp' attribute is set to a true value
77             ## + implicitly calls close()
78             sub DESTROY {
79 16 100   16   1340 $_[0]->unlink() if ($_[0]{temp});
80 16         32 $_[0]->close();
81             }
82              
83             ##======================================================================
84             ## Utilities
85              
86             ##--------------------------------------------------------------
87             ## Utilities: fcntl
88              
89             ## $flags = CLASS_OR_OBJECT->fcflags($mode)
90             ## + returns Fcntl flags for symbolic string $mode
91             sub fcflags {
92 474 50   474 0 861 shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
93 474         331 my $mode = shift;
94 474   50     531 $mode //= 'r';
95 474 100       1108 return $mode if ($mode =~ /^[0-9]+$/); ##-- numeric mode is interpreted as Fcntl bitmask
96 17         39 my $fread = $mode =~ /[r<]/;
97 17         21 my $fwrite = $mode =~ /[wa>\+]/;
98 17   66     54 my $fappend = ($mode =~ /[a]/ || $mode =~ />>/);
99 17 100       34 my $flags = ($fread
    0          
    50          
100             ? ($fwrite ? (O_RDWR|O_CREAT) : O_RDONLY)
101             : ($fwrite ? (O_WRONLY|O_CREAT) : 0)
102             );
103 17 100 100     56 $flags |= O_TRUNC if ($fwrite && !$fappend);
104 17         28 return $flags;
105             }
106              
107             ## $fcflags = fcgetfl($fh)
108             ## + returns Fcntl flags for filehandle $fh
109             sub fcgetfl {
110 0 0   0 0 0 shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
111 0         0 my $fh = shift;
112 0         0 return CORE::fcntl($fh,F_GETFL,0);
113             }
114              
115             ## $bool = CLASS_OR_OBJECT->fcread($mode)
116             ## + returns true if any read-bits are set for $mode
117             sub fcread {
118 80 50   80 0 185 shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
119 80         78 my $flags = fcflags(shift);
120 80   33     253 return ($flags&O_RDONLY)==O_RDONLY || ($flags&O_RDWR)==O_RDWR;
121             }
122              
123             ## $bool = CLASS_OR_OBJECT->fcwrite($mode)
124             ## + returns true if any write-bits are set for $mode
125             sub fcwrite {
126 135 50   135 0 371 shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
127 135         148 my $flags = fcflags(shift);
128 135   66     590 return ($flags&O_WRONLY)==O_WRONLY || ($flags&O_RDWR)==O_RDWR;
129             }
130              
131             ## $bool = CLASS_OR_OBJECT->fctrunc($mode)
132             ## + returns true if truncate-bits are set for $mode
133             sub fctrunc {
134 122 50   122 0 241 shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
135 122         116 my $flags = fcflags(shift);
136 122         293 return ($flags&O_TRUNC)==O_TRUNC;
137             }
138              
139             ## $bool = CLASS_OR_OBJECT->fccreat($mode)
140             sub fccreat {
141 0 0   0 0 0 shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
142 0         0 my $flags = fcflags(shift);
143 0         0 return ($flags&O_CREAT)==O_CREAT;
144             }
145              
146             ## $str = CLASS_OR_OBJECT->fcperl($mode)
147             ## + return perl mode-string for $mode
148             sub fcperl {
149 54 50   54 0 118 shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
150 54         58 my $flags = fcflags(shift);
151 54 100       57 return (fcread($flags)
    100          
    0          
    0          
    50          
152             ? (fcwrite($flags) ##-- +read
153             ? (fctrunc($flags) ##-- +read,+write
154             ? '+>' : '+<') ##-- +read,+write,+/-trunc
155             : '<')
156             : (fcwrite($flags) ##-- -read
157             ? (fctrunc($flags) ##-- -read,+write
158             ? '>' : '>>') ##-- -read,+write,+/-trunc
159             : '<') ##-- -read,-write : default
160             );
161             }
162              
163             ## $fh_or_undef = CLASS_OR_OBJECT->fcopen($file,$mode)
164             ## $fh_or_undef = CLASS_OR_OBJECT->fcopen($file,$mode,$perms)
165             ## + opens $file with fcntl- or perl-style mode $mode
166             sub fcopen {
167 54 50   54 0 166 shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
168 54         60 my ($file,$flags,$perms) = @_;
169 54         58 $flags = fcflags($flags);
170 54   33     75 $perms //= (0666 & ~umask);
171 54         58 my $mode = fcperl($flags);
172              
173 54         44 my ($sysfh);
174 54 50       66 if (ref($file)) {
175             ##-- dup an existing filehandle
176 0         0 $sysfh = $file;
177             }
178             else {
179             ##-- use sysopen() to honor O_CREAT and O_TRUNC
180 54 50       1836 sysopen($sysfh, $file, $flags, $perms) or return undef;
181             }
182              
183             ##-- now open perl-fh from system fh
184 54 50       504 open(my $fh, "${mode}&=", fileno($sysfh)) or return undef;
185 54 100 100     68 if (fcwrite($flags) && !fctrunc($flags)) {
186             ##-- append mode: seek to end of file
187 21 50       50 seek($fh, 0, SEEK_END) or return undef;
188             }
189 54         256 return $fh;
190             }
191              
192             ##--------------------------------------------------------------
193             ## Utilities: pack sizes
194              
195             ## $len = CLASS->packsize($packfmt)
196             ## $len = CLASS->packsize($packfmt,@args)
197             ## + get pack-size for $packfmt with args @args
198             sub packsize {
199 5     5   2715 use bytes; ##-- deprecated in perl v5.18.2
  5         41  
  5         35  
200 5     5   128 no warnings;
  5         6  
  5         11432  
201 52     52 0 190 return bytes::length(pack($_[0],@_[1..$#_]));
202             }
203              
204              
205             ##--------------------------------------------------------------
206             ## Utilities: JSON
207              
208             ## $data = CLASS->loadJsonString( $string,%opts)
209             ## $data = CLASS->loadJsonString(\$string,%opts)
210             ## + %opts passed to JSON::from_json(), e.g. (relaxed=>0)
211             ## + supports $opts{json} = $json_obj
212             sub loadJsonString {
213 13 50   13 0 33 my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
214 13 50       22 my $bufr = ref($_[0]) ? $_[0] : \$_[0];
215 13         27 my %opts = @_[1..$#_];
216 13 50       21 return $opts{json}->decode($$bufr) if ($opts{json});
217 13         61 return JSON::from_json($$bufr, {utf8=>!utf8::is_utf8($$bufr), relaxed=>1, allow_nonref=>1, %opts});
218             }
219              
220             ## $data = CLASS->loadJsonFile($filename_or_handle,%opts)
221             sub loadJsonFile {
222 13 50   13 0 29 my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
223 13         11 my $file = shift;
224 13 50       58 my $fh = ref($file) ? $file : IO::File->new("<$file");
225 13 50       525 return undef if (!$fh);
226 13         36 binmode($fh,':raw');
227 13         35 local $/=undef;
228 13         121 my $buf = <$fh>;
229 13 50       63 close($fh) if (!ref($file));
230 13         27 return $that->loadJsonString(\$buf,@_);
231             }
232              
233             ## $str = CLASS->saveJsonString($data)
234             ## $str = CLASS->saveJsonString($data,%opts)
235             ## + %opts passed to JSON::to_json(), e.g. (pretty=>0, canonical=>0)'
236             ## + supports $opts{json} = $json_obj
237             sub saveJsonString {
238 35 50   35 0 367 my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
239 35         34 my $data = shift;
240 35         47 my %opts = @_;
241 35 50       55 return $opts{json}->encode($data) if ($opts{json});
242 35         154 return JSON::to_json($data, {utf8=>1, allow_nonref=>1, allow_unknown=>1, allow_blessed=>1, convert_blessed=>1, pretty=>1, canonical=>1, %opts});
243             }
244              
245             ## $bool = CLASS->saveJsonFile($data,$filename_or_handle,%opts)
246             sub saveJsonFile {
247 23 50   23 0 69 my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
248 23         18 my $data = shift;
249 23         17 my $file = shift;
250 23 50       119 my $fh = ref($file) ? $file : IO::File->new(">$file");
251 23 50 0     1716 logconfess((ref($that)||$that)."::saveJsonFile() failed to open file '$file': $!") if (!$fh);
252 23         78 binmode($fh,':raw');
253 23 50       65 $fh->print($that->saveJsonString($data,@_)) or return undef;
254 23 50       1088 if (!ref($file)) { close($fh) || return undef; }
  23 50       597  
255 23         98 return 1;
256             }
257              
258             ##--------------------------------------------------------------
259             ## Utilities: debugging
260              
261             ## $idxbuf = $tied->slurpIndex()
262             ## + slurps whole raw index-file into a string-buffer (for debugging)
263             sub slurpIndex {
264 0     0 0 0 my $tied = shift;
265 0 0       0 return undef if (!$tied->opened);
266 0         0 my $fh = $tied->{idxfh};
267 0 0       0 CORE::seek($fh, 0, SEEK_SET) or return undef;
268 0         0 local $/ = undef;
269 0         0 return <$fh>;
270             }
271              
272             ## $idxtxt = $tied->indexText()
273             ## @idxtxt = $tied->indexText()
274             ## + slurps whole index file and returns it as a text-buffer (for debugging)
275             sub indexText {
276 0     0 0 0 my $tied = shift;
277 0   0     0 my @idx = map {join(' ',unpack($tied->{pack_ix},$_))} unpack("(A[$tied->{len_ix}])*", $tied->slurpIndex//'');
  0         0  
278 0 0       0 return wantarray ? @idx : join("\n",@idx)."\n";
279             }
280              
281             ## $datbuf = $tied->slurpData()
282             ## + slurps whole raw data-file into a string-buffer (for debugging)
283             sub slurpData {
284 0     0 0 0 my $tied = shift;
285 0 0       0 return undef if (!$tied->opened);
286 0         0 my $fh = $tied->{datfh};
287 0 0       0 CORE::seek($fh, 0, SEEK_SET) or return undef;
288 0         0 local $/ = undef;
289 0         0 return <$fh>;
290             }
291              
292              
293             ##======================================================================
294             ## Subclass API: Data I/O
295              
296             ## $bool = $tied->writeData($data)
297             ## + write item $data to $tied->{datfh} at its current position
298             ## + after writing, $tied->{datfh} should be positioned to the first byte following the written item
299             ## + $tied is assumed to be opened in write-mode
300             ## + default implementation just writes $data as a byte-string (undef is written as the empty string)
301             ## + can be overridden by subclasses to perform transparent encoding of complex data
302             sub writeData {
303 24   50 24 1 64 return $_[0]{datfh}->print($_[1]//'');
304             }
305              
306             ## $data_or_undef = $tied->readData($length)
307             ## + read item data record of length $length from $tied->{datfh} at its current position
308             ## + default implementation just reads a byte-string of length $length
309             sub readData {
310 106 50   106 1 320 CORE::read($_[0]{datfh}, my $buf, $_[1])==$_[1] or return undef;
311 106         252 return $buf;
312             }
313              
314             ##======================================================================
315             ## Subclass API: Index I/O
316              
317             ## ($off,$len) = $tied->readIndex($index)
318             ## ($off,$len) = $tied->readIndex(undef)
319             ## + gets index-record for item at logical index $index from $tied->{idxfh}
320             ## + if $index is undef, read from the current position of $tied->{idxfh}
321             ## + $index is assumed to exist in the array
322             ## + returns the empty list on error
323             sub readIndex {
324 133 50 33 133 1 511 !defined($_[1]) or CORE::seek($_[0]{idxfh}, $_[1]*$_[0]{len_ix}, SEEK_SET) or return qw();
325 133 50       520 CORE::read($_[0]{idxfh}, my $buf, $_[0]{len_ix})==$_[0]{len_ix} or return qw();
326 133         358 return unpack($_[0]{pack_ix}, $buf);
327             }
328              
329             ## $tied_or_undef = $tied->writeIndex($index,$off,$len)
330             ## $tied_or_undef = $tied->writeIndex(undef,$off,$len)
331             ## + writes index-record for item at logical index $index to $tied->{idxfh}
332             ## + if $index is undef, write at the current position of $tied->{idxfh}
333             ## + returns undef list on error
334             sub writeIndex {
335 75 50 66 75 1 667 !defined($_[1]) or CORE::seek($_[0]{idxfh}, $_[1]*$_[0]{len_ix}, SEEK_SET) or return undef;
336 75 50       253 $_[0]{idxfh}->print(pack($_[0]{pack_ix}, $_[2], $_[3])) or return undef;
337 75         395 return $_[0];
338             }
339              
340             ## $tied_or_undef = $tied->shiftIndex($start,$n,$shift)
341             ## + moves $n index records starting from $start by $shift positions (may be negative)
342             ## + operates directly on $tied->{idxfh}
343             ## + doesn't change old values unless they are overwritten
344             sub shiftIndex {
345 4     4 1 4 my ($tied,$start,$n,$shift) = @_;
346              
347             ##-- common variables
348 4         5 my $idxfh = $tied->{idxfh};
349 4         4 my $len_ix = $tied->{len_ix};
350 4   50     11 my $bsize = $tied->{bsize} // 2**21;
351 4         6 my $bstart = $len_ix * $start;
352 4         2 my $bn = $len_ix * $n;
353 4         4 my $bshift = $len_ix * $shift;
354 4         4 my ($buf,$boff,$blen);
355              
356             ##-- dispatch by shift direction
357 4 100       5 if ($shift > 0) {
358             ##-- shift right (copy right-to-left)
359 2 50       6 CORE::seek($tied->{idxfh}, $bstart+$bn, SEEK_SET) or return undef;
360 2         3 while ($bn > 0) {
361 2 50       5 $blen = $bn > $bsize ? $bsize : $bn;
362 2 50       5 CORE::seek($idxfh, -$blen, SEEK_CUR) or return undef;
363 2 50       10 CORE::read($idxfh, $buf, $blen)==$blen or return undef;
364 2 50       4 CORE::seek($idxfh, $bshift-$blen, SEEK_CUR) or return undef;
365 2 50       6 $idxfh->print($buf) or return undef;
366 2 50       25 CORE::seek($idxfh, -$bshift, SEEK_CUR) or return undef;
367 2         3 $bn -= $blen;
368             }
369             } else {
370             ##-- shift left (copy left-to-right)
371 2 50       10 CORE::seek($tied->{idxfh}, $bstart, SEEK_SET) or return undef;
372 2         5 while ($bn > 0) {
373 2 50       3 $blen = $bn > $bsize ? $bsize : $bn;
374 2 50       7 CORE::read($idxfh, $buf, $blen)==$blen or return undef;
375 2 50       4 CORE::seek($idxfh, $bshift-$blen, SEEK_CUR) or return undef;
376 2 50       7 $idxfh->print($buf) or return undef;
377 2 50       26 CORE::seek($idxfh, -$bshift, SEEK_CUR) or return undef;
378 2         5 $bn -= $blen;
379             }
380             }
381              
382 4         4 return $tied;
383             }
384              
385              
386              
387             ##======================================================================
388             ## Object API
389              
390             ##--------------------------------------------------------------
391             ## Object API: header
392              
393             ## @keys = $tied->headerKeys()
394             ## + keys to save as header
395             sub headerKeys {
396 23   100 23 1 16 return grep {!ref($_[0]{$_}) && $_ !~ m{^(?:file|mode|perms)$}} keys %{$_[0]};
  291         835  
  23         89  
397             }
398              
399             ## \%header = $tied->headerData()
400             ## + data to save as header
401             sub headerData {
402 23     23 1 20 my $tied = shift;
403 23         39 return {(map {($_=>$tied->{$_})} $tied->headerKeys), class=>ref($tied)};
  175         281  
404             }
405              
406             ## $tied_or_undef = $tied->loadHeader()
407             ## $tied_or_undef = $tied->loadHeader($headerFile,%opts)
408             ## + loads header from "$tied->{file}.hdr"
409             ## + %opts are passed to loadJsonFile()
410             sub loadHeader {
411 13     13 1 18 my ($tied,$hfile,%opts) = @_;
412 13 50 33     47 $hfile //= $tied->{file}.".hdr" if (defined($tied->{file}));
413 13 50       16 confess(ref($tied)."::loadHeader(): no header-file specified and no 'file' attribute defined") if (!defined($hfile));
414 13 50       27 my $hdata = $tied->loadJsonFile($hfile,%opts)
415             or confess(ref($tied)."::loadHeader(): failed to load header data from '$hfile'");
416 13         392 @$tied{keys %$hdata} = values %$hdata;
417 13         59 return $tied;
418             }
419              
420             ## $tied_or_undef = $tied->saveHeader()
421             ## $tied_or_undef = $tied->saveHeader($headerFile)
422             ## + saves header data to $headerFile
423             ## + %opts are passed to saveJsonFile()
424             sub saveHeader {
425 23     23 1 34 my ($tied,$hfile,%opts) = @_;
426 23 50 33     92 $hfile //= $tied->{file}.".hdr" if (defined($tied->{file}));
427 23 50       34 confess(ref($tied)."::saveHeader(): no header-file specified and no 'file' attribute defined") if (!defined($hfile));
428 23         47 return $tied->saveJsonFile($tied->headerData(), $hfile, %opts);
429             }
430              
431             ##--------------------------------------------------------------
432             ## Object API: open/close
433              
434             ## $tied_or_undef = $tied->open($file,$mode)
435             ## $tied_or_undef = $tied->open($file)
436             ## $tied_or_undef = $tied->open()
437             ## + opens file(s)
438             sub open {
439 26     26 1 31 my ($tied,$file,$mode) = @_;
440 26   66     62 $file //= $tied->{file};
441 26   66     55 $mode //= $tied->{mode};
442 26 50       47 $tied->close() if ($tied->opened);
443 26         30 $tied->{file} = $file;
444 26         46 $tied->{mode} = $mode = fcflags($mode);
445              
446 26 100 66     46 if (fcread($mode) && !fctrunc($mode)) {
447 13 50 33     151 (!-e "$file.hdr" && fccreat($mode))
      33        
448             or $tied->loadHeader()
449             or confess(ref($tied)."::failed to load header from '$tied->{file}.hdr': $!");
450             }
451              
452             $tied->{idxfh} = fcopen("$file.idx", $mode, $tied->{perms})
453 26 50       68 or confess(ref($tied)."::open failed for index-file $file.idx: $!");
454             $tied->{datfh} = fcopen("$file", $mode, $tied->{perms})
455 26 50       67 or confess(ref($tied)."::open failed for data-file $file: $!");
456 26         100 binmode($_) foreach (@$tied{qw(idxfh datfh)});
457              
458             ##-- pack lengths
459             #use bytes; ##-- deprecated in perl v5.18.2
460 26         38 $tied->{len_o} = packsize($tied->{pack_o});
461 26         2286 $tied->{len_l} = packsize($tied->{pack_l});
462 26         63 $tied->{len_ix} = $tied->{len_o} + $tied->{len_l};
463 26         38 $tied->{pack_ix} = $tied->{pack_o}.$tied->{pack_l};
464              
465 26         96 return $tied;
466             }
467              
468             ## $tied_or_undef = $tied->close()
469             ## + close any opened file, writes header if opened in write mode
470             sub close {
471 36     36 1 31 my $tied = shift;
472 36 100       48 return $tied if (!$tied->opened);
473 25 100 66     38 if ($tied->opened && fcwrite($tied->{mode})) {
474 23 50       47 $tied->saveHeader() or
475             confess(ref($tied)."::close(): failed to save header file");
476             }
477 25         268 delete @$tied{qw(idxfh datfh)}; ##-- should auto-close if not shared
478 25         39 undef $tied->{file};
479 25         119 return $tied;
480             }
481              
482             ## $bool = $tied->reopen()
483             ## + closes and re-opens underlying filehandles
484             ## + should cause a "real" flush even on systems without a working IO::Handle::flush
485             sub reopen {
486 7     7 1 7 my $tied = shift;
487 7         9 my ($file,$mode) = @$tied{qw(file mode)};
488 7   33     9 return $tied->opened() && $tied->close() && $tied->open($file, $mode & ~O_TRUNC);
489             }
490              
491             ## $bool = $tied->opened()
492             ## + returns true iff object is opened
493             sub opened {
494 105     105 1 89 my $tied = shift;
495             return (ref($tied)
496             && defined($tied->{idxfh})
497             && defined($tied->{datfh})
498 105   66     618 );
499             }
500              
501             ## $tied_or_undef = $tied->flush()
502             ## $tied_or_undef = $tied->flush($flushHeader)
503             ## + attempts to flush underlying filehandles using underlying filehandles' flush() method
504             ## (ususally IO::Handle::flush)
505             ## + also writes header file
506             ## + calls reopen() if underlying filehandles don't support a flush() method
507             sub flush {
508 5     5 1 7 my ($tied,$flushHeader) = @_;
509 5         5 my $rc = $tied->opened;
510 5         4 if (0 && $rc && UNIVERSAL::can($tied->{idxfh},'flush') && UNIVERSAL::can($tied->{datfh},'flush')) {
511             ##-- use fh flush() method
512             $rc = $tied->{idxfh}->flush() && $tied->{datfh}->flush() && (!$flushHeader || $tied->saveHeaderFile());
513             }
514             else {
515             ##-- use reopen()
516 5         8 $rc = $tied->reopen();
517             }
518 5 50       19 return $rc ? $tied : undef;
519             }
520              
521             ##--------------------------------------------------------------
522             ## Object API: file operations
523              
524             ## $tied_or_undef = $tied->unlink()
525             ## $tied_or_undef = $tied->unlink($file)
526             ## + attempts to unlink underlying files
527             ## + implicitly calls close()
528             sub unlink {
529 11     11 1 1488 my ($tied,$file) = @_;
530 11   66     50 $file //= $tied->{file};
531 11         28 $tied->close();
532 11 100       27 return undef if (!defined($file));
533 10         14 foreach ('','.idx','.hdr') {
534 30 50       1050 CORE::unlink("${file}$_") or return undef;
535             }
536 10         37 return $tied;
537             }
538              
539             ## $tied_or_undef = $tied->rename($newname)
540             ## + renames underlying file(s) using CORE::rename()
541             ## + implicitly close()s and re-open()s $tied
542             ## + object must be opened in write-mode
543             sub rename {
544 1     1 1 5 my ($tied,$newfile) = @_;
545 1         3 my $flags = fcflags($tied->{mode});
546 1         2 my $oldfile = $tied->{file};
547 1 50 33     3 return undef if (!$tied->opened() || !fcwrite($flags) || !$tied->close);
      33        
548 1         2 foreach ('','.idx','.hdr') {
549 3 50       71 CORE::rename("${oldfile}$_","${newfile}$_") or return undef;
550             }
551 1         3 return $tied->open($newfile, ($flags & ~O_TRUNC));
552             }
553              
554             ## $dst_object_or_undef = $tied_src->copy($dst_filename, %dst_opts)
555             ## $dst_object_or_undef = $tied_src->copy($dst_object)
556             ## + copies underlying file(s) using File::Copy::copy()
557             ## + source object must be opened
558             ## + implicitly calls flush() on both source and destination objects
559             ## + if a destination object is specified, it must be opened in write-mode
560             sub copy {
561 2     2 1 12 my ($src,$dst,%opts) = @_;
562 2 50 33     3 return undef if (!$src->opened || !$src->flush);
563 2 100       7 $dst = $src->new($dst, %opts, mode=>'rw') if (!ref($dst));
564 2 0 33     3 return undef if (!$dst->opened && !$dst->open($opts{file}, 'rw'));
565              
566 2         4 foreach (qw(idxfh datfh)) {
567 4 50       213 CORE::seek($src->{$_}, 0, SEEK_SET) or return undef;
568 4 50       9 CORE::seek($dst->{$_}, 0, SEEK_SET) or return undef;
569 4 50       10 File::Copy::copy($src->{$_}, $dst->{$_}) or return undef;
570             }
571 2         144 return $dst->flush();
572             }
573              
574             ## $tied_or_undef = $tied->move($newname)
575             ## + renames underlying file(s) using File::Copy::move()
576             ## + implicitly close()s and re-open()s $tied
577             ## + object must be opened in write-mode
578             sub move {
579 1     1 1 3 my ($tied,$newfile) = @_;
580 1         3 my $flags = fcflags($tied->{mode});
581 1         2 my $oldfile = $tied->{file};
582 1 50 33     3 return undef if (!$tied->opened() || !fcwrite($flags) || !$tied->close);
      33        
583 1         3 foreach ('','.idx','.hdr') {
584 3 50       99 File::Copy::move("${oldfile}$_","${newfile}$_") or return undef;
585             }
586 1         35 return $tied->open($newfile, ($flags & ~O_TRUNC));
587             }
588              
589              
590              
591             ##--------------------------------------------------------------
592             ## Object API: consolidate
593              
594             ## $tied_or_undef = $tied->consolidate()
595             ## $tied_or_undef = $tied->consolidate($tmpfile)
596             ## + consolidates file data: ensures data in $tied->{datfh} are in index-order and contain no gaps or unused blocks
597             ## + object must be opened in write-mode
598             ## + uses $tmpfile as a temporary file for consolidation (default="$tied->{file}.tmp")
599             sub consolidate {
600 1     1 1 277 my ($tied,$tmpfile) = @_;
601              
602             ##-- open tempfile
603 1   33     8 $tmpfile //= "$tied->{file}.tmp";
604             my $tmpfh = fcopen($tmpfile, $tied->{mode}, $tied->{perms})
605 1 50       3 or confess(ref($tied)."::open failed for temporary data-file $tmpfile: $!");
606 1         2 binmode($tmpfh);
607              
608             ##-- copy data
609 1         4 my ($file,$idxfh,$datfh,$len_ix,$pack_ix) = @$tied{qw(file idxfh datfh len_ix pack_ix)};
610 1         1 my ($buf,$off,$len);
611 1         4 my $size = $tied->size;
612 1 50       4 CORE::seek($idxfh, 0, SEEK_SET) or return undef;
613 1 50       3 CORE::seek($tmpfh, 0, SEEK_SET) or return undef;
614 1         3 for (my $i=0; $i < $size; ++$i) {
615 3 50       28 CORE::read($idxfh, $buf, $len_ix)==$len_ix or return undef;
616 3         4 ($off,$len) = unpack($pack_ix, $buf);
617              
618             ##-- update index record (in-place)
619 3 50       7 CORE::seek($idxfh, $i*$len_ix, SEEK_SET) or return undef;
620 3 50       10 $idxfh->print(pack($pack_ix, CORE::tell($tmpfh),$len)) or return undef;
621              
622             ##-- copy data record
623 3 50       15 next if ($len == 0);
624 3 50       6 CORE::seek($datfh, $off, SEEK_SET) or return undef;
625 3 50       8 CORE::read($datfh, $buf, $len)==$len or return undef;
626 3 50       5 $tmpfh->print($buf) or return undef;
627             }
628              
629             ##-- close data-filehandles
630 1 50       26 CORE::close($tmpfh)
631             or confess(ref($tied)."::consolidate(): failed to close temp-file '$tmpfile': $!");
632 1 50       6 CORE::close($datfh)
633             or confess(ref($tied)."::consolidate(): failed to close old data-file '$file': $!");
634              
635             ##-- replace old datafile
636 1         2 undef $tmpfh;
637 1         2 undef $datfh;
638 1         2 delete $tied->{datfh};
639 1 50       56 CORE::unlink($file)
640             or confess(ref($tied)."::consolidate(): failed to unlink old data-file '$tied->{file}': $!");
641             #CORE::rename($tmpfile, $file) ##-- win32 chokes here with "Permission denied"
642 1 50       5 File::Copy::move($tmpfile, $file)
643             or confess(ref($tied)."::consolidate(): failed to move temp-file '$tmpfile' to '$file': $!");
644              
645             ##-- re-open
646             $tied->{datfh} = fcopen("$file", (fcflags($tied->{mode}) & ~O_TRUNC), $tied->{perms})
647 1 50       100 or confess(ref($tied)."::consolidate(): failed to re-open data-file $file: $!");
648              
649 1         4 return $tied;
650             }
651              
652             ##--------------------------------------------------------------
653             ## Object API: advisory locking
654              
655             ## $bool = $tied->flock()
656             ## $bool = $tied->flock($lock)
657             ## + get an advisory lock of type $lock (default=LOCK_EX) on $tied->{datfh}, using perl's flock() function
658             ## + implicitly calls flush() prior to locking
659             sub flock {
660 0     0 1 0 my ($tied,$op) = @_;
661 0 0       0 return undef if (!$tied->opened);
662 0         0 $tied->flush();
663 0   0     0 return CORE::flock($tied->{datfh}, ($op // LOCK_EX));
664             }
665              
666             ## $bool = $tied->funlock()
667             ## $bool = $tied->funlock($lock)
668             ## + unlock $tied->{datfh} using perl's flock() function; $lock defaults to LOCK_UN
669             sub funlock {
670 0   0 0 1 0 return $_[0]->flock( LOCK_UN | ($_[1]//0) );
671             }
672              
673              
674             ##======================================================================
675             ## API: Tied Array
676              
677             ##--------------------------------------------------------------
678             ## API: Tied Array: mandatory methods
679              
680             ## $tied = tie(@array, $tieClass, $file,%opts)
681             ## $tied = TIEARRAY($tieClass, $file,%opts)
682 5     5   95 BEGIN { *TIEARRAY = \&new; }
683              
684             ## $count = $tied->FETCHSIZE()
685             ## + like scalar(@array)
686             ## + re-positions $tied->{idxfh} to eof
687 5     5   3757 BEGIN { *size = \&FETCHSIZE; }
688             sub FETCHSIZE {
689 288 50   288   12222 return undef if (!$_[0]{idxfh});
690             #return ((-s $_[0]{idxfh}) / $_[0]{len_ix}); ##-- doesn't handle recent writes correctly (probably due to perl i/o buffering)
691             ##
692 288 50       702 CORE::seek($_[0]{idxfh},0,SEEK_END) or return undef;
693 288         581 return CORE::tell($_[0]{idxfh}) / $_[0]{len_ix};
694             }
695              
696             ## $val = $tied->FETCH($index)
697             ## $val = $tied->FETCH($index)
698             sub FETCH {
699             ##-- sanity check
700 131 50   131   1451 return undef if ($_[1] >= $_[0]->size);
701              
702             ##-- get index record from $idxfh
703 131 50       177 my ($off,$len) = $_[0]->readIndex($_[1]) or return undef;
704              
705             ##-- get data record from $datfh
706 131 50       368 CORE::seek($_[0]{datfh}, $off, SEEK_SET) or return undef;
707 131         185 return $_[0]->readData($len);
708             }
709              
710             ## $val = $tied->STORE($index,$val)
711             ## + no consistency checking or optimization; just appends a new record to the end of $datfh and updates $idxfh
712             sub STORE {
713             ##-- append encoded record to $datfh
714 74 50   74   563 CORE::seek($_[0]{datfh}, 0, SEEK_END) or return undef;
715 74         76 my $off0 = CORE::tell($_[0]{datfh});
716 74 50       136 $_[0]->writeData($_[2]) or return undef;
717 74         1010 my $off1 = CORE::tell($_[0]{datfh});
718              
719             ##-- update index record in $idxfh
720 74 50       126 $_[0]->writeIndex($_[1], $off0, ($off1-$off0)) or return undef;
721              
722             ##-- return
723 74         169 return $_[2];
724             }
725              
726             ## $count = $tied->STORESIZE($count)
727             ## $count = $tied->STORESIZE($count) ##-- local extension
728             ## + modifies only $idxfh
729             sub STORESIZE {
730 0     0   0 my $oldsize = $_[0]->size;
731 0 0       0 if ($_[1] < $oldsize) {
    0          
732             ##-- shrink
733 0 0       0 CORE::truncate($_[0]{idxfh}, $_[1]*$_[0]{len_ix}) or return undef;
734             } elsif ($_[1] > $oldsize) {
735             ##-- grow (idxfh only)
736 0 0       0 CORE::seek($_[0]{idxfh}, $_[1]*$_[0]{len_ix}-1, SEEK_SET) or return undef;
737 0         0 $_[0]{idxfh}->print("\0");
738             }
739 0         0 return $_[1];
740             }
741              
742             ## $bool = $tied->EXISTS($index)
743             sub EXISTS {
744 0     0   0 return $_[1] < $_[0]->size;
745             }
746              
747             ## undef = $tied->DELETE($index)
748             ## + really just wraps $tied->STORE($index,undef)
749             sub DELETE {
750 0     0   0 return $_[0]->STORE($_[1],undef);
751             }
752              
753             ##--------------------------------------------------------------
754             ## API: Tied Array: optional methods
755              
756             ## undef = $tied->CLEAR()
757             sub CLEAR {
758 10 50   10   261 CORE::truncate($_[0]{idxfh}, 0) or return undef;
759 10 50       109 CORE::truncate($_[0]{datfh}, 0) or return undef;
760 10         61 return $_[0];
761             }
762              
763             ## $newsize = $tied->PUSH(@vals)
764             sub PUSH {
765 1     1   2 my $tied = shift;
766              
767 1 50       5 CORE::seek($tied->{datfh}, 0, SEEK_END) or return undef;
768 1 50       4 CORE::seek($tied->{idxfh}, 0, SEEK_END) or return undef;
769 1         1 my ($off0,$off1);
770 1         2 foreach (@_) {
771 1         1 my $off0 = CORE::tell($tied->{datfh});
772 1 50       2 $tied->writeData($_) or return undef;
773 1         7 my $off1 = CORE::tell($tied->{datfh});
774              
775             ##-- update index record in $idxfh
776 1 50       3 $tied->writeIndex(undef, $off0, ($off1-$off0)) or return undef;
777             }
778              
779 1 50       4 return $tied->size if (defined(wantarray));
780             }
781              
782             ## $val = $tied->POP()
783             ## + truncates data-file if we're popping the final data-record
784             sub POP {
785 1 50   1   4 return undef if (!(my $size=$_[0]->size));
786              
787             ##-- get final index record (& truncate it)
788 1 50       4 my ($off,$len) = $_[0]->readIndex($size-1) or return undef;
789 1 50       23 CORE::truncate($_[0]{idxfh}, ($size-1)*$_[0]{len_ix}) or return undef;
790              
791             ##-- get corresponding data-record
792 1 50       4 CORE::seek($_[0]{datfh}, $off, SEEK_SET) or return undef;
793 1         2 my $val = $_[0]->readData($len);
794              
795             ##-- maybe trim data-file
796 1 50       21 CORE::truncate($_[0]{datfh}, $off) if (($off+$len) == (-s $_[0]{datfh}));
797 1         4 return $val;
798             }
799              
800             ## $val = $tied->SHIFT()
801             ## + truncates data-file if we're shifting the final data-record
802             sub SHIFT {
803             ##-- get first index record
804 1 50   1   5 my ($off,$len) = $_[0]->readIndex(0) or return undef;
805              
806             ##-- defer to SPLICE
807 1         4 my $val = $_[0]->SPLICE(0,1);
808              
809             ##-- maybe trim data-file
810 1 50       8 CORE::truncate($_[0]{datfh}, $off) if (($off+$len) == (-s $_[0]{datfh}));
811 1         4 return $val;
812             }
813              
814             ## @removed = $tied->SPLICE($offset, $length, @newvals)
815             ## $last_removed = $tied->SPLICE($offset, $length, @newvals)
816             sub SPLICE {
817 4     4   9 my $tied = shift;
818 4         7 my $size = $tied->size();
819 4 50       8 my $off = (@_) ? shift : 0;
820 4 50       6 $off += $size if ($off < 0);
821 4 50       7 my $len = (@_) ? shift : ($size-$off);
822 4 50       6 $len += $size-$off if ($len < 0);
823              
824             ##-- get result-list
825 4         3 my ($i,@result);
826 4 100       8 if (wantarray) {
    50          
827 3         6 for ($i=$off; $i <= $len; ++$i) {
828 4         7 push(@result, $tied->FETCH($i));
829             }
830             } elsif ($len > 0) {
831 1         2 @result = ($tied->FETCH($off+$len-1));
832             }
833              
834             ##-- shift post-splice index records (expensive, but generally not so bad as default Tie::Array iterated FETCH()+STORE())
835 4         4 my $shift = scalar(@_) - $len;
836 4 50       13 $tied->shiftIndex($off+$len, $size-($off+$len), $shift) if ($shift != 0);
837              
838             ##-- store new values
839 4         9 for ($i=0; $i < @_; ++$i) {
840 4         7 $tied->STORE($off+$i, $_[$i]);
841             }
842              
843             ##-- maybe shrink array
844 4 100 50     66 CORE::truncate($tied->{idxfh}, ($size+$shift)*$tied->{len_ix}) or return undef if ($shift < 0);
845              
846             ##-- return
847 4 100       28 return wantarray ? @result : $result[0];
848             }
849              
850             ## @vals = $tied->UNSHIFT(@vals)
851             ## + just defers to SPLICE
852             sub UNSHIFT {
853 0     0     return scalar shift->SPLICE(0,0,@_);
854             }
855              
856             ## ? = $tied->EXTEND($newcount)
857              
858             1; ##-- be happpy