nano-sharp-hub 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/nano-sharp-hub.gemspec +12 -0
  3. data/rack-3.2.6/CHANGELOG.md +1346 -0
  4. data/rack-3.2.6/CONTRIBUTING.md +144 -0
  5. data/rack-3.2.6/MIT-LICENSE +20 -0
  6. data/rack-3.2.6/README.md +384 -0
  7. data/rack-3.2.6/SPEC.rdoc +258 -0
  8. data/rack-3.2.6/lib/rack/auth/abstract/handler.rb +41 -0
  9. data/rack-3.2.6/lib/rack/auth/abstract/request.rb +51 -0
  10. data/rack-3.2.6/lib/rack/auth/basic.rb +58 -0
  11. data/rack-3.2.6/lib/rack/bad_request.rb +8 -0
  12. data/rack-3.2.6/lib/rack/body_proxy.rb +63 -0
  13. data/rack-3.2.6/lib/rack/builder.rb +296 -0
  14. data/rack-3.2.6/lib/rack/cascade.rb +67 -0
  15. data/rack-3.2.6/lib/rack/common_logger.rb +89 -0
  16. data/rack-3.2.6/lib/rack/conditional_get.rb +87 -0
  17. data/rack-3.2.6/lib/rack/config.rb +22 -0
  18. data/rack-3.2.6/lib/rack/constants.rb +68 -0
  19. data/rack-3.2.6/lib/rack/content_length.rb +34 -0
  20. data/rack-3.2.6/lib/rack/content_type.rb +33 -0
  21. data/rack-3.2.6/lib/rack/deflater.rb +158 -0
  22. data/rack-3.2.6/lib/rack/directory.rb +208 -0
  23. data/rack-3.2.6/lib/rack/etag.rb +71 -0
  24. data/rack-3.2.6/lib/rack/events.rb +172 -0
  25. data/rack-3.2.6/lib/rack/files.rb +216 -0
  26. data/rack-3.2.6/lib/rack/head.rb +25 -0
  27. data/rack-3.2.6/lib/rack/headers.rb +238 -0
  28. data/rack-3.2.6/lib/rack/lint.rb +964 -0
  29. data/rack-3.2.6/lib/rack/lock.rb +29 -0
  30. data/rack-3.2.6/lib/rack/media_type.rb +52 -0
  31. data/rack-3.2.6/lib/rack/method_override.rb +56 -0
  32. data/rack-3.2.6/lib/rack/mime.rb +694 -0
  33. data/rack-3.2.6/lib/rack/mock.rb +3 -0
  34. data/rack-3.2.6/lib/rack/mock_request.rb +161 -0
  35. data/rack-3.2.6/lib/rack/mock_response.rb +156 -0
  36. data/rack-3.2.6/lib/rack/multipart/generator.rb +99 -0
  37. data/rack-3.2.6/lib/rack/multipart/parser.rb +621 -0
  38. data/rack-3.2.6/lib/rack/multipart/uploaded_file.rb +82 -0
  39. data/rack-3.2.6/lib/rack/multipart.rb +77 -0
  40. data/rack-3.2.6/lib/rack/null_logger.rb +48 -0
  41. data/rack-3.2.6/lib/rack/query_parser.rb +261 -0
  42. data/rack-3.2.6/lib/rack/recursive.rb +66 -0
  43. data/rack-3.2.6/lib/rack/reloader.rb +112 -0
  44. data/rack-3.2.6/lib/rack/request.rb +790 -0
  45. data/rack-3.2.6/lib/rack/response.rb +403 -0
  46. data/rack-3.2.6/lib/rack/rewindable_input.rb +116 -0
  47. data/rack-3.2.6/lib/rack/runtime.rb +35 -0
  48. data/rack-3.2.6/lib/rack/sendfile.rb +197 -0
  49. data/rack-3.2.6/lib/rack/show_exceptions.rb +409 -0
  50. data/rack-3.2.6/lib/rack/show_status.rb +121 -0
  51. data/rack-3.2.6/lib/rack/static.rb +192 -0
  52. data/rack-3.2.6/lib/rack/tempfile_reaper.rb +33 -0
  53. data/rack-3.2.6/lib/rack/urlmap.rb +99 -0
  54. data/rack-3.2.6/lib/rack/utils.rb +714 -0
  55. data/rack-3.2.6/lib/rack/version.rb +17 -0
  56. data/rack-3.2.6/lib/rack.rb +64 -0
  57. metadata +96 -0
@@ -0,0 +1,621 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'strscan'
4
+
5
+ require_relative '../utils'
6
+ require_relative '../bad_request'
7
+
8
+ module Rack
9
+ module Multipart
10
+ class MultipartPartLimitError < Errno::EMFILE
11
+ include BadRequest
12
+ end
13
+
14
+ class MultipartTotalPartLimitError < StandardError
15
+ include BadRequest
16
+ end
17
+
18
+ # Use specific error class when parsing multipart request
19
+ # that ends early.
20
+ class EmptyContentError < ::EOFError
21
+ include BadRequest
22
+ end
23
+
24
+ # Base class for multipart exceptions that do not subclass from
25
+ # other exception classes for backwards compatibility.
26
+ class BoundaryTooLongError < StandardError
27
+ include BadRequest
28
+ end
29
+
30
+ # Prefer to use the BoundaryTooLongError class or Rack::BadRequest.
31
+ Error = BoundaryTooLongError
32
+
33
+ EOL = "\r\n"
34
+ FWS = /[ \t]+(?:\r\n[ \t]+)?/ # whitespace with optional folding
35
+ HEADER_VALUE = "(?:[^\r\n]|\r\n[ \t])*" # anything but a non-folding CRLF
36
+ MULTIPART = %r|\Amultipart/.*?boundary(\s*)=\"?([^\";,]+)\"?|ni
37
+ MULTIPART_CONTENT_TYPE = /^Content-Type:#{FWS}?(#{HEADER_VALUE})/ni
38
+ MULTIPART_CONTENT_DISPOSITION = /^Content-Disposition:#{FWS}?(#{HEADER_VALUE})/ni
39
+ MULTIPART_CONTENT_ID = /^Content-ID:#{FWS}?(#{HEADER_VALUE})/ni
40
+
41
+ # Rack::Multipart::Parser handles parsing of multipart/form-data requests.
42
+ #
43
+ # File Parameter Contents
44
+ #
45
+ # When processing file uploads, the parser returns a hash containing
46
+ # information about uploaded files. For +file+ parameters, the hash includes:
47
+ #
48
+ # * +:filename+ - The original filename, already URL decoded by the parser
49
+ # * +:type+ - The content type of the uploaded file
50
+ # * +:name+ - The parameter name from the form
51
+ # * +:tempfile+ - A Tempfile object containing the uploaded data
52
+ # * +:head+ - The raw header content for this part
53
+ class Parser
54
+ BUFSIZE = 1_048_576
55
+ TEXT_PLAIN = "text/plain"
56
+ TEMPFILE_FACTORY = lambda { |filename, content_type|
57
+ extension = ::File.extname(filename.gsub("\0", '%00'))[0, 129]
58
+
59
+ Tempfile.new(["RackMultipart", extension])
60
+ }
61
+
62
+ BOUNDARY_START_LIMIT = 16 * 1024
63
+ private_constant :BOUNDARY_START_LIMIT
64
+
65
+ MIME_HEADER_BYTESIZE_LIMIT = 64 * 1024
66
+ private_constant :MIME_HEADER_BYTESIZE_LIMIT
67
+
68
+ env_int = lambda do |key, val|
69
+ if str_val = ENV[key]
70
+ begin
71
+ val = Integer(str_val, 10)
72
+ rescue ArgumentError
73
+ raise ArgumentError, "non-integer value provided for environment variable #{key}"
74
+ end
75
+ end
76
+
77
+ val
78
+ end
79
+
80
+ BUFFERED_UPLOAD_BYTESIZE_LIMIT = env_int.call("RACK_MULTIPART_BUFFERED_UPLOAD_BYTESIZE_LIMIT", 16 * 1024 * 1024)
81
+ private_constant :BUFFERED_UPLOAD_BYTESIZE_LIMIT
82
+
83
+ bytesize_limit = env_int.call("RACK_MULTIPART_PARSER_BYTESIZE_LIMIT", 10 * 1024 * 1024 * 1024)
84
+ PARSER_BYTESIZE_LIMIT = bytesize_limit > 0 ? bytesize_limit : nil
85
+ private_constant :PARSER_BYTESIZE_LIMIT
86
+
87
+ CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT = env_int.call("RACK_MULTIPART_CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT", 8 * 1024)
88
+ private_constant :CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT
89
+
90
+ class BoundedIO # :nodoc:
91
+ def initialize(io, content_length)
92
+ @io = io
93
+ @content_length = content_length
94
+ @cursor = 0
95
+ end
96
+
97
+ def read(size, outbuf = nil)
98
+ return if @cursor >= @content_length
99
+
100
+ left = @content_length - @cursor
101
+
102
+ str = if left < size
103
+ @io.read left, outbuf
104
+ else
105
+ @io.read size, outbuf
106
+ end
107
+
108
+ if str
109
+ @cursor += str.bytesize
110
+ else
111
+ # Raise an error for mismatching content-length and actual contents
112
+ raise EOFError, "bad content body"
113
+ end
114
+
115
+ str
116
+ end
117
+ end
118
+
119
+ MultipartInfo = Struct.new :params, :tmp_files
120
+ EMPTY = MultipartInfo.new(nil, [])
121
+
122
+ def self.parse_boundary(content_type)
123
+ return unless content_type
124
+ data = content_type.match(MULTIPART)
125
+ return unless data
126
+
127
+ unless data[1].empty?
128
+ raise Error, "whitespace between boundary parameter name and equal sign"
129
+ end
130
+ if data.post_match.match?(/boundary\s*=/i)
131
+ raise BoundaryTooLongError, "multiple boundary parameters found in multipart content type"
132
+ end
133
+
134
+ data[2]
135
+ end
136
+
137
+ def self.parse(io, content_length, content_type, tmpfile, bufsize, qp)
138
+ return EMPTY if 0 == content_length
139
+
140
+ boundary = parse_boundary content_type
141
+ return EMPTY unless boundary
142
+
143
+ if PARSER_BYTESIZE_LIMIT && content_length && content_length > PARSER_BYTESIZE_LIMIT
144
+ raise Error, "multipart Content-Length #{content_length} exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
145
+ end
146
+
147
+ if boundary.length > 70
148
+ # RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary.
149
+ # Most clients use no more than 55 characters.
150
+ raise BoundaryTooLongError, "multipart boundary size too large (#{boundary.length} characters)"
151
+ end
152
+
153
+ io = BoundedIO.new(io, content_length) if content_length
154
+
155
+ parser = new(boundary, tmpfile, bufsize, qp)
156
+ parser.parse(io)
157
+
158
+ parser.result
159
+ end
160
+
161
+ class Collector
162
+ class MimePart < Struct.new(:body, :head, :filename, :content_type, :name)
163
+ def get_data
164
+ data = body
165
+ if filename == ""
166
+ # filename is blank which means no file has been selected
167
+ return
168
+ elsif filename
169
+ body.rewind if body.respond_to?(:rewind)
170
+
171
+ # Take the basename of the upload's original filename.
172
+ # This handles the full Windows paths given by Internet Explorer
173
+ # (and perhaps other broken user agents) without affecting
174
+ # those which give the lone filename.
175
+ fn = filename.split(/[\/\\]/).last
176
+
177
+ data = { filename: fn, type: content_type,
178
+ name: name, tempfile: body, head: head }
179
+ end
180
+
181
+ yield data
182
+ end
183
+ end
184
+
185
+ class BufferPart < MimePart
186
+ def file?; false; end
187
+ def close; end
188
+ end
189
+
190
+ class TempfilePart < MimePart
191
+ def file?; true; end
192
+ def close; body.close; end
193
+ end
194
+
195
+ include Enumerable
196
+
197
+ def initialize(tempfile)
198
+ @tempfile = tempfile
199
+ @mime_parts = []
200
+ @open_files = 0
201
+ end
202
+
203
+ def each
204
+ @mime_parts.each { |part| yield part }
205
+ end
206
+
207
+ def on_mime_head(mime_index, head, filename, content_type, name)
208
+ if filename
209
+ body = @tempfile.call(filename, content_type)
210
+ body.binmode if body.respond_to?(:binmode)
211
+ klass = TempfilePart
212
+ @open_files += 1
213
+ else
214
+ body = String.new
215
+ klass = BufferPart
216
+ end
217
+
218
+ @mime_parts[mime_index] = klass.new(body, head, filename, content_type, name)
219
+
220
+ check_part_limits
221
+ end
222
+
223
+ def on_mime_body(mime_index, content)
224
+ @mime_parts[mime_index].body << content
225
+ end
226
+
227
+ def on_mime_finish(mime_index)
228
+ end
229
+
230
+ private
231
+
232
+ def check_part_limits
233
+ file_limit = Utils.multipart_file_limit
234
+ part_limit = Utils.multipart_total_part_limit
235
+
236
+ if file_limit && file_limit > 0
237
+ if @open_files >= file_limit
238
+ @mime_parts.each(&:close)
239
+ raise MultipartPartLimitError, 'Maximum file multiparts in content reached'
240
+ end
241
+ end
242
+
243
+ if part_limit && part_limit > 0
244
+ if @mime_parts.size >= part_limit
245
+ @mime_parts.each(&:close)
246
+ raise MultipartTotalPartLimitError, 'Maximum total multiparts in content reached'
247
+ end
248
+ end
249
+ end
250
+ end
251
+
252
+ attr_reader :state
253
+
254
+ def initialize(boundary, tempfile, bufsize, query_parser)
255
+ @query_parser = query_parser
256
+ @params = query_parser.make_params
257
+ @bufsize = bufsize
258
+
259
+ @state = :FAST_FORWARD
260
+ @mime_index = 0
261
+ @body_retained = nil
262
+ @retained_size = 0
263
+ @total_bytes_read = (0 if PARSER_BYTESIZE_LIMIT)
264
+ @content_disposition_quoted_escapes = 0
265
+ @collector = Collector.new tempfile
266
+
267
+ @sbuf = StringScanner.new("".dup)
268
+ @body_regex = /(?:#{EOL}|\A)--#{Regexp.quote(boundary)}(?:#{EOL}|--)/m
269
+ @body_regex_at_end = /#{@body_regex}\z/m
270
+ @end_boundary_size = boundary.bytesize + 4 # (-- at start, -- at finish)
271
+ @rx_max_size = boundary.bytesize + 6 # (\r\n-- at start, either \r\n or -- at finish)
272
+ @head_regex = /(.*?#{EOL})#{EOL}/m
273
+ end
274
+
275
+ def parse(io)
276
+ @total_bytes_read &&= nil if io.is_a?(BoundedIO)
277
+ outbuf = String.new
278
+ read_data(io, outbuf)
279
+
280
+ loop do
281
+ status =
282
+ case @state
283
+ when :FAST_FORWARD
284
+ handle_fast_forward
285
+ when :CONSUME_TOKEN
286
+ handle_consume_token
287
+ when :MIME_HEAD
288
+ handle_mime_head
289
+ when :MIME_BODY
290
+ handle_mime_body
291
+ else # when :DONE
292
+ return
293
+ end
294
+
295
+ read_data(io, outbuf) if status == :want_read
296
+ end
297
+ end
298
+
299
+ def result
300
+ @collector.each do |part|
301
+ part.get_data do |data|
302
+ tag_multipart_encoding(part.filename, part.content_type, part.name, data)
303
+ name, data = handle_dummy_encoding(part.name, data)
304
+ @query_parser.normalize_params(@params, name, data)
305
+ end
306
+ end
307
+ MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
308
+ end
309
+
310
+ private
311
+
312
+ def read_data(io, outbuf)
313
+ content = io.read(@bufsize, outbuf)
314
+ handle_empty_content!(content)
315
+ if @total_bytes_read
316
+ @total_bytes_read += content.bytesize
317
+ if @total_bytes_read > PARSER_BYTESIZE_LIMIT
318
+ raise Error, "multipart upload exceeds limit of #{PARSER_BYTESIZE_LIMIT} bytes"
319
+ end
320
+ end
321
+ @sbuf.concat(content)
322
+ end
323
+
324
+ # This handles the initial parser state. We read until we find the starting
325
+ # boundary, then we can transition to the next state. If we find the ending
326
+ # boundary, this is an invalid multipart upload, but keep scanning for opening
327
+ # boundary in that case. If no boundary found, we need to keep reading data
328
+ # and retry. It's highly unlikely the initial read will not consume the
329
+ # boundary. The client would have to deliberately craft a response
330
+ # with the opening boundary beyond the buffer size for that to happen.
331
+ def handle_fast_forward
332
+ while true
333
+ case consume_boundary
334
+ when :BOUNDARY
335
+ # found opening boundary, transition to next state
336
+ @state = :MIME_HEAD
337
+ return
338
+ when :END_BOUNDARY
339
+ # invalid multipart upload
340
+ if @sbuf.pos == @end_boundary_size && @sbuf.rest == EOL
341
+ # stop parsing a buffer if a buffer is only an end boundary.
342
+ @state = :DONE
343
+ return
344
+ end
345
+
346
+ # retry for opening boundary
347
+ else
348
+ # We raise if we don't find the multipart boundary, to avoid unbounded memory
349
+ # buffering. Note that the actual limit is the higher of 16KB and the buffer size (1MB by default)
350
+ raise Error, "multipart boundary not found within limit" if @sbuf.string.bytesize > BOUNDARY_START_LIMIT
351
+
352
+ # no boundary found, keep reading data
353
+ return :want_read
354
+ end
355
+ end
356
+ end
357
+
358
+ def handle_consume_token
359
+ tok = consume_boundary
360
+ # break if we're at the end of a buffer, but not if it is the end of a field
361
+ @state = if tok == :END_BOUNDARY || (@sbuf.eos? && tok != :BOUNDARY)
362
+ :DONE
363
+ else
364
+ :MIME_HEAD
365
+ end
366
+ end
367
+
368
+ CONTENT_DISPOSITION_MAX_PARAMS = 16
369
+ CONTENT_DISPOSITION_MAX_BYTES = 1536
370
+ OBS_UNFOLD = /\r\n([ \t])/
371
+ private_constant :OBS_UNFOLD
372
+
373
+ def handle_mime_head
374
+ if @sbuf.scan_until(@head_regex)
375
+ head = @sbuf[1]
376
+ content_type = head[MULTIPART_CONTENT_TYPE, 1]
377
+ content_type.gsub!(OBS_UNFOLD, '\1') if content_type
378
+
379
+ if (disposition = head[MULTIPART_CONTENT_DISPOSITION, 1]) &&
380
+ disposition.bytesize <= CONTENT_DISPOSITION_MAX_BYTES
381
+
382
+ # Implement OBS unfolding (RFC 5322 Section 2.2.3)
383
+ disposition.gsub!(OBS_UNFOLD, '\1')
384
+
385
+ # ignore actual content-disposition value (should always be form-data)
386
+ i = disposition.index(';')
387
+ disposition.slice!(0, i+1)
388
+ param = nil
389
+ num_params = 0
390
+
391
+ # Parse parameter list
392
+ while i = disposition.index('=')
393
+ # Only parse up to max parameters, to avoid potential denial of service
394
+ num_params += 1
395
+ break if num_params > CONTENT_DISPOSITION_MAX_PARAMS
396
+
397
+ # Found end of parameter name, ensure forward progress in loop
398
+ param = disposition.slice!(0, i+1)
399
+
400
+ # Remove ending equals and preceding whitespace from parameter name
401
+ param.chomp!('=')
402
+ param.lstrip!
403
+
404
+ if disposition[0] == '"'
405
+ # Parameter value is quoted, parse it, handling backslash escapes
406
+ disposition.slice!(0, 1)
407
+ value = String.new
408
+
409
+ while i = disposition.index(/(["\\])/)
410
+ c = $1
411
+
412
+ # Append all content until ending quote or escape
413
+ value << disposition.slice!(0, i)
414
+
415
+ # Remove either backslash or ending quote,
416
+ # ensures forward progress in loop
417
+ disposition.slice!(0, 1)
418
+
419
+ # stop parsing parameter value if found ending quote
420
+ break if c == '"'
421
+
422
+ @content_disposition_quoted_escapes += 1
423
+ if @content_disposition_quoted_escapes > CONTENT_DISPOSITION_QUOTED_ESCAPES_LIMIT
424
+ raise Error, "number of quoted escapes during content disposition parsing exceeds limit"
425
+ end
426
+
427
+ escaped_char = disposition.slice!(0, 1)
428
+ if param == 'filename' && escaped_char != '"'
429
+ # Possible IE uploaded filename, append both escape backslash and value
430
+ value << c << escaped_char
431
+ else
432
+ # Other only append escaped value
433
+ value << escaped_char
434
+ end
435
+ end
436
+ else
437
+ if i = disposition.index(';')
438
+ # Parameter value unquoted (which may be invalid), value ends at semicolon
439
+ value = disposition.slice!(0, i)
440
+ else
441
+ # If no ending semicolon, assume remainder of line is value and stop
442
+ # parsing
443
+ disposition.strip!
444
+ value = disposition
445
+ disposition = ''
446
+ end
447
+ end
448
+
449
+ case param
450
+ when 'name'
451
+ name = value
452
+ when 'filename'
453
+ filename = value
454
+ when 'filename*'
455
+ filename_star = value
456
+ # else
457
+ # ignore other parameters
458
+ end
459
+
460
+ # skip trailing semicolon, to proceed to next parameter
461
+ if i = disposition.index(';')
462
+ disposition.slice!(0, i+1)
463
+ end
464
+ end
465
+ else
466
+ name = head[MULTIPART_CONTENT_ID, 1]
467
+ end
468
+
469
+ if filename_star
470
+ encoding, _, filename = filename_star.split("'", 3)
471
+ filename = normalize_filename(filename || '')
472
+ filename.force_encoding(find_encoding(encoding))
473
+ elsif filename
474
+ filename = normalize_filename(filename)
475
+ end
476
+
477
+ if name.nil? || name.empty?
478
+ name = filename || "#{content_type || TEXT_PLAIN}[]".dup
479
+ end
480
+
481
+ # Mime part head data is retained for both TempfilePart and BufferPart
482
+ # for the entireity of the parse, even though it isn't used for BufferPart.
483
+ update_retained_size(head.bytesize)
484
+
485
+ # If a filename is given, a TempfilePart will be used, so the body will
486
+ # not be buffered in memory. However, if a filename is not given, a BufferPart
487
+ # will be used, and the body will be buffered in memory.
488
+ @body_retained = !filename
489
+
490
+ @collector.on_mime_head @mime_index, head, filename, content_type, name
491
+ @state = :MIME_BODY
492
+ else
493
+ # We raise if the mime part header is too large, to avoid unbounded memory
494
+ # buffering. Note that the actual limit is the higher of 64KB and the buffer size (1MB by default)
495
+ raise Error, "multipart mime part header too large" if @sbuf.rest.bytesize > MIME_HEADER_BYTESIZE_LIMIT
496
+
497
+ return :want_read
498
+ end
499
+ end
500
+
501
+ def handle_mime_body
502
+ if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
503
+ body = body_with_boundary.sub(@body_regex_at_end, '') # remove the boundary from the string
504
+ update_retained_size(body.bytesize) if @body_retained
505
+ @collector.on_mime_body @mime_index, body
506
+ @sbuf.pos += body.length + 2 # skip \r\n after the content
507
+ @state = :CONSUME_TOKEN
508
+ @mime_index += 1
509
+ else
510
+ # Save what we have so far
511
+ if @rx_max_size < @sbuf.rest_size
512
+ delta = @sbuf.rest_size - @rx_max_size
513
+ body = @sbuf.peek(delta)
514
+ update_retained_size(body.bytesize) if @body_retained
515
+ @collector.on_mime_body @mime_index, body
516
+ @sbuf.pos += delta
517
+ @sbuf.string = @sbuf.rest
518
+ end
519
+ :want_read
520
+ end
521
+ end
522
+
523
+ def update_retained_size(size)
524
+ @retained_size += size
525
+ if @retained_size > BUFFERED_UPLOAD_BYTESIZE_LIMIT
526
+ raise Error, "multipart data over retained size limit"
527
+ end
528
+ end
529
+
530
+ # Scan until the we find the start or end of the boundary.
531
+ # If we find it, return the appropriate symbol for the start or
532
+ # end of the boundary. If we don't find the start or end of the
533
+ # boundary, clear the buffer and return nil.
534
+ def consume_boundary
535
+ if read_buffer = @sbuf.scan_until(@body_regex)
536
+ read_buffer.end_with?(EOL) ? :BOUNDARY : :END_BOUNDARY
537
+ else
538
+ @sbuf.terminate
539
+ nil
540
+ end
541
+ end
542
+
543
+ def normalize_filename(filename)
544
+ if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
545
+ filename = Utils.unescape_path(filename)
546
+ end
547
+
548
+ filename.scrub!
549
+
550
+ filename.split(/[\/\\]/).last || String.new
551
+ end
552
+
553
+ CHARSET = "charset"
554
+ deprecate_constant :CHARSET
555
+
556
+ def tag_multipart_encoding(filename, content_type, name, body)
557
+ name = name.to_s
558
+ encoding = Encoding::UTF_8
559
+
560
+ name.force_encoding(encoding)
561
+
562
+ return if filename
563
+
564
+ if content_type
565
+ list = content_type.split(';')
566
+ type_subtype = list.first
567
+ type_subtype.strip!
568
+ if TEXT_PLAIN == type_subtype
569
+ rest = list.drop 1
570
+ rest.each do |param|
571
+ k, v = param.split('=', 2)
572
+ k.strip!
573
+ v.strip!
574
+ v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
575
+ if k == "charset"
576
+ encoding = find_encoding(v)
577
+ end
578
+ end
579
+ end
580
+ end
581
+
582
+ name.force_encoding(encoding)
583
+ body.force_encoding(encoding)
584
+ end
585
+
586
+ # Return the related Encoding object. However, because
587
+ # enc is submitted by the user, it may be invalid, so
588
+ # use a binary encoding in that case.
589
+ def find_encoding(enc)
590
+ Encoding.find enc
591
+ rescue ArgumentError
592
+ Encoding::BINARY
593
+ end
594
+
595
+ REENCODE_DUMMY_ENCODINGS = {
596
+ # ISO-2022-JP is a legacy but still widely used encoding in Japan
597
+ # Here we convert ISO-2022-JP to UTF-8 so that it can be handled.
598
+ Encoding::ISO_2022_JP => true
599
+
600
+ # Other dummy encodings are rarely used and have not been supported yet.
601
+ # Adding support for them will require careful considerations.
602
+ }
603
+
604
+ def handle_dummy_encoding(name, body)
605
+ # A string object with a 'dummy' encoding does not have full functionality and can cause errors.
606
+ # So here we covert it to UTF-8 so that it can be handled properly.
607
+ if name.encoding.dummy? && REENCODE_DUMMY_ENCODINGS[name.encoding]
608
+ name = name.encode(Encoding::UTF_8)
609
+ body = body.encode(Encoding::UTF_8)
610
+ end
611
+ return name, body
612
+ end
613
+
614
+ def handle_empty_content!(content)
615
+ if content.nil? || content.empty?
616
+ raise EmptyContentError
617
+ end
618
+ end
619
+ end
620
+ end
621
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tempfile'
4
+ require 'fileutils'
5
+
6
+ module Rack
7
+ module Multipart
8
+ # Despite the misleading name, UploadedFile is designed for use for
9
+ # preparing multipart file upload bodies, generally for use in tests.
10
+ # It is not designed for and should not be used for handling uploaded
11
+ # files (there is no need for that, since Rack's multipart parser
12
+ # already creates Tempfiles for that). Using this with non-trusted
13
+ # filenames can create a security vulnerability.
14
+ #
15
+ # You should only use this class if you plan on passing the instances
16
+ # to Rack::MockRequest for use in creating multipart request bodies.
17
+ #
18
+ # UploadedFile delegates most methods to the tempfile it contains.
19
+ class UploadedFile
20
+ # The provided name of the file. This generally is the basename of
21
+ # path provided during initialization, but it can contain slashes if they
22
+ # were present in the filename argument when the instance was created.
23
+ attr_reader :original_filename
24
+
25
+ # The content type of the instance.
26
+ attr_accessor :content_type
27
+
28
+ # Create a new UploadedFile. For backwards compatibility, this accepts
29
+ # both positional and keyword versions of the same arguments:
30
+ #
31
+ # filepath/path :: The path to the file
32
+ # ct/content_type :: The content_type of the file
33
+ # bin/binary :: Whether to set binmode on the file before copying data into it.
34
+ #
35
+ # If both positional and keyword arguments are present, the keyword arguments
36
+ # take precedence.
37
+ #
38
+ # The following keyword-only arguments are also accepted:
39
+ #
40
+ # filename :: Override the filename to use for the file. This is so the
41
+ # filename for the upload does not need to match the basename of
42
+ # the file path. This should not contain slashes, unless you are
43
+ # trying to test how an application handles invalid filenames in
44
+ # multipart upload bodies.
45
+ # io :: Use the given IO-like instance as the tempfile, instead of creating
46
+ # a Tempfile instance. This is useful for building multipart file
47
+ # upload bodies without a file being present on the filesystem. If you are
48
+ # providing this, you should also provide the filename argument.
49
+ def initialize(filepath = nil, ct = "text/plain", bin = false,
50
+ path: filepath, content_type: ct, binary: bin, filename: nil, io: nil)
51
+ if io
52
+ @tempfile = io
53
+ @original_filename = filename
54
+ else
55
+ raise "#{path} file does not exist" unless ::File.exist?(path)
56
+ @original_filename = filename || ::File.basename(path)
57
+ @tempfile = Tempfile.new([@original_filename, ::File.extname(path)], encoding: Encoding::BINARY)
58
+ @tempfile.binmode if binary
59
+ FileUtils.copy_file(path, @tempfile.path)
60
+ end
61
+ @content_type = content_type
62
+ end
63
+
64
+ # The path of the tempfile for the instance, if the tempfile has a path.
65
+ # nil if the tempfile does not have a path.
66
+ def path
67
+ @tempfile.path if @tempfile.respond_to?(:path)
68
+ end
69
+ alias_method :local_path, :path
70
+
71
+ # Return true if the tempfile responds to the method.
72
+ def respond_to_missing?(*args)
73
+ @tempfile.respond_to?(*args)
74
+ end
75
+
76
+ # Delegate method missing calls to the tempfile.
77
+ def method_missing(method_name, *args, &block) #:nodoc:
78
+ @tempfile.__send__(method_name, *args, &block)
79
+ end
80
+ end
81
+ end
82
+ end