File Coverage

blib/lib/Net/DNS/ZoneFile.pm
Criterion Covered Total %
statement 240 240 100.0
branch 104 104 100.0
path n/a
condition 23 23 100.0
subroutine 35 35 100.0
pod 8 8 100.0
total 410 410 100.0


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