File Coverage

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


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