File Coverage

blib/lib/HTTP/Range.pm
Criterion Covered Total %
statement 103 105 98.1
branch 28 34 82.3
condition 2 3 66.6
subroutine 21 21 100.0
pod 2 2 100.0
total 156 165 94.5


line stmt bran cond sub pod time code
1             # Copyright (C) 2004 Joshua Hoblitt
2             #
3             # $Id: Range.pm,v 1.4 2004/07/22 07:42:35 jhoblitt Exp $
4              
5             package HTTP::Range;
6              
7 3     3   30222 use strict;
  3         7  
  3         139  
8              
9 3     3   18 use vars qw( $VERSION );
  3         6  
  3         326  
10             $VERSION = 0.02;
11              
12             require IO::String;
13             require HTTP::Request;
14             require HTTP::Response;
15             require Set::Infinite;
16 3     3   2004 use HTTP::Status qw( RC_OK );
  3         10197  
  3         404  
17 3     3   4797 use Params::Validate qw( :all );
  3         39435  
  3         851  
18 3     3   3952 use UNIVERSAL qw( isa can );
  3         53  
  3         17  
19 3     3   1404 use Carp qw( croak );
  3         6  
  3         5059  
20              
21             my $DEBUG = 0;
22              
23             sub split
24             {
25 14     14 1 27275 my $class = shift;
26            
27             my %args = validate( @_,
28             {
29             request => {
30             type => OBJECT,
31             isa => 'HTTP::Request',
32             },
33             length => {
34             type => SCALAR,
35             callbacks => {
36 8     8   372 'length is > 0' => sub { $_[0] > 0 },
37 8     8   671 'length is + integer' => sub { $_[0] =~ /^\d+$/ },
38             },
39             },
40             segments => {
41             type => SCALAR,
42             default => 4,
43             callbacks => {
44 5     5   274 'segments is > 1' => sub { $_[0] > 1 },
45 4     4   155 'segments is + integer' => sub { $_[0] =~ /^\d+$/ },
46 6     6   378 'segments is <= length' => sub { $_[0] <= $_[1]->{ 'length' } },
47             },
48             },
49             },
50 14         1181 );
51              
52             # size of byte range per requested segment
53 3         57 $args{ 'seg_size' } = int ( $args{ 'length' } / $args{ 'segments' } );
54              
55             # if the length is not evenly divisible by the number of segments we have to
56             # account for the leftover bytes
57 3         11 $args{ 'seg_extras' } = $args{ 'length' } % $args{ 'segments' };
58              
59             # total number of bytes to process
60 3         7 $args{ 'len_remain' } = $args{ 'length' };
61              
62 3         7 my @requests;
63 3   66     15 while ( $args{ 'len_remain' } || $args{ 'seg_extras' } ) {
64             # size of this segment
65 8         15 my $seg_len = $args{ 'seg_size' };
66              
67             # do we have extra bytes?
68 8 100       24 if ( $args{ 'seg_extras' } ) {
69 4         6 $seg_len++;
70 4         7 $args{ 'seg_extras' }--;
71             }
72              
73             # offset into length
74 8         103 $args{ 'len_index' } = $args{ 'length' } - $args{ 'len_remain' };
75            
76             # bytes remaining
77 8         14 $args{ 'len_remain' } -= $seg_len;
78              
79             # copy the request object - this must be a deep clone
80 8         29 my $req = $args{ 'request' }->clone;
81              
82             # start-end of byte offset for this segment
83 8         1340 $req->header( Range => "bytes=$args{ 'len_index' }-"
84             . ( $args{ 'len_index' } + $seg_len - 1 ) );
85              
86 8         421 push( @requests, $req );
87             }
88              
89 3 50       21 return( wantarray ? @requests : \@requests );
90             }
91              
92             sub join
93             {
94 19     19 1 24918 my $class = shift;
95            
96             my %args = validate( @_,
97             {
98             responses => {
99             type => ARRAYREF,
100             },
101             length => {
102             type => SCALAR,
103             optional => 1,
104             callbacks => {
105 5     5   191 'length is > 0' => sub { shift > 0 },
106 6     6   449 'length is + integer' => sub { $_[0] =~ /^\d+$/ },
107             },
108             },
109             segments => {
110             type => SCALAR,
111             optional => 1,
112             callbacks => {
113 5     5   423 'segments is > 1' => sub { $_[0] > 1 },
114 3     3   197 'segments is + integer' => sub { $_[0] =~ /^\d+$/ },
115             'segments is == responses' => sub {
116 2     2   2 $_[0] == @{ $_[1]->{ 'responses' } };
  2         172  
117             },
118             'segments is <= length' => sub {
119 3 100   3   13 if ( $_[1]->{ 'length' } ) {
120 1         162 return $_[0] <= $_[1]->{ 'length' };
121             } else {
122 2         8 return 1;
123             }
124             },
125             },
126             },
127             },
128 19         1225 );
129              
130             # validate each object in the responses arrayref
131 9         147 foreach my $res ( @{ $args{ 'responses' } } ) {
  9         28  
132 39 100       1678 croak "not isa HTTP::Response" unless isa( $res, 'HTTP::Response' );
133 38 100       109 croak "not a successful HTTP status" unless HTTP::Status::is_success( $res->code );
134 37 100       491 croak "multi-part messages are not supported" if @{[ $res->parts ]};
  37         135  
135 36 100       1610 croak "segment has invalid content length" unless length $res->content == $res->content_length;
136             }
137              
138             # scalar w/ IO::Handle interface to hold the reassembled segments
139 5         238 my $content = IO::String->new;
140              
141             # set of content ranges processed
142 5         395 my @ranges;
143              
144             # put segments in order
145 5         15 my @responses = sort _byrange @{ $args{ 'responses' } };
  5         48  
146              
147 5         194 foreach my $res ( @responses ) {
148             # figure out the offset and size of the segment and write it to the file handle
149 18         222 my ( $start, $end ) = _parse_range( $res );
150 18         850 my $len = $end - $start + 1;
151              
152             # add a span per content range
153 18         102 push( @ranges, Set::Infinite->new( [ $start, $end ] ) );
154              
155             # seek to the appropriate location and write the current segment
156             # functions (instead of methods) are used for compatibility with IO::Handle
157 18 50       1350 unless ( defined sysseek( $content, $start, 0 ) ) {
158 0         0 croak "sysseeking response content";
159             }
160 18 50       369 if ( syswrite( $content, $res->content, $res->header( 'Content-Length' ), 0 ) != $len ){
161 0         0 croak "syswriting response content";
162             }
163              
164             # free the contents memory
165 18         1491 $res->content( undef );
166             }
167              
168             # if a content length was specified check it against what was received
169 5 100       95 if ( defined $args{ 'length' } ) {
170 3 100       8 if ( $args{ 'length'} != length ${ $content->string_ref } ) {
  3         14  
171 1         148 croak "specified content length does not equal received content length";
172             }
173              
174             # create a set of spans representing our segments
175 2         16 my $set = Set::Infinite->new;
176 2         43 $set = $set->union( $_ ) for @ranges;
177 2         415 $set = $set->integer;
178             # work around a bug in Set::Infinite
179 2         270 $set->_cleanup;
180 2 50       11 warn "ranges are @ranges\n" if $DEBUG;
181 2 50       7 warn "range set is: $set\n" if $DEBUG;
182              
183             # create a span representing our content length
184 2         13 my $len_set = Set::Infinite->new( [ 0, $args{ 'length' } -1 ] );
185              
186             # look for differences between our segments and content length
187 2         108 $len_set = $len_set->minus( $set );
188 2 50       596 warn "left over set is: $len_set\n" if $DEBUG;
189 2 100       12 croak "missing or incomplete segments" if $len_set;
190            
191             }
192              
193             # sort the segment spans
194             # these should already be in order as they were created in order of the
195             # sorted responses
196 3         48 @ranges = sort { $a <=> $b } @ranges;
  9         224  
197              
198             # look for spans (segments) that overlap each other
199 3         84 my $last_span;
200 3         7 foreach my $span ( @ranges ) {
201 10 100       350 if ( ! defined( $last_span ) ) {
202 3         4 $last_span = $span;
203 3         6 next;
204             }
205              
206 7 100       25 croak "segments overlap" if $last_span->intersection( $span );
207             }
208              
209             # create the return HTTP::Response object as a clone of the first object passed in
210 2         150 my $r = @{ $args{ 'responses' } }[0]->clone;
  2         33  
211              
212             # attempt to look like a single request by removing the Content-Range and
213             # resetting the HTTP status code + message
214 2         348 $r->remove_header( 'Content-Range' );
215 2         69 $r->code( RC_OK );
216 2         58 $r->message( HTTP::Status::status_message( $r->code ) );
217              
218             # set the content and it's length
219 2         44 $r->content_ref( $content->string_ref );
220 2         52 $r->header( content_length => length ${ $r->content_ref } );
  2         6  
221            
222 2         126 return( $r );
223             }
224              
225             sub _parse_range
226             {
227 56     56   799 my $res = shift;
228              
229 56         223 return $res->header( 'Content-Range' ) =~ /bytes (\d+)-(\d+)/;
230             }
231              
232             sub _byrange
233             {
234 19     19   566 (_parse_range( $a ))[0] <=> (_parse_range( $b ))[0];
235             }
236              
237             1;
238              
239             __END__