File Coverage

blib/lib/Net/CIDR/Lite.pm
Criterion Covered Total %
statement 300 328 91.4
branch 120 168 71.4
condition 36 59 61.0
subroutine 34 36 94.4
pod 14 14 100.0
total 504 605 83.3


line stmt bran cond sub pod time code
1             package Net::CIDR::Lite;
2              
3 2     2   148871 use strict;
  2         9  
  2         122  
4 2     2   12 use vars qw($VERSION);
  2         5  
  2         106  
5 2     2   12 use Carp qw(confess);
  2         10  
  2         6567  
6              
7             $VERSION = '0.21';
8              
9             my %masks;
10             my @fields = qw(PACK UNPACK NBITS MASKS);
11              
12             # Preloaded methods go here.
13              
14             sub new {
15 18     18 1 1335 my $proto = shift;
16 18   33     126 my $class = ref($proto) || $proto;
17 18         47 my $self = bless {}, $class;
18 18         49 $self->add_any($_) for @_;
19 18         58 $self;
20             }
21              
22             sub add_any {
23 15     15 1 22 my $self = shift;
24 15         22 for (@_) {
25 15 100       37 tr|/|| && do { $self->add($_); next };
  8         16  
  8         18  
26 7 100       14 tr|-|| && do { $self->add_range($_); next };
  2         5  
  2         4  
27 5 50       24 UNIVERSAL::isa($_, 'Net::CIDR::Lite') && do {
28 0         0 $self->add_cidr($_); next
29 0         0 };
30 5         12 $self->add_ip($_), next;
31             }
32 15         36 $self;
33             }
34              
35             sub add {
36 19     19 1 56 my $self = shift;
37 19         55 my ($ip, $mask) = split "/", shift;
38 19 100 50     249 $self->_init($ip) || confess "Can't determine ip format" unless %$self;
39 19 100 66     343 confess "Bad mask $mask"
40             unless $mask =~ /^\d+$/ and $mask <= $self->{NBITS}-8;
41 18         23 $mask += 8;
42 18 50       38 my $start = $self->{PACK}->($ip) & $self->{MASKS}[$mask]
43             or confess "Bad ip address: $ip";
44 18         44 my $end = $self->_add_bit($start, $mask);
45 18 100       64 ++$$self{RANGES}{$start} || delete $$self{RANGES}{$start};
46 18 50       56 --$$self{RANGES}{$end} || delete $$self{RANGES}{$end};
47 18         33 $self;
48             }
49              
50             sub clean {
51 7     7 1 14 my $self = shift;
52 7 100       21 return $self unless $self->{RANGES};
53 4         6 my $ranges = $$self{RANGES};
54 4         6 my $total;
55             $$self{RANGES} = {
56 4         14 map { $total ? ($total+=$$ranges{$_})? () : ($_=>-1)
57 12 50       38 : do { $total+=$$ranges{$_}; ($_=>1) }
  6 100       8  
  6         12  
58             } sort keys %$ranges
59             };
60 4         13 $self;
61             }
62              
63             sub list {
64 5     5 1 20 my $self = shift;
65 5 100       20 return unless $self->{NBITS};
66 4         5 my $nbits = $$self{NBITS};
67 4         6 my ($start, $total);
68 0         0 my @results;
69 4         6 for my $ip (sort keys %{$$self{RANGES}}) {
  4         19  
70 8 100       14 $start = $ip unless $total;
71 8         14 $total += $$self{RANGES}{$ip};
72 8 100       18 unless ($total) {
73 4         9 while ($start lt $ip) {
74 5         6 my ($end, $bits);
75 5         7 my $sbit = $nbits-1;
76             # Find the position of the last 1 bit
77 5   100     377 $sbit-- while !vec($start, $sbit^7, 1) and $sbit>0;
78 5         12 for my $pos ($sbit+1..$nbits) {
79 117         217 $end = $self->_add_bit($start, $pos);
80 117 100       280 $bits = $pos-8, last if $end le $ip;
81             }
82 5         15 push @results, $self->{UNPACK}->($start) . "/$bits";
83 5         23 $start = $end;
84             }
85             }
86             }
87 4 50       23 wantarray ? @results : \@results;
88             }
89              
90             sub list_range {
91 3     3 1 7 my $self = shift;
92 3         3 my ($start, $total);
93 0         0 my @results;
94 3         5 for my $ip (sort keys %{$$self{RANGES}}) {
  3         12  
95 6 100       12 $start = $ip unless $total;
96 6         9 $total += $$self{RANGES}{$ip};
97 6 100       13 unless ($total) {
98 3         8 $ip = $self->_minus_one($ip);
99 3         8 push @results,
100             $self->{UNPACK}->($start) . "-" . $self->{UNPACK}->($ip);
101             }
102             }
103 3 50       16 wantarray ? @results : \@results;
104             }
105              
106             sub list_short_range {
107 3     3 1 5 my $self = shift;
108            
109 3         3 my $start;
110             my $total;
111 0         0 my @results;
112            
113 3         4 for my $ip (sort keys %{$$self{RANGES}}) {
  3         16  
114             # we begin new range when $total is zero
115 12 100       24 $start = $ip if not $total;
116            
117             # add to total (1 for start of the range or -1 for end of the range)
118 12         20 $total += $$self{RANGES}{$ip};
119            
120             # in case of end of range
121 12 100       23 if (not $total) {
122 6         13 while ($ip gt $start) {
123 10         20 $ip = $self->_minus_one($ip);
124            
125             # in case of single ip not a range
126 10 100       24 if ($ip eq $start) {
127 3         8 push @results,
128             $self->{UNPACK}->($start);
129 3         13 next;
130             }
131            
132             # get the last ip octet number
133 7         15 my $to_octet = ( unpack('C5', $ip) )[4];
134              
135             # next ip end will be current end masked by c subnet mask 255.255.255.0 - /24
136 7         16 $ip = $ip & $self->{MASKS}[32];
137              
138             # if the ip range is in the same c subnet
139 7 100       17 if ($ip eq ($start & $self->{MASKS}[32])) {
140 3         6 push @results,
141             $self->{UNPACK}->($start) . "-" . $to_octet;
142             }
143             # otherwise the range start is .0 (end of range masked by c subnet mask)
144             else {
145 4         12 push @results,
146             $self->{UNPACK}->($ip & $self->{MASKS}[32]) . "-" . $to_octet;
147             }
148             };
149             }
150             }
151 3 50       25 wantarray ? @results : \@results;
152             }
153              
154             sub _init {
155 16     16   24 my $self = shift;
156 16         22 my $ip = shift;
157 16         17 my ($nbits, $pack, $unpack);
158 16 100       30 if (_pack_ipv4($ip)) {
    50          
159 13         22 $nbits = 40;
160 13         19 $pack = \&_pack_ipv4;
161 13         22 $unpack = \&_unpack_ipv4;
162             } elsif (_pack_ipv6($ip)) {
163 3         6 $nbits = 136;
164 3         5 $pack = \&_pack_ipv6;
165 3         8 $unpack = \&_unpack_ipv6;
166             } else {
167 0         0 return;
168             }
169 16         35 $$self{PACK} = $pack;
170 16         29 $$self{UNPACK} = $unpack;
171 16         26 $$self{NBITS} = $nbits;
172 356         1175 $$self{MASKS} = $masks{$nbits} ||= [
173 16   100     72 map { pack("B*", substr("1" x $_ . "0" x $nbits, 0, $nbits))
174             } 0..$nbits
175             ];
176 16         53 $$self{RANGES} = {};
177 16         56 $self;
178             }
179              
180             sub _pack_ipv4 {
181 51     51   165 my @nums = split /\./, shift(), -1;
182 51 100       129 return unless @nums == 4;
183 48         68 for (@nums) {
184 192 50 33     1054 return unless /^\d{1,3}$/ and $_ <= 255;
185             }
186 48         223 pack("CC*", 0, @nums);
187             }
188              
189             sub _unpack_ipv4 {
190 22     22   154 join(".", unpack("xC*", shift));
191             }
192              
193             sub _pack_ipv6 {
194 9     9   14 my $ip = shift;
195 9         20 $ip =~ s/^::$/::0/;
196 9 50 66     39 return if $ip =~ /^:/ and $ip !~ s/^::/:/;
197 9 50 33     29 return if $ip =~ /:$/ and $ip !~ s/::$/:/;
198 9         41 my @nums = split /:/, $ip, -1;
199 9 50       25 return unless @nums <= 8;
200 9         18 my ($empty, $ipv4, $str) = (0,'','');
201 9         18 for (@nums) {
202 60 50       102 return if $ipv4;
203 60 100       234 $str .= "0" x (4-length) . $_, next if /^[a-fA-F\d]{1,4}$/;
204 2 50       5 do { return if $empty++ }, $str .= "X", next if $_ eq '';
  2 50       7  
205 0 0       0 next if $ipv4 = _pack_ipv4($_);
206 0         0 return;
207             }
208 9 50 33     25 return if $ipv4 and @nums > 6;
209 9 50       28 $str =~ s/X/"0" x (($ipv4 ? 25 : 33)-length($str))/e if $empty;
  2 100       11  
210 9         74 pack("H*", "00" . $str).$ipv4;
211             }
212              
213             sub _unpack_ipv6 {
214 3     3   36 _compress_ipv6(join(":", unpack("xH*", shift) =~ /..../g)),
215             }
216              
217             # Replace longest run of null blocks with a double colon
218             sub _compress_ipv6 {
219 3     3   5 my $ip = shift;
220 3 50       36 if (my @runs = $ip =~ /((?:(?:^|:)(?:0000))+:?)/g ) {
221 3         5 my $max = $runs[0];
222 3         9 for (@runs[1..$#runs]) {
223 0 0       0 $max = $_ if length($max) < length;
224             }
225 3         34 $ip =~ s/$max/::/;
226             }
227 3         11 $ip =~ s/:0{1,3}/:/g;
228 3         12 $ip;
229             }
230              
231             # Add a single IP address
232             sub add_ip {
233 5     5 1 6 my $self = shift;
234 5         8 my $ip = shift;
235 5 100 33     11 $self->_init($ip) || confess "Can't determine ip format" unless %$self;
236 5 50       9 my $start = $self->{PACK}->($ip) or confess "Bad ip address: $ip";
237 5         11 my $end = $self->_add_bit($start, $self->{NBITS});
238 5 100       17 ++$$self{RANGES}{$start} || delete $$self{RANGES}{$start};
239 5 50       13 --$$self{RANGES}{$end} || delete $$self{RANGES}{$end};
240 5         15 $self;
241             }
242              
243             # Add a hyphenated range of IP addresses
244             sub add_range {
245 3     3 1 8 my $self = shift;
246 3         7 local $_ = shift;
247 3         19 my ($ip_start, $ip_end, $crud) = split /\s*-\s*/;
248 3 50       12 confess "Only one hyphen allowed in range" if defined $crud;
249 3 100 50     18 $self->_init($ip_start) || confess "Can't determine ip format"
250             unless %$self;
251 3 50       11 my $start = $self->{PACK}->($ip_start)
252             or confess "Bad ip address: $ip_start";
253 3 50       21 my $end = $self->{PACK}->($ip_end)
254             or confess "Bad ip address: $ip_end";
255 3 50       10 confess "Start IP is greater than end IP" if $start gt $end;
256 3         13 $end = $self->_add_bit($end, $$self{NBITS});
257 3 50       15 ++$$self{RANGES}{$start} || delete $$self{RANGES}{$start};
258 3 50       12 --$$self{RANGES}{$end} || delete $$self{RANGES}{$end};
259 3         7 $self;
260             }
261              
262             # Add ranges from another Net::CIDR::Lite object
263             sub add_cidr {
264 0     0 1 0 my $self = shift;
265 0         0 my $cidr = shift;
266 0 0       0 confess "Not a CIDR object" unless UNIVERSAL::isa($cidr, 'Net::CIDR::Lite');
267 0 0       0 unless (%$self) {
268 0         0 @$self{@fields} = @$cidr{@fields};
269             }
270 0         0 $$self{RANGES}{$_} += $$cidr{RANGES}{$_} for keys %{$$cidr{RANGES}};
  0         0  
271 0         0 $self;
272             }
273              
274             # Increment the ip address at the given bit position
275             # bit position is in range 1 to # of bits in ip
276             # where 1 is high order bit, # of bits is low order bit
277             sub _add_bit {
278 182     182   265 my $self= shift;
279 182         211 my $base= shift();
280 182         219 my $bits= shift()-1;
281 182         397 while (vec($base, $bits^7, 1)) {
282 382         585 vec($base, $bits^7, 1) = 0;
283 382         916 $bits--;
284 382 100       1129 return $base if $bits < 0;
285             }
286 178         357 vec($base, $bits^7, 1) = 1;
287 178         384 return $base;
288             }
289              
290             # Subtract one from an ip address
291             sub _minus_one {
292 13     13   14 my $self = shift;
293 13         20 my $nbits = $self->{NBITS};
294 13         38 my $ip = shift;
295 13         21 $ip = ~$ip;
296 13         48 $ip = $self->_add_bit($ip, $nbits);
297 13         27 $ip = $self->_add_bit($ip, $nbits);
298 13         34 $self->_add_bit(~$ip, $nbits);
299             }
300              
301             sub find {
302 5     5 1 92 my $self = shift;
303 5 100       21 $self->prep_find unless $self->{FIND};
304 5 50       6 return $self->bin_find(@_) unless @{$self->{FIND}} < $self->{PCT};
  5         14  
305 5 100       16 return 0 unless $self->{PACK};
306 4         8 my $this_ip = $self->{PACK}->(shift);
307 4         8 my $ranges = $self->{RANGES};
308 4         5 my $last = -1;
309 4         5 for my $ip (@{$self->{FIND}}) {
  4         8  
310 7 100       15 last if $this_ip lt $ip;
311 4         9 $last = $ranges->{$ip};
312             }
313 4         14 $last > 0;
314             }
315              
316             sub bin_find {
317 4     4 1 10 my $self = shift;
318 4         10 my $ip = $self->{PACK}->(shift);
319 4 100       13 $self->prep_find unless $self->{FIND};
320 4         6 my $find = $self->{FIND};
321 4         8 my ($start, $end) = (0, $#$find);
322 4 100 100     23 return unless $ip ge $find->[$start] and $ip lt $find->[$end];
323 2         5 while ($end - $start > 0) {
324 4         8 my $mid = int(($start+$end)/2);
325 4 100       8 if ($start == $mid) {
326 2 100       6 if ($find->[$end] eq $ip) {
327 1         2 $start = $end;
328 1         3 } else { $end = $start }
329             } else {
330 2 100       8 ($find->[$mid] lt $ip ? $start : $end) = $mid;
331             }
332             }
333 2         10 $self->{RANGES}{$find->[$start]} > 0;
334             }
335              
336             sub prep_find {
337 3     3 1 4 my $self = shift;
338 3         8 $self->clean;
339 3   50     15 $self->{PCT} = shift || 20;
340 3         6 my $aref = $self->{FIND} = [];
341 3         5 push @$aref, $_ for sort keys %{$self->{RANGES}};
  3         19  
342 3         6 $self;
343             }
344              
345             sub spanner {
346 2     2 1 151 Net::CIDR::Lite::Span->new(@_);
347             }
348              
349             sub _ranges {
350 3     3   4 sort keys %{shift->{RANGES}};
  3         16  
351             }
352              
353 2     2   5 sub _packer { shift->{PACK} }
354 2     2   5 sub _unpacker { shift->{UNPACK} }
355              
356             package Net::CIDR::Lite::Span;
357 2     2   22 use Carp qw(confess);
  2         2  
  2         2707  
358              
359             sub new {
360 2     2   4 my $proto = shift;
361 2   33     11 my $class = ref($proto) || $proto;
362 2         10 my $self = bless {RANGES=>{}}, $class;
363 2         6 $self->add(@_);
364             }
365              
366             sub add {
367 3     3   98 my $self = shift;
368 3         11 my $ranges = $self->{RANGES};
369 3 100 66     16 if (@_ && !$self->{PACK}) {
370 2         3 my $cidr = $_[0];
371 2 50       9 $cidr = Net::CIDR::Lite->new($cidr) unless ref($cidr);
372 2         6 $self->{PACK} = $cidr->_packer;
373 2         7 $self->{UNPACK} = $cidr->_unpacker;
374             }
375 3         10 while (@_) {
376 3         6 my ($cidr, $label) = (shift, shift);
377 3 100       12 $cidr = Net::CIDR::Lite->new($cidr) unless ref($cidr);
378 3         7 $cidr->clean;
379 3         9 for my $ip ($cidr->_ranges) {
380 4         5 push @{$ranges->{$ip}}, $label;
  4         26  
381             }
382             }
383 3         8 $self;
384             }
385              
386             sub find {
387 4     4   48 my $self = shift;
388 4         6 my $pack = $self->{PACK};
389 4         6 my $unpack = $self->{UNPACK};
390 4         5 my %results;
391             my $in_range;
392 4 100       14 $self->prep_find unless $self->{FIND};
393 4 50       9 return {} unless @_;
394 4 100       6 return { map { $_ => {} } @_ } unless @{$self->{FIND}};
  1         7  
  4         30  
395 3 100       4 return $self->bin_find(@_) if @_/@{$self->{FIND}} < $self->{PCT};
  3         29  
396 2 50       4 my @ips = sort map { $pack->($_) || confess "Bad IP: $_" } @_;
  3         5  
397 2         3 my $last;
398 2         3 for my $ip (@{$self->{FIND}}) {
  2         5  
399 5 100       14 if ($ips[0] lt $ip) {
400 3   100     18 $results{$unpack->(shift @ips)} = $self->_in_range($last)
401             while @ips and $ips[0] lt $ip;
402             }
403 5 100       11 last unless @ips;
404 3         5 $last = $ip;
405             }
406 2 50       6 if (@ips) {
407 0         0 my $no_range = $self->_in_range({});
408 0         0 $results{$unpack->(shift @ips)} = $no_range while @ips;
409             }
410 2         14 \%results;
411             }
412              
413             sub bin_find {
414 1     1   2 my $self = shift;
415 1 50       3 return {} unless @_;
416 1 50       3 $self->prep_find unless $self->{FIND};
417 1 50       1 return { map { $_ => {} } @_ } unless @{$self->{FIND}};
  0         0  
  1         3  
418 1         2 my $pack = $self->{PACK};
419 1         2 my $unpack = $self->{UNPACK};
420 1         2 my $find = $self->{FIND};
421 1         2 my %results;
422 1 50       2 for my $ip ( map { $pack->($_) || confess "Bad IP: $_" } @_) {
  1         2  
423 1         3 my ($start, $end) = (0, $#$find);
424 1 50 33     8 $results{$unpack->($ip)} = $self->_in_range, next
425             unless $ip ge $find->[$start] and $ip lt $find->[$end];
426 1         3 while ($start < $end) {
427 2         5 my $mid = int(($start+$end)/2);
428 2 100       4 if ($start == $mid) {
429 1 50       3 if ($find->[$end] eq $ip) {
430 1         3 $start = $end;
431 0         0 } else { $end = $start }
432             } else {
433 1 50       4 ($find->[$mid] lt $ip ? $start : $end) = $mid;
434             }
435             }
436 1         3 $results{$unpack->($ip)} = $self->_in_range($find->[$start]);
437             }
438 1         4 \%results;
439             }
440              
441             sub _in_range {
442 4     4   5 my $self = shift;
443 4   100     11 my $ip = shift || '';
444 4   100     12 my $aref = $self->{PREPPED}{$ip} || [];
445 4         10 my $key = join "|", sort @$aref;
446 4   50     19 $self->{CACHE}{$key} ||= { map { $_ => 1 } @$aref };
  5         24  
447             }
448              
449             sub prep_find {
450 4     4   95 my $self = shift;
451 4   100     15 my $pct = shift || 4;
452 4         10 $self->{PCT} = $pct/100;
453 4         17 $self->{FIND} = [ sort keys %{$self->{RANGES}} ];
  4         24  
454 4         9 $self->{PREPPED} = {};
455 4         521 $self->{CACHE} = {};
456 4         6 my %cache;
457             my %in_range;
458 4         5 for my $ip (@{$self->{FIND}}) {
  4         10  
459 10         18 my $keys = $self->{RANGES}{$ip};
460 10         28 $_ = !$_ for @in_range{@$keys};
461 10         31 my @keys = grep $in_range{$_}, keys %in_range;
462 10         17 my $key_str = join "|", @keys;
463 10   100     50 $self->{PREPPED}{$ip} = $cache{$key_str} ||= \@keys;
464             }
465 4         15 $self;
466             }
467              
468             sub clean {
469 0     0     my $self = shift;
470 0 0         unless ($self->{PACK}) {
471 0           my $ip = shift;
472 0           my $cidr = Net::CIDR::Lite->new($ip);
473 0           return $cidr->clean($ip);
474             }
475 0   0       my $ip = $self->{PACK}->(shift) || return;
476 0           $self->{UNPACK}->($ip);
477             }
478              
479             1;
480             __END__