File Coverage

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


line stmt bran cond sub pod time code
1             package Net::DNS::ZoneFile;
2              
3 12     12   546784 use strict;
  12         28  
  12         572  
4 12     12   70 use warnings;
  12         24  
  12         1345  
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   765 use integer;
  12         48  
  12         81  
46 12     12   395 use Carp;
  12         28  
  12         1184  
47              
48 12     12   106 use base qw(Exporter);
  12         50  
  12         3040  
49             our @EXPORT = qw(parse read readfh);
50              
51 12         26 use constant UTF8 => scalar eval { ## not UTF-EBCDIC [see Unicode TR#16 3.6]
52 12         47 require Encode;
53 12         18941 Encode::encode_utf8( chr(182) ) eq pack( 'H*', 'C2B6' );
54 12     12   107 };
  12         22  
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 268440 my ( $class, $filename, $origin ) = @_;
91 67         675 my $self = bless {fileopen => {}}, $class;
92              
93 67         278 $self->_origin($origin);
94              
95 67 100       205 if ( ref($filename) ) {
96 16         51 $self->{filehandle} = $self->{filename} = $filename;
97 16 100       154 return $self if ref($filename) =~ /IO::File|FileHandle|GLOB|Text/;
98 1         147 croak 'argument not a file handle';
99             }
100              
101 51 100       391 croak 'filename argument undefined' unless $filename;
102 50         416 my $discipline = UTF8 ? '<:encoding(UTF-8)' : '<';
103 50 100       296 $self->{filehandle} = IO::File->new( $filename, $discipline ) or croak "$filename: $!";
104 46         11760 $self->{fileopen}->{$filename}++;
105 46         120 $self->{filename} = $filename;
106 46         261 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 5127 my ($self) = @_;
131              
132 101 100       285 return &_read unless ref $self; # compatibility interface
133              
134 96 100       225 if (wantarray) {
135 9         20 my @zone; # return entire zone
136 9         19 eval {
137 9         61 local $SIG{__DIE__};
138 9         40 while ( my $rr = $self->_getRR ) {
139 183         625 push( @zone, $rr );
140             }
141             };
142 9 100       38 croak join ' ', $@, ' file', $self->name, 'line', $self->line, "\n " if $@;
143 8         64 return @zone;
144             }
145              
146 87         181 my $rr = eval {
147 87         315 local $SIG{__DIE__};
148 87         268 $self->_getRR; # return single RR
149             };
150 87 100       392 croak join ' ', $@, ' file', $self->name, 'line', $self->line, "\n " if $@;
151 61         208 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 1459 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 1207 my $self = shift;
180 91 100       304 return $self->{eom} if defined $self->{eom};
181 89         459 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 481 my $context = shift->{context};
196 5     5   30 return &$context( sub { Net::DNS::Domain->new('@') } )->string;
  5         34  
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   27 my $name = shift;
261 18 100       54 return $name if ref($name); ## file handle
262 11 100       73 return $name unless $include_dir;
263 3         16 require File::Spec;
264 3 100       35 return $name if File::Spec->file_name_is_absolute($name);
265 2 100       39 return $name if -f $name; ## file in current directory
266 1         23 return File::Spec->catfile( $include_dir, $name );
267             }
268              
269              
270             sub _read {
271 12     12   20 my ($arg1) = @_;
272 12 100 100     51 shift if !ref($arg1) && $arg1 eq __PACKAGE__;
273 12         20 my $filename = shift;
274 12         18 local $include_dir = shift;
275              
276 12         26 my $zonefile = Net::DNS::ZoneFile->new( _filename($filename) );
277 10         17 my @zone;
278 10         16 eval {
279 10         37 local $SIG{__DIE__};
280 10         13 my $rr;
281 10         27 push( @zone, $rr ) while $rr = $zonefile->_getRR;
282             };
283 10 100       152 return wantarray ? @zone : \@zone unless $@;
    100          
284 2         281 carp $@;
285 2 100       20 return wantarray ? @zone : undef;
286             }
287              
288              
289             {
290              
291             package Net::DNS::ZoneFile::Text; ## no critic ProhibitMultiplePackages
292              
293 12     12   2594 use overload ( '<>' => 'readline' );
  12         6980  
  12         157  
294              
295             sub new {
296 7     7   15 my ( $class, $data ) = @_;
297 7         19 my $self = bless {}, $class;
298 7 100       154 $self->{data} = [split /\n/, ref($data) ? $$data : $data];
299 7         45 return $self;
300             }
301              
302             sub readline {
303 40     40   60 my $self = shift;
304 40         104 $self->{line}++;
305 40         50 return shift( @{$self->{data}} );
  40         209  
306             }
307              
308             sub close {
309 5     5   15 shift->{data} = [];
310 5         10 return 1;
311             }
312              
313             sub input_line_number {
314 5     5   18 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 16 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 5237 my ($arg1) = @_;
351 7 100       30 shift if $arg1 eq __PACKAGE__;
352 7         13 my $string = shift;
353 7         15 my @include = grep {defined} shift;
  7         21  
354 7         34 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   6344 use overload ( '<>' => 'readline' );
  12         29  
  12         62  
366              
367             sub new {
368 12     12   316 my ( $class, $range, $template, $line ) = @_;
369 12         24 my $self = bless {}, $class;
370              
371 12         45 my ( $bound, $step ) = split m#[/]#, $range; # initial iterator state
372 12         35 my ( $first, $last ) = split m#[-]#, $bound;
373 12   100     33 $first ||= 0;
374 12   100     34 $last ||= $first;
375 12   100     52 $step ||= 1; # coerce step to match range
376 12 100       33 $step = ( $last < $first ) ? -abs($step) : abs($step);
377 12         68 $self->{count} = int( ( $last - $first ) / $step ) + 1;
378              
379 12         25 for ($template) {
380 12         25 s/\\\$/\\036/g; # disguise escaped dollar
381 12         18 s/\$\$/\\036/g; # disguise escaped dollar
382 12         31 s/^"(.*)"$/$1/s; # unwrap BIND's quoted template
383 12         21 @{$self}{qw(instant step template line)} = ( $first, $step, $_, $line );
  12         54  
384             }
385 12         24 return $self;
386             }
387              
388             sub readline {
389 27     27   33 my $self = shift;
390 27 100       86 return unless $self->{count}-- > 0; # EOF
391              
392 16         49 my $instant = $self->{instant}; # update iterator state
393 16         31 $self->{instant} += $self->{step};
394              
395 16         40 local $_ = $self->{template}; # copy template
396 16         84 while (/\$\{(.*)\}/) { # interpolate ${...}
397 10         42 my $s = _format( $instant, split /\,/, $1 );
398 9         187 s/\$\{$1\}/$s/eg;
  9         40  
399             }
400              
401 15         42 s/\$/$instant/eg; # interpolate $
  6         16  
402 15         110 s/\\036/\$/g; # reinstate escaped $
403 15         44 return $_;
404             }
405              
406             sub close {
407 11     11   19 shift->{count} = 0; # suppress iterator
408 11         15 return 1;
409             }
410              
411             sub input_line_number {
412 12     12   216 return shift->{line}; # fixed: identifies $GENERATE
413             }
414              
415              
416             sub _format { ## convert $GENERATE iteration number to specified format
417 10     10   13 my $number = shift; # per ISC BIND 9.7
418 10   100     27 my $offset = shift || 0;
419 10   100     22 my $length = shift || 0;
420 10   100     23 my $format = shift || 'd';
421              
422 10         14 my $value = $number + $offset;
423 10   100     21 my $digit = $length || 1;
424 10 100       66 return substr sprintf( "%01.$digit$format", $value ), -$length if $format =~ /[doxX]/;
425              
426 3         26 my $nibble = join( '.', split //, sprintf ".%32.32lx", $value );
427 3 100       20 return reverse lc( substr $nibble, -$length ) if $format =~ /[n]/;
428 2 100       10 return reverse uc( substr $nibble, -$length ) if $format =~ /[N]/;
429 1         17 die "unknown $format format";
430             }
431              
432             }
433              
434              
435             sub _generate { ## expand $GENERATE into input stream
436 12     12   29 my ( $self, $range, $template ) = @_;
437              
438 12         27 my $handle = Net::DNS::ZoneFile::Generator->new( $range, $template, $self->line );
439              
440 12         59 $self->{parent} = bless {%$self}, ref($self); # save state, create link
441 12         87 delete $self->{latest}; # forget current domain name
442 12         63 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   566 my $self = shift;
450              
451 407         729 my $fh = $self->{filehandle};
452 407         2971 while (<$fh>) {
453 555 100       2275 next if /^\s*;/; # discard comment line
454 467 100       1712 next unless /\S/; # discard blank line
455              
456 388 100       1021 if (/["(]/) {
457 59         126 s/\\\\/\\092/g; # disguise escaped escape
458 59         99 s/\\"/\\034/g; # disguise escaped quote
459 59         112 s/\\\(/\\040/g; # disguise escaped bracket
460 59         103 s/\\\)/\\041/g; # disguise escaped bracket
461 59         95 s/\\;/\\059/g; # disguise escaped semicolon
462 59 100       1498 my @token = grep { defined && length } split /(^\s)|$LEX_REGEX/o;
  959         2352  
463              
464 59         324 while ( $token[-1] =~ /^"[^"]*$/ ) { # multiline quoted string
465 2         6 $_ = pop(@token) . <$fh>; # reparse fragments
466 2         6 s/\\\\/\\092/g; # disguise escaped escape
467 2         4 s/\\"/\\034/g; # disguise escaped quote
468 2         2 s/\\\(/\\040/g; # disguise escaped bracket
469 2         3 s/\\\)/\\041/g; # disguise escaped bracket
470 2         4 s/\\;/\\059/g; # disguise escaped semicolon
471 2 100       62 push @token, grep { defined && length } split /$LEX_REGEX/o;
  11         24  
472 2         10 $_ = join ' ', @token; # reconstitute RR string
473             }
474              
475 59 100       135 if ( grep { $_ eq '(' } @token ) { # concatenate multiline RR
  245         499  
476 24         45 until ( grep { $_ eq ')' } @token ) {
  1891         2936  
477 84         195 $_ = pop(@token) . <$fh>;
478 84         146 s/\\\\/\\092/g; # disguise escaped escape
479 84         111 s/\\"/\\034/g; # disguise escaped quote
480 84         118 s/\\\(/\\040/g; # disguise escaped bracket
481 84         117 s/\\\)/\\041/g; # disguise escaped bracket
482 84         109 s/\\;/\\059/g; # disguise escaped semicolon
483 84 100       1094 push @token, grep { defined && length } split /$LEX_REGEX/o;
  1033         2224  
484 84 100       245 chomp $token[-1] unless $token[-1] =~ /^"[^"]*$/;
485             }
486 24         156 $_ = join ' ', @token; # reconstitute RR string
487             }
488             }
489              
490 388 100       1368 return $_ unless /^[\$]/; # RR string
491              
492 36 100       612 my @token = grep { defined && length } split /$LEX_REGEX/o;
  229         642  
493 36 100       250 if (/^\$INCLUDE/) { # directive
    100          
    100          
    100          
494 7         23 my ( $keyword, @argument ) = @token;
495 7 100       31 die '$INCLUDE incomplete' unless @argument;
496 6         23 $fh = $self->_include(@argument);
497              
498             } elsif (/^\$GENERATE/) { # directive
499 13         77 my ( $keyword, $range, @template ) = @token;
500 13 100       44 die '$GENERATE incomplete' unless @template;
501 12         54 $fh = $self->_generate( $range, "@template" );
502              
503             } elsif (/^\$ORIGIN/) { # directive
504 9         92 my ( $keyword, $origin ) = @token;
505 9 100       43 die '$ORIGIN incomplete' unless defined $origin;
506 8         25 $self->_origin($origin);
507              
508             } elsif (/^\$TTL/) { # directive
509 4         31 my ( $keyword, $ttl ) = @token;
510 4 100       29 die '$TTL incomplete' unless defined $ttl;
511 3         18 $self->{TTL} = Net::DNS::RR::ttl( {}, $ttl );
512              
513             } else { # unrecognised
514 3         7 my ($keyword) = @token;
515 3         36 die qq[unknown "$keyword" directive];
516             }
517             }
518              
519 45         170 $self->{eom} = $self->line; # end of file
520 45         934 $fh->close();
521 45   100     908 my $link = $self->{parent} || return; # end of zone
522 14         120 %$self = %$link; # end $INCLUDE
523 14         40 return $self->_getline; # resume input
524             }
525              
526              
527             sub _getRR { ## get RR from current source
528 313     313   491 my $self = shift;
529              
530 313         488 local $_;
531 313 100       604 $self->_getline || return; # line already in $_
532              
533 287         861 my $noname = s/^\s/\@\t/; # placeholder for empty RR name
534              
535             # construct RR object with context specific dynamically scoped $ORIGIN
536 287         463 my $context = $self->{context};
537 287     287   1333 my $rr = &$context( sub { Net::DNS::RR->_new_string($_) } );
  287         951  
538              
539 268         1109 my $latest = $self->{latest}; # overwrite placeholder
540 268 100 100     691 $rr->{owner} = $latest->{owner} if $noname && $latest;
541              
542 268 100       665 $self->{class} = $rr->class unless $self->{class}; # propagate RR class
543 268         800 $rr->class( $self->{class} );
544              
545 268 100       607 unless ( defined $self->{TTL} ) {
546 243 100       617 $self->{TTL} = $rr->minimum if $rr->type eq 'SOA'; # default TTL
547             }
548 268 100       797 $rr->{ttl} = $self->{TTL} unless defined $rr->{ttl};
549              
550 268         1087 return $self->{latest} = $rr;
551             }
552              
553              
554             sub _include { ## open $INCLUDE file
555 6     6   17 my ( $self, $include, $origin ) = @_;
556              
557 6         19 my $filename = _filename($include);
558 6 100       44 die qq(\$INCLUDE $filename: Unexpected recursion) if $self->{fileopen}->{$filename}++;
559              
560 5         46 my $discipline = join( ':', '<', PerlIO::get_layers $self->{filehandle} );
561 5 100       88 my $filehandle = IO::File->new( $filename, $discipline ) or die qq(\$INCLUDE $filename: $!);
562              
563 4         522 $self->{parent} = bless {%$self}, ref($self); # save state, create link
564 4         10 delete $self->{latest}; # forget current domain name
565 4 100       14 $self->_origin($origin) if $origin;
566 4         8 $self->{filename} = $filename;
567 4         196 return $self->{filehandle} = $filehandle;
568             }
569              
570              
571             sub _origin { ## change $ORIGIN (scope: current file)
572 76     76   191 my ( $self, $name ) = @_;
573 76         228 my $context = $self->{context};
574 76 100       578 $context = Net::DNS::Domain->origin(undef) unless $context;
575 76     76   464 $self->{context} = &$context( sub { Net::DNS::Domain->origin($name) } );
  76         208  
576 76         304 delete $self->{latest}; # forget previous owner
577 76         167 return;
578             }
579              
580              
581             1;
582             __END__