File Coverage

blib/lib/Net/CIDR/Set.pm
Criterion Covered Total %
statement 210 228 92.1
branch 51 60 85.0
condition 17 29 58.6
subroutine 43 49 87.7
pod 25 26 96.1
total 346 392 88.2


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