File Coverage

blib/lib/URI/VersionRange/Util.pm
Criterion Covered Total %
statement 252 255 98.8
branch 129 148 87.1
condition 49 72 68.0
subroutine 24 24 100.0
pod 8 8 100.0
total 462 507 91.1


line stmt bran cond sub pod time code
1             package URI::VersionRange::Util;
2              
3 6     6   238490 use feature ':5.10';
  6         15  
  6         879  
4 6     6   37 use strict;
  6         11  
  6         193  
5 6     6   1280 use utf8;
  6         668  
  6         37  
6 6     6   207 use warnings;
  6         37  
  6         346  
7              
8 6     6   44 use Exporter qw(import);
  6         12  
  6         212  
9 6     6   31 use Carp ();
  6         11  
  6         48671  
10              
11             our $VERSION = '2.25';
12              
13             our @EXPORT = qw(
14             parse_semver normalize_semver is_semver
15             native_range_to_vers
16             version_compare semver_version_compare generic_version_compare
17             );
18              
19             # https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
20             my $SEMVER_REGEXP = qr{(?x)
21             ^
22             (?P0|[1-9]\d*)
23             \.
24             (?P0|[1-9]\d*)
25             \.
26             (?P0|[1-9]\d*)
27             (?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?
28             (?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?
29             $
30             };
31              
32             sub native_range_to_vers {
33              
34 751     751 1 198582 my ($scheme, $range) = @_;
35              
36 751         4721 my %TYPES = (
37             conan => \&_conan_native_range,
38             gem => \&_gem_native_range,
39             nginx => \&_nginx_native_range,
40             npm => \&_semver_native_range,
41             nuget => \&_nuget_native_range,
42             raku => \&_raku_native_range,
43             semver => \&_semver_native_range,
44             );
45              
46 751 100       2664 if (defined $TYPES{$scheme}) {
47 740         1907 return sprintf 'vers:%s/%s', $scheme, $TYPES{$scheme}->($range);
48             }
49              
50 11         29 my @parts = split /\,/, $range;
51 11         24 map {s{\s+}{}g} @parts;
  14         55  
52              
53 11         99 return sprintf 'vers:%s/%s', $scheme, join '|', sort @parts;
54              
55             }
56              
57             sub _nginx_native_range {
58              
59 4     4   8 my $native = shift;
60 4         18 $native =~ s/\s+//g;
61              
62 4         10 my @plus = ();
63 4         7 my @others = ();
64              
65 4         18 my @p = split /,/, $native;
66              
67 4         11 foreach my $part (@p) {
68              
69 5 50       17 next unless length $part;
70              
71             # "1.0.0-1.0.1"
72 5 100       20 if ($part =~ /^(.*?)-(.*)$/) {
73 1         4 my $min = normalize_semver($1);
74 1         6 my $max = normalize_semver($2);
75 1         5 push @others, ">=$min|<=$max";
76 1         5 next;
77             }
78              
79             # "1.0.1+"
80 4 100       24 if ($part =~ /^\s*(.*?)\+\s*$/) {
81 3         13 my $version = normalize_semver($1);
82 3 50       15 push @plus, $version if defined $version;
83 3         9 next;
84             }
85              
86 1 50       3 if (my $v = normalize_semver($part)) {
87 1         3 push @others, $v;
88 1         6 next;
89             }
90              
91 0         0 push @others, $part;
92             }
93              
94 4 100       10 if (@plus) {
95              
96 2         6 my %seen = ();
97              
98             # Remove duplicates and sort
99             @plus = sort {
100              
101 1         4 my $A = parse_semver($a);
102 1         3 my $B = parse_semver($b);
103              
104             $A->{major} <=> $B->{major} || $A->{minor} <=> $B->{minor} || $A->{patch} <=> $B->{patch}
105              
106 2 50 33     6 } grep { !$seen{$_}++ } @plus;
  1         15  
  3         18  
107              
108             # One "+"
109 2 100       9 if (@plus == 1) {
110              
111 1         4 my $semver = parse_semver($plus[0]);
112 1         7 my $upper = join('.', $semver->{major}, $semver->{minor} + 1, 0);
113              
114 1         19 return join('|', (">=$plus[0]", "<$upper", @others));
115              
116             }
117              
118 1         30 my @parts = (">=$plus[0]");
119              
120             # Skip first plus
121 1         5 for (my $i = 1; $i < @plus; $i++) {
122 1         6 push @parts, "<$plus[$i]", ">=$plus[$i]";
123             }
124              
125 1         15 return join('|', @parts, @others);
126              
127             }
128              
129 2         7 return join('|', grep {$_} @others);
  2         23  
130              
131             }
132              
133             sub _nuget_native_range {
134              
135 10     10   25 my $native = shift;
136              
137             # https://learn.microsoft.com/en-us/nuget/concepts/package-versioning
138              
139             # Notation Applied rule Description
140             # 1.0 x ≥ 1.0 Minimum version, inclusive
141             # [1.0,) x ≥ 1.0 Minimum version, inclusive
142             # (1.0,) x > 1.0 Minimum version, exclusive
143             # [1.0] x == 1.0 Exact version match
144             # (,1.0] x ≤ 1.0 Maximum version, inclusive
145             # (,1.0) x < 1.0 Maximum version, exclusive
146             # [1.0,2.0] 1.0 ≤ x ≤ 2.0 Exact range, inclusive
147             # (1.0,2.0) 1.0 < x < 2.0 Exact range, exclusive
148             # [1.0,2.0) 1.0 ≤ x < 2.0 Mixed inclusive minimum and exclusive maximum version
149             # (1.0) invalid invalid
150              
151             my @parts = map {
152 18 100       242 /^\((.*)\)$/
    100          
    100          
    100          
    100          
    100          
    100          
    50          
153             ? Carp::croak 'Invalid nuget version range' # (1.0)
154             : /^\($/ ? '>0.0' # (
155             : /^\)$/ ? '' # )
156             : /^\[(.*)\]$/ ? $1 # [1.0]
157             : /^\[(.*)/ ? ">=$1" # [1.0
158             : /(.*)\]$/ ? "<=$1" # 1.0]
159             : /^\((.*)/ ? ">$1" # (1.0
160             : /(.*)\)$/ ? "<$1" # 1.0)
161             : $_ # 1.0
162 10         45 } grep {$_} split /\,/, $native;
  18         56  
163              
164 10         30 return join('|', grep {$_} @parts);
  18         154  
165              
166             }
167              
168             sub _semver_native_range {
169              
170 491     491   904 my $native = shift;
171              
172 491         1545 $native =~ s/^(.*)\s\-\s(.*)$/>= $1 <= $2/g; # TODO
173 491         3682 $native =~ s/(>\=|<\=|>|<)\s+/$1/g;
174              
175 491         2925 my @p = grep {$_} split /(?:\s+|\|\|)/, $native;
  767         2072  
176 491         1036 my @constraints = ();
177              
178 491         1402 foreach my $part (@p) {
179              
180 632         1233 $part =~ s{v(\d+)}{$1};
181 632         1028 $part =~ s{^=}{};
182 632         2035 $part =~ s/^\s+|\s+$//g;
183              
184             # Wildcards (1.x or 2.0.x)
185 632 100       1440 if (my @wildcards = _semver_wildcards('>=', $part)) {
186 3         6 push @constraints, @wildcards;
187 3         10 next;
188             }
189              
190             # Tilde operator
191 629 100       1341 if ($part =~ /^(~)(.*)/) {
192 4         31 push @constraints, _tilde_operator('semver', $2);
193 4         13 next;
194             }
195              
196             # Caret operator
197 625 100       1361 if ($part =~ /^(\^)(.*)/) {
198 10         29 push @constraints, _caret_operator('semver', $2);
199 10         24 next;
200             }
201              
202             # Operators
203 615 100       2925 if ($part =~ /(>\=|<\=|>|<)(.*)/) {
204              
205 595         2019 my ($operator, $version) = ($1, $2);
206              
207             # Wildcards (1.x or 2.0.x)
208 595 100       1032 if (my @wildcards = _semver_wildcards($operator, $version)) {
209 2         7 push @constraints, @wildcards;
210 2         7 next;
211             }
212              
213              
214 593         1189 $version = normalize_semver($version);
215              
216 593 50 33     2133 if ($version and is_semver($version)) {
217 593         1553 push @constraints, join('', $operator, $version);
218 593         1653 next;
219             }
220              
221             }
222              
223 20 100       104 if ($part ne '*') {
224              
225 19         81 my $version = normalize_semver($part);
226              
227 19 50 33     114 if ($version and is_semver($version)) {
228 19         65 push @constraints, normalize_semver($part);
229 19         73 next;
230             }
231              
232             }
233              
234 1         4 push @constraints, $part;
235              
236             }
237              
238 491         4144 return join '|', @constraints;
239             }
240              
241             sub _conan_native_range {
242              
243 228     228   444 my $native = shift;
244              
245 228         2208 my @p = grep {$_} split /(?:\s+|\|\|)/, $native;
  349         1194  
246 228         600 my @parts = ();
247              
248 228         568 foreach my $part (@p) {
249              
250 343         801 $part =~ s{^=}{}g;
251 343         666 $part =~ s{\-$}{}g;
252 343         585 $part =~ s{\,}{}g;
253              
254 343 100       866 if ($part =~ /^(\*|\*\-)$/) {
255 3         9 push @parts, '>=0.0.0';
256 3         8 next;
257             }
258              
259 340 100       736 if ($part =~ /^(~)(.*)/) {
260 4         17 push @parts, _tilde_operator('conan', $2);
261 4         11 next;
262             }
263              
264 336 100       680 if ($part =~ /^(\^)(.*)/) {
265 6         23 push @parts, _caret_operator('conan', $2);
266 6         15 next;
267              
268             }
269              
270 330         808 push @parts, $part;
271              
272             }
273              
274 228         2411 return join '|', @parts;
275              
276             }
277              
278             sub _raku_native_range {
279              
280 6     6   15 my $native = shift;
281              
282             my @parts = map {
283 6 100       71 /(.*)\+$/
    100          
    100          
284             ? ">=$1" # 1.0+
285             : /^(\d+)\.\*/ ? ">=$1" # 1.*
286             : /^(\d+)\.(\d+)\.\*/ ? ">=$1.0" # 1.0.*
287             : $_ # 1.0
288 6         23 } grep {$_} split /\,/, $native;
  6         20  
289              
290 6         16 return join('|', grep {$_} @parts);
  6         54  
291              
292             }
293              
294             sub _gem_native_range {
295              
296             # Convert GEM version spec to VERS range
297              
298 1     1   3 my $native = shift;
299              
300             # Specification From ... To (exclusive)
301             # ">= 3.0" 3.0 ... ∞
302             # "~> 3.0" 3.0 ... 4.0
303             # "~> 3.0.0" 3.0.0 ... 3.1
304             # "~> 3.5" 3.5 ... 4.0
305             # "~> 3.5.0" 3.5.0 ... 3.6
306             # "~> 3" 3.0 ... 4.0
307              
308 1 50       20 if ($native =~ /^(~>)(.*)/) {
309 1         6 return _tilde_operator('gem', $2);
310             }
311              
312 0         0 return $native;
313              
314             }
315              
316             sub _semver_wildcards {
317              
318 1227     1227   2158 my ($operator, $term) = @_;
319 1227         2108 $term =~ s/\s+//g;
320              
321             # >= major.x
322 1227 100 100     4344 if ($operator eq '>=' && $term =~ /^(\d+)\.x(?:\.x)?$/) {
323 1         4 my $major = $1;
324 1         9 return (">=$major.0.0", '<' . ($major + 1) . ".0.0");
325             }
326              
327             # >= major.minor.x
328 1226 100 100     3467 if ($operator eq '>=' && $term =~ /^(\d+)\.(\d+)\.x$/) {
329 4         15 my ($major, $minor) = ($1, $2);
330 4         23 return (">=$major.$minor.0", "<$major." . ($minor + 1) . ".0");
331             }
332              
333 1222         2546 return ();
334              
335             }
336              
337             sub _tilde_operator {
338              
339 9     9   45 my ($scheme, $version) = @_;
340 9         32 $version =~ s/\s+//;
341              
342 9         27 my $semver = parse_semver($version);
343              
344 9   66     45 my $has_prerelease = defined($semver->{prerelease}) && ($semver->{prerelease} ne '');
345 9         22 my ($major, $minor, $patch) = @{$semver}{qw[major minor patch]};
  9         34  
346              
347 9 100 66     36 if ($has_prerelease && defined $patch) {
348              
349 1         3 my $lower = join('.', $major, $minor, $patch);
350 1         3 my $upper = join('.', $major, $minor, $patch + 1);
351              
352 1         8 return join '|', ">=$version", "<$lower", ">=$lower", "<$upper";
353             }
354              
355 8         26 my ($upper_major, $upper_minor, $upper_patch) = ($major, $minor, $patch);
356              
357 8 100       31 if ($patch > 0) {
    100          
358 6         33 ($upper_minor, $upper_patch) = ($minor + 1, 0);
359             }
360             elsif ($minor > 0) {
361 1         4 ($upper_minor, $upper_patch) = ($minor + 1, 0);
362             }
363             else {
364 1         4 ($upper_major, $upper_minor, $upper_patch) = ($major + 1, 0, 0);
365             }
366              
367 8         31 my @upper = ($upper_major, $upper_minor, $upper_patch);
368              
369             SWITCH:
370 8         22 for ($scheme) {
371              
372 8 100       49 if (/gem/) {
373 1         3 pop @upper;
374 1         4 last SWITCH;
375             }
376              
377 7 100       35 if (/conan/) {
378              
379             # strip trailing zeros
380 4   66     34 pop @upper while @upper && $upper[-1] == 0;
381 4         12 $upper[-1] .= '-';
382 4         12 last SWITCH;
383             }
384              
385             }
386              
387             # >= min and < max version
388 8         73 return join '|', ">=$version", sprintf('<%s', join('.', @upper));
389              
390             }
391              
392             sub _caret_operator {
393              
394 16     16   61 my ($scheme, $version) = @_;
395 16         39 $version =~ s/\s+//;
396              
397 16         35 my $semver = parse_semver($version);
398 16         33 my ($major, $minor, $patch) = @{$semver}{qw[major minor patch]};
  16         48  
399              
400 16         37 my ($upper_major, $upper_minor, $upper_patch) = ($major, $minor, $patch);
401              
402 16 100       53 if ($major > 0) {
    100          
403 13         55 ($upper_major, $upper_minor, $upper_patch) = ($major + 1, 0, 0);
404             }
405             elsif ($minor > 0) {
406 2         6 ($upper_minor, $upper_patch) = ($minor + 1, 0);
407             }
408             else {
409 1         4 $upper_patch = $patch + 1;
410             }
411              
412 16         39 my @upper = ($upper_major, $upper_minor, $upper_patch);
413              
414             SWITCH:
415 16         37 for ($scheme) {
416              
417 16 100       57 if (/conan/) {
418              
419             # strip trailing zeros
420 6   66     56 pop @upper while @upper && $upper[-1] == 0;
421 6         123 $upper[-1] .= '-';
422 6         18 last SWITCH;
423             }
424              
425             }
426              
427             # >= min and < max version
428 16         107 return join '|', ">=$version", sprintf('<%s', join('.', @upper));
429             }
430              
431             sub is_semver {
432 612 50   612 1 5281 ($_[0] =~ /$SEMVER_REGEXP/) ? 1 : 0;
433             }
434              
435             sub parse_semver {
436              
437 1805     1805 1 2785 my $version = shift;
438              
439             # FIX semver (1 --> 1.0.0 or 1.0 -> 1.0.0)
440 1805         4457 my @parts = split /\./, $version;
441              
442 1805 100       3463 $version = join '.', (@parts, 0, 0) if (@parts == 1);
443 1805 100       3328 $version = join '.', (@parts, 0) if (@parts == 2);
444              
445 1805         5801 my %semver = (major => 0, minor => 0, patch => 0, prerelease => undef, buildmetadata => undef);
446              
447 1805 50       13322 if ($version =~ /$SEMVER_REGEXP/) {
448 1805         3299 %semver = map { $_ => $+{$_} } qw[major minor patch prerelease buildmetadata];
  9025         34780  
449             }
450              
451 1805 100       7912 return wantarray ? %semver : \%semver;
452              
453             }
454              
455             sub normalize_semver {
456 637 50   637 1 1555 return unless $_[0];
457 637         1560 return hash_to_semver(parse_semver($_[0]));
458             }
459              
460             sub hash_to_semver {
461              
462 637     637 1 2891 my %hash = (major => 0, minor => 0, patch => 0, prerelease => undef, buildmetadata => undef, @_);
463              
464 637         2093 my $semver = join '.', $hash{major}, $hash{minor}, $hash{patch};
465              
466 637 100       1592 $semver .= '-' . $hash{prerelease} if $hash{prerelease};
467 637 50       1420 $semver .= '+' . $hash{buildmetadata} if $hash{buildmetadata};
468              
469 637         1822 return $semver;
470             }
471              
472             sub version_compare {
473              
474 1070     1070 1 1491 my $scheme = shift;
475              
476 1070         2771 my %TYPES = (npm => \&semver_version_compare, semver => \&semver_version_compare);
477              
478 1070 100       2280 if (defined $TYPES{$scheme}) {
479 494         940 return $TYPES{$scheme}->(@_);
480             }
481              
482 576         1140 return generic_version_compare(@_);
483             }
484              
485             # Semver compare
486              
487             sub semver_version_compare {
488              
489 586 100   586 1 241072 return 0 if $_[0] eq $_[1];
490              
491 570         1196 my $a = parse_semver($_[0]);
492 570         1175 my $b = parse_semver($_[1]);
493              
494 570         1293 my $major_cmp = $a->{major} <=> $b->{major};
495 570 100       2053 return $major_cmp if $major_cmp != 0;
496              
497 334         624 my $minor_cmp = $a->{minor} <=> $b->{minor};
498 334 100       1313 return $minor_cmp if $minor_cmp != 0;
499              
500 156         293 my $patch_cmp = $a->{patch} <=> $b->{patch};
501 156 100       717 return $patch_cmp if $patch_cmp != 0;
502              
503 86 100 100     417 return -1 if defined $a->{prerelease} && !defined $b->{prerelease};
504 76 100 100     269 return 1 if !defined $a->{prerelease} && defined $b->{prerelease};
505 68 50 66     227 return 0 if !defined $a->{prerelease} && !defined $b->{prerelease};
506              
507 58 50 33     273 if (defined $a->{prerelease} && defined $b->{prerelease}) {
508              
509 58         161 my @pre_a = split(/\./, $a->{prerelease});
510 58         129 my @pre_b = split(/\./, $b->{prerelease});
511              
512 58 100       145 my $min = @pre_a < @pre_b ? scalar(@pre_a) : scalar(@pre_b);
513              
514 58         199 for my $i (0 .. $min - 1) {
515 92         221 my $pre_cmp = _cmp_prerelease($pre_a[$i], $pre_b[$i]);
516 92 100       478 return $pre_cmp if $pre_cmp != 0;
517             }
518              
519 20         149 return @pre_a <=> @pre_b;
520              
521             }
522              
523 0         0 return 0;
524              
525             }
526              
527             sub _cmp_prerelease {
528              
529 92     92   221 my ($a, $b) = @_;
530              
531 92 100 100     400 if ($a =~ /^\d+$/ && $b =~ /^\d+$/) {
532 16         49 return $a <=> $b;
533             }
534              
535 76 100 100     334 if ($a =~ /^\d+$/ || $b =~ /^\d+$/) {
536 10 100       46 return $a =~ /^\d+$/ ? -1 : 1;
537             }
538              
539 66         157 return $a cmp $b;
540              
541             }
542              
543             # Optimized version of Sort::Version
544              
545             sub generic_version_compare {
546              
547 576     576 1 1110 my ($a, $b) = @_;
548              
549 576         1277 $a =~ s/^[vV]//;
550 576         1105 $b =~ s/^[vV]//;
551              
552 576 100       1167 return 0 if $a eq $b;
553              
554 570         3197 my @A = ($a =~ /([-.]|\d+|[^-.\d]+)/g);
555 570         2452 my @B = ($b =~ /([-.]|\d+|[^-.\d]+)/g);
556              
557 570         870 my ($A, $B);
558              
559 570   66     1863 while (@A and @B) {
560              
561 1830         2566 $A = shift @A;
562 1830         2647 $B = shift @B;
563              
564 1830 50 33     3528 return -1 if $A eq '-' && $B ne '-';
565 1830 50 33     3377 return 1 if $B eq '-' && $A ne '-';
566              
567 1830 50 66     3760 return -1 if $A eq '.' && $B ne '.';
568 1830 50 66     3828 return 1 if $B eq '.' && $A ne '.';
569              
570 1830 50 33     3246 next if $A eq '-' && $B eq '-';
571 1830 100 66     4751 next if $A eq '.' && $B eq '.';
572              
573 1202 100 100     4660 if ($A =~ /^\d+$/ and $B =~ /^\d+$/) {
574 1196 100 100     3554 my $num_cmp = ($A =~ /^0/ || $B =~ /^0/) ? ($A cmp $B) : ($A <=> $B);
575 1196 100       5007 return $num_cmp if $num_cmp;
576             }
577             else {
578 6         23 my $str_cmp = uc($A) cmp uc($B);
579 6         36 return $str_cmp;
580             }
581              
582             }
583              
584 30         83 return @A <=> @B;
585              
586             }
587              
588             1;
589              
590             __END__