File Coverage

lib/Net/BART.pm
Criterion Covered Total %
statement 248 308 80.5
branch 92 146 63.0
condition 17 30 56.6
subroutine 29 31 93.5
pod 0 10 0.0
total 386 525 73.5


line stmt bran cond sub pod time code
1             package Net::BART;
2              
3 1     1   118635 use strict;
  1         2  
  1         43  
4 1     1   6 use warnings;
  1         2  
  1         66  
5 1     1   34 use Carp qw(croak);
  1         3  
  1         53  
6 1     1   324 use Net::BART::BitSet256;
  1         2  
  1         43  
7 1     1   326 use Net::BART::SparseArray256;
  1         2  
  1         51  
8 1     1   437 use Net::BART::Art qw(pfx_to_idx octet_to_idx idx_to_pfx prefix_decompose);
  1         3  
  1         116  
9 1     1   522 use Net::BART::LPM qw(@LOOKUP_TBL);
  1         4  
  1         201  
10 1     1   669 use Net::BART::Node;
  1         3  
  1         4600  
11              
12             our $VERSION = '0.01';
13              
14             # --- Fast IPv4 parsing (no regex on hot path) ---
15              
16             sub _parse_prefix {
17 284     284   512 my ($str) = @_;
18 284         524 my $slash = index($str, '/');
19 284         489 my ($addr_str, $prefix_len);
20 284 50       547 if ($slash >= 0) {
21 284         491 $addr_str = substr($str, 0, $slash);
22 284         596 $prefix_len = substr($str, $slash + 1) + 0;
23             } else {
24 0         0 $addr_str = $str;
25             }
26              
27 284         546 my $is_ipv6 = (index($addr_str, ':') >= 0);
28 284 100       754 my $bytes = $is_ipv6 ? _parse_ipv6($addr_str) : _parse_ipv4_fast($addr_str);
29              
30 284 50       636 if (!defined $prefix_len) {
31 0 0       0 $prefix_len = $is_ipv6 ? 128 : 32;
32             }
33              
34 284         740 _mask_prefix($bytes, $prefix_len);
35 284         755 return ($bytes, $prefix_len, $is_ipv6);
36             }
37              
38             sub _parse_ip {
39 0     0   0 my ($str) = @_;
40 0 0       0 if (index($str, ':') >= 0) {
41 0         0 return (_parse_ipv6($str), 1);
42             }
43 0         0 return (_parse_ipv4_fast($str), 0);
44             }
45              
46             # Fast IPv4 parser - no regex, no validation overhead on hot path
47             sub _parse_ipv4_fast {
48 281     281   484 my ($str) = @_;
49 281         409 my $d1 = index($str, '.');
50 281         558 my $d2 = index($str, '.', $d1 + 1);
51 281         522 my $d3 = index($str, '.', $d2 + 1);
52             return [
53 281         1058 substr($str, 0, $d1) + 0,
54             substr($str, $d1 + 1, $d2 - $d1 - 1) + 0,
55             substr($str, $d2 + 1, $d3 - $d2 - 1) + 0,
56             substr($str, $d3 + 1) + 0,
57             ];
58             }
59              
60             sub _parse_ipv6 {
61 7     7   15 my ($str) = @_;
62 7         10 my @halves;
63 7 50       17 if (index($str, '::') >= 0) {
64 7         30 my ($left, $right) = split /::/, $str, 2;
65 7 50       26 my @left_groups = $left ? (split /:/, $left) : ();
66 7 100       20 my @right_groups = $right ? (split /:/, $right) : ();
67 7         13 my $fill = 8 - @left_groups - @right_groups;
68 7 50       20 croak "Invalid IPv6 address: $str" if $fill < 0;
69 7         36 @halves = (@left_groups, (('0') x $fill), @right_groups);
70             } else {
71 0         0 @halves = split /:/, $str;
72             }
73 7 50       17 croak "Invalid IPv6 address: $str" unless @halves == 8;
74 7         9 my @bytes;
75 7         16 for my $h (@halves) {
76 56         82 my $val = hex($h);
77 56         118 push @bytes, ($val >> 8) & 0xFF, $val & 0xFF;
78             }
79 7         21 return \@bytes;
80             }
81              
82             sub _mask_prefix {
83 284     284   444 my ($bytes, $prefix_len) = @_;
84 284         414 my $full_bytes = $prefix_len >> 3;
85 284         451 my $remaining = $prefix_len & 7;
86 284         410 my $total = scalar @$bytes;
87 284 100 66     693 if ($remaining && $full_bytes < $total) {
88 2         7 $bytes->[$full_bytes] &= (0xFF << (8 - $remaining)) & 0xFF;
89 2         3 $full_bytes++;
90             }
91 284         635 for my $i ($full_bytes .. $total - 1) {
92 351         740 $bytes->[$i] = 0;
93             }
94             }
95              
96             sub _format_ip {
97 2     2   6 my ($bytes, $is_ipv6) = @_;
98 2 50       5 if ($is_ipv6) {
99 0         0 my @groups;
100 0         0 for (my $i = 0; $i < 16; $i += 2) {
101 0         0 push @groups, sprintf("%x", ($bytes->[$i] << 8) | $bytes->[$i + 1]);
102             }
103 0         0 return join(':', @groups);
104             }
105 2         19 return join('.', @$bytes);
106             }
107              
108             # --- Table ---
109              
110             sub new {
111 13     13 0 212610 return bless {
112             root4 => Net::BART::Node::Bart->new,
113             root6 => Net::BART::Node::Bart->new,
114             size4 => 0,
115             size6 => 0,
116             }, $_[0];
117             }
118              
119             sub insert {
120 278     278 0 2752 my ($self, $prefix_str, $value) = @_;
121 278         656 my ($addr, $prefix_len, $is_ipv6) = _parse_prefix($prefix_str);
122 278 100       680 my $root = $is_ipv6 ? $self->{root6} : $self->{root4};
123 278         531 my $is_new = _do_insert($root, $addr, $prefix_len, 0, $value);
124 278 100       655 if ($is_new) {
125 277 100       473 if ($is_ipv6) { $self->{size6}++ } else { $self->{size4}++ }
  3         7  
  274         571  
126             }
127 278         910 return $is_new;
128             }
129              
130             # Non-method for speed (avoids $self-> dispatch overhead in recursion)
131             sub _do_insert {
132 804     804   1594 my ($node, $addr, $prefix_len, $depth, $value) = @_;
133              
134 804         1114 my $strides = $prefix_len >> 3;
135 804         1049 my $lastbits = $prefix_len & 7;
136              
137 804 100       1596 if ($prefix_len == 0) {
138 1         5 return $node->insert_prefix(1, $value);
139             }
140              
141 803 100 100     1715 if ($lastbits && $depth == $strides) {
142 2         10 return $node->insert_prefix(pfx_to_idx($addr->[$depth], $lastbits), $value);
143             }
144              
145 801 100 100     2809 if (!$lastbits && $depth == $strides - 1) {
146 271         598 return _do_insert_fringe($node, $addr, $prefix_len, $depth, $value);
147             }
148              
149             # Navigate
150 530         807 my $octet = $addr->[$depth];
151 530         1286 my ($child, $exists) = $node->get_child($octet);
152              
153 530 100       1168 if (!$exists) {
154 10         52 $node->set_child($octet, Net::BART::Node::Leaf->new(
155             addr => [@$addr], prefix_len => $prefix_len, value => $value,
156             ));
157 10         32 return 1;
158             }
159              
160 520         843 my $ref = ref($child);
161              
162 520 100       1055 if ($ref eq 'Net::BART::Node::Leaf') {
163 6 50       19 if ($child->matches_prefix($addr, $prefix_len)) {
164 0         0 $child->[2] = $value; # LEAF_VALUE
165 0         0 return 0;
166             }
167 6         18 my $new_node = Net::BART::Node::Bart->new;
168 6         51 _do_insert($new_node, $child->[0], $child->[1], $depth + 1, $child->[2]);
169 6         19 $node->set_child($octet, $new_node);
170 6         17 return _do_insert($new_node, $addr, $prefix_len, $depth + 1, $value);
171             }
172              
173 514 100       1033 if ($ref eq 'Net::BART::Node::Fringe') {
174 5 50 33     21 if (!$lastbits && $depth == $strides - 1) {
175 0         0 $child->[0] = $value;
176 0         0 return 0;
177             }
178 5         17 my $new_node = Net::BART::Node::Bart->new;
179 5         17 $new_node->insert_prefix(1, $child->[0]);
180 5         14 $node->set_child($octet, $new_node);
181 5         53 return _do_insert($new_node, $addr, $prefix_len, $depth + 1, $value);
182             }
183              
184 509         1068 return _do_insert($child, $addr, $prefix_len, $depth + 1, $value);
185             }
186              
187             sub _do_insert_fringe {
188 271     271   507 my ($node, $addr, $prefix_len, $depth, $value) = @_;
189 271         420 my $octet = $addr->[$depth];
190 271         579 my ($child, $exists) = $node->get_child($octet);
191              
192 271 100       671 if (!$exists) {
193 270         820 $node->set_child($octet, Net::BART::Node::Fringe->new(value => $value));
194 270         969 return 1;
195             }
196              
197 1         4 my $ref = ref($child);
198              
199 1 50       5 if ($ref eq 'Net::BART::Node::Fringe') {
200 1         2 $child->[0] = $value;
201 1         4 return 0;
202             }
203              
204 0 0       0 if ($ref eq 'Net::BART::Node::Bart') {
205 0         0 return $child->insert_prefix(1, $value);
206             }
207              
208 0 0       0 if ($ref eq 'Net::BART::Node::Leaf') {
209 0         0 my $new_node = Net::BART::Node::Bart->new;
210 0         0 _do_insert($new_node, $child->[0], $child->[1], $depth + 1, $child->[2]);
211 0         0 $new_node->insert_prefix(1, $value);
212 0         0 $node->set_child($octet, $new_node);
213 0         0 return 1;
214             }
215              
216 0         0 return 0;
217             }
218              
219             sub delete {
220 2     2 0 1254 my ($self, $prefix_str) = @_;
221 2         8 my ($addr, $prefix_len, $is_ipv6) = _parse_prefix($prefix_str);
222 2 50       7 my $root = $is_ipv6 ? $self->{root6} : $self->{root4};
223 2         6 my ($val, $ok) = _do_delete($root, $addr, $prefix_len, 0);
224 2 100       5 if ($ok) {
225 1 50       3 if ($is_ipv6) { $self->{size6}-- } else { $self->{size4}-- }
  0         0  
  1         3  
226             }
227 2         10 return ($val, $ok);
228             }
229              
230             sub _do_delete {
231 4     4   9 my ($node, $addr, $prefix_len, $depth) = @_;
232              
233 4         7 my $strides = $prefix_len >> 3;
234 4         6 my $lastbits = $prefix_len & 7;
235              
236 4 50       11 if ($prefix_len == 0) {
237 0         0 return $node->delete_prefix(1);
238             }
239              
240 4 50 33     10 if ($lastbits && $depth == $strides) {
241 0         0 return $node->delete_prefix(pfx_to_idx($addr->[$depth], $lastbits));
242             }
243              
244 4 100 66     19 if (!$lastbits && $depth == $strides - 1) {
245 2         3 my $octet = $addr->[$depth];
246 2         6 my ($child, $exists) = $node->get_child($octet);
247 2 100       7 return (undef, 0) unless $exists;
248              
249 1         3 my $ref = ref($child);
250 1 50       3 if ($ref eq 'Net::BART::Node::Fringe') {
251 1         4 $node->delete_child($octet);
252 1         5 return ($child->[0], 1);
253             }
254 0 0       0 if ($ref eq 'Net::BART::Node::Bart') {
255 0         0 my ($val, $ok) = $child->delete_prefix(1);
256 0 0 0     0 if ($ok && $child->is_empty) {
257 0         0 $node->delete_child($octet);
258             }
259 0         0 return ($val, $ok);
260             }
261 0         0 return (undef, 0);
262             }
263              
264 2         5 my $octet = $addr->[$depth];
265 2         8 my ($child, $exists) = $node->get_child($octet);
266 2 50       8 return (undef, 0) unless $exists;
267              
268 2         5 my $ref = ref($child);
269 2 50       5 if ($ref eq 'Net::BART::Node::Leaf') {
270 0 0       0 if ($child->matches_prefix($addr, $prefix_len)) {
271 0         0 $node->delete_child($octet);
272 0         0 return ($child->[2], 1);
273             }
274 0         0 return (undef, 0);
275             }
276              
277 2 50       6 if ($ref eq 'Net::BART::Node::Fringe') {
278 0         0 return (undef, 0);
279             }
280              
281 2         8 my ($val, $ok) = _do_delete($child, $addr, $prefix_len, $depth + 1);
282 2 50 66     11 if ($ok && $child->is_empty) {
283 0         0 $node->delete_child($octet);
284             }
285 2         7 return ($val, $ok);
286             }
287              
288             # Lookup: longest matching prefix for an IP address.
289             # Heavily optimized - this is the primary hot path.
290             sub lookup {
291 22     22 0 14659 my ($self, $ip_str) = @_;
292              
293             # Inline fast IPv4 parse
294 22         38 my ($bytes, $is_ipv6);
295 22 100       66 if (index($ip_str, ':') >= 0) {
296 4         12 $bytes = _parse_ipv6($ip_str);
297 4         9 $is_ipv6 = 1;
298             } else {
299 18         32 my $d1 = index($ip_str, '.');
300 18         35 my $d2 = index($ip_str, '.', $d1 + 1);
301 18         26 my $d3 = index($ip_str, '.', $d2 + 1);
302 18         98 $bytes = [
303             substr($ip_str, 0, $d1) + 0,
304             substr($ip_str, $d1 + 1, $d2 - $d1 - 1) + 0,
305             substr($ip_str, $d2 + 1, $d3 - $d2 - 1) + 0,
306             substr($ip_str, $d3 + 1) + 0,
307             ];
308 18         32 $is_ipv6 = 0;
309             }
310              
311 22 100       56 my $root = $is_ipv6 ? $self->{root6} : $self->{root4};
312 22 100       44 my $max_depth = $is_ipv6 ? 16 : 4;
313              
314             # Walk down, storing nodes/octets for backtrack LPM.
315             # Use flat arrays instead of array-of-arrays for speed.
316 22         49 my (@nodes, @octets);
317 22         33 my $node = $root;
318 22         59 my $sp = 0;
319              
320 22         67 for my $depth (0 .. $max_depth - 1) {
321 48         69 my $octet = $bytes->[$depth];
322 48         81 $nodes[$sp] = $node;
323 48         87 $octets[$sp] = $octet;
324 48         61 $sp++;
325              
326             # Inline get_child: $node->[1] is children sparse array
327 48         66 my $chd = $node->[1];
328 48         67 my $chd_bs = $chd->[0];
329 48 100       171 unless ($chd_bs->[$octet >> 6] & (1 << ($octet & 63))) {
330 10         23 last; # no child
331             }
332 38         118 my $child = $chd->[1][$chd_bs->rank($octet) - 1];
333              
334 38         66 my $ref = ref($child);
335 38 100       92 if ($ref eq 'Net::BART::Node::Fringe') {
336 7         56 return ($child->[0], 1);
337             }
338 31 100       107 if ($ref eq 'Net::BART::Node::Leaf') {
339 5 100       20 if ($child->contains_ip($bytes)) {
340 3         21 return ($child->[2], 1);
341             }
342 2         7 last;
343             }
344 26         49 $node = $child;
345             }
346              
347             # Backtrack: LPM at each stacked node
348 12         34 for (my $i = $sp - 1; $i >= 0; $i--) {
349 16         56 my ($val, $ok) = $nodes[$i]->lpm($octets[$i]);
350 16 100       82 return ($val, 1) if $ok;
351             }
352              
353 4         22 return (undef, 0);
354             }
355              
356             # Contains: check if any prefix contains the IP.
357             sub contains {
358 3     3 0 13 my ($self, $ip_str) = @_;
359              
360 3         6 my ($bytes, $is_ipv6);
361 3 50       9 if (index($ip_str, ':') >= 0) {
362 0         0 $bytes = _parse_ipv6($ip_str);
363 0         0 $is_ipv6 = 1;
364             } else {
365 3         5 my $d1 = index($ip_str, '.');
366 3         8 my $d2 = index($ip_str, '.', $d1 + 1);
367 3         5 my $d3 = index($ip_str, '.', $d2 + 1);
368 3         19 $bytes = [
369             substr($ip_str, 0, $d1) + 0,
370             substr($ip_str, $d1 + 1, $d2 - $d1 - 1) + 0,
371             substr($ip_str, $d2 + 1, $d3 - $d2 - 1) + 0,
372             substr($ip_str, $d3 + 1) + 0,
373             ];
374 3         7 $is_ipv6 = 0;
375             }
376              
377 3 50       10 my $node = $is_ipv6 ? $self->{root6} : $self->{root4};
378 3 50       8 my $max_depth = $is_ipv6 ? 16 : 4;
379              
380 3         7 for my $depth (0 .. $max_depth - 1) {
381 3         6 my $octet = $bytes->[$depth];
382              
383             # Inline lpm_test
384 3         6 my $pfx_bs = $node->[0][0];
385 3         8 my $lut = $LOOKUP_TBL[($octet >> 1) + 128];
386 3 50       13 if (($pfx_bs->[0] & $lut->[0]) | ($pfx_bs->[1] & $lut->[1]) |
387             ($pfx_bs->[2] & $lut->[2]) | ($pfx_bs->[3] & $lut->[3])) {
388 0         0 return 1;
389             }
390              
391             # Inline get_child
392 3         7 my $chd = $node->[1];
393 3         4 my $chd_bs = $chd->[0];
394 3 100       15 unless ($chd_bs->[$octet >> 6] & (1 << ($octet & 63))) {
395 1         7 return 0;
396             }
397 2         9 my $child = $chd->[1][$chd_bs->rank($octet) - 1];
398              
399 2         47 my $ref = ref($child);
400 2 50       8 if ($ref eq 'Net::BART::Node::Fringe') { return 1 }
  2         15  
401 0 0       0 if ($ref eq 'Net::BART::Node::Leaf') {
402 0 0       0 return $child->contains_ip($bytes) ? 1 : 0;
403             }
404 0         0 $node = $child;
405             }
406 0         0 return 0;
407             }
408              
409             # Exact match get.
410             sub get {
411 4     4 0 2624 my ($self, $prefix_str) = @_;
412 4         15 my ($addr, $prefix_len, $is_ipv6) = _parse_prefix($prefix_str);
413 4 50       31 my $root = $is_ipv6 ? $self->{root6} : $self->{root4};
414 4         11 return _do_get($root, $addr, $prefix_len, 0);
415             }
416              
417             sub _do_get {
418 6     6   14 my ($node, $addr, $prefix_len, $depth) = @_;
419              
420 6         10 my $strides = $prefix_len >> 3;
421 6         9 my $lastbits = $prefix_len & 7;
422              
423 6 50       17 if ($prefix_len == 0) {
424 0         0 return $node->get_prefix(1);
425             }
426              
427 6 50 33     16 if ($lastbits && $depth == $strides) {
428 0         0 return $node->get_prefix(pfx_to_idx($addr->[$depth], $lastbits));
429             }
430              
431 6 100 66     29 if (!$lastbits && $depth == $strides - 1) {
432 4         14 my ($child, $exists) = $node->get_child($addr->[$depth]);
433 4 100       16 return (undef, 0) unless $exists;
434 3         7 my $ref = ref($child);
435 3 100       9 if ($ref eq 'Net::BART::Node::Fringe') { return ($child->[0], 1) }
  2         12  
436 1 50       3 if ($ref eq 'Net::BART::Node::Bart') { return $child->get_prefix(1) }
  1         5  
437 0         0 return (undef, 0);
438             }
439              
440 2         10 my ($child, $exists) = $node->get_child($addr->[$depth]);
441 2 50       7 return (undef, 0) unless $exists;
442 2         4 my $ref = ref($child);
443 2 50       7 if ($ref eq 'Net::BART::Node::Leaf') {
444 0 0       0 return $child->matches_prefix($addr, $prefix_len) ? ($child->[2], 1) : (undef, 0);
445             }
446 2 50       5 return (undef, 0) if $ref eq 'Net::BART::Node::Fringe';
447 2         9 return _do_get($child, $addr, $prefix_len, $depth + 1);
448             }
449              
450 6     6 0 1323 sub size { return $_[0]->{size4} + $_[0]->{size6} }
451 1     1 0 6 sub size4 { return $_[0]->{size4} }
452 2     2 0 441 sub size6 { return $_[0]->{size6} }
453              
454             # Walk all prefixes.
455             sub walk {
456 1     1 0 13 my ($self, $callback) = @_;
457 1         5 _walk_node($self->{root4}, [], 0, 0, $callback);
458 1         14 _walk_node($self->{root6}, [], 1, 0, $callback);
459             }
460              
461             sub _walk_node {
462 2     2   5 my ($node, $path, $is_ipv6, $depth, $callback) = @_;
463 2 100       6 my $total_bytes = $is_ipv6 ? 16 : 4;
464              
465             # Visit prefixes at this node
466             $node->[0]->each_pair(sub {
467 0     0   0 my ($idx, $val) = @_;
468 0         0 my ($octet, $pfx_len_in_stride) = idx_to_pfx($idx);
469 0         0 my $total_bits = $depth * 8 + $pfx_len_in_stride;
470 0         0 my @addr = @$path;
471 0 0       0 push @addr, $octet if $pfx_len_in_stride > 0;
472 0         0 while (@addr < $total_bytes) { push @addr, 0 }
  0         0  
473 0         0 _mask_prefix(\@addr, $total_bits);
474 0         0 $callback->(_format_ip(\@addr, $is_ipv6) . "/$total_bits", $val);
475 2         15 });
476              
477             # Visit children
478             $node->[1]->each_pair(sub {
479 2     2   5 my ($octet, $child) = @_;
480 2         5 my @child_path = (@$path, $octet);
481 2         5 my $ref = ref($child);
482              
483 2 100       8 if ($ref eq 'Net::BART::Node::Leaf') {
    50          
484 1         3 $callback->(_format_ip($child->[0], $is_ipv6) . "/$child->[1]", $child->[2]);
485             } elsif ($ref eq 'Net::BART::Node::Fringe') {
486 1         2 my $total_bits = ($depth + 1) * 8;
487 1         29 my @addr = @child_path;
488 1         4 while (@addr < $total_bytes) { push @addr, 0 }
  3         22  
489 1         6 $callback->(_format_ip(\@addr, $is_ipv6) . "/$total_bits", $child->[0]);
490             } else {
491 0           _walk_node($child, \@child_path, $is_ipv6, $depth + 1, $callback);
492             }
493 2         23 });
494             }
495              
496             1;
497              
498             __END__