protocol-multipart 0.2.0 → 0.4.0
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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/lib/protocol/multipart/form_data/parser.rb +106 -0
- data/lib/protocol/multipart/form_data.rb +16 -58
- data/lib/protocol/multipart/parser.rb +30 -30
- data/lib/protocol/multipart/version.rb +1 -1
- data/readme.md +10 -0
- data/releases.md +10 -0
- data.tar.gz.sig +0 -0
- metadata +16 -1
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 32d7a5d9ab53cbe80b179ea8ff2ac318415484eea858c4fc8f170c2bdcdb998a
|
|
4
|
+
data.tar.gz: 8540f4f11737134c960c1bcdd6303a2418c17ca6c754864e9b83204bc93410fb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ce50fbd0c0bb2afc3396eba792684b2db2cfd839808bede5b0e38196966481d5924a1548dabbb6f42621b7afe0039ba8342e27c22ee815de142cc1e266ac7ee1
|
|
7
|
+
data.tar.gz: 534c8bbc606b71e2d532d861a7d1633c53e28188f7542c1d5aa5000b758715a2eecad9ab1ed047a0f69d6e9def50ba019fe0f0e3965cf979e3c99aa4b1d5affe
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2026, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative "../form_data"
|
|
7
|
+
|
|
8
|
+
require "protocol/url/form_data/nested"
|
|
9
|
+
|
|
10
|
+
module Protocol
|
|
11
|
+
module Multipart
|
|
12
|
+
class FormData
|
|
13
|
+
# A configurable parser for `multipart/form-data` bodies.
|
|
14
|
+
class Parser
|
|
15
|
+
CONTENT_TYPE = "multipart/form-data"
|
|
16
|
+
|
|
17
|
+
# Initialize the form data parser.
|
|
18
|
+
# @parameter field_size_limit [Integer | Nil] The buffered field size limit.
|
|
19
|
+
# @parameter upload_size_limit [Integer | Nil] The file upload size limit.
|
|
20
|
+
# @parameter total_size_limit [Integer | Nil] The combined size limit for all fields and uploads.
|
|
21
|
+
# @parameter depth_limit [Integer | Nil] The bracketed form name depth limit.
|
|
22
|
+
# @parameter options [Hash] Limits passed to the underlying multipart parser.
|
|
23
|
+
def initialize(field_size_limit: FIELD_SIZE_LIMIT, upload_size_limit: UPLOAD_SIZE_LIMIT, total_size_limit: TOTAL_SIZE_LIMIT, depth_limit: Protocol::URL::FormData::Nested::DEPTH_LIMIT, **options)
|
|
24
|
+
if depth_limit and depth_limit < 0
|
|
25
|
+
raise ArgumentError, "Form data limits must be non-negative!"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
@field_size_limit = field_size_limit
|
|
29
|
+
@upload_size_limit = upload_size_limit
|
|
30
|
+
@total_size_limit = total_size_limit
|
|
31
|
+
@depth_limit = depth_limit
|
|
32
|
+
@options = options
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Parse multipart form data into a nested hash.
|
|
36
|
+
#
|
|
37
|
+
# When a block is given, each value is passed through the block before assignment. The value returned by the block is assigned to the result. Uploads require a block because they must be consumed before parsing advances to the next part.
|
|
38
|
+
#
|
|
39
|
+
# @parameter readable [IO, IO::Stream] The readable stream containing multipart form data.
|
|
40
|
+
# @parameter result [Object] The result to populate. It must support `#add` and `#to_h`.
|
|
41
|
+
# @parameter boundary [String] The multipart boundary.
|
|
42
|
+
# @yields {|name, value| ...} Each form entry before assignment.
|
|
43
|
+
# @returns [Hash] The nested form data.
|
|
44
|
+
def parse(readable, result = make_result, boundary:)
|
|
45
|
+
each(readable, boundary:) do |name, value|
|
|
46
|
+
if block_given?
|
|
47
|
+
value = yield(name, value)
|
|
48
|
+
elsif value.is_a?(Upload)
|
|
49
|
+
raise ArgumentError, "A block is required to consume file uploads!"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
result.add(name, value)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
return result.to_h
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Incrementally enumerate multipart form data.
|
|
59
|
+
#
|
|
60
|
+
# Fields are yielded as strings. File uploads are yielded as streaming {Upload} instances and are only readable during the corresponding block invocation.
|
|
61
|
+
#
|
|
62
|
+
# @parameter readable [IO, IO::Stream] The readable stream containing multipart form data.
|
|
63
|
+
# @parameter boundary [String] The multipart boundary.
|
|
64
|
+
# @yields {|name, value| ...} Each form field name and its string or streaming upload value.
|
|
65
|
+
# @returns [Enumerator | Boolean] An enumerator without a block, or true when complete.
|
|
66
|
+
def each(readable, boundary:)
|
|
67
|
+
return to_enum(__method__, readable, boundary:) unless block_given?
|
|
68
|
+
|
|
69
|
+
total_size_limit = ByteLimit.new(@total_size_limit, name: :total_size)
|
|
70
|
+
parser = Multipart::Parser.new(readable, boundary, **@options)
|
|
71
|
+
|
|
72
|
+
parser.each do |part|
|
|
73
|
+
disposition = part.headers["content-disposition"]
|
|
74
|
+
|
|
75
|
+
unless disposition&.type == "form-data" and name = disposition["name"]
|
|
76
|
+
raise ArgumentError, "Multipart form part is missing a form-data name!"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
if filename = disposition["filename"]
|
|
80
|
+
upload = Upload.new(part, filename, @upload_size_limit, total_size_limit)
|
|
81
|
+
yield name, upload
|
|
82
|
+
upload.discard
|
|
83
|
+
else
|
|
84
|
+
field_size_limit = ByteLimit.new(@field_size_limit, name: :field_size)
|
|
85
|
+
value = String.new.b
|
|
86
|
+
|
|
87
|
+
part.each do |chunk|
|
|
88
|
+
field_size_limit.consume(chunk.bytesize)
|
|
89
|
+
total_size_limit.consume(chunk.bytesize)
|
|
90
|
+
value << chunk
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
yield name, value
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def make_result
|
|
101
|
+
return Protocol::URL::FormData::Nested.new(depth_limit: @depth_limit)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -16,27 +16,27 @@ module Protocol
|
|
|
16
16
|
class FormData < Mixed
|
|
17
17
|
include Escape
|
|
18
18
|
|
|
19
|
-
# The
|
|
20
|
-
|
|
19
|
+
# The buffered form field size limit.
|
|
20
|
+
FIELD_SIZE_LIMIT = 2 * 1024 * 1024
|
|
21
21
|
|
|
22
|
-
# The
|
|
23
|
-
|
|
22
|
+
# The streamed file upload size limit.
|
|
23
|
+
UPLOAD_SIZE_LIMIT = 128 * 1024 * 1024
|
|
24
24
|
|
|
25
|
-
# The
|
|
26
|
-
|
|
25
|
+
# The combined size limit for all form fields and file uploads.
|
|
26
|
+
TOTAL_SIZE_LIMIT = 256 * 1024 * 1024
|
|
27
27
|
|
|
28
28
|
# A file upload yielded while parsing form data.
|
|
29
29
|
class Upload
|
|
30
30
|
# Initialize a streamed file upload.
|
|
31
31
|
# @parameter part [Parser::Part] The underlying multipart part.
|
|
32
32
|
# @parameter filename [String] The submitted filename.
|
|
33
|
-
# @parameter
|
|
34
|
-
# @parameter
|
|
35
|
-
def initialize(part, filename,
|
|
33
|
+
# @parameter size_limit [Integer | Nil] The upload size limit.
|
|
34
|
+
# @parameter total_size_limit [ByteLimit] The shared form-data size limit.
|
|
35
|
+
def initialize(part, filename, size_limit, total_size_limit)
|
|
36
36
|
@part = part
|
|
37
37
|
@filename = filename
|
|
38
|
-
@
|
|
39
|
-
@
|
|
38
|
+
@size_limit = ByteLimit.new(size_limit, name: :upload_size)
|
|
39
|
+
@total_size_limit = total_size_limit
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
# The submitted filename.
|
|
@@ -49,7 +49,7 @@ module Protocol
|
|
|
49
49
|
|
|
50
50
|
# The number of upload bytes consumed so far.
|
|
51
51
|
def size
|
|
52
|
-
@
|
|
52
|
+
@size_limit.size
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
# Whether the complete upload has been consumed.
|
|
@@ -63,8 +63,8 @@ module Protocol
|
|
|
63
63
|
return to_enum(:each, chunk_size) unless block_given?
|
|
64
64
|
|
|
65
65
|
@part.each(chunk_size) do |chunk|
|
|
66
|
-
@
|
|
67
|
-
@
|
|
66
|
+
@size_limit.consume(chunk.bytesize)
|
|
67
|
+
@total_size_limit.consume(chunk.bytesize)
|
|
68
68
|
yield chunk
|
|
69
69
|
end
|
|
70
70
|
|
|
@@ -78,50 +78,6 @@ module Protocol
|
|
|
78
78
|
end
|
|
79
79
|
end
|
|
80
80
|
|
|
81
|
-
# Parse multipart form data.
|
|
82
|
-
#
|
|
83
|
-
# Fields are yielded as strings. File uploads are yielded as streaming {Upload} instances and are only readable during the corresponding block invocation.
|
|
84
|
-
#
|
|
85
|
-
# @parameter readable [IO, IO::Stream] The readable stream containing multipart form data.
|
|
86
|
-
# @parameter boundary [String] The multipart boundary.
|
|
87
|
-
# @parameter maximum_field_size [Integer | Nil] The maximum size of each buffered field.
|
|
88
|
-
# @parameter maximum_upload_size [Integer | Nil] The maximum size of each file upload.
|
|
89
|
-
# @parameter maximum_total_size [Integer | Nil] The maximum combined size of all fields and uploads.
|
|
90
|
-
# @yields {|name, value| ...} Each form field name and its string or streaming upload value.
|
|
91
|
-
def self.parse(readable, boundary, maximum_field_size: MAXIMUM_FIELD_SIZE, maximum_upload_size: MAXIMUM_UPLOAD_SIZE, maximum_total_size: MAXIMUM_TOTAL_SIZE, **options)
|
|
92
|
-
unless block_given?
|
|
93
|
-
return enum_for(__method__, readable, boundary, maximum_field_size: maximum_field_size, maximum_upload_size: maximum_upload_size, maximum_total_size: maximum_total_size, **options)
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
total_limit = ByteLimit.new(maximum_total_size, name: :total_size)
|
|
97
|
-
parser = Parser.new(readable, boundary, **options)
|
|
98
|
-
|
|
99
|
-
parser.each do |part|
|
|
100
|
-
disposition = part.headers["content-disposition"]
|
|
101
|
-
|
|
102
|
-
unless disposition&.type == "form-data" and name = disposition["name"]
|
|
103
|
-
raise ArgumentError, "Multipart form part is missing a form-data name!"
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
if filename = disposition["filename"]
|
|
107
|
-
upload = Upload.new(part, filename, maximum_upload_size, total_limit)
|
|
108
|
-
yield name, upload
|
|
109
|
-
upload.discard
|
|
110
|
-
else
|
|
111
|
-
field_limit = ByteLimit.new(maximum_field_size, name: :field_size)
|
|
112
|
-
value = String.new.b
|
|
113
|
-
|
|
114
|
-
part.each do |chunk|
|
|
115
|
-
field_limit.consume(chunk.bytesize)
|
|
116
|
-
total_limit.consume(chunk.bytesize)
|
|
117
|
-
value << chunk
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
yield name, value
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
|
|
125
81
|
# Returns the MIME type for form data.
|
|
126
82
|
#
|
|
127
83
|
# @returns [String] The MIME type "multipart/form-data".
|
|
@@ -147,3 +103,5 @@ module Protocol
|
|
|
147
103
|
end
|
|
148
104
|
end
|
|
149
105
|
end
|
|
106
|
+
|
|
107
|
+
require_relative "form_data/parser"
|
|
@@ -14,17 +14,17 @@ module Protocol
|
|
|
14
14
|
HEADER_PATTERN = /\A([!-9;-~]+):[ \t]*([^\x00-\x08\x0a-\x1f\x7f]*)\z/.freeze
|
|
15
15
|
private_constant :HEADER_PATTERN
|
|
16
16
|
|
|
17
|
-
# The
|
|
18
|
-
|
|
17
|
+
# The preamble size limit.
|
|
18
|
+
PREAMBLE_SIZE_LIMIT = 64 * 1024
|
|
19
19
|
|
|
20
|
-
# The
|
|
21
|
-
|
|
20
|
+
# The header size limit for each part.
|
|
21
|
+
HEADER_SIZE_LIMIT = 64 * 1024
|
|
22
22
|
|
|
23
|
-
# The
|
|
24
|
-
|
|
23
|
+
# The header count limit for each part.
|
|
24
|
+
HEADER_COUNT_LIMIT = 64
|
|
25
25
|
|
|
26
|
-
# The
|
|
27
|
-
|
|
26
|
+
# The part count limit.
|
|
27
|
+
PART_COUNT_LIMIT = 128
|
|
28
28
|
|
|
29
29
|
# Represents a single part within a multipart message.
|
|
30
30
|
class Part
|
|
@@ -163,12 +163,12 @@ module Protocol
|
|
|
163
163
|
#
|
|
164
164
|
# @parameter readable [IO, IO::Stream] The readable stream containing multipart data.
|
|
165
165
|
# @parameter boundary [String] The boundary string that separates the parts.
|
|
166
|
-
# @parameter
|
|
167
|
-
# @parameter
|
|
168
|
-
# @parameter
|
|
169
|
-
# @parameter
|
|
170
|
-
def initialize(readable, boundary,
|
|
171
|
-
limits = [
|
|
166
|
+
# @parameter preamble_size_limit [Integer | Nil] The preamble size limit, or nil for no limit.
|
|
167
|
+
# @parameter header_size_limit [Integer | Nil] The header size limit per part, or nil for no limit.
|
|
168
|
+
# @parameter header_count_limit [Integer | Nil] The header count limit per part, or nil for no limit.
|
|
169
|
+
# @parameter part_count_limit [Integer | Nil] The part count limit, or nil for no limit.
|
|
170
|
+
def initialize(readable, boundary, preamble_size_limit: PREAMBLE_SIZE_LIMIT, header_size_limit: HEADER_SIZE_LIMIT, header_count_limit: HEADER_COUNT_LIMIT, part_count_limit: PART_COUNT_LIMIT)
|
|
171
|
+
limits = [preamble_size_limit, header_size_limit, header_count_limit, part_count_limit]
|
|
172
172
|
|
|
173
173
|
if limits.any?{|limit| limit and limit < 0}
|
|
174
174
|
raise ArgumentError, "Multipart limits must be non-negative!"
|
|
@@ -176,10 +176,10 @@ module Protocol
|
|
|
176
176
|
|
|
177
177
|
@readable = IO::Stream(readable)
|
|
178
178
|
@boundary = boundary
|
|
179
|
-
@
|
|
180
|
-
@
|
|
181
|
-
@
|
|
182
|
-
@
|
|
179
|
+
@preamble_size_limit = preamble_size_limit
|
|
180
|
+
@header_size_limit = header_size_limit
|
|
181
|
+
@header_count_limit = header_count_limit
|
|
182
|
+
@part_count_limit = part_count_limit
|
|
183
183
|
|
|
184
184
|
@boundary_marker = "--#{@boundary}\r\n".freeze
|
|
185
185
|
end
|
|
@@ -195,12 +195,12 @@ module Protocol
|
|
|
195
195
|
|
|
196
196
|
# Read lines until we find the first boundary:
|
|
197
197
|
while true
|
|
198
|
-
if line = read_line(preamble_size, @
|
|
198
|
+
if line = read_line(preamble_size, @preamble_size_limit, allowance: @boundary_marker.bytesize, chomp: false)
|
|
199
199
|
if line == @boundary_marker
|
|
200
200
|
break
|
|
201
201
|
else
|
|
202
202
|
preamble_size += line.bytesize
|
|
203
|
-
check_limit(:preamble_size, preamble_size, @
|
|
203
|
+
check_limit(:preamble_size, preamble_size, @preamble_size_limit)
|
|
204
204
|
end
|
|
205
205
|
else
|
|
206
206
|
# End of stream reached without finding boundary:
|
|
@@ -212,7 +212,7 @@ module Protocol
|
|
|
212
212
|
|
|
213
213
|
while true
|
|
214
214
|
part_count += 1
|
|
215
|
-
check_limit(:part_count, part_count, @
|
|
215
|
+
check_limit(:part_count, part_count, @part_count_limit)
|
|
216
216
|
|
|
217
217
|
part = read_part
|
|
218
218
|
break unless part
|
|
@@ -234,18 +234,18 @@ module Protocol
|
|
|
234
234
|
|
|
235
235
|
private
|
|
236
236
|
|
|
237
|
-
def read_line(size,
|
|
238
|
-
if
|
|
239
|
-
limit =
|
|
237
|
+
def read_line(size, limit, allowance: 0, chomp:)
|
|
238
|
+
if limit
|
|
239
|
+
limit = limit - size + allowance + 1
|
|
240
240
|
return @readable.gets("\r\n", limit, chomp: chomp)
|
|
241
241
|
else
|
|
242
242
|
return @readable.gets("\r\n", chomp: chomp)
|
|
243
243
|
end
|
|
244
244
|
end
|
|
245
245
|
|
|
246
|
-
def check_limit(name, value,
|
|
247
|
-
if
|
|
248
|
-
raise RangeError, "Multipart #{name} exceeded limit of #{
|
|
246
|
+
def check_limit(name, value, limit)
|
|
247
|
+
if limit and value > limit
|
|
248
|
+
raise RangeError, "Multipart #{name} exceeded limit of #{limit}!"
|
|
249
249
|
end
|
|
250
250
|
end
|
|
251
251
|
|
|
@@ -255,18 +255,18 @@ module Protocol
|
|
|
255
255
|
header_count = 0
|
|
256
256
|
|
|
257
257
|
# Read headers until empty line
|
|
258
|
-
while line = read_line(header_size, @
|
|
258
|
+
while line = read_line(header_size, @header_size_limit, allowance: 2, chomp: true)
|
|
259
259
|
if line.empty?
|
|
260
260
|
break # End of headers
|
|
261
261
|
end
|
|
262
262
|
|
|
263
263
|
header_size += line.bytesize + 2
|
|
264
|
-
check_limit(:header_size, header_size, @
|
|
264
|
+
check_limit(:header_size, header_size, @header_size_limit)
|
|
265
265
|
|
|
266
266
|
if match = line.match(HEADER_PATTERN)
|
|
267
267
|
# Parse header line (name: value)
|
|
268
268
|
header_count += 1
|
|
269
|
-
check_limit(:header_count, header_count, @
|
|
269
|
+
check_limit(:header_count, header_count, @header_count_limit)
|
|
270
270
|
|
|
271
271
|
fields << [match[1], match[2].strip]
|
|
272
272
|
else
|
data/readme.md
CHANGED
|
@@ -6,6 +6,16 @@
|
|
|
6
6
|
|
|
7
7
|
Please see the [project releases](https://socketry.github.io/protocol-multipart/releases/index) for all releases.
|
|
8
8
|
|
|
9
|
+
### v0.4.0
|
|
10
|
+
|
|
11
|
+
- Use consistent limit naming for multipart parser constraints.
|
|
12
|
+
|
|
13
|
+
### v0.3.0
|
|
14
|
+
|
|
15
|
+
- Add a configurable `Protocol::Multipart::FormData::Parser` which parses a streaming body and explicit boundary into nested arguments.
|
|
16
|
+
- Use `Protocol::URL::FormData::Nested` so URL-encoded and multipart forms share hierarchy semantics.
|
|
17
|
+
- Allow `Protocol::Multipart::FormData::Parser#parse` to populate a supplied result object.
|
|
18
|
+
|
|
9
19
|
### v0.2.0
|
|
10
20
|
|
|
11
21
|
- Add strict, policy-driven parsing for parameterized `Content-Type` and `Content-Disposition` fields using `Protocol::Multipart::Headers`.
|
data/releases.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
+
## v0.4.0
|
|
4
|
+
|
|
5
|
+
- Use consistent limit naming for multipart parser constraints.
|
|
6
|
+
|
|
7
|
+
## v0.3.0
|
|
8
|
+
|
|
9
|
+
- Add a configurable `Protocol::Multipart::FormData::Parser` which parses a streaming body and explicit boundary into nested arguments.
|
|
10
|
+
- Use `Protocol::URL::FormData::Nested` so URL-encoded and multipart forms share hierarchy semantics.
|
|
11
|
+
- Allow `Protocol::Multipart::FormData::Parser#parse` to populate a supplied result object.
|
|
12
|
+
|
|
3
13
|
## v0.2.0
|
|
4
14
|
|
|
5
15
|
- Add strict, policy-driven parsing for parameterized `Content-Type` and `Content-Disposition` fields using `Protocol::Multipart::Headers`.
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: protocol-multipart
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Williams
|
|
@@ -66,6 +66,20 @@ dependencies:
|
|
|
66
66
|
- - "~>"
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
68
|
version: '0.67'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: protocol-url
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0.5'
|
|
76
|
+
type: :runtime
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0.5'
|
|
69
83
|
executables: []
|
|
70
84
|
extensions: []
|
|
71
85
|
extra_rdoc_files: []
|
|
@@ -75,6 +89,7 @@ files:
|
|
|
75
89
|
- lib/protocol/multipart/byte_limit.rb
|
|
76
90
|
- lib/protocol/multipart/escape.rb
|
|
77
91
|
- lib/protocol/multipart/form_data.rb
|
|
92
|
+
- lib/protocol/multipart/form_data/parser.rb
|
|
78
93
|
- lib/protocol/multipart/header/content_disposition.rb
|
|
79
94
|
- lib/protocol/multipart/header/content_type.rb
|
|
80
95
|
- lib/protocol/multipart/header/parameterized.rb
|
metadata.gz.sig
CHANGED
|
Binary file
|