protocol-multipart 0.2.0 → 0.3.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: 6f780cb8777ab2bcdacd17295c0017d194f0e43e864b76064c2f5a17feaeebce
4
- data.tar.gz: 5b6c27f78eb2dc61fe351ae3c493847413580af1b6c194cd090a6eeca9a46ad6
3
+ metadata.gz: 891a0cd8210426411b1b44e2304bdfb282ab907adf1f3bf53089b2ecd6407a44
4
+ data.tar.gz: 7d24bcf5941c609e11dea28e1554e03c8696282a90f5d344c3aad580fa1484de
5
5
  SHA512:
6
- metadata.gz: '08d99314fcd9dbafde62056380a4d17a4288f5d0fef2cc33a00ac55ddcfd9bf2dfeaabf4e26c209a8b94b7bd01c553e883767323143b7f2f55e04e02a571ef0c'
7
- data.tar.gz: 0770603bb53169f98a8fd2582d464c1677fe906fddbebc3470f14e426d3bdc4db9cf05a3757ba615724fefa9510ad7cb11085becbc884d7950593a93ae00f3f1
6
+ metadata.gz: f515cd40482a06d977d689f2d43a917aef4193f5bbd2e23d0fe7b13fdc5984d42fbe5a8841736ac1d3deb1e9fa8ced71788ec17d5aacd4cedd2e17f3ac75219e
7
+ data.tar.gz: e96bc422119c3c7001b3e6588281964ebab9445a6a52e8293ffb1035f01172a2f77e6497db43f36b2cd7cf4cbc319714c8352c4ae4946d8d9dbc0bc87e7d5441
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 maximum_field_size [Integer | Nil] The maximum size of each buffered field.
19
+ # @parameter maximum_upload_size [Integer | Nil] The maximum size of each file upload.
20
+ # @parameter maximum_total_size [Integer | Nil] The maximum combined size of all fields and uploads.
21
+ # @parameter maximum_depth [Integer | Nil] The maximum depth of a bracketed form name.
22
+ # @parameter options [Hash] Limits passed to the underlying multipart parser.
23
+ def initialize(maximum_field_size: MAXIMUM_FIELD_SIZE, maximum_upload_size: MAXIMUM_UPLOAD_SIZE, maximum_total_size: MAXIMUM_TOTAL_SIZE, maximum_depth: Protocol::URL::FormData::Nested::MAXIMUM_DEPTH, **options)
24
+ if maximum_depth and maximum_depth < 0
25
+ raise ArgumentError, "Form data limits must be non-negative!"
26
+ end
27
+
28
+ @maximum_field_size = maximum_field_size
29
+ @maximum_upload_size = maximum_upload_size
30
+ @maximum_total_size = maximum_total_size
31
+ @maximum_depth = maximum_depth
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_limit = ByteLimit.new(@maximum_total_size, 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, @maximum_upload_size, total_limit)
81
+ yield name, upload
82
+ upload.discard
83
+ else
84
+ field_limit = ByteLimit.new(@maximum_field_size, name: :field_size)
85
+ value = String.new.b
86
+
87
+ part.each do |chunk|
88
+ field_limit.consume(chunk.bytesize)
89
+ total_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(maximum_depth: @maximum_depth)
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -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"
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Protocol
7
7
  module Multipart
8
- VERSION = "0.2.0"
8
+ VERSION = "0.3.0"
9
9
  end
10
10
  end
data/readme.md CHANGED
@@ -6,6 +6,12 @@
6
6
 
7
7
  Please see the [project releases](https://socketry.github.io/protocol-multipart/releases/index) for all releases.
8
8
 
9
+ ### v0.3.0
10
+
11
+ - Add a configurable `Protocol::Multipart::FormData::Parser` which parses a streaming body and explicit boundary into nested arguments.
12
+ - Use `Protocol::URL::FormData::Nested` so URL-encoded and multipart forms share hierarchy semantics.
13
+ - Allow `Protocol::Multipart::FormData::Parser#parse` to populate a supplied result object.
14
+
9
15
  ### v0.2.0
10
16
 
11
17
  - Add strict, policy-driven parsing for parameterized `Content-Type` and `Content-Disposition` fields using `Protocol::Multipart::Headers`.
data/releases.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Releases
2
2
 
3
+ ## v0.3.0
4
+
5
+ - Add a configurable `Protocol::Multipart::FormData::Parser` which parses a streaming body and explicit boundary into nested arguments.
6
+ - Use `Protocol::URL::FormData::Nested` so URL-encoded and multipart forms share hierarchy semantics.
7
+ - Allow `Protocol::Multipart::FormData::Parser#parse` to populate a supplied result object.
8
+
3
9
  ## v0.2.0
4
10
 
5
11
  - 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.2.0
4
+ version: 0.3.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