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 +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 +2 -44
- data/lib/protocol/multipart/version.rb +1 -1
- data/readme.md +6 -0
- data/releases.md +6 -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: 891a0cd8210426411b1b44e2304bdfb282ab907adf1f3bf53089b2ecd6407a44
|
|
4
|
+
data.tar.gz: 7d24bcf5941c609e11dea28e1554e03c8696282a90f5d344c3aad580fa1484de
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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"
|
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.
|
|
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
|