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         11  
  2         63  
9 2     2   9 use warnings;
  2         4  
  2         75  
10 2     2   12 use Exporter 'import';
  2         4  
  2         188  
11              
12             our $VERSION = '1.000';
13              
14             our @EXPORT_OK = qw(extract_multipart_boundary parse_multipart_form_data);
15              
16 2     2   15 use constant DEFAULT_REQUEST_BODY_BUFFER => 262144;
  2         4  
  2         2855  
17              
18             sub extract_multipart_boundary {
19 18     18 1 1257 my ($content_type) = @_;
20 18         134 my ($boundary_quoted, $boundary_unquoted) = $content_type =~ m/;\s*boundary\s*=\s*(?:"((?:\\[\\"]|[^"])+)"|([^";]+))/i;
21 18 100       67 $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 93689 my ($input, $length, $boundary, $options) = @_;
27 25   100     96 $options ||= {};
28 25         59 my $input_is_scalar = ref $input eq 'SCALAR';
29 25 100       69 binmode $input unless $input_is_scalar;
30 25   50     65 my $remaining = 0 + ($length || 0);
31 25         60 my $next_boundary = "\r\n--$boundary\r\n";
32 25         48 my $end_boundary = "\r\n--$boundary--";
33 25   100     97 my $buffer_size = 0 + ($options->{buffer_size} || DEFAULT_REQUEST_BODY_BUFFER);
34 25         50 my $buffer = "\r\n";
35 25         49 my (%state, @parts, $current);
36 25         61 READER: while ($remaining > 0) {
37 25 100       53 if ($input_is_scalar) {
38 17         42 $buffer .= substr $$input, 0, $remaining;
39 17         30 $remaining = 0;
40             } else {
41 8 50       24 my $chunk = $remaining < $buffer_size ? $remaining : $buffer_size;
42 8 50       54 last unless my $read = read $input, $buffer, $chunk, length $buffer;
43 8         19 $remaining -= $read;
44             }
45              
46 25 50 33     110 unless ($state{parsing_headers} or $state{parsing_body}) {
47 25         72 my $next_pos = index $buffer, $next_boundary;
48 25         79 my $end_pos = index $buffer, $end_boundary;
49 25 100 100     122 if ($next_pos >= 0 and ($end_pos < 0 or $end_pos > $next_pos)) {
    100 100        
50 22         55 substr $buffer, 0, $next_pos + length($next_boundary), '';
51 22         44 $state{parsing_headers} = 1;
52 22         95 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         6 next; # read more to find start of multipart data
58             }
59             }
60              
61 22         65 while (length $buffer) {
62 234 100       426 if ($state{parsing_headers}) {
63 118         302 while ((my $pos = index $buffer, "\r\n") >= 0) {
64 295 100       549 if ($pos == 0) { # end of headers
65 116         179 $state{parsing_headers} = 0;
66 116         156 $state{parsing_body} = 1;
67 116         136 $state{parsed_optional_crlf} = 0;
68 116         181 last;
69             }
70              
71 179         433 my $header = substr $buffer, 0, $pos + 2, '';
72 179         866 my ($name, $value) = split /\s*:\s*/, $header, 2;
73 179 100       454 return undef unless defined $value;
74 177         1068 $value =~ s/\s*\z//;
75              
76 177         487 $current->{headers}{lc $name} = $value;
77 177 100       427 if (lc $name eq 'content-disposition') {
78 115         685 while ($value =~ m/;\s*([^=\s]+)\s*=\s*(?:"((?:\\[\\"]|[^"])*)"|([^";]*))/ig) {
79 160         519 my ($field_name, $field_quoted, $field_unquoted) = ($1, $2, $3);
80 160 50 66     443 next unless lc $field_name eq 'name' or lc $field_name eq 'filename';
81 160 100       436 $field_quoted =~ s/\\([\\"])/$1/g if defined $field_quoted;
82 160 100       909 $current->{lc $field_name} = defined $field_quoted ? $field_quoted : $field_unquoted;
83             }
84             }
85             }
86 116 50       280 next READER if $state{parsing_headers}; # read more to find end of headers
87             } else {
88 116         163 my $append = '';
89 116         204 my $next_pos = index $buffer, $next_boundary;
90 116         256 my $end_pos = index $buffer, $end_boundary;
91 116 100 66     468 if ($next_pos >= 0 and ($end_pos < 0 or $end_pos > $next_pos)) {
    50 66        
    0          
92 97 100 66     326 if (!$state{parsed_optional_crlf} and $next_pos >= 2) {
93 84         137 substr $buffer, 0, 2, '';
94 84         117 $next_pos -= 2;
95 84         129 $state{parsed_optional_crlf} = 1;
96             }
97 97         175 $append = substr $buffer, 0, $next_pos, '';
98 97         146 substr $buffer, 0, length($next_boundary), '';
99 97         128 $state{parsing_body} = 0;
100 97         133 $state{parsing_headers} = 1;
101             } elsif ($end_pos >= 0) {
102 19 50 33     81 if (!$state{parsed_optional_crlf} and $end_pos >= 2) {
103 19         37 substr $buffer, 0, 2, '';
104 19         30 $end_pos -= 2;
105 19         31 $state{parsed_optional_crlf} = 1;
106             }
107 19         39 $append = substr $buffer, 0, $end_pos; # no replacement, we're done here
108 19         29 $state{parsing_body} = 0;
109 19         32 $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         181 $current->{size} += length $append;
119 116 100 100     314 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         83 my $is_eof = !$state{parsing_body};
122 41 100       78 if (defined $options->{on_file_buffer}) {
123 12         36 $options->{on_file_buffer}->($append, my $dummy = $current, $is_eof);
124             } else {
125             # create temp file even if empty
126 29 50       97 unless (defined $current->{file}) {
127 29         957 require File::Temp;
128 29 100       17414 $current->{file} = File::Temp->new(@{$options->{tempfile_args} || []});
  29         211  
129 29         11858 binmode $current->{file};
130             }
131 29         63 print {$current->{file}} $append;
  29         241  
132 29 50       91 if ($is_eof) { # finalize temp file
133 29         819 $current->{file}->flush;
134 29         320 seek $current->{file}, 0, 0;
135             }
136             }
137             } else {
138 62 50       149 $current->{content} = '' unless defined $current->{content};
139 62         125 $current->{content} .= $append;
140             }
141             }
142              
143 116 100       385 last READER if $state{done}; # end of multipart data
144 97 50       176 next READER if $state{parsing_body}; # read more to find end of part
145              
146             # new part started
147 97         502 push @parts, $current = {headers => {}, name => undef, filename => undef, size => 0};
148             }
149             }
150             }
151 23 100       80 return undef unless $state{done};
152              
153 20         109 return \@parts;
154             }
155              
156             1;