File Coverage

blib/lib/Net/CIDR/Lite.pm
Criterion Covered Total %
statement 306 328 93.2
branch 124 168 73.8
condition 40 62 64.5
subroutine 34 36 94.4
pod 14 14 100.0
total 518 608 85.2


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