File Coverage

blib/lib/URI/PackageURL.pm
Criterion Covered Total %
statement 117 122 95.9
branch 53 58 91.3
condition 3 6 50.0
subroutine 22 24 91.6
pod 12 16 75.0
total 207 226 91.5


line stmt bran cond sub pod time code
1             package URI::PackageURL;
2              
3 4     4   113656 use strict;
  4         30  
  4         114  
4 4     4   19 use warnings;
  4         4  
  4         101  
5 4     4   23 use Carp;
  4         7  
  4         187  
6 4     4   1994 use utf8;
  4         47  
  4         19  
7 4     4   113 use feature ':5.10';
  4         8  
  4         370  
8              
9 4     4   18 use Exporter qw(import);
  4         6  
  4         136  
10              
11 4     4   18 use constant DEBUG => $ENV{PURL_DEBUG};
  4         7  
  4         302  
12              
13 4     4   1967 use overload '""' => 'to_string', fallback => 1;
  4         1585  
  4         20  
14              
15             our $VERSION = '1.10';
16              
17             our @EXPORT = qw(encode_purl decode_purl);
18              
19             sub encode_purl {
20              
21 1     1 1 73 my (%components) = @_;
22              
23 1         9 my $purl = URI::PackageURL->new(%components);
24 1         4 return $purl->to_string;
25              
26             }
27              
28             sub decode_purl {
29 4     4 1 7932 return URI::PackageURL->from_string(shift);
30             }
31              
32             sub new {
33              
34 44     44 1 108687 my ($class, %components) = @_;
35              
36 44 100       355 Carp::croak "Invalid PackageURL: 'type' component is required" if (!defined $components{type});
37 42 100       407 Carp::croak "Invalid PackageURL: 'name' component is required" if (!defined $components{name});
38              
39 38         106 my $self = bless normalize_components(%components), $class;
40              
41 32         100 return $self;
42              
43             }
44              
45 0 0   0 1 0 sub scheme { shift->{scheme} || 'pkg' }
46 36     36 1 182 sub type { shift->{type} }
47 58     58 1 131 sub namespace { shift->{namespace} }
48 36     36 1 80 sub name { shift->{name} }
49 63     63 1 136 sub version { shift->{version} }
50 35     35 1 82 sub qualifiers { shift->{qualifiers} }
51 36     36 1 74 sub subpath { shift->{subpath} }
52              
53             sub normalize_components {
54              
55 38     38 0 119 my (%components) = @_;
56              
57 38         69 $components{type} = lc $components{type};
58              
59 38 100       80 if ($components{type} eq 'cpan') {
60 2         6 $components{name} =~ s/-/::/g;
61             }
62              
63 38 100       78 if ($components{type} eq 'pypi') {
64 1         3 $components{name} =~ s/_/-/g;
65             }
66              
67 38 100       60 if (grep { $_ eq $components{type} } qw(bitbucket deb github golang hex npm pypi)) {
  266         395  
68 10         28 $components{name} = lc $components{name};
69             }
70              
71              
72             # Checks
73              
74 38 100       83 if (my $qualifiers = $components{qualifiers}) {
75 14         18 foreach (keys %{$qualifiers}) {
  14         37  
76 23 100       156 Carp::croak "Invalid PackageURL: '$_' is not a valid qualifier" if ($_ =~ /\s/);
77             }
78             }
79              
80 37 100       75 if ($components{type} eq 'swift') {
81 3 100       97 Carp::croak "Invalid PackageURL: Swift 'version' is required" if (!defined $components{version});
82 2 100       94 Carp::croak "Invalid PackageURL: Swift 'namespace' is required" if (!defined $components{namespace});
83             }
84              
85 35 100       67 if ($components{type} eq 'cran') {
86 2 100       99 Carp::croak "Invalid PackageURL: Cran 'version' is required" if (!defined $components{version});
87             }
88              
89 34 100       56 if ($components{type} eq 'conan') {
90              
91 4 100 66     16 if (defined $components{namespace} && $components{namespace} ne '') {
92              
93 2 100       7 if (!defined $components{qualifiers}->{channel}) {
94 1         98 Carp::croak
95             "Invalid PackageURL: Conan 'channel' qualifier does not exist for namespace '$components{namespace}'";
96             }
97              
98             }
99             else {
100              
101 2 100       4 if (defined $components{qualifiers}->{channel}) {
102 1         90 Carp::croak
103             "Invalid PackageURL: Conan 'namespace' does not exist for channel '$components{qualifiers}->{channel}'";
104             }
105              
106             }
107              
108             }
109              
110 32         72 return \%components;
111              
112             }
113              
114             sub from_string {
115              
116 4     4 0 12 my ($class, $string) = @_;
117              
118 4         8 my %components = ();
119              
120              
121             # Split the purl string once from right on '#'
122             # The left side is the remainder
123             # Strip the right side from leading and trailing '/'
124             # Split this on '/'
125             # Discard any empty string segment from that split
126             # Discard any '.' or '..' segment from that split
127             # Percent-decode each segment
128             # UTF-8-decode each segment if needed in your programming language
129             # Join segments back with a '/'
130             # This is the subpath
131              
132 4         16 my @s1 = split('#', $string);
133              
134 4 100       13 if ($s1[1]) {
135 1         8 $s1[1] =~ s{(^\/|\/$)}{};
136 1 50 33     4 my @subpath = map { url_decode($_) } grep { $_ ne '' && $_ ne '.' && $_ ne '..' } split /\//, $s1[1];
  3         6  
  3         17  
137 1         5 $components{subpath} = join '/', @subpath;
138             }
139              
140             # Split the remainder once from right on '?'
141             # The left side is the remainder
142             # The right side is the qualifiers string
143             # Split the qualifiers on '&'. Each part is a key=value pair
144             # For each pair, split the key=value once from left on '=':
145             # The key is the lowercase left side
146             # The value is the percent-decoded right side
147             # UTF-8-decode the value if needed in your programming language
148             # Discard any key/value pairs where the value is empty
149             # If the key is checksums, split the value on ',' to create a list of checksums
150             # This list of key/value is the qualifiers object
151              
152 4         10 my @s2 = split(/\?/, $s1[0]);
153              
154 4 100       12 if ($s2[1]) {
155              
156 2         5 my @qualifiers = split('&', $s2[1]);
157              
158 2         4 foreach my $qualifier (@qualifiers) {
159              
160 3         9 my ($key, $value) = split('=', $qualifier);
161 3         9 $value = url_decode($value);
162              
163 3 50       9 if ($key eq 'checksums') {
164 0         0 $value = [split(',', $value)];
165             }
166              
167 3         10 $components{qualifiers}->{$key} = $value;
168              
169             }
170              
171             }
172              
173              
174             # Split the remainder once from left on ':'
175             # The left side lowercased is the scheme
176             # The right side is the remainder
177              
178 4         11 my @s3 = split(':', $s2[0], 2);
179 4         13 $components{scheme} = lc $s3[0];
180              
181              
182             # Strip the remainder from leading and trailing '/'
183             # Split this once from left on '/'
184             # The left side lowercased is the type
185             # The right side is the remainder
186              
187 4         41 $s3[1] =~ s{(^\/|\/$)}{};
188 4         30 my @s4 = split('/', $s3[1], 2);
189 4         11 $components{type} = lc $s4[0];
190              
191              
192             # Split the remainder once from right on '@'
193             # The left side is the remainder
194             # Percent-decode the right side. This is the version.
195             # UTF-8-decode the version if needed in your programming language
196             # This is the version
197              
198 4         11 my @s5 = split('@', $s4[1]);
199 4 50       16 $components{version} = url_decode($s5[1]) if ($s5[1]);
200              
201              
202             # Split the remainder once from right on '/'
203             # The left side is the remainder
204             # Percent-decode the right side. This is the name
205             # UTF-8-decode this name if needed in your programming language
206             # Apply type-specific normalization to the name if needed
207             # This is the name
208              
209 4         13 my @s6 = split('/', $s5[0], 2);
210 4 100       15 $components{name} = (scalar @s6 > 1) ? url_decode($s6[1]) : url_decode($s6[0]);
211              
212              
213             # Split the remainder on '/'
214             # Discard any empty segment from that split
215             # Percent-decode each segment
216             # UTF-8-decode the each segment if needed in your programming language
217             # Apply type-specific normalization to each segment if needed
218             # Join segments back with a '/'
219             # This is the namespace
220              
221 4 100       10 if (scalar @s6 > 1) {
222 3         8 my @s7 = split('/', $s6[0]);
223 3         7 $components{namespace} = join '/', map { url_decode($_) } @s7;
  3         5  
224             }
225              
226 4         21 return $class->new(%components);
227              
228             }
229              
230             sub to_string {
231              
232 32     32 1 258 my ($self) = @_;
233              
234 32         64 my @purl = ('pkg', ':', $self->type, '/');
235              
236             # Namespace
237 32 100       59 if ($self->namespace) {
238              
239 23         43 my @ns = map { url_encode($_) } split(/\//, $self->namespace);
  24         36  
240 23         66 push @purl, (join('/', @ns), '/');
241              
242             }
243              
244             # Name
245 32         52 push @purl, url_encode($self->name);
246              
247             # Version
248 32 100       54 push @purl, ('@', url_encode($self->version)) if ($self->version);
249              
250             # Qualifiers
251 32 100       58 if (my $qualifiers = $self->qualifiers) {
252              
253 13         15 my @qualifiers = map { sprintf('%s=%s', $_, url_encode($qualifiers->{$_})) } sort keys %{$qualifiers};
  21         37  
  13         37  
254 13 100       45 push @purl, ('?', join('&', @qualifiers)) if (@qualifiers);
255              
256             }
257              
258             # Subpath
259 32 100       56 push @purl, ('#', $self->subpath) if ($self->subpath);
260              
261 32         111 return join '', @purl;
262              
263             }
264              
265             sub url_encode {
266 104     104 0 122 my $string = shift;
267              
268             # RFC-3986 (but exclude "/" and ":")
269 104         172 $string =~ s/([^A-Za-z0-9\-._~\/:])/sprintf '%%%02X', ord $1/ge;
  3         17  
270 104         223 return $string;
271             }
272              
273              
274             sub url_decode {
275 17     17 0 21 my $string = shift;
276 17         23 $string =~ s/%([0-9a-fA-F]{2})/chr hex $1/ge;
  0         0  
277 17         40 return $string;
278             }
279              
280             sub TO_JSON {
281              
282 0     0 1   my ($self) = @_;
283              
284             return {
285 0           type => $self->type,
286             name => $self->name,
287             version => $self->version,
288             namespace => $self->namespace,
289             qualifiers => $self->qualifiers,
290             subpath => $self->subpath,
291             };
292              
293             }
294              
295             1;
296             __END__