File Coverage

blib/lib/File/Value.pm
Criterion Covered Total %
statement 81 146 55.4
branch 47 114 41.2
condition 25 55 45.4
subroutine 11 18 61.1
pod 6 10 60.0
total 170 343 49.5


line stmt bran cond sub pod time code
1             # To do: change default behavior and add options:
2             # snag foo -> defaults to:
3             # --mknextcopy for file if it exists, else snag_file
4             # --mknext for dir if it exists, else snag_dir
5             # add options: --noversion, --dir (default is 'file'), --nocopy
6             # Add list of regexs and substitutions for versions, esp. so that
7             # after foo.txt comes foo_1.txt instead of foo.txt_1; order is preference:
8             # qr/[-_]?(\d+)(\.[a-z]*)?$/, etc.
9             # also check for case of foo_\d{4,6} which is a date be prepared to use
10             # today's date for next version
11             #
12             package File::Value;
13              
14 3     3   85134 use 5.006;
  3         11  
  3         139  
15 3     3   16 use warnings;
  3         6  
  3         85  
16 3     3   19 use strict;
  3         9  
  3         4000  
17              
18             our $VERSION;
19             $VERSION = sprintf "%d.%02d", q$Name: Release-1-02 $ =~ /Release-(\d+)-(\d+)/;
20              
21             require Exporter;
22             our @ISA = qw(Exporter);
23              
24             our @EXPORT = qw();
25             our @EXPORT_OK = qw(
26             file_value
27             flvl
28             list_high_version
29             list_low_version
30             snag_dir
31             snag_file
32             snag_version
33             fiso_dname
34             fiso_uname
35             );
36             our %EXPORT_TAGS = (all => [ @EXPORT_OK ]);
37              
38             # if length is 0, go for it.
39             #
40             my $ridiculous = 4294967296; # max length is 2**32 yyy a better way?
41              
42             # returns empty string on success, else error message
43 19     19 1 2637338 sub file_value { my( $file, $value, $how, $length )=@_;
44              
45 19         56 my $ret_value = \$_[1];
46              
47 19 50       74 $file or return "must specify a file name";
48              
49             # Force caller to be explicit about whether doing read/write/append.
50             #
51 19 100       165 $file !~ /^\s*(<|>|>>)\s*(\S.*)/ and
52             return "file ($file) must begin with '<', '>', or '>>'";
53 18         121 my ($mode, $statfname) = ($1, $2);
54              
55 18   100     102 $how ||= "trim"; # trim (def), raw, untaint
56 18         37 $how = lc($how);
57 18 100 100     105 $how ne "trim" && $how ne "raw" && $how ne "untaint" and
      100        
58             return "third arg ($how) must be one of: trim, raw, or untaint";
59              
60 17 100       86 if ($mode =~ />>?/) {
61             # If we get here, we're doing value-to-file case (> or >>).
62              
63             # ignore $how and $length, but we need specified $value
64 5 50       20 defined($value) or
65             return "needs a value to put in '$file'";
66             # for $how, here we only understand "trim" or "raw", but
67             # "trim" removes initial and final \n, adding one final \n
68 5 100       21 if ($how eq "trim") {
69 4         15 $value =~ s/^\s+//s;
70 4         2023 $value =~ s/\s+$/\n/s;
71             }
72 5 50       3640 open(OUT, $file) or
73             return "$statfname: $!";
74 5         1564 my $r = print OUT $value;
75 5         896 close(OUT);
76 5 50       54 return ($r ? "" : "write failed: $!");
77             }
78              
79             # If we get here, we're doing file-to-value case.
80              
81 12 100 100     59 my $go_for_it = (defined($length) && $length eq "0" ? 1 : 0);
82 12         18 my $statlength = undef;
83              
84 12 100       47 if (defined($length)) {
    50          
85 3 50       18 $length !~ /^\d+$/ and
86             return "length unspecified or not an integer";
87             }
88             elsif ($statfname ne "-") {
89             # yyy tested with "-" (stdin or stdout)?
90             # no length means read whole file, but be reasonable
91 9         245 $statlength = (-s $statfname);
92 9 50       80 ! defined($statlength) and
93             return "$statfname: $!";
94 9 50       45 $length = ($statlength > $ridiculous
95             ? $ridiculous : $statlength);
96             }
97             else {
98 0         0 $length = $ridiculous;
99             }
100              
101 12 50       476 open(IN, $file) or
102             return "$statfname: $!";
103 12 100       34 if ($go_for_it) { # don't be reasonable about length
104 1         6 local $/;
105 1         24 $$ret_value = ;
106 1         16 close(IN);
107             }
108             else {
109 11         21 my ($offset, $n) = (0, 0);
110             # Ask for the whole $length but be prepared not to get it.
111 11   66     947 1 while ($n = read(IN, $$ret_value, $length, $offset))
      100        
112             # yyy and print("offset=$offset\n")
113             and $n > 0 and ($offset += $n) < $length;
114 11         135 close(IN);
115 11 50       44 ! defined($n) and
116             return "$statfname: failed to read $length bytes: $!";
117 11 50 66     71 return "$statfname: read fewer bytes than expected"
118             if (defined($statlength) && $offset < $statlength);
119             #if (defined($statlength) && $n < $statlength);
120             }
121              
122 12 100       41 if ($how eq "trim") {
    100          
123 8         42 $$ret_value =~ s/^\s+//;
124 8         32 $$ret_value =~ s/\s+$//;
125             }
126             elsif ($how eq "untaint") {
127 1 50       7 if ($$ret_value =~ /([-\@\w.]+)/) {
128 1         4 $$ret_value = $1;
129             }
130             }
131             # elsif ($how eq "raw") { then no further processing }
132              
133 12         51 return "";
134             }
135              
136             # Faster simpler cruder version of file_value(), just in case.
137             #
138 0     0 0 0 sub flvl { my( $file, $value )=@_; # $file must begin with >, <, or >>
139              
140 0 0       0 if ($file =~ /^\s*>>?/) {
141 0 0       0 open(OUT, $file) or return "$file: $!";
142 0         0 my $r = print OUT $value;
143 0 0       0 close(OUT); return ($r ? '' : "write failed: $!");
  0         0  
144             }
145              
146             # If we get here, we're doing file-to-value case.
147 0 0       0 open(IN, $file) or return "$file: $!";
148 0         0 local $/; $_[1] = ; # slurp mode (entire file)
  0         0  
149 0         0 close(IN); return '';
  0         0  
150             }
151              
152 3     3   25 use File::Spec;
  3         8  
  3         1560  
153              
154             our $Win; # whether we're running on Windows
155             defined($Win) or # if we're on a Windows platform avoid -l
156             $Win = grep(/Win32|OS2/i, @File::Spec::ISA);
157             our ($B, $C); # short names for boundary char for output (B) and regexes (C)
158             #our $B = $Win ? '\\\\' : '/'; # short name for boundary (\) in regexes
159             $Win and
160             $B = '\\',
161             $C = $B.$B,
162             1
163             or
164             $B = $C = '/';
165             ;
166             #$B = '\\';
167             #$C = $B.$B; #
168              
169             # Return the full normalized name of a filesystem-based object that can
170             # legitimately be specified either by its enclosing directory name or
171             # by the directory name plus something in that directory. Useful when
172             # a user could correctly say 'myobj' or 'myobj/minter.bdb' but we will
173             # eventually need the longer form to test existence or create the object.
174             #
175             # It's important (eg, find_minder) that existence of enclosing directory
176             # not be mistaken for existence of the object, since creating that dir
177             # by itself (eg, with 'snag') could be a good way of reserving the minder
178             # that you are _about_ to create.
179             #
180             # Need this when the user will be specifying the location of an object
181             # that's defined by both an enclosing directory $base and a well-known
182             # file or directory $last under $base. It is valid for the user to say
183             # either $base/$last or $base, but an existence test against $base/$last
184             # is what we need to perform to verify whether it's a legitimate object,
185             # with some tedious special cases when $base is '/' or '.'. Typical use
186             # cases: user specifies "pairtree_root", "ptname", "ptname/pairtree_root",
187             # "mintername/", or "mintername/minter.bdb".
188             #
189             # Lengthen base path with last component according to the table, normalizing
190             # multiple slashes between $base and $last. Useful to get tedious details
191             # right and when path may already have the last component on it (in which
192             # case we don't want it there twice). Removes final slashes from $last.
193             #
194             # $base $last Returns
195             # 1. / bar /bar
196             # 2. . bar bar
197             # 3. foo bar foo/bar
198             # 4. foo/ bar foo/bar
199             # 5. foo/bar bar foo/bar
200             # 6. bar bar bar
201             # 7. "" bar bar
202             # 8. foo "" foo
203             #
204             # yyy might not port to Windows for the root directory cases
205             # yyy was called prep_file in pt script
206             # FISO = FIle System Object
207             # dname = "down" name (full name with descender, suitable for -X tests)
208             # uname = "up" name (upper name without descender, still unique, suitable
209             # for communicating with users)
210 14     14 0 3684 sub fiso_dname { my( $base, $last )=@_;
211              
212 14 100 100     78 return (($base || "") . ($last || ""))
      100        
      100        
213             unless ($base and $last); # cases 7-8 done
214 12         86 $last =~ s{${C}*(.*)${C}*$}{$1}o; # remove bounding slashes
215 12 100       228 return "${B}$last" if $base =~ m{^${C}+$}o; # case 1 done
216 9         33 $base =~ s{${C}+$}{}o; # remove trailing slashes
217 9 100       37 return "$last" if $base =~ m{^\.${C}*$}o; # case 2 done
218 7 100       52 return "$base${B}$last" if $base !~ m{$last$}; # cases 3-4 gone
219 4 100       25 return "$last" if $base =~ m{^$last$}; # case 6 eliminated
220 3         19 $base =~ s{${C}*$last$}{}; # remove $last and preceding slashes
221 3         18 return "$base${B}$last"; # case 5 done
222             }
223              
224             # Return parent of $oname (object name) with trailing slash intact.
225             # xxx This needs to work closely with fiso_dname
226             #
227 8     8 0 13 sub fiso_uname { my( $oname )=@_;
228              
229 8 100       21 return "" unless $oname;
230 6 100       33 return "${B}" if $oname =~ m,^${C}+$,o;
231 4 50       17 return ".${B}" if $oname =~ m,^\.${C}*$,o;
232             # next assumes $oname was a directory?
233             # final / means this fiso_dname was a dir? yyy
234 4         26 $oname =~ s,[^${C}]+${C}*$,,o;
235 4 100       16 return ".${B}" if $oname =~ m,^$,;
236 2         9 return $oname;
237             }
238              
239 3     3   17 use File::Glob ':glob'; # standard use of module, which we need
  3         4  
  3         2770  
240             # as vanilla glob won't match whitespace
241              
242 0     0 1   sub list_high_version { my( $template )=@_;
243              
244 0   0       $template ||= "";
245 0           my ($max, $n, $fname) = (-1, -1, undef);
246 0           for (grep( /$template\d+$/, bsd_glob("$template*") )) {
247 0           ($n) = m/(\d+)$/; # extract number for comparisons
248 0 0         $max < $n and
249             ($max, $fname) = ($n, $_);
250             }
251 0           return ($max, $fname);
252             }
253              
254             # Tempting for program maintenance to combine computation of low and high,
255             # but that would make computation of the high (by far the most common call)
256             # pay a performance penalty for a seldom used value. So we separate.
257             #
258 0     0 1   sub list_low_version { my( $template )=@_;
259              
260 0   0       $template ||= "";
261 0           my ($min, $n, $fname) = (-1, -1, undef);
262 0           for (grep( /$template\d+$/, bsd_glob("$template*") )) {
263 0           ($n) = m/(\d+)$/; # extract number for comparisons
264 0 0 0       $min > $n || $min < 0 and
265             ($min, $fname) = ($n, $_);
266             }
267 0           return ($min, $fname);
268             }
269              
270             sub snag_opt_defaults { return {
271              
272 0     0 0   as_dir => undef, # create node as dir (default is to
273             # infer it from argument if possible)
274             no_type_mismatch => 0, # complain if mismatch (default don't)
275             mknextcopy => 0, # copy unnumbered file (default don't)
276             };
277             }
278              
279 0     0 1   sub snag_version { my( $template, $opt ) = (shift, shift);
280              
281 0 0         return (-1, "no base name given") if ! defined $template;
282 0   0       $opt ||= snag_opt_defaults();
283 0           my $as_dir = $opt->{as_dir}; # might be undefined (ok)
284              
285             # Carve off terminal digit string $digits ("1" default).
286 0 0         my $digits = ($template =~ s/(\d+)$//)
287             ? $1 : "1";
288 0           my $vfmt = '%0'. length($digits) . 'd'; # version number format
289              
290 0 0 0       $opt->{mknextcopy} and -f $template || # pre-condition of mknextcopy
291             return (-1, "mknextcopy only works if '$template' is a "
292             . "pre-existing file");
293              
294 0   0       $opt->{no_type_mismatch} ||= 0; # default this option to false
295 0           my ($hnum, $hnode) = list_high_version($template);
296 0 0         if ($hnum == -1) {
297 0   0       $as_dir ||= 0; # default type here is "file"
298 0           $hnum = $digits - 1; # number we'll start trying for
299             }
300             else { # current high is in $hnode
301 0           my $isa_dir = -d $hnode;
302              
303             # Note that ||= can't be used to set default if 0 (or "")
304             # is a valid specified value, as for $as_dir below.
305             #
306 0 0         if (defined $as_dir) {
307 0 0 0       $opt->{no_type_mismatch} and # if intolerant and
    0 0        
    0          
308             ($isa_dir xor $as_dir) and # a mismatch
309             return (-1, "high version ($hnode" .
310             ($isa_dir ? "/" : "") . ") is a " .
311             ($isa_dir ? "directory" : "file") .
312             ", different from type requested");
313             }
314             else {
315 0           $as_dir = $isa_dir; # infer type from $hnode
316             }
317             }
318              
319 3     3   18 use constant MAX_TRIES => 3; # number of races we might run
  3         8  
  3         688  
320 0           my $hmax = $hnum + MAX_TRIES;
321 0           my ($try, $msg);
322 0           while (++$hnum <= $hmax) {
323 0           $try = $template . sprintf($vfmt, $hnum);
324 0 0         $msg = $as_dir ?
325             snag_dir($try) : snag_file($try);
326 0 0         last if ! $msg; # success
327             #return ($hnum, $try) if ! $msg; # success
328 0 0         return (-1, $msg) if $msg ne "1"; # error, not race
329             }
330 0 0         $msg and return (-1, "gave up after " . MAX_TRIES . " races lost");
331              
332             # If we get here, $try contains the node that we snagged.
333 0 0         if ($opt->{mknextcopy}) {
334 0 0         return (-1, "mknextcopy only works if '$try' is a file")
335             unless -f $try; # re-check pre-condition
336 3     3   2900 use File::Copy;
  3         24375  
  3         658  
337 0 0         copy($template, $try) or
338             return (-1, "mknextcopy failed: $!");
339             }
340 0           return ($hnum, $try);
341             }
342              
343 0     0 1   sub snag_dir { my( $node )=@_;
344              
345 0 0         return "snag_dir: no directory given" if ! defined $node;
346 0 0         return "" if mkdir($node);
347 0 0         return "1" if -e $node; # potential race condition detector
348 0           return $!; # else return system error message
349             }
350              
351 3     3   26 use Fcntl;
  3         116  
  3         1234  
352 0     0 1   sub snag_file { my( $node )=@_;
353              
354 0 0         return "snag_file: no file given" if ! defined $node;
355 0 0 0       return ""
      0        
356             # Create, but don't succeed if it doesn't exist (O_EXCL).
357             # Return success even if for some reason the close fails.
358             if sysopen(FH, $node, O_CREAT | O_EXCL)
359             && (close(FH) || 1);
360 0 0         return "1" if -e $node; # potential race condition detector
361 0           return $!; # else return system error message
362             }
363              
364             1; # End of File::Value
365              
366             __END__