File Coverage

blib/lib/Net/CIDR/Lite.pm
Criterion Covered Total %
statement 307 329 93.3
branch 129 172 75.0
condition 46 67 68.6
subroutine 34 36 94.4
pod 14 14 100.0
total 530 618 85.7


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