File Coverage

blib/lib/Net/DNS/ZoneFile.pm
Criterion Covered Total %
statement 247 247 100.0
branch 104 104 100.0
condition 23 23 100.0
subroutine 37 37 100.0
pod 8 8 100.0
total 419 419 100.0


line stmt bran cond sub pod time code
1             package Net::DNS::ZoneFile;
2              
3 12     12   3865 use strict;
  12         29  
  12         775  
4 12     12   74 use warnings;
  12         26  
  12         699  
5              
6             our $VERSION = (qw$Id: ZoneFile.pm 1910 2023-03-30 19:16:30Z willem $)[2];
7              
8              
9             =head1 NAME
10              
11             Net::DNS::ZoneFile - DNS zone file
12              
13             =head1 SYNOPSIS
14              
15             use Net::DNS::ZoneFile;
16              
17             $zonefile = Net::DNS::ZoneFile->new( 'named.example' );
18              
19             while ( $rr = $zonefile->read ) {
20             $rr->print;
21             }
22              
23             @zone = $zonefile->read;
24              
25              
26             =head1 DESCRIPTION
27              
28             Each Net::DNS::ZoneFile object instance represents a zone file
29             together with any subordinate files introduced by the $INCLUDE
30             directive. Zone file syntax is defined by RFC1035.
31              
32             A program may have multiple zone file objects, each maintaining
33             its own independent parser state information.
34              
35             The parser supports both the $TTL directive defined by RFC2308
36             and the BIND $GENERATE syntax extension.
37              
38             All RRs in a zone file must have the same class, which may be
39             specified for the first RR encountered and is then propagated
40             automatically to all subsequent records.
41              
42             =cut
43              
44              
45 12     12   593 use integer;
  12         37  
  12         386  
46 12     12   499 use Carp;
  12         31  
  12         876  
47 12     12   84 use IO::File;
  12         36  
  12         2249  
48              
49 12     12   80 use base qw(Exporter);
  12         25  
  12         1657  
50             our @EXPORT = qw(parse read readfh);
51              
52 12     12   86 use constant PERLIO => defined eval { require PerlIO };
  12         25  
  12         26  
  12         2351  
53              
54 12         22 use constant UTF8 => scalar eval { ## not UTF-EBCDIC [see Unicode TR#16 3.6]
55 12         47 require Encode;
56 12         136 Encode::encode_utf8( chr(182) ) eq pack( 'H*', 'C2B6' );
57 12     12   290 };
  12         38  
58              
59             require Net::DNS::Domain;
60             require Net::DNS::RR;
61              
62              
63             =head1 METHODS
64              
65              
66             =head2 new
67              
68             $zonefile = Net::DNS::ZoneFile->new( 'filename', ['example.com'] );
69              
70             $handle = IO::File->new( 'filename', '<:encoding(ISO8859-7)' );
71             $zonefile = Net::DNS::ZoneFile->new( $handle, ['example.com'] );
72              
73             The new() constructor returns a Net::DNS::ZoneFile object which
74             represents the zone file specified in the argument list.
75              
76             The specified file or file handle is open for reading and closed when
77             exhausted or all references to the ZoneFile object cease to exist.
78              
79             The optional second argument specifies $ORIGIN for the zone file.
80              
81             Zone files are presumed to be UTF-8 encoded where that is supported.
82              
83             Alternative character encodings may be specified indirectly by creating
84             a file handle with the desired encoding layer, which is then passed as
85             an argument to new(). The specified encoding is propagated to files
86             introduced by $INCLUDE directives.
87              
88             =cut
89              
90             sub new {
91 67     67 1 30566 my ( $class, $filename, $origin ) = @_;
92 67         233 my $self = bless {fileopen => {}}, $class;
93              
94 67         222 $self->_origin($origin);
95              
96 67 100       192 if ( ref($filename) ) {
97 16         49 $self->{filehandle} = $self->{filename} = $filename;
98 16 100       178 return $self if ref($filename) =~ /IO::File|FileHandle|GLOB|Text/;
99 1         134 croak 'argument not a file handle';
100             }
101              
102 51 100       345 croak 'filename argument undefined' unless $filename;
103 50         86 my $discipline = UTF8 ? '<:encoding(UTF-8)' : '<';
104 50 100       241 $self->{filehandle} = IO::File->new( $filename, $discipline ) or croak "$filename: $!";
105 46         11058 $self->{fileopen}->{$filename}++;
106 46         99 $self->{filename} = $filename;
107 46         257 return $self;
108             }
109              
110              
111             =head2 read
112              
113             $rr = $zonefile->read;
114             @rr = $zonefile->read;
115              
116             When invoked in scalar context, read() returns a Net::DNS::RR object
117             representing the next resource record encountered in the zone file,
118             or undefined if end of data has been reached.
119              
120             When invoked in list context, read() returns the list of Net::DNS::RR
121             objects in the order that they appear in the zone file.
122              
123             Comments and blank lines are silently disregarded.
124              
125             $INCLUDE, $ORIGIN, $TTL and $GENERATE directives are processed
126             transparently.
127              
128             =cut
129              
130             sub read {
131 101     101 1 4306 my ($self) = @_;
132              
133 101 100       282 return &_read unless ref $self; # compatibility interface
134              
135 96 100       214 if (wantarray) {
136 9         17 my @zone; # return entire zone
137 9         15 eval {
138 9         32 local $SIG{__DIE__};
139 9         27 while ( my $rr = $self->_getRR ) {
140 183         479 push( @zone, $rr );
141             }
142             };
143 9 100       37 croak join ' ', $@, ' file', $self->name, 'line', $self->line, "\n " if $@;
144 8         59 return @zone;
145             }
146              
147 87         128 my $rr = eval {
148 87         303 local $SIG{__DIE__};
149 87         201 $self->_getRR; # return single RR
150             };
151 87 100       362 croak join ' ', $@, ' file', $self->name, 'line', $self->line, "\n " if $@;
152 60         228 return $rr;
153             }
154              
155              
156             =head2 name
157              
158             $filename = $zonefile->name;
159              
160             Returns the name of the current zone file.
161             Embedded $INCLUDE directives will cause this to differ from the
162             filename argument supplied when the object was created.
163              
164             =cut
165              
166             sub name {
167 41     41 1 1283 return shift->{filename};
168             }
169              
170              
171             =head2 line
172              
173             $line = $zonefile->line;
174              
175             Returns the number of the last line read from the current zone file.
176              
177             =cut
178              
179             sub line {
180 92     92 1 1011 my $self = shift;
181 92 100       231 return $self->{eom} if defined $self->{eom};
182 90         372 return $self->{filehandle}->input_line_number;
183             }
184              
185              
186             =head2 origin
187              
188             $origin = $zonefile->origin;
189              
190             Returns the fully qualified name of the current origin within the
191             zone file.
192              
193             =cut
194              
195             sub origin {
196 5     5 1 444 my $context = shift->{context};
197 5     5   25 return &$context( sub { Net::DNS::Domain->new('@') } )->string;
  5         21  
198             }
199              
200              
201             =head2 ttl
202              
203             $ttl = $zonefile->ttl;
204              
205             Returns the default TTL as specified by the $TTL directive.
206              
207             =cut
208              
209             sub ttl {
210 2     2 1 9 return shift->{TTL};
211             }
212              
213              
214             =head1 COMPATIBILITY WITH Net::DNS::ZoneFile 1.04
215              
216             Applications which depended on the defunct Net::DNS::ZoneFile 1.04
217             CPAN distribution will continue to operate with minimal change using
218             the compatibility interface described below.
219             New application code should use the object-oriented interface.
220              
221             use Net::DNS::ZoneFile;
222              
223             $listref = Net::DNS::ZoneFile->read( $filename );
224             $listref = Net::DNS::ZoneFile->read( $filename, $include_dir );
225              
226             $listref = Net::DNS::ZoneFile->readfh( $filehandle );
227             $listref = Net::DNS::ZoneFile->readfh( $filehandle, $include_dir );
228              
229             $listref = Net::DNS::ZoneFile->parse( $string );
230             $listref = Net::DNS::ZoneFile->parse( \$string );
231             $listref = Net::DNS::ZoneFile->parse( $string, $include_dir );
232             $listref = Net::DNS::ZoneFile->parse( \$string, $include_dir );
233              
234             $_->print for @$listref;
235              
236             The optional second argument specifies the default path for filenames.
237             The current working directory is used by default.
238              
239             Although not available in the original implementation, the RR list can
240             be obtained directly by calling any of these methods in list context.
241              
242             @rr = Net::DNS::ZoneFile->read( $filename, $include_dir );
243              
244             The partial result is returned if an error is encountered by the parser.
245              
246              
247             =head2 read
248              
249             $listref = Net::DNS::ZoneFile->read( $filename );
250             $listref = Net::DNS::ZoneFile->read( $filename, $include_dir );
251              
252             read() parses the contents of the specified file
253             and returns a reference to the list of Net::DNS::RR objects.
254             The return value is undefined if an error is encountered by the parser.
255              
256             =cut
257              
258             our $include_dir; ## dynamically scoped
259              
260             sub _filename { ## rebase unqualified filename
261 18     18   29 my $name = shift;
262 18 100       47 return $name if ref($name); ## file handle
263 11 100       30 return $name unless $include_dir;
264 3         21 require File::Spec;
265 3 100       29 return $name if File::Spec->file_name_is_absolute($name);
266 2 100       36 return $name if -f $name; ## file in current directory
267 1         27 return File::Spec->catfile( $include_dir, $name );
268             }
269              
270              
271             sub _read {
272 12     12   25 my ($arg1) = @_;
273 12 100 100     52 shift if !ref($arg1) && $arg1 eq __PACKAGE__;
274 12         18 my $filename = shift;
275 12         22 local $include_dir = shift;
276              
277 12         21 my $zonefile = Net::DNS::ZoneFile->new( _filename($filename) );
278 10         19 my @zone;
279 10         16 eval {
280 10         36 local $SIG{__DIE__};
281 10         19 my $rr;
282 10         25 push( @zone, $rr ) while $rr = $zonefile->_getRR;
283             };
284 10 100       105 return wantarray ? @zone : \@zone unless $@;
    100          
285 2         251 carp $@;
286 2 100       23 return wantarray ? @zone : undef;
287             }
288              
289              
290             {
291              
292             package Net::DNS::ZoneFile::Text; ## no critic ProhibitMultiplePackages
293              
294 12     12   16122 use overload ( '<>' => 'readline' );
  12         3981  
  12         75  
295              
296             sub new {
297 7     7   18 my ( $class, $data ) = @_;
298 7         15 my $self = bless {}, $class;
299 7 100       115 $self->{data} = [split /\n/, ref($data) ? $$data : $data];
300 7         30 return $self;
301             }
302              
303             sub readline {
304 40     40   58 my $self = shift;
305 40         64 $self->{line}++;
306 40         53 return shift( @{$self->{data}} );
  40         140  
307             }
308              
309             sub close {
310 5     5   13 shift->{data} = [];
311 5         9 return 1;
312             }
313              
314             sub input_line_number {
315 5     5   14 return shift->{line};
316             }
317              
318             }
319              
320              
321             =head2 readfh
322              
323             $listref = Net::DNS::ZoneFile->readfh( $filehandle );
324             $listref = Net::DNS::ZoneFile->readfh( $filehandle, $include_dir );
325              
326             readfh() parses data from the specified file handle
327             and returns a reference to the list of Net::DNS::RR objects.
328             The return value is undefined if an error is encountered by the parser.
329              
330             =cut
331              
332             sub readfh {
333 7     7 1 14 return &_read;
334             }
335              
336              
337             =head2 parse
338              
339             $listref = Net::DNS::ZoneFile->parse( $string );
340             $listref = Net::DNS::ZoneFile->parse( \$string );
341             $listref = Net::DNS::ZoneFile->parse( $string, $include_dir );
342             $listref = Net::DNS::ZoneFile->parse( \$string, $include_dir );
343              
344             parse() interprets the text in the argument string
345             and returns a reference to the list of Net::DNS::RR objects.
346             The return value is undefined if an error is encountered by the parser.
347              
348             =cut
349              
350             sub parse {
351 7     7 1 5741 my ($arg1) = @_;
352 7 100       28 shift if $arg1 eq __PACKAGE__;
353 7         15 my $string = shift;
354 7         16 my @include = grep {defined} shift;
  7         23  
355 7         30 return &readfh( Net::DNS::ZoneFile::Text->new($string), @include );
356             }
357              
358              
359             ########################################
360              
361              
362             {
363              
364             package Net::DNS::ZoneFile::Generator; ## no critic ProhibitMultiplePackages
365              
366 12     12   5001 use overload ( '<>' => 'readline' );
  12         49  
  12         63  
367              
368             sub new {
369 12     12   291 my ( $class, $range, $template, $line ) = @_;
370 12         25 my $self = bless {}, $class;
371              
372 12         40 my ( $bound, $step ) = split m#[/]#, $range; # initial iterator state
373 12         33 my ( $first, $last ) = split m#[-]#, $bound;
374 12   100     33 $first ||= 0;
375 12   100     32 $last ||= $first;
376 12   100     44 $step ||= 1; # coerce step to match range
377 12 100       40 $step = ( $last < $first ) ? -abs($step) : abs($step);
378 12         62 $self->{count} = int( ( $last - $first ) / $step ) + 1;
379              
380 12         25 for ($template) {
381 12         26 s/\\\$/\\036/g; # disguise escaped dollar
382 12         17 s/\$\$/\\036/g; # disguise escaped dollar
383 12         26 s/^"(.*)"$/$1/s; # unwrap BIND's quoted template
384 12         21 @{$self}{qw(instant step template line)} = ( $first, $step, $_, $line );
  12         49  
385             }
386 12         25 return $self;
387             }
388              
389             sub readline {
390 27     27   39 my $self = shift;
391 27 100       87 return unless $self->{count}-- > 0; # EOF
392              
393 16         27 my $instant = $self->{instant}; # update iterator state
394 16         25 $self->{instant} += $self->{step};
395              
396 16         25 local $_ = $self->{template}; # copy template
397 16         80 while (/\$\{(.*)\}/) { # interpolate ${...}
398 10         52 my $s = _format( $instant, split /\,/, $1 );
399 9         162 s/\$\{$1\}/$s/eg;
  9         50  
400             }
401              
402 15         46 s/\$/$instant/eg; # interpolate $
  6         20  
403 15         31 s/\\036/\$/g; # reinstate escaped $
404 15         49 return $_;
405             }
406              
407             sub close {
408 11     11   15 shift->{count} = 0; # suppress iterator
409 11         14 return 1;
410             }
411              
412             sub input_line_number {
413 12     12   122 return shift->{line}; # fixed: identifies $GENERATE
414             }
415              
416              
417             sub _format { ## convert $GENERATE iteration number to specified format
418 10     10   17 my $number = shift; # per ISC BIND 9.7
419 10   100     29 my $offset = shift || 0;
420 10   100     26 my $length = shift || 0;
421 10   100     26 my $format = shift || 'd';
422              
423 10         14 my $value = $number + $offset;
424 10   100     26 my $digit = $length || 1;
425 10 100       68 return substr sprintf( "%01.$digit$format", $value ), -$length if $format =~ /[doxX]/;
426              
427 3         29 my $nibble = join( '.', split //, sprintf ".%32.32lx", $value );
428 3 100       19 return reverse lc( substr $nibble, -$length ) if $format =~ /[n]/;
429 2 100       11 return reverse uc( substr $nibble, -$length ) if $format =~ /[N]/;
430 1         38 die "unknown $format format";
431             }
432              
433             }
434              
435              
436             sub _generate { ## expand $GENERATE into input stream
437 12     12   26 my ( $self, $range, $template ) = @_;
438              
439 12         21 my $handle = Net::DNS::ZoneFile::Generator->new( $range, $template, $self->line );
440              
441 12         18 delete $self->{latest}; # forget previous owner
442 12         52 $self->{parent} = bless {%$self}, ref($self); # save state, create link
443 12         57 return $self->{filehandle} = $handle;
444             }
445              
446              
447             my $LEX_REGEX = q/("[^"]*"|"[^"]*$)|;[^\n]*|([()])|[ \t\n\r\f]+/;
448              
449             sub _getline { ## get line from current source
450 407     407   533 my $self = shift;
451              
452 407         589 my $fh = $self->{filehandle};
453 407         2636 while (<$fh>) {
454 559 100       2179 next if /^\s*;/; # discard comment line
455 467 100       1633 next unless /\S/; # discard blank line
456              
457 388 100       967 if (/["(]/) {
458 59         147 s/\\\\/\\092/g; # disguise escaped escape
459 59         111 s/\\"/\\034/g; # disguise escaped quote
460 59         113 s/\\\(/\\040/g; # disguise escaped bracket
461 59         108 s/\\\)/\\041/g; # disguise escaped bracket
462 59         102 s/\\;/\\059/g; # disguise escaped semicolon
463 59 100       1226 my @token = grep { defined && length } split /(^\s)|$LEX_REGEX/o;
  959         2285  
464              
465 59         318 while ( $token[-1] =~ /^"[^"]*$/ ) { # multiline quoted string
466 2         12 $_ = pop(@token) . <$fh>; # reparse fragments
467 2         7 s/\\\\/\\092/g; # disguise escaped escape
468 2         4 s/\\"/\\034/g; # disguise escaped quote
469 2         13 s/\\\(/\\040/g; # disguise escaped bracket
470 2         5 s/\\\)/\\041/g; # disguise escaped bracket
471 2         3 s/\\;/\\059/g; # disguise escaped semicolon
472 2 100       65 push @token, grep { defined && length } split /$LEX_REGEX/o;
  11         34  
473 2         13 $_ = join ' ', @token; # reconstitute RR string
474             }
475              
476 59 100       119 if ( grep { $_ eq '(' } @token ) { # concatenate multiline RR
  245         497  
477 24         50 until ( grep { $_ eq ')' } @token ) {
  1891         2799  
478 84         229 $_ = pop(@token) . <$fh>;
479 84         168 s/\\\\/\\092/g; # disguise escaped escape
480 84         107 s/\\"/\\034/g; # disguise escaped quote
481 84         115 s/\\\(/\\040/g; # disguise escaped bracket
482 84         119 s/\\\)/\\041/g; # disguise escaped bracket
483 84         105 s/\\;/\\059/g; # disguise escaped semicolon
484 84 100       892 push @token, grep { defined && length } split /$LEX_REGEX/o;
  1033         2208  
485 84 100       313 chomp $token[-1] unless $token[-1] =~ /^"[^"]*$/;
486             }
487 24         132 $_ = join ' ', @token; # reconstitute RR string
488             }
489             }
490              
491 388 100       1356 return $_ unless /^[\$]/; # RR string
492              
493 36 100       482 my @token = grep { defined && length } split /$LEX_REGEX/o;
  229         662  
494 36 100       173 if (/^\$INCLUDE/) { # directive
    100          
    100          
    100          
495 7         17 my ( $keyword, @argument ) = @token;
496 7 100       25 die '$INCLUDE incomplete' unless @argument;
497 6         16 $fh = $self->_include(@argument);
498              
499             } elsif (/^\$GENERATE/) { # directive
500 13         37 my ( $keyword, $range, @template ) = @token;
501 13 100       40 die '$GENERATE incomplete' unless @template;
502 12         49 $fh = $self->_generate( $range, "@template" );
503              
504             } elsif (/^\$ORIGIN/) { # directive
505 9         20 my ( $keyword, $origin ) = @token;
506 9 100       33 die '$ORIGIN incomplete' unless defined $origin;
507 8         22 $self->_origin($origin);
508              
509             } elsif (/^\$TTL/) { # directive
510 4         9 my ( $keyword, $ttl ) = @token;
511 4 100       19 die '$TTL incomplete' unless defined $ttl;
512 3         17 $self->{TTL} = Net::DNS::RR::ttl( {}, $ttl );
513              
514             } else { # unrecognised
515 3         5 my ($keyword) = @token;
516 3         32 die qq[unknown "$keyword" directive];
517             }
518             }
519              
520 45         204 $self->{eom} = $self->line; # end of file
521 45         899 $fh->close();
522 45   100     842 my $link = $self->{parent} || return; # end of zone
523 14         107 %$self = %$link; # end $INCLUDE
524 14         40 return $self->_getline; # resume input
525             }
526              
527              
528             sub _getRR { ## get RR from current source
529 313     313   489 my $self = shift;
530              
531 313         412 local $_;
532 313 100       545 $self->_getline || return; # line already in $_
533              
534 287         737 my $noname = s/^\s/\@\t/; # placeholder for empty RR name
535              
536             # construct RR object with context specific dynamically scoped $ORIGIN
537 287         463 my $context = $self->{context};
538 287     287   1231 my $rr = &$context( sub { Net::DNS::RR->_new_string($_) } );
  287         826  
539              
540 267         854 my $latest = $self->{latest}; # overwrite placeholder
541 267 100 100     616 $rr->{owner} = $latest->{owner} if $noname && $latest;
542              
543 267 100       575 $self->{class} = $rr->class unless $self->{class}; # propagate RR class
544 267         782 $rr->class( $self->{class} );
545              
546 267 100       596 unless ( defined $self->{TTL} ) {
547 242 100       590 $self->{TTL} = $rr->minimum if $rr->type eq 'SOA'; # default TTL
548             }
549 267 100       614 $rr->{ttl} = $self->{TTL} unless defined $rr->{ttl};
550              
551 267         944 return $self->{latest} = $rr;
552             }
553              
554              
555             sub _include { ## open $INCLUDE file
556 6     6   12 my ( $self, $include, $origin ) = @_;
557              
558 6         10 my $filename = _filename($include);
559 6 100       35 die qq(\$INCLUDE $filename: Unexpected recursion) if $self->{fileopen}->{$filename}++;
560              
561 5         33 my $discipline = PERLIO ? join( ':', '<', PerlIO::get_layers $self->{filehandle} ) : '<';
562 5 100       75 my $filehandle = IO::File->new( $filename, $discipline ) or die qq(\$INCLUDE $filename: $!);
563              
564 4         520 delete $self->{latest}; # forget previous owner
565 4         29 $self->{parent} = bless {%$self}, ref($self); # save state, create link
566 4 100       14 $self->_origin($origin) if $origin;
567 4         8 $self->{filename} = $filename;
568 4         140 return $self->{filehandle} = $filehandle;
569             }
570              
571              
572             sub _origin { ## change $ORIGIN (scope: current file)
573 76     76   161 my ( $self, $name ) = @_;
574 76         189 my $context = $self->{context};
575 76 100       448 $context = Net::DNS::Domain->origin(undef) unless $context;
576 76     76   417 $self->{context} = &$context( sub { Net::DNS::Domain->origin($name) } );
  76         212  
577 76         276 delete $self->{latest}; # forget previous owner
578 76         168 return;
579             }
580              
581              
582             1;
583             __END__