File Coverage

blib/lib/Net/CIDR/Set.pm
Criterion Covered Total %
statement 207 228 90.7
branch 51 60 85.0
condition 17 29 58.6
subroutine 42 49 85.7
pod 25 26 96.1
total 342 392 87.2


line stmt bran cond sub pod time code
1             package Net::CIDR::Set;
2              
3             # ABSTRACT: Manipulate sets of IP addresses
4              
5 8     8   941953 use v5.14;
  8         32  
6 8     8   54 use warnings;
  8         49  
  8         672  
7              
8 8     8   54 use Carp qw( croak confess );
  8         14  
  8         517  
9 8     8   4570 use Net::CIDR::Set::IPv4;
  8         38  
  8         370  
10 8     8   5146 use Net::CIDR::Set::IPv6;
  8         39  
  8         444  
11              
12 8     8   6193 use overload '""' => 'as_string';
  8         16742  
  8         66  
13              
14 8     8   750 use namespace::autoclean;
  8         16  
  8         62  
15              
16             our $VERSION = '0.19';
17              
18              
19             {
20             my %type_map = (
21             ipv4 => 'Net::CIDR::Set::IPv4',
22             ipv6 => 'Net::CIDR::Set::IPv6',
23             );
24              
25             sub new {
26 109     109 1 1467824 my $self = shift;
27 109   66     461 my $class = ref $self || $self;
28 109         338 my $set = bless { ranges => [] }, $class;
29 109 50       305 my $opt = 'HASH' eq ref $_[0] ? shift : {};
30 109 50       356 if ( defined( my $type = delete $opt->{type} ) ) {
    100          
31 0   0     0 my $coder_class = $type_map{$type} || $type;
32 0         0 $set->{coder} = $coder_class->new;
33             }
34             elsif ( ref $self ) {
35 16         32 $set->{coder} = $self->{coder};
36             }
37 109         292 my @unk = keys %$opt;
38 109 50       243 croak "Unknown options: ", _and( sort @unk ) if @unk;
39 109 100       380 $set->add( @_ ) if @_;
40 107         366 return $set;
41             }
42             }
43              
44             # Return the index of the first element >= the supplied value. If the
45             # supplied value is larger than any element in the list the returned
46             # value will be equal to the size of the list.
47             sub _find_pos {
48 328     328   437 my $self = shift;
49 328         417 my $val = shift;
50 328   100     701 my $low = shift || 0;
51              
52 328         369 my $high = scalar( @{ $self->{ranges} } );
  328         498  
53              
54 328         659 while ( $low < $high ) {
55 192         345 my $mid = int( ( $low + $high ) / 2 );
56 192         319 my $cmp = $val cmp $self->{ranges}[$mid];
57 192 100       936 if ( $cmp < 0 ) {
    100          
58 43         93 $high = $mid;
59             }
60             elsif ( $cmp > 0 ) {
61 97         194 $low = $mid + 1;
62             }
63             else {
64 52         136 return $mid;
65             }
66             }
67              
68 276         460 return $low;
69             }
70              
71             sub _inc {
72 678     678   215760 my @b = reverse unpack 'C*', shift;
73 678         1117 for ( @b ) {
74 2990 100       5829 last unless ++$_ == 256;
75 2313         3973 $_ = 0;
76             }
77 678         2538 return pack 'C*', reverse @b;
78             }
79              
80             sub _dec {
81 158     158   2705 my @b = reverse unpack 'C*', shift;
82 158         331 for ( @b ) {
83 605 100       1162 last unless $_-- == 0;
84 448         654 $_ = 255;
85             }
86 158         843 return pack 'C*', reverse @b;
87             }
88              
89             sub _guess_coder {
90 91     91   221 my ( $self, $ip ) = @_;
91 91         143 for my $class ( qw( Net::CIDR::Set::IPv4 Net::CIDR::Set::IPv6 ) ) {
92 128         475 my $coder = $class->new;
93 128         204 ( eval { $coder->encode( $ip ) } );
  128         340  
94 128 100       557 return $coder unless $@;
95             }
96 4         481 croak "Can't decode $ip as an IPv4 or IPv6 address";
97             }
98              
99             sub _encode {
100 153     153   300 my ( $self, $ip ) = @_;
101 153   100     725 my $cdr = $self->{coder} ||= $self->_guess_coder( $ip );
102 149         314 return $cdr->encode( $ip );
103             }
104              
105             {
106             for my $dele ( qw( _decode _nbits ) ) {
107 8     8   7252 no strict 'refs'; ## no critic (ProhibitNoStrict)
  8         18  
  8         23882  
108             my $meth = $dele =~ s/^_//r;
109             *{$dele} = sub {
110 107     107   155 my $self = shift;
111 107   33     235 my $cdr = $self->{coder} || croak "Don't know how to $meth yet";
112 107         355 return $cdr->$meth( @_ );
113             };
114             }
115             }
116              
117             sub _conjunction {
118 2     2   756 my ( $conj, @list ) = @_;
119 2         8 my $last = pop @list;
120 2         20 return join " $conj ", join( ', ', @list ), $last;
121             }
122              
123 1     1   30 sub _and { _conjunction( 'and', @_ ) }
124              
125             sub _check_and_coerce {
126 9     9   20 my ( $self, @others ) = @_;
127              
128             my %class = map {
129 20   100     28 eval { ( defined $_ && $_->nbits || '' ) => $_ }
  20         72  
130 9         49 } map { $_->{coder} } grep { defined } $self, @others;
  20         40  
  20         41  
131              
132 9         41 my @found = sort grep $_, keys %class;
133              
134 9 50       24 croak "Can't mix ", _and( @found ), " bit addresses"
135             if @found > 1;
136              
137 9   66     53 $self->{coder} ||= $class{ $found[0] };
138 9         40 return $self;
139             }
140              
141              
142             sub invert {
143 15     15 1 24 my $self = shift;
144              
145 15         38 my @pad = ( 0 ) x ( $self->_nbits / 8 );
146 15         31 my ( $min, $max ) = map { pack 'C*', $_, @pad } 0, 1;
  30         97  
147              
148 15 50       37 if ( $self->is_empty ) {
149 0         0 $self->{ranges} = [ $min, $max ];
150 0         0 return;
151             }
152              
153 15 100       42 if ( $self->{ranges}[0] eq $min ) {
154 8         13 shift @{ $self->{ranges} };
  8         18  
155             }
156             else {
157 7         11 unshift @{ $self->{ranges} }, $min;
  7         16  
158             }
159              
160 15 100       33 if ( $self->{ranges}[-1] eq $max ) {
161 8         16 pop @{ $self->{ranges} };
  8         19  
162             }
163             else {
164 7         11 push @{ $self->{ranges} }, $max;
  7         23  
165             }
166             }
167              
168              
169             sub copy {
170 16     16 1 24 my $self = shift;
171 16         35 my $copy = $self->new;
172 16         26 @{ $copy->{ranges} } = @{ $self->{ranges} };
  16         50  
  16         28  
173 16         29 return $copy;
174             }
175              
176             sub _add_range {
177 164     164   307 my ( $self, $from, $to ) = @_;
178 164         333 my $fpos = $self->_find_pos( $from );
179 164         4238 my $tpos = $self->_find_pos( _inc( $to ), $fpos );
180              
181 164 100       395 $from = $self->{ranges}[ --$fpos ] if ( $fpos & 1 );
182 164 100       313 $to = $self->{ranges}[ $tpos++ ] if ( $tpos & 1 );
183              
184 164         202 splice @{ $self->{ranges} }, $fpos, $tpos - $fpos, ( $from, $to );
  164         608  
185             }
186              
187              
188             sub add {
189 93     93 1 3429 my ( $self, @addr ) = @_;
190 93         207 for my $ip ( map { split /\s*,\s*/ } @addr ) {
  120         636  
191 153 50       374 my ( $lo, $hi ) = $self->_encode( $ip )
192             or croak "Can't decode $ip";
193 148         342 $self->_add_range( $lo, $hi );
194             }
195             }
196              
197              
198             sub remove {
199 1     1 1 530 my $self = shift;
200              
201 1         6 $self->invert;
202 1         3 $self->add( @_ );
203 1         3 $self->invert;
204             }
205              
206              
207             sub merge {
208 9     9 1 15 my $self = shift;
209 9         27 $self->_check_and_coerce( @_ );
210              
211             # TODO: This isn't very efficient - and merge gets called from all
212             # sorts of other places.
213 9         20 for my $other ( @_ ) {
214 11         25 my $iter = $other->_iterate_runs;
215 11         21 while ( my ( $from, $to ) = $iter->() ) {
216 16         34 $self->_add_range( $from, $to );
217             }
218             }
219             }
220              
221              
222             *contains = *contains_all;
223              
224             sub contains_all {
225 0     0 1 0 my $self = shift;
226 0         0 my $class = ref $self;
227 0         0 return $class->new( @_ )->subset( $self );
228             }
229              
230              
231             sub contains_any {
232 0     0 1 0 my $self = shift;
233 0         0 my $class = ref $self;
234 0         0 return !$class->new( @_ )->intersection( $self )->is_empty;
235             }
236              
237             sub _iterate_runs {
238 62     62   88 my $self = shift;
239              
240 62         84 my $pos = 0;
241 62         72 my $limit = scalar( @{ $self->{ranges} } );
  62         93  
242              
243             return sub {
244 147 100   147   456 return if $pos >= $limit;
245 85         146 my @r = @{ $self->{ranges} }[ $pos, $pos + 1 ];
  85         233  
246 85         146 $pos += 2;
247 85         249 return @r;
248 62         331 };
249             }
250              
251             sub compliment {
252 0     0 0 0 croak "That's very kind of you - but I expect you meant complement";
253             }
254              
255              
256             sub complement {
257 10     10 1 29 my $new = shift->copy;
258             # TODO: What if it's empty?
259 10         30 $new->invert;
260 10         33 return $new;
261             }
262              
263              
264             sub union {
265 6     6 1 32 my $new = shift->copy;
266 6         22 $new->merge( @_ );
267 6         24 return $new;
268             }
269              
270              
271             sub intersection {
272 3     3 1 10 my $self = shift;
273 3         10 my $class = ref $self;
274 3         10 my $new = $class->new;
275 3         10 $new->merge( map { $_->complement } $self, @_ );
  6         17  
276 3         23 $new->invert;
277 3         11 return $new;
278             }
279              
280              
281             sub xor {
282 0     0 1 0 my $self = shift;
283 0         0 return $self->union( @_ )
284             ->intersection( $self->intersection( @_ )->complement );
285             }
286              
287              
288             sub diff {
289 0     0 1 0 my $self = shift;
290 0         0 my $other = shift;
291 0         0 return $self->intersection( $other->union( @_ )->complement );
292             }
293              
294              
295             sub is_empty {
296 16     16 1 22 my $self = shift;
297 16         25 return @{ $self->{ranges} } == 0;
  16         50  
298             }
299              
300              
301             sub superset {
302 0     0 1 0 my $other = pop;
303 0         0 return $other->subset( reverse( @_ ) );
304             }
305              
306              
307             sub subset {
308 0     0 1 0 my $self = shift;
309 0   0     0 my $other = shift || croak "I need two sets to compare";
310 0         0 return $self->equals( $self->intersection( $other ) );
311             }
312              
313              
314             sub equals {
315 36 50   36 1 144 return unless @_;
316              
317             # Array of array refs
318 36         62 my @edges = map { $_->{ranges} } @_;
  72         129  
319 36         95 my $medge = scalar( @edges ) - 1;
320              
321 36         58 POS: for ( my $pos = 0;; $pos++ ) {
322 114         228 my $v = $edges[0]->[$pos];
323 114 100       169 if ( defined( $v ) ) {
324 78         149 for ( @edges[ 1 .. $medge ] ) {
325 78         105 my $vv = $_->[$pos];
326 78 50 33     252 return unless defined( $vv ) && $vv eq $v;
327             }
328             }
329             else {
330 36         56 for ( @edges[ 1 .. $medge ] ) {
331 36 50       66 return if defined $_->[$pos];
332             }
333             }
334              
335 114 100       237 last POS unless defined( $v );
336             }
337              
338 36         111 return 1;
339             }
340              
341              
342             sub iterate_addresses {
343 1     1 1 2 my ( $self, @args ) = @_;
344 1         11 my $iter = $self->_iterate_runs;
345 1         1 my @r = ();
346             return sub {
347 2     2   3 while ( 1 ) {
348 3 100 100     11 @r = $iter->() or return unless @r;
349 2 100       6 return $self->_decode( ( my $last, $r[0] )
350             = ( $r[0], _inc( $r[0] ) ), @args )
351             unless $r[0] eq $r[1];
352 1         3 @r = ();
353             }
354 1         18 };
355             }
356              
357              
358             sub iterate_cidr {
359 8     8 1 23 my ( $self, @args ) = @_;
360 8         39 my $iter = $self->_iterate_runs;
361 8         27 my $size = $self->_nbits;
362 8         27 my @r = ();
363             return sub {
364 39     39   61 while ( 1 ) {
365 55 100 100     112 @r = $iter->() or return unless @r;
366 47 100       94 unless ( $r[0] eq $r[1] ) {
367 31         569 ( my $bits = unpack 'B*', $r[0] ) =~ /(0*)$/;
368 31         62 my $pad = length $1;
369 31 100       59 $pad = $size if $pad > $size;
370 31         41 while ( 1 ) {
371 205         732 my $next = _inc( $r[0] | pack 'B*',
372             ( '0' x ( length( $bits ) - $pad ) ) . ( '1' x $pad ) );
373 205 100       493 return $self->_decode( ( my $last, $r[0] ) = ( $r[0], $next ),
374             @args )
375             if $next le $r[1];
376 174         257 $pad--;
377             }
378             }
379 16         30 @r = ();
380             }
381 8         83 };
382             }
383              
384              
385             sub iterate_ranges {
386 42     42 1 100 my ( $self, @args ) = @_;
387 42         119 my $iter = $self->_iterate_runs;
388             return sub {
389 94 100   94   146 return unless my @r = $iter->();
390 52         123 return $self->_decode( @r, @args );
391 42         174 };
392             }
393              
394              
395             sub as_array {
396 51     51 1 89 my ( $self, $iter ) = @_;
397 51         89 my @addr = ();
398 51         93 while ( my $addr = $iter->() ) {
399 84         264 push @addr, $addr;
400             }
401 51         404 return @addr;
402             }
403              
404              
405             sub as_address_array {
406 1     1 1 1 my $self = shift;
407 1         6 return $self->as_array( $self->iterate_addresses( @_ ) );
408             }
409              
410              
411             sub as_cidr_array {
412 8     8 1 325 my $self = shift;
413 8         29 return $self->as_array( $self->iterate_cidr( @_ ) );
414             }
415              
416              
417             sub as_range_array {
418 42     42 1 72 my $self = shift;
419 42         113 return $self->as_array( $self->iterate_ranges( @_ ) );
420             }
421              
422              
423 36     36 1 498 sub as_string { join ', ', shift->as_range_array( @_ ) }
424              
425             1;
426              
427             __END__