File Coverage

blib/lib/File/FormatIdentification/Pronom.pm
Criterion Covered Total %
statement 293 620 47.2
branch 50 106 47.1
condition 36 54 66.6
subroutine 31 50 62.0
pod 0 20 0.0
total 410 850 48.2


line stmt bran cond sub pod time code
1             package File::FormatIdentification::Pronom;
2              
3 1     1   363233 use feature qw(say);
  1         3  
  1         149  
4 1     1   38 use strict;
  1         2  
  1         47  
5 1     1   6 use warnings;
  1         3  
  1         26  
6 1     1   741 use XML::LibXML;
  1         49525  
  1         19  
7 1     1   156 use Carp;
  1         3  
  1         49  
8 1     1   8 use List::Util qw( none first );
  1         2  
  1         64  
9 1     1   7 use Scalar::Util;
  1         2  
  1         28  
10 1     1   469 use YAML::XS;
  1         2976  
  1         51  
11 1     1   498 use File::FormatIdentification::Regex;
  1         5  
  1         72  
12 1     1   599 use Moose;
  1         497521  
  1         7  
13 1     1   8552 use v5.21; # special regex syntax introduced with 5.21 needed!
  1         4  
14 1     1   8 use feature qw(signatures);
  1         2  
  1         145  
15 1     1   10 no warnings qw( experimental::signatures);
  1         4  
  1         73  
16              
17             our $VERSION = '0.07'; # VERSION
18              
19             # ABSTRACT Perl extension for parsing PRONOM-Signatures using DROID-Signature file
20              
21             # Preloaded methods go here.
22             # flattens a regex-structure to a regex-string, expects a signature-pattern and a list of regex-structures
23             # returns regex
24             #
25 1     1   7 no warnings 'recursion';
  1         2  
  1         530  
26              
27 50262     50262   72336 sub _flatten_rx_recursive ($regex, $lastpos, $open_brackets, @rx_groups) {
  50262         74240  
  50262         73091  
  50262         69487  
  50262         89085  
  50262         68690  
28 50262         75594 my $rx = shift @rx_groups;
29             my $sub_close_open_brackets = sub {
30 23442     23442   54510 while ( $open_brackets > 0 ) {
31 3900         8254 $regex .= ")";
32 3900         8443 $open_brackets--;
33             }
34 50262         172323 };
35              
36             #use Data::Printer;
37             #say "_flatten_rx_recursive";
38             #p( @rx_groups );
39             #p( $rx );
40 50262         85607 my $bracket_symbol = "(";
41 50262 50       101170 if ( !defined $regex ) { confess; }
  0         0  
42              
43 50262 100       93016 if ( !defined $rx ) { # do nothing
44 23442         38480 &$sub_close_open_brackets();
45             }
46             else {
47 26820         57818 my $pos_diff = $rx->{position} - $lastpos;
48 26820         42433 my $local_regex = $rx->{regex};
49 26820 50       51359 if ( !defined $local_regex ) {
50 0         0 $local_regex = '';
51             }
52 26820 100       51273 if ( $pos_diff > 0 ) { # is deeper
    50          
53             # look a head, if same pos found, then use bracket, otherwise not
54 18453 100 100     76622 if (
      66        
55             (
56             scalar @rx_groups > 0
57             && ( $rx_groups[0]->{position} == $rx->{position} )
58             )
59             || $pos_diff > 1
60             )
61             { # use (
62             $regex = &_flatten_rx_recursive(
63             "$regex" . ( $bracket_symbol x $pos_diff ) . $local_regex,
64 3900         17254 $rx->{position}, $open_brackets += $pos_diff, @rx_groups );
65             }
66             else {
67             $regex = &_flatten_rx_recursive(
68             "$regex$local_regex", $rx->{position},
69 14553         45081 $open_brackets, @rx_groups
70             );
71             } ## end else [ if ( scalar @rx_groups...)]
72             }
73             elsif ( 0 == $pos_diff ) {
74 8367         24509 File::FormatIdentification::Regex::simplify_two_or_combined_regex(
75             $regex, $local_regex );
76 8367         1241901 $regex =
77             &_flatten_rx_recursive( "$regex|$local_regex", $lastpos,
78             $open_brackets, @rx_groups );
79             }
80             else { #( $pos_diff < 0 ) # is higher
81             $regex = &_flatten_rx_recursive(
82             "$regex)$local_regex",
83             $rx->{position},
84 0         0 $open_brackets - 1, #($rx->{position} - $lastpos),
85             @rx_groups
86             );
87             }
88             }
89 50262         158169 return $regex;
90             } ## end sub _flatten_rx_recursive ($$$@)
91 1     1   9 use warnings 'recursion';
  1         3  
  1         2207  
92              
93 23442     23442   36161 sub _flatten_rx ($regex, @rx_groups) {
  23442         35917  
  23442         36431  
  23442         33411  
94             #say "calling flatten_rx with regex=$regex quality=$quality";
95             #use Data::Printer;
96             #p( @rx_groups );
97 23442         44236 return _flatten_rx_recursive( $regex, 0, 0, @rx_groups );
98             } ## end sub _flatten_rx ($@)
99              
100             # expands pattern of form "FFFB[10:EB]" to FFFB10, FFFB11, ... FFFBEB
101 26820     26820   39268 sub _expand_pattern ($pattern) {
  26820         45103  
  26820         36732  
102 26820         58979 $pattern =~ s/(?<=\[)!/^/g;
103 26820         45303 $pattern =~ s/(?<=[0-9A-F]{2}):(?=[0-9A-F]{2})\]/-]/g;
104 26820         289284 $pattern =~ s/([0-9A-F]{2})/\\x{$1}/g;
105              
106             # substitute hex with printable ASCII-Output
107             #$pattern =~ s#\\x\{(3[0-9]|[46][1-9A-F]|[57][0-9A])\}#chr( hex($1) );#egs;
108 26820         123053 $pattern =~ s#\\x\{([46][1-9A-F]|[57][0-9A]|3[0-9])\}#chr( hex($1) );#egs;
  89934         358334  
109 26820         67200 return $pattern;
110             } ## end sub _expand_pattern ($)
111              
112 4245     4245   6410 sub _expand_offsets_residual_loop($maxoffset, $offset_expanded, $byte) {
  4245         6808  
  4245         6311  
  4245         6225  
  4245         5925  
113 4245         8586 my $maxloops = int( $maxoffset / 32766 );
114 4245         7418 my $maxresidual = $maxoffset % 32766;
115 4245         10208 for ( my $i = 0 ; $i < $maxloops ; $i++ ) {
116 0         0 $offset_expanded .= $byte . "{32766}";
117             }
118 4245         9458 $offset_expanded .= $byte . "{$maxresidual}";
119 4245         10189 return $offset_expanded;
120             }
121              
122 4983     4983   7347 sub _expand_offsets_calc_repetitions( $minoffset, $maxoffset, $offset_expanded, $byte) {
  4983         8138  
  4983         8218  
  4983         8029  
  4983         7311  
  4983         6990  
123             # workaround, because perl quantifier limits,
124             # calc How many repetitions we need! Both offsets should be less than 32766
125             #TODO: check if this comes from Droid or is calculated
126              
127 4983         7728 my $mintmp = 0;
128 4983         7422 my $maxtmp = 0;
129 4983 50 33     16589 if ( defined $minoffset && ( length($minoffset) > 0 ) ) {
130 4983         9018 $mintmp = $minoffset;
131             }
132 4983 100 66     14857 if ( defined $maxoffset && ( length($maxoffset) > 0 ) ) {
133 3678         6081 $maxtmp = $maxoffset;
134             }
135              
136 4983         6941 my $maxloops;
137 4983 100       9515 if ( $maxtmp >= $mintmp ) {
138 4953         10568 $maxloops = int( $maxtmp / 32766 );
139             }
140             else {
141 30         82 $maxloops = int( $mintmp / 32766 );
142             }
143 4983         8493 my $maxresidual = $maxtmp % 32766;
144 4983         8303 my $minresidual = $mintmp % 32766;
145              
146             #say "\tMaxloops=$maxloops maxres = $maxresidual minres=$minresidual";
147 4983         7442 my @offsets;
148 4983         7166 my $minstr = 0;
149 4983         7539 my $maxstr = 0;
150 4983 50 33     15261 if ( defined $minoffset && length($minoffset) > 0 ) {
151 4983         7377 $minstr = $minresidual;
152 4983         8052 $mintmp = $mintmp - $minresidual;
153             }
154              
155 4983         11585 for ( my $i = 0 ; $i <= $maxloops ; $i++ ) {
156              
157             # loop, so we assure the special handling of residuals
158 6093 100       13318 if ( $maxtmp > $maxresidual ) {
    50          
159 1299         1888 $maxstr = 32766;
160             } elsif ( $maxtmp < 0 ) {
161 0         0 $maxstr = 0;
162             } else {
163 4794         7217 $maxstr = $maxresidual;
164             }
165 6093 100       12621 if ( $mintmp > $minresidual ) {
    100          
166 108         163 $minstr = 32766;
167             } elsif ( $mintmp < 0 ) {
168 297         438 $minstr = 0;
169             } else {
170 5688         8176 $minstr = $minresidual;
171             }
172             #### handle residuals
173 6093 100       11856 if ( $i == 0 ) {
    100          
174 4983         7237 $minstr = $minresidual;
175 4983         7136 $mintmp = $mintmp - $minresidual;
176             } elsif ( $i == $maxloops ) {
177 189         386 $maxstr = $maxresidual;
178 189         317 $maxtmp = $maxtmp - $maxresidual;
179             }
180             # mark offsets
181 6093         8634 my $tmp;
182 6093 100 66     22250 if (defined $maxstr and $maxstr > 0 and $minstr > $maxstr) {
      100        
183 27         56 $tmp->{minoffset} = $maxstr;
184 27         99 $tmp->{maxoffset} = $minstr;
185             } else {
186 6066         14148 $tmp->{minoffset} = $minstr;
187 6066         10934 $tmp->{maxoffset} = $maxstr;
188             }
189 6093         15227 push @offsets, $tmp;
190             } ## end for ( my $i = 0 ; $i <=...)
191              
192             my @filtered = map {
193 4983 100 66     9683 if ( !defined $maxoffset || length($maxoffset) == 0 ) {
  6093         17777  
194 1305         2654 $_->{maxoffset} = "";
195             }
196 6093 50 33     18171 if ( !defined $minoffset || length($minoffset) == 0 ) {
197 0         0 $_->{minoffset} = "";
198             }
199 6093         15263 $_;
200             } @offsets;
201 4983         10138 foreach my $tmp (@filtered) {
202             # ? at the end - means non-greedy
203             #$offset_expanded .= $byte."{" . $tmp->{minoffset} . "," . $tmp->{maxoffset} . "}?";
204             $offset_expanded .=
205 6093         20527 $byte . "{" . $tmp->{minoffset} . "," . $tmp->{maxoffset} . "}";
206             } ## end foreach my $tmp (@filtered)
207 4983         18970 return $offset_expanded;
208             }
209              
210             # expands offsets min,max to regex ".{$min,$max}" and uses workarounds if $min or $max exceeds 32766
211 26820     26820   39621 sub _expand_offsets($minoffset, $maxoffset) {
  26820         42841  
  26820         38631  
  26820         37064  
212 26820         41704 my $byte =
213             '.'; # HINT: needs the character set modifier "aa" in $foo=~m/$regex/aa
214             #my $byte = '[\x00-\xff]';
215 26820         39913 my $offset_expanded = "";
216 26820 100 66     201949 if (( defined $minoffset )
    100 100        
      66        
      100        
      66        
      33        
      66        
217             && ( length($minoffset) > 0 )
218             && ( defined $maxoffset )
219             && ( length($maxoffset) > 0 )
220             && ( $minoffset == $maxoffset ) )
221             {
222 21729 100       45744 if ( $minoffset > 0 ) {
223 4245         8027 $offset_expanded = _expand_offsets_residual_loop($maxoffset, $offset_expanded, $byte);
224             } ## end if ( $minoffset > 0 )
225             }
226             elsif ( ( ( not defined $minoffset ) || ( length($minoffset) == 0 ) )
227             && ( ( not defined $maxoffset ) || ( length($maxoffset) == 0 ) ) )
228             {
229 108         226 $offset_expanded = "";
230             }
231             else {
232 4983         12125 $offset_expanded = _expand_offsets_calc_repetitions($minoffset, $maxoffset, $offset_expanded, $byte)
233              
234             } ## end else [ if ( ( ( not defined $minoffset...)))]
235              
236             #say "DEBUG: minoffset='$minoffset' maxoffset='$maxoffset' --> offset_expanded='$offset_expanded'";
237              
238             # minimization steps
239 26820         50237 $offset_expanded =~ s#{0,}#*#g;
240 26820         39399 $offset_expanded =~ s#{0,1}#?#g;
241 26820         42293 $offset_expanded =~ s#{1,}#+#g;
242 26820         62189 return $offset_expanded;
243             } ## end sub _expand_offsets($$)
244              
245             # got XPath-object and returns a regex-structure as hashref
246 18663     18663   27448 sub _parse_fragments ($fq) {
  18663         29094  
  18663         25543  
247 18663         41293 my $position = $fq->getAttribute('Position');
248 18663         215561 my $minoffset = $fq->getAttribute('MinOffset');
249 18663         160129 my $maxoffset = $fq->getAttribute('MaxOffset');
250 18663         182802 my $rx = $fq->textContent;
251 18663         37508 my $expanded = _expand_pattern($rx);
252 18663         30212 my $ret;
253 18663         46294 $ret->{position} = $position;
254 18663         33323 $ret->{direction} = "left";
255 18663         32137 $ret->{regex} = "";
256              
257 18663         36710 my ($offset_expanded) = _expand_offsets( $minoffset, $maxoffset );
258              
259 18663 100       85467 if ( $fq->localname eq "LeftFragment" ) {
    50          
260 6480         12180 $ret->{direction} = "left";
261 6480         17510 $ret->{regex} = "($expanded)$offset_expanded";
262             }
263             elsif ( $fq->localname eq "RightFragment" ) {
264 12183         23861 $ret->{direction} = "right";
265 12183         31435 $ret->{regex} = "$offset_expanded($expanded)";
266             }
267              
268             #say "pF: rx=$rx expanded=$expanded offset=$offset_expanded";
269 18663         49970 return $ret;
270             } ## end sub _parse_fragments ($)
271              
272             # got XPath-object and search direction and returns a regex-structure as hashref
273 8157     8157   11786 sub _parse_subsequence ($ssq, $direction = undef) {
  8157         13447  
  8157         14015  
  8157         11530  
274 8157         19958 my $position = $ssq->getAttribute('Position');
275 8157         102481 my $minoffset = $ssq->getAttribute('SubSeqMinOffset');
276 8157         77283 my $maxoffset = $ssq->getAttribute('SubSeqMaxOffset');
277              
278 8157         69960 my $rx = $ssq->getElementsByTagName('Sequence')->get_node(1)->textContent;
279              
280 8157         434887 my @lnodes = $ssq->getElementsByTagName('LeftFragment');
281 8157         387404 my @rnodes = $ssq->getElementsByTagName('RightFragment');
282 8157         278977 my @lrx_fragments = map { _parse_fragments($_) } @lnodes;
  6480         12346  
283 8157         14266 my @rrx_fragments = map { _parse_fragments($_) } @rnodes;
  12183         23260  
284 8157         19432 my $lregex = _flatten_rx( "", @lrx_fragments );
285 8157         16929 my $rregex = _flatten_rx( "", @rrx_fragments );
286 8157         18089 my $expanded = _expand_pattern($rx);
287              
288             #if ( length($minoffset) > 0
289             # && length($maxoffset) > 0
290             # && $minoffset > $maxoffset ) {
291             # confess(
292             #"parse_subsequence: Maxoffset=$maxoffset < Minoffset=$minoffset! regex= '$rx'"
293             # );
294             # } ## end if ( length($minoffset...))
295              
296 8157         18941 my $offset_expanded = _expand_offsets( $minoffset, $maxoffset );
297 8157         26664 my $prefix;
298             my $suffix;
299 8157         0 my $ret;
300 8157         0 my $regex;
301 8157 100 66     34528 if ( !defined $direction || length($direction) == 0 ) {
    100          
    50          
302 471         1763 $regex = join( "", $lregex, $expanded, $rregex );
303             }
304             elsif ( $direction eq "BOFoffset" ) {
305 6885         21060 $regex =
306             join( "", $offset_expanded, "(", $lregex, $expanded, $rregex, ")" );
307             }
308             elsif ( $direction eq "EOFoffset" ) {
309 801         2440 $regex =
310             join( "", "(", $lregex, $expanded, $rregex, ")", $offset_expanded );
311             }
312             else {
313 0         0 warn "unknown reference '$direction' found\n";
314 0         0 $regex = join( "", $lregex, $expanded, $rregex );
315             }
316             $ret->{regex} =
317 8157         24041 File::FormatIdentification::Regex::peep_hole_optimizer($regex);
318 8157         20437 $ret->{position} = $position;
319              
320 8157         43898 return $ret;
321             } ## end sub _parse_subsequence ($$)
322              
323             # got XPath-object and returns regex-string
324 7128     7128   10787 sub _parse_bytesequence ($bsq) {
  7128         12583  
  7128         10314  
325             #say "rx_groups in parse_byte_sequence:";
326 7128         16805 my $reference = $bsq->getAttribute('Reference');
327             ; # if BOFoffset -> anchored begin of file, EOFofset -> end of file
328 7128         84386 my @nodes = $bsq->getElementsByTagName('SubSequence');
329 7128         285053 my @rx_groups = map { _parse_subsequence( $_, $reference ) } @nodes;
  8157         45613  
330 7128         143489 my $expanded = "";
331 7128         15264 my $regex_flattened = _flatten_rx( $expanded, @rx_groups );
332              
333             #my $ro = Regexp::Optimizer->new;
334             #my $ro = Regexp::Assemble->new;
335             #$ro->add( $regex_flattened);
336             #$regex_flattened = $ro->as_string($regex_flattened);
337             #$regex_flattened = $ro->re;
338 7128         12451 my $regex;
339 7128 100 66     31199 if ( !defined $reference || 0 == length($reference) ) {
    100          
    50          
340 276         603 $regex = "$regex_flattened";
341             }
342             elsif ( $reference eq "BOFoffset" ) {
343 6051         14221 $regex = "\\A$regex_flattened";
344             }
345             elsif ( $reference eq "EOFoffset" ) {
346 801         2012 $regex = "$regex_flattened\\Z";
347             }
348             else {
349 0         0 warn "unknown reference '$reference' found\n";
350 0         0 $regex = "$regex_flattened";
351             }
352              
353 1     1   15 use Regexp::Optimizer;
  1         3  
  1         3484  
354 7128         26918 my $ro = Regexp::Optimizer->new;
355              
356             #say "regex='$regex'";
357             #$regex = $ro->as_string( $regex );
358 7128         61789 return $regex;
359             } ## end sub _parse_bytesequence ($)
360              
361             # ($%signatures, $%internal) = parse_signaturefile( $file )
362 3     3   17 sub _parse_signaturefile($pronomfile) {
  3         12  
  3         6  
363 3         10 my %signatures;
364              
365             # hash{internalid}->{regex} = $regex
366             # ->{signature} = $signature
367             my %internal_signatures;
368              
369 3         25 my $dom = XML::LibXML->load_xml( location => $pronomfile );
370 3         320560 $dom->indexElements();
371 3         215 my $xp = XML::LibXML::XPathContext->new($dom);
372 3         60 $xp->registerNs( 'droid',
373             'http://www.nationalarchives.gov.uk/pronom/SignatureFile' );
374              
375             # find Fileformats
376             #my $tmp = $xp->find('/*[local-name() = "FFSignatureFile"]')->get_node(1);
377             #say "E:", $tmp->nodeName;
378             #say "EXISTS:", $xp->exists('/droid:FFSignatureFile');
379             #say "EXISTS2", $xp->exists('/droid:FFSignatureFile/droid:FileFormatCollection/droid:FileFormat');
380              
381 3         33 my $fmts = $xp->find(
382             '/*[local-name() = "FFSignatureFile"]/*[local-name() = "FileFormatCollection"]/*[local-name() = "FileFormat"]'
383             );
384 3         10650 foreach my $fmt ( $fmts->get_nodelist() ) {
385 6891         18943 my $id = $fmt->getAttribute('ID');
386 6891         74182 my $mimetype = $fmt->getAttribute('MIMEtype');
387 6891         65221 my $name = $fmt->getAttribute('Name');
388 6891         59877 my $puid = $fmt->getAttribute('PUID');
389 6891         57719 my $version = $fmt->getAttribute('Version');
390             #
391              
392             ##
393             my @extensions =
394 6891         58617 map { $_->textContent() } $fmt->getElementsByTagName('Extension');
  9057         252133  
395             my @internalsignatures =
396 6891         31545 map { $_->textContent() }
  5988         236374  
397             $fmt->getElementsByTagName('InternalSignatureID');
398 6891         85838 my @haspriorityover = map { $_->textContent() }
  3249         79282  
399             $fmt->getElementsByTagName('HasPriorityOverFileFormatID');
400 6891         207464 $signatures{$id}->{mimetype} = $mimetype;
401 6891         50057 $signatures{$id}->{name} = $name;
402 6891         15325 $signatures{$id}->{puid} = $puid;
403 6891         15363 $signatures{$id}->{version} = $version; # optional
404 6891         16972 $signatures{$id}->{extensions} = \@extensions;
405 6891         14944 $signatures{$id}->{internal_signatures} = \@internalsignatures;
406              
407 6891         14001 foreach my $prio (@haspriorityover) {
408 3249         9215 $signatures{$id}->{priorityover}->{$prio} = 1;
409             }
410              
411 6891         13982 foreach my $internal (@internalsignatures) {
412 5988         32536 $internal_signatures{$internal}->{signature} = $id;
413             }
414             } ## end foreach my $fmt ( $fmts->get_nodelist...)
415              
416             # find InternalSignatures
417 3         513 my $sigs =
418             $xp->find(
419             '/*[local-name() = "FFSignatureFile"]/*[local-name() = "InternalSignatureCollection"]/*[local-name() = "InternalSignature"]'
420             );
421              
422 3         12646 foreach my $sig ( $sigs->get_nodelist() ) {
423              
424 6054         92440 my $id = $sig->getAttribute('ID');
425 6054         83995 my $specificity = $sig->getAttribute('Specificity');
426 6054         83302 $internal_signatures{$id}->{specificity} = $specificity;
427              
428             #p( $sig->toString() );
429 6054         14970 my @nodes = $sig->getElementsByTagName('ByteSequence');
430              
431             #p( @nodes );
432 6054         377675 my @rx_groups = map { _parse_bytesequence($_) } @nodes;
  7128         31867  
433             my @rx_quality =
434 6054         88797 map { File::FormatIdentification::Regex::calc_quality($_); }
  7128         17616  
435             @rx_groups;
436              
437 6054         20553 $internal_signatures{$id}->{regex} = \@rx_groups;
438 6054         25542 $internal_signatures{$id}->{quality} = \@rx_quality;
439             } ## end foreach my $sig ( $sigs->get_nodelist...)
440              
441 3         760 return ( \%signatures, \%internal_signatures );
442             } ## end sub _parse_signaturefile($)
443              
444 0     0 0 0 sub uniq_signature_ids_by_priority ($self, @signatures){
  0         0  
  0         0  
  0         0  
445 0         0 my %found_signature_ids;
446              
447             # which PUIDs are in list?
448 0         0 foreach my $signatureid (@signatures) {
449 0 0       0 if ( defined $signatureid ) {
450 0         0 $found_signature_ids{$signatureid} = 1;
451             }
452             }
453              
454             # remove all signatures when actual signature has priority over
455 0         0 foreach my $signatureid ( keys %found_signature_ids ) {
456 0         0 foreach my $priority_over_sid (
457 0         0 keys %{ $self->{signatures}->{$signatureid}->{priorityover} } )
458             {
459 0 0       0 if ( exists $found_signature_ids{$priority_over_sid} ) {
460 0         0 delete $found_signature_ids{$priority_over_sid};
461             }
462             } ## end foreach my $priority_over_sid...
463             } ## end foreach my $signatureid ( keys...)
464              
465             # reduce list to all signatures with correct priority
466             my @result =
467 0         0 grep { defined $found_signature_ids{ $_->{signature} } } @signatures;
  0         0  
468 0         0 return @result;
469             } ## end sub uniq_signature_ids_by_priority
470              
471             has 'droid_signature_filename' => (
472             is => 'ro',
473             required => 1,
474             reader => 'get_droid_signature_filename',
475             trigger => sub {
476             my $self = shift;
477              
478             #say "TRIGGER";
479             my $yaml_file = $self->get_droid_signature_filename() . ".yaml";
480             if ( $self->{auto_load} && -e $yaml_file ) {
481             $self->load_from_yamlfile($yaml_file);
482             }
483             else {
484             my ( $signatures, $internal_signatures ) =
485             _parse_signaturefile( $self->{droid_signature_filename} );
486             $self->{signatures} = $signatures;
487             $self->{internal_signatures} = $internal_signatures;
488              
489             #die;
490             if ( $self->{auto_store} ) {
491             $self->save_as_yamlfile($yaml_file);
492             }
493             } ## end else [ if ( $self->{auto_load...})]
494             foreach my $s ( keys %{ $self->{signatures} } ) {
495             my $puid = $self->{signatures}->{$s}->{puid};
496             if ( defined $puid && length($puid) > 0 ) {
497             $self->{puids}->{$puid} = $s;
498             }
499             }
500             }
501             );
502              
503 2     2 0 6 sub save_as_yamlfile ($self, $filename) {
  2         7  
  2         7  
  2         4  
504 2         5 my @res;
505 2         9 push @res, $self->{signatures};
506 2         5 push @res, $self->{internal_signatures};
507 2         34130 YAML::XS::DumpFile( "$filename", @res );
508 2         271116 return;
509             } ## end sub save_as_yamlfile
510              
511 1     1 0 3 sub load_from_yamlfile ($self, $filename) {
  1         3  
  1         3  
  1         3  
512 1         7 my ( $sig, $int ) = YAML::XS::LoadFile($filename);
513 1         78215 $self->{signatures} = $sig;
514 1         3 $self->{internal_signatures} = $int;
515 1         8 return;
516             } ## end sub load_from_yamlfile
517              
518             has 'auto_store' => (
519             is => 'ro',
520             default => 1,
521             );
522              
523             has 'auto_load' => (
524             is => 'ro',
525             default => 1,
526             );
527              
528 0     0 0   sub get_all_signature_ids ($self) {
  0            
  0            
529 0           my @sigs = sort { $a <=> $b } keys %{ $self->{signatures} };
  0            
  0            
530 0           return @sigs;
531             }
532              
533 0     0 0   sub get_signature_id_by_puid ($self, $puid) {
  0            
  0            
  0            
534 0           my $sig = $self->{puids}->{$puid};
535 0           return $sig;
536             }
537              
538 0     0 0   sub get_internal_ids_by_puid ($self, $puid) {
  0            
  0            
  0            
539 0           my $sig = $self->get_signature_id_by_puid($puid);
540 0           my @ids = ();
541 0 0         if ( defined $sig ) {
542 0           @ids = grep { defined $_ }
543 0           @{ $self->{signatures}->{$sig}->{internal_signatures} };
  0            
544             }
545 0           return @ids;
546             }
547              
548 0     0 0   sub get_file_endings_by_puid ($self, $puid){
  0            
  0            
  0            
549 0           my $sig = $self->get_signature_id_by_puid($puid);
550 0           my @endings = ();
551 0 0         if ( defined $sig ) {
552 0           @endings = $self->{signatures}->{$sig}->{extensions};
553             }
554 0           return @endings;
555             }
556              
557 0     0 0   sub get_all_internal_ids ($self) {
  0            
  0            
558 0           my @ids = sort { $a <=> $b } keys %{ $self->{internal_signatures} };
  0            
  0            
559 0           foreach my $id (@ids) {
560 0 0         if ( !defined $id ) { confess("$id not defined") }
  0            
561             }
562 0           return @ids;
563             }
564              
565 0     0 0   sub get_all_puids ($self){
  0            
  0            
566             my @ids =
567 0           sort grep { defined $_ }
568 0           map { $self->{signatures}->{$_}->{puid}; }
569 0           grep { defined $_ } $self->get_all_signature_ids();
  0            
570 0           return @ids;
571             }
572              
573 0     0 0   sub get_regular_expressions_by_internal_id ($self, $internalid){
  0            
  0            
  0            
574 0 0         if ( !defined $internalid ) { confess("internalid must exists!"); }
  0            
575 0           return @{ $self->{internal_signatures}->{$internalid}->{regex} };
  0            
576             }
577              
578 0     0 0   sub get_all_regular_expressions ($self) {
  0            
  0            
579 0           my @ids = $self->get_all_internal_ids();
580 0           my @regexes = ();
581 0           foreach my $id (@ids) {
582 0           my @rx = $self->get_regular_expressions_by_internal_id($id);
583 0           push @regexes, @rx;
584             }
585 0           my @ret = sort @regexes;
586 0           return @ret;
587             }
588              
589 0     0 0   sub get_qualities_by_internal_id ($self, $internalid) {
  0            
  0            
  0            
590 0 0         if ( !defined $internalid ) { confess("internalid must exists!"); }
  0            
591 0           my $value = $self->{internal_signatures}->{$internalid}->{quality};
592 0 0         if ( defined $value ) {
593 0           return @{$value};
  0            
594             }
595 0           return;
596             }
597              
598 0     0 0   sub get_signature_id_by_internal_id ($self, $internalid) {
  0            
  0            
  0            
599 0 0         if ( !defined $internalid ) { confess("internalid must exists!"); }
  0            
600 0           return $self->{internal_signatures}->{$internalid}->{signature};
601             }
602              
603 0     0 0   sub get_name_by_signature_id ($self, $signature) {
  0            
  0            
  0            
604 0           return $self->{signatures}->{$signature}->{name};
605             }
606              
607 0     0 0   sub get_puid_by_signature_id ($self, $signature) {
  0            
  0            
  0            
608 0           return $self->{signatures}->{$signature}->{puid};
609             }
610              
611 0     0 0   sub get_puid_by_internal_id ($self, $internalid) {
  0            
  0            
  0            
612 0 0         if ( !defined $internalid ) { confess("internalid must exists!"); }
  0            
613 0           my $signature = $self->get_signature_id_by_internal_id($internalid);
614 0           return $self->get_puid_by_signature_id($signature);
615             }
616              
617 0     0 0   sub get_quality_sorted_internal_ids ($self){
  0            
  0            
618             my @ids = sort {
619             # sort by regexes
620 0           my @a_rxq = @{ $self->{internal_signatures}->{$a}->{quality} };
  0            
  0            
621 0           my @b_rxq = @{ $self->{internal_signatures}->{$b}->{quality} };
  0            
622 0           my $aq = 0;
623 0           foreach my $as (@a_rxq) { $aq += $as; }
  0            
624 0           my $bq = 0;
625 0           foreach my $bs (@b_rxq) { $bq += $bs; }
  0            
626              
627             #use Data::Printer;
628             #p( $a );
629             #p( $aq );
630 0           $aq <=> $bq;
631             } $self->get_all_internal_ids();
632 0           return @ids;
633             }
634              
635 0     0 0   sub get_combined_regex_by_puid ($self, $puid) {
  0            
  0            
  0            
636 0           my @internals = $self->get_internal_ids_by_puid($puid);
637             #use Data::Printer;
638             #p( $puid );
639             #p( @internals );
640             my @regexes = map {
641 0           my @regexes_per_internal =
  0            
642             $self->get_regular_expressions_by_internal_id($_);
643 0           my $combined =
644             File::FormatIdentification::Regex::and_combine(@regexes_per_internal);
645              
646             #p( $combined );
647 0           $combined;
648             } @internals;
649 0           my $result = File::FormatIdentification::Regex::or_combine(@regexes);
650             #p( $result );
651 0           return $result;
652             }
653              
654 0     0     sub _prepare_statistics ($self) {
  0            
  0            
655 0           my $results;
656             # count of PUIDs
657             # count of internal ids (IDs per PUID)
658             # count of regexes
659             # count of file endings only
660             # count of internal ids without PUID
661             # larges and shortest regex
662             # complex and simple regex
663             # common regexes
664             #say "stat";
665 0           my @puids = $self->get_all_puids();
666 0           my $puids = scalar(@puids);
667 0           my @internals = $self->get_all_internal_ids();
668 0           my $internals = scalar(@internals);
669 0           my $regexes = 0;
670 0           my $fileendingsonly = 0;
671 0           my @fileendingsonly = ();
672 0           my $fileendings = 0;
673 0           my $int_per_puid = 0;
674 0           my $internal_without_puid = 0;
675 0           my @internal_without_puid = ();
676 0           my @quality_sorted_internal_ids = $self->get_quality_sorted_internal_ids();
677 0           my %uniq_regexes;
678              
679 0           foreach my $internalid (@internals) {
680 0           my @regexes =
681             $self->get_regular_expressions_by_internal_id($internalid);
682 0           foreach my $rx (@regexes) {
683 0           my @tmp = ();
684 0 0         if ( exists $uniq_regexes{$rx} ) {
685 0           @tmp = @{ $uniq_regexes{$rx} };
  0            
686             }
687 0           push @tmp, $internalid;
688 0           $uniq_regexes{$rx} = \@tmp;
689             }
690              
691 0           $regexes += scalar(@regexes);
692 0           my $sigid = $self->get_signature_id_by_internal_id($internalid);
693 0 0         if ( !defined $sigid ) {
694 0           $internal_without_puid++;
695 0           push @internal_without_puid, $internalid;
696             }
697             }
698 0           foreach my $puid (@puids) {
699 0           my @ints = $self->get_internal_ids_by_puid($puid);
700 0           my @fileendings = $self->get_file_endings_by_puid($puid);
701 0 0         if ( 0 == scalar(@ints) ) {
702 0           $fileendingsonly++;
703 0           push @fileendingsonly, $puid;
704             }
705             else {
706 0           $fileendings += scalar(@fileendings);
707 0           $int_per_puid += scalar(@ints);
708             }
709             }
710 0           foreach my $i (@quality_sorted_internal_ids) {
711 0           my $regex =
712             join( "#", $self->get_regular_expressions_by_internal_id($i) );
713 0           my $quality = join( " ", $self->get_qualities_by_internal_id($i) );
714              
715             }
716              
717 0           $results->{filename} = $self->get_droid_signature_filename();
718 0           $results->{count_of_puids} = $puids;
719 0           $results->{count_of_internal_ids} = $internals;
720 0           $results->{count_of_regular_expressions} = $regexes;
721 0           $results->{count_of_fileendings} = $fileendings;
722 0           $results->{count_of_puid_with_fileendings_only} = $fileendingsonly;
723 0           $results->{puids_with_fileendings_only} = \@fileendingsonly;
724 0           $results->{count_of_orphaned_internal_ids} = $internal_without_puid;
725 0           $results->{internal_ids_without_puids} = \@internal_without_puid;
726 1     1   8 no warnings;
  1         3  
  1         1649  
727              
728 0           for ( my $i = 0 ; $i <= 4 ; $i++ ) {
729 0           my $best_quality_internal = pop @quality_sorted_internal_ids;
730 0 0         if ( defined $best_quality_internal ) {
731 0           my $best_quality = join( ";",
732             $self->get_qualities_by_internal_id($best_quality_internal) );
733 0           my $best_puid =
734             $self->get_puid_by_internal_id($best_quality_internal);
735 0           my $best_name =
736             $self->get_name_by_signature_id(
737             $self->get_signature_id_by_internal_id($best_quality_internal)
738             );
739 0           my $best_regex = $self->get_combined_regex_by_puid($best_puid);
740             $results->{nth_best_quality}->[$i]->{internal_id} =
741 0           $best_quality_internal;
742 0           $results->{nth_best_quality}->[$i]->{puid} = $best_puid;
743 0           $results->{nth_best_quality}->[$i]->{name} = $best_name;
744 0           $results->{nth_best_quality}->[$i]->{quality} = $best_quality;
745 0           $results->{nth_best_quality}->[$i]->{combined_regex} = $best_regex;
746             }
747             }
748 0           for ( my $i = 0 ; $i <= 4 ; $i++ ) {
749 0           my $worst_quality_internal = shift @quality_sorted_internal_ids;
750 0 0         if ( defined $worst_quality_internal ) {
751 0           my $worst_quality = join( ";",
752             $self->get_qualities_by_internal_id($worst_quality_internal) );
753 0           my $worst_puid =
754             $self->get_puid_by_internal_id($worst_quality_internal);
755 0           my $worst_name =
756             $self->get_name_by_signature_id(
757             $self->get_signature_id_by_internal_id($worst_quality_internal)
758             );
759 0           my $worst_regex = $self->get_combined_regex_by_puid($worst_puid);
760             $results->{nth_worst_quality}->[$i]->{internal_id} =
761 0           $worst_quality_internal;
762 0           $results->{nth_worst_quality}->[$i]->{puid} = $worst_puid;
763 0           $results->{nth_worst_quality}->[$i]->{name} = $worst_name;
764 0           $results->{nth_worst_quality}->[$i]->{quality} = $worst_quality;
765             $results->{nth_worst_quality}->[$i]->{combined_regex} =
766 0           $worst_regex;
767             }
768             }
769             my @multiple_used_regex = grep {
770 0           my $tmp = $uniq_regexes{$_};
  0            
771 0           my @tmp = @{$tmp};
  0            
772 0           scalar(@tmp) > 1
773             } sort keys %uniq_regexes;
774 0           $results->{count_of_multiple_used_regex} = scalar(@multiple_used_regex);
775 0           for ( my $i = 0 ; $i <= $#multiple_used_regex ; $i++ ) {
776             $results->{multiple_used_regex}->[$i]->{regex} =
777 0           $multiple_used_regex[$i];
778 0           my @ids = join( ",", @{ $uniq_regexes{ $multiple_used_regex[$i] } } );
  0            
779 0           $results->{multiple_used_regex}->[$i]->{internal_ids} = \@ids;
780             }
781 0           return $results;
782             }
783              
784 0     0 0   sub print_csv_statistics ($self, $csv_file) {
  0            
  0            
  0            
785 0           my $results = $self->_prepare_statistics();
786 0           my $version = $results->{filename};
787 0           $version =~ s/DROID_SignatureFile_V(\d+)\.xml/$1/;
788 0           $results->{version} = $version;
789 0           $results->{best_quality_puid} = $results->{nth_best_quality}->[0]->{puid};
790             $results->{best_quality_internal_id} =
791 0           $results->{nth_best_quality}->[0]->{internal_id};
792             $results->{best_quality_quality} =
793 0           $results->{nth_best_quality}->[0]->{quality};
794             $results->{best_quality_combined_regex} =
795 0           $results->{nth_best_quality}->[0]->{combined_regex};
796 0           $results->{worst_quality_puid} = $results->{nth_worst_quality}->[0]->{puid};
797             $results->{worst_quality_internal_id} =
798 0           $results->{nth_worst_quality}->[0]->{internal_id};
799             $results->{worst_quality_quality} =
800 0           $results->{nth_worst_quality}->[0]->{quality};
801             $results->{worst_quality_combined_regex} =
802 0           $results->{nth_worst_quality}->[0]->{combined_regex};
803              
804 0           my @headers =
805             qw(version filename count_of_puids count_of_internal_ids count_of_regular_expressions count_of_fileendings count_of_puid_with_fileendings_only count_of_orphaned_internal_ids count_of_multiple_used_regex best_quality_puid best_quality_internal_id best_quality_quality best_quality_combined_regex worst_quality_puid worst_quality_internal_id worst_quality_quality worst_quality_combined_regex);
806 0           my $file_exists = (-e $csv_file);
807 0 0         open (my $FH, ">>", "$csv_file") or croak "Can't open file '$csv_file', $0";
808 0 0         if (not $file_exists) {
809 0           say $FH "#", join( ",", @headers );
810             }
811             say $FH join(
812             ",",
813             map {
814 0           my $result = $results->{$_};
  0            
815 0 0         if ( !defined $result ) { $result = ""; }
  0            
816 0           "\"$result\"";
817             } @headers
818             );
819 0           close ($FH);
820 0           return;
821             }
822              
823 0     0 0   sub print_statistics ($self, $verbose = undef){
  0            
  0            
  0            
824 0           my $results = $self->_prepare_statistics();
825              
826 0           say "Statistics of file $results->{filename}";
827 0           say "=======================================";
828 0           say "";
829 0           say "Countings";
830 0           say "---------------------------------------";
831 0           say "Count of PUIDs: $results->{count_of_puids}";
832 0           say
833             " internal IDs: $results->{count_of_internal_ids}";
834 0           say
835             " regular expressions: $results->{count_of_regular_expressions}";
836 0           say
837             " file endings: $results->{count_of_fileendings}";
838 0           say
839             " PUIDs with file endings only: $results->{count_of_puid_with_fileendings_only}";
840              
841 0 0         if ( defined $verbose ) {
842             say " (",
843 0           join( ", ", sort @{ $results->{puids_with_fileendings_only} } ), ")";
  0            
844             }
845             say
846 0           " orphaned internal IDs: $results->{count_of_orphaned_internal_ids}";
847 0 0         if ( defined $verbose ) {
848             say " (",
849 0           join( ", ", sort {$a <=> $b} @{ $results->{internal_ids_without_puids} } ), ")";
  0            
  0            
850             }
851 0           say "";
852 0           say "Quality of internal IDs";
853 0           say "---------------------------------------";
854              
855 0           my $nth = 1;
856 0           foreach my $n ( @{ $results->{nth_best_quality} } ) {
  0            
857 0           say
858             "$nth-best quality internal ID (PUID, name): $n->{internal_id} ($n->{puid}, $n->{name}) -> $n->{quality}";
859 0 0         if ( defined $verbose ) {
860 0           say " combined regex: ", $n->{combined_regex};
861             }
862 0           $nth++;
863             }
864 0           say "";
865 0           $nth = 1;
866 0           foreach my $n ( @{ $results->{nth_worst_quality} } ) {
  0            
867 0           say
868             "$nth-worst quality internal ID (PUID, name): $n->{internal_id} ($n->{puid}, $n->{name}) -> $n->{quality}";
869 0 0         if ( defined $verbose ) {
870 0           say " combined regex: ", $n->{combined_regex};
871             }
872 0           $nth++;
873             }
874 0           say "";
875              
876 0           say "";
877 0           say "Regular expressions";
878 0           say "---------------------------------------";
879 0           say
880             "Count of multiple used regular expressions: $results->{count_of_multiple_used_regex}";
881 0 0         if ( defined $verbose ) {
882 0           for ( my $i = 0 ; $i < $results->{count_of_multiple_used_regex} ; $i++ )
883             {
884 0           say " common regex group no $i:";
885             say " regex='"
886 0           . $results->{multiple_used_regex}->[$i]->{regex} . "'";
887             say " internal IDs: ",
888 0           join( ",", @{ $results->{multiple_used_regex}->[$i]->{internal_ids} } );
  0            
889             }
890             }
891 0           say "";
892              
893             #my @rx = $self->get_all_regular_expressions();
894             #use Data::Printer;
895             #p( %uniq_regexes );
896 0           return;
897             }
898              
899             1;
900              
901 1     1   9 no Moose;
  1         15  
  1         6  
902             __PACKAGE__->meta->make_immutable;
903             1;
904              
905             __END__
906              
907             =pod
908              
909             =encoding UTF-8
910              
911             =head1 NAME
912              
913             File::FormatIdentification::Pronom
914              
915             =head1 VERSION
916              
917             version 0.07
918              
919             =head1 SYNOPSIS
920              
921             use File::FormatIdentification::Pronom;
922             my $pronomfile = "Droid-Signature.xml";
923             my ( $signatures, $internals ) = parse_signaturefile($pronomfile);
924              
925             =head1 DESCRIPTION
926              
927             The module allows to handle Droid signatures. Droid is a utility which
928             uses the PRONOM database to identify file formats.
929              
930             See https://www.nationalarchives.gov.uk/PRONOM/ for details.
931              
932             With this module you could:
933              
934             =over
935              
936             =item convert Droid signatures to Perl regular expressions
937              
938             =item analyze files and display which/where pattern of Droid signature matches via tag-files for wxHexEditor
939              
940             =item calc statistics about Droid signatures
941              
942             =back
943              
944             The module is in early alpha state and should not be used in production.
945              
946             =head2 Examples
947              
948             =head3 Colorize wxHexeditor fields
949              
950             See example file F<bin/pronom2wxhexeditor.pl>. This colorizes the hex-blob to check PRONOM pattern matches for a given file.
951              
952             =head3 Identify file
953              
954             There are better tools for the job, but as a proof of concept certainly not bad: Identifying the file type of a file.
955              
956             my $pronom = File::FormatIdentification::Pronom->new(
957             "droid_signature_filename" => $pronomfile
958             );
959             # .. $filestream is a scalar representing a file
960             foreach my $internalid ( $pronom->get_all_internal_ids() ) {
961             my $sig = $pronom->get_signature_id_by_internal_id($internalid);
962             next unless defined $sig;
963             my @regexes = $pronom->get_regular_expressions_by_internal_id($internalid);
964             if ( all {$filestream =~ m/$_/saa} @regexes ) {
965             my $puid = $pronom->get_puid_by_signature_id($sig);
966             my $name = $pronom->get_name_by_signature_id($sig);
967             my $quality = $pronom->get_qualities_by_internal_id($internalid);
968             say "$binaryfile identified as $name with PUID $puid (regex quality $quality)";
969             }
970             }
971              
972             See example file F<bin/pronomidentify.pl> for a full working script.
973              
974             =head3 Get PRONOM Statistics
975              
976             To get a feeling for which signatures need to be revised in PRONOM, or why which file formats are difficult to recognize,
977             you can get detailed statistics for given signature files.
978              
979             In the blog entry under L<https://kulturreste.blogspot.com/2018/10/heres-tool-make-it-work.html> the statistic report is presented in more detail.
980              
981             =head2 EXPORT
982              
983             None by default.
984              
985             =head1 NAME
986              
987             File::FormatIdentification::Pronom - Perl extension for parsing PRONOM-Signatures using DROID-Signature file
988              
989             =head1 SEE ALSO
990              
991             L<File::FormatIdentification::Regex>
992              
993             =head1 AUTHOR
994              
995             Andreas Romeyke L<pause@andreas-romeyke.de>
996              
997             =head1 COPYRIGHT AND LICENSE
998              
999             Copyright (C) 2018/19/20 by Andreas Romeyke
1000              
1001             This library is free software; you can redistribute it and/or modify
1002             it under the same terms as Perl itself, either Perl version 5.24.1 or,
1003             at your option, any later version of Perl 5 you may have available.
1004              
1005             The droid-signature file in t/ is from L<https://www.nationalarchives.gov.uk/PRONOM/Default.aspx>
1006             and without guarantee, it does not look like it is legally protected. If there are any legal claims,
1007             please let me know that I can remove them from the distribution.
1008              
1009             =head1 BUGS
1010              
1011             =over
1012              
1013             =item Some droid recipes results in PCREs which are greedy and therefore the running
1014             time could be exponential with size of binary file.
1015              
1016             =back
1017              
1018             =head1 CONTRIBUTING
1019              
1020             Please feel free to send me comments and patches to my email address. You can clone the modules
1021             from L<https://art1pirat.spdns.org/art1/File-FormatIdentification-Pronom> and send me merge requests.
1022              
1023             =head1 AUTHOR
1024              
1025             Andreas Romeyke <pause@andreas-romeyke.de>
1026              
1027             =head1 COPYRIGHT AND LICENSE
1028              
1029             This software is copyright (c) 2018 by Andreas Romeyke.
1030              
1031             This is free software; you can redistribute it and/or modify it under
1032             the same terms as the Perl 5 programming language system itself.
1033              
1034             =cut