[rubygems/rubygems] Use a constant empty tar header to avoid extra allocations
[ruby.git] / test / cgi / test_cgi_multipart.rb
blob5e8ec253901930d7482cbbe19a8356f0830939fa
1 # frozen_string_literal: true
2 require 'test/unit'
3 require 'cgi'
4 require 'tempfile'
5 require 'stringio'
6 require_relative 'update_env'
9 ##
10 ## usage:
11 ##   boundary = 'foobar1234'  # or nil
12 ##   multipart = MultiPart.new(boundary)
13 ##   multipart.append('name1', 'value1')
14 ##   multipart.append('file1', File.read('file1.html'), 'file1.html')
15 ##   str = multipart.close()
16 ##   str.each_line {|line| p line }
17 ##   ## output:
18 ##   # "--foobar1234\r\n"
19 ##   # "Content-Disposition: form-data: name=\"name1\"\r\n"
20 ##   # "\r\n"
21 ##   # "value1\r\n"
22 ##   # "--foobar1234\r\n"
23 ##   # "Content-Disposition: form-data: name=\"file1\"; filename=\"file1.html\"\r\n"
24 ##   # "Content-Type: text/html\r\n"
25 ##   # "\r\n"
26 ##   # "<html>\n"
27 ##   # "<body><p>Hello</p></body>\n"
28 ##   # "</html>\n"
29 ##   # "\r\n"
30 ##   # "--foobar1234--\r\n"
32 class MultiPart
34   def initialize(boundary=nil)
35     @boundary = boundary || create_boundary()
36     @buf = ''.dup
37     @buf.force_encoding(::Encoding::ASCII_8BIT) if defined?(::Encoding)
38   end
39   attr_reader :boundary
41   def append(name, value, filename=nil, content_type=nil)
42     content_type = detect_content_type(filename) if filename && content_type.nil?
43     s = filename ? "; filename=\"#{filename}\"" : ''
44     buf = @buf
45     buf << "--#{boundary}\r\n"
46     buf << "Content-Disposition: form-data: name=\"#{name}\"#{s}\r\n"
47     buf << "Content-Type: #{content_type}\r\n" if content_type
48     buf << "\r\n"
49     buf << value.b
50     buf << "\r\n"
51     return self
52   end
54   def close
55     buf = @buf
56     @buf = ''.dup
57     return buf << "--#{boundary}--\r\n"
58   end
60   def create_boundary()  #:nodoc:
61     return "--boundary#{rand().to_s[2..-1]}"
62   end
64   def detect_content_type(filename)   #:nodoc:
65     filename =~ /\.(\w+)\z/
66     return MIME_TYPES[$1] || 'application/octet-stream'
67   end
69   MIME_TYPES = {
70     'gif'      =>  'image/gif',
71     'jpg'      =>  'image/jpeg',
72     'jpeg'     =>  'image/jpeg',
73     'png'      =>  'image/png',
74     'bmp'      =>  'image/bmp',
75     'tif'      =>  'image/tiff',
76     'tiff'     =>  'image/tiff',
77     'htm'      =>  'text/html',
78     'html'     =>  'text/html',
79     'xml'      =>  'text/xml',
80     'txt'      =>  'text/plain',
81     'text'     =>  'text/plain',
82     'css'      =>  'text/css',
83     'mpg'      =>  'video/mpeg',
84     'mpeg'     =>  'video/mpeg',
85     'mov'      =>  'video/quicktime',
86     'avi'      =>  'video/x-msvideo',
87     'mp3'      =>  'audio/mpeg',
88     'mid'      =>  'audio/midi',
89     'wav'      =>  'audio/x-wav',
90     'zip'      =>  'application/zip',
91     #'tar.gz'   =>  'application/gtar',
92     'gz'       =>  'application/gzip',
93     'bz2'      =>  'application/bzip2',
94     'rtf'      =>  'application/rtf',
95     'pdf'      =>  'application/pdf',
96     'ps'       =>  'application/postscript',
97     'js'       =>  'application/x-javascript',
98     'xls'      =>  'application/vnd.ms-excel',
99     'doc'      =>  'application/msword',
100     'ppt'      =>  'application/vnd.ms-powerpoint',
101   }
107 class CGIMultipartTest < Test::Unit::TestCase
108   include UpdateEnv
111   def setup
112     @environ = {}
113     update_env(
114       'REQUEST_METHOD' => 'POST',
115       'CONTENT_TYPE' => nil,
116       'CONTENT_LENGTH' => nil,
117     )
118     @tempfiles = []
119   end
121   def teardown
122     ENV.update(@environ)
123     $stdin.close() if $stdin.is_a?(Tempfile)
124     $stdin = STDIN
125     @tempfiles.each {|t|
126       t.close!
127     }
128   end
130   def _prepare(data)
131     ## create multipart input
132     multipart = MultiPart.new(defined?(@boundary) ? @boundary : nil)
133     data.each do |hash|
134       multipart.append(hash[:name], hash[:value], hash[:filename])
135     end
136     input = multipart.close()
137     input = yield(input) if block_given?
138     #$stderr.puts "*** debug: input=\n#{input.collect{|line| line.inspect}.join("\n")}"
139     @boundary ||= multipart.boundary
140     ## set environment
141     ENV['CONTENT_TYPE'] = "multipart/form-data; boundary=#{@boundary}"
142     ENV['CONTENT_LENGTH'] = input.length.to_s
143     ENV['REQUEST_METHOD'] = 'POST'
144     ## set $stdin
145     tmpfile = Tempfile.new('test_cgi_multipart')
146     @tempfiles << tmpfile
147     tmpfile.binmode
148     tmpfile << input
149     tmpfile.rewind()
150     $stdin = tmpfile
151   end
153   def _test_multipart(cgi_options={})
154     caller(0).find {|s| s =~ /in `test_(.*?)'/ }
155     #testname = $1
156     #$stderr.puts "*** debug: testname=#{testname.inspect}"
157     _prepare(@data)
158     options = {:accept_charset=>"UTF-8"}
159     options.merge! cgi_options
160     cgi = CGI.new(options)
161     expected_names = @data.collect{|hash| hash[:name] }.sort
162     assert_equal(expected_names, cgi.params.keys.sort)
163     threshold = 1024*10
164     @data.each do |hash|
165       name = hash[:name]
166       expected = hash[:value]
167       if hash[:filename] #if file
168         expected_class = @expected_class || (hash[:value].length < threshold ? StringIO : Tempfile)
169         assert(cgi.files.keys.member?(hash[:name]))
170       else
171         expected_class = String
172         assert_equal(expected, cgi[name])
173         assert_equal(false,cgi.files.keys.member?(hash[:name]))
174       end
175       assert_kind_of(expected_class, cgi[name])
176       assert_equal(expected, cgi[name].read())
177       assert_equal(hash[:filename] || '', cgi[name].original_filename)  #if hash[:filename]
178       assert_equal(hash[:content_type] || '', cgi[name].content_type)  #if hash[:content_type]
179     end
180   ensure
181     if cgi
182       cgi.params.each {|name, vals|
183         vals.each {|val|
184           if val.kind_of?(Tempfile) && val.path
185             val.close!
186           end
187         }
188       }
189     end
190   end
193   def _read(basename)
194     filename = File.join(File.dirname(__FILE__), 'testdata', basename)
195     s = File.open(filename, 'rb') {|f| f.read() }
197     return s
198   end
201   def test_cgi_multipart_stringio
202     @boundary = '----WebKitFormBoundaryAAfvAII+YL9102cX'
203     @data = [
204       {:name=>'hidden1', :value=>'foobar'},
205       {:name=>'text1',   :value=>"\xE3\x81\x82\xE3\x81\x84\xE3\x81\x86\xE3\x81\x88\xE3\x81\x8A".dup},
206       {:name=>'file1',   :value=>_read('file1.html'),
207        :filename=>'file1.html', :content_type=>'text/html'},
208       {:name=>'image1',  :value=>_read('small.png'),
209        :filename=>'small.png',  :content_type=>'image/png'},  # small image
210     ]
211     @data[1][:value].force_encoding(::Encoding::UTF_8) if defined?(::Encoding)
212     @expected_class = StringIO
213     _test_multipart()
214   end
217   def test_cgi_multipart_tempfile
218     @boundary = '----WebKitFormBoundaryAAfvAII+YL9102cX'
219     @data = [
220       {:name=>'hidden1', :value=>'foobar'},
221       {:name=>'text1',   :value=>"\xE3\x81\x82\xE3\x81\x84\xE3\x81\x86\xE3\x81\x88\xE3\x81\x8A".dup},
222       {:name=>'file1',   :value=>_read('file1.html'),
223        :filename=>'file1.html', :content_type=>'text/html'},
224       {:name=>'image1',  :value=>_read('large.png'),
225        :filename=>'large.png',  :content_type=>'image/png'},  # large image
226     ]
227     @data[1][:value].force_encoding(::Encoding::UTF_8) if defined?(::Encoding)
228     @expected_class = Tempfile
229     _test_multipart()
230   end
233   def _set_const(klass, name, value)
234     old = nil
235     klass.class_eval do
236       old = const_get(name)
237       remove_const(name)
238       const_set(name, value)
239     end
240     return old
241   end
244   def test_cgi_multipart_maxmultipartlength
245     @data = [
246       {:name=>'image1', :value=>_read('large.png'),
247        :filename=>'large.png', :content_type=>'image/png'},  # large image
248     ]
249     begin
250       ex = assert_raise(StandardError) do
251         _test_multipart(:max_multipart_length=>2 * 1024) # set via simple scalar
252       end
253       assert_equal("too large multipart data.", ex.message)
254     ensure
255     end
256   end
259   def test_cgi_multipart_maxmultipartlength_lambda
260     @data = [
261       {:name=>'image1', :value=>_read('large.png'),
262        :filename=>'large.png', :content_type=>'image/png'},  # large image
263     ]
264     begin
265       ex = assert_raise(StandardError) do
266         _test_multipart(:max_multipart_length=>lambda{2*1024}) # set via lambda
267       end
268       assert_equal("too large multipart data.", ex.message)
269     ensure
270     end
271   end
274   def test_cgi_multipart_maxmultipartcount
275     @data = [
276       {:name=>'file1', :value=>_read('file1.html'),
277        :filename=>'file1.html', :content_type=>'text/html'},
278     ]
279     item = @data.first
280     500.times { @data << item }
281     #original = _set_const(CGI, :MAX_MULTIPART_COUNT, 128)
282     begin
283       ex = assert_raise(StandardError) do
284         _test_multipart()
285       end
286       assert_equal("too many parameters.", ex.message)
287     ensure
288       #_set_const(CGI, :MAX_MULTIPART_COUNT, original)
289     end
290   end if CGI.const_defined?(:MAX_MULTIPART_COUNT)
293   def test_cgi_multipart_badbody   ## [ruby-dev:28470]
294     @data = [
295       {:name=>'file1', :value=>_read('file1.html'),
296        :filename=>'file1.html', :content_type=>'text/html'},
297     ]
298     _prepare(@data) do |input|
299       input2 = input.sub(/--(\r\n)?\z/, "\r\n")
300       assert input2 != input
301       #p input2
302       input2
303     end
304     ex = assert_raise(EOFError) do
305       CGI.new(:accept_charset=>"UTF-8")
306     end
307     assert_equal("bad content body", ex.message)
308     #
309     _prepare(@data) do |input|
310       input2 = input.sub(/--(\r\n)?\z/, "")
311       assert input2 != input
312       #p input2
313       input2
314     end
315     ex = assert_raise(EOFError) do
316       CGI.new(:accept_charset=>"UTF-8")
317     end
318     assert_equal("bad content body", ex.message)
319   end
322   def test_cgi_multipart_quoteboundary  ## [JVN#84798830]
323     @boundary = '(.|\n)*'
324     @data = [
325       {:name=>'hidden1', :value=>'foobar'},
326       {:name=>'text1',   :value=>"\xE3\x81\x82\xE3\x81\x84\xE3\x81\x86\xE3\x81\x88\xE3\x81\x8A".dup},
327       {:name=>'file1',   :value=>_read('file1.html'),
328        :filename=>'file1.html', :content_type=>'text/html'},
329       {:name=>'image1',  :value=>_read('small.png'),
330        :filename=>'small.png',  :content_type=>'image/png'},  # small image
331     ]
332     @data[1][:value].force_encoding("UTF-8")
333     _prepare(@data)
334     cgi = CGI.new(:accept_charset=>"UTF-8")
335     assert_equal('file1.html', cgi['file1'].original_filename)
336   end
338   def test_cgi_multipart_boundary_10240 # [Bug #3866]
339     @boundary = 'AaB03x'
340     @data = [
341       {:name=>'file',   :value=>"b"*10134,
342        :filename=>'file.txt', :content_type=>'text/plain'},
343       {:name=>'foo',  :value=>"bar"},
344     ]
345     _prepare(@data)
346     cgi = CGI.new(:accept_charset=>"UTF-8")
347     assert_equal(cgi['foo'], 'bar')
348     assert_equal(cgi['file'].read, 'b'*10134)
349     cgi['file'].close! if cgi['file'].kind_of? Tempfile
350   end
352   def test_cgi_multipart_without_tempfile
353     assert_in_out_err([], <<-'EOM')
354       require 'cgi'
355       require 'stringio'
356       ENV['REQUEST_METHOD'] = 'POST'
357       ENV['CONTENT_TYPE'] = 'multipart/form-data; boundary=foobar1234'
358       body = <<-BODY.gsub(/\n/, "\r\n")
359 --foobar1234
360 Content-Disposition: form-data: name=\"name1\"
362 value1
363 --foobar1234
364 Content-Disposition: form-data: name=\"file1\"; filename=\"file1.html\"
365 Content-Type: text/html
367 <html>
368 <body><p>Hello</p></body>
369 </html>
371 --foobar1234--
372 BODY
373       ENV['CONTENT_LENGTH'] = body.size.to_s
374       $stdin = StringIO.new(body)
375       CGI.new
376     EOM
377   end
379   ###
381   self.instance_methods.each do |method|
382     private method if method =~ /^test_(.*)/ && $1 != ENV['TEST']
383   end if ENV['TEST']