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   26929 use 5.10.0; ##-- for // operator
  5         10  
9 5     5   2085 use Tie::Array;
  5         4069  
  5         98  
10 5     5   2840 use JSON qw();
  5         48647  
  5         115  
11 5     5   24 use Fcntl qw(:DEFAULT :seek :flock);
  5         5  
  5         1592  
12 5     5   2085 use File::Copy qw();
  5         10431  
  5         91  
13 5     5   1923 use IO::File;
  5         30841  
  5         914  
14 5     5   24 use Carp qw(confess);
  5         6  
  5         157  
15 5     5   15 use strict;
  5         5  
  5         4074  
16              
17             ##======================================================================
18             ## Globals
19              
20             our @ISA = qw(Tie::Array);
21             our $VERSION = '0.08';
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 443 my $that = shift;
51 17 50       49 my $file = (@_ % 2)==0 ? undef : shift;
52 17         38 my %opts = @_;
53 17   66     51 my $tied = bless({
54             $that->defaults(),
55             file => $file,
56             @_,
57             }, ref($that)||$that);
58 17 50       95 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 186 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   1617 $_[0]->unlink() if ($_[0]{temp});
80 16         36 $_[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 865 shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
93 474         353 my $mode = shift;
94 474   50     863 $mode //= 'r';
95 474 100       1197 return $mode if ($mode =~ /^[0-9]+$/); ##-- numeric mode is interpreted as Fcntl bitmask
96 17         40 my $fread = $mode =~ /[r<]/;
97 17         26 my $fwrite = $mode =~ /[wa>\+]/;
98 17   66     58 my $fappend = ($mode =~ /[a]/ || $mode =~ />>/);
99 17 100       43 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     59 $flags |= O_TRUNC if ($fwrite && !$fappend);
104 17         34 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 187 shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
119 80         86 my $flags = fcflags(shift);
120 80   33     264 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 413 shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
127 135         144 my $flags = fcflags(shift);
128 135   66     600 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 262 shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
135 122         119 my $flags = fcflags(shift);
136 122         301 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 130 shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
150 54         67 my $flags = fcflags(shift);
151 54 100       67 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 176 shift if (UNIVERSAL::isa($_[0],__PACKAGE__));
168 54         64 my ($file,$flags,$perms) = @_;
169 54         61 $flags = fcflags($flags);
170 54   33     78 $perms //= (0666 & ~umask);
171 54         61 my $mode = fcperl($flags);
172              
173 54         53 my ($sysfh);
174 54 50       68 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       1987 sysopen($sysfh, $file, $flags, $perms) or return undef;
181             }
182              
183             ##-- now open perl-fh from system fh
184 54 50       551 open(my $fh, "${mode}&=", fileno($sysfh)) or return undef;
185 54 100 100     72 if (fcwrite($flags) && !fctrunc($flags)) {
186             ##-- append mode: seek to end of file
187 21 50       51 seek($fh, 0, SEEK_END) or return undef;
188             }
189 54         285 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   2466 use bytes; ##-- deprecated in perl v5.18.2
  5         36  
  5         34  
200 5     5   122 no warnings;
  5         5  
  5         10785  
201 52     52 0 214 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 37 my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
214 13 50       20 my $bufr = ref($_[0]) ? $_[0] : \$_[0];
215 13         29 my %opts = @_[1..$#_];
216 13 50       25 return $opts{json}->decode($$bufr) if ($opts{json});
217 13         67 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 33 my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
223 13         11 my $file = shift;
224 13 50       62 my $fh = ref($file) ? $file : IO::File->new("<$file");
225 13 50       565 return undef if (!$fh);
226 13         39 binmode($fh,':raw');
227 13         37 local $/=undef;
228 13         131 my $buf = <$fh>;
229 13 50       64 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 313 my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
239 35         42 my $data = shift;
240 35         44 my %opts = @_;
241 35 50       61 return $opts{json}->encode($data) if ($opts{json});
242 35         156 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 63 my $that = UNIVERSAL::isa($_[0],__PACKAGE__) ? shift : __PACKAGE__;
248 23         20 my $data = shift;
249 23         23 my $file = shift;
250 23 50       148 my $fh = ref($file) ? $file : IO::File->new(">$file");
251 23 50 0     1807 logconfess((ref($that)||$that)."::saveJsonFile() failed to open file '$file': $!") if (!$fh);
252 23         86 binmode($fh,':raw');
253 23 50       72 $fh->print($that->saveJsonString($data,@_)) or return undef;
254 23 50       1180 if (!ref($file)) { close($fh) || return undef; }
  23 50       610  
255 23         107 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 358 CORE::read($_[0]{datfh}, my $buf, $_[1])==$_[1] or return undef;
311 106         268 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 513 !defined($_[1]) or CORE::seek($_[0]{idxfh}, $_[1]*$_[0]{len_ix}, SEEK_SET) or return qw();
325 133 50       553 CORE::read($_[0]{idxfh}, my $buf, $_[0]{len_ix})==$_[0]{len_ix} or return qw();
326 133         386 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 666 !defined($_[1]) or CORE::seek($_[0]{idxfh}, $_[1]*$_[0]{len_ix}, SEEK_SET) or return undef;
336 75 50       275 $_[0]{idxfh}->print(pack($_[0]{pack_ix}, $_[2], $_[3])) or return undef;
337 75         397 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 5 my ($tied,$start,$n,$shift) = @_;
346              
347             ##-- common variables
348 4         4 my $idxfh = $tied->{idxfh};
349 4         5 my $len_ix = $tied->{len_ix};
350 4   50     11 my $bsize = $tied->{bsize} // 2**21;
351 4         9 my $bstart = $len_ix * $start;
352 4         5 my $bn = $len_ix * $n;
353 4         2 my $bshift = $len_ix * $shift;
354 4         3 my ($buf,$boff,$blen);
355              
356             ##-- dispatch by shift direction
357 4 100       7 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         4 while ($bn > 0) {
361 2 50       4 $blen = $bn > $bsize ? $bsize : $bn;
362 2 50       7 CORE::seek($idxfh, -$blen, SEEK_CUR) or return undef;
363 2 50       7 CORE::read($idxfh, $buf, $blen)==$blen or return undef;
364 2 50       6 CORE::seek($idxfh, $bshift-$blen, SEEK_CUR) or return undef;
365 2 50       4 $idxfh->print($buf) or return undef;
366 2 50       27 CORE::seek($idxfh, -$bshift, SEEK_CUR) or return undef;
367 2         4 $bn -= $blen;
368             }
369             } else {
370             ##-- shift left (copy left-to-right)
371 2 50       5 CORE::seek($tied->{idxfh}, $bstart, SEEK_SET) or return undef;
372 2         5 while ($bn > 0) {
373 2 50       4 $blen = $bn > $bsize ? $bsize : $bn;
374 2 50       8 CORE::read($idxfh, $buf, $blen)==$blen or return undef;
375 2 50       5 CORE::seek($idxfh, $bshift-$blen, SEEK_CUR) or return undef;
376 2 50       6 $idxfh->print($buf) or return undef;
377 2 50       25 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 41 return grep {!ref($_[0]{$_}) && $_ !~ m{^(?:file|mode|perms)$}} keys %{$_[0]};
  291         804  
  23         92  
397             }
398              
399             ## \%header = $tied->headerData()
400             ## + data to save as header
401             sub headerData {
402 23     23 1 18 my $tied = shift;
403 23         44 return {(map {($_=>$tied->{$_})} $tied->headerKeys), class=>ref($tied)};
  175         306  
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 20 my ($tied,$hfile,%opts) = @_;
412 13 50 33     53 $hfile //= $tied->{file}.".hdr" if (defined($tied->{file}));
413 13 50       19 confess(ref($tied)."::loadHeader(): no header-file specified and no 'file' attribute defined") if (!defined($hfile));
414 13 50       28 my $hdata = $tied->loadJsonFile($hfile,%opts)
415             or confess(ref($tied)."::loadHeader(): failed to load header data from '$hfile'");
416 13         420 @$tied{keys %$hdata} = values %$hdata;
417 13         63 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     105 $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         45 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 32 my ($tied,$file,$mode) = @_;
440 26   66     75 $file //= $tied->{file};
441 26   66     65 $mode //= $tied->{mode};
442 26 50       49 $tied->close() if ($tied->opened);
443 26         34 $tied->{file} = $file;
444 26         50 $tied->{mode} = $mode = fcflags($mode);
445              
446 26 100 66     50 if (fcread($mode) && !fctrunc($mode)) {
447 13 50 33     165 (!-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       74 or confess(ref($tied)."::open failed for index-file $file.idx: $!");
454             $tied->{datfh} = fcopen("$file", $mode, $tied->{perms})
455 26 50       58 or confess(ref($tied)."::open failed for data-file $file: $!");
456 26         104 binmode($_) foreach (@$tied{qw(idxfh datfh)});
457              
458             ##-- pack lengths
459             #use bytes; ##-- deprecated in perl v5.18.2
460 26         43 $tied->{len_o} = packsize($tied->{pack_o});
461 26         2303 $tied->{len_l} = packsize($tied->{pack_l});
462 26         72 $tied->{len_ix} = $tied->{len_o} + $tied->{len_l};
463 26         39 $tied->{pack_ix} = $tied->{pack_o}.$tied->{pack_l};
464              
465 26         107 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 37 my $tied = shift;
472 36 100       56 return $tied if (!$tied->opened);
473 25 100 66     38 if ($tied->opened && fcwrite($tied->{mode})) {
474 23 50       53 $tied->saveHeader() or
475             confess(ref($tied)."::close(): failed to save header file");
476             }
477 25         315 delete @$tied{qw(idxfh datfh)}; ##-- should auto-close if not shared
478 25         36 undef $tied->{file};
479 25         120 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 6 my $tied = shift;
487 7         12 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 97 my $tied = shift;
495             return (ref($tied)
496             && defined($tied->{idxfh})
497             && defined($tied->{datfh})
498 105   66     700 );
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 6 my ($tied,$flushHeader) = @_;
509 5         9 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         10 $rc = $tied->reopen();
517             }
518 5 50       26 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 1642 my ($tied,$file) = @_;
530 11   66     51 $file //= $tied->{file};
531 11         29 $tied->close();
532 11 100       27 return undef if (!defined($file));
533 10         20 foreach ('','.idx','.hdr') {
534 30 50       1150 CORE::unlink("${file}$_") or return undef;
535             }
536 10         42 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 8 my ($tied,$newfile) = @_;
545 1         3 my $flags = fcflags($tied->{mode});
546 1         3 my $oldfile = $tied->{file};
547 1 50 33     4 return undef if (!$tied->opened() || !fcwrite($flags) || !$tied->close);
      33        
548 1         3 foreach ('','.idx','.hdr') {
549 3 50       73 CORE::rename("${oldfile}$_","${newfile}$_") or return undef;
550             }
551 1         4 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       11 $dst = $src->new($dst, %opts, mode=>'rw') if (!ref($dst));
564 2 0 33     4 return undef if (!$dst->opened && !$dst->open($opts{file}, 'rw'));
565              
566 2         4 foreach (qw(idxfh datfh)) {
567 4 50       247 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       12 File::Copy::copy($src->{$_}, $dst->{$_}) or return undef;
570             }
571 2         233 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 2 my ($tied,$newfile) = @_;
580 1         3 my $flags = fcflags($tied->{mode});
581 1         3 my $oldfile = $tied->{file};
582 1 50 33     3 return undef if (!$tied->opened() || !fcwrite($flags) || !$tied->close);
      33        
583 1         4 foreach ('','.idx','.hdr') {
584 3 50       104 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 284 my ($tied,$tmpfile) = @_;
601              
602             ##-- open tempfile
603 1   33     7 $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         5 my $size = $tied->size;
612 1 50       4 CORE::seek($idxfh, 0, SEEK_SET) or return undef;
613 1 50       2 CORE::seek($tmpfh, 0, SEEK_SET) or return undef;
614 1         4 for (my $i=0; $i < $size; ++$i) {
615 3 50       28 CORE::read($idxfh, $buf, $len_ix)==$len_ix or return undef;
616 3         5 ($off,$len) = unpack($pack_ix, $buf);
617              
618             ##-- update index record (in-place)
619 3 50       8 CORE::seek($idxfh, $i*$len_ix, SEEK_SET) or return undef;
620 3 50       9 $idxfh->print(pack($pack_ix, CORE::tell($tmpfh),$len)) or return undef;
621              
622             ##-- copy data record
623 3 50       16 next if ($len == 0);
624 3 50       7 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       6 $tmpfh->print($buf) or return undef;
627             }
628              
629             ##-- close data-filehandles
630 1 50       27 CORE::close($tmpfh)
631             or confess(ref($tied)."::consolidate(): failed to close temp-file '$tmpfile': $!");
632 1 50       5 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         1 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       4 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       95 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   87 BEGIN { *TIEARRAY = \&new; }
683              
684             ## $count = $tied->FETCHSIZE()
685             ## + like scalar(@array)
686             ## + re-positions $tied->{idxfh} to eof
687 5     5   3652 BEGIN { *size = \&FETCHSIZE; }
688             sub FETCHSIZE {
689 288 50   288   12631 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       708 CORE::seek($_[0]{idxfh},0,SEEK_END) or return undef;
693 288         583 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   1505 return undef if ($_[1] >= $_[0]->size);
701              
702             ##-- get index record from $idxfh
703 131 50       190 my ($off,$len) = $_[0]->readIndex($_[1]) or return undef;
704              
705             ##-- get data record from $datfh
706 131 50       404 CORE::seek($_[0]{datfh}, $off, SEEK_SET) or return undef;
707 131         190 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   546 CORE::seek($_[0]{datfh}, 0, SEEK_END) or return undef;
715 74         86 my $off0 = CORE::tell($_[0]{datfh});
716 74 50       127 $_[0]->writeData($_[2]) or return undef;
717 74         987 my $off1 = CORE::tell($_[0]{datfh});
718              
719             ##-- update index record in $idxfh
720 74 50       129 $_[0]->writeIndex($_[1], $off0, ($off1-$off0)) or return undef;
721              
722             ##-- return
723 74         167 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   251 CORE::truncate($_[0]{idxfh}, 0) or return undef;
759 10 50       124 CORE::truncate($_[0]{datfh}, 0) or return undef;
760 10         69 return $_[0];
761             }
762              
763             ## $newsize = $tied->PUSH(@vals)
764             sub PUSH {
765 1     1   1 my $tied = shift;
766              
767 1 50       6 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         2 my $off0 = CORE::tell($tied->{datfh});
772 1 50       2 $tied->writeData($_) or return undef;
773 1         8 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       5 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   3 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         22 my $val = $_[0]->readData($len);
794              
795             ##-- maybe trim data-file
796 1 50       45 CORE::truncate($_[0]{datfh}, $off) if (($off+$len) == (-s $_[0]{datfh}));
797 1         5 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   4 my ($off,$len) = $_[0]->readIndex(0) or return undef;
805              
806             ##-- defer to SPLICE
807 1         3 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   10 my $tied = shift;
818 4         5 my $size = $tied->size();
819 4 50       7 my $off = (@_) ? shift : 0;
820 4 50       8 $off += $size if ($off < 0);
821 4 50       5 my $len = (@_) ? shift : ($size-$off);
822 4 50       5 $len += $size-$off if ($len < 0);
823              
824             ##-- get result-list
825 4         4 my ($i,@result);
826 4 100       7 if (wantarray) {
    50          
827 3         7 for ($i=$off; $i <= $len; ++$i) {
828 4         6 push(@result, $tied->FETCH($i));
829             }
830             } elsif ($len > 0) {
831 1         3 @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         5 my $shift = scalar(@_) - $len;
836 4 50       12 $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     53 CORE::truncate($tied->{idxfh}, ($size+$shift)*$tied->{len_ix}) or return undef if ($shift < 0);
845              
846             ##-- return
847 4 100       26 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