File Coverage

blib/lib/Net/CIDR/Lite.pm
Criterion Covered Total %
statement 307 329 93.3
branch 129 172 75.0
condition 49 70 70.0
subroutine 34 36 94.4
pod 14 14 100.0
total 533 621 85.8


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