File Coverage

blib/lib/CGI/Tiny/Multipart.pm
Criterion Covered Total %
statement 105 109 96.3
branch 51 66 77.2
condition 30 39 76.9
subroutine 6 6 100.0
pod 2 2 100.0
total 194 222 87.3


line stmt bran cond sub pod time code
1             package CGI::Tiny::Multipart;
2             # ABSTRACT: Tiny multipart/form-data form parser
3              
4             # This file is part of CGI::Tiny which is released under:
5             # The Artistic License 2.0 (GPL Compatible)
6             # See the documentation for CGI::Tiny for full license details.
7              
8 2     2   1204 use strict;
  2         11  
  2         64  
9 2     2   10 use warnings;
  2         4  
  2         54  
10 2     2   10 use Exporter 'import';
  2         3  
  2         192  
11              
12             our $VERSION = '1.001';
13              
14             our @EXPORT_OK = qw(extract_multipart_boundary parse_multipart_form_data);
15              
16 2     2   14 use constant DEFAULT_REQUEST_BODY_BUFFER => 262144;
  2         4  
  2         2836  
17              
18             sub extract_multipart_boundary {
19 18     18 1 1338 my ($content_type) = @_;
20 18         143 my ($boundary_quoted, $boundary_unquoted) = $content_type =~ m/;\s*boundary\s*=\s*(?:"((?:\\[\\"]|[^"])+)"|([^";]+))/i;
21 18 100       68 $boundary_quoted =~ s/\\([\\"])/$1/g if defined $boundary_quoted;
22 18 100       117 return defined $boundary_quoted ? $boundary_quoted : $boundary_unquoted;
23             }
24              
25             sub parse_multipart_form_data {
26 25     25 1 106532 my ($input, $length, $boundary, $options) = @_;
27 25   100     131 $options ||= {};
28 25         86 my $input_is_scalar = ref $input eq 'SCALAR';
29 25 100       97 binmode $input unless $input_is_scalar;
30 25   50     86 my $remaining = 0 + ($length || 0);
31 25         79 my $next_boundary = "\r\n--$boundary\r\n";
32 25         69 my $end_boundary = "\r\n--$boundary--";
33 25   100     142 my $buffer_size = 0 + ($options->{buffer_size} || DEFAULT_REQUEST_BODY_BUFFER);
34 25         57 my $buffer = "\r\n";
35 25         64 my (%state, @parts, $current);
36 25         90 READER: while ($remaining > 0) {
37 25 100       65 if ($input_is_scalar) {
38 17         60 $buffer .= substr $$input, 0, $remaining;
39 17         37 $remaining = 0;
40             } else {
41 8 50       22 my $chunk = $remaining < $buffer_size ? $remaining : $buffer_size;
42 8 50       58 last unless my $read = read $input, $buffer, $chunk, length $buffer;
43 8         19 $remaining -= $read;
44             }
45              
46 25 50 33     147 unless ($state{parsing_headers} or $state{parsing_body}) {
47 25         103 my $next_pos = index $buffer, $next_boundary;
48 25         93 my $end_pos = index $buffer, $end_boundary;
49 25 100 100     181 if ($next_pos >= 0 and ($end_pos < 0 or $end_pos > $next_pos)) {
    100 100        
50 22         62 substr $buffer, 0, $next_pos + length($next_boundary), '';
51 22         61 $state{parsing_headers} = 1;
52 22         211 push @parts, $current = {headers => {}, name => undef, filename => undef, size => 0};
53             } elsif ($end_pos >= 0) {
54 1         4 $state{done} = 1;
55 1         4 last; # end of multipart data
56             } else {
57 2         9 next; # read more to find start of multipart data
58             }
59             }
60              
61 22         83 while (length $buffer) {
62 234 100       430 if ($state{parsing_headers}) {
63 118         348 while ((my $pos = index $buffer, "\r\n") >= 0) {
64 295 100       571 if ($pos == 0) { # end of headers
65 116         185 $state{parsing_headers} = 0;
66 116         165 $state{parsing_body} = 1;
67 116         160 $state{parsed_optional_crlf} = 0;
68 116         198 last;
69             }
70              
71 179         409 my $header = substr $buffer, 0, $pos + 2, '';
72 179         942 my ($name, $value) = split /\s*:\s*/, $header, 2;
73 179 100       485 return undef unless defined $value;
74 177         1075 $value =~ s/\s*\z//;
75              
76 177         506 $current->{headers}{lc $name} = $value;
77 177 100       449 if (lc $name eq 'content-disposition') {
78 115         730 while ($value =~ m/;\s*([^=\s]+)\s*=\s*(?:"((?:\\[\\"]|[^"])*)"|([^";]*))/ig) {
79 160         514 my ($field_name, $field_quoted, $field_unquoted) = ($1, $2, $3);
80 160 50 66     458 next unless lc $field_name eq 'name' or lc $field_name eq 'filename';
81 160 100       465 $field_quoted =~ s/\\([\\"])/$1/g if defined $field_quoted;
82 160 100       1005 $current->{lc $field_name} = defined $field_quoted ? $field_quoted : $field_unquoted;
83             }
84             }
85             }
86 116 50       290 next READER if $state{parsing_headers}; # read more to find end of headers
87             } else {
88 116         169 my $append = '';
89 116         207 my $next_pos = index $buffer, $next_boundary;
90 116         282 my $end_pos = index $buffer, $end_boundary;
91 116 100 66     510 if ($next_pos >= 0 and ($end_pos < 0 or $end_pos > $next_pos)) {
    50 66        
    0          
92 97 100 66     314 if (!$state{parsed_optional_crlf} and $next_pos >= 2) {
93 84         147 substr $buffer, 0, 2, '';
94 84         119 $next_pos -= 2;
95 84         119 $state{parsed_optional_crlf} = 1;
96             }
97 97         196 $append = substr $buffer, 0, $next_pos, '';
98 97         199 substr $buffer, 0, length($next_boundary), '';
99 97         221 $state{parsing_body} = 0;
100 97         134 $state{parsing_headers} = 1;
101             } elsif ($end_pos >= 0) {
102 19 50 33     100 if (!$state{parsed_optional_crlf} and $end_pos >= 2) {
103 19         42 substr $buffer, 0, 2, '';
104 19         35 $end_pos -= 2;
105 19         30 $state{parsed_optional_crlf} = 1;
106             }
107 19         47 $append = substr $buffer, 0, $end_pos; # no replacement, we're done here
108 19         30 $state{parsing_body} = 0;
109 19         36 $state{done} = 1;
110             } elsif (length($buffer) > length($next_boundary) + 2) {
111 0 0       0 if (!$state{parsed_optional_crlf}) {
112 0         0 substr $buffer, 0, 2, '';
113 0         0 $state{parsed_optional_crlf} = 1;
114             }
115 0         0 $append = substr $buffer, 0, length($buffer) - length($next_boundary), '';
116             }
117              
118 116         192 $current->{size} += length $append;
119 116 100 100     329 unless (defined $current->{filename} and $options->{discard_files}) {
120 103 100 100     373 if ($options->{parse_as_files} or (defined $current->{filename} and !defined $options->{parse_as_files})) {
      100        
121 41         74 my $is_eof = !$state{parsing_body};
122 41 100       82 if (defined $options->{on_file_buffer}) {
123 12         30 $options->{on_file_buffer}->($append, my $dummy = $current, $is_eof);
124             } else {
125             # create temp file even if empty
126 29 50       86 unless (defined $current->{file}) {
127 29         1247 require File::Temp;
128 29 100       18484 $current->{file} = File::Temp->new(@{$options->{tempfile_args} || []});
  29         218  
129 29         12961 binmode $current->{file};
130             }
131 29         52 print {$current->{file}} $append;
  29         305  
132 29 50       102 if ($is_eof) { # finalize temp file
133 29         903 $current->{file}->flush;
134 29         346 seek $current->{file}, 0, 0;
135             }
136             }
137             } else {
138 62 50       163 $current->{content} = '' unless defined $current->{content};
139 62         120 $current->{content} .= $append;
140             }
141             }
142              
143 116 100       398 last READER if $state{done}; # end of multipart data
144 97 50       191 next READER if $state{parsing_body}; # read more to find end of part
145              
146             # new part started
147 97         488 push @parts, $current = {headers => {}, name => undef, filename => undef, size => 0};
148             }
149             }
150             }
151 23 100       172 return undef unless $state{done};
152              
153 20         133 return \@parts;
154             }
155              
156             1;