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   697612 use v5.14;
  8         25  
6 8     8   52 use warnings;
  8         47  
  8         493  
7              
8 8     8   46 use Carp qw( croak confess );
  8         13  
  8         451  
9 8     8   3364 use Net::CIDR::Set::IPv4;
  8         28  
  8         268  
10 8     8   3346 use Net::CIDR::Set::IPv6;
  8         45  
  8         308  
11              
12 8     8   4341 use overload '""' => 'as_string';
  8         11733  
  8         47  
13              
14 8     8   609 use namespace::autoclean;
  8         26  
  8         47  
15              
16             our $VERSION = '0.20';
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 894756 my $self = shift;
27 109   66     320 my $class = ref $self || $self;
28 109         289 my $set = bless { ranges => [] }, $class;
29 109 50       241 my $opt = 'HASH' eq ref $_[0] ? shift : {};
30 109 50       312 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         218 my @unk = keys %$opt;
38 109 50       196 croak "Unknown options: ", _and( sort @unk ) if @unk;
39 109 100       324 $set->add( @_ ) if @_;
40 107         374 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   359 my $self = shift;
49 328         366 my $val = shift;
50 328   100     693 my $low = shift || 0;
51              
52 328         348 my $high = scalar( @{ $self->{ranges} } );
  328         489  
53              
54 328         543 while ( $low < $high ) {
55 192         263 my $mid = int( ( $low + $high ) / 2 );
56 192         232 my $cmp = $val cmp $self->{ranges}[$mid];
57 192 100       280 if ( $cmp < 0 ) {
    100          
58 43         53 $high = $mid;
59             }
60             elsif ( $cmp > 0 ) {
61 97         146 $low = $mid + 1;
62             }
63             else {
64 52         109 return $mid;
65             }
66             }
67              
68 276         372 return $low;
69             }
70              
71             sub _inc {
72 678     678   163626 my @b = reverse unpack 'C*', shift;
73 678         863 for ( @b ) {
74 2990 100       3781 last unless ++$_ == 256;
75 2313         2273 $_ = 0;
76             }
77 678         1918 return pack 'C*', reverse @b;
78             }
79              
80             sub _dec {
81 158     158   1822 my @b = reverse unpack 'C*', shift;
82 158         236 for ( @b ) {
83 605 100       846 last unless $_-- == 0;
84 448         473 $_ = 255;
85             }
86 158         519 return pack 'C*', reverse @b;
87             }
88              
89             sub _guess_coder {
90 91     91   136 my ( $self, $ip ) = @_;
91 91         125 for my $class ( qw( Net::CIDR::Set::IPv4 Net::CIDR::Set::IPv6 ) ) {
92 128         385 my $coder = $class->new;
93 128         173 ( eval { $coder->encode( $ip ) } );
  128         260  
94 128 100       476 return $coder unless $@;
95             }
96 4         259 croak "Can't decode $ip as an IPv4 or IPv6 address";
97             }
98              
99             sub _encode {
100 153     153   238 my ( $self, $ip ) = @_;
101 153   100     538 my $cdr = $self->{coder} ||= $self->_guess_coder( $ip );
102 149         317 return $cdr->encode( $ip );
103             }
104              
105             {
106             for my $dele ( qw( _decode _nbits ) ) {
107 8     8   5176 no strict 'refs'; ## no critic (ProhibitNoStrict)
  8         12  
  8         16523  
108             my $meth = $dele =~ s/^_//r;
109             *{$dele} = sub {
110 107     107   114 my $self = shift;
111 107   33     210 my $cdr = $self->{coder} || croak "Don't know how to $meth yet";
112 107         266 return $cdr->$meth( @_ );
113             };
114             }
115             }
116              
117             sub _conjunction {
118 2     2   518 my ( $conj, @list ) = @_;
119 2         3 my $last = pop @list;
120 2         11 return join " $conj ", join( ', ', @list ), $last;
121             }
122              
123 1     1   3 sub _and { _conjunction( 'and', @_ ) }
124              
125             sub _check_and_coerce {
126 9     9   27 my ( $self, @others ) = @_;
127              
128             my %class = map {
129 20   100     24 eval { ( defined $_ && $_->nbits || '' ) => $_ }
  20         52  
130 9         15 } map { $_->{coder} } grep { defined } $self, @others;
  20         28  
  20         34  
131              
132 9         31 my @found = sort grep $_, keys %class;
133              
134 9 50       16 croak "Can't mix ", _and( @found ), " bit addresses"
135             if @found > 1;
136              
137 9   66     27 $self->{coder} ||= $class{ $found[0] };
138 9         16 return $self;
139             }
140              
141              
142             sub invert {
143 15     15 1 21 my $self = shift;
144              
145 15         32 my @pad = ( 0 ) x ( $self->_nbits / 8 );
146 15         26 my ( $min, $max ) = map { pack 'C*', $_, @pad } 0, 1;
  30         79  
147              
148 15 50       33 if ( $self->is_empty ) {
149 0         0 $self->{ranges} = [ $min, $max ];
150 0         0 return;
151             }
152              
153 15 100       33 if ( $self->{ranges}[0] eq $min ) {
154 8         10 shift @{ $self->{ranges} };
  8         12  
155             }
156             else {
157 7         11 unshift @{ $self->{ranges} }, $min;
  7         16  
158             }
159              
160 15 100       31 if ( $self->{ranges}[-1] eq $max ) {
161 8         26 pop @{ $self->{ranges} };
  8         15  
162             }
163             else {
164 7         8 push @{ $self->{ranges} }, $max;
  7         20  
165             }
166             }
167              
168              
169             sub copy {
170 16     16 1 25 my $self = shift;
171 16         32 my $copy = $self->new;
172 16         23 @{ $copy->{ranges} } = @{ $self->{ranges} };
  16         44  
  16         37  
173 16         27 return $copy;
174             }
175              
176             sub _add_range {
177 164     164   296 my ( $self, $from, $to ) = @_;
178 164         286 my $fpos = $self->_find_pos( $from );
179 164         257 my $tpos = $self->_find_pos( _inc( $to ), $fpos );
180              
181 164 100       337 $from = $self->{ranges}[ --$fpos ] if ( $fpos & 1 );
182 164 100       299 $to = $self->{ranges}[ $tpos++ ] if ( $tpos & 1 );
183              
184 164         174 splice @{ $self->{ranges} }, $fpos, $tpos - $fpos, ( $from, $to );
  164         571  
185             }
186              
187              
188             sub add {
189 93     93 1 1421 my ( $self, @addr ) = @_;
190 93         213 for my $ip ( map { split /\s*,\s*/ } @addr ) {
  120         500  
191 153 50       278 my ( $lo, $hi ) = $self->_encode( $ip )
192             or croak "Can't decode $ip";
193 148         288 $self->_add_range( $lo, $hi );
194             }
195             }
196              
197              
198             sub remove {
199 1     1 1 468 my $self = shift;
200              
201 1         5 $self->invert;
202 1         12 $self->add( @_ );
203 1         2 $self->invert;
204             }
205              
206              
207             sub merge {
208 9     9 1 11 my $self = shift;
209 9         22 $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         32 for my $other ( @_ ) {
214 11         20 my $iter = $other->_iterate_runs;
215 11         18 while ( my ( $from, $to ) = $iter->() ) {
216 16         25 $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   69 my $self = shift;
239              
240 62         74 my $pos = 0;
241 62         73 my $limit = scalar( @{ $self->{ranges} } );
  62         93  
242              
243             return sub {
244 147 100   147   385 return if $pos >= $limit;
245 85         127 my @r = @{ $self->{ranges} }[ $pos, $pos + 1 ];
  85         200  
246 85         103 $pos += 2;
247 85         202 return @r;
248 62         298 };
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 24 my $new = shift->copy;
258             # TODO: What if it's empty?
259 10         42 $new->invert;
260 10         33 return $new;
261             }
262              
263              
264             sub union {
265 6     6 1 24 my $new = shift->copy;
266 6         22 $new->merge( @_ );
267 6         15 return $new;
268             }
269              
270              
271             sub intersection {
272 3     3 1 10 my $self = shift;
273 3         7 my $class = ref $self;
274 3         10 my $new = $class->new;
275 3         10 $new->merge( map { $_->complement } $self, @_ );
  6         13  
276 3         17 $new->invert;
277 3         7 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 19 my $self = shift;
297 16         20 return @{ $self->{ranges} } == 0;
  16         46  
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 138 return unless @_;
316              
317             # Array of array refs
318 36         56 my @edges = map { $_->{ranges} } @_;
  72         126  
319 36         61 my $medge = scalar( @edges ) - 1;
320              
321 36         53 POS: for ( my $pos = 0;; $pos++ ) {
322 114         152 my $v = $edges[0]->[$pos];
323 114 100       154 if ( defined( $v ) ) {
324 78         128 for ( @edges[ 1 .. $medge ] ) {
325 78         102 my $vv = $_->[$pos];
326 78 50 33     246 return unless defined( $vv ) && $vv eq $v;
327             }
328             }
329             else {
330 36         49 for ( @edges[ 1 .. $medge ] ) {
331 36 50       64 return if defined $_->[$pos];
332             }
333             }
334              
335 114 100       203 last POS unless defined( $v );
336             }
337              
338 36         125 return 1;
339             }
340              
341              
342             sub iterate_addresses {
343 1     1 1 2 my ( $self, @args ) = @_;
344 1         3 my $iter = $self->_iterate_runs;
345 1         2 my @r = ();
346             return sub {
347 2     2   2 while ( 1 ) {
348 3 100 100     7 @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         2 @r = ();
353             }
354 1         6 };
355             }
356              
357              
358             sub iterate_cidr {
359 8     8 1 15 my ( $self, @args ) = @_;
360 8         18 my $iter = $self->_iterate_runs;
361 8         15 my $size = $self->_nbits;
362 8         27 my @r = ();
363             return sub {
364 39     39   33 while ( 1 ) {
365 55 100 100     84 @r = $iter->() or return unless @r;
366 47 100       63 unless ( $r[0] eq $r[1] ) {
367 31         367 ( my $bits = unpack 'B*', $r[0] ) =~ /(0*)$/;
368 31         42 my $pad = length $1;
369 31 100       41 $pad = $size if $pad > $size;
370 31         28 while ( 1 ) {
371 205         429 my $next = _inc( $r[0] | pack 'B*',
372             ( '0' x ( length( $bits ) - $pad ) ) . ( '1' x $pad ) );
373 205 100       305 return $self->_decode( ( my $last, $r[0] ) = ( $r[0], $next ),
374             @args )
375             if $next le $r[1];
376 174         148 $pad--;
377             }
378             }
379 16         20 @r = ();
380             }
381 8         44 };
382             }
383              
384              
385             sub iterate_ranges {
386 42     42 1 68 my ( $self, @args ) = @_;
387 42         86 my $iter = $self->_iterate_runs;
388             return sub {
389 94 100   94   158 return unless my @r = $iter->();
390 52         104 return $self->_decode( @r, @args );
391 42         146 };
392             }
393              
394              
395             sub as_array {
396 51     51 1 104 my ( $self, $iter ) = @_;
397 51         65 my @addr = ();
398 51         76 while ( my $addr = $iter->() ) {
399 84         172 push @addr, $addr;
400             }
401 51         352 return @addr;
402             }
403              
404              
405             sub as_address_array {
406 1     1 1 1 my $self = shift;
407 1         4 return $self->as_array( $self->iterate_addresses( @_ ) );
408             }
409              
410              
411             sub as_cidr_array {
412 8     8 1 272 my $self = shift;
413 8         19 return $self->as_array( $self->iterate_cidr( @_ ) );
414             }
415              
416              
417             sub as_range_array {
418 42     42 1 63 my $self = shift;
419 42         95 return $self->as_array( $self->iterate_ranges( @_ ) );
420             }
421              
422              
423 36     36 1 407 sub as_string { join ', ', shift->as_range_array( @_ ) }
424              
425             1;
426              
427             __END__