File Coverage

blib/lib/TinyDNS/Reader/Merged.pm
Criterion Covered Total %
statement 38 40 95.0
branch 7 10 70.0
condition 2 6 33.3
subroutine 5 5 100.0
pod 0 2 0.0
total 52 63 82.5


line stmt bran cond sub pod time code
1              
2             =head1 NAME
3            
4             TinyDNS::Reader::Merged - Read a TinyDNS file and merge records.
5            
6             =head1 DESCRIPTION
7            
8             The L<TinyDNS::Reader> module will allow you to parse a TinyDNS zonefile,
9             into a number of distinct records, one for each line.
10            
11             However there is no handling the case where you'd expect a single DNS
12             value to have multiple values.
13            
14             For example if you were to parse this file you would get two records:
15            
16             =for example begin
17            
18             db.example.com:1.2.3.4:300
19             db.example.com:10.20.30.40:300
20            
21             =for example end
22            
23             On a purely record-by-record basis that is as-expected, however when you
24             actually come to manipulate your DNS records you'd expect to have a single
25             logical object with values something like this:
26            
27             =for example begin
28            
29             {
30             'ttl' => '300',
31             'value' => [
32             '1.2.3.4',
33             '10.20.30.40'
34             ],
35             'name' => 'db.example.com',
36             'type' => 'A'
37             },
38            
39             =for example end
40            
41             This module takes care of that for you, by merging records which consist
42             of identical "name" + "type" pairs.
43            
44             Use it as a drop-in replacing for L<TinyDNS::Reader>.
45            
46             =cut
47              
48             =head1 AUTHOR
49            
50             Steve Kemp <steve@steve.org.uk>
51            
52             =cut
53              
54             =head1 COPYRIGHT AND LICENSE
55            
56             Copyright (C) 2014 Steve Kemp <steve@steve.org.uk>.
57            
58             This code was developed for an online Git-based DNS hosting solution,
59             which can be found at:
60            
61             =over 8
62            
63             =item *
64             https://dns-api.com/
65            
66             =back
67            
68             This library is free software. You can modify and or distribute it under
69             the same terms as Perl itself.
70            
71             =cut
72              
73              
74             package TinyDNS::Reader::Merged;
75              
76 2     2   22593 use TinyDNS::Reader;
  2         5  
  2         51  
77              
78 2     2   23 use strict;
  2         4  
  2         67  
79 2     2   8 use warnings;
  2         4  
  2         804  
80              
81              
82             =begin doc
83            
84             Constructor.
85            
86             This module expects to be given a file parameter, pointing to a
87             file which can be parsed.
88            
89             =end doc
90            
91             =cut
92              
93             sub new
94             {
95 4     4 0 9710     my ( $proto, %supplied ) = (@_);
96 4   33     27     my $class = ref($proto) || $proto;
97              
98 4         7     my $self = {};
99 4         12     bless( $self, $class );
100              
101             #
102             # Create our child object.
103             #
104 4 50       19     if ( $supplied{ 'file' } )
    50          
105                 {
106 0         0         $self->{ 'obj' } = TinyDNS::Reader->new( file => $supplied{ 'file' } );
107                 }
108                 elsif ( $supplied{ 'text' } )
109                 {
110 4         20         $self->{ 'obj' } = TinyDNS::Reader->new( text => $supplied{ 'text' } );
111                 }
112                 else
113                 {
114 0         0         die "Missing 'text' or 'file' argument";
115                 }
116 4         12     return $self;
117             }
118              
119              
120             =begin doc
121            
122             Parse the records and return a merged set.
123            
124             The parsing is delegated to L<TinyDNS::Reader>, so all supported record-types
125             work as expected.
126            
127             =end doc
128            
129             =cut
130              
131             sub parse
132             {
133 4     4 0 1818     my ($self) = (@_);
134              
135 4         19     my $records = $self->{ 'obj' }->parse();
136              
137              
138             #
139             # Process each entry
140             #
141 4         6     my $res;
142              
143 4         8     my %seen = ();
144              
145 4         7     foreach my $r (@$records)
146                 {
147 5         14         my $name = $r->name();
148 5         11         my $type = $r->type();
149 5         14         my $val = $r->value();
150 5         12         my $ttl = $r->ttl();
151 5         14         my $hash = $r->hash();
152              
153             # skip if we've seen this name+type pair before.
154 5 100       56         next if ( $seen{ $name }{ $type } );
155              
156             #
157             # Look for other values with the same type.
158             #
159             # NOTE: O(N^2) - needs improvement.
160             #
161 4         8         foreach my $x (@$records)
162                     {
163 5         14             my $name2 = $x->name();
164 5         13             my $type2 = $x->type();
165 5         12             my $val2 = $x->value();
166 5         12             my $hash2 = $x->hash();
167              
168 5 100       19             next if ( $hash eq $hash2 );
169              
170             #
171             # If this record has the same name/type as the
172             # previous one then merge in the new value.
173             #
174             # NOTE: This means the TTL comes from the first
175             # of the records. Which is fine.
176             #
177 1 50 33     8             if ( ( $name eq $name2 ) &&
178                              ( $type eq $type2 ) )
179                         {
180 1         5                 $r->add($val2);
181                         }
182                     }
183              
184 4         12         push( @$res,
185                           { name => $name,
186                              value => $r->value(),
187                              ttl => $ttl,
188                              type => $type
189                           } );
190              
191              
192             #
193             # We've seen this name/type pair now.
194             #
195 4         16         $seen{ $name }{ $type } += 1;
196                 }
197              
198             #
199             # Return the merged/updated results.
200             #
201 4         28     return ($res);
202             }
203              
204             1;
205