File Coverage

blib/lib/App/Licensecheck.pm
Criterion Covered Total %
statement 492 610 80.6
branch 207 284 72.8
condition 109 160 68.1
subroutine 44 59 74.5
pod 0 10 0.0
total 852 1123 75.8


line stmt bran cond sub pod time code
1 11     11   12518371 use Feature::Compat::Class 0.04;
  11         5527  
  11         58  
2              
3 11     11   137778 use v5.12;
  11         41  
4 11     11   86 use utf8;
  11         22  
  11         110  
5 11     11   277 use warnings;
  11         24  
  11         304  
6 11     11   5120 use autodie;
  11         166973  
  11         61  
7              
8             =head1 NAME
9              
10             App::Licensecheck - functions for a simple license checker for source files
11              
12             =head1 VERSION
13              
14             Version v3.3.4
15              
16             =head1 SYNOPSIS
17              
18             use Path::Tiny;
19             use App::Licensecheck;
20              
21             my $tempfile = Path::Tiny->tempfile;
22              
23             $tempfile->spew(<
24             # Dummy file simply stating some copyright and license.
25             # Copyright (C) 2020, 2022 Foo Bar.
26             #
27             # This file is licensed under version 2 or later of the GPL.
28             EOF
29              
30             my $app = App::Licensecheck->new( top_lines => 0 ); # Parse whole files
31              
32             my @output = $app->parse($tempfile);
33              
34             my $license = $output[0]; # => is "GNU General Public License v2.0 or later"
35             my $copyrights = $output[1]; # => is "2020, 2022 Foo Bar."
36              
37             =head1 DESCRIPTION
38              
39             L is the core of L script
40             to check for licenses of source files.
41             See the script for casual usage.
42              
43             =cut
44              
45             package App::Licensecheck v3.3.4;
46              
47 11     11   76432 use Carp qw(croak);
  11         34  
  11         633  
48 11     11   5425 use Log::Any ();
  11         93411  
  11         422  
49 11     11   6010 use List::SomeUtils qw(nsort_by uniq);
  11         158098  
  11         1056  
50 11     11   9659 use Path::Tiny();
  11         162524  
  11         416  
51 11     11   5451 use Feature::Compat::Try;
  11         4299  
  11         59  
52 11     11   21452 use Fcntl qw(:seek);
  11         44  
  11         1840  
53 11     11   106 use Encode 2.93;
  11         239  
  11         1016  
54 11     11   5543 use Array::IntSpan;
  11         36862  
  11         473  
55 11     11   2831 use Regexp::Pattern::License 3.4.0;
  11         985740  
  11         602  
56 11     11   6419 use Regexp::Pattern 0.2.12;
  11         16576  
  11         96  
57             use String::Copyright 0.003 {
58 9   33     9464 format => sub { join ' ', $_->[0] || (), $_->[1] || () }
      33        
59 11     11   6169 };
  11         307573  
  11         185  
60             use String::Copyright 0.003 {
61             threshold_after => 5,
62 3   33     2792 format => sub { join ' ', $_->[0] || (), $_->[1] || () },
      66        
63             },
64 11     11   3751 'copyright' => { -as => 'copyright_optimistic' };
  11         158  
  11         114  
65              
66             class Trait {
67             field $log;
68             field $name :param;
69             field $begin :param;
70             field $end :param;
71             field $file :param;
72              
73             ADJUST {
74             $log = Log::Any->get_logger;
75              
76             $log->tracef(
77             'located trait: %s: %d-%d "%s"',
78             $name, $begin, $end,
79             $file
80             ? substr(
81             $file->content_extracleaned, $begin,
82             $end - $begin
83             )
84             : ''
85             );
86             }
87              
88 3498     3498   6901 method name { return $name }
  3498         11021  
89 685     685   2290 method begin { return $begin }
  685         3580  
90 8717     8717   18098 method end { return $end }
  8717         24632  
91 8     8   27 method file { return $file }
  8         123  
92             }
93              
94             class Exception {
95             field $log;
96             field $id :param;
97             field $begin :param;
98             field $end :param;
99             field $file :param;
100              
101             ADJUST {
102             $log = Log::Any->get_logger;
103              
104             $log->tracef(
105             'detected exception: %s: %d-%d',
106             $id->{caption}, $begin, $end
107             );
108             }
109              
110 56     56   154 method id { return $id }
  56         317  
111 0     0   0 method begin { return $begin }
  0         0  
112 0     0   0 method end { return $end }
  0         0  
113 0     0   0 method file { return $file }
  0         0  
114             }
115              
116             class Flaw {
117             field $log;
118             field $id :param;
119             field $begin :param;
120             field $end :param;
121             field $file :param;
122              
123             ADJUST {
124             $log = Log::Any->get_logger;
125              
126             $log->tracef(
127             'detected flaw: %s: %d-%d',
128             $id->{caption}, $begin, $end
129             );
130             }
131              
132 28     28   79 method id { return $id }
  28         150  
133 0     0   0 method begin { return $begin }
  0         0  
134 0     0   0 method end { return $end }
  0         0  
135 0     0   0 method file { return $file }
  0         0  
136             }
137              
138             class Licensing {
139             field $log;
140             field $name :param;
141              
142             ADJUST {
143             $log = Log::Any->get_logger;
144              
145             $log->debugf(
146             'collected some licensing: %s',
147             $name
148             );
149             }
150              
151 357     357   1533 method name { return $name }
  357         2711  
152             }
153              
154             class Fulltext {
155             field $log;
156             field $name :param;
157             field $begin :param;
158             field $end :param;
159             field $file :param;
160             field $traits :param = undef;
161              
162             ADJUST {
163             $log = Log::Any->get_logger;
164              
165             $log->debugf(
166             'collected fulltext: %s: %d-%d',
167             $name, $begin, $end
168             );
169             }
170              
171 0     0   0 method name { return $name }
  0         0  
172 0     0   0 method begin { return $begin }
  0         0  
173 0     0   0 method end { return $end }
  0         0  
174 0     0   0 method file { return $file }
  0         0  
175 0     0   0 method traits { return $traits }
  0         0  
176             }
177              
178             class Grant {
179             field $log;
180             field $name :param;
181             field $begin :param;
182             field $end :param;
183             field $file :param;
184             field $traits :param = undef;
185              
186             ADJUST {
187             $log = Log::Any->get_logger;
188              
189             $log->debugf(
190             'collected grant: %s: %d-%d "%s"',
191             $name, $begin, $end,
192             $file
193             ? substr(
194             $file->content_extracleaned, $begin,
195             $end - $begin
196             )
197             : ''
198             );
199             }
200              
201 5     5   25 method name { return $name }
  5         23  
202 3     3   9 method begin { return $begin }
  3         14  
203 3     3   12 method end { return $end }
  3         13  
204 0     0   0 method file { return $file }
  0         0  
205 0     0   0 method traits { return $file }
  0         0  
206             }
207              
208 11     11   70004 use namespace::clean qw(-except new);
  11         114783  
  11         92  
209              
210             class App::Licensecheck;
211              
212             # try enable RE2 engine
213             eval { require re::engine::RE2 };
214             my @OPT_RE2 = $@ ? () : ( engine => 'RE2' );
215              
216             # fatalize Unicode::UTF8 and PerlIO::encoding decoding errors
217 11     11   7282 use warnings FATAL => 'utf8';
  11         37  
  11         967  
218             $PerlIO::encoding::fallback = Encode::FB_CROAK;
219              
220 11     11   99 no if ( $] >= 5.034 ), warnings => "experimental::try";
  11         42  
  11         284  
221              
222             field $log;
223              
224             field $path;
225              
226             # resolve patterns
227              
228             field $schemes :param = undef;
229              
230             field @shortname_schemes;
231              
232             # parse
233              
234             field $top_lines :param //= 60;
235             field $end_bytes :param //= 5000; # roughly 60 lines of 80 chars
236             field $encoding :param = undef;
237             field $fh;
238             field $content :param = undef;
239             field $tail_content;
240             field $offset;
241             field $license;
242             field $copyrights;
243              
244             ADJUST {
245             $log = Log::Any->get_logger;
246              
247             if ($schemes) {
248             croak $log->fatal('parameter "schemes" must be an array reference')
249             unless ref($schemes) eq 'ARRAY';
250             @shortname_schemes = @$schemes;
251             }
252             else {
253             $schemes = [];
254             }
255              
256             if ( $encoding and not ref($encoding) eq 'OBJECT' ) {
257             $encoding = find_encoding($encoding);
258             }
259             }
260              
261             method list_licenses
262 0     0 0 0 {
263 0         0 my %names;
264              
265             KEY:
266 0         0 for my $key ( keys %Regexp::Pattern::License::RE ) {
267 0         0 for my $key2 ( keys %{ $Regexp::Pattern::License::RE{$key} } ) {
  0         0  
268 0         0 my %attr;
269 0         0 my @attr = split /[.]/, $key2;
270              
271 0 0       0 next unless $attr[0] eq 'name';
272              
273             # TODO: simplify, and require R::P::License v3.8.1
274 0 0       0 if ( $Regexp::Pattern::License::VERSION < v3.8.1 ) {
275 0 0       0 push @attr, undef
276             if @attr % 2;
277 0         0 %attr = @attr[ 2 .. $#attr ];
278 0 0       0 next if exists $attr{version};
279 0 0       0 next if exists $attr{until};
280             }
281             else {
282 0         0 %attr = @attr[ 2 .. $#attr ];
283 0 0       0 next if exists $attr{until};
284             }
285 0         0 for my $org (@shortname_schemes) {
286 0 0 0     0 if ( exists $attr{org} and $attr{org} eq $org ) {
287 0         0 $names{$key} = $Regexp::Pattern::License::RE{$key}{$key2};
288 0         0 next KEY;
289             }
290             }
291             }
292 0   0     0 $names{$key} = $Regexp::Pattern::License::RE{$key}{name} // $key;
293             }
294 0         0 my @result = sort { lc $a cmp lc $b } values %names;
  0         0  
295              
296 0         0 return @result;
297             }
298              
299             sub list_naming_schemes
300             {
301 0     0 0 0 my $_prop = '(?:[a-z][a-z0-9_]*)';
302 0         0 my $_any = '[a-z0-9_.()]';
303              
304             my @result = uniq sort
305 0         0 map {/^(?:name|caption)\.alt\.org\.($_prop)$_any*/}
306 0         0 map { keys %{ $Regexp::Pattern::License::RE{$_} } }
  0         0  
307 0         0 grep {/^[a-z]/} keys %Regexp::Pattern::License::RE;
  0         0  
308              
309 0         0 return @result;
310             }
311              
312             method parse
313 347     347 0 1187 {
314 347         997 ($path) = @_;
315              
316 347         1678 $path = Path::Tiny::path($path);
317              
318             try {
319             return $self->parse_file;
320             }
321 347         12823 catch ($e) {
322             if ( $encoding and $e =~ /does not map to Unicode/ ) {
323             $log->warnf(
324             'failed decoding file %s as %s, will try iso-8859-1',
325             $path, $encoding->name
326             );
327             $log->debugf( 'decoding error: %s', $e );
328             try {
329             $encoding = find_encoding('iso-8859-1');
330             return $self->parse_file;
331             }
332             catch ($e) {
333             if (/does not map to Unicode/) {
334             $log->warnf(
335             'failed decoding file %s as iso-8859-1, will try raw',
336             $path
337             );
338             $log->debugf( 'decoding error: %s', $e );
339             $encoding = undef;
340             return $self->parse_file;
341             }
342             else {
343             die $log->fatalf( 'unknown error: %s', $e );
344             }
345             }
346             }
347             else {
348             die $log->fatalf( 'unknown error: %s', $e );
349             }
350             }
351             }
352              
353             method parse_file
354 347     347 0 1164 {
355             # TODO: stop reuse slots across files, and drop this hack
356 347         766 $content = undef;
357 347         622 $license = undef;
358 347         728 $copyrights = undef;
359              
360 347 100       1113 if ( $top_lines == 0 ) {
361 340         1242 $license = $self->parse_license;
362 340         2326 $copyrights = copyright( $self->content_cleaned );
363             }
364             else {
365 7         25 $license = $self->parse_license;
366 7         36 $copyrights = copyright_optimistic( $self->content_cleaned );
367 7 100 33     3765 if ( $offset and not $copyrights and $license eq 'UNKNOWN' ) {
      66        
368              
369             # TODO: stop reuse slots across files, and drop this hack
370 6         87 $tail_content = undef;
371              
372 6         18 $license = $self->parse_license;
373 6         45 $copyrights = copyright_optimistic( $self->content_cleaned );
374             }
375 7         11858 $fh->close;
376             }
377              
378 347         2392560 return ( $license, $copyrights );
379             }
380              
381             method content
382 3557     3557 0 9209 {
383 3557 100 100     8677 if ( $top_lines == 0 ) {
    100          
    50          
384 3506 100       17020 return $content
385             if defined($content);
386              
387 340 50       1168 if ( not defined($encoding) ) {
388 340         1612 $log->debugf( 'reading whole file %s as raw bytes', $path );
389 340         2598 $content = $path->slurp_raw;
390             }
391             else {
392 0         0 my $id = $encoding->name;
393 0         0 $log->debugf( 'decoding whole file %s as %s', $path, $id );
394 0         0 $content = $path->slurp( { binmode => ":encoding($id)" } );
395             }
396 340 50       96101 $log->trace("----- content -----\n$content----- end content -----")
397             if $log->is_trace;
398             }
399             elsif ( not defined($license) or not defined($copyrights) ) {
400              
401             # TODO: distinguish header from full content
402 19 100       76 return $content
403             if defined($content);
404              
405 7         18 $content = '';
406              
407 7 50       18 if ( not defined($encoding) ) {
408 7         43 $log->debugf( 'reading part(s) of file %s as raw bytes', $path );
409 7         1442 $fh = $path->openr_raw;
410             }
411             else {
412 0         0 my $id = $encoding->name;
413 0         0 $log->debugf( 'decoding part(s) of file %s as %s', $path, $id );
414 0         0 $fh = $path->openr(":encoding($id)");
415             }
416              
417 7         1512 while ( my $line = $fh->getline ) {
418 77 100       11702 last if ( $fh->input_line_number > $top_lines );
419 70         2160 $content .= $line;
420             }
421 7 50       133 $log->trace("----- header -----\n$content----- end header -----")
422             if $log->is_trace;
423              
424 7 50       448 if ($end_bytes) {
425 7         34 my $position = $fh->tell; # see IO::Seekable
426 7         68 my $filesize = $path->stat->size;
427 7 100       9815 if ( $position >= $filesize - $end_bytes ) { # header overlaps
    50          
    0          
428 1 50       11 if ( $position < $filesize ) {
    0          
429 1         66 $log->debugf(
430             'tail offset set to %s (end of header)',
431             $position
432             );
433 1         91 $offset = $position;
434             }
435             elsif ( $position = $filesize ) {
436 0         0 $log->debug('header end matches file size');
437 0         0 $offset = 0;
438             }
439             else {
440 0         0 $log->error('header end beyond file size');
441 0         0 $offset = 0;
442             }
443             }
444             elsif ( $position > 0 ) {
445 6         16 $offset = $filesize - $end_bytes;
446 6         25 $log->debugf(
447             'tail offset set to %s',
448             $offset
449             );
450             }
451             elsif ( $position < 0 ) {
452 0         0 $log->error('header end could not be resolved');
453 0         0 $offset = 0;
454             }
455             else {
456 0         0 $log->error('header end oddly at beginning of file');
457 0         0 $offset = 0;
458             }
459             }
460             }
461             elsif ($offset) {
462              
463             # TODO: distinguish tail from full content
464 32 100       140 return $content
465             if defined($tail_content);
466              
467 6         14 $tail_content = '';
468 6         42 $fh->seek( $offset, SEEK_SET ); # see IO::Seekable
469 6         336 $tail_content .= join( '', $fh->getlines );
470 6 50       679 $log->trace("----- tail -----\n$tail_content----- end tail -----")
471             if $log->is_trace;
472              
473 6         457 $content = $tail_content;
474             }
475             else {
476 0         0 $log->errorf(
477             'tail offset not usable: %s',
478             $offset
479             );
480 0         0 return '';
481             }
482              
483             # TODO: distinguish comment-mangled content from pristine content
484 353 50       7445 local $_ = $content or return '';
485              
486             # Remove generic comments: look for 4 or more lines beginning with
487             # regular comment pattern and trim it. Fall back to old algorithm
488             # if no such pattern found.
489 353         14790 my @matches = m/^[ \t]*([^a-zA-Z0-9\s]{1,3})[ \t]+\S/mg;
490 353 100       1591 if ( @matches >= 4 ) {
491 133         4919 my $comment_re = qr/^[ \t]*[\Q$matches[0]\E]{1,3}[ \t]*/m;
492 133         5870 s/$comment_re//g;
493             }
494              
495 353         9257 my @wordmatches = m/^[ \t]*(dnl|REM|COMMENT)[ \t]+\S/mg;
496 353 100       1408 if ( @wordmatches >= 4 ) {
497 3         151 my $comment_re = qr/^[ \t]*\Q$wordmatches[0]\E[ \t]*/m;
498 3         87 s/$comment_re//g;
499             }
500              
501             # Remove other side of "boxed" comments
502 353         58304 s/[ \t]*[*#][ \t]*$//gm;
503              
504             # Remove Fortran comments
505 353         5937 s/^[cC]$//gm;
506 353         3366 s/^[cC] //gm;
507              
508             # Remove C / C++ comments
509 353         290025 s#(\*/|/\*|(?
510              
511             # Strip escaped newline
512 353         2011 s/\s*\\n\s*/ /g;
513              
514 353         1129 $content = $_;
515              
516 353         2072 return $content;
517             }
518              
519             my $html_xml_tags_re = qr/<\/?(?:p|br|ref)(?:\s[^>]*)?>/i;
520              
521             # clean cruft
522             method content_cleaned
523 353     353 0 1276 {
524 353 50       1478 local $_ = $self->content or return '';
525              
526             # strip common html and xml tags
527 353         3493 s/$html_xml_tags_re//g;
528              
529             # TODO: decode latin1/UTF-8/HTML data instead
530 353         5999 s/\xcb\x97|\xe2\x80[\x90-\x95|\xe2\x81\x83|\xe2\x88\x92|\xef\x89\xa3|\xef\xbc\x8d]|[&](?:ndash|mdash|horbar|minus|[#](?:727|820[8-9]|821[0-3]|8259|8722|65123|65293|x727|z201[0-5]|x2043|x2212|xFE63|xFF0D))[;]/-/gm;
531 353         5990 s/\x58\xa9|\xc2\xa9|\xe2\x92\x9e|\xe2\x92\xb8|\xe2\x93\x92|\xf0\x9f\x84\x92|\xf0\x9f\x84\xab|\xf0\x9f\x85\x92|[&](?:copy|[#](?:169|9374|9400|9426|127250|127275|127314|x0A9|x249E|x24b8|x24D2|x0F112|x0F12B|x0F152))[;]/©/gm;
532              
533             # TODO: decode nroff files specifically instead
534 353         2341 s/\\//gm; # de-cruft nroff files
535              
536 353         2942 return $_;
537             }
538              
539             # clean cruft and whitespace
540             method content_extracleaned
541 3204     3204 0 8218 {
542 3204 50       8393 local $_ = $self->content or return '';
543              
544             # strip trailing dash, assuming it is soft-wrap
545             # (example: disclaimers in GNU autotools file "install-sh")
546 3204         48131 s/-\r?\n//g;
547              
548             # strip common html and xml tags
549 3204         22061 s/$html_xml_tags_re//g;
550              
551 3204         68073 tr/\t\r\n/ /;
552              
553             # this also removes quotes
554 3204         80528 tr% A-Za-z.,:@;0-9\(\)/-%%cd;
555 3204         171904 tr/ //s;
556              
557 3204         31980 return $_;
558             }
559              
560             my $any = '[A-Za-z_][A-Za-z0-9_]*';
561             my $str = '[A-Za-z][A-Za-z0-9_]*';
562             my $re_prop_attrs = qr/
563             \A(?'prop'$str)\.alt(?:
564             \.org\.(?'org'$str)|
565             \.version\.(?'version'$str)|
566             \.since\.date_(?'since_date'\d{8})|
567             \.until\.date_(?'until_date'\d{8})|
568             \.synth\.$any|
569             (?'other'\.$any)
570             )*\z/x;
571              
572             method best_value
573 12492     12492 0 28819 {
574 12492         25950 my ( $hashref, @props ) = @_;
575 12492         15719 my $value;
576              
577             PROPERTY:
578 12492         20263 for my $prop (@props) {
579 12520         17478 for my $org (@shortname_schemes) {
580 16688         97752 for ( keys %$hashref ) {
581 312324         1287826 /$re_prop_attrs/;
582 11 100 100 11   66404 next unless $+{prop} and $+{prop} eq $prop;
  11         4813  
  11         134543  
  312324         1782140  
583 32275 100 100     212061 next unless $+{org} and $+{org} eq $org;
584 4352 50       17034 next if $+{version};
585 4352 100       14761 next if $+{other};
586 4174 100       13877 next if $+{until_date};
587              
588 3724         12788 $value = $hashref->{$_};
589 3724         7479 last PROPERTY;
590             }
591             }
592 8796   100     35149 $value ||= $hashref->{$prop};
593             }
594              
595 12492         53141 return $value;
596             }
597              
598             my $type_re
599             = qr/^type:([a-z][a-z0-9_]*)(?::([a-z][a-z0-9_]*))?(?::([a-z][a-z0-9_]*))?/;
600              
601             our %RE;
602             my ( %L, @RE_EXCEPTION, @RE_LICENSE, @RE_NAME );
603              
604             method init_licensepatterns
605 353     353 0 1384 {
606             # reuse if already resolved
607 353 100       1786 return %L if exists $L{re_trait};
608              
609 11         397 Regexp::Pattern->import(
610             're',
611             'License::*' => (
612             @OPT_RE2,
613             subject => 'trait',
614             -prefix => 'EXCEPTION_',
615             -has_tag_matching => '^type:trait:exception(?:\z|:)',
616             -lacks_tag_matching => '^type:trait:exception:prefix(?:\z|:)',
617             ),
618             'License::*' => (
619             @OPT_RE2,
620             capture => 'named',
621             subject => 'trait',
622             -prefix => 'TRAIT_',
623             -has_tag_matching => '^type:trait(?:\z|:)',
624             -lacks_tag_matching => '^type:trait:exception(?!:prefix)(?:\z|:)',
625             ),
626             'License::version' => (
627             @OPT_RE2,
628             capture => 'named',
629             subject => 'trait',
630             anchorleft => 1,
631             -prefix => 'ANCHORLEFT_NAMED_',
632             ),
633             'License::version_later' => (
634             @OPT_RE2,
635             capture => 'named',
636             subject => 'trait',
637             anchorleft => 1,
638             -prefix => 'ANCHORLEFT_NAMED_',
639             ),
640             'License::any_of' => (
641             subject => 'trait',
642             -prefix => 'LOCAL_TRAIT_',
643             ),
644             'License::by_fsf' => (
645             subject => 'trait',
646             -prefix => 'LOCAL_TRAIT_',
647             ),
648             'License::fsf_unlimited' => (
649             subject => 'trait',
650             -prefix => 'LOCAL_TRAIT_',
651             ),
652             'License::fsf_unlimited_retention' => (
653             subject => 'trait',
654             -prefix => 'LOCAL_TRAIT_',
655             ),
656             'License::licensed_under' => (
657             subject => 'trait',
658             -prefix => 'LOCAL_TRAIT_',
659             ),
660             'License::or_at_option' => (
661             subject => 'trait',
662             -prefix => 'LOCAL_TRAIT_',
663             ),
664             'License::version' => (
665             capture => 'numbered',
666             subject => 'trait',
667             -prefix => 'LOCAL_TRAIT_KEEP_',
668             ),
669             'License::version_numberstring' => (
670             capture => 'numbered',
671             subject => 'trait',
672             -prefix => 'LOCAL_TRAIT_KEEP_',
673             ),
674             'License::apache' => (
675             subject => 'name',
676             -prefix => 'LOCAL_NAME_',
677             ),
678             'License::gpl' => (
679             subject => 'name',
680             -prefix => 'LOCAL_NAME_',
681             ),
682             'License::lgpl' => (
683             subject => 'name',
684             -prefix => 'LOCAL_NAME_',
685             ),
686             'License::mit' => (
687             subject => 'name',
688             -prefix => 'LOCAL_NAME_',
689             ),
690             'License::*' => (
691             @OPT_RE2,
692             subject => 'name',
693             -prefix => 'NAME_',
694             anchorleft => 1,
695             -lacks_tag_matching => '^type:trait(?:\z|:)',
696             ),
697             'License::*' => (
698             @OPT_RE2,
699             subject => 'grant',
700             -prefix => 'GRANT_',
701             -lacks_tag_matching => '^type:trait(?:\z|:)',
702             ),
703             'License::*' => (
704             @OPT_RE2,
705             subject => 'license',
706             -prefix => 'LICENSE_',
707             -lacks_tag_matching => '^type:trait(?:\z|:)',
708             ),
709             );
710              
711 11         15218249 @RE_EXCEPTION = sort map /^EXCEPTION_(.*)/, keys(%RE);
712 11         13010 @RE_LICENSE = sort map /^LICENSE_(.*)/, keys(%RE);
713 11         12946 @RE_NAME = sort map /^NAME_(.*)/, keys(%RE);
714              
715 11         2596 foreach my $key ( grep {/^[a-z]/} keys(%Regexp::Pattern::License::RE) ) {
  6204         10795  
716 6204         18724 my $val = $Regexp::Pattern::License::RE{$key};
717 6204   66     12249 $L{name}{$key} = $self->best_value( $val, 'name' ) || $key;
718             $L{caption}{$key}
719 6204   66     13276 = $self->best_value( $val, 'caption' ) || $val->{name} || $key;
720 6204         9667 foreach ( @{ $val->{tags} } ) {
  6204         18670  
721 11737 100       49675 /$type_re/ or next;
722 6215         18610 $L{type}{$1}{$key} = 1;
723 6215 100 100     20791 if ( $2 and $1 eq 'singleversion' ) {
724 2343         5988 $L{series}{$key} = $2;
725             }
726 6215 100 100     17446 if ( $2 and $1 eq 'usage' ) {
727 297         871 $L{usage}{$key} = $2;
728             }
729              
730             # TODO: simplify, and require Regexp::Pattern::License v3.9.0
731 6215 100 100     17643 if ( $3 and $1 eq 'trait' ) {
732 253 100       886 if ( substr( $key, 0, 14 ) eq 'except_prefix_' ) {
733 55         299 $L{TRAITS_exception_prefix}{$key} = undef;
734             }
735             else {
736 198         1143 $L{"TRAITS_$2_$3"}{$key} = undef;
737             }
738             }
739             }
740             }
741              
742             # FIXME: drop when perl doesn't mysteriously freak out over it
743 11         683 foreach (qw(any_of)) {
744 11         103 $L{re_trait}{$_} = '';
745             }
746              
747             #<<< do not let perltidy touch this (keep long regex on one line)
748 11         27550 $L{multi_1} = qr/$RE{LOCAL_TRAIT_licensed_under}$RE{LOCAL_TRAIT_any_of}(?:[^.]|\.\S)*$RE{LOCAL_NAME_lgpl}$RE{LOCAL_TRAIT_KEEP_version}?/i;
749 11         10643 $L{multi_2} = qr/$RE{LOCAL_TRAIT_licensed_under}$RE{LOCAL_TRAIT_any_of}(?:[^.]|\.\S)*$RE{LOCAL_NAME_gpl}$RE{LOCAL_TRAIT_KEEP_version}?/i;
750 11         32109 $L{lgpl_5} = qr/$RE{LOCAL_TRAIT_licensed_under}$RE{LOCAL_NAME_lgpl}(?:$RE{LOCAL_TRAIT_by_fsf})?[,;:]?(?: either)? ?$RE{LOCAL_TRAIT_KEEP_version_numberstring},? $RE{LOCAL_TRAIT_or_at_option} $RE{LOCAL_TRAIT_KEEP_version_numberstring}/i;
751 11         27913 $L{gpl_7} = qr/either $RE{LOCAL_NAME_gpl}$RE{LOCAL_TRAIT_KEEP_version}?(?: \((?:the )?"?GPL"?\))?, or $RE{LOCAL_NAME_lgpl}$RE{LOCAL_TRAIT_KEEP_version}?/i;
752 11         314 $L{bsd_1} = qr/THIS SOFTWARE IS PROVIDED (?:BY (?:\S+ ){1,15})?AS IS AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY/;
753 11         13223 $L{apache_1} = qr/$RE{LOCAL_NAME_apache}$RE{LOCAL_TRAIT_KEEP_version}?(?:(?: or)? [^ ,]*?apache[^ ,]*| \([^(),]+\))*,? or $RE{LOCAL_NAME_gpl}$RE{LOCAL_TRAIT_KEEP_version}?/i;
754 11         3862 $L{apache_2} = qr/$RE{LOCAL_NAME_apache}$RE{LOCAL_TRAIT_KEEP_version}?(?:(?: or)? [^ ,]*?apache[^ ,]*| \([^(),]\))*,? or(?: the)? bsd(?:[ -](\d)-clause)?\b/i;
755 11         3898 $L{apache_4} = qr/$RE{LOCAL_NAME_apache}$RE{LOCAL_TRAIT_KEEP_version}?(?:(?: or)? [^ ,]*?apache[^ ,]*| \([^(),]\))*,? or $RE{LOCAL_NAME_mit}\b/i;
756 11         684 $L{fsful} = qr/This (\w+)(?: (?:file|script))? is free software; $RE{LOCAL_TRAIT_fsf_unlimited}/i;
757 11         597 $L{fsfullr} = qr/This (\w+)(?: (?:file|script))? is free software; $RE{LOCAL_TRAIT_fsf_unlimited_retention}/i;
758 11         111 $L{trailing_space} = qr/\s+$/;
759 11         104 $L{LEFTANCHOR_version_of} = qr/^ of /;
760             #>>>
761             }
762              
763             # license objects where atomic scan must always be applied
764             my %L_grant_stepwise_incomplete = (
765              
766             # usage
767              
768             # singleversion
769             apache_2 => 1,
770              
771             # versioned
772             gpl => 1,
773             lgpl => 1,
774              
775             # other
776             mit_new => 1, # misdetects ambiguous "MIT X11" grant
777             public_domain => 1,
778             );
779              
780             # license objects where stepwise scan cannot be skipped
781             my %L_grant_atomic_incomplete = (
782             afl_1_1 => 1,
783             afl_1_2 => 1,
784             afl_2 => 1,
785             afl_2_1 => 1,
786             afl_3 => 1,
787             apache_1_1 => 1,
788             artistic_1 => 1,
789             artistic_2 => 1,
790             bsl_1 => 1,
791             cc_by_2_5 => 1,
792             cc_by_sa => 1,
793             cpl_1 => 1,
794             mpl => 1,
795             mpl_1 => 1,
796             mpl_1_1 => 1,
797             mpl_2 => 1,
798             openssl => 1,
799             postgresql => 1,
800             zpl_2_1 => 1,
801             );
802              
803             # scan for grants first stepwise and if not found then also atomic
804             # flip either of these flags to test stepwise/atomic pattern coverage
805             my $skip_stepwise = 0;
806             my $force_atomic = 0;
807              
808             my $contains_bsd2_re = qr/^license:contains:license:bsd_2_clause/;
809             my @L_contains_bsd = grep {
810             $Regexp::Pattern::License::RE{$_}{tags}
811             and grep /$contains_bsd2_re/,
812             @{ $Regexp::Pattern::License::RE{$_}{tags} }
813             } keys(%Regexp::Pattern::License::RE);
814              
815             my $id2patterns_re = qr/(.*)(?:_(\d+(?:\.\d+)*)(_or_later)?)?/;
816              
817             method parse_license
818 353     353 0 862 {
819 353         1270 my $licensetext = $self->content_extracleaned;
820              
821 353         2302 $self->init_licensepatterns;
822              
823 353         902 my @L_type_usage = sort keys %{ $L{type}{usage} };
  353         9046  
824 353         1344 my @L_type_singleversion = sort keys %{ $L{type}{singleversion} };
  353         51516  
825 353         3559 my @L_type_versioned = sort keys %{ $L{type}{versioned} };
  353         21699  
826 353         2105 my @L_type_unversioned = sort keys %{ $L{type}{unversioned} };
  353         31583  
827 353         2562 my @L_type_combo = sort keys %{ $L{type}{combo} };
  353         2026  
828 353         868 my @L_type_group = sort keys %{ $L{type}{group} };
  353         1932  
829              
830 353         986 my $license = "";
831 353         607 my @spdx_gplver;
832              
833 353         1095 my @agpl = qw(agpl agpl_1 agpl_2 agpl_3);
834 353         920 my @gpl = qw(gpl gpl_1 gpl_2 gpl_3);
835 353         895 my @lgpl = qw(lgpl lgpl_2 lgpl_2_1 lgpl_3);
836              
837 353         3316 my $coverage = Array::IntSpan->new();
838 353         6219 my %match;
839 353         1162 my ( %grant, %license );
840              
841             # @clues, @expressions, and @exceptions contains DEP-5 or SPDX identifiers,
842             # and @flaws contains non-SPDX notes.
843 353         0 my ( @clues, @expressions, @exceptions, @flaws );
844              
845             my $patterns2id = sub {
846 3     3   11 my ( $id, $ver ) = @_;
847 3 50       10 return $id
848             unless ($ver);
849 3         7 $_ = $ver;
850 3         16 s/\.0$//g;
851 3         7 s/\./_/g;
852 3         17 return "${id}_$_";
853 353         2783 };
854             my $id2patterns = sub {
855 234     234   3330 return $_[0] =~ /$id2patterns_re/;
856 353         1651 };
857             my $gen_license = sub {
858 357     357   1622 my ( $id, $v, $later, $id2, $v2, $later2 ) = @_;
859 357         732 my @spdx;
860 357   33     2193 my $name = $L{name}{$id} || $id;
861 357   33     1769 my $desc = $L{caption}{$id} || $id;
862 357 100       1003 if ($v) {
863 5 100       34 push @spdx, $later ? "$name-$v+" : "$name-$v";
864 5 100       26 $v .= ' or later' if ($later);
865             }
866             else {
867 352         1017 push @spdx, $name;
868             }
869 357         746 my ( $name2, $desc2 );
870 357 100       1015 if ($id2) {
871 5   33     36 $name2 = $L{name}{$id2} || $id2;
872 5   33     18 $desc2 = $L{caption}{$id2} || $id2;
873 5 100       18 if ($v2) {
874 4 100       23 push @spdx, $later2 ? "$name2-$v2+" : "$name2-$v2";
875 4 100       16 $v2 .= ' or later' if ($later2);
876             }
877             else {
878 1         3 push @spdx, $name2;
879             }
880             }
881 357 100       2511 my $legacy = join(
    100          
    100          
882             ' ',
883             $desc,
884             $v ? "(v$v)" : (),
885             $desc2 ? "or $desc2" : (),
886             $v2 ? "(v$v2)" : (),
887             );
888 357         1657 my $expr = join( ' or ', sort @spdx );
889 357         5056 push @expressions, Licensing->new( name => $expr );
890 357   66     5901 $license = join( ' ', $L{caption}{$legacy} || $legacy, $license );
891 353         2196 };
892              
893             # fulltext
894 353         1867 $log->trace('scan for license fulltext');
895 353         2018 my %pos_license;
896 353         1153 foreach my $id (@RE_LICENSE) {
897 172264 100       654001 next unless ( $RE{"LICENSE_$id"} );
898 126374         26968444 while ( $licensetext =~ /$RE{"LICENSE_$id"}/g ) {
899 329         82148 $pos_license{ $-[0] }{$id} = Trait->new(
900             name => "license($id)",
901             begin => $-[0],
902             end => $+[0],
903             file => $self,
904             );
905             }
906             }
907              
908 353         1340 foreach my $trait ( keys %{ $L{TRAITS_exception_prefix} } ) {
  353         3271  
909              
910 1765 100       875888 next unless ( $licensetext =~ /$RE{"TRAIT_$trait"}/ );
911 45         60100 while ( $licensetext =~ /$RE{"TRAIT_$trait"}/g ) {
912             next
913             if (
914             defined(
915 49 50       1551 $coverage->get_range( $-[0], $+[0] )->get_element(0)
916             )
917             );
918 49         4246 push @clues,
919             Trait->new(
920             name => $trait,
921             begin => $-[0],
922             end => $+[0],
923             file => $self,
924             );
925             }
926             }
927 353         3580 foreach my $pos ( sort { $a <=> $b } keys %pos_license ) {
  151         760  
928              
929             # pick longest or most specific among matched license fulltexts
930 329     329   3929 my @licenses = nsort_by { $pos_license{$pos}{$_}->end }
931 297 100       3864 grep { $pos_license{$pos}{$_} ? $pos_license{$pos}{$_}->end : () } (
  144342         261766  
932             @L_type_group,
933             @L_type_combo,
934             @L_type_unversioned,
935             @L_type_versioned,
936             @L_type_singleversion,
937             @L_type_usage,
938             );
939 297         3224 my $license = pop @licenses;
940 297 50       1059 next unless ($license);
941             next
942             if defined(
943 297 100       1146 $coverage->get_range( $pos, $pos_license{$pos}{$license}->end )
944             ->get_element(0) );
945             $coverage->set_range(
946             $pos_license{$pos}{$license}->begin,
947             $pos_license{$pos}{$license}->end,
948 276         15674 $pos_license{$pos}{$license}
949             );
950 276         9747 $license{$license} = 1;
951             }
952              
953             # grant, stepwise
954 353         5229 $log->trace('scan stepwise for license grant');
955 353         2367 foreach my $trait ( keys %{ $L{TRAITS_grant_prefix} } ) {
  353         2323  
956              
957 1412         571155 while ( $licensetext =~ /$RE{"TRAIT_$trait"}/g ) {
958             next
959             if (
960             defined(
961 1714 100       1326292 $coverage->get_range( $-[0], $+[0] )->get_element(0)
962             )
963             );
964 1617         91668 push @clues,
965             Trait->new(
966             name => $trait,
967             begin => $-[0],
968             end => $+[0],
969             file => $self,
970             );
971             }
972             }
973             LICENSED_UNDER:
974 353         29070 foreach my $licensed_under (
975 2396         4152 sort { $a->end <=> $b->end }
976 1666         3709 grep { exists $L{TRAITS_grant_prefix}{ $_->name } } @clues
977             )
978             {
979 1617         6100 my $pos = $licensed_under->end;
980              
981             # possible grant names
982 1617         72740 my @grant_types = (
983             @L_type_combo,
984             @L_type_unversioned,
985             @L_type_versioned,
986             @L_type_singleversion,
987             @L_type_usage,
988             );
989              
990             # optional grant version
991 1617         3555 my ( $version, $later );
992              
993             # scan for prepended version
994 1617         19629 substr( $licensetext, $pos ) =~ $RE{ANCHORLEFT_NAMED_version};
995 1617 100       13367 if ( $+{version_number} ) {
996 9         139 push @clues,
997             Trait->new(
998             name => 'version',
999             begin => $pos + $-[0],
1000             end => $pos + $+[0],
1001             file => $self,
1002             );
1003 9         185 $version = $+{version_number};
1004 9 50       75 if ( $+{version_later} ) {
1005 0         0 push @clues,
1006             Trait->new(
1007             name => 'or_later',
1008             begin => $pos + $-[2],
1009             end => $pos + $+[2],
1010             file => $self,
1011             );
1012 0         0 $later = $+{version_later};
1013             }
1014 9 50       178 if (substr( $licensetext, $pos + $+[0] )
1015             =~ $L{LEFTANCHOR_version_of} )
1016             {
1017 0         0 push @clues,
1018             Trait->new(
1019             name => 'version_of',
1020             begin => $pos + $-[0],
1021             end => $pos + $+[0],
1022             file => $self,
1023             );
1024 0         0 $pos += $+[0];
1025 0         0 @grant_types = @L_type_versioned;
1026             }
1027             else {
1028 9         31 $version = '';
1029             }
1030             }
1031              
1032             # scan for name
1033 1617         5013 foreach my $id (@RE_NAME) {
1034 789096 100       9724122 if ( substr( $licensetext, $pos ) =~ $RE{"NAME_$id"} ) {
1035 598         7458 $match{$id}{name}{ $pos + $-[0] } = Trait->new(
1036             name => "name($id)",
1037             begin => $pos + $-[0],
1038             end => $pos + $+[0],
1039             file => $self,
1040             );
1041             }
1042             }
1043              
1044             # pick longest matched license name
1045             # TODO: include all of most specific type when more are longest
1046 452     452   3569 my @names = nsort_by { $match{$_}{name}{$pos}->end }
1047 1617 50 66     13224 grep { $match{$_} and $match{$_}{name} and $match{$_}{name}{$pos} }
  779394         1339702  
1048             @grant_types;
1049 1617         17075 my $name = pop @names;
1050 1617 50 66     27765 if ( $name
      66        
      33        
      66        
1051             and $match{$name}{name}{$pos}
1052             and !defined(
1053             $coverage->get_range( $pos, $match{$name}{name}{$pos}->end )
1054             ->get_element(0)
1055             )
1056             and ( !$skip_stepwise or $L_grant_atomic_incomplete{$name} )
1057             )
1058             {
1059 215         11101 my $pos_end = $pos = $match{$name}{name}{$pos}->end;
1060              
1061             # may include version
1062 215 100 66     969 if ( !$version and grep { $_ eq $name } @L_type_versioned ) {
  21715 100 66     34702  
1063 40         641 substr( $licensetext, $pos ) =~ $RE{ANCHORLEFT_NAMED_version};
1064 40 100       399 if ( $+{version_number} ) {
1065 4         46 push @clues, Trait->new(
1066             name => 'version',
1067             begin => $pos + $-[0],
1068             end => $pos + $+[0],
1069             file => $self,
1070             );
1071 4         59 $version = $+{version_number};
1072 4         23 $pos_end = $pos + $+[1];
1073 4 50       30 if ( $+{version_later} ) {
1074 0         0 push @clues, Trait->new(
1075             name => 'or_later',
1076             begin => $pos + $-[2],
1077             end => $pos + $+[2],
1078             file => $self,
1079             );
1080 0         0 $later = $+{version_later};
1081 0         0 $pos_end = $pos + $+[2];
1082             }
1083             }
1084             }
1085 37275         57809 elsif ( !$version and grep { $_ eq $name } @L_type_singleversion )
1086             {
1087             substr( $licensetext, $pos )
1088 96         1542 =~ $RE{ANCHORLEFT_NAMED_version_later};
1089 96 100       895 if ( $+{version_later} ) {
1090 3         34 push @clues, Trait->new(
1091             name => 'or_later',
1092             begin => $pos + $-[1],
1093             end => $pos + $+[1],
1094             file => $self,
1095             );
1096 3         52 $later = $+{version_later};
1097 3         11 $pos_end = $pos + $+[1];
1098             }
1099             }
1100 215 100       854 if ($version) {
1101 4         23 $version =~ s/(?:\.0)+$//;
1102 4         13 $version =~ s/\./_/g;
1103 4         16 $name .= "_$version";
1104             }
1105 215 100       696 if ($later) {
1106 3         12 my $latername = "${name}_or_later";
1107 3         13 push @clues, Trait->new(
1108             name => $latername,
1109             begin => $licensed_under->begin,
1110             end => $pos_end,
1111             file => $self,
1112             );
1113 3         48 $grant{$latername} = $clues[-1];
1114 3 50       13 next LICENSED_UNDER if grep { $grant{$_} } @RE_NAME;
  1464         2201  
1115             }
1116 212         1160 $grant{$name} = Trait->new(
1117             name => "grant($name)",
1118             begin => $licensed_under->begin,
1119             end => $pos_end,
1120             file => $self,
1121             );
1122 212         8393 push @clues, $grant{$name};
1123             }
1124             }
1125              
1126             # GNU oddities
1127 353 100       1716 if ( grep { $match{$_}{name} } @agpl, @gpl, @lgpl ) {
  4236         11234  
1128 103         570 $log->trace('scan for GNU oddities');
1129              
1130             # address in AGPL/GPL/LGPL
1131 103         37110 while ( $licensetext =~ /$RE{TRAIT_addr_fsf}/g ) {
1132 43         165 foreach (
1133             qw(addr_fsf_franklin_steet addr_fsf_mass addr_fsf_temple))
1134             {
1135 129 100       18974 if ( defined $+{$_} ) {
1136             push @flaws, Flaw->new(
1137 14         237 id => $Regexp::Pattern::License::RE{$_},
1138             begin => $-[0],
1139             end => $+[0],
1140             file => $self,
1141             );
1142             }
1143             }
1144             }
1145             }
1146              
1147             # exceptions
1148             # TODO: conditionally limit to AGPL/GPL/LGPL
1149 353         2487 foreach (@RE_EXCEPTION) {
1150 12708 100       2751861 if ( $licensetext =~ $RE{"EXCEPTION_$_"} ) {
1151             my $exception = Exception->new(
1152 56         1014 id => $Regexp::Pattern::License::RE{$_},
1153             begin => $-[0],
1154             end => $+[0],
1155             file => $self,
1156             );
1157 56         905 $coverage->set_range( $-[0], $+[0], $exception );
1158 56         2127 push @exceptions, $exception;
1159             }
1160             }
1161              
1162             # oddities
1163 353         2780 $log->trace('scan for oddities');
1164              
1165             # generated file
1166 353 100       486115 if ( $licensetext =~ $RE{TRAIT_generated} ) {
1167             push @flaws, Flaw->new(
1168             id => $Regexp::Pattern::License::RE{generated},
1169 14         197 begin => $-[0],
1170             end => $+[0],
1171             file => $self,
1172             );
1173             }
1174              
1175             # multi-licensing
1176 353         1328 my @multilicenses;
1177              
1178             # LGPL, dual-licensed
1179             # FIXME: add test covering this pattern
1180 353 100       1268 if ( grep { $match{$_}{name} } @lgpl ) {
  1412         4107  
1181 31         171 $log->trace('scan for LGPL dual-license grant');
1182 31 50       947 if ( $licensetext =~ $L{multi_1} ) {
1183 0         0 my $meta = Trait->new(
1184             name => 'grant(multi#1)',
1185             begin => $-[0],
1186             end => $+[0],
1187             file => $self,
1188             );
1189 0         0 $log->tracef(
1190             'detected custom pattern multi#1: %s %s %s: %s',
1191             'lgpl', $1, $2, $-[0]
1192             );
1193 0         0 push @multilicenses, 'lgpl', $1, $2;
1194             }
1195             }
1196              
1197             # GPL, dual-licensed
1198             # FIXME: add test covering this pattern
1199 353 100       921 if ( grep { $match{$_}{name} } @gpl ) {
  1412         3463  
1200 74         313 $log->trace('scan for GPL dual-license grant');
1201 74 50       1747 if ( $licensetext =~ $L{multi_2} ) {
1202 0         0 $log->tracef(
1203             'detected custom pattern multi#2: %s %s %s: %s',
1204             'gpl', $1, $2, $-[0]
1205             );
1206 0         0 push @multilicenses, 'gpl', $1, $2;
1207             }
1208             }
1209              
1210 353 50       1201 $gen_license->(@multilicenses) if (@multilicenses);
1211              
1212             # LGPL
1213 353 100       848 if ( grep { $match{$_}{name} } @lgpl ) {
  1412         3013  
1214 31         182 $log->trace('scan for LGPL fulltext/grant');
1215              
1216             # LGPL, dual versions last
1217 31 100       81846 if ( $licensetext =~ $L{lgpl_5} ) {
1218 5         72 my $grant = Trait->new(
1219             name => 'grant(lgpl#5)',
1220             begin => $-[0],
1221             end => $+[0],
1222             file => $self,
1223             );
1224 5         94 $license = "LGPL (v$1 or v$2) $license";
1225 5         25 my $expr = "LGPL-$1 or LGPL-$2";
1226 5         21 push @expressions,
1227             Grant->new(
1228             name => $expr,
1229             begin => $grant->begin,
1230             end => $grant->end,
1231             file => $grant->file,
1232             );
1233 5         83 $match{ 'lgpl_' . $1 =~ tr/./_/r }{custom} = 1;
1234 5         52 $match{ 'lgpl_' . $2 =~ tr/./_/r }{custom} = 1;
1235 5         24 $match{lgpl}{custom} = 1;
1236             }
1237             }
1238              
1239             # GPL or LGPL
1240 353 100       1052 if ( grep { $match{$_}{name} } @gpl ) {
  1412         3360  
1241 74         316 $log->trace('scan for GPL or LGPL dual-license grant');
1242 74 100       7171 if ( $licensetext =~ $L{gpl_7} ) {
1243 2         26 my $grant = Trait->new(
1244             name => "grant(gpl#7)",
1245             begin => $-[0],
1246             end => $+[0],
1247             file => $self,
1248             );
1249 2         30 $gen_license->( 'gpl', $1, $2, 'lgpl', $3, $4 );
1250 2         8 $match{gpl}{custom} = 1;
1251 2         8 $match{lgpl}{custom} = 1;
1252             }
1253             }
1254              
1255             # BSD
1256 353 100 100     1153 if ( grep { $match{$_}{name} } @L_contains_bsd
  3177         8358  
1257             and $licensetext =~ $L{bsd_1} )
1258             {
1259 1         6 $log->trace('scan for BSD fulltext');
1260 1         17 my $grant = Trait->new(
1261             name => 'license(bsd#1)',
1262             begin => $-[0],
1263             end => $+[0],
1264             file => $self,
1265             );
1266 1         14 for ($licensetext) {
1267 1 50       5 next if ( $license{bsd_4_clause} );
1268 1 50       20 if ( $licensetext =~ $RE{TRAIT_clause_advertising} ) {
1269 0         0 my $grant = Trait->new(
1270             name => 'clause_advertising',
1271             begin => $-[0],
1272             end => $+[0],
1273             file => $self,
1274             );
1275 0         0 $gen_license->('bsd_4_clause');
1276 0         0 next;
1277             }
1278 1 50       6 next if ( $license{bsd_3_clause} );
1279 1 50       12 if ( $licensetext =~ $RE{TRAIT_clause_non_endorsement} ) {
1280 0         0 my $grant = Trait->new(
1281             name => 'clause_non_endorsement',
1282             begin => $-[0],
1283             end => $+[0],
1284             file => $self,
1285             );
1286 0         0 $gen_license->('bsd_3_clause');
1287 0         0 next;
1288             }
1289 1 50       6 next if ( $license{bsd_2_clause} );
1290 1 50       17 if ( $licensetext =~ $RE{TRAIT_clause_reproduction} ) {
1291             next
1292             if (
1293             defined(
1294 1 50       14 $coverage->get_range( $-[0], $+[0] )->get_element(0)
1295             )
1296             );
1297 0         0 my $grant = Trait->new(
1298             name => 'clause_reproduction',
1299             begin => $-[0],
1300             end => $+[0],
1301             file => $self,
1302             );
1303 0         0 $gen_license->('bsd_2_clause');
1304 0         0 next;
1305             }
1306 0         0 $gen_license->('bsd');
1307             }
1308             }
1309              
1310             # Apache dual-licensed with GPL/BSD/MIT
1311 353 100       1774 if ( $match{apache}{name} ) {
1312 11         67 $log->trace('scan for Apache license grant');
1313 11         65 for ($licensetext) {
1314 11 100       10952 if ( $licensetext =~ $L{apache_1} ) {
1315 2         35 my $grant = Trait->new(
1316             name => 'grant(apache#1)',
1317             begin => $-[0],
1318             end => $+[0],
1319             file => $self,
1320             );
1321 2         40 $gen_license->( 'apache', $1, $2, 'gpl', $3, $4 );
1322 2         20 $match{ $patterns2id->( 'apache', $1 ) }{custom} = 1;
1323 2         11 next;
1324             }
1325 9 50       9989 if ( $licensetext =~ $L{apache_2} ) {
1326 0         0 my $grant = Trait->new(
1327             name => 'grant(apache#2)',
1328             begin => $-[0],
1329             end => $+[0],
1330             file => $self,
1331             );
1332 0 0       0 $gen_license->(
1333             'apache', $1, $2,
1334             $3 ? "bsd_${3}_clause" : ''
1335             );
1336 0         0 $match{ $patterns2id->( 'apache', $1 ) }{custom} = 1;
1337 0         0 next;
1338             }
1339 9 100       217 if ( $licensetext =~ $L{apache_4} ) {
1340 1         13 my $grant = Trait->new(
1341             name => 'grant(apache#4)',
1342             begin => $-[0],
1343             end => $+[0],
1344             file => $self,
1345             );
1346 1         12 $gen_license->( 'apache', $1, $2, 'mit', $3, $4 );
1347 1         13 $match{ $patterns2id->( 'apache', $1 ) }{custom} = 1;
1348 1         6 next;
1349             }
1350             }
1351             }
1352              
1353             # FSFUL
1354             # FIXME: add test covering this pattern
1355 353         1559 $log->trace('scan for FSFUL fulltext');
1356 353 100       2630 if ( not $license{fsful} ) {
1357 352 50       4298 if ( $licensetext =~ $L{fsful} ) {
1358 0         0 my $grant = Trait->new(
1359             name => 'grant(fsful#1)',
1360             begin => $-[0],
1361             end => $+[0],
1362             file => $self,
1363             );
1364 0         0 $license = "FSF Unlimited ($1 derivation) $license";
1365 0         0 my $expr = "FSFUL~$1";
1366 0         0 push @expressions,
1367             Fulltext->new(
1368             name => $expr,
1369             begin => $grant->begin,
1370             end => $grant->end,
1371             file => $grant->file,
1372             );
1373 0         0 $match{fsful}{custom} = 1;
1374             }
1375             }
1376              
1377             # FSFULLR
1378             # FIXME: add test covering this pattern
1379 353         1430 $log->trace('scan for FSFULLR fulltext');
1380 353 100       2168 if ( not $license{fsfullr} ) {
1381 352 50       3924 if ( $licensetext =~ $L{fsfullr} ) {
1382 0         0 my $grant = Trait->new(
1383             name => 'grant(fsfullr#1)',
1384             begin => $-[0],
1385             end => $+[0],
1386             file => $self,
1387             );
1388 0         0 $license
1389             = "FSF Unlimited (with Retention, $1 derivation) $license";
1390 0         0 my $expr = "FSFULLR~$1";
1391 0         0 push @expressions,
1392             Fulltext->new(
1393             name => $expr,
1394             begin => $grant->begin,
1395             end => $grant->end,
1396             file => $grant->file,
1397             );
1398 0         0 $match{fsfullr}{custom} = 1;
1399             }
1400             }
1401              
1402             # usage
1403 353         1436 $log->trace('scan atomic for singleversion usage license grant');
1404 353         2056 foreach my $id (@L_type_usage) {
1405 9531 50       25773 next if ( $match{$id}{custom} );
1406 9531 50 33     32448 if ( !$grant{$id}
      66        
1407             and ( $L_grant_stepwise_incomplete{$id} or $force_atomic ) )
1408             {
1409 0 0       0 if ( $licensetext =~ $RE{"GRANT_$id"} ) {
1410 0         0 my $grant = Trait->new(
1411             name => "grant($id)",
1412             begin => $-[0],
1413             end => $+[0],
1414             file => $self,
1415             );
1416 0 0       0 unless (
1417             defined(
1418             $coverage->get_range( $-[0], $+[0] )->get_element(0)
1419             )
1420             )
1421             {
1422 0         0 $grant{$id} = Grant->new(
1423             name => $id,
1424             begin => $grant->begin,
1425             end => $grant->end,
1426             file => $grant->file,
1427             );
1428             }
1429             }
1430             }
1431              
1432 9531 100       17852 if ( $grant{$id} ) {
1433             $coverage->set_range(
1434             $grant{$id}->begin, $grant{$id}->end,
1435 67         389 $grant{$id}
1436             );
1437 67         2476 $gen_license->( $id2patterns->($id) );
1438              
1439             # skip singleversion and unversioned equivalents
1440 67 50       451 if ( $L{usage}{$id} ) {
1441 67         730 $log->tracef( 'flagged license object: %s', $id );
1442 67         391 $match{ $L{usage}{$id} }{custom} = 1;
1443 67 50       339 if ( $L{series}{ $L{usage}{$id} } ) {
1444             $log->tracef(
1445             'flagged license object: %s',
1446 67         297 $L{usage}{$id}
1447             );
1448 67         396 $match{ $L{series}{ $L{usage}{$id} } }{custom} = 1;
1449             }
1450             }
1451             }
1452             }
1453              
1454             # singleversion
1455 353         1975 $log->trace('scan atomic for singleversion license grant');
1456 353         2357 foreach my $id (@L_type_singleversion) {
1457 75189 100 100     450075 if ( !$license{$id}
      66        
      66        
1458             and !$grant{$id}
1459             and !$match{$id}{custom}
1460             and ( $L_grant_stepwise_incomplete{$id} or $force_atomic ) )
1461             {
1462 343 50       2101040 if ( $licensetext =~ $RE{"GRANT_$id"} ) {
1463 0         0 my $grant = Trait->new(
1464             name => "grant($id)",
1465             begin => $-[0],
1466             end => $+[0],
1467             file => $self,
1468             );
1469 0 0       0 unless (
1470             defined(
1471             $coverage->get_range( $-[0], $+[0] )->get_element(0)
1472             )
1473             )
1474             {
1475 0         0 $grant{$id} = Grant->new(
1476             name => $id,
1477             begin => $grant->begin,
1478             end => $grant->end,
1479             file => $grant->file,
1480             );
1481             }
1482             }
1483             }
1484              
1485 75189 100 100     203061 if ( $license{$id} or $grant{$id} ) {
1486             $coverage->set_range(
1487             $grant{$id}->begin, $grant{$id}->end,
1488             $grant{$id}
1489 178 100       1432 ) if $grant{$id};
1490             $gen_license->( $id2patterns->($id) )
1491 178 100       4846 unless ( $match{$id}{custom} );
1492              
1493             # skip unversioned equivalent
1494 178 50       1130 if ( $L{series}{$id} ) {
1495 178         938 $log->tracef( 'flagged license object: %s', $id );
1496 178         1655 $match{ $L{series}{$id} }{custom} = 1;
1497             }
1498             }
1499             }
1500              
1501             # versioned
1502 353         2514 $log->trace('scan atomic for versioned license grant');
1503 353         2783 foreach my $id (@L_type_versioned) {
1504 35653 100       99170 next if ( $match{$id}{custom} );
1505              
1506             # skip name part of another name detected as grant
1507             # TODO: use less brittle method than name of clue
1508             next
1509             if ( $id eq 'cc_by'
1510 35410 100 100     63905 and grep { $_->name eq 'grant(cc_by_sa_3)' } @clues );
  1832         4122  
1511              
1512             # skip embedded or referenced licenses
1513 35408 100 100     61055 next if ( $license{rpsl_1} and grep { $id eq $_ } qw(mpl python) );
  400         867  
1514              
1515 35406 50       56458 next if ( $license{$id} );
1516 35406 100 66     117532 if ( !$grant{$id}
      66        
1517             and ( $L_grant_stepwise_incomplete{$id} or $force_atomic ) )
1518             {
1519 590 50       4091 if ( $RE{"GRANT_$id"} ) {
1520 590 100       3647607 if ( $licensetext =~ $RE{"GRANT_$id"} ) {
1521 5         104 my $grant = Trait->new(
1522             name => "grant($id)",
1523             begin => $-[0],
1524             end => $+[0],
1525             file => $self,
1526             );
1527 5 50       102 unless (
1528             defined(
1529             $coverage->get_range( $-[0], $+[0] )
1530             ->get_element(0)
1531             )
1532             )
1533             {
1534 0         0 $grant{$id} = Grant->new(
1535             name => $id,
1536             begin => $grant->begin,
1537             end => $grant->end,
1538             file => $grant->file,
1539             );
1540             }
1541             }
1542             }
1543             }
1544              
1545 35406 100       68263 if ( $grant{$id} ) {
1546             $coverage->set_range(
1547             $grant{$id}->begin, $grant{$id}->end,
1548 15         111 $grant{$id}
1549             );
1550 15         851 $gen_license->($id);
1551             }
1552             }
1553              
1554             # other
1555             # TODO: add @L_type_group
1556 353         2967 $log->trace('scan atomic for misc fulltext/grant');
1557 353         2939 foreach my $id ( @L_type_unversioned, @L_type_combo ) {
1558 49773 50 66     181119 next if ( !$license{$id} and $match{$id}{custom} );
1559              
1560             next
1561             unless ( $license{$id}
1562             or $grant{$id}
1563 49773 100 100     225076 or $L_grant_stepwise_incomplete{$id}
      100        
      66        
1564             or $force_atomic );
1565              
1566             # skip embedded or referenced licenses
1567 799 50 33     2700 next if ( $license{caldera} and $id eq 'bsd' );
1568 799 50 66     2305 next if ( $license{cube} and $id eq 'zlib' );
1569 799 50 66     1918 next if ( $license{dsdp} and $id eq 'ntp' );
1570 799 50 66     2046 next if ( $license{mit_cmu} and $id eq 'ntp_disclaimer' );
1571 799 50 66     2033 next if ( $license{ntp_disclaimer} and $id eq 'ntp' );
1572              
1573 799 100 100     2709564 if ( !$license{$id}
      100        
1574             and !$grant{$id}
1575             and $licensetext =~ $RE{"GRANT_$id"} )
1576             {
1577 3         49 my $grant = Trait->new(
1578             name => "grant($id)",
1579             begin => $-[0],
1580             end => $+[0],
1581             file => $self,
1582             );
1583 3 50       57 unless (
1584             defined(
1585             $coverage->get_range( $-[0], $+[0] )->get_element(0)
1586             )
1587             )
1588             {
1589 3         176 $grant{$id} = Grant->new(
1590             name => $id,
1591             begin => $grant->begin,
1592             end => $grant->end,
1593             file => $grant->file,
1594             );
1595             }
1596             }
1597 799 100 100     5008 if ( $license{$id} or $grant{$id} ) {
1598             $coverage->set_range(
1599             $grant{$id}->begin, $grant{$id}->end,
1600             $grant{$id}
1601 103 100       459 ) if $grant{$id};
1602 103         1334 $gen_license->($id);
1603             }
1604             }
1605              
1606 353         6500 $license =~ s/$L{trailing_space}//;
1607 353         1624 my $expr = join( ' and/or ', sort map { $_->name } @expressions );
  362         1917  
1608 353   100     1324 $expr ||= 'UNKNOWN';
1609 353 100       1172 if (@exceptions) {
1610 54 100       265 $expr = "($expr)"
1611             if ( @expressions > 1 );
1612             $expr .= ' with ' . join(
1613             '_AND_',
1614 54         153 sort map { $self->best_value( $_->id, 'name' ) } @exceptions
  56         263  
1615             ) . ' exception';
1616             }
1617 353 100       1098 if (@flaws) {
1618             $license .= ' [' . join(
1619             ', ',
1620 28         80 sort map { $self->best_value( $_->id, qw(caption name) ) } @flaws
  28         128  
1621             ) . ']';
1622             }
1623 353         2308 $log->infof( 'resolved license expression: %s', $expr );
1624 353   50     52087 return ( @$schemes ? $expr : $license ) || 'UNKNOWN';
1625             }
1626              
1627             =encoding UTF-8
1628              
1629             =head1 AUTHOR
1630              
1631             Jonas Smedegaard C<< >>
1632              
1633             =head1 COPYRIGHT AND LICENSE
1634              
1635             This program is based on the script "licensecheck" from the KDE SDK,
1636             originally introduced by Stefan Westerfeld C<< >>.
1637              
1638             Copyright © 2007, 2008 Adam D. Barratt
1639              
1640             Copyright © 2012 Francesco Poli
1641              
1642             Copyright © 2016-2022 Jonas Smedegaard
1643              
1644             Copyright © 2017-2022 Purism SPC
1645              
1646             This program is free software:
1647             you can redistribute it and/or modify it
1648             under the terms of the GNU Affero General Public License
1649             as published by the Free Software Foundation,
1650             either version 3, or (at your option) any later version.
1651              
1652             This program is distributed in the hope that it will be useful,
1653             but WITHOUT ANY WARRANTY;
1654             without even the implied warranty
1655             of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
1656             See the GNU Affero General Public License for more details.
1657              
1658             You should have received a copy
1659             of the GNU Affero General Public License along with this program.
1660             If not, see .
1661              
1662             =cut
1663              
1664             1;