File Coverage

blib/lib/Net/DNS/RR/SVCB.pm
Criterion Covered Total %
statement 213 213 100.0
branch 80 80 100.0
path n/a
condition 11 11 100.0
subroutine 38 38 100.0
pod 6 12 100.0
total 348 354 100.0


line stmt bran path cond sub pod time code
1               package Net::DNS::RR::SVCB;
2                
3 2       2   13 use strict;
  2           4  
  2           59  
4 2       2   6 use warnings;
  2           2  
  2           143  
5               our $VERSION = (qw$Id: SVCB.pm 2043 2026-01-14 13:35:59Z willem $)[2];
6                
7 2       2   9 use base qw(Net::DNS::RR);
  2           4  
  2           187  
8                
9                
10               =head1 NAME
11                
12               Net::DNS::RR::SVCB - DNS SVCB resource record
13                
14               =cut
15                
16 2       2   10 use integer;
  2           3  
  2           13  
17                
18 2       2   50 use Net::DNS::DomainName;
  2           2  
  2           37  
19 2       2   1174 use Net::DNS::RR::A;
  2           6  
  2           66  
20 2       2   1151 use Net::DNS::RR::AAAA;
  2           5  
  2           68  
21 2       2   1196 use Net::DNS::Text;
  2           10  
  2           95  
22 2       2   1075 use MIME::Base64;
  2           1576  
  2           5784  
23                
24                
25               my %keybyname = (
26               mandatory => 'key0', # RFC9460(8)
27               alpn => 'key1', # RFC9460(7.1)
28               'no-default-alpn' => 'key2', # RFC9460(7.1)
29               port => 'key3', # RFC9460(7.2)
30               ipv4hint => 'key4', # RFC9460(7.3)
31               ech => 'key5', # RFC9460
32               ipv6hint => 'key6', # RFC9460(7.3)
33               dohpath => 'key7', # RFC9461
34               ohttp => 'key8', # RFC9540(4)
35               'tls-supported-groups' => 'key9',
36               );
37                
38                
39               sub _decode_rdata { ## decode rdata from wire-format octet string
40 14       14   20 my ( $self, $data, $offset ) = @_;
41                
42 14           24 my $limit = $self->{rdlength};
43 14           39 my $rdata = $self->{rdata} = substr $$data, $offset, $limit;
44 14           33 $self->{SvcPriority} = unpack 'n', $rdata;
45 14           50 ( $self->{TargetName}, $offset ) = Net::DNS::DomainName->decode( \$rdata, 2 );
46                
47 14           36 my $params = $self->{SvcParams} = [];
48 14           31 while ( ( my $start = $offset + 4 ) <= $limit ) {
49 12           26 my ( $key, $size ) = unpack( "\@$offset n2", $rdata );
50 12 100         34 last if ( $offset = $start + $size ) > $limit;
51 10           28 push @$params, ( $key, substr $rdata, $start, $size );
52               }
53 14 100         25 die $self->type . ': corrupt RDATA' unless $offset == $limit;
54 12           23 return;
55               }
56                
57                
58               sub _encode_rdata { ## encode rdata as wire-format octet string
59 8       8   11 my $self = shift;
60                
61 8 100         16 return $self->{rdata} if $self->{rdata};
62 5           14 my @packed = pack 'n a*', $self->{SvcPriority}, $self->{TargetName}->encode;
63 5     100     14 my $params = $self->{SvcParams} || [];
64 5           9 my @params = @$params;
65 5           10 while (@params) {
66 1           3 my $key = shift @params;
67 1           3 my $val = shift @params;
68 1           6 push @packed, pack( 'n2a*', $key, length($val), $val );
69               }
70 5           14 return join '', @packed;
71               }
72                
73                
74               sub _format_rdata { ## format rdata portion of RR string.
75 25       25   34 my $self = shift;
76                
77 25           95 my @rdata = unpack 'H4', pack 'n', $self->{SvcPriority};
78                
79 25           70 my $encode = $self->{TargetName}->encode();
80 25           42 my $length = 2 + length $encode;
81 25           137 my @target = grep {length} split /(\S{32})/, unpack 'H*', $encode;
  57           93  
82 25           67 my $target = substr $self->{TargetName}->string, 0, 40;
83 25 100         76 push @rdata, join '', shift(@target), "\t; $target\n" unless $target eq '.';
84 25           40 push @rdata, @target;
85                
86 25     100     57 my $params = $self->{SvcParams} || [];
87 25           42 my @params = @$params;
88 25           44 while (@params) {
89 30           32 my $key = shift @params;
90 30           36 my $val = shift @params;
91 30           73 push @rdata, "\n", unpack 'H4H4', pack( 'n2', $key, length $val );
92 30           68 my @hex = grep {length} split /(\S{32})/, unpack 'H*', $val;
  46           57  
93 30 100         54 push @rdata, shift @hex if @hex;
94 30 100         49 push @rdata, "\t; key$key\n" unless $key < 16;
95 30           43 push @rdata, @hex;
96 30           55 $length += 4 + length $val;
97               }
98 25 100         43 if ( $self->{rdata} ) {
99 5 100         12 if ( my $corrupt = substr $self->{rdata}, $length ) {
100 2           5 my ( $hex, @hex ) = grep {length} split /(\S{32})/, unpack 'H*', $corrupt;
  2           26  
101 2           6 push @rdata, "\n$hex\t; corrupt RDATA\n", @hex;
102 2           3 $length += length $corrupt;
103               }
104               }
105 25           125 return ( "\\# $length", @rdata );
106               }
107                
108                
109               sub _parse_rdata { ## populate RR from rdata in argument list
110 44       44   111 my ( $self, @argument ) = @_;
111                
112 44           106 $self->svcpriority( shift @argument );
113 44           100 $self->targetname( shift @argument );
114                
115 44           96 while ( local $_ = shift @argument ) {
116 55           321 m/^([^=]+)(=?)(.*)$/;
117 55           137 my $key = $1;
118 55 100         176 my $val = length($3) ? $3 : $2 ? shift @argument : '';
    100            
119 55 100         137 if (/^key\d+/) {
120 12           80 $self->$key($val);
121               } else {
122 43           65 local $_ = $val;
123 43 100         105 die <<'RIP' if /\\092[,\\]/;
124               SVCB: Please use standard RFC1035 escapes
125               RFC9460 double-escape insanity not implemented
126               RIP
127 41           68 s/^"([^"]*)"$/$1/s; # strip enclosing quotes
128 41           59 s/\\,/\\044/g; # disguise (RFC1035) escaped comma
129 41           1034 $self->$key( split /,/ );
130               }
131               }
132 31           71 return;
133               }
134                
135                
136               sub _post_parse { ## parser post processing
137 43       43   58 my $self = shift;
138                
139 43     100     93 my $paramref = $self->{SvcParams} || [];
140 43 100         126 my %svcparam = scalar(@$paramref) ? @$paramref : return;
141                
142 31           160 $self->key0(undef); # ruse to force sorting of SvcParams
143 31 100         59 if ( defined $svcparam{0} ) {
144 7           8 my %unique;
145 7           32 foreach ( grep { !$unique{$_}++ } unpack 'n*', $svcparam{0} ) {
  10           30  
146 9 100         42 die( $self->type . qq[: unexpected "key0" in mandatory list] ) if $unique{0};
147 8 100         24 die( $self->type . qq[: duplicate "key$_" in mandatory list] ) if --$unique{$_};
148 7 100         18 die( $self->type . qq[: mandatory "key$_" not present] ) unless defined $svcparam{$_};
149               }
150 4           11 $self->mandatory( keys %unique ); # restore mandatory key list
151               }
152 28 100   100     77 die( $self->type . qq[: expected alpn="..." not present] ) if defined( $svcparam{2} ) && !$svcparam{1};
153 26           76 return;
154               }
155                
156                
157               sub _defaults { ## specify RR attribute default values
158 2       2   5 my $self = shift;
159                
160 2           9 $self->_parse_rdata(qw(0 .));
161 2           4 return;
162               }
163                
164                
165               sub svcpriority {
166 50       50 0 84 my ( $self, @value ) = @_; # uncoverable pod
167 50           74 for (@value) { $self->{SvcPriority} = 0 + $_ }
  45           120  
168 50           95 return $self->{SvcPriority};
169               }
170                
171                
172               sub targetname {
173 51       51 0 969 my ( $self, @value ) = @_; # uncoverable pod
174 51 100         176 $self->{TargetName} = Net::DNS::DomainName->new(@value) if @value;
175 51 100         174 my $target = $self->{TargetName} ? $self->{TargetName}->name : return;
176 49 100         141 return $target unless $target eq '.';
177 16 100         56 return $self->{SvcPriority} ? $self->owner : undef;
178               }
179                
180                
181               sub mandatory { ## mandatory=key1,port,...
182 13       13 1 583 my ( $self, @value ) = @_;
183 13 100         23 my @list = map { $keybyname{lc $_} || $_ } @value;
  15           59  
184 13 100         21 my @keys = map { /(\d+)$/ ? $1 : die( $self->type . qq[: unexpected "$_"] ) } @list;
  15           84  
185 12           33 return $self->_SvcParam( 0, _integer16( sort { $a <=> $b } @keys ) );
  4           12  
186               }
187                
188               sub alpn { ## alpn=h3,h2,...
189 5       5 1 1862 my ( $self, @value ) = @_;
190 5           16 return $self->_SvcParam( 1, _string(@value) );
191               }
192                
193               sub no_default_alpn { ## no-default-alpn (Boolean)
194 6       6 0 14 my ( $self, @value ) = @_; # uncoverable pod
195 6 100         19 return $self->_SvcParam(2) if defined wantarray;
196 4           29 return $self->_SvcParam( 2, _boolean(@value) );
197               }
198                
199               sub port { ## port=1234
200 8       8 1 591 my ( $self, @value ) = @_;
201 8           19 return $self->_SvcParam( 3, map { _integer16($_) } @value );
  7           14  
202               }
203                
204               sub ipv4hint { ## ipv4hint=192.0.2.1,...
205 4       4 1 621 my ( $self, @value ) = @_;
206 4           12 return $self->_SvcParam( 4, _address4(@value) );
207               }
208                
209               sub ech { ## ech=base64
210 5       5 1 561 my ( $self, @value ) = @_;
211 5           12 return $self->_SvcParam( 5, map { _base64($_) } @value );
  4           12  
212               }
213                
214               sub ipv6hint { ## ipv6hint=2001:DB8::1,...
215 6       6 1 555 my ( $self, @value ) = @_;
216 6           17 return $self->_SvcParam( 6, _address6(@value) );
217               }
218                
219               sub dohpath { ## dohpath=/dns-query{?dns}
220 3       3 0 595 my ( $self, @value ) = @_; # uncoverable pod
221 3           8 return $self->_SvcParam( 7, map { substr _string($_), 1 } @value );
  2           7  
222               }
223                
224               sub ohttp { ## ohttp
225 4       4 0 534 my ( $self, @value ) = @_; # uncoverable pod
226 4 100         13 return $self->_SvcParam(8) if defined wantarray;
227 3           8 return $self->_SvcParam( 8, _boolean(@value) );
228               }
229                
230               sub tls_supported_groups { ## tls_supported_groups=29,23
231 3       3 0 7 my ( $self, @value ) = @_; # uncoverable pod
232 3           8 return $self->_SvcParam( 9, _integer16(@value) );
233               }
234                
235                
236               ########################################
237                
238                
239               sub _concatenate { ## concatenate octet string(s)
240 43       43   93 my @arg = @_;
241 43 100         250 return scalar(@arg) ? join( '', @arg ) : @arg;
242               }
243                
244               sub _boolean {
245 7       7   16 my @arg = @_;
246 7 100         46 return '' unless scalar @arg;
247 3 100         6 return map { $_ ? '' : undef } @arg;
  3           16  
248               }
249                
250               sub _string {
251 7       7   17 my @arg = @_;
252 7           13 my @val = map { split /,/ } @arg;
  8           23  
253 7           14 return _concatenate( map { Net::DNS::Text->new($_)->encode() } @val );
  8           37  
254               }
255                
256               sub _base64 {
257 4       4   9 my @arg = @_;
258 4           7 return _concatenate( map { MIME::Base64::decode($_) } @arg );
  4           17  
259               }
260                
261               sub _integer16 {
262 22       22   35 my @arg = @_;
263 22           28 my @val = map { split /,/ } @arg;
  25           50  
264 22           29 return _concatenate( map { pack( 'n', $_ ) } @val );
  25           90  
265               }
266                
267               sub _address4 {
268 4       4   10 my @arg = @_;
269 4           9 my @val = map { split /,/ } @arg;
  2           8  
270 4           8 return _concatenate( map { Net::DNS::RR::A::address( {}, $_ ) } @val );
  2           12  
271               }
272                
273               sub _address6 {
274 6       6   10 my @arg = @_;
275 6           13 my @val = map { split /,/ } @arg;
  5           14  
276 6           12 return _concatenate( map { Net::DNS::RR::AAAA::address( {}, $_ ) } @val );
  5           20  
277               }
278                
279                
280               sub AUTOLOAD { ## Dynamic constructor/accessor methods
281 57       57   6284 my ( $self, @argument ) = @_;
282                
283 57           88 our $AUTOLOAD;
284 57           189 my ($method) = reverse split /::/, $AUTOLOAD;
285 57           161 my $canonical = lc($method);
286 57           132 $canonical =~ s/-/_/g;
287 57 100         204 if ( $self->can($canonical) ) {
288 2       2   19 no strict 'refs'; ## no critic ProhibitNoStrict
  2           4  
  2           1841  
289 4       11   20 *{$AUTOLOAD} = sub { shift->$canonical(@_) };
  4           27  
  11           1357  
290 4           13 return $self->$canonical(@argument);
291               }
292                
293 53           81 my $super = "SUPER::$method";
294 53 100         295 return $self->$super(@argument) unless $method =~ /^key[0]*(\d+)$/i;
295 52           116 my $key = $1;
296                
297 52 100         111 return $self->_SvcParam($key) unless @argument;
298 43           84 my $first = shift @argument;
299 43 100         99 my $value = defined $first ? Net::DNS::Text->new($first)->raw : $first;
300 43           96 return $self->_SvcParam( $key, $value, @argument );
301               }
302                
303                
304               sub _SvcParam {
305 108       108   207 my ( $self, $key, @argument ) = @_;
306                
307 108     100     318 my $paramsref = $self->{SvcParams} || [];
308 108           244 my %svcparams = @$paramsref;
309                
310 108 100         189 if ( scalar @argument ) {
311 82           122 my $arg = shift @argument; # keyNN($value);
312 82 100         152 delete $svcparams{$key} unless defined $arg;
313 82 100         181 die( $self->type . qq[: duplicate SvcParam "key$key"] ) if defined $svcparams{$key};
314 81 100         165 die( $self->type . qq[: invalid SvcParam "key$key"] ) if $key > 65534;
315 80 100         145 die( $self->type . qq[: unexpected "key$key" value] ) if scalar @argument;
316 78           117 delete $self->{rdata};
317 78 100         200 $svcparams{$key} = $arg if defined $arg;
318 78           210 $self->{SvcParams} = [map { ( $_, $svcparams{$_} ) } sort { $a <=> $b } keys %svcparams];
  160           381  
  168           206  
319               } else {
320 26 100         59 die( $self->type . qq[: no value specified for "key$key"] ) unless defined wantarray;
321               }
322                
323 98           492 return $svcparams{$key};
324               }
325                
326               ########################################
327                
328                
329               1;
330               __END__