protocol-multipart 0.1.1 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8020a17a66f95c8dcc80853403a434db667e787dcc1e7e33a3402c72f12356c1
4
- data.tar.gz: 6779e480527eec3c7fa1ca93663d823c89bc326cc27e48212d4d42c7e91c9ca9
3
+ metadata.gz: 6f780cb8777ab2bcdacd17295c0017d194f0e43e864b76064c2f5a17feaeebce
4
+ data.tar.gz: 5b6c27f78eb2dc61fe351ae3c493847413580af1b6c194cd090a6eeca9a46ad6
5
5
  SHA512:
6
- metadata.gz: 8081e11c625c5b8e68e7f1a19a8e7c814ba7b9fc092d2ad91877b56d2f9e6822aacbe777891d667a7d5bf0ef802ded0882656146f38f7c8ce40b63d643d8d10d
7
- data.tar.gz: eb2e3cf8796c4ac0eb1146d834732eb1e140215c3605c0933cbadbf6a7dc8fceb2a01bd9f9902a1b78bb7c1c189ae5b8e8f79d5a5be14f62bf568af9d5aa2e0e
6
+ metadata.gz: '08d99314fcd9dbafde62056380a4d17a4288f5d0fef2cc33a00ac55ddcfd9bf2dfeaabf4e26c209a8b94b7bd01c553e883767323143b7f2f55e04e02a571ef0c'
7
+ data.tar.gz: 0770603bb53169f98a8fd2582d464c1677fe906fddbebc3470f14e426d3bdc4db9cf05a3757ba615724fefa9510ad7cb11085becbc884d7950593a93ae00f3f1
checksums.yaml.gz.sig CHANGED
Binary file
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ module Protocol
7
+ module Multipart
8
+ # Tracks consumed bytes against an optional maximum.
9
+ class ByteLimit
10
+ # Initialize a byte limit.
11
+ # @parameter maximum [Integer | Nil] The maximum number of bytes, or nil for no limit.
12
+ # @parameter name [Symbol] The name used when reporting a limit violation.
13
+ def initialize(maximum, name: :size)
14
+ if maximum and maximum < 0
15
+ raise ArgumentError, "Multipart limits must be non-negative!"
16
+ end
17
+
18
+ @maximum = maximum
19
+ @name = name
20
+ @size = 0
21
+ end
22
+
23
+ # The number of bytes consumed.
24
+ attr :size
25
+
26
+ # Consume the given number of bytes.
27
+ # @parameter size [Integer] The number of bytes to consume.
28
+ # @returns [Integer] The total number of bytes consumed.
29
+ def consume(size)
30
+ @size += size
31
+
32
+ if @maximum and @size > @maximum
33
+ raise RangeError, "Multipart #{@name} exceeded limit of #{@maximum}!"
34
+ end
35
+
36
+ return @size
37
+ end
38
+ end
39
+ end
40
+ end
@@ -4,8 +4,10 @@
4
4
  # Copyright, 2025, by Samuel Williams.
5
5
 
6
6
  require_relative "mixed"
7
+ require_relative "parser"
7
8
  require_relative "string_part"
8
9
  require_relative "escape"
10
+ require_relative "byte_limit"
9
11
 
10
12
  module Protocol
11
13
  module Multipart
@@ -14,6 +16,112 @@ module Protocol
14
16
  class FormData < Mixed
15
17
  include Escape
16
18
 
19
+ # The default maximum size of a buffered form field.
20
+ MAXIMUM_FIELD_SIZE = 2 * 1024 * 1024
21
+
22
+ # The default maximum size of a streamed file upload.
23
+ MAXIMUM_UPLOAD_SIZE = 128 * 1024 * 1024
24
+
25
+ # The default maximum combined size of all form fields and file uploads.
26
+ MAXIMUM_TOTAL_SIZE = 256 * 1024 * 1024
27
+
28
+ # A file upload yielded while parsing form data.
29
+ class Upload
30
+ # Initialize a streamed file upload.
31
+ # @parameter part [Parser::Part] The underlying multipart part.
32
+ # @parameter filename [String] The submitted filename.
33
+ # @parameter maximum_size [Integer | Nil] The maximum upload size.
34
+ # @parameter total_limit [ByteLimit] The shared form-data size limit.
35
+ def initialize(part, filename, maximum_size, total_limit)
36
+ @part = part
37
+ @filename = filename
38
+ @limit = ByteLimit.new(maximum_size, name: :upload_size)
39
+ @total_limit = total_limit
40
+ end
41
+
42
+ # The submitted filename.
43
+ attr :filename
44
+
45
+ # The multipart headers associated with this upload.
46
+ def headers
47
+ @part.headers
48
+ end
49
+
50
+ # The number of upload bytes consumed so far.
51
+ def size
52
+ @limit.size
53
+ end
54
+
55
+ # Whether the complete upload has been consumed.
56
+ def ended?
57
+ @part.ended?
58
+ end
59
+
60
+ # Iterate over the upload body.
61
+ # @parameter chunk_size [Integer] The maximum chunk size.
62
+ def each(chunk_size = 8192)
63
+ return to_enum(:each, chunk_size) unless block_given?
64
+
65
+ @part.each(chunk_size) do |chunk|
66
+ @limit.consume(chunk.bytesize)
67
+ @total_limit.consume(chunk.bytesize)
68
+ yield chunk
69
+ end
70
+
71
+ return self
72
+ end
73
+
74
+ # Consume any unread upload body while applying its limits.
75
+ def discard
76
+ self.each{|chunk|}
77
+ return nil
78
+ end
79
+ end
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
+
17
125
  # Returns the MIME type for form data.
18
126
  #
19
127
  # @returns [String] The MIME type "multipart/form-data".
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require_relative "parameterized"
7
+
8
+ module Protocol
9
+ module Multipart
10
+ module Header
11
+ # A MIME Content-Disposition header value.
12
+ class ContentDisposition < Parameterized
13
+ VALUE_PATTERN = /\A[ \t]*(#{TOKEN})[ \t]*/.freeze
14
+ NAME = "content-disposition"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require_relative "parameterized"
7
+
8
+ module Protocol
9
+ module Multipart
10
+ module Header
11
+ # A MIME Content-Type header value.
12
+ class ContentType < Parameterized
13
+ VALUE_PATTERN = /\A[ \t]*(#{TOKEN}\/#{TOKEN})[ \t]*/.freeze
14
+ NAME = "content-type"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -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 "protocol/http/error"
7
+
8
+ module Protocol
9
+ module Multipart
10
+ module Header
11
+ # A parameterized MIME header value.
12
+ class Parameterized
13
+ TOKEN = "[!#$%&'*+\\-.^_`|~0-9A-Za-z]+"
14
+ VALUE_PATTERN = /\A[ \t]*(#{TOKEN}(?:\/#{TOKEN})?)[ \t]*/.freeze
15
+ PARAMETER_PATTERN = /\G;[ \t]*(#{TOKEN})[ \t]*=[ \t]*(?:"((?:\\[^\x00-\x1f\x7f]|[^"\\\x00-\x1f\x7f])*)"|(#{TOKEN}))[ \t]*/.freeze
16
+ NAME = nil
17
+
18
+ # Parse a parameterized header value.
19
+ # @parameter string [String] The header value.
20
+ # @returns [Parameterized] The parsed header.
21
+ # @raises [ArgumentError] If the header value is malformed or contains duplicate parameters.
22
+ def self.parse(string)
23
+ unless match = self::VALUE_PATTERN.match(string)
24
+ raise ArgumentError, "Invalid header value: #{string.inspect}!"
25
+ end
26
+
27
+ type = match[1].downcase
28
+ parameters = {}
29
+ offset = match.end(0)
30
+
31
+ while offset < string.length
32
+ unless match = PARAMETER_PATTERN.match(string, offset)
33
+ raise ArgumentError, "Invalid header parameter at offset #{offset}: #{string.inspect}!"
34
+ end
35
+
36
+ name = match[1].downcase
37
+
38
+ if parameters.key?(name)
39
+ raise ArgumentError, "Duplicate header parameter: #{name.inspect}!"
40
+ end
41
+
42
+ if quoted = match[2]
43
+ parameters[name] = quoted.gsub(/\\(.)/, "\\1")
44
+ else
45
+ parameters[name] = match[3]
46
+ end
47
+
48
+ offset = match.end(0)
49
+ end
50
+
51
+ return new(type, parameters)
52
+ end
53
+
54
+ # Coerce an object to a parameterized header value.
55
+ # @parameter value [Object] The header value.
56
+ # @returns [Parameterized] The parsed header.
57
+ def self.coerce(value)
58
+ if value.is_a?(self)
59
+ return value
60
+ end
61
+
62
+ return parse(value.to_s)
63
+ end
64
+
65
+ # Initialize a parameterized header value.
66
+ # @parameter type [String] The primary header value.
67
+ # @parameter parameters [Hash] The named parameters.
68
+ def initialize(type, parameters = {})
69
+ @type = type
70
+ @parameters = parameters
71
+ end
72
+
73
+ # The primary header value.
74
+ attr :type
75
+
76
+ # The named parameters.
77
+ attr :parameters
78
+
79
+ # Fetch a named parameter.
80
+ # @parameter name [String] The case-insensitive parameter name.
81
+ # @returns [String | Nil] The parameter value, if present.
82
+ def [](name)
83
+ @parameters[name.downcase]
84
+ end
85
+
86
+ # Reject a second value for this singular MIME field.
87
+ def <<(value)
88
+ raise Protocol::HTTP::DuplicateHeaderError.new(self.class::NAME, self, value)
89
+ end
90
+
91
+ # Convert the header to its wire representation.
92
+ # @returns [String] The serialized header value.
93
+ def to_s
94
+ value = String.new(@type)
95
+
96
+ @parameters.each do |name, parameter|
97
+ escaped = parameter.gsub(/["\\]/, "\\\\\\0")
98
+ value << "; #{name}=\"#{escaped}\""
99
+ end
100
+
101
+ return value
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ require "protocol/http/headers"
7
+
8
+ require_relative "header/content_disposition"
9
+ require_relative "header/content_type"
10
+
11
+ module Protocol
12
+ module Multipart
13
+ # @namespace
14
+ module Header
15
+ end
16
+
17
+ # The header fields associated with a multipart body or part.
18
+ class Headers < Protocol::HTTP::Headers
19
+ POLICY = {
20
+ "content-description" => false,
21
+ "content-disposition" => Header::ContentDisposition,
22
+ "content-id" => false,
23
+ "content-length" => false,
24
+ "content-transfer-encoding" => false,
25
+ "content-type" => Header::ContentType,
26
+ "mime-version" => false,
27
+ }.tap do |policy|
28
+ policy.default = Protocol::HTTP::Header::Multiple
29
+ end
30
+
31
+ # Initialize the multipart headers.
32
+ # @parameter fields [Array] An array of `[key, value]` pairs.
33
+ # @parameter tail [Integer | Nil] The index of the trailer start.
34
+ # @parameter indexed [Hash | Nil] The cached header index.
35
+ # @parameter policy [Hash] The header normalization policy.
36
+ def initialize(fields = [], tail = nil, indexed: nil, policy: POLICY)
37
+ super
38
+ end
39
+ end
40
+ end
41
+ end
@@ -4,18 +4,34 @@
4
4
  # Copyright, 2025, by Samuel Williams.
5
5
 
6
6
  require "io/stream"
7
+ require_relative "headers"
7
8
 
8
9
  module Protocol
9
10
  module Multipart
10
11
  # A parser for multipart data based on RFC 2046 and RFC 2387.
11
12
  # Parses multipart bodies and provides an enumerable interface to access the parts.
12
13
  class Parser
14
+ HEADER_PATTERN = /\A([!-9;-~]+):[ \t]*([^\x00-\x08\x0a-\x1f\x7f]*)\z/.freeze
15
+ private_constant :HEADER_PATTERN
16
+
17
+ # The default maximum number of preamble bytes before the first boundary.
18
+ MAXIMUM_PREAMBLE_SIZE = 64 * 1024
19
+
20
+ # The default maximum number of header bytes in each part.
21
+ MAXIMUM_HEADER_SIZE = 64 * 1024
22
+
23
+ # The default maximum number of headers in each part.
24
+ MAXIMUM_HEADER_COUNT = 64
25
+
26
+ # The default maximum number of parts.
27
+ MAXIMUM_PART_COUNT = 128
28
+
13
29
  # Represents a single part within a multipart message.
14
30
  class Part
15
31
  # Initialize a new part with a readable stream, headers, and a boundary string.
16
32
  #
17
33
  # @parameter readable [IO::Stream] The readable stream that contains the part's data.
18
- # @parameter headers [Hash] The headers associated with this part.
34
+ # @parameter headers [Headers] The headers associated with this part.
19
35
  # @parameter boundary [String] The boundary string used to separate parts.
20
36
  def initialize(readable, headers, boundary)
21
37
  @readable = readable
@@ -25,7 +41,7 @@ module Protocol
25
41
  @is_closing = false
26
42
  end
27
43
 
28
- # @attribute [Hash] The headers associated with this part.
44
+ # @attribute [Headers] The headers associated with this part.
29
45
  attr_reader :headers
30
46
 
31
47
  # Iterate through the part content in chunks.
@@ -147,9 +163,23 @@ module Protocol
147
163
  #
148
164
  # @parameter readable [IO, IO::Stream] The readable stream containing multipart data.
149
165
  # @parameter boundary [String] The boundary string that separates the parts.
150
- def initialize(readable, boundary)
166
+ # @parameter maximum_preamble_size [Integer | Nil] The maximum preamble size, or nil for no limit.
167
+ # @parameter maximum_header_size [Integer | Nil] The maximum header size per part, or nil for no limit.
168
+ # @parameter maximum_header_count [Integer | Nil] The maximum header count per part, or nil for no limit.
169
+ # @parameter maximum_part_count [Integer | Nil] The maximum part count, or nil for no limit.
170
+ def initialize(readable, boundary, maximum_preamble_size: MAXIMUM_PREAMBLE_SIZE, maximum_header_size: MAXIMUM_HEADER_SIZE, maximum_header_count: MAXIMUM_HEADER_COUNT, maximum_part_count: MAXIMUM_PART_COUNT)
171
+ limits = [maximum_preamble_size, maximum_header_size, maximum_header_count, maximum_part_count]
172
+
173
+ if limits.any?{|limit| limit and limit < 0}
174
+ raise ArgumentError, "Multipart limits must be non-negative!"
175
+ end
176
+
151
177
  @readable = IO::Stream(readable)
152
178
  @boundary = boundary
179
+ @maximum_preamble_size = maximum_preamble_size
180
+ @maximum_header_size = maximum_header_size
181
+ @maximum_header_count = maximum_header_count
182
+ @maximum_part_count = maximum_part_count
153
183
 
154
184
  @boundary_marker = "--#{@boundary}\r\n".freeze
155
185
  end
@@ -161,11 +191,16 @@ module Protocol
161
191
  def each
162
192
  return to_enum unless block_given?
163
193
 
194
+ preamble_size = 0
195
+
164
196
  # Read lines until we find the first boundary:
165
197
  while true
166
- if line = @readable.gets("\r\n", chomp: false)
198
+ if line = read_line(preamble_size, @maximum_preamble_size, allowance: @boundary_marker.bytesize, chomp: false)
167
199
  if line == @boundary_marker
168
200
  break
201
+ else
202
+ preamble_size += line.bytesize
203
+ check_limit(:preamble_size, preamble_size, @maximum_preamble_size)
169
204
  end
170
205
  else
171
206
  # End of stream reached without finding boundary:
@@ -173,18 +208,21 @@ module Protocol
173
208
  end
174
209
  end
175
210
 
211
+ part_count = 0
212
+
176
213
  while true
214
+ part_count += 1
215
+ check_limit(:part_count, part_count, @maximum_part_count)
216
+
177
217
  part = read_part
178
218
  break unless part
179
219
 
180
220
  if part.read_empty_boundary?
181
221
  else
182
- begin
183
- yield part
184
- ensure
185
- # After yielding, ensure the part is finished to advance to the next boundary. This is either a no-op if user already read the part, or reads remaining data.
186
- part.discard
187
- end
222
+ yield part
223
+
224
+ # Advance to the next boundary after the consumer returns normally. If the consumer raises, stop parsing without draining the request body.
225
+ part.discard
188
226
  end
189
227
 
190
228
  # Check if this was the last part:
@@ -196,34 +234,41 @@ module Protocol
196
234
 
197
235
  private
198
236
 
237
+ def read_line(size, maximum, allowance: 0, chomp:)
238
+ if maximum
239
+ limit = maximum - size + allowance + 1
240
+ return @readable.gets("\r\n", limit, chomp: chomp)
241
+ else
242
+ return @readable.gets("\r\n", chomp: chomp)
243
+ end
244
+ end
245
+
246
+ def check_limit(name, value, maximum)
247
+ if maximum and value > maximum
248
+ raise RangeError, "Multipart #{name} exceeded limit of #{maximum}!"
249
+ end
250
+ end
251
+
199
252
  def read_part
200
- headers = {}
201
- value = nil
253
+ fields = []
254
+ header_size = 0
255
+ header_count = 0
202
256
 
203
257
  # Read headers until empty line
204
- while line = @readable.gets("\r\n", chomp: true)
258
+ while line = read_line(header_size, @maximum_header_size, allowance: 2, chomp: true)
205
259
  if line.empty?
206
260
  break # End of headers
207
- elsif match = line.match(/^\s+([^:]+)$/)
208
- if value
209
- value << " " << match[1]
210
- else
211
- raise RuntimeError, "Unexpected whitespace before header name: #{line.inspect}"
212
- end
213
- elsif match = line.match(/^([^:]+):\s*(.*)$/)
261
+ end
262
+
263
+ header_size += line.bytesize + 2
264
+ check_limit(:header_size, header_size, @maximum_header_size)
265
+
266
+ if match = line.match(HEADER_PATTERN)
214
267
  # Parse header line (name: value)
215
- name = match[1].strip.downcase
216
- value = match[2].strip
268
+ header_count += 1
269
+ check_limit(:header_count, header_count, @maximum_header_count)
217
270
 
218
- if current = headers[name]
219
- if current.is_a?(Array)
220
- current << value
221
- else
222
- headers[name] = [current, value]
223
- end
224
- else
225
- headers[name] = value
226
- end
271
+ fields << [match[1], match[2].strip]
227
272
  else
228
273
  raise RuntimeError, "Invalid header line: #{line.inspect}"
229
274
  end
@@ -233,7 +278,7 @@ module Protocol
233
278
  raise EOFError, "Unexpected end of stream while reading headers!"
234
279
  end
235
280
 
236
- return Part.new(@readable, headers, @boundary)
281
+ return Part.new(@readable, Headers.new(fields), @boundary)
237
282
  end
238
283
  end
239
284
  end
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Protocol
7
7
  module Multipart
8
- VERSION = "0.1.1"
8
+ VERSION = "0.2.0"
9
9
  end
10
10
  end
@@ -4,6 +4,8 @@
4
4
  # Copyright, 2025, by Samuel Williams.
5
5
 
6
6
  require_relative "multipart/version"
7
+ require_relative "multipart/byte_limit"
8
+ require_relative "multipart/headers"
7
9
  require_relative "multipart/parser"
8
10
  require_relative "multipart/mixed"
9
11
  require_relative "multipart/part"
data/readme.md CHANGED
@@ -6,6 +6,13 @@
6
6
 
7
7
  Please see the [project releases](https://socketry.github.io/protocol-multipart/releases/index) for all releases.
8
8
 
9
+ ### v0.2.0
10
+
11
+ - Add strict, policy-driven parsing for parameterized `Content-Type` and `Content-Disposition` fields using `Protocol::Multipart::Headers`.
12
+ - Limit multipart preamble size, header size, header count and part count by default.
13
+ - Add `Protocol::Multipart::ByteLimit` for limiting streamed multipart content.
14
+ - Add streaming multipart form-data parsing with field, upload and total content limits.
15
+
9
16
  ### v0.1.0
10
17
 
11
18
  ## Contributing
@@ -18,6 +25,22 @@ We welcome contributions to this project.
18
25
  4. Push to the branch (`git push origin my-new-feature`).
19
26
  5. Create new Pull Request.
20
27
 
28
+ ### Running Tests
29
+
30
+ To run the test suite:
31
+
32
+ ``` shell
33
+ bundle exec sus
34
+ ```
35
+
36
+ ### Making Releases
37
+
38
+ To make a new release:
39
+
40
+ ``` shell
41
+ bundle exec bake gem:release:patch # or minor or major
42
+ ```
43
+
21
44
  ### Developer Certificate of Origin
22
45
 
23
46
  In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
data/releases.md CHANGED
@@ -1,3 +1,10 @@
1
1
  # Releases
2
2
 
3
+ ## v0.2.0
4
+
5
+ - Add strict, policy-driven parsing for parameterized `Content-Type` and `Content-Disposition` fields using `Protocol::Multipart::Headers`.
6
+ - Limit multipart preamble size, header size, header count and part count by default.
7
+ - Add `Protocol::Multipart::ByteLimit` for limiting streamed multipart content.
8
+ - Add streaming multipart form-data parsing with field, upload and total content limits.
9
+
3
10
  ## v0.1.0
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.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -52,14 +52,33 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0.8'
55
+ - !ruby/object:Gem::Dependency
56
+ name: protocol-http
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.67'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.67'
55
69
  executables: []
56
70
  extensions: []
57
71
  extra_rdoc_files: []
58
72
  files:
59
73
  - lib/protocol/multipart.rb
60
74
  - lib/protocol/multipart/boundary.rb
75
+ - lib/protocol/multipart/byte_limit.rb
61
76
  - lib/protocol/multipart/escape.rb
62
77
  - lib/protocol/multipart/form_data.rb
78
+ - lib/protocol/multipart/header/content_disposition.rb
79
+ - lib/protocol/multipart/header/content_type.rb
80
+ - lib/protocol/multipart/header/parameterized.rb
81
+ - lib/protocol/multipart/headers.rb
63
82
  - lib/protocol/multipart/io_part.rb
64
83
  - lib/protocol/multipart/mixed.rb
65
84
  - lib/protocol/multipart/parser.rb
@@ -82,14 +101,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
82
101
  requirements:
83
102
  - - ">="
84
103
  - !ruby/object:Gem::Version
85
- version: '3.2'
104
+ version: '3.3'
86
105
  required_rubygems_version: !ruby/object:Gem::Requirement
87
106
  requirements:
88
107
  - - ">="
89
108
  - !ruby/object:Gem::Version
90
109
  version: '0'
91
110
  requirements: []
92
- rubygems_version: 3.6.7
111
+ rubygems_version: 4.0.10
93
112
  specification_version: 4
94
113
  summary: Provides abstractions to handle the multipart format.
95
114
  test_files: []
metadata.gz.sig CHANGED
Binary file