File Coverage

blib/lib/File/Strfile.pm
Criterion Covered Total %
statement 132 146 90.4
branch 42 60 70.0
condition 10 14 71.4
subroutine 17 18 94.4
pod 9 9 100.0
total 210 247 85.0


line stmt bran cond sub pod time code
1             package File::Strfile;
2             our $VERSION = '0.03';
3 6     6   891338 use 5.016;
  6         22  
4 6     6   51 use strict;
  6         43  
  6         195  
5 6     6   56 use warnings;
  6         27  
  6         647  
6              
7             require Exporter;
8             our @ISA = qw(Exporter);
9             our @EXPORT_OK = qw(%STRFLAGS);
10              
11 6     6   36 use Carp;
  6         18  
  6         404  
12 6     6   53 use File::Spec;
  6         10  
  6         226  
13 6     6   29 use List::Util qw(shuffle sum);
  6         9  
  6         16719  
14              
15             our %STRFLAGS = (
16             RANDOM => 0x1,
17             ORDERED => 0x2,
18             ROTATED => 0x4,
19             );
20              
21             my @VERSIONS = (1, 2);
22              
23             my $STRFILE_HDR_LEN = 24;
24              
25             # Strfile header format:
26             # uint32 version;
27             # uint32 strnum;
28             # uint32 longest str len;
29             # uint32 shortest str len;
30             # uint32 flags; (see %STRFLAGS)
31             # uint8[4] long-aligned space
32             # [0] is delimit char
33             # Header is Big-Endian.
34              
35             sub new {
36              
37 13     13 1 889399 my $class = shift;
38 13         30 my $src = shift;
39 13         25 my $param = shift;
40 13         620 my $self = {
41             SrcFile => File::Spec->rel2abs($src),
42             _srcfh => undef,
43             Version => 1,
44             StrNum => 0,
45             LongLen => 0,
46             ShortLen => 0xffffffff,
47             Flags => 0,
48             Delimit => '%',
49             Offsets => [],
50             };
51              
52 13         52 bless $self, $class;
53              
54             open $self->{_srcfh}, '<', $self->{SrcFile}
55 13 50       942 or croak "Failed to open $self->{SrcFile} for reading: $!";
56              
57 13 100       67 if ($param->{DataFile}) {
58              
59 8         39 $self->read_strfile($param->{DataFile});
60              
61             } else {
62              
63 5         30 $self->_create_strfile_data();
64              
65 5 50       18 if ($param->{Delimit}) {
66 0         0 $self->{Delimit} = unpack "a", $param->{Delimit};
67             }
68              
69             }
70              
71 13 100       49 if (defined $param->{Version}) {
72             croak "$param->{Version} is an invalid strfile version"
73 3 50       14 unless _version_check($param->{Version});
74 3         9 $self->{Version} = $param->{Version};
75             }
76              
77             # Order flag gets priority over Random
78 13 100       68 if ($param->{FcOrder}) {
    50          
    50          
79 1         6 $self->order(1);
80             } elsif ($param->{Order}) {
81 0         0 $self->order();
82             } elsif ($param->{Random}) {
83 0         0 $self->random();
84             }
85              
86 13 100       42 $self->{Flags} |= $STRFLAGS{ROTATED} if $param->{Rotate};
87              
88 13         57 return $self;
89              
90             }
91              
92             sub _version_check {
93              
94 11     11   23 my $ver = shift;
95              
96 11 50       43 return (grep { $ver == $_ } @VERSIONS) ? 1 : 0;
  22         129  
97              
98             }
99              
100             sub _create_strfile_data {
101              
102 5     5   8 my $self = shift;
103              
104 5         34 seek $self->{_srcfh}, 0, 0;
105              
106             # Each offset table must start with 0x00
107 5         10 push @{$self->{Offsets}}, tell $self->{_srcfh};
  5         21  
108              
109 5         11 my $coff = 0;
110 5         9 my $loff = 0;
111              
112 5         11 my $curlen = 0;
113              
114 5         11 my $l = '';
115              
116 5         28 while (defined $l) {
117              
118 56         394 $l = readline $self->{_srcfh};
119              
120 56 100 100     224 if (not defined $l or $l eq "$self->{Delimit}\n") {
121              
122 21         44 $coff = tell $self->{_srcfh};
123 21   100     63 $curlen = $coff - $loff - (length $l // 0);
124 21         26 $loff = $coff;
125              
126 21 50       50 next unless $curlen;
127              
128 21         31 push @{$self->{Offsets}}, $coff;
  21         64  
129 21         43 $self->{StrNum}++;
130              
131 21 100       47 if ($curlen < $self->{ShortLen}) {
132 10         33 $self->{ShortLen} = $curlen;
133             }
134            
135 21 100       53 if ($curlen > $self->{LongLen}) {
136 9         22 $self->{LongLen} = $curlen;
137             }
138             }
139              
140             }
141              
142 5         17 $self->{Version} = 1;
143              
144             }
145              
146             sub read_strfile {
147              
148 8     8 1 16 my $self = shift;
149 8         17 my $file = shift;
150              
151 8 50       274 open my $fh, '<', $file or croak "Failed to open $file for reading: $!";
152 8         28 binmode $fh;
153              
154 8         258 read $fh, my ($buf), $STRFILE_HDR_LEN;
155              
156             (
157             $self->{Version},
158             $self->{StrNum},
159             $self->{LongLen},
160             $self->{ShortLen},
161             $self->{Flags},
162             $self->{Delimit},
163             # We're ignoring 3 padding bytes
164 8         75 ) = unpack "N N N N N a", $buf;
165              
166 8 50       34 unless (_version_check($self->{Version})) {
167 0         0 croak "$file bogus strfile";
168             }
169              
170 8 50       41 if ($self->{LongLen} < $self->{ShortLen}) {
171 0         0 croak "$file bogus strfile";
172             }
173              
174 8 50       64 if ($self->{Flags} > sum values %STRFLAGS) {
175 0         0 croak "$file bogus strfile";
176             }
177              
178 8         33 $self->{DataFile} = $file;
179              
180 8         21 $self->{Offsets} = [];
181              
182 8         34 foreach my $i (0 .. $self->{StrNum}) {
183 39         52 my $off;
184             # v1 strfiles use 64-bit offsets
185 39 100       107 if ($self->{Version} == 1) {
    50          
186 21         38 read $fh, $off, 8;
187 21         48 (my ($u), $self->{Offsets}->[$i]) = unpack "N N", $off;
188 21 50       49 croak "Offset $i exceeds 4GB" if $u;
189             # v2 strfiles use 32-bit offsets
190             } elsif ($self->{Version} == 2) {
191 18         36 read $fh, $off, 4;
192 18         48 $self->{Offsets}->[$i] = unpack "N", $off;
193             }
194              
195             }
196              
197 8         183 close $fh;
198              
199             }
200              
201             sub order {
202              
203 8     8 1 41 my $self = shift;
204 8         17 my $fc = shift;
205              
206             # Ignore leading non-alphanumeric characters.
207 8         31 my @strings = map { s/^[\W_]+//r } $self->strings();
  32         124  
208 8 100       50 @strings = map { fc } @strings if $fc;
  16         87  
209              
210             my @offsets =
211 32         73 map { $self->{Offsets}->[$_] }
212 8         50 sort { $strings[$a] cmp $strings[$b] } (0 .. $self->{StrNum} - 1);
  45         92  
213              
214 8         32 push @offsets, $self->{Offsets}->[$self->{StrNum}];
215              
216 8         58 $self->{Offsets} = \@offsets;
217              
218 8         44 $self->{Flags} |= $STRFLAGS{ORDERED};
219              
220             }
221              
222             sub random {
223              
224 0     0 1 0 my $self = shift;
225              
226             my @offsets = map {
227 0         0 $self->{Offsets}->[$_]
228 0         0 } shuffle(0 .. $self->{StrNum} - 1);
229              
230 0         0 push @offsets, $self->{Offsets}->[$self->{StrNum}];
231              
232 0         0 $self->{Offsets} = \@offsets;
233              
234 0         0 $self->{Flags} |= $STRFLAGS{RANDOM};
235              
236             # Unset Ordered flag, as it takes priority over Random
237 0 0       0 if ($self->{Flags} & $STRFLAGS{ORDERED}) {
238 0         0 $self->{Flags} -= $STRFLAGS{ORDERED};
239             }
240              
241             }
242              
243             sub string {
244              
245 60     60 1 98 my $self = shift;
246 60         121 my $n = shift;
247              
248 60 100       160 return undef if $n >= $self->{StrNum};
249              
250 59         531 seek $self->{_srcfh}, $self->{Offsets}->[$n], 0;
251              
252 59         148 my $string = '';
253 59         89 my $l = '';
254 59         136 while (defined $l) {
255              
256 164         832 $l = readline $self->{_srcfh};
257              
258 164 100 100     649 last if not defined $l or $l eq "$self->{Delimit}\n";
259              
260 105         324 $string .= $l;
261              
262             }
263              
264             # ROT13
265 59 100       176 $string =~ tr/A-Za-z/N-ZA-Mn-za-m/ if $self->{Flags} & $STRFLAGS{ROTATED};
266              
267 59         229 return $string;
268              
269             }
270              
271             sub strings {
272              
273 14     14 1 28 my $self = shift;
274              
275 14         54 return map { $self->string($_) } (0 .. $self->{StrNum} - 1);
  58         160  
276              
277             }
278              
279             sub strings_like {
280              
281 3     3 1 4117 my $self = shift;
282 3         8 my $re = shift;
283              
284 3         11 return grep { /$re/m } $self->strings();
  15         117  
285              
286             }
287              
288             sub get {
289              
290 81     81 1 3686 my $self = shift;
291 81         155 my $get = shift;
292              
293 81 50 33     572 return undef if $get =~ /^_/ or not defined $self->{$get};
294              
295 81         486 return $self->{$get};
296              
297             }
298              
299             sub write_strfile {
300              
301 3     3 1 58 my $self = shift;
302 3   33     18 my $file = shift // "$self->{SrcFile}.dat";
303              
304 3 50       881 open my $fh, '>', $file or croak "Failed to open $file for writing: $!";
305 3         17 binmode $fh;
306              
307             my $hdr = pack "N N N N N c x x x", (
308             $self->{Version},
309             $self->{StrNum},
310             $self->{LongLen},
311             $self->{ShortLen},
312             $self->{Flags},
313             ord $self->{Delimit},
314 3         41 );
315              
316 3         7 print { $fh } $hdr;
  3         54  
317              
318 3         17 foreach my $i (0 .. $self->{StrNum}) {
319              
320 14         20 my $off;
321 14 100       43 if ($self->{Version} == 1) {
    50          
322 8         21 $off = pack "N N", (0, $self->{Offsets}->[$i]);
323             } elsif ($self->{Version} == 2) {
324 6         14 $off = pack "N", $self->{Offsets}->[$i];
325             }
326              
327 14         23 print { $fh } $off;
  14         31  
328              
329             }
330              
331 3         196 close $fh;
332              
333             }
334              
335             DESTROY {
336              
337 13     13   3854 my $self = shift;
338              
339 13         831 close $self->{_srcfh};
340              
341             }
342              
343             1;
344              
345              
346              
347             =head1 NAME
348              
349             File::Strfile - OO strfile interface
350              
351             =head1 SYNOPSIS
352              
353             use File::Strfile;
354              
355             $strfile = File::Strfile->new($src);
356              
357             $strfile->read_strfile($datafile);
358              
359             $strfile->random();
360              
361             $strfile->order();
362              
363             $str0 = $strfile->string(0);
364              
365             foreach my $str ($strfile->strings()) {
366             ...
367             }
368              
369             $strfile->write_strfile($datafile);
370              
371             =head1 DESCRIPTION
372              
373             File::Strfile provides an object oriented interface for reading and writing
374             strfiles, a file format often associated with the classic UNIX program
375             L. Strfiles are used to provide random access to strings stored in
376             another file, called the strfile source. The source files
377             consists of a collection of strings seperated by delimiting lines, which are
378             lines containing only a single delimiting character, typically a percentage (%)
379             sign. The strfile data
380             files are usually stored in the same directory as the source files, with the
381             same name but with the ".dat" suffix added. They contain a header that describes
382             the strfile database and a table of offsets pointing to each string in the
383             source file.
384              
385             This module only provides an interface for manipulating strfile data files, not
386             the source text files themselves.
387              
388             =head1 Object Methods
389              
390             =head2 File::Strfile->new($srcpath, [{opt => 'val'}])
391              
392             Returns a new File::Strfile object. $srcpath is the path to the source strfile.
393              
394             new() can be given a hash reference of options. Note that all options are
395             case-sensitive.
396              
397             =over 4
398              
399             =item DataFile
400              
401             Path to the strfile-generated data file. Instead of new() creating strfile data
402             from scratch, it will read data the from the given data file by calling
403             read_strfile(). Some fields can be overrided by passing additional options.
404              
405             =item Version
406              
407             Set version for outputted strfile. The following are acceptable version numbers:
408              
409             =over 4
410              
411             =item 1
412              
413             Original strfile version. Stores string offsets as unsigned 64-bit integars.
414             Most common. Default.
415              
416             =item 2
417              
418             Newer strfile version. Stores string offsets as unsigned 32-bit integars.
419              
420             =back
421              
422             =item Random
423              
424             Randomize the order of string offsets.
425              
426             =item Order
427              
428             Order string offsets alphabetically.
429              
430             =item FcOrder
431              
432             Order string offsets alphabetically, case-insensitive.
433              
434             =item Rotate
435              
436             Mark the source file as being ROT-13 ciphered.
437              
438             =item Delimit
439              
440             Set delimitting character. Default is a percentage sign (%). This option does
441             not work with the DataFile option.
442              
443             =back
444              
445             new() dies upon failure.
446              
447             =head2 $strfile->read_strfile($file)
448              
449             Read strfile data from $file.
450              
451             =head2 $strfile->order([$fc])
452              
453             Order strings alphabetically. If $fc is true, sort is done insensitive to case.
454              
455             =head2 $strfile->random()
456              
457             Randomize the order of strings.
458              
459             =head2 $strfile->string($n)
460              
461             Get $n-th string from string file. Returns undef if $n-th string does not exist.
462              
463             =head2 $strfile->strings()
464              
465             Returns list of all strings in strfile, in the order specified by the offset
466             table.
467              
468             =head2 $strfile->strings_like($re)
469              
470             Return list of strings that evaluate true given the qr regex $re.
471              
472             For example, to get every string that starts with 'YOW!':
473              
474             my @yows = $strfile->strings_like(qr/^YOW!/)
475              
476             Note that the 'm' (multiline) option is automatically used and does not need
477             to be specified.
478              
479             =head2 $strfile->get($member)
480              
481             Return value of $member in $strfile object. Note $member is case-sensitive.
482             The following are valid members:
483              
484             =over 4
485              
486             =item SrcFile
487              
488             Absolute path to strfile source file.
489              
490             =item Version
491              
492             Version of $strfile.
493              
494             =item StrNum
495              
496             Number of strings in $strfile.
497              
498             =item LongLen
499              
500             Length (in bytes) of the longest string in $strfile.
501              
502             =item ShortLen
503              
504             Length (in bytes) of the shortest string in $strfile.
505              
506             =item Flags
507              
508             Flag bitfield for $strfile. See documentation for %STRFLAGS for what each
509             bitmask means.
510              
511             =item Delimit
512              
513             Delimitting character.
514              
515             =item Offsets
516              
517             Array ref of strfile offsets. The last offset will not be a string offset but
518             the EOF offset.
519              
520             =back
521              
522             On failure, get() returns undef.
523              
524             =head2 $strfile->write_strfile([$file])
525              
526             Write $strfile data file to either $file. If $file is not supplied, write to
527             source file path + '.dat' suffix.
528              
529             =head1 Global Variables
530              
531             =over 4
532              
533             =item $File::Strfile::VERSION
534              
535             File::Strfile version.
536              
537             =item %File::Strfile::STRFLAGS
538              
539             Hash of strfile flags and their bitmasks.
540              
541             =over 4
542              
543             =item RANDOM => 0x1
544              
545             Strings were randomly sorted.
546              
547             =item ORDERED => 0x2
548              
549             Strings were sorted alphabetically. Takes priority over Random.
550              
551             =item ROTATED => 0x4
552              
553             Strings are ROT-13 ciphered.
554              
555             =back
556              
557             Able to be exported.
558              
559             use File::Strfile qw(%STRFLAGS);
560              
561             =back
562              
563             =head1 EXAMPLES
564              
565             Here is an example of a typical source strfile:
566              
567             A can of ASPARAGUS, 73 pigeons, some LIVE ammo, and a FROZEN DAIQUIRI!!
568             %
569             A dwarf is passing out somewhere in Detroit!
570             %
571             A wide-eyed, innocent UNICORN, poised delicately in a MEADOW filled
572             with LILACS, LOLLIPOPS & small CHILDREN at the HUSH of twilight??
573             %
574             Actually, what I'd like is a little toy spaceship!!
575              
576             =head1 RESTRICTIONS
577              
578             Despite version 1 strfiles storing string offsets as unsigned 64-bit integars,
579             they are still read as 32-bit. This means that File::Strfile will not be
580             able to read strfile sources
581             larger than 4GB (about the size of 1,000 plaintext KJV Bibles).
582              
583             File::Strfile tries to emulate the original BSD strfile's behavior as close as
584             possible, which means it will also inherit some of its quirks.
585              
586             =head1 AUTHOR
587              
588             Written by Samuel Young ELE.
589              
590             =head1 COPYRIGHT
591              
592             Copyright 2024, Samuel Young
593              
594             This library is free software; you may redistribute it and/or
595             modify it under the same terms as Perl itself.
596              
597             =head1 SEE ALSO
598              
599             L, L
600              
601             =cut