File Coverage

blib/lib/Net/DNS/Extlang.pm
Criterion Covered Total %
statement 23 232 9.9
branch 4 136 2.9
condition 2 33 6.0
subroutine 7 20 35.0
pod 4 4 100.0
total 40 425 9.4


line stmt bran cond sub pod time code
1             package Net::DNS::Extlang;
2              
3 3     3   48288 use 5.20.0;
  3         12  
4             our $VERSION = '0.1';
5              
6             =head1 NAME
7              
8             Net::DNS::Extlang - DNS extension language
9              
10             =head1 Version
11              
12             Version 0.1.
13              
14             =head1 SYNOPSIS
15              
16             use Net::DNS::Extlang;
17              
18             $ext = new Net::DNS::Extlang(file => '/etc/dnsext.txt',
19             domain => 'arpa, lang => 'en', resolver => resobj)
20              
21             =head1 DESCRIPTION
22              
23             The Net::DNS::Extlang module reads and stores RR descriptions from files
24             or the DNS. If file is provided, it reads descriptions from that file,
25             otherwise it looks in .rrname. and .rrtype.
26             for descriptions in the desired language.
27              
28             Provide a resolver if you want other than the default resolver
29             settings.
30              
31             =cut
32              
33 3     3   18 use strict;
  3         7  
  3         66  
34 3     3   1792 use integer;
  3         44  
  3         16  
35 3     3   80 use Carp;
  3         6  
  3         1928  
36             require Net::DNS::Resolver;
37              
38             =head1 METHODS
39              
40             =head2 new
41              
42             $ext = new Net::DNS::Extlang(file => '/etc/dnsext.txt',
43             domain => 'arpa', lang => 'en', resolver => resobj)
44              
45             Create an object corresponding to a set of extension language entries
46             in a file or the DNS. Provide either a file or a domain argument.
47             If you provide a domain, the lang and resolver are optional.
48              
49             In addition to using its methods, you can push the object onto @INC
50             to let it automatically create rrtype routines as required.
51              
52             =cut
53              
54             sub new {
55 2     2 1 988 my $class = shift;
56              
57 2         12 my %args = (lang => 'en', file => undef, domain => undef, resolver => undef, @_);
58              
59             my $self = bless {
60             file => $args{'file'},
61             domain => $args{'domain'},
62 2         12 lang => $args{'lang'},
63             rrnames => {}, # RRs by name
64             rrnums => {}, # RRs by number
65             }, $class;
66              
67 2 50 66     12 if($args{file} and $args{domain}) {
68 0         0 croak "Cannot get extensions from both file and DNS";
69             }
70 2 100       7 if($args{file}) {
71 1         5 _xlreadfile($self, $args{file});
72             }
73 1         4 $self;
74             }
75              
76             # read a file, set the text parts of $self->rrnames and $self->rrnums
77              
78             sub _xlreadfile {
79 1     1   4 my ($self, $file) = @_;
80              
81 1 50       278 open(my $rrfile, "<", $file) or croak "Cannot open ext lange file $file";
82 0           my @xllist = ();
83              
84 0           while(<$rrfile>) {
85 0           chomp;
86 0 0         next if m{^\s*($|#)}; # comments or blank line
87 0 0         if(m{^\s+(.*)}) {
88 0           push @xllist, $1;
89 0           next;
90             }
91             # must be a new one, store current one
92 0 0         _xlstorerecord($self, @xllist) if $#xllist >= 0;
93              
94 0           @xllist = ($_);
95             }
96 0 0         _xlstorerecord($self, @xllist) if $#xllist >= 0;
97              
98 0           close $rrfile;
99             }
100              
101             # store a record with rrname/number and list of fields
102             # only do rudimentary syntax checking here
103              
104             # match head record, $1 = name, $2 = number, $3 = description
105             # ignores I/A third subfield
106             my $headpattern = qr{^ (?[a-z0-9][-a-z0-9]*):(?\d+)(?: :[a-z]+)? (?: \s+ (?.*))?$}ix;
107              
108              
109             # match a field, $1 = type, $2 = quals, $3 - name, $4 = comment
110             my $fieldpattern = qr{^ (?I[124]|AA?|AAAA|[ZNRSTX]|B32|B64|T6|X[P68]) # field type
111             (?:\[ (? (?:[CALMX]|[-a-zA-Z0-9]+=\d+) (?:,(?:[CALMX]|[-a-zA-Z0-9]+=\d+))* )\])?
112             (?: :(?[-a-zA-Z0-9]+))? # optional field name
113             (?: \s+ (?.*))?$}ix; # optional comment
114              
115             sub _xlstorerecord {
116 0     0     my ($self, $rr, @fieldlist) = @_;
117              
118 0 0         croak "no rr record" if !$rr;
119              
120 0           my ($rrname, $rrnum, $rrcomment ) = $rr =~ m{$headpattern};
121 0 0 0       croak "invalid rr record $rr" if !$rrname or !$rrnum;
122              
123 0           $rrnum = 0+$rrnum; # force to a number
124             # parse each of them into a hash of fields via $fieldpattern
125 0           my @fieldstructs = ();
126 0           foreach my $field (@fieldlist) {
127 0 0         $field =~ m{$fieldpattern} || croak("invalid field in $rrname: $field");
128 3     3   1814 push @fieldstructs, { %+ }; # copy the field's entries from %+
  3         1307  
  3         11872  
  0            
129             }
130              
131             # make up an rr thing
132 0           my $rrr = {
133             mnemon => $rrname,
134             number => $rrnum,
135             comment => $rrcomment,
136             fields => \@fieldstructs
137             };
138              
139             # stash it by name and number
140 0           $self->{rrnames}->{$rrname} = $rrr;
141 0           $self->{rrnums}->{$rrnum} = $rrr;
142              
143 0           $rrr;
144             }
145              
146             =head2 getrr
147              
148             %rrinfo = $ext->getrr(nameornumber)
149              
150             Retrieve the rr description by number (if the argument is all digits)
151             or name (otherwise.) %rrinfo is a hash with fields mnemon, number,
152             comment, and fields: the lines in the description
153             stanza. Each field is a hash with entries type (field type),
154             quals (optional qualifiers), name (optional field name), and comment.
155              
156             Descriptions from a file are all loaded by new(), from the DNS
157             are fetched as needed.
158             If there's no description for that name or number it returns undef.
159              
160             =cut
161              
162             sub getrr {
163 0     0 1   my ($self, $rrn) = @_;
164 0           my ($res, $name);
165              
166 0 0         croak("Need rrname or rrtype in getrr") unless $rrn;
167            
168 0 0         if($rrn =~ m{^\d+$}) { # look up by number
169 0 0         return $self->{rrnums}->{$rrn} if exists $self->{rrnums}->{$rrn};
170 0 0         return undef if defined $self->{file}; # not in the file
171             # try from the DNS
172 0           $name = "$rrn.rrtype.$self->{domain}";
173             } else { # look up by name
174 0           $rrn = uc $rrn; # RRTYPES are UPPER CASE
175              
176 0 0         return $self->{rrnames}->{$rrn} if exists $self->{rrnames}->{$rrn};
177 0 0         return undef if defined $self->{file}; # not in the file
178             # try from the DNS
179 0           $name = "$rrn.rrname.$self->{domain}";
180             }
181              
182             # look it up
183 0           $res = $self->{resolver};
184 0 0         $res = $self->{resolver} = new Net::DNS::Resolver unless $res;
185 0           my $answer = $res->query($name, 'TXT');
186 0 0         return undef unless $answer; # nothing there
187              
188 0           foreach my $rr ($answer->answer) {
189 0 0         next if $rr->type ne 'TXT';
190              
191 0           my @txt = $rr->txtdata;
192              
193 0 0         next unless $txt[0] eq "RRTYPE=1";
194            
195 0           my ($trname, $trno) = $txt[1] =~ m{$headpattern};
196 0 0 0       croak "invalid description $txt[1]" if !$trname or !$trno;
197              
198             # make sure it's the right rr
199 0 0         if($rrn =~ m{^\d+$}) {
200 0 0         croak "wrong rrtype $rrn $txt[1]" if $rrn != $trno;
201             } else {
202 0 0         croak "wrong rrtype $rrn $txt[1]" if lc $rrn ne lc $trname;
203             }
204            
205 0           shift @txt; # get rid of desc tag
206 0           return _xlstorerecord($self, @txt); # will croak if bad syntax
207             }
208              
209             # didn't find it, note for next time
210 0 0         if($rrn =~ m{^\d+$}) { # look up by number
211 0           $self->{rrnums}->{$rrn} = undef;
212             } else { # look up by name
213 0           $self->{rrnames}->{$rrn} = undef;
214             }
215             }
216              
217             =head2 compile / compilerr
218              
219             $code = $ext->compile(nameornumber)
220             $code = $ext->compilerr($rrr)
221              
222             Compile the rr description into Net::DNS::RR: and return
223             the perl code, suitable to pass to eval().
224             nameornumber is looked up, $rrr is an rr description such as getrr()
225             returns.
226              
227             If there's no description it returns null.
228              
229             Compiled methods include:
230              
231             _decode_rdata, _encode_rdata, _format_rdata, _parse_rdata, _defaults
232              
233             get/set for each field named to match the field, or fieldN if the field
234             has no name or a duplicate name.
235             If field names match built in names or perl keywords, the get/set
236             method name is prefixed with 'f'.
237              
238             =cut
239              
240              
241             # $rrr is a rrinfo hash, %pats are patterns to select from based on the
242             # type and quals where it looks for type[quals], then type, then
243             # "default". When checking for quals they are alphabetized so a query
244             # for N[C,A] will match N[A,C]
245             #my $CDEBUG = 0;
246              
247             sub _cchunk($@) {
248 0     0     my ($rrr, %pats) = @_;
249              
250 0 0         if(exists $rrr->{quals}) {
251 0           my $q = join(',', sort split /,/,uc $rrr->{quals}); # alphabetize them
252 0           my $k = uc $rrr->{type} . "[$q]";
253             # print "check $k\n" if $CDEBUG;
254 0 0         return $pats{$k} if exists $pats{$k};
255             }
256             # print "check $rrr->{type}\n" if $CDEBUG;
257 0 0         return $pats{uc $rrr->{type}} if exists $pats{uc $rrr->{type}};
258 0           return $pats{"default"};
259             }
260              
261             # substitite #WORD# in the string with $p{WORD} in the list
262             # csub($string, 'FOO' => "foo", 'BAR' => "bahr", ... )
263             sub _csub($@) {
264 0     0     my ($str, %subs) = @_;
265              
266 0           return $str =~ s{#([A-Z]+)#}{$subs{$1}}gr;
267             }
268              
269             # names that conflict with RR methods
270             my %dirtywords = map { ($_, 1) } qw( new decode encode canonical print string plain token name owner next last
271             type class ttl dump rdatastr rdata rdstring rdlength destroy autoload );
272              
273             sub compile {
274 0     0 1   my ($self, $rrn) = @_;
275              
276 0 0         croak("Need rrname or rrtype in compile") unless $rrn;
277            
278 0           my $rrr = $self->getrr($rrn);
279 0 0         $self->compilerr($rrr) if $rrr;
280             }
281              
282             sub compilerr {
283 0     0 1   my ($self, $rrr) = @_;
284              
285 0           my $rrname = uc $rrr->{mnemon};
286 0           my $rrnum = $rrr->{number};
287 0           my $rrcomment = $rrr->{comment};
288 0           my $rrfields = $rrr->{fields};
289            
290 0           my ($usedomainname, # if there's an N field
291             $usetext, # if there's an S field
292             $usemailbox, # if theres an N[A] field
293             $usebase64, # if there's a B32 or B64 field
294             $usetime, # if there's a time field
295             $userrtype, # if there's a rrtype field
296             $usensechelp, # if there's a rrtype list field or nsec3 base32
297             %fields, # field names in use
298             $fieldno, # to generate fieldN names
299             $decode, # contents of decode routine
300             $encode, # contents of encode routine
301             $format, # contents of format routine
302             $parse, # contents of parse routine
303             $defaults, # contents of defaults routine
304             $fieldfns # functions get/set fields
305             );
306              
307 0           foreach my $f (@$rrfields) {
308 0           $fieldno++;
309 0           my ($type, $quals, $name) = (uc $f->{type}, $f->{quals}, lc $f->{name});
310              
311 0 0         if($type eq "Z") { # no Z types implemented yet
312 0           carp("Unimplemented field type Z[$quals] in $rrname");
313 0           return undef;
314             }
315              
316             # censor dirty words
317 0 0         $name = $f->{name} = "f$name" if $dirtywords{$name};
318              
319             # make a name if there isn't one yet
320 0 0 0       if(!$name or exists $fields{$name}) {
321 0           $name = "field$fieldno";
322 0           $f->{name} = $name;
323             }
324 0           $fields{$name} = $fieldno;
325              
326 0 0 0       if($type eq 'N') {
    0          
    0          
    0          
    0          
    0          
327 0           $usedomainname = 1;
328 0 0 0       $usemailbox = 1 if defined $quals and $quals =~ m{A};
329             } elsif($type eq 'S') {
330 0           $usetext = 1;
331             } elsif($type eq "B64") {
332 0           $usebase64 = 1;
333             } elsif($type eq "B32") {
334 0           $usensechelp = 1;
335             } elsif($type eq "T" or $type eq "T6") {
336 0           $usetime = 1;
337             } elsif($type eq "R" ) {
338 0 0 0       if(defined($quals) and $quals eq "L") {
339 0           $usensechelp = 1;
340             } else {
341 0           $userrtype = 1;
342             }
343             }
344             }
345             # now get them in order, in a perhaps overcomplex way
346 0           my @fields = map { $_->{name} } @$rrfields;
  0            
347            
348             #print "fields are ",join(",", @fields), "\n";
349            
350             # generate per-field functions
351 0           $fieldfns = _perfield($rrfields);
352            
353             # default function
354 0           $defaults = _fielddefault($rrfields);
355 0           $decode = _fielddecode($rrfields);
356 0           $encode = _fieldencode($rrfields);
357 0           $parse = _fieldparse($rrfields);
358 0           $format = _fieldformat($rrfields);
359              
360             # other modules to include, depending on the type
361 0           my $uses = "";
362 0 0         $uses = "use Net::DNS::DomainName;\n" if $usedomainname;
363 0 0         $uses .= "use Net::DNS::Mailbox;\n" if $usemailbox;
364 0 0         $uses .= "use Net::DNS::Text;\n" if $usetext;
365 0 0         $uses .= "use MIME::Base64;\n" if $usebase64;
366 0 0         $uses .= "use Net::DNS::Extlang::Time qw(_encodetime _string2time);\n" if $usetime;
367 0 0         $uses .= "use Net::DNS::Parameters qw(typebyname typebyval);\n" if $userrtype;
368 0 0         $uses .= "use Net::DNS::Extlang::Nsechelp;\n" if $usensechelp;
369              
370             # glom it all together into one string to eval
371 0           my $code = <
372             # generated routine for $rrname $rrcomment
373             package Net::DNS::RR::$rrname;
374             use strict;
375             use base qw(Net::DNS::RR);
376             $uses
377             use Carp;
378             use integer;
379              
380             sub _decode_rdata { ## decode rdata from wire-format octet string
381             my (\$self, \$data, \$offset, \@opaque ) = \@_;
382             my \$origoffset = \$offset;
383             ## \$data reference to a wire-format packet buffer
384             ## \$offset location of rdata within packet buffer
385             $decode
386             }
387              
388             sub _encode_rdata { ## encode rdata as wire-format octet string
389             my (\$self, \$offset, \@opaque) = \@_;
390             my \$encdata = '';
391              
392             $encode
393             }
394              
395             sub _format_rdata { ## format rdata portion of RR string.
396             my (\$self, \@opaque) = \@_;
397              
398             $format
399             }
400              
401              
402             sub _parse_rdata { ## populate RR from rdata in argument list
403             my \$self = shift;
404              
405             $parse
406             }
407              
408             sub _defaults { ## specify RR attribute default values
409             my \$self = shift;
410              
411             ## Note that this code is executed once only after module is loaded.
412             $defaults
413             }
414             $fieldfns
415              
416             # also make by number
417             package Net::DNS::RR::TYPE$rrnum;
418             use strict;
419             use base qw(Net::DNS::RR::$rrname);
420              
421             1;
422             CODE
423              
424 0           return $code;
425             }
426              
427             # make the per-field functions
428             # field function
429             my $ffpat = <<'EOF';
430             sub #FIELD# {
431             my $self = shift;
432              
433             $self->{#FIELD#} = #SETVAL# if scalar @_;
434             #GETVAL#;
435             }
436             EOF
437              
438             # decode text of Base64
439             my $b64field = <<'EOF';
440             sub #FIELD# {
441             my $self = shift;
442              
443             $self->#FIELD#_bin( MIME::Base64::decode( join "", @_ ) ) if scalar @_;
444             MIME::Base64::encode( $self->#FIELD#_bin(), "" ) if defined wantarray;
445             }
446             EOF
447              
448             # decode text of hex field
449             my $hexfield = <<'EOF';
450             sub #FIELD# {
451             my $self = shift;
452              
453             $self->#FIELD#_bin( pack "H*", map { die "!hex!" if m/[^0-9A-Fa-f]/; $_ } join "", @_ ) if scalar @_;
454             unpack "H*", $self->#FIELD#_bin() if defined wantarray;
455              
456             }
457             EOF
458              
459             # counted hex field
460             #my $hexone = <<'EOF';
461             #sub #FIELD# {
462             # my ($self) = @_;
463              
464             # if(scalar @_) {
465             # my $arg = shift;
466             # die "!hex!" if $arg =~ m/[^0-9A-Fa-f]/;
467             # $self->{#FIELD#} = pack "H*", $arg;
468             # }
469             # unpack "H*", $self->{#FIELD#} if defined wantarray;
470              
471             #}
472             #EOF
473              
474             # integer field with named values
475             my $ivalfield = <<'EOF';
476             my %#FIELD#_vals = #VALMAP#;
477             sub #FIELD# {
478             my $self = shift;
479              
480             return $self->{#FIELD#} unless scalar @_;
481              
482             my $newval = shift || 0;
483             return $self->{#FIELD#} = $newval unless $newval =~ /\D/;
484              
485             my $typenum = $#FIELD#_vals{$newval};
486             $typenum || croak "unknown #FIELD# $newval"; # handle or'ed together fields someday
487             $self->{#FIELD#} = $typenum;
488             }
489             EOF
490              
491             # AAAA field
492             my $aaaafield = <<'EOF';
493             sub #FIELD#_long {
494             my $addr = pack 'a*@16', grep defined, shift->{#FIELD#};
495             sprintf '%x:%x:%x:%x:%x:%x:%x:%x', unpack 'n8', $addr;
496             }
497              
498             sub #FIELD#_short {
499             my $addr = pack 'a*@16', grep defined, shift->{#FIELD#};
500             for ( sprintf ':%x:%x:%x:%x:%x:%x:%x:%x:', unpack 'n8', $addr ) {
501             s/(:0[:0]+:)(?!.+:0\1)/::/; # squash longest zero sequence
502             s/^:// unless /^::/; # prune LH :
503             s/:$// unless /::$/; # prune RH :
504             return $_;
505             }
506             }
507              
508             sub #FIELD# {
509             my $self = shift;
510              
511             return #FIELD#_long($self) unless scalar @_;
512              
513             my $addr = shift;
514             my @parse = split /:/, "0$addr";
515              
516             if ( (@parse)[$#parse] =~ /\./ ) { # embedded IPv4
517             my @ip4 = split /\./, pop(@parse);
518             my $rhs = pop(@ip4);
519             my @ip6 = map { /./ ? hex($_) : (0) x ( 7 - @parse ) } @parse;
520             return $self->{#FIELD#} = pack 'n6 C4', @ip6, @ip4, (0) x ( 3 - @ip4 ), $rhs;
521             }
522              
523             # Note: pack() masks overlarge values, mostly without warning.
524             my @expand = map { /./ ? hex($_) : (0) x ( 9 - @parse ) } @parse;
525             $self->{#FIELD#} = pack 'n8', @expand;
526             }
527             EOF
528              
529              
530              
531             # bin field for hex or b64 or b32
532             sub _bn($$$) {
533 0     0     my ($type, $name, $quals) = @_;
534              
535 0 0 0       return "${name}_bin" if $type eq "B64" or ($type eq "X" and not defined $quals);
      0        
536 0           $name;
537             }
538              
539             sub _perfield {
540 0     0     my ($rrfields) = @_;
541 0           my ($fieldfns);
542            
543 0           foreach my $f (@$rrfields) {
544 0           my ($type, $quals, $name) = (uc $f->{type}, $f->{quals}, $f->{name});
545             # make a field function
546              
547             # if it's an integer field with named values
548 0 0 0       if($type =~ m{I\d} and $quals) {
549 0           my $valmap = "(" . join(",", map { my ($n, $v) = split /=/,$_,2; " '$n' => $v"; } split /,/,$quals) . ")";
  0            
  0            
550 0           $fieldfns .= _csub($ivalfield, FIELD => $name, VALMAP => $valmap);
551 0           next;
552             }
553              
554             # if it's AAAA
555 0 0         if($type eq "AAAA") { # setter, long and short setter
556 0           $fieldfns .= _csub($aaaafield, FIELD => $name );
557 0           next;
558             }
559              
560             # call it fieldbin if it's a base64
561 0           my $setval = _cchunk($f,
562             'N' => 'new Net::DNS::DomainName(shift)',
563             'N[C]' => 'new Net::DNS::DomainName1035(shift)',
564             'N[A]' => 'new Net::DNS::Mailbox1035(shift)',
565             'N[A,C]' => 'new Net::DNS::Mailbox1035(shift)',
566             'A' => 'pack "C4", split /\./,shift',
567             'AA' => 'pack "n4", map hex($_), split /:/, shift',
568             'S[M]' => '[map Net::DNS::Text->new($_), @_]',
569             'S' => ' Net::DNS::Text->new(shift)',
570             'T' => '_string2time(shift)',
571             'R' => 'typebyname(shift)',
572             'R[L]' => '_type2bm(@_)',
573             'X6' => 'pack "C6", map hex($_), split /[:-]/, shift',
574             'X8' => 'pack "C8", map hex($_), split /[:-]/, shift',
575             'B32' => '_decode_base32(shift)',
576             'X[C]' => 'pack "H*", shift',
577             'default' => 'shift');
578              
579             # FN in getval means it's a function, not just part of an expression
580 0           my $getval = _cchunk($f,
581             'default' => ' || undef',
582             'I1' => ' || 0',
583             'I2' => ' || 0',
584             'I4' => ' || 0',
585             'N' => "->name if \$self->{$name}",
586             'N[A]' => "->address if \$self->{$name}",
587             'N[A,C]' => "->address if \$self->{$name}",
588             'A' => "FNjoin '.', unpack 'C4', \$self->{$name} if \$self->{$name}",
589             'AA' => "FNsprintf '%x:%x:%x:%x', unpack 'n4',\$self->{$name} if \$self->{$name}",
590             'S[M]' => '|| []',
591             'S' => ' || ""',
592             'T' => "FN_encodetime(\$self->{$name})",
593             'R' => "FNtypebyval(\$self->{$name})",
594             'R[L]' => "FN_bm2type(\$self->{$name})",
595             'X6' => "FNjoin '-', unpack 'H2H2H2H2H2H2', \$self->{$name}",
596             'X8' => "FNjoin '-', unpack 'H2H2H2H2H2H2H2H2', \$self->{$name}",
597             'B32' => "FN_encode_base32(\$self->{$name})",
598             'X[C]' => "FNunpack 'H*', \$self->{$name}",
599              
600            
601             );
602 0 0         if(substr($getval, 0,2) eq "FN") {
603 0           $getval = substr($getval,2);
604             } else {
605 0           $getval = "\$self->{" . _bn($type, $name, $quals) . "} $getval";
606             }
607 0           $fieldfns .= _csub($ffpat, FIELD => _bn($type, $name, $quals), SETVAL => $setval, GETVAL => $getval);
608              
609 0 0         if($type eq "B64") { # extra set/get function for text version of the field
610 0           $fieldfns .= _csub($b64field, FIELD => $name );
611             }
612 0 0 0       if($type eq "X" and not defined $quals) { # extra set/get function for text version of the field
613 0           $fieldfns .= _csub($hexfield, FIELD => $name );
614             }
615             }
616 0           $fieldfns;
617             }
618              
619             sub _fielddefault {
620 0     0     my ($rrfields) = @_;
621 0           my ($defaults);
622            
623 0           foreach my $f (@$rrfields) {
624 0           my ($type, $quals, $name) = (uc $f->{type}, $f->{quals}, $f->{name});
625              
626 0           my $defval = _cchunk($f,
627             'default' => 'undef',
628             'I1' => '0',
629             'I2' => '0',
630             'I4' => '0',
631             'A' => 'pack "x4",0',
632             'AA' => 'pack "x8"',
633             'AAAA' => 'pack "x16"',
634             'S[M]' => '[]',
635             'S' => '""',
636             );
637              
638 0           $defaults .= _csub(" \$self->{#FIELD#} = #DEFVAL#;\n",
639             FIELD => _bn($type, $name, $quals),
640             DEFVAL => $defval);
641             }
642 0           $defaults;
643             }
644              
645             # extract fields from binary data
646             # triple of unpack code, size or 0 or -1, code string with #O# offset
647             # and #F# binary field name
648             my $stringdecode = <<'EOF';
649             my $limit = $offset + $self->{rdlength};
650             my $text;
651             my $txtdata = $self->{#F#} = [];
652             while ( $offset < $limit ) {
653             ( $text, $offset ) = decode Net::DNS::Text( $data, $offset );
654             push @$txtdata, $text;
655             }
656             croak('corrupt TXT data') unless $offset == $limit; # more or less FUBAR
657             EOF
658              
659             # single trailing string for S[X]
660             my $onestringdecode = <<'EOF';
661             my $limit = $origoffset + $self->{rdlength};
662             $self->{#F#} = decode Net::DNS::Text( $data, $offset, $limit - $offset );
663             EOF
664              
665             # counted field
666             my $countdecode = <<'EOF';
667             my $#F#_len = unpack "\@$offset C", $$data;
668             $self->{#F#} = unpack "\@$offset x a$#F#_len", $$data;
669             $offset += 1 + $#F#_len;
670             EOF
671              
672              
673             sub _fielddecode {
674 0     0     my ($rrfields) = @_;
675 0           my ($decode);
676 0           my $offoff = 0;
677            
678 0           foreach my $f (@$rrfields) {
679 0           my ($type, $quals, $name) = (uc $f->{type}, $f->{quals}, $f->{name});
680              
681 0           my $cch = _cchunk($f, 'default' => [ '???', '???', -1 ],
682             'I1' => [ 'C', 1, undef ],
683             'I2' => [ 'n', 2, undef ],
684             'I4' => [ 'N', 4, undef ],
685             'A' => [ 'a4', 4, undef ],
686             'AA' => ['a8', 8, undef ],
687             'AAAA' => ['a16', 8, undef ],
688             'N' => [ undef, 0, '($self->{#F#}, $offset) = decode Net::DNS::DomainName( $data, $offset, @opaque );'],
689             'N[C]' => [ undef, 0, '($self->{#F#}, $offset) = decode Net::DNS::DomainName1035( $data, $offset, @opaque );'],
690             'N[A,C]' => [ undef, 0, '($self->{#F#}, $offset) = decode Net::DNS::DomainName1035( $data, $offset, @opaque );'],
691             'S' => [ undef, 0, '( $self->{#F#}, $offset ) = decode Net::DNS::Text( $data, $offset );' ],
692             'S[M]' => [ undef, -1, $stringdecode ],
693             'S[X]' => [ undef, -1, $onestringdecode ],
694             'B64' => [ undef, -1, '$self->{#F#_bin} = substr $$data, $offset, $self->{rdlength} - ($offset-$origoffset);'],
695             'X' => [ undef, -1, '$self->{#F#_bin} = substr $$data, $offset, $self->{rdlength} - ($offset-$origoffset);'],
696             'X[C]' => [ undef, 0, $countdecode ],
697             'B32' => [ undef, 0, $countdecode ],
698             'R[L]' => [ undef, -1, '$self->{#F#} = substr $$data, $offset, $self->{rdlength} - ($offset-$origoffset);'],
699             'T' => [ 'N', 4, undef ],
700             'R' => [ 'n', 2, undef ],
701             'X6' => [ 'a6', 6, undef ],
702             'X8' => [ 'a8', 8, undef ],
703            
704             );
705            
706 0           my ($pat, $size, $code) = @$cch;
707 0 0         croak "$name field after end of decoded data" if $offoff < 0;
708              
709 0 0         if($pat) {
710 0           $decode .= "\t\$self->{$name} = unpack \"\\\@\$offset $pat\",\$\$data;\n\t\$offset += $size;\n";
711 0           $offoff += $size;
712             } else {
713 0           $decode .= _csub("\t$code\n", F => $name);
714 0 0         if($size < 0) { $offoff = -1; }
  0            
715 0           else { $offoff += $size; } # 0 for offset updated, -1 for not so this has to be last
716             }
717             }
718 0           $decode;
719             }
720              
721             # turn fields into binary data
722             # triple of pack codes, and code to create the stuff to pack, size
723             # default code is the field
724             # size of -1 means unknown, will fail if something later wants it
725             # stores the data into $encdata
726              
727             sub _fieldencode {
728 0     0     my ($rrfields) = @_;
729 0           my ($packpat, @args, $packcode);
730            
731 0           foreach my $f (@$rrfields) {
732 0           my ($type, $quals, $name) = (uc $f->{type}, $f->{quals}, $f->{name});
733              
734 0           my $cch = _cchunk($f, 'default' => [ '???', '???', -1 ],
735             'I1' => [ 'C', undef, 1 ],
736             'I2' => [ 'n', undef, 2 ],
737             'I4' => [ 'N', undef, 4 ],
738             'A' => [ 'a4', undef, 4 ],
739             'AA' => [ 'a8', undef, 8 ],
740             'AAAA' => [ 'a16', undef,16 ],
741             'N' => [ 'a*', '#F#->encode(#O#, @opaque)', -1 ],
742             'N[A]' => [ 'a*', '#F#->encode(#O#, @opaque)', -1 ],
743             'N[A,C]' => [ 'a*', '#F#->encode(#O#, @opaque)', -1 ],
744             'S' => [ 'a*', ' #F#->encode', -1 ], # encode provides the length
745             'S[X]' => [ 'a*', '#F#->raw', -1 ],
746             'S[M]' => [ 'a*', 'join("", map( $_->encode, @{#F#}))', -1 ],
747             'B64' => [ 'a*', undef, -1 ],
748             'X[C]' => [ 'Ca*', 'length(#F#),#F#', -1 ],
749             'B32' => [ 'Ca*', 'length(#F#),#F#', -1 ],
750             'X' => [ 'a*', undef, -1 ],
751             'T' => [ 'N', undef, 4 ],
752             'R' => [ 'n', undef, 2 ],
753             'R[L]' => [ 'a*', undef, -1 ],
754             'X6' => [ 'a6', undef, 6 ],
755             'X8' => [ 'a8', undef, 8 ],
756             );
757            
758 0           my ($pat, $field, $size) = @$cch;
759 0 0         $field = '#F#' unless $field;
760              
761             # handle names that need to know the offset
762 0 0         if($field =~ m{#O#}) {
763 0 0         if($packpat) { # flush out any pending stuff
764 0 0         if($packcode) {
765 0           $packcode .= "\t\$encdata .= ";
766             } else {
767 0           $packcode = "\t\$encdata = ";
768             }
769 0 0         if($packpat =~ m{^(a\*)+$}) { # all a's, just concat
770 0           $packcode .= join(" . ", @args) . ";\n";
771             } else {
772 0           $packcode .= "pack '$packpat'," . join(", ", @args) . ";\n";
773             }
774 0           $packpat = ""; @args = ();
  0            
775             }
776 0 0         if($packcode) {
777 0           $field =~ s{#O#}{\$offset+(length \$encdata)};
778             } else {
779 0           $field =~ s{#O#}{\$offset}; # first field, plain offset
780             }
781             }
782 0           $packpat .= $pat;
783 0           push @args,$field =~ s{#F#}{'$self->{' . _bn($type, $name, $quals) . '}'}egr;
  0            
784             }
785             # now generate the code
786 0 0         if($packpat) {
787 0 0         if($packcode) {
788 0           $packcode .= "\t\$encdata .= ";
789             } else {
790 0           $packcode = "\t\$encdata = ";
791             }
792 0 0         if($packpat =~ m{^(a\*)+$}) { # all a's, just concat
793 0           $packcode .= join(" . ", @args) . ";\n";
794             } else {
795 0           $packcode .= "pack '$packpat'," . join(", ", @args) . ";\n";
796             }
797             }
798 0           $packcode;
799             }
800              
801             # parse arguments to make a new RR
802             sub _fieldparse {
803 0     0     my ($rrfields) = @_;
804 0           my ($decode, $eaten); # $eaten means all the arguments have been eaten
805            
806 0           foreach my $f (@$rrfields) {
807 0           my ($type, $quals, $name) = (uc $f->{type}, $f->{quals}, $f->{name});
808              
809 0 0         carp("Field with no argument $name") if $eaten;
810              
811             #print "parse $type $name ";
812             # check for a field that takes multiple arguments
813 0           my $val = _cchunk($f, 'default' => 'shift',
814             'S[M]' => '@_',
815             'B64' => '@_',
816             'X' => '@_',
817             'X[C]' => 'shift',
818             'R[L]' => '@_',
819             );
820             #print "$val\n";
821 0 0         $eaten = 1 if $val =~ m'@_';
822 0           $decode .= _csub(" \$self->#FIELD#(#VAL#);\n",
823             FIELD => $name,
824             VAL => $val);
825             }
826 0           $decode;
827             }
828              
829             # format RR fields into an array
830             sub _fieldformat {
831 0     0     my ($rrfields) = @_;
832 0           my (@rdata); # $eaten means all the arguments have been eaten
833            
834 0           foreach my $f (@$rrfields) {
835 0           my ($type, $quals, $name) = (uc $f->{type}, $f->{quals}, $f->{name});
836              
837 0           my $fmt = _cchunk($f, 'default' => '$self->{#FIELD#}',
838             'N' => '$self->{#FIELD#}->string',
839             'N[C]' => '$self->{#FIELD#}->string',
840             'N[C,A]' => '$self->#FIELD#}->string',
841             'S' => '$self->{#FIELD#}->string',
842             'S[M]' => '(map $_->string, @{$self->{#FIELD#}})',
843             'A' => '$self->#FIELD#()',
844             'AA' => "sprintf('%x:%x:%x:%x', unpack 'n4',\$self->{#FIELD#})",
845             'AAAA' => '$self->#FIELD#_short',
846             'B64' => 'split(/\s+/, encode_base64( $self->{#FIELD#_bin}))',
847             'B32' => '$self->#FIELD#()',
848             'X' => '$self->#FIELD#()',
849             'R' => '$self->#FIELD#()', # same as R[L]
850             'T' => '$self->#FIELD#()',
851             'X6' => '$self->#FIELD#()',
852             'X8' => '$self->#FIELD#()',
853             );
854 0           push @rdata, _csub($fmt, FIELD => $name);
855             }
856 0           "(" . join(",\n\t", @rdata) . "\n\t);\n";
857             }
858              
859             =head1 Field types
860              
861             =head2 I1, I2, I4 -- bigendian integers
862              
863             Display is unsigned integer
864              
865             =head2 R, R[L] - 16 bit RRTYPE, or NSEC grouped bitmap of RRTYPEs
866              
867             Display is symbolic RRTYPE or typeN, or list thereof
868              
869             =head2 A, AA, AAAA - 32, 64, 128 bit address
870              
871             Display is 1.1.1.1 or xx:xx::xx
872              
873             =head2 N - regular and compressed domain name, mailbox domain name
874              
875             Display is a domain name. Option C means RFC1035 compression, option A
876             means it's really a mailbox.
877             Options only for the last field in a record: O means the name is optional.
878              
879             =head2 S, S[M], S[X] - string, multiple strings, uncounted final string
880              
881             Quoted string or strings. M and X must be last field.
882              
883             =head2 B32/64 - base32/64
884              
885             Display is string.
886             B32 is preceded in the record by a length byte.
887             B64 is uncounted so must be last field, display can have embedded spaces.
888              
889             =head2 X, X[C] - hex, hex with one byte count.
890              
891             Uncounted X must be the last field, display can contain spaces.
892              
893             =head2 X6, X8 - EUI48 and EUI64
894              
895             Display is six or eight bytes of hex with optional hyphens.
896              
897             =head2 T. T6 - unix timestamp
898              
899             T is four bytes, T6 is six bytes.
900             Display is number of seconds since 1970 or yyyymmddhhmmss.
901              
902             =head2 Z[...] - special cases
903              
904             Defined in the spec but not implemented
905              
906             =cut
907             1;
908             __END__