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   1182 use strict;
  2         8  
  2         63  
9 2     2   9 use warnings;
  2         5  
  2         54  
10 2     2   12 use Exporter 'import';
  2         4  
  2         189  
11              
12             our $VERSION = '1.002';
13              
14             our @EXPORT_OK = qw(extract_multipart_boundary parse_multipart_form_data);
15              
16 2     2   40 use constant DEFAULT_REQUEST_BODY_BUFFER => 262144;
  2         4  
  2         2916  
17              
18             sub extract_multipart_boundary {
19 18     18 1 1366 my ($content_type) = @_;
20 18         155 my ($boundary_quoted, $boundary_unquoted) = $content_type =~ m/;\s*boundary\s*=\s*(?:"((?:\\[\\"]|[^"])+)"|([^";]+))/i;
21 18 100       69 $boundary_quoted =~ s/\\([\\"])/$1/g if defined $boundary_quoted;
22 18 100       94 return defined $boundary_quoted ? $boundary_quoted : $boundary_unquoted;
23             }
24              
25             sub parse_multipart_form_data {
26 25     25 1 95786 my ($input, $length, $boundary, $options) = @_;
27 25   100     98 $options ||= {};
28 25         65 my $input_is_scalar = ref $input eq 'SCALAR';
29 25 100       76 binmode $input unless $input_is_scalar;
30 25   50     76 my $remaining = 0 + ($length || 0);
31 25         70 my $next_boundary = "\r\n--$boundary\r\n";
32 25         51 my $end_boundary = "\r\n--$boundary--";
33 25   100     105 my $buffer_size = 0 + ($options->{buffer_size} || DEFAULT_REQUEST_BODY_BUFFER);
34 25         53 my $buffer = "\r\n";
35 25         52 my (%state, @parts, $current);
36 25         70 READER: while ($remaining > 0) {
37 25 100       59 if ($input_is_scalar) {
38 17         45 $buffer .= substr $$input, 0, $remaining;
39 17         30 $remaining = 0;
40             } else {
41 8 50       27 my $chunk = $remaining < $buffer_size ? $remaining : $buffer_size;
42 8 50       63 last unless my $read = read $input, $buffer, $chunk, length $buffer;
43 8         25 $remaining -= $read;
44             }
45              
46 25 50 33     116 unless ($state{parsing_headers} or $state{parsing_body}) {
47 25         86 my $next_pos = index $buffer, $next_boundary;
48 25         126 my $end_pos = index $buffer, $end_boundary;
49 25 100 100     139 if ($next_pos >= 0 and ($end_pos < 0 or $end_pos > $next_pos)) {
    100 100        
50 22         58 substr $buffer, 0, $next_pos + length($next_boundary), '';
51 22         43 $state{parsing_headers} = 1;
52 22         115 push @parts, $current = {headers => {}, name => undef, filename => undef, size => 0};
53             } elsif ($end_pos >= 0) {
54 1         3 $state{done} = 1;
55 1         3 last; # end of multipart data
56             } else {
57 2         7 next; # read more to find start of multipart data
58             }
59             }
60              
61 22         66 while (length $buffer) {
62 234 100       416 if ($state{parsing_headers}) {
63 118         302 while ((my $pos = index $buffer, "\r\n") >= 0) {
64 295 100       576 if ($pos == 0) { # end of headers
65 116         177 $state{parsing_headers} = 0;
66 116         167 $state{parsing_body} = 1;
67 116         162 $state{parsed_optional_crlf} = 0;
68 116         185 last;
69             }
70              
71 179         409 my $header = substr $buffer, 0, $pos + 2, '';
72 179         936 my ($name, $value) = split /\s*:\s*/, $header, 2;
73 179 100       426 return undef unless defined $value;
74 177         1092 $value =~ s/\s*\z//;
75              
76 177         502 $current->{headers}{lc $name} = $value;
77 177 100       424 if (lc $name eq 'content-disposition') {
78 115         723 while ($value =~ m/;\s*([^=\s]+)\s*=\s*(?:"((?:\\[\\"]|[^"])*)"|([^";]*))/ig) {
79 160         534 my ($field_name, $field_quoted, $field_unquoted) = ($1, $2, $3);
80 160 50 66     467 next unless lc $field_name eq 'name' or lc $field_name eq 'filename';
81 160 100       477 $field_quoted =~ s/\\([\\"])/$1/g if defined $field_quoted;
82 160 100       972 $current->{lc $field_name} = defined $field_quoted ? $field_quoted : $field_unquoted;
83             }
84             }
85             }
86 116 50       289 next READER if $state{parsing_headers}; # read more to find end of headers
87             } else {
88 116         161 my $append = '';
89 116         213 my $next_pos = index $buffer, $next_boundary;
90 116         292 my $end_pos = index $buffer, $end_boundary;
91 116 100 66     452 if ($next_pos >= 0 and ($end_pos < 0 or $end_pos > $next_pos)) {
    50 66        
    0          
92 97 100 66     315 if (!$state{parsed_optional_crlf} and $next_pos >= 2) {
93 84         142 substr $buffer, 0, 2, '';
94 84         119 $next_pos -= 2;
95 84         164 $state{parsed_optional_crlf} = 1;
96             }
97 97         175 $append = substr $buffer, 0, $next_pos, '';
98 97         145 substr $buffer, 0, length($next_boundary), '';
99 97         161 $state{parsing_body} = 0;
100 97         131 $state{parsing_headers} = 1;
101             } elsif ($end_pos >= 0) {
102 19 50 33     86 if (!$state{parsed_optional_crlf} and $end_pos >= 2) {
103 19         37 substr $buffer, 0, 2, '';
104 19         29 $end_pos -= 2;
105 19         31 $state{parsed_optional_crlf} = 1;
106             }
107 19         40 $append = substr $buffer, 0, $end_pos; # no replacement, we're done here
108 19         33 $state{parsing_body} = 0;
109 19         35 $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         191 $current->{size} += length $append;
119 116 100 100     322 unless (defined $current->{filename} and $options->{discard_files}) {
120 103 100 100     341 if ($options->{parse_as_files} or (defined $current->{filename} and !defined $options->{parse_as_files})) {
      100        
121 41         69 my $is_eof = !$state{parsing_body};
122 41 100       80 if (defined $options->{on_file_buffer}) {
123 12         35 $options->{on_file_buffer}->($append, my $dummy = $current, $is_eof);
124             } else {
125             # create temp file even if empty
126 29 50       65 unless (defined $current->{file}) {
127 29         979 require File::Temp;
128 29 100       18273 $current->{file} = File::Temp->new(@{$options->{tempfile_args} || []});
  29         223  
129 29         12485 binmode $current->{file};
130             }
131 29         54 print {$current->{file}} $append;
  29         269  
132 29 50       85 if ($is_eof) { # finalize temp file
133 29         833 $current->{file}->flush;
134 29         373 seek $current->{file}, 0, 0;
135             }
136             }
137             } else {
138 62 50       182 $current->{content} = '' unless defined $current->{content};
139 62         118 $current->{content} .= $append;
140             }
141             }
142              
143 116 100       375 last READER if $state{done}; # end of multipart data
144 97 50       185 next READER if $state{parsing_body}; # read more to find end of part
145              
146             # new part started
147 97         484 push @parts, $current = {headers => {}, name => undef, filename => undef, size => 0};
148             }
149             }
150             }
151 23 100       81 return undef unless $state{done};
152              
153 20         111 return \@parts;
154             }
155              
156             1;