File Coverage

blib/lib/HTTP/Date.pm
Criterion Covered Total %
statement 72 72 100.0
branch 56 62 90.3
condition 25 26 96.1
subroutine 6 6 100.0
pod 5 5 100.0
total 164 171 95.9


line stmt bran cond sub pod time code
1             package HTTP::Date;
2              
3 5     5   591593 use strict;
  5         9  
  5         9928  
4              
5             our $VERSION = '6.08';
6              
7             require Exporter;
8             our @ISA = qw(Exporter);
9             our @EXPORT = qw(time2str str2time);
10             our @EXPORT_OK = qw(parse_date time2iso time2isoz);
11              
12             require Time::Local;
13              
14             our ( @DoW, @MoY, %MoY );
15             @DoW = qw(Sun Mon Tue Wed Thu Fri Sat);
16             @MoY = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);
17             @MoY{@MoY} = ( 1 .. 12 );
18              
19             my %GMT_ZONE = ( GMT => 1, UTC => 1, UT => 1, Z => 1 );
20              
21             sub time2str (;$) {
22 1     1 1 2200 my $time = shift;
23 1 50       4 $time = time unless defined $time;
24 1         5 my ( $sec, $min, $hour, $mday, $mon, $year, $wday ) = gmtime($time);
25 1         10 sprintf(
26             "%s, %02d %s %04d %02d:%02d:%02d GMT",
27             $DoW[$wday],
28             $mday, $MoY[$mon], $year + 1900,
29             $hour, $min, $sec
30             );
31             }
32              
33             sub str2time ($;$) {
34 142     142 1 979957 my $str = shift;
35 142 100       266 return undef unless defined $str;
36              
37             # fast exit for strictly conforming string
38 141 100       286 if ( $str
39             =~ /^[SMTWF][a-z][a-z], ([0-9][0-9]) ([JFMAJSOND][a-z][a-z]) ([0-9][0-9][0-9][0-9]) ([0-9][0-9]):([0-9][0-9]):([0-9][0-9]) GMT$/
40             ) {
41 3         6 return eval {
42 3         24 my $t = Time::Local::timegm( $6, $5, $4, $1, $MoY{$2} - 1, $3 );
43 3 100       118 $t < 0 ? undef : $t;
44             };
45             }
46              
47 138         227 my @d = parse_date($str);
48 138 100       213 return undef unless @d;
49 126         119 $d[1]--; # month
50              
51 126         142 my $tz = pop(@d);
52 126 100       165 unless ( defined $tz ) {
53 63 100       99 unless ( defined( $tz = shift ) ) {
54 20         24 return eval {
55 20         21 my $frac = $d[-1];
56 20         49 $frac -= ( $d[-1] = int($frac) );
57 20         49 my $t = Time::Local::timelocal( reverse @d ) + $frac;
58 15 100       840 $t < 0 ? undef : $t;
59             };
60             }
61             }
62              
63 106         99 my $offset = 0;
64 106 100       289 if ( $GMT_ZONE{ uc $tz } ) {
    100          
65              
66             # offset already zero
67             }
68             elsif ( $tz =~ /^([-+])?([0-9][0-9]?):?([0-9][0-9])?$/ ) {
69 29         52 $offset = 3600 * $2;
70 29 50       62 $offset += 60 * $3 if $3;
71 29 100 100     89 $offset *= -1 if $1 && $1 eq '-';
72             }
73             else {
74 4 50       5 eval { require Time::Zone } || return undef;
  4         621  
75 4         1320 $offset = Time::Zone::tz_offset($tz);
76 4 100       138 return undef unless defined $offset;
77             }
78              
79 105         107 return eval {
80 105         108 my $frac = $d[-1];
81 105         132 $frac -= ( $d[-1] = int($frac) );
82 105         215 my $t = Time::Local::timegm( reverse @d ) + $frac;
83 105 100       2547 $t < 0 ? undef : $t - $offset;
84             };
85             }
86              
87             sub parse_date ($) {
88 157     157 1 316254 local ($_) = shift;
89 157 50       254 return unless defined;
90              
91             # Reject over-long input up front, before any regex runs, so hostile
92             # strings cannot drive the parsing regexes into pathological backtracking.
93             # Length is measured as given (leading/trailing whitespace included). This
94             # cap is a security limit, not a tunable: every format we accept is far
95             # shorter, and the parsing regexes still contain adjacent unbounded
96             # quantifiers, so raising it demands re-benchmarking against hostile input.
97 157 100       312 return if length($_) > 64;
98              
99             # More lax parsing below
100 151         358 s/^\s+//; # kill leading space
101 151         321 s/^(?:Sun|Mon|Tue|Wed|Thu|Fri|Sat)[a-z]*,?\s*//i; # Useless weekday
102              
103 151         211 my ( $day, $mon, $yr, $hr, $min, $sec, $tz, $ampm );
104              
105             # Then we are able to check for most of the formats with this regexp
106             (
107 151 100 100     2061 ( $day, $mon, $yr, $hr, $min, $sec, $tz )
      100        
      100        
      100        
108             = /^
109             ([0-9][0-9]?) # day
110             (?:\s+|[-\/])
111             (\w+) # month
112             (?:\s+|[-\/])
113             ([0-9]+) # year
114             (?:
115             (?:\s+|:) # separator before clock
116             ([0-9][0-9]?):([0-9][0-9]) # hour:min
117             (?::([0-9][0-9]))? # optional seconds
118             )? # optional clock
119             \s*
120             ([-+]?[0-9]{2,4}|(?![APap][Mm]\b)[A-Za-z]+)? # timezone
121             \s*
122             (?:\(\w+\)|\w{3,})? # ASCII representation of timezone.
123             \s*$
124             /x
125             )
126              
127             ||
128              
129             # Try the ctime and asctime format
130             (
131             ( $mon, $day, $hr, $min, $sec, $tz, $yr )
132             = /^
133             (\w{1,3}) # month
134             \s+
135             ([0-9][0-9]?) # day
136             \s+
137             ([0-9][0-9]?):([0-9][0-9]) # hour:min
138             (?::([0-9][0-9]))? # optional seconds
139             \s+
140             (?:([A-Za-z]+)\s+)? # optional timezone
141             ([0-9]+) # year
142             \s*$ # allow trailing whitespace
143             /x
144             )
145              
146             ||
147              
148             # Then the Unix 'ls -l' date format
149             (
150             ( $mon, $day, $yr, $hr, $min, $sec )
151             = /^
152             (\w{3}) # month
153             \s+
154             ([0-9][0-9]?) # day
155             \s+
156             (?:
157             ([0-9][0-9][0-9][0-9]) | # year
158             ([0-9]{1,2}):([0-9]{2}) # hour:min
159             (?::([0-9][0-9]))? # optional seconds
160             )
161             \s*$
162             /x
163             )
164              
165             ||
166              
167             # ISO 8601 format '1996-02-29 12:00:00 -0100' and variants
168             (
169             ( $yr, $mon, $day, $hr, $min, $sec, $tz )
170             = /^
171             ([0-9]{4}) # year
172             [-\/]?
173             ([0-9][0-9]?) # numerical month
174             [-\/]?
175             ([0-9][0-9]?) # day
176             (?:
177             (?:\s+|[-:Tt]) # separator before clock
178             ([0-9][0-9]?):?([0-9][0-9]) # hour:min
179             (?::?([0-9][0-9](?:\.[0-9]*)?))? # optional seconds (and fractional)
180             )? # optional clock
181             \s*
182             ([-+]?[0-9][0-9]?:?(?:[0-9][0-9])?
183             |Z|z)? # timezone (Z is "zero meridian", i.e. GMT)
184             \s*$
185             /x
186             )
187              
188             ||
189              
190             # Windows 'dir': '11-12-96 03:52PM' and four-digit year variant
191             (
192             ( $mon, $day, $yr, $hr, $min, $ampm )
193             = /^
194             ([0-9]{2}) # numerical month
195             -
196             ([0-9]{2}) # day
197             -
198             ([0-9]{2,4}) # year
199             \s+
200             ([0-9][0-9]?):([0-9][0-9])([APap][Mm]) # hour:min AM or PM
201             \s*$
202             /x
203             )
204              
205             || return; # unrecognized format
206              
207             # Translate month name to number
208             $mon
209             = $MoY{$mon}
210 139   100     974 || $MoY{"\u\L$mon"}
211             || ( $mon =~ /^[0-9][0-9]?$/ && $mon >= 1 && $mon <= 12 && int($mon) )
212             || return;
213              
214             # If the year is missing, we assume first date before the current,
215             # because of the formats we support such dates are mostly present
216             # on "ls -l" listings.
217 135 100 66     282 unless ( defined $yr ) {
218 1         2 my $cur_mon;
219 1         9 ( $cur_mon, $yr ) = (localtime)[ 4, 5 ];
220 1         3 $yr += 1900;
221 1         1 $cur_mon++;
222 1 50       3 $yr-- if $mon > $cur_mon;
223             }
224             elsif ( length($yr) < 3 ) {
225              
226             # Find "obvious" year
227             my $cur_yr = (localtime)[5] + 1900;
228             my $m = $cur_yr % 100;
229             my $tmp = $yr;
230             $yr += $cur_yr - $m;
231             $m -= $tmp;
232             $yr += ( $m > 0 ) ? 100 : -100
233             if abs($m) > 50;
234             }
235              
236             # Make sure clock elements are defined
237 135 100       202 $hr = 0 unless defined($hr);
238 135 100       184 $min = 0 unless defined($min);
239 135 100       172 $sec = 0 unless defined($sec);
240              
241             # Compensate for AM/PM
242 135 100       205 if ($ampm) {
243 17         23 $ampm = uc $ampm;
244 17 100 100     44 $hr = 0 if $hr == 12 && $ampm eq 'AM';
245 17 100 100     37 $hr += 12 if $ampm eq 'PM' && $hr != 12;
246             }
247              
248 135 100       433 return ( $yr, $mon, $day, $hr, $min, $sec, $tz )
249             if wantarray;
250              
251 4 100       8 if ( defined $tz ) {
252 2 50       10 $tz = "Z" if $tz =~ /^(GMT|UTC?|[-+]?0+)$/;
253             }
254             else {
255 2         4 $tz = "";
256             }
257 4         39 return sprintf(
258             "%04d-%02d-%02d %02d:%02d:%02d%s",
259             $yr, $mon, $day, $hr, $min, $sec, $tz
260             );
261             }
262              
263             sub time2iso (;$) {
264 14     14 1 31 my $time = shift;
265 14 100       26 $time = time unless defined $time;
266 14         100 my ( $sec, $min, $hour, $mday, $mon, $year ) = localtime($time);
267 14         80 sprintf(
268             "%04d-%02d-%02d %02d:%02d:%02d",
269             $year + 1900, $mon + 1, $mday, $hour, $min, $sec
270             );
271             }
272              
273             sub time2isoz (;$) {
274 2     2 1 423 my $time = shift;
275 2 100       6 $time = time unless defined $time;
276 2         5 my ( $sec, $min, $hour, $mday, $mon, $year ) = gmtime($time);
277 2         9 sprintf(
278             "%04d-%02d-%02d %02d:%02d:%02dZ",
279             $year + 1900, $mon + 1, $mday, $hour, $min, $sec
280             );
281             }
282              
283             1;
284              
285             # ABSTRACT: HTTP::Date - date conversion routines
286             #
287              
288             __END__