File Coverage

blib/lib/String/License.pm
Criterion Covered Total %
statement 431 467 92.2
branch 157 198 79.2
condition 108 148 72.9
subroutine 32 33 96.9
pod 2 9 22.2
total 730 855 85.3


line stmt bran cond sub pod time code
1 10     10   10345674 use v5.20;
  10         126  
2 10     10   70 use utf8;
  10         25  
  10         114  
3 10     10   236 use warnings;
  10         28  
  10         297  
4 10     10   55 use feature qw(signatures);
  10         26  
  10         947  
5 10     10   60 no warnings qw(experimental::signatures);
  10         40  
  10         415  
6              
7 10     10   4533 use Feature::Compat::Class 0.04;
  10         4046  
  10         53  
8              
9             =head1 NAME
10              
11             String::License - detect source code license statements in a text string
12              
13             =head1 VERSION
14              
15             Version v0.0.6
16              
17             =head1 SYNOPSIS
18              
19             use String::License;
20             use String::License::Naming::Custom;
21              
22             my $string = 'Licensed under same terms as Perl itself';
23              
24             my $expressed = String::License->new( string => $string );
25             my $expression = $expressed->as_text; # => "Perl"
26              
27             my $desc = String::License::Naming::Custom->new;
28             my $described = String::License->new( string => $string, naming => $desc );
29             my $description = $described->as_text; # => "The Perl 5 License"
30              
31             =head1 DESCRIPTION
32              
33             L identifies license statements in a string
34             and serializes them in a normalized format.
35              
36             =cut
37              
38             package String::License v0.0.6;
39              
40 10     10   121868 use Carp qw(croak);
  10         32  
  10         477  
41 10     10   4457 use Log::Any ();
  10         89005  
  10         344  
42 10     10   84 use Scalar::Util qw(blessed);
  10         25  
  10         641  
43 10     10   5199 use List::SomeUtils qw(nsort_by uniq);
  10         139024  
  10         1007  
44 10     10   5035 use Array::IntSpan;
  10         33207  
  10         550  
45 10     10   5035 use Regexp::Pattern::License 3.4.0;
  10         1931955  
  10         845  
46 10     10   5657 use Regexp::Pattern 0.2.12;
  10         14342  
  10         64  
47 10     10   4935 use String::License::Naming::Custom;
  10         31  
  10         426  
48 10     10   4058 use String::License::Naming::SPDX;
  10         28  
  10         430  
49              
50 10     10   70 use namespace::clean;
  10         23  
  10         40  
51              
52             class Tag {
53             field $id :param;
54             field $begin :param;
55             field $end :param;
56              
57 336     336   774 method data () { return lc __CLASS__, $id, $begin, $end }
  336         523  
  336         463  
  336         1941  
58             }
59              
60             class Exception :isa(Tag) { }
61              
62             class Flaw :isa(Tag) { }
63              
64             class Licensing {
65             field $id :param;
66              
67 333     333   698 method data () { return lc __CLASS__, $id }
  333         538  
  333         512  
  333         1807  
68             }
69              
70             class Fulltext :isa(Tag) { }
71              
72             class Grant :isa(Tag) { }
73              
74             class String::License;
75              
76             # try enable RE2 engine
77             eval { require re::engine::RE2 };
78             my @OPT_RE2 = $@ ? () : ( engine => 'RE2' );
79              
80             field $log;
81              
82             =head1 CONSTRUCTOR
83              
84             =over
85              
86             =item new
87              
88             my $licensed = String::License->new( string => 'Licensed under GPLv2' );
89              
90             Accepts named arguments,
91             and constructs and returns a String::License object.
92              
93             The following options are recognized:
94              
95             =over
96              
97             =item string => STRING
98              
99             The scalar string to parse for licensing information.
100              
101             =cut
102              
103             field $string :param = undef;
104              
105             =item naming => OBJ
106              
107             A L object,
108             used to define license naming conventions.
109              
110             By default uses L.
111              
112             Since instantiation of naming schemes is expensive,
113             there can be a significant speed boost
114             in passing a pre-initialized naming object
115             when processing multiple strings.
116              
117             =cut
118              
119             field $naming :param = undef;
120              
121             field $license = '';
122             field $expr = '';
123              
124             field $coverage;
125             field @loose_licensing;
126              
127             =back
128              
129             =back
130              
131             =cut
132              
133             ADJUST {
134             $log = Log::Any->get_logger;
135              
136             if ( defined $naming ) {
137             croak $log->fatal(
138             'parameter "naming" must be a String::License::Naming object')
139             unless defined blessed($naming)
140             and $naming->isa('String::License::Naming');
141             }
142             else {
143             $naming = String::License::Naming::SPDX->new;
144             }
145              
146             $coverage = Array::IntSpan->new();
147             }
148              
149             =head1 METHODS
150              
151             =over
152              
153             =cut
154              
155 2651         3639 method note ( $name, $begin, $end )
  2651         3772  
  2651         4253  
  2651         3635  
  2651         3267  
156 2651     2651 0 7084 {
157 2651         10732 $log->tracef(
158             'noted %s: %d-%d "%s"',
159             $name, $begin, $end,
160             substr( $string, $begin, $end - $begin )
161             );
162              
163 2651         81271 return [ $begin, $end ];
164             }
165              
166 669         1027 method tag ($obj)
  669         1022  
  669         889  
167 669     669 0 1743 {
168 669         1212 my ( $name, $desc, $type, $id, $begin, $end );
169              
170 669         2111 ( $type, $id, $begin, $end ) = $obj->data;
171              
172 669 100       1929 if ( ref($id) ) {
173 584         1402 ( $name, $desc ) = @$id;
174             }
175             else {
176             $name
177 85         465 = $self->best_value( $Regexp::Pattern::License::RE{$id}, 'name' );
178             $desc = $self->best_value(
179 85         266 $Regexp::Pattern::License::RE{$id},
180             qw(caption name)
181             );
182             }
183              
184 669 100       1747 if ( $type eq 'licensing' ) {
185 333         973 push @loose_licensing, [ $type, $name, $desc ];
186             }
187             else {
188 336         1872 $coverage->set_range( $begin, $end, [ $type, $name, $desc ] );
189 336         10688 $log->tracef(
190             'tagged %s: %s: %d-%d',
191             $type, $desc, $begin, $end
192             );
193             }
194              
195 669         2778 return $obj;
196             }
197              
198 2144         2934 method contains_tag ( $begin, $end )
  2144         3742  
  2144         2999  
  2144         2641  
199 2144     2144 0 5822 {
200 2144         6366 return defined( $coverage->get_range( $begin, $end )->get_element(0) );
201             }
202              
203 331         611 method get_tags ()
  331         475  
204 331     331 0 1357 {
205 331         630 my ( @thing, %set );
206              
207 331         1659 @thing = $coverage->get_range_list;
208 331         6693 $set{grant}{ $_->[1] } = $_ for @loose_licensing;
209 331         1413 for my $i ( 0 .. $#thing ) {
210 336         676 my ( $begin, $end, $thing, $type, $key );
211              
212 336         936 ( $begin, $end, $thing ) = $coverage->get_element($i);
213 336         3379 $type = $thing->[0];
214              
215             # TODO: drop fallback when all flaws have shortname
216 336   66     971 $key = $thing->[1] || $thing->[2];
217              
218 336 50       1716 next unless $type =~ /^[a-z]/;
219              
220 336         1624 $set{$type}{$key} = $thing;
221             }
222              
223             return (
224 331         1220 [ values %{ $set{fulltexts} }, values %{ $set{grant} } ],
  331         1169  
225 331         1210 [ values %{ $set{exception} } ],
226 331         1098 [ values %{ $set{flaw} } ],
  331         2349  
227             );
228             }
229              
230 0         0 method string ()
  0         0  
231 0     0 1 0 {
232 0         0 return $string;
233             }
234              
235             my $any = '[A-Za-z_][A-Za-z0-9_]*';
236             my $str = '[A-Za-z][A-Za-z0-9_]*';
237             my $re_prop_attrs = qr/
238             \A(?'prop'$str)\.alt(?:
239             \.org\.(?'org'$str)|
240             \.version\.(?'version'$str)|
241             \.since\.date_(?'since_date'\d{8})|
242             \.until\.date_(?'until_date'\d{8})|
243             \.synth\.$any|
244             (?'other'\.$any)
245             )*\z/x;
246              
247 11450         14452 method best_value ( $hashref, @props )
  11450         15050  
  11450         17217  
  11450         13580  
248 11450     11450 0 26506 {
249 11450         13926 my $value;
250              
251             PROPERTY:
252 11450         18248 for my $prop (@props) {
253 11530         28233 for my $org ( $naming->list_schemes ) {
254 16916         93059 for ( keys %$hashref ) {
255 312914         1262611 /$re_prop_attrs/;
256 10 100 100 10   37671 next unless $+{prop} and $+{prop} eq $prop;
  10         4811  
  10         102195  
  312914         1754871  
257 31208 100 100     201859 next unless $+{org} and $+{org} eq $org;
258 3885 50       14699 next if $+{version};
259 3885 100       12950 next if $+{other};
260 3712 100       12342 next if $+{until_date};
261              
262 3282         11039 $value = $hashref->{$_};
263 3282         6383 last PROPERTY;
264             }
265             }
266 8248   100     35478 $value ||= $hashref->{$prop};
267             }
268              
269 11450         46309 return $value;
270             }
271              
272             my $type_re
273             = qr/^type:([a-z][a-z0-9_]*)(?::([a-z][a-z0-9_]*))?(?::([a-z][a-z0-9_]*))?/;
274              
275             our %RE;
276             my ( %L, @RE_EXCEPTION, @RE_LICENSE, @RE_NAME );
277              
278 331         525 method init_licensepatterns ()
  331         599  
279 331     331 0 640 {
280             # reuse if already resolved
281 331 100       1232 return %L if exists $L{re_trait};
282              
283 10         360 Regexp::Pattern->import(
284             're',
285             'License::*' => (
286             @OPT_RE2,
287             subject => 'trait',
288             -prefix => 'EXCEPTION_',
289             -has_tag_matching => '^type:trait:exception(?:\z|:)',
290             -lacks_tag_matching => '^type:trait:exception:prefix(?:\z|:)',
291             ),
292             'License::*' => (
293             @OPT_RE2,
294             capture => 'named',
295             subject => 'trait',
296             -prefix => 'TRAIT_',
297             -has_tag_matching => '^type:trait(?:\z|:)',
298             -lacks_tag_matching => '^type:trait:exception(?!:prefix)(?:\z|:)',
299             ),
300             'License::version' => (
301             @OPT_RE2,
302             capture => 'named',
303             subject => 'trait',
304             anchorleft => 1,
305             -prefix => 'ANCHORLEFT_NAMED_',
306             ),
307             'License::version_later' => (
308             @OPT_RE2,
309             capture => 'named',
310             subject => 'trait',
311             anchorleft => 1,
312             -prefix => 'ANCHORLEFT_NAMED_',
313             ),
314             'License::any_of' => (
315             subject => 'trait',
316             -prefix => 'LOCAL_TRAIT_',
317             ),
318             'License::by_fsf' => (
319             subject => 'trait',
320             -prefix => 'LOCAL_TRAIT_',
321             ),
322             'License::fsf_unlimited' => (
323             subject => 'trait',
324             -prefix => 'LOCAL_TRAIT_',
325             ),
326             'License::fsf_unlimited_retention' => (
327             subject => 'trait',
328             -prefix => 'LOCAL_TRAIT_',
329             ),
330             'License::licensed_under' => (
331             subject => 'trait',
332             -prefix => 'LOCAL_TRAIT_',
333             ),
334             'License::or_at_option' => (
335             subject => 'trait',
336             -prefix => 'LOCAL_TRAIT_',
337             ),
338             'License::version' => (
339             capture => 'numbered',
340             subject => 'trait',
341             -prefix => 'LOCAL_TRAIT_KEEP_',
342             ),
343             'License::version_numberstring' => (
344             capture => 'numbered',
345             subject => 'trait',
346             -prefix => 'LOCAL_TRAIT_KEEP_',
347             ),
348             'License::apache' => (
349             subject => 'name',
350             -prefix => 'LOCAL_NAME_',
351             ),
352             'License::gpl' => (
353             subject => 'name',
354             -prefix => 'LOCAL_NAME_',
355             ),
356             'License::lgpl' => (
357             subject => 'name',
358             -prefix => 'LOCAL_NAME_',
359             ),
360             'License::mit' => (
361             subject => 'name',
362             -prefix => 'LOCAL_NAME_',
363             ),
364             'License::*' => (
365             @OPT_RE2,
366             subject => 'name',
367             -prefix => 'NAME_',
368             anchorleft => 1,
369             -lacks_tag_matching => '^type:trait(?:\z|:)',
370             ),
371             'License::*' => (
372             @OPT_RE2,
373             subject => 'grant',
374             -prefix => 'GRANT_',
375             -lacks_tag_matching => '^type:trait(?:\z|:)',
376             ),
377             'License::*' => (
378             @OPT_RE2,
379             subject => 'license',
380             -prefix => 'LICENSE_',
381             -lacks_tag_matching => '^type:trait(?:\z|:)',
382             ),
383             );
384              
385 10         16380551 @RE_EXCEPTION = sort map {/^EXCEPTION_(.*)/} keys(%RE);
  15540         20361  
386 10         2366 @RE_LICENSE = sort map {/^LICENSE_(.*)/} keys(%RE);
  15540         27644  
387 10         2785 @RE_NAME = sort map {/^NAME_(.*)/} keys(%RE);
  15540         25987  
388              
389 10         2195 foreach my $key ( grep {/^[a-z]/} keys(%Regexp::Pattern::License::RE) ) {
  5640         9733  
390 5640         15923 my $val = $Regexp::Pattern::License::RE{$key};
391 5640   66     11538 $L{name}{$key} = $self->best_value( $val, 'name' ) || $key;
392             $L{caption}{$key}
393 5640   66     11829 = $self->best_value( $val, 'caption' ) || $val->{name} || $key;
394 5640         8473 foreach ( @{ $val->{tags} } ) {
  5640         16987  
395 10670 100       44815 /$type_re/ or next;
396 5650         16769 $L{type}{$1}{$key} = 1;
397 5650 100 100     18761 if ( $2 and $1 eq 'singleversion' ) {
398 2130         5330 $L{series}{$key} = $2;
399             }
400 5650 100 100     16141 if ( $2 and $1 eq 'usage' ) {
401 270         776 $L{usage}{$key} = $2;
402             }
403              
404             # TODO: simplify, and require Regexp::Pattern::License v3.9.0
405 5650 100 100     15959 if ( $3 and $1 eq 'trait' ) {
406 230 100       769 if ( substr( $key, 0, 14 ) eq 'except_prefix_' ) {
407 50         207 $L{TRAITS_exception_prefix}{$key} = undef;
408             }
409             else {
410 180         1001 $L{"TRAITS_$2_$3"}{$key} = undef;
411             }
412             }
413             }
414             }
415              
416             # FIXME: drop when perl doesn't mysteriously freak out over it
417 10         431 foreach (qw(any_of)) {
418 10         119 $L{re_trait}{$_} = '';
419             }
420              
421             #<<< do not let perltidy touch this (keep long regex on one line)
422 10         20892 $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;
423 10         8276 $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;
424 10         23382 $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;
425 10         22342 $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;
426 10         8471 $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;
427 10         3100 $L{apache_2} = qr/$RE{LOCAL_NAME_apache}$RE{LOCAL_TRAIT_KEEP_version}?(?:(?: or)? [^ ,]*?apache[^ ,]*| \([^(),]\))*,? or(?: the)? bsd(?:[ -](\d)-clause)?\b/i;
428 10         2983 $L{apache_4} = qr/$RE{LOCAL_NAME_apache}$RE{LOCAL_TRAIT_KEEP_version}?(?:(?: or)? [^ ,]*?apache[^ ,]*| \([^(),]\))*,? or $RE{LOCAL_NAME_mit}\b/i;
429 10         646 $L{fsful} = qr/This (\w+)(?: (?:file|script))? is free software; $RE{LOCAL_TRAIT_fsf_unlimited}/i;
430 10         582 $L{fsfullr} = qr/This (\w+)(?: (?:file|script))? is free software; $RE{LOCAL_TRAIT_fsf_unlimited_retention}/i;
431 10         82 $L{trailing_space} = qr/\s+$/;
432 10         81 $L{LEFTANCHOR_version_of} = qr/^ of /;
433             #>>>
434             }
435              
436             # license objects where atomic scan must always be applied
437             my %L_grant_stepwise_incomplete = (
438              
439             # usage
440              
441             # singleversion
442             apache_2 => 1,
443              
444             # versioned
445             gpl => 1,
446             lgpl => 1,
447              
448             # other
449             mit_new => 1, # misdetects ambiguous "MIT X11" grant
450             public_domain => 1,
451             );
452              
453             # license objects where stepwise scan cannot be skipped
454             my %L_grant_atomic_incomplete = (
455             afl_1_1 => 1,
456             afl_1_2 => 1,
457             afl_2 => 1,
458             afl_2_1 => 1,
459             afl_3 => 1,
460             apache_1_1 => 1,
461             artistic_1 => 1,
462             artistic_2 => 1,
463             bsl_1 => 1,
464             cc_by_2_5 => 1,
465             cc_by_sa => 1,
466             cpl_1 => 1,
467             mpl => 1,
468             mpl_1 => 1,
469             mpl_1_1 => 1,
470             mpl_2 => 1,
471             openssl => 1,
472             postgresql => 1,
473             zpl_2_1 => 1,
474             );
475              
476             # scan for grants first stepwise and if not found then also atomic
477             # flip either of these flags to test stepwise/atomic pattern coverage
478             my $skip_stepwise = 0;
479             my $force_atomic = 0;
480              
481             my $id2patterns_re = qr/(.*)(?:_(\d+(?:\.\d+)*)(_or_later)?)?/;
482              
483 331         530 method resolve ()
  331         481  
484 331     331 0 870 {
485 331         1315 $self->init_licensepatterns;
486              
487 331         738 my @L_type_usage = sort keys %{ $L{type}{usage} };
  331         6709  
488 331         1105 my @L_type_singleversion = sort keys %{ $L{type}{singleversion} };
  331         43965  
489 331         3079 my @L_type_versioned = sort keys %{ $L{type}{versioned} };
  331         18617  
490 331         1953 my @L_type_unversioned = sort keys %{ $L{type}{unversioned} };
  331         26519  
491 331         2216 my @L_type_combo = sort keys %{ $L{type}{combo} };
  331         1738  
492 331         721 my @L_type_group = sort keys %{ $L{type}{group} };
  331         1535  
493              
494 331         666 my @spdx_gplver;
495              
496 331         931 my @agpl = qw(agpl agpl_1 agpl_2 agpl_3);
497 331         838 my @gpl = qw(gpl gpl_1 gpl_2 gpl_3);
498 331         738 my @lgpl = qw(lgpl lgpl_2 lgpl_2_1 lgpl_3);
499              
500 331         472 my %match;
501 331         555 my ( %grant, %license );
502              
503 3     3   4 my $patterns2id = sub ( $stem, $version = undef ) {
  3         5  
  3         7  
  3         9  
504 3 50       10 return $stem
505             unless ($version);
506 3         31 $version =~ s/\.0$//g;
507 3         10 $version =~ s/\./_/g;
508 3         16 return "${stem}_$version";
509 331         1948 };
510 222     222   384 my $id2patterns = sub ($id) {
  222         463  
  222         483  
511 222         3191 return $id =~ /$id2patterns_re/;
512 331         1193 };
513 333         615 my $gen_license = sub (
514 333         660 $id, $v = undef, $later = undef, $id2 = undef, $v2 = undef,
  333         501  
  333         526  
  333         503  
515             $later2 = undef
516             )
517 333     333   463 {
  333         553  
  333         503  
518 333         745 my ( @spdx, $name, $desc, $name2, $desc2, $legacy, $expr );
519              
520 333   33     1670 $name = $L{name}{$id} || $id;
521 333   33     1365 $desc = $L{caption}{$id} || $id;
522 333 100       848 if ($v) {
523 5 100       35 push @spdx, $later ? "$name-$v+" : "$name-$v";
524 5 100       25 $v .= ' or later' if ($later);
525             }
526             else {
527 328         846 push @spdx, $name;
528             }
529 333 100       789 if ($id2) {
530 5   33     25 $name2 = $L{name}{$id2} || $id2;
531 5   33     15 $desc2 = $L{caption}{$id2} || $id2;
532 5 100       11 if ($v2) {
533 4 100       14 push @spdx, $later2 ? "$name2-$v2+" : "$name2-$v2";
534 4 100       11 $v2 .= ' or later' if ($later2);
535             }
536             else {
537 1         4 push @spdx, $name2;
538             }
539             }
540 333 100       2001 $legacy = join(
    100          
    100          
541             ' ',
542             $desc,
543             $v ? "(v$v)" : (),
544             $desc2 ? "or $desc2" : (),
545             $v2 ? "(v$v2)" : (),
546             );
547 333         1097 $expr = join( ' or ', sort @spdx );
548             $self->tag(
549             Licensing->new(
550 333   66     5079 id => [ $expr, $L{caption}{$legacy} || $legacy ]
551             )
552             );
553 331         1437 };
554              
555             # fulltext
556 331         1558 $log->trace('scan for license fulltext');
557 331         1322 my %pos_license;
558 331         906 foreach my $id (@RE_LICENSE) {
559 161528 100       427242 next unless ( $RE{"LICENSE_$id"} );
560 118498         2821694 while ( $string =~ /$RE{"LICENSE_$id"}/g ) {
561 288         3052 $pos_license{ $-[0] }{$id}
562             = $self->note( "license($id)", $-[0], $+[0] );
563             }
564             }
565              
566 331         837 foreach my $trait ( keys %{ $L{TRAITS_exception_prefix} } ) {
  331         2213  
567              
568 1655 100       39936 next unless ( $string =~ /$RE{"TRAIT_$trait"}/ );
569 42         973 while ( $string =~ /$RE{"TRAIT_$trait"}/g ) {
570 44 50       304 next if $self->contains_tag( $-[0], $+[0] );
571 44         2326 $self->note( $trait, $-[0], $+[0] );
572             }
573             }
574 331         1906 foreach my $pos ( sort { $a <=> $b } keys %pos_license ) {
  113         417  
575              
576             # pick longest or most specific among matched license fulltexts
577 288     288   3395 my @licenses = nsort_by { $pos_license{$pos}{$_}->[1] }
578 261 100       2498 grep { $pos_license{$pos}{$_} ? $pos_license{$pos}{$_}->[1] : () } (
  126846         204755  
579             @L_type_group,
580             @L_type_combo,
581             @L_type_unversioned,
582             @L_type_versioned,
583             @L_type_singleversion,
584             @L_type_usage,
585             );
586 261         2361 my $license = pop @licenses;
587 261 50       775 next unless ($license);
588             next
589 261 100       1036 if $self->contains_tag( $pos, $pos_license{$pos}{$license}->[1] );
590             $self->tag(
591             Fulltext->new(
592             id => [ $license, $license ], # TODO: try resolve caption
593             begin => $pos_license{$pos}{$license}->[0],
594 246         13984 end => $pos_license{$pos}{$license}->[1]
595             )
596             );
597 246         1174 $license{$license} = 1;
598             }
599              
600             # grant, stepwise
601 331         2625 my @prefixes;
602 331         1416 $log->trace('scan stepwise for license grant');
603 331         1262 foreach my $trait ( keys %{ $L{TRAITS_grant_prefix} } ) {
  331         1478  
604              
605 1324         37956 while ( $string =~ /$RE{"TRAIT_$trait"}/g ) {
606 1640 100       16030 next if $self->contains_tag( $-[0], $+[0] );
607 1565         66882 push @prefixes, $self->note( $trait, $-[0], $+[0] );
608             }
609             }
610             LICENSED_UNDER:
611 331         2306 foreach my $licensed_under ( sort { $a->[1] <=> $b->[1] } @prefixes ) {
  2388         3745  
612 1565         3918 my $pos = $licensed_under->[1];
613              
614             # possible grant names
615 1565         59283 my @grant_types = (
616             @L_type_combo,
617             @L_type_unversioned,
618             @L_type_versioned,
619             @L_type_singleversion,
620             @L_type_usage,
621             );
622              
623             # optional grant version
624 1565         2883 my ( $version, $later );
625              
626             # scan for prepended version
627 1565         33161 substr( $string, $pos ) =~ $RE{ANCHORLEFT_NAMED_version};
628 1565 100       11595 if ( $+{version_number} ) {
629 11         102 $self->note( 'version', $pos + $-[0], $pos + $+[0] );
630 11         91 $version = $+{version_number};
631 11 50       99 if ( $+{version_later} ) {
632 0         0 $self->note( 'or_later', $pos + $-[2], $pos + $+[2] );
633 0         0 $later = $+{version_later};
634             }
635 11 50       250 if (
636             substr( $string, $pos + $+[0] ) =~ $L{LEFTANCHOR_version_of} )
637             {
638 0         0 $self->note( 'version_of', $pos + $-[0], $pos + $+[0] );
639 0         0 $pos += $+[0];
640 0         0 @grant_types = @L_type_versioned;
641             }
642             else {
643 11         34 $version = '';
644             }
645             }
646              
647             # scan for name
648 1565         4559 foreach my $id (@RE_NAME) {
649 763720 100       5468596 if ( substr( $string, $pos ) =~ $RE{"NAME_$id"} ) {
650 539         4328 $match{$id}{name}{ $pos + $-[0] }
651             = $self->note( "name($id)", $pos + $-[0], $pos + $+[0] );
652             }
653             }
654              
655             # pick longest matched license name
656             # TODO: include all of most specific type when more are longest
657 401     401   3968 my @names = nsort_by { $match{$_}{name}{$pos}->[1] }
658 1565 50 66     11095 grep { $match{$_} and $match{$_}{name} and $match{$_}{name}{$pos} }
  754330         1253265  
659             @grant_types;
660 1565         14545 my $name = pop @names;
661 1565 50 66     26129 if ( $name
      66        
      33        
      66        
662             and $match{$name}{name}{$pos}
663             and !$self->contains_tag( $pos, $match{$name}{name}{$pos}->[1] )
664             and ( !$skip_stepwise or $L_grant_atomic_incomplete{$name} ) )
665             {
666 192         9339 my $pos_end = $pos = $match{$name}{name}{$pos}->[1];
667              
668             # may include version
669 192 100 66     742 if ( !$version and grep { $_ eq $name } @L_type_versioned ) {
  19392 100 66     29635  
670 38         796 substr( $string, $pos ) =~ $RE{ANCHORLEFT_NAMED_version};
671 38 100       377 if ( $+{version_number} ) {
672 4         45 $self->note( 'version', $pos + $-[0], $pos + $+[0] );
673 4         31 $version = $+{version_number};
674 4         20 $pos_end = $pos + $+[1];
675 4 50       30 if ( $+{version_later} ) {
676 0         0 $self->note( 'or_later', $pos + $-[2], $pos + $+[2] );
677 0         0 $later = $+{version_later};
678 0         0 $pos_end = $pos + $+[2];
679             }
680             }
681             }
682 32802         48489 elsif ( !$version and grep { $_ eq $name } @L_type_singleversion )
683             {
684             substr( $string, $pos )
685 81         1825 =~ $RE{ANCHORLEFT_NAMED_version_later};
686 81 100       793 if ( $+{version_later} ) {
687 3         25 $self->note( 'or_later', $pos + $-[1], $pos + $+[1] );
688 3         19 $later = $+{version_later};
689 3         15 $pos_end = $pos + $+[1];
690             }
691             }
692 192 100       759 if ($version) {
693 4         30 $version =~ s/(?:\.0)+$//;
694 4         14 $version =~ s/\./_/g;
695 4         13 $name .= "_$version";
696             }
697 192 100       513 if ($later) {
698 3         9 my $latername = "${name}_or_later";
699 3         62 $self->note( $latername, $licensed_under->[0], $pos_end );
700 3         10 $grant{$latername} = 1;
701 3 50       10 next LICENSED_UNDER if grep { $grant{$_} } @RE_NAME;
  1464         2046  
702             }
703 189         1027 $self->note( "grant($name)", $licensed_under->[0], $pos_end );
704 189         4761 $grant{$name} = 1;
705             }
706             }
707              
708             # GNU oddities
709 331 100       1370 if ( grep { $match{$_}{name} } @agpl, @gpl, @lgpl ) {
  3972         8777  
710 96         491 $log->trace('scan for GNU oddities');
711              
712             # address in AGPL/GPL/LGPL
713 96         6726 while ( $string =~ /$RE{TRAIT_addr_fsf}/g ) {
714 41         193 foreach (
715             qw(addr_fsf_franklin_steet addr_fsf_mass addr_fsf_temple))
716             {
717 123 100       1371 if ( defined $+{$_} ) {
718 14         229 $self->tag(
719             Flaw->new( id => $_, begin => $-[0], end => $+[0] ) );
720             }
721             }
722             }
723             }
724              
725             # exceptions
726             # TODO: conditionally limit to AGPL/GPL/LGPL
727 331         1085 foreach (@RE_EXCEPTION) {
728 11916 100       331358 if ( $string =~ $RE{"EXCEPTION_$_"} ) {
729 55         1021 $self->tag(
730             Exception->new( id => $_, begin => $-[0], end => $+[0] ) );
731             }
732             }
733              
734             # oddities
735 331         1962 $log->trace('scan for oddities');
736              
737             # generated file
738 331 100       22945 if ( $string =~ $RE{TRAIT_generated} ) {
739 14         197 $self->tag(
740             Flaw->new( id => 'generated', begin => $-[0], end => $+[0] ) );
741             }
742              
743             # multi-licensing
744 331         912 my @multilicenses;
745              
746             # LGPL, dual-licensed
747             # FIXME: add test covering this pattern
748 331 100       930 if ( grep { $match{$_}{name} } @lgpl ) {
  1324         3203  
749 29         118 $log->trace('scan for LGPL dual-license grant');
750 29 50       765 if ( $string =~ $L{multi_1} ) {
751 0         0 $self->note( 'grant(multi#1)', $-[0], $+[0] );
752 0         0 push @multilicenses, 'lgpl', $1, $2;
753             }
754             }
755              
756             # GPL, dual-licensed
757             # FIXME: add test covering this pattern
758 331 100       760 if ( grep { $match{$_}{name} } @gpl ) {
  1324         2737  
759 68         289 $log->trace('scan for GPL dual-license grant');
760 68 50       1556 if ( $string =~ $L{multi_2} ) {
761 0         0 $self->note( 'grant(multi#2)', $-[0], $+[0] );
762 0         0 push @multilicenses, 'gpl', $1, $2;
763             }
764             }
765              
766 331 50       1035 $gen_license->(@multilicenses) if (@multilicenses);
767              
768             # LGPL
769 331 100       682 if ( grep { $match{$_}{name} } @lgpl ) {
  1324         2576  
770 29         100 $log->trace('scan for LGPL fulltext/grant');
771              
772             # LGPL, dual versions last
773 29 100       62670 if ( $string =~ $L{lgpl_5} ) {
774 5         127 $self->tag(
775             Grant->new(
776             id => [ "LGPL-$1 or LGPL-$2", "LGPL (v$1 or v$2)" ],
777             begin => $-[0], end => $+[0],
778             )
779             );
780 5         66 $match{ 'lgpl_' . $1 =~ tr/./_/r }{custom} = 1;
781 5         33 $match{ 'lgpl_' . $2 =~ tr/./_/r }{custom} = 1;
782 5         18 $match{lgpl}{custom} = 1;
783             }
784             }
785              
786             # GPL or LGPL
787 331 100       739 if ( grep { $match{$_}{name} } @gpl ) {
  1324         2618  
788 68         2181 $log->trace('scan for GPL or LGPL dual-license grant');
789 68 100       7311 if ( $string =~ $L{gpl_7} ) {
790 2         14 $self->note( "grant(gpl#7)", $-[0], $+[0] );
791 2         11 $gen_license->( 'gpl', $1, $2, 'lgpl', $3, $4 );
792 2         8 $match{gpl}{custom} = 1;
793 2         6 $match{lgpl}{custom} = 1;
794             }
795             }
796              
797             # Apache dual-licensed with GPL/BSD/MIT
798 331 100       1276 if ( $match{apache}{name} ) {
799 11         77 $log->trace('scan for Apache license grant');
800 11         155 for ($string) {
801 11 100       11168 if ( $string =~ $L{apache_1} ) {
802 2         15 $self->note( 'grant(apache#1)', $-[0], $+[0] );
803 2         22 $gen_license->( 'apache', $1, $2, 'gpl', $3, $4 );
804 2         16 $match{ $patterns2id->( 'apache', $1 ) }{custom} = 1;
805 2         11 next;
806             }
807 9 50       10914 if ( $string =~ $L{apache_2} ) {
808 0         0 $self->note( 'grant(apache#2)', $-[0], $+[0] );
809 0 0       0 $gen_license->(
810             'apache', $1, $2,
811             $3 ? "bsd_${3}_clause" : ''
812             );
813 0         0 $match{ $patterns2id->( 'apache', $1 ) }{custom} = 1;
814 0         0 next;
815             }
816 9 100       188 if ( $string =~ $L{apache_4} ) {
817 1         7 $self->note( 'grant(apache#4)', $-[0], $+[0] );
818 1         15 $gen_license->( 'apache', $1, $2, 'mit', $3, $4 );
819 1         19 $match{ $patterns2id->( 'apache', $1 ) }{custom} = 1;
820 1         3 next;
821             }
822             }
823             }
824              
825             # FSFUL
826             # FIXME: add test covering this pattern
827 331         1153 $log->trace('scan for FSFUL fulltext');
828 331 50 66     6651 if ( !$license{fsful}
829             and $string =~ $L{fsful} )
830             {
831 0         0 $self->tag(
832             Fulltext->new(
833             id => [ "FSFUL~$1", "FSF Unlimited ($1 derivation)" ],
834             begin => $-[0], end => $+[0],
835             )
836             );
837 0         0 $match{fsful}{custom} = 1;
838             }
839              
840             # FSFULLR
841             # FIXME: add test covering this pattern
842 331         1460 $log->trace('scan for FSFULLR fulltext');
843 331 50 66     5685 if ( !$license{fsfullr}
844             and $string =~ $L{fsfullr} )
845             {
846 0         0 $self->tag(
847             Fulltext->new(
848             id => [
849             "FSFULLR~$1",
850             "FSF Unlimited (with Retention, $1 derivation)"
851             ],
852             begin => $-[0], end => $+[0],
853             )
854             );
855 0         0 $match{fsfullr}{custom} = 1;
856             }
857              
858             # usage
859 331         1320 $log->trace('scan atomic for singleversion usage license grant');
860 331         1311 foreach my $id (@L_type_usage) {
861 8937 50       20332 next if ( $match{$id}{custom} );
862 8937 50 33     30712 if ( !$grant{$id}
      66        
      33        
863             and ( $L_grant_stepwise_incomplete{$id} or $force_atomic )
864             and $string =~ $RE{"GRANT_$id"} )
865             {
866 0 0       0 if ( $self->contains_tag( $-[0], $+[0] ) ) {
867 0         0 $log->tracef( 'skip grant in covered range: %s', $id );
868             }
869             else {
870 0         0 $self->tag(
871             Grant->new( id => $id, begin => $-[0], end => $+[0] ) );
872 0         0 $grant{$id} = 1;
873             }
874             }
875              
876 8937 100       16205 if ( $grant{$id} ) {
877 63         258 $gen_license->( $id2patterns->($id) );
878              
879             # skip singleversion and unversioned equivalents
880 63 50       420 if ( $L{usage}{$id} ) {
881 63         314 $log->tracef( 'flagged license object: %s', $id );
882 63         337 $match{ $L{usage}{$id} }{custom} = 1;
883 63 50       302 if ( $L{series}{ $L{usage}{$id} } ) {
884             $log->tracef(
885             'flagged license object: %s',
886 63         209 $L{usage}{$id}
887             );
888 63         360 $match{ $L{series}{ $L{usage}{$id} } }{custom} = 1;
889             }
890             }
891             }
892             }
893              
894             # singleversion
895 331         1371 $log->trace('scan atomic for singleversion license grant');
896 331         1367 foreach my $id (@L_type_singleversion) {
897 70503 50 100     446196 if ( !$license{$id}
      66        
      100        
      66        
898             and !$grant{$id}
899             and !$match{$id}{custom}
900             and ( $L_grant_stepwise_incomplete{$id} or $force_atomic )
901             and $string =~ $RE{"GRANT_$id"} )
902             {
903 0 0       0 if ( $self->contains_tag( $-[0], $+[0] ) ) {
904 0         0 $log->tracef( 'skip grant in covered range: %s', $id );
905             }
906             else {
907 0         0 $self->tag(
908             Grant->new( id => $id, begin => $-[0], end => $+[0] ) );
909 0         0 $grant{$id} = 1;
910             }
911             }
912              
913 70503 100 100     188215 if ( $license{$id} or $grant{$id} ) {
914             $gen_license->( $id2patterns->($id) )
915 170 100       1217 unless ( $match{$id}{custom} );
916              
917             # skip unversioned equivalent
918 170 50       1212 if ( $L{series}{$id} ) {
919 170         744 $log->tracef( 'flagged license object: %s', $id );
920 170         1218 $match{ $L{series}{$id} }{custom} = 1;
921             }
922             }
923             }
924              
925             # versioned
926 331         1919 $log->trace('scan atomic for versioned license grant');
927 331         1648 foreach my $id (@L_type_versioned) {
928 33431 100       72611 next if ( $match{$id}{custom} );
929              
930             # skip embedded or referenced licenses
931 33200 100 100     56758 next if ( $license{rpsl_1} and grep { $id eq $_ } qw(mpl python) );
  400         857  
932              
933 33198 50       52787 next if ( $license{$id} );
934 33198 100 66     199020 if ( !$grant{$id}
      100        
      66        
      100        
935             and ( $L_grant_stepwise_incomplete{$id} or $force_atomic )
936             and $RE{"GRANT_$id"}
937             and $string =~ $RE{"GRANT_$id"} )
938             {
939 6 100       64 if ( $self->contains_tag( $-[0], $+[0] ) ) {
940 4         668 $log->tracef( 'skip grant in covered range: %s', $id );
941             }
942             else {
943 2         136 $self->tag(
944             Grant->new( id => $id, begin => $-[0], end => $+[0] ) );
945 2         19 $grant{$id} = 1;
946             }
947             }
948              
949 33198 100       59655 if ( $grant{$id} ) {
950 16         90 $gen_license->($id);
951             }
952             }
953              
954             # other
955             # TODO: add @L_type_group
956 331         1808 $log->trace('scan atomic for misc fulltext/grant');
957 331         1725 foreach my $id ( @L_type_unversioned, @L_type_combo ) {
958 46671 50 66     147590 next if ( !$license{$id} and $match{$id}{custom} );
959              
960             next
961             unless ( $license{$id}
962             or $grant{$id}
963 46671 100 100     207090 or $L_grant_stepwise_incomplete{$id}
      100        
      66        
964             or $force_atomic );
965              
966             # skip embedded or referenced licenses
967 748 50 33     2228 next if ( $license{caldera} and $id eq 'bsd' );
968 748 50 66     1975 next if ( $license{cube} and $id eq 'zlib' );
969 748 50 66     1717 next if ( $license{dsdp} and $id eq 'ntp' );
970 748 50 66     1735 next if ( $license{mit_cmu} and $id eq 'ntp_disclaimer' );
971 748 50 66     1660 next if ( $license{ntp_disclaimer} and $id eq 'ntp' );
972              
973 748 50 100     66090 if ( !$license{$id}
      66        
974             and !$grant{$id}
975             and $string =~ $RE{"GRANT_$id"} )
976             {
977 0 0       0 if ( $self->contains_tag( $-[0], $+[0] ) ) {
978 0         0 $log->tracef( 'skip grant in covered range: %s', $id );
979             }
980             else {
981 0         0 $self->tag(
982             Grant->new( id => $id, begin => $-[0], end => $+[0] ) );
983 0         0 $grant{$id} = 1;
984             }
985             }
986 748 100 100     3574 if ( $license{$id} or $grant{$id} ) {
987 90         367 $gen_license->($id);
988             }
989             }
990              
991             # Expressions and exceptions contain DEP-5 or SPDX identifiers;
992             # flaws contains non-SPDX notes.
993 331         1917 my ( $licenses, $exceptions, $flaws ) = $self->get_tags;
994              
995 331         854 my @expressions = map { $_->[1] } @$licenses;
  338         1033  
996 331         695 my @license = map { $_->[2] } @$licenses;
  338         783  
997 331         1329 $expr = join( ' and/or ', sort @expressions );
998 331         1007 $license = join( ' and/or ', sort @license );
999 331   100     950 $expr ||= 'UNKNOWN';
1000 331   100     858 $license ||= 'UNKNOWN';
1001              
1002 331 100       817 if (@$exceptions) {
1003 53 100       203 $expr = "($expr)"
1004             if ( @expressions > 1 );
1005             $expr .= ' with ' . join(
1006             '_AND_',
1007 53         159 sort map { $_->[1] } @$exceptions
  55         272  
1008             ) . ' exception';
1009             }
1010 331 100       835 if (@$flaws) {
1011             $license .= ' [' . join(
1012             ', ',
1013 28         63 sort map { $_->[2] } @$flaws
  28         128  
1014             ) . ']';
1015             }
1016 331         1493 $log->infof( 'resolved license expression: %s', $expr );
1017              
1018 331         33063 return $self;
1019             }
1020              
1021             =item as_text
1022              
1023             Returns identified licensing patterns as a string,
1024             either structured as SPDX License Expressions,
1025             or with scheme-less naming as a short description.
1026              
1027             =cut
1028              
1029 331         661 method as_text ()
  331         519  
1030 331     331 1 6610 {
1031 331 100       1632 if ( $naming->list_schemes ) {
1032 311 50       1477 $self->resolve
1033             unless $expr;
1034              
1035 311         1855 return $expr;
1036             }
1037              
1038             $self->resolve
1039 20 50       84 unless $license;
1040              
1041 20         81 return $license;
1042             }
1043              
1044             =back
1045              
1046             =encoding UTF-8
1047              
1048             =head1 AUTHOR
1049              
1050             Jonas Smedegaard C<< >>
1051              
1052             =head1 COPYRIGHT AND LICENSE
1053              
1054             This program is based on the script "licensecheck" from the KDE SDK,
1055             originally introduced by Stefan Westerfeld C<< >>.
1056              
1057             Copyright © 2007, 2008 Adam D. Barratt
1058              
1059             Copyright © 2016-2023 Jonas Smedegaard
1060              
1061             Copyright © 2017-2022 Purism SPC
1062              
1063             This program is free software:
1064             you can redistribute it and/or modify it
1065             under the terms of the GNU Affero General Public License
1066             as published by the Free Software Foundation,
1067             either version 3, or (at your option) any later version.
1068              
1069             This program is distributed in the hope that it will be useful,
1070             but WITHOUT ANY WARRANTY;
1071             without even the implied warranty
1072             of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
1073             See the GNU Affero General Public License for more details.
1074              
1075             You should have received a copy
1076             of the GNU Affero General Public License along with this program.
1077             If not, see .
1078              
1079             =cut
1080              
1081             1;