File Coverage

blib/lib/Net/DNS/RR/OPT.pm
Criterion Covered Total %
statement 306 306 100.0
branch 114 114 100.0
path n/a
condition 32 32 100.0
subroutine 56 56 100.0
pod 11 14 100.0
total 519 522 100.0


line stmt bran path cond sub pod time code
1               package Net::DNS::RR::OPT;
2                
3 12       12   80 use strict;
  12           45  
  12           542  
4 12       12   53 use warnings;
  12           25  
  12           1057  
5               our $VERSION = (qw$Id: OPT.pm 2005 2025-01-28 13:22:10Z willem $)[2];
6                
7 12       12   136 use base qw(Net::DNS::RR);
  12           15  
  12           1011  
8                
9                
10               =head1 NAME
11                
12               Net::DNS::RR::OPT - DNS OPT resource record
13                
14               =cut
15                
16 12       12   79 use integer;
  12           28  
  12           106  
17                
18 12       12   339 use Carp;
  12           17  
  12           994  
19 12       12   62 use Net::DNS::Parameters qw(:rcode :ednsoption);
  12           18  
  12           2064  
20                
21 12       12   82 use constant UTIL => scalar eval { require Scalar::Util; Scalar::Util->can('isdual') };
  12           18  
  12           18  
  12           37  
  12           1076  
22                
23 12       12   88 use constant OPT => Net::DNS::Parameters::typebyname qw(OPT);
  12           37  
  12           43  
24                
25               require Net::DNS::DomainName;
26               require Net::DNS::RR::A;
27               require Net::DNS::RR::AAAA;
28               require Net::DNS::Text;
29                
30                
31               sub _decode_rdata { ## decode rdata from wire-format octet string
32 57       57   134 my ( $self, $data, $offset ) = @_;
33                
34 57           123 my $class = delete $self->{class}; # OPT redefines CLASS and TTL fields
35 57 100         317 $self->udpsize($class) if defined $class;
36                
37 57           96 my $ttl = delete $self->{ttl};
38 57 100         303 $self->_ttl($ttl) if defined $ttl;
39                
40 57           130 my $limit = $offset + $self->{rdlength} - 4;
41 57           155 while ( $offset <= $limit ) {
42 35           54 my ( $code, $length ) = unpack "\@$offset nn", $$data;
43 35           50 my $value = unpack "\@$offset x4 a$length", $$data;
44 35 100         64 my @value = map { ref($_) ? @$_ : defined($_) ? $_ : () } $self->{option}{$code}, $value;
  70 100         105  
45 35 100         73 $self->{option}{$code} = ( scalar(@value) == 1 ) ? $value : \@value;
46 35           57 $offset += $length + 4;
47               }
48 57           124 return;
49               }
50                
51                
52               sub _encode_rdata { ## encode rdata as wire-format octet string
53 64       64   126 my $self = shift;
54                
55 64     100     271 my $option = $self->{option} || {};
56 64           208 my @option = $self->options;
57 64           172 foreach my $item (@option) {
58 30 100         39 my @value = map { ref($_) ? @$_ : $_ } $option->{$item};
  30           46  
59 30           32 $item = join '', map { pack( 'nna*', $item, length($_), $_ ) } @value;
  32           77  
60               }
61 64           199 return join '', @option;
62               }
63                
64                
65               sub encode { ## override RR method
66 63       63 1 124 my $self = shift;
67 63           257 my $data = $self->_encode_rdata;
68 63           189 return pack 'C n n N na*', 0, OPT, $self->udpsize, $self->_ttl, length($data), $data;
69               }
70                
71                
72               sub string { ## override RR method
73 6       6 1 1105 my @line = split /[\r\n]+/, shift->json;
74 6           13 return join '', map {";;$_\n"} @line;
  64           139  
75               }
76                
77               sub class { ## override RR method
78 2       2 1 990 my ( $self, @value ) = @_;
79 2           11 $self->_deprecate(qq[please use "UDPsize()"]);
80 2           6 return $self->udpsize(@value);
81               }
82                
83               sub ttl { ## override RR method
84 2       2 1 828 my ( $self, @value ) = @_;
85 2           6 $self->_deprecate(qq[please use "flags()", "rcode()" or "version()"]);
86 2           5 return $self->_ttl(@value);
87               }
88                
89               sub _ttl {
90 121       121   213 my ( $self, @value ) = @_;
91 121           266 for (@value) {
92 58           302 @{$self}{qw(rcode version flags)} = unpack 'C2n', pack( 'N', $_ );
  58           242  
93 58           174 $self->{rcode} = $self->{rcode} << 4;
94 58           130 return;
95               }
96 63           186 return unpack 'N', pack( 'C2n', $self->rcode >> 4, $self->version, $self->flags );
97               }
98                
99               sub generic { ## override RR method
100 1       1 1 1 my $self = shift;
101 1           3 local $self->{class} = $self->udpsize;
102 1           2 my @xttl = ( $self->rcode >> 4, $self->version, $self->flags );
103 1           3 local $self->{ttl} = unpack 'N', pack( 'C2n', @xttl );
104 1           5 return $self->SUPER::generic;
105               }
106                
107               sub token { ## override RR method
108 1       1 1 2 return grep { !m/^[()]$/ } split /\s+/, &generic;
  6           25  
109               }
110                
111               sub json {
112 6       6 0 7 my $self = shift; # uncoverable pod
113                
114 6           11 my $version = $self->version;
115 6 100         19 unless ( $version == 0 ) {
116 1           3 my $content = unpack 'H*', $self->encode;
117 1           8 return <<"QQ";
118               { "EDNS-VERSION": $version,
119               "BASE16": "$content"
120               }
121               QQ
122               }
123                
124 5           9 my $flags = $self->flags;
125 5           9 my $rcode = $self->rcode;
126 5           9 my $size = $self->udpsize;
127 5           14 my @format = map { join( "\n\t\t\t", $self->_format_option($_) ) } $self->options;
  28           40  
128 5 100         12 my @indent = scalar(@format) ? "\n\t\t" : ();
129 5           28 my @option = join ",\n\t\t", @format;
130                
131 5           82 return <<"QQ";
132               { "EDNS-VERSION": $version,
133               "FLAGS": $flags,
134               "RCODE": $rcode,
135               "UDPSIZE": $size,
136               "OPTIONS": [@indent@option ]
137               }
138               QQ
139               }
140                
141                
142               sub version {
143 90       90 1 155 my ( $self, @value ) = @_;
144 90           206 for (@value) { $self->{version} = 0 + $_ }
  3           7  
145 90     100     385 return $self->{version} || 0;
146               }
147                
148                
149               sub udpsize {
150 224       224 0 415 my ( $self, @value ) = @_; # uncoverable pod
151 224 100         491 for (@value) { $self->{udpsize} = ( $_ > 512 ) ? $_ : 0 }
  151           502  
152 224     100     686 return $self->{udpsize} || 0;
153               }
154                
155               sub size {
156 2       2 0 816 my ( $self, @value ) = @_; # uncoverable pod
157 2           10 $self->_deprecate(qq[size() is an alias of "UDPsize()"]);
158 2           5 return $self->udpsize(@value);
159               }
160                
161                
162               sub rcode {
163 506       506 1 869 my ( $self, @value ) = @_;
164 506 100         892 for (@value) { $self->{rcode} = ( $_ < 16 ) ? 0 : $_ } # discard non-EDNS rcodes 1 .. 15
  226           558  
165 506     100     1672 return $self->{rcode} || 0;
166               }
167                
168                
169               sub flags {
170 328       328 1 493 my ( $self, @value ) = @_;
171 328           482 for (@value) { $self->{flags} = 0 + $_ }
  96           226  
172 328     100     1371 return $self->{flags} || 0;
173               }
174                
175                
176               sub options {
177 74       74 1 126 my $self = shift;
178 74     100     231 my $option = $self->{option} || {};
179 74 100         272 @{$self->{index}} = sort { $a <=> $b } keys %$option unless defined $self->{index};
  39           102  
  199           188  
180 74           106 return @{$self->{index}};
  74           234  
181               }
182                
183               sub option {
184 158       158 1 9183 my ( $self, $name, @value ) = @_;
185 158           272 my $number = ednsoptionbyname($name);
186 158 100         298 return $self->_get_option($number) unless scalar @value;
187 83           137 my $value = $self->_set_option( $number, @value );
188 82 100         292 return $@ ? croak( ( split /\sat/i, $@ )[0] ) : $value;
189               }
190                
191                
192               ########################################
193                
194               sub _get_option {
195 103       103   143 my ( $self, $number ) = @_;
196                
197 103     100     172 my $options = $self->{option} || {};
198 103 100         137 my @payload = map { ref($_) ? @$_ : $_ } $options->{$number};
  103           194  
199 103 100         195 return shift @payload unless wantarray;
200 52           69 my $optname = ednsoptionbyval($number);
201 52           103 my $package = join '::', __PACKAGE__, $optname;
202 52           100 $package =~ s/-/_/g;
203 52           310 my $structured = $package->can('_decompose');
204 52           64 foreach my $value (@payload) {
205 56           51 my @value;
206 56 100         69 if ( length $value ) {
207 52 100         74 @value = eval { $package->_decompose($value) } if $structured;
  39           63  
208 52 100         126 @value = {BASE16 => unpack 'H*', $value} unless scalar @value;
209 52 100         71 warn $@ if $@;
210               } else {
211 4 100         12 @value = $structured ? {'OPTION-LENGTH' => 0} : '';
212               }
213 56           130 $value = {$optname => @value};
214               }
215 52           94 return @payload;
216               }
217                
218                
219               sub _set_option {
220 83       83   129 my ( $self, $number, @value ) = @_;
221 83           90 my ($arg) = @value;
222                
223 83     100     150 my $options = $self->{option} || {};
224 83           108 delete $options->{$number};
225 83           107 delete $self->{index};
226 83 100         119 delete $self->{option} unless scalar( keys %$options );
227                
228 83 100         122 return unless defined $arg;
229 82           117 $self->{option} = $options;
230                
231 82 100         116 if ( ref($arg) eq 'HASH' ) {
232 53           71 for ( keys %$arg ) { $$arg{uc $_} = $$arg{$_} } # tolerate mixed case
  67           115  
233 53           68 my $length = $$arg{'OPTION-LENGTH'};
234 53           51 my $octets = $$arg{'OPTION-DATA'};
235 53 100         106 $octets = pack 'H*', $$arg{'BASE16'} if defined $$arg{'BASE16'};
236 53 100   100     79 $octets = '' if defined($length) && $length == 0;
237 53 100         116 return $options->{$number} = $octets if defined $octets;
238               }
239                
240 43           70 my $option = ednsoptionbyval($number);
241 43           87 my $package = join '::', __PACKAGE__, $option;
242 43           87 $package =~ s/-/_/g;
243 43 100   100     382 return eval { $options->{$number} = $package->_compose(@value) } if length($arg) && $package->can('_compose');
  38           75  
244                
245 5 100         111 croak "unable to compose option $number" if ref($arg);
246 4           11 return $options->{$number} = $arg;
247               }
248                
249                
250               sub _specified {
251 217       217   615 my $self = shift;
252 217           398 return scalar grep { $self->{$_} } qw(udpsize flags rcode option);
  868           1742  
253               }
254                
255                
256               sub _format_option {
257 28       28   30 my ( $self, $number ) = @_;
258 28           34 my @option = $self->_get_option($number);
259 28           47 return map { Net::DNS::RR::_wrap( _JSONify($_) ) } @option;
  30           34  
260               }
261                
262                
263               sub _JSONify {
264 127       127   782 my $value = shift;
265 127 100         150 return 'null' unless defined $value;
266                
267 126 100         175 if ( ref($value) eq 'HASH' ) {
268 56           92 my @tags = sort keys %$value;
269 56           63 my $tail = pop @tags;
270 56 100         67 for ( $$value{BASE16} ) { $_ = pack( 'U0a*', $_ ) if defined } # mark as UTF-8
  56           86  
271 56           56 my @body = map { my @x = ( qq("$_":), _JSONify( $$value{$_} ) ); $x[-1] .= ','; @x } @tags;
  13           50  
  13           14  
  13           21  
272 56           108 push @body, ( qq("$tail":), _JSONify( $$value{$tail} ) );
273 56           67 $body[0] = '{' . $body[0];
274 56           53 $body[-1] .= '}';
275 56           126 return @body;
276               }
277                
278 70 100         91 if ( ref($value) eq 'ARRAY' ) {
279 4           6 my @array = @$value;
280 4           5 my @tail = map { _JSONify($_) } grep {defined} pop @array;
  4           6  
  4           8  
281 4           5 my @body = map { my @x = _JSONify($_); $x[-1] .= ','; @x } @array;
  10           11  
  10           9  
  10           15  
282 4           13 return ( '[', @body, @tail, ']' );
283               }
284                
285 66           76 my $string = "$value"; ## stringify, then use isdual() as discriminant
286 66 100         129 return $string if UTIL && Scalar::Util::isdual($value); # native numeric representation
287 36           40 for ($string) {
288 36 100         57 unless ( utf8::is_utf8($value) ) {
289 16 100         53 return $_ if /^-?\d+$/; # integer (string representation)
290 15 100         27 return $_ if /^-?\d+\.\d+$/; # non-integer
291 14 100         35 return $_ if /^-?\d+(\.\d+)?e[+-]\d\d?$/i;
292               }
293 33           43 s/\\/\\\\/g; # escaped escape
294 33           33 s/^"(.*)"$/$1/; # strip enclosing quotes
295 33           42 s/"/\\"/g; # escape interior quotes
296               }
297 33           57 return qq("$string");
298               }
299                
300                
301               ## no critic ProhibitMultiplePackages
302               package Net::DNS::RR::OPT::NSID; # RFC5001
303                
304               sub _compose {
305 2 100     2   3 my ( undef, @argument ) = map { ref($_) ? %$_ : $_ } @_;
  4           11  
306 2           10 return pack 'H*', pop @argument;
307               }
308                
309 2       2   10 sub _decompose { return pack 'U0a*', unpack 'H*', pop @_ } # mark as UTF-8
310                
311                
312               package Net::DNS::RR::OPT::DAU; # RFC6975
313                
314               sub _compose {
315 6 100     6   8 my ( undef, @argument ) = map { ref($_) ? @$_ : $_ } @_;
  19           31  
316 6           28 return pack 'C*', @argument;
317               }
318                
319 6       6   18 sub _decompose { return [unpack 'C*', pop @_] }
320                
321                
322               package Net::DNS::RR::OPT::DHU; # RFC6975
323               our @ISA = qw(Net::DNS::RR::OPT::DAU);
324                
325               package Net::DNS::RR::OPT::N3U; # RFC6975
326               our @ISA = qw(Net::DNS::RR::OPT::DAU);
327                
328                
329               package Net::DNS::RR::OPT::CLIENT_SUBNET; # RFC7871
330                
331               my %family = qw(1 Net::DNS::RR::A 2 Net::DNS::RR::AAAA);
332               my @field8 = qw(FAMILY SOURCE-PREFIX SCOPE-PREFIX ADDRESS);
333                
334               sub _compose {
335 4       4   4 shift @_;
336 4 100         15 my %argument = ( map( ( $_ => 0 ), @field8 ), map { ref($_) ? %$_ : $_ } @_ );
  9           25  
337 4     100     20 my $family = $family{$argument{FAMILY}} || die 'unrecognised address family';
338 3           6 my $bitmask = $argument{'SOURCE-PREFIX'};
339 3           12 my $address = bless( {}, $family )->address( $argument{ADDRESS} );
340 3           29 return pack 'a* B*', pack( 'nC2', @argument{@field8} ), unpack "B$bitmask", $address;
341               }
342                
343               sub _decompose {
344 4       4   6 my %object;
345 4           20 @object{@field8} = unpack 'nC2a*', pop @_;
346 4     100     20 my $family = $family{$object{FAMILY}} || die 'unrecognised address family';
347 3           5 for ( $object{ADDRESS} ) {
348 3           18 $_ = bless( {address => $_}, $family )->address;
349 3           32 s/:[:0]+$/::/;
350               }
351 3           6 return \%object;
352               }
353                
354                
355               package Net::DNS::RR::OPT::EXPIRE; # RFC7314
356                
357               sub _compose {
358 2 100     2   5 my ( undef, @argument ) = map { ref($_) ? %$_ : $_ } @_;
  4           10  
359 2           11 return pack 'N', pop @argument;
360               }
361                
362               sub _decompose {
363 2       2   6 my $argument = pop @_;
364 2           7 return {'EXPIRE-TIMER' => unpack 'N', $argument};
365               }
366                
367                
368               package Net::DNS::RR::OPT::COOKIE; # RFC7873
369                
370               my @field10 = qw(CLIENT SERVER);
371                
372               sub _compose {
373 3       3   6 my ( undef, @argument ) = @_;
374 3           6 for ( ref( $argument[0] ) ) {
375 3 100         8 /HASH/ && ( @argument = @{$argument[0]}{@field10} );
  1           3  
376 3 100         7 /ARRAY/ && ( @argument = @{$argument[0]} );
  1           3  
377               }
378 3     100     7 return pack 'a8a*', map { pack 'H*', $_ || '' } @argument;
  5           27  
379               }
380                
381               sub _decompose {
382 2       2   4 my %object;
383 2           6 @object{@field10} = map { pack 'U0a*', $_ } unpack 'H16H*', pop @_; # mark as UTF-8
  4           15  
384 2           5 return \%object;
385               }
386                
387                
388               package Net::DNS::RR::OPT::TCP_KEEPALIVE; # RFC7828
389                
390               sub _compose {
391 2 100     2   3 my ( undef, @argument ) = map { ref($_) ? %$_ : $_ } @_;
  4           10  
392 2           11 return pack 'n', pop @argument;
393               }
394                
395               sub _decompose {
396 2       2   3 my $argument = pop @_;
397 2           9 return {'TIMEOUT' => unpack 'n', $argument};
398               }
399                
400                
401               package Net::DNS::RR::OPT::PADDING; # RFC7830
402                
403               sub _compose {
404 5 100     5   12 my ( undef, @argument ) = map { ref($_) ? %$_ : $_ } @_;
  12           33  
405 5     100     17 my $length = pop(@argument) || 0;
406 5           37 return pack "x$length";
407               }
408                
409               sub _decompose {
410 3       3   5 my $argument = pop @_;
411 3 100         18 return {'OPTION-LENGTH' => length $argument} if $argument =~ /^\000*$/;
412 1           4 return {'BASE16' => unpack 'H*', $argument};
413               }
414                
415                
416               package Net::DNS::RR::OPT::CHAIN; # RFC7901
417                
418               sub _compose {
419 1 100     1   2 my ( undef, @argument ) = map { ref($_) ? %$_ : $_ } @_;
  2           6  
420 1           8 return Net::DNS::DomainName->new( pop @argument )->encode;
421               }
422                
423               sub _decompose {
424 2       2   5 my $argument = pop @_;
425 2           10 return {'CLOSEST-TRUST-POINT' => Net::DNS::DomainName->decode( \$argument )->string};
426               }
427                
428                
429               package Net::DNS::RR::OPT::KEY_TAG; # RFC8145
430                
431               sub _compose {
432 2 100     2   4 my ( undef, @argument ) = map { ref($_) ? @$_ : $_ } @_;
  7           14  
433 2           9 return pack 'n*', @argument;
434               }
435                
436 2       2   6 sub _decompose { return [unpack 'n*', pop @_] }
437                
438                
439               package Net::DNS::RR::OPT::EXTENDED_ERROR; # RFC8914
440                
441               sub _compose {
442 6       6   10 my ( undef, @arg ) = @_;
443 6 100         14 my %arg = ref( $arg[0] ) ? %{$arg[0]} : @arg;
  3           8  
444 6     100     25 my $text = join '', Net::DNS::RR::OPT::_JSONify( $arg{'EXTRA-TEXT'} || '' );
445 6           20 return pack 'na*', $arg{'INFO-CODE'}, Net::DNS::Text->new($text)->raw;
446               }
447                
448               sub _decompose {
449 4       4   11 my ( $code, $text ) = unpack 'na*', pop @_;
450 4           8 my $error = $Net::DNS::Parameters::dnserrorbyval{$code};
451 4 100         26 my @error = defined($error) ? ( 'ERROR' => $error ) : ();
452 4           15 my $extra = Net::DNS::Text->decode( \$text, 0, length $text );
453 4           10 for ( $extra->value ) {
454 4 100         48 last unless /^[\[\{]/;
455 2           5 s/([\$\@])/\\$1/g; ## Here be dragons!
456 2           3 my $REGEX = q/("[^"]*"|[\[\]{}:,]|[-0-9.Ee+]+)|\s+|(.)/;
457 2 100         105 my @split = grep { defined && length } split /$REGEX/o;
  67           121  
458 2       1   7 my $value = eval join( ' ', 'no integer;', map { s/^:$/=>/; $_ } @split );
  22       1   21  
  22           173  
  1           6  
  1           2  
  1           4  
  1           6  
  1           1  
  1           5  
459 2 100         41 return {'INFO-CODE' => $code, @error, 'EXTRA-TEXT' => $value} if ref($value);
460               }
461 3           8 return {'INFO-CODE' => $code, @error, 'EXTRA-TEXT' => $extra->value};
462               }
463                
464                
465               package Net::DNS::RR::OPT::REPORT_CHANNEL; # RFC9567
466                
467               sub _compose {
468 2 100     2   5 my ( undef, @argument ) = map { ref($_) ? %$_ : $_ } @_;
  5           11  
469 2           8 return Net::DNS::DomainName->new( pop @argument )->encode;
470               }
471                
472               sub _decompose {
473 2       2   4 my $argument = pop @_;
474 2           6 return {'AGENT-DOMAIN' => Net::DNS::DomainName->decode( \$argument )->string};
475               }
476                
477                
478               package Net::DNS::RR::OPT::ZONEVERSION; # RFC9660
479                
480               my @field19 = qw(LABELCOUNT TYPE VERSION);
481                
482               sub _compose {
483 3       3   7 my ( undef, @argument ) = @_;
484 3           7 for ( ref( $argument[0] ) ) {
485 3 100         9 /HASH/ && ( @argument = @{$argument[0]}{@field19} );
  1           3  
486 3 100         9 /ARRAY/ && ( @argument = @{$argument[0]} );
  2           5  
487               }
488 3 100         15 return scalar(@argument) ? pack( 'C2H*', @argument ) : '';
489               }
490                
491               sub _decompose {
492 8       8   7 my %object;
493 8           18 my ( $l, $t, $v ) = unpack 'C2H*', pop @_;
494 8           31 @object{@field19} = ( $l, $t, pack 'U0a*', $v ); # mark hex data as UTF-8
495 8           11 return \%object;
496               }
497                
498               ########################################
499                
500                
501               1;
502               __END__