File Coverage

blib/lib/DNS/ZoneSerialNumber.pm
Criterion Covered Total %
statement 153 175 87.4
branch 56 72 77.7
condition 37 54 68.5
subroutine 30 32 93.7
pod 19 19 100.0
total 295 352 83.8


line stmt bran cond sub pod time code
1             package DNS::ZoneSerialNumber;
2              
3 1     1   25963 use 5.006000;
  1         5  
  1         49  
4 1     1   6 use strict;
  1         2  
  1         192  
5              
6             use overload
7 1         20 '>' => \>,
8             '>=' => \>e,
9             '<' => \<,
10             '<=' => \<e,
11             '==' => \&eq,
12             '!=' => \&ne,
13             '<=>' => \&compare,
14             '+=' => \&increment,
15             '-=' => \&decrement,
16             '=' => \&_copy,
17             '+' => \&next,
18             '-' => \&previous,
19             '0+' => \&serial,
20             # Not sure why this is necessary and can't be generated from 0+, but it
21             # it seems to be required by Test::More::is_deeply. It might need to change
22             # some day to return an explicit string rather than relying on Perl to
23             # convert.
24             '""' => \&serial,
25 1     1   2006 ;
  1         1329  
26              
27 1     1   169 use Carp qw/croak/;
  1         3  
  1         99  
28             $Carp::Internal{ ( __PACKAGE__ ) }++;
29              
30 1     1   5 use constant SERIAL_BITS => 32;
  1         2  
  1         102  
31 1     1   5 use constant SERIAL_MAX => ( 2**SERIAL_BITS ) - 1;
  1         2  
  1         61  
32 1     1   5 use constant SERIAL_HALF => 2**( SERIAL_BITS - 1 );
  1         2  
  1         55  
33 1     1   5 use constant INCREMENT_MAX => ( 2**( SERIAL_BITS - 1 ) ) - 1;
  1         1  
  1         2484  
34              
35             our @ISA = qw();
36             our $VERSION = '1.01';
37              
38             =head1 NAME
39              
40             DNS::ZoneSerialNumber - Manipulate DNS zone serial numbers.
41              
42             =head1 SYNOPSIS
43              
44             use DNS::ZoneSerialNumber;
45             my $zsn = DNS::ZoneSerialNumber->new(100);
46             $zsn->increment();
47             print "The new serial number is ", $zsn->serial, "\n";
48              
49             =head1 DESCRIPTION
50              
51             DNS::ZoneSerialNumber encapsulates a DNS zone serial number and provides RFC
52             1982, 1912, and 2136 compliant manipulation, comparison, and validation
53             methods. This module automatically handles serial number overflows, underflows,
54             and invalid comparisons, as well as simple increments and decrements.
55              
56             =head1 METHODS
57              
58             =head2 new
59              
60             Constructor for the DNS::ZoneSerialNumber object. Accepts a single optional
61             parameter, the serial number that the object should represent. If not
62             specified, defaults to 1. If an invalid serial number is specified, the method
63             will L.
64              
65             On success, returns the DNS::ZoneSerialNumber object.
66              
67             =cut
68              
69             sub new {
70 36     36 1 78 my ( $class, $serial ) = @_;
71              
72 36 100       72 if ( defined $serial ) {
73 35 100       81 if ( ref $serial eq 'DNS::ZoneSerialNumber' ) {
74 5         12 $serial = $serial->serial;
75             }
76 35         57 __check_valid_serial_and_croak( $serial );
77             } else {
78 1         3 $serial = 1;
79             }
80              
81 36         97 my $self = { serial => $serial, };
82              
83 36         76 bless $self, $class;
84 36         148 return $self;
85             }
86              
87             sub __check_valid_serial_and_croak {
88 91 100   91   174 if ( !__check_valid_serial( @_ ) ) {
89 2         382 croak 'Invalid serial (must be numeric, positive, non-zero, and <= ' . SERIAL_MAX . ')';
90             }
91             }
92              
93             sub __check_valid_serial {
94 91     91   110 my ( $serial ) = @_;
95 91 100 33     1075 if ( ( !defined $serial )
      66        
      100        
96             || ( $serial !~ /^\d+$/ )
97             || ( $serial > SERIAL_MAX )
98             || ( $serial == 0 ) )
99             {
100 2         7 return 0;
101             }
102 89         286 return 1;
103             }
104              
105             sub __check_valid_increment_and_croak {
106 29     29   39 my ( $serial ) = @_;
107 29 100 33     292 if ( ( !defined $serial )
      66        
108             || ( $serial !~ /^\d+$/ )
109             || ( $serial > INCREMENT_MAX ) )
110             {
111 2         385 croak 'Invalid amount (must be numeric, positive, and <= ' . INCREMENT_MAX . ')';
112             }
113             }
114              
115             sub _compare {
116 38     38   69 my ( $self, $i2, $swapped ) = @_;
117 38         49 my $i1;
118              
119 38 100       106 if ( ref $i2 eq 'DNS::ZoneSerialNumber' ) {
120 2         6 $i2 = $i2->serial;
121             }
122 38         75 __check_valid_serial_and_croak( $i2 );
123              
124 38 50       88 if ( $swapped ) {
125 0         0 $i1 = $i2;
126 0         0 $i2 = $self->{serial};
127             } else {
128 38         86 $i1 = $self->{serial};
129             }
130              
131 38 100       89 if ( $i1 == $i2 ) { return 0; }
  16         41  
132              
133             # Logic taken from RFC 1982 (I know it's not pretty but it's meant to
134             # resemble the RFC).
135 22 100 100     184 if ( ( $i1 < $i2 && $i2 - $i1 < SERIAL_HALF )
      66        
      66        
136             || ( $i1 > $i2 && $i1 - $i2 > SERIAL_HALF ) )
137             {
138 8         26 return -1;
139             }
140              
141 14 100 100     106 if ( ( $i1 < $i2 && $i2 - $i1 > SERIAL_HALF )
      66        
      66        
142             || ( $i1 > $i2 && $i1 - $i2 < SERIAL_HALF ) )
143             {
144 8         31 return 1;
145             }
146             # As per RFC 1982 there are value pairs that can not be logically compared.
147             # They are neither less than, greater than, nor equal to, each other. If we
148             # encounter one of these pairs, simply return undef. <=> returns undef when
149             # comparing against NaN, so returning undef from a compare function is not
150             # completely unheard of.
151 6         14 return undef;
152             }
153              
154             sub _copy {
155 0     0   0 my ( $self ) = @_;
156 0         0 return DNS::ZoneSerialNumber->new( $self->serial );
157             }
158              
159             =head2 valid
160              
161             Accepts a single parameter, the serial number to test for validity.
162              
163             Returns true or false depending representing whether or not the specified
164             serial number represents a valid serial number. Valid serial numbers are
165             positive integers between 1 and SERIAL_MAX (inclusive). See L for
166             details.
167              
168             Note: This method may be called statically or as an instance method.
169              
170             =cut
171              
172             sub valid {
173 0     0 1 0 my ( $self, $serial ) = @_;
174 0 0       0 if ( !ref $self ) {
175 0         0 $serial = $self;
176             }
177 0 0       0 if ( ref $serial eq 'DNS::ZoneSerialNumber' ) {
178 0         0 $serial = $serial->serial;
179             }
180 0         0 return __check_valid_serial( $serial );
181             }
182              
183             =head2 serial
184              
185             Accepts no parameters. Returns the represented serial number as a Perl scalar.
186              
187             Note: In string or numeric context, a DNS::ZoneSerialNumber object will return
188             an appropriate representation of its serial number automatically.
189              
190             =cut
191              
192             sub serial {
193 41     41 1 3202 my ( $self ) = @_;
194 41         291 return $self->{serial};
195             }
196              
197             =head2 set
198              
199             Accepts a single parameter, the new serial number. Returns the
200             DNS::ZoneSerialNumber object with the updated serial number.
201              
202             Sets the serial number represented by the object to the specified serial
203             number. If the specified serial number is invalid the method will L.
204              
205             =cut
206              
207             sub set {
208 13     13 1 1788 my ( $self, $newval ) = @_;
209 13 100       43 if ( ref $newval eq 'DNS::ZoneSerialNumber' ) {
210 1         5 $newval = $newval->serial;
211             }
212 13         49 __check_valid_serial_and_croak( $newval );
213 11         24 $self->{serial} = $newval;
214 11         26 return $self;
215             }
216              
217             =head2 set_from_date
218              
219             Accepts a single optional parameter, the revision count of the new date-based
220             serial number. If an invalid revision count is specified (< 0 or > 99), the
221             method will L. Returns the DNS::ZoneSerialNumber with the updated serial
222             number.
223              
224             Sets the serial number represented by the object to a serial number based on
225             the current date in the format specified by RFC 1912 (YYYYMMDDnn). This format
226             allows for a two-digit revision count (nn) which defaults to "00" unless
227             specified.
228              
229             =cut
230              
231             sub set_from_date {
232 2     2 1 3 my ( $self, $revisions ) = @_;
233 2 100       6 if ( !defined $revisions ) { $revisions = 0; }
  1         2  
234 2 50 33     21 if ( $revisions !~ /^\d+$/ || $revisions > 99 ) {
235 0         0 croak 'Revision count invalid';
236             }
237 2         7 my @time = localtime();
238 2         18 my $new_serial = sprintf( '%04d%02d%02d%02d', $time[5] + 1900, $time[4] + 1, $time[3], $revisions );
239 2         4 return $self->set( $new_serial );
240             }
241              
242             =head2 steps_to_set
243              
244             Accepts a single parameter, the new serial number. If the specified serial
245             number is invalid, the method will L. In array context, returns an
246             in-order array of DNS::ZoneSerialNumber objects representing the serial numbers
247             that must be set in order to safely set the specified serial number. In scalar
248             context the number of required steps is returned.
249              
250             Due to the way RFC 1982 defines serial number comparisons, it is not possible
251             to simply set a zone's serial number to any number considered less than the
252             current serial number. If this is done, DNS servers will assume that the new
253             serial number is older than the prior serial number. In order to set the serial
254             number to a lower value without DNS servers believing the serial number is
255             lower, it must first be set to a higher number (and eventually overflowed) and
256             propagated out. This method generates the list of serial numbers that must be
257             set, in order, to allow a serial number to be set to a lower value without DNS
258             servers believing the serial number is older. On success, this method
259             necessarily returns an array of 1 or 2 elements (or the numbers 1 or 2 in
260             scalar context).
261              
262             If the specified serial number is greater than or equal to the represented
263             serial number, no additional steps are required and an array of a single
264             element (or the number 1 in scalar context) is returned.
265              
266             Please note that because this module always avoids the serial number 0, it may
267             compute a different set of increments to arrive at the specified serial number
268             than other tools.
269              
270             For more information see RFC 1982.
271              
272             =cut
273              
274             sub steps_to_set {
275 4     4 1 8 my ( $self, $serial ) = @_;
276 4         6 my $s = $self->{serial};
277              
278 4 100       11 if ( ref $serial eq 'DNS::ZoneSerialNumber' ) {
279 1         4 $serial = $serial->serial;
280             }
281 4         9 __check_valid_serial_and_croak( $serial );
282              
283 4         10 my $cmp = $self->compare( $serial );
284 4 100       11 if ( $self->lte( $serial ) ) {
285 1 50       4 if ( wantarray ) {
286 0         0 return ( DNS::ZoneSerialNumber->new( $serial ) );
287             }
288 1         6 return 1;
289             }
290              
291 3 100       9 if ( wantarray ) {
292             return (
293 2         7 $self->next( INCREMENT_MAX ), # Returns a DNS::ZoneSerialNumber
294             DNS::ZoneSerialNumber->new( $serial )
295             );
296             }
297 1         5 return 2;
298             }
299              
300             =head2 incomparable
301              
302             Accepts no parameters. Returns a DNS::ZoneSerialNumber object representing the
303             incomparable value for the currently represented serial number.
304              
305             See RFC 1982 for more information about incomparable serial numbers.
306              
307             =cut
308              
309             sub incomparable {
310 4     4 1 7 my ( $self ) = @_;
311 4         14 my $r = DNS::ZoneSerialNumber->new( $self );
312             # Work around increment limits.
313 4         12 $r->increment( SERIAL_HALF - 1 );
314 4         10 $r->increment();
315 4         29 return $r;
316             }
317              
318             =head2 is_incomparable
319              
320             Accepts a single parameter, the serial number against which the represented
321             serial number should be checked for incomparability. If the specified serial
322             number is invalid, the method will L.
323              
324             Returns true if the serial numbers are incomparable or false otherwise.
325              
326             See RFC 1982 for more information about incomparable serial numbers.
327              
328             =cut
329              
330             sub is_incomparable {
331 1     1 1 3 my ( $self, $serial ) = @_;
332 1 50       13 if ( ref $serial eq 'DNS::ZoneSerialNumber' ) {
333 0         0 $serial = $serial->serial;
334             }
335 1         3 __check_valid_serial_and_croak( $serial );
336 1 50       4 if ( $self->incomparable()->eq( $serial ) ) {
337 1         5 return 1;
338             }
339 0         0 return 0;
340             }
341              
342             =head2 next
343              
344             Accepts a single optional parameter, the amount to increment by (n). If no
345             parameter is specified, the amount defaults to 1. If an invalid amount is
346             specified, the method will L. Please see L for details.
347             Returns the next nth serial number in sequence as a DNS::ZoneSerialNumber
348             object. The currently represented serial number is unchanged.
349              
350             If the serial number overflows the serial maximum it will automatically roll
351             over through the serial minimum.
352              
353             This method is also available as the overloaded operator "+". Please note that
354             the protections against invalid increments can be circumvented via compound
355             addition using the overloaded methods. For example, the following will succeed
356             even though it results in an invalid increment due to the fact the addition
357             was done in multiple steps:
358              
359             my $new_zsn = $zsn + DNS::ZoneSerialNumber::INCREMENT_MAX + 1;
360              
361             However, the following will (correctly) generate an error:
362              
363             my $new_zsn = $zsn + ( DNS::ZoneSerialNumber::INCREMENT_MAX + 1 );
364              
365             =cut
366              
367             sub next {
368 24     24 1 46 my ( $self, $amount ) = @_;
369 24         42 my $s = $self->{serial};
370              
371 24 100       59 if ( !defined $amount ) { $amount = 1; }
  6         632  
372 24 50       54 if ( ref $amount eq 'DNS::ZoneSerialNumber' ) {
373 0         0 $amount = $amount->serial;
374             }
375 24         67 __check_valid_increment_and_croak( $amount );
376              
377 22         30 $s += $amount;
378 22 100       44 if ( $s > SERIAL_MAX ) {
379             # The off-by-one here is intentional to skip the serial number 0.
380 2         3 $s -= SERIAL_MAX;
381             }
382 22 50       44 if ( $s == 0 ) {
383 0         0 $s = 1;
384             }
385 22         56 return DNS::ZoneSerialNumber->new( $s );
386             }
387              
388             =head2 previous
389              
390             Accepts a single optional parameter, the amount to decrement by (n). If no
391             parameter is specified, the amount defaults to 1. If an invalid amount is
392             specified, the method will L. Please see L for details.
393             Returns the prior nth serial number in sequence as a DNS::ZoneSerialNumber
394             object. The currently represented serial number is unchanged.
395              
396             If the serial number underflows the serial minimum it will automatically roll
397             over through the serial maximum.
398              
399             This method is also available as the overloaded operator "-".
400              
401             =cut
402              
403             sub previous {
404 5     5 1 496 my ( $self, $amount, $swapped ) = @_;
405 5         5 my $s;
406              
407 5 100       15 if ( !defined $amount ) { $amount = 1; }
  1         1  
408 5 100       16 if ( ref $amount eq 'DNS::ZoneSerialNumber' ) {
409 3         9 $amount = $amount->serial;
410             }
411 5         13 __check_valid_increment_and_croak( $amount );
412              
413 5 50       11 if ( $swapped ) {
414 0         0 $s = $amount;
415 0         0 $amount = $self->{serial};
416             } else {
417 5         10 $s = $self->{serial};
418             }
419              
420 5         8 $s -= $amount;
421 5 100       10 if ( $s < 1 ) {
422             # The off-by-one here is intentional to skip the serial number 0.
423 2         4 $s += SERIAL_MAX;
424             }
425 5 50       13 if ( $s == 0 ) {
426 0         0 $s = SERIAL_MAX;
427             }
428 5         14 return DNS::ZoneSerialNumber->new( $s );
429             }
430              
431             =head2 increment
432              
433             Accepts a single optional parameter, the amount to increment by (n). If no
434             parameter is specified, the amount defaults to 1. If an invalid amount is
435             specified, the method will L. Please see L for details. Sets
436             the currently represented serial number to the nth next serial number in
437             sequence and returns the DNS::ZoneSerialNumber object with the updated value.
438              
439             If the serial number overflows the serial maximum it will automatically roll
440             over through the serial minimum.
441              
442             This method is also available as the overloaded operator "++".
443              
444             =cut
445              
446             sub increment {
447 13     13 1 30 my ( $self, $amount ) = @_;
448              
449 13         35 $self->{serial} = $self->next( $amount )->serial;
450 12         34 return $self;
451             }
452              
453             =head2 decrement
454              
455             Accepts a single optional parameter, the amount to decrement by (n). If no
456             parameter is specified, the amount defaults to 1. If an invalid amount is
457             specified, the method will L. Please see L for details. Sets
458             the currently represented serial number to the nth prior serial number in
459             sequence and returns the DNS::ZoneSerialNumber object with the updated value.
460              
461             If the serial number underflows the serial minimum it will automatically roll
462             over through the serial maximum.
463              
464             This method is also available as the overloaded operator "--".
465              
466             =cut
467              
468             sub decrement {
469 2     2 1 4 my ( $self, $amount ) = @_;
470              
471 2         5 $self->{serial} = $self->previous( $amount )->serial;
472 2         6 return $self;
473             }
474              
475             =head2 compare
476              
477             Accepts a single parameter, the serial number to be compared against the one
478             represented by the DNS::ZoneSerialNumber object. If the supplied serial number
479             is invalid, the method will L.
480              
481             This method's behavior is the same as the L<<=>> operator, however in the case
482             of incomparable numbers undef is returned. This method is also available as the
483             overloaded operator "<=>".
484              
485             =cut
486              
487             sub compare {
488 9     9 1 22 my ( $self, $i2, $swapped ) = @_;
489 9         28 return $self->_compare( $i2, $swapped );
490             }
491              
492             =head2 Overloaded Comparison Methods
493              
494             All of the following methods accept a single argument, the serial number to be
495             compared against the one represented by the DNS::ZoneSerialNumber object. If
496             the supplied serial number is invalid, the method will L.
497              
498             Each method true or false as a result of the comparison. In the case of
499             incomparable numbers, false is returned by all methods except L. All
500             of the following methods are also available as overloaded comparison operators.
501              
502             The comparison is performed with the encapsulated serial number treated as the
503             left operand. For example:
504              
505             $zsn->gt(100)
506              
507             Is the equivalent of writing:
508              
509             $zsn > 100
510              
511             The following methods are available:
512              
513             =head3 gt (>)
514              
515             =head3 gte (>=)
516              
517             =head3 lt (<)
518              
519             =head3 lte (<=)
520              
521             =head3 eq (==)
522              
523             =head3 ne (!=)
524              
525             =cut
526              
527             sub gt {
528 3     3 1 9 my ( $self, $i2, $swapped ) = @_;
529 3         14 my $r = $self->_compare( $i2, $swapped );
530 3 100 100     21 if ( defined $r && $r == 1 ) { return 1; }
  1         7  
531 2         11 return 0;
532             }
533              
534             sub gte {
535 1     1 1 5 my ( $self, $i2, $swapped ) = @_;
536 1         5 my $r = $self->_compare( $i2, $swapped );
537 1 50 33     14 if ( defined $r && $r >= 0 ) { return 1; }
  1         8  
538 0         0 return 0;
539             }
540              
541             sub lt {
542 1     1 1 3 my ( $self, $i2, $swapped ) = @_;
543 1         5 my $r = $self->_compare( $i2, $swapped );
544 1 50 33     9 if ( defined $r && $r == -1 ) { return 1; }
  1         5  
545 0         0 return 0;
546             }
547              
548             sub lte {
549 6     6 1 18 my ( $self, $i2, $swapped ) = @_;
550 6         17 my $r = $self->_compare( $i2, $swapped );
551 6 100 100     34 if ( defined $r && $r <= 0 ) { return 1; }
  2         12  
552 4         71 return 0;
553             }
554              
555             sub eq {
556 16     16 1 1739 my ( $self, $i2, $swapped ) = @_;
557 16         42 my $r = $self->_compare( $i2, $swapped );
558 16 100 100     75 if ( defined $r && $r == 0 ) { return 1; }
  14         932  
559 2         14 return 0;
560             }
561              
562             sub ne {
563 2     2 1 5 my ( $self, $i2, $swapped ) = @_;
564 2         7 my $r = $self->_compare( $i2, $swapped );
565 2 50 66     20 if ( !defined $r || $r != 0 ) { return 1; }
  2         13  
566 0           return 0;
567             }
568              
569             1;
570              
571             =head1 CONSTANTS
572              
573             DNS::ZoneSerialNumber contains the following internal constants representing
574             definitions and rules used by DNS::ZoneSerialNumber and RFC 1982. These
575             constants are not exported but are available if accessed via the full
576             namespace (eg, DNS::ZoneSerialNumber::SERIAL_BITS).
577              
578             =head2 SERIAL_BITS
579              
580             The number of bits used to represent a DNS zone serial number. Set at 32.
581              
582             =head2 SERIAL_MAX
583              
584             The maximum value a serial number of SERIAL_BITS size can store. Computed as:
585              
586             ( 2 ** SERIAL_MAX ) - 1
587              
588             =head2 SERIAL_HALF
589              
590             Approximately half the serial maximum value as used in RFC 1982 equality
591             calculations. This value is used in serial number comparisons and in
592             calculating incomparable serial numbers. Computed as:
593              
594             2 ** ( SERIAL_BITS - 1 )
595              
596             =head2 INCREMENT_MAX
597              
598             The maximum amount by which a serial number can be incremented in a single
599             step. If incremented by more than this amount, the serial number would appear
600             to have gone "backwards", see RFC 1982 for details. Computed as:
601              
602             ( 2 ** ( SERIAL_BITS - 1 ) ) - 1
603              
604             =head1 SERIAL NUMBER 0
605              
606             As per RFC 2136, the serial number 0 is not used and is skipped for all
607             additive and subtractive calculations. For example, if a DNS::ZoneSerialNumber
608             object representing the serial number SERIAL_MAX is then incremented by 1, the
609             new serial number will be set to 1 rather than 0.
610              
611             Comparisons will still take serial number 0 into account as expected.
612              
613             =head1 INVALID COMPARISONS
614              
615             The serial number logic provided by RFC 1982 defines two serial numbers with a
616             difference of ( 2 ** SERIAL_MAX ) to be considered neither greater than, less
617             than, nor equal to each other. The RFC provides no recommendations on how to
618             handle comparisons of these numbers and suggests they not be compared directly
619             or used together in the same environment. DNS::ZoneSerialNumber attempts to
620             compare these serial numbers using the same logic that BIND uses: all
621             comparison methods will return false for any comparison of these serial number
622             pairs, except for L which returns true, and L (<=>) which
623             returns undef.
624              
625             =head1 OPERATOR OVERLOADING
626              
627             DNS::ZoneSerialNumber provides overloaded operators for many of its provided
628             methods. The author is currently unsure as to whether or not this is a good
629             idea. If it proves to be problematic, the overloads may be removed (or made
630             optional) in a future version.
631              
632             =head1 CHANGES
633              
634             =head2 1.01 - 20120120, jeagle
635              
636             Minor documentation updates.
637              
638             =head2 1.00 - 20120118, jeagle
639              
640             Initial release to CPAN.
641              
642             =head1 SEE ALSO
643              
644             DNS::ZoneParse, Net::DNS, RFC 1982, RFC 1912, RFC 2136
645              
646             =head1 AUTHOR
647              
648             John Eaglesham
649              
650             =head1 COPYRIGHT AND LICENSE
651              
652             Copyright (C) 2011-2012 by John Eaglesham
653              
654             This library is free software; you can redistribute it and/or modify
655             it under the same terms as Perl itself, either Perl version 5.6.0 or,
656             at your option, any later version of Perl 5 you may have available.
657              
658             =cut
659