File Coverage

blib/lib/URI/PackageURL.pm
Criterion Covered Total %
statement 120 125 96.0
branch 50 56 89.2
condition 6 9 66.6
subroutine 22 24 91.6
pod 12 16 75.0
total 210 230 91.3


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