protocol-multipart 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 404efdeaff535479636d3b9106596c80064d24dc9b7b5f9022a5cadee9cd3a0d
4
+ data.tar.gz: 85229551ef122a278ee1e1cf638267bef7383871093c645205da1133b5e490a6
5
+ SHA512:
6
+ metadata.gz: 0d07255671ea2766adca286ffa8924310ffba1d63a5bdcb844ff8eb8623b1119cc7b00b6d3e95046ef84d8fcf5e71b59b873d83dca53a58c351ebb0ace8f5d53
7
+ data.tar.gz: d91be37cf9c4cf5892aa78d64848332f7321c6a9cbdbdf5713bbf4ec0d67f4d8615840e71faf13165d6511811f8efa1771657531b1c4b6447f7ed2b6bfcdcebd
checksums.yaml.gz.sig ADDED
Binary file
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require "securerandom"
7
+
8
+ module Protocol
9
+ module Multipart
10
+ # Generate a secure boundary string for multipart messages.
11
+ # Approximately 192 bits of entropy by default, which is sufficient for most applications.
12
+ def self.secure_boundary(prefix = nil, length = 24)
13
+ if prefix
14
+ "#{prefix}-#{SecureRandom.urlsafe_base64(length, false)}"
15
+ else
16
+ SecureRandom.urlsafe_base64(length, false)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ module Protocol
7
+ module Multipart
8
+ # Utilities for escaping and unescaping field names and values in multipart data
9
+ module Escape
10
+ # Escape field names according to RFC 7578 and RFC 2046
11
+ # Quotes and backslashes need to be escaped with backslashes
12
+ def escape_field_name(name)
13
+ name.to_s.gsub(/([\\"])/, '\\\\\1')
14
+ end
15
+
16
+ # Unescape field names that were escaped with escape_field_name
17
+ def unescape_field_name(name)
18
+ name.to_s.gsub(/\\([\\"])/, '\1')
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require_relative "mixed"
7
+ require_relative "string_part"
8
+ require_relative "escape"
9
+
10
+ module Protocol
11
+ module Multipart
12
+ # FormData class for handling multipart/form-data format used in HTTP forms.
13
+ # Extends Mixed to provide specific support for form fields with names and values.
14
+ class FormData < Mixed
15
+ include Escape
16
+
17
+ # Returns the MIME type for form data.
18
+ #
19
+ # @returns [String] The MIME type "multipart/form-data".
20
+ def self.mime_type
21
+ "multipart/form-data"
22
+ end
23
+
24
+ # Adds a form field to the multipart form data.
25
+ #
26
+ # @parameter name [String] The field name.
27
+ # @parameter value [String] The field value.
28
+ # @parameter headers [Hash] Additional headers for the field.
29
+ # @returns [StringPart] The created StringPart for the field.
30
+ def add_field(name, value, headers = {})
31
+ headers = headers.merge(
32
+ "content-disposition" => "form-data; name=\"#{escape_field_name name}\""
33
+ )
34
+
35
+ StringPart.new(headers, value).tap do |part|
36
+ @parts << part
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require_relative "part"
7
+ require_relative "escape"
8
+
9
+ module Protocol
10
+ module Multipart
11
+ # A part that contains IO content (files, streams, etc.).
12
+ class IOPart < Part
13
+ extend Escape
14
+
15
+ # Opens a file and creates an IOPart for it.
16
+ #
17
+ # @parameter path [String] The file path to open.
18
+ # @parameter mime_type [String | Nil] The MIME type of the file, or nil to use application/octet-stream.
19
+ # @parameter name [String | Nil] The name to use for the file, or nil to use the filename.
20
+ # @parameter headers [Hash] Additional headers for the part.
21
+ # @returns [IOPart] A new IOPart instance for the file.
22
+ def self.open(path, mime_type: nil, name: nil, headers: {})
23
+ io = File.open(path, "rb")
24
+
25
+ name ||= File.basename(path)
26
+
27
+ headers = headers.merge(
28
+ "content-disposition" => "attachment; filename=\"#{escape_field_name name}\"",
29
+ "content-type" => mime_type || "application/octet-stream"
30
+ )
31
+
32
+ return new(headers, io)
33
+ end
34
+
35
+ # Initialize a new IOPart with the given headers and IO object.
36
+ #
37
+ # @parameter headers [Hash] Headers for the part.
38
+ # @parameter io [IO] The IO object containing the part's data.
39
+ def initialize(headers, io)
40
+ super(headers)
41
+ @io = io
42
+ end
43
+
44
+ # The underlying IO object.
45
+ # @attribute [IO] The IO object containing the part's data.
46
+ attr_reader :io
47
+
48
+ # Write the part's content to the provided writable stream.
49
+ #
50
+ # @parameter writable [IO] The destination stream to write to.
51
+ # @parameter boundary [String] The boundary string for the multipart message.
52
+ def call(writable, boundary)
53
+ while chunk = @io.read(8192)
54
+ break if chunk.empty?
55
+ writable.write(chunk)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require_relative "part"
7
+ require_relative "boundary"
8
+
9
+ module Protocol
10
+ module Multipart
11
+ # Represents a multipart/mixed message.
12
+ # A composite part that contains multiple nested parts with a boundary separator.
13
+ class Mixed < Part
14
+ # Returns the MIME type for mixed multipart data.
15
+ #
16
+ # @returns [String] The MIME type "multipart/mixed".
17
+ def self.mime_type
18
+ "multipart/mixed"
19
+ end
20
+
21
+ # Initialize a new multipart/mixed container.
22
+ #
23
+ # @parameter headers [Hash] Headers for the multipart container.
24
+ # @parameter parts [Array] The parts to include in this multipart container.
25
+ # @parameter boundary [String] The boundary string to use for separating parts.
26
+ # @parameter mime_type [String] The MIME type to use for this container.
27
+ def initialize(headers = {}, parts = [], boundary: Multipart.secure_boundary, mime_type: self.class.mime_type)
28
+ super(headers)
29
+
30
+ @boundary = boundary
31
+ @parts = parts
32
+ @headers["content-type"] = "#{mime_type}; boundary=#{@boundary}"
33
+ end
34
+
35
+ # @attribute [String] The boundary string used to separate parts.
36
+ attr :boundary
37
+
38
+ # @attribute [Array(Part)] The parts of the body.
39
+ attr :parts
40
+
41
+ private def write_headers(writable, headers)
42
+ headers.each do |key, value|
43
+ writable.write("#{key}: #{value}\r\n")
44
+ end
45
+
46
+ writable.write("\r\n")
47
+ end
48
+
49
+ # Writes the multipart container and all its parts to the writable stream.
50
+ # This method serializes the multipart container, including all nested parts,
51
+ # with appropriate boundaries between them.
52
+ #
53
+ # @parameter writable [IO] The writable stream to write the multipart data to.
54
+ # @parameter boundary [String | Nil] The parent boundary string, if this is a nested multipart.
55
+ def call(writable, boundary = nil)
56
+ return if @parts.empty?
57
+
58
+ first = true
59
+ initial_boundary = "--#{@boundary}\r\n".freeze
60
+ middle_boundary = "\r\n--#{@boundary}\r\n".freeze
61
+
62
+ # Write each part:
63
+ @parts.each do |part|
64
+ if first
65
+ # Write the initial boundary:
66
+ writable.write(initial_boundary)
67
+ first = false
68
+ else
69
+ # Write the boundary before each part:
70
+ writable.write(middle_boundary)
71
+ end
72
+
73
+ self.write_headers(writable, part.headers)
74
+ part.call(writable, middle_boundary)
75
+ end
76
+
77
+ unless first
78
+ # Write the final boundary:
79
+ writable.write("\r\n--#{@boundary}--\r\n")
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,240 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require "io/stream"
7
+
8
+ module Protocol
9
+ module Multipart
10
+ # A parser for multipart data based on RFC 2046 and RFC 2387.
11
+ # Parses multipart bodies and provides an enumerable interface to access the parts.
12
+ class Parser
13
+ # Represents a single part within a multipart message.
14
+ class Part
15
+ # Initialize a new part with a readable stream, headers, and a boundary string.
16
+ #
17
+ # @parameter readable [IO::Stream] The readable stream that contains the part's data.
18
+ # @parameter headers [Hash] The headers associated with this part.
19
+ # @parameter boundary [String] The boundary string used to separate parts.
20
+ def initialize(readable, headers, boundary)
21
+ @readable = readable
22
+ @boundary = boundary
23
+ @headers = headers
24
+ @ended = false
25
+ @is_closing = false
26
+ end
27
+
28
+ # @attribute [Hash] The headers associated with this part.
29
+ attr_reader :headers
30
+
31
+ # Iterate through the part content in chunks.
32
+ #
33
+ # @parameter chunk_size [Integer] The size of each chunk to read.
34
+ # @returns [Enumerable] An enumerable of content chunks if no block given.
35
+ def each(chunk_size = 8192)
36
+ return to_enum(:each, chunk_size) unless block_given?
37
+
38
+ return unless @readable
39
+
40
+ boundary_marker = "\r\n--#{@boundary}"
41
+
42
+ # Stream data in chunks using read_until with a limit
43
+ while @readable
44
+ if chunk = @readable.read_until(boundary_marker, limit: chunk_size, chomp: true)
45
+ # We found the boundary, check if it's a closing boundary:
46
+ if suffix = @readable.read_until("\r\n", chomp: true)
47
+ @is_closing = (suffix == "--")
48
+ @ended = true
49
+ @readable = nil
50
+ else
51
+ @readable = nil
52
+ raise EOFError, "Unexpected end of stream while reading part data!"
53
+ end
54
+ else
55
+ chunk = @readable.read(chunk_size)
56
+ end
57
+
58
+ if chunk
59
+ yield chunk unless chunk.empty?
60
+ else
61
+ # No more data to read, break the loop:
62
+ break
63
+ end
64
+ end
65
+ end
66
+
67
+ # Checks if the next content is an empty boundary (part with no content).
68
+ #
69
+ # @returns [Boolean] True if an empty boundary was found and read, false otherwise.
70
+ def read_empty_boundary?
71
+ boundary_marker = "--#{@boundary}"
72
+ if @readable.peek(boundary_marker.bytesize) == boundary_marker
73
+ @readable.read(boundary_marker.bytesize)
74
+ self.read_boundary_suffix
75
+
76
+ return true
77
+ end
78
+
79
+ return false
80
+ end
81
+
82
+ # Reads the suffix after a boundary to determine if it's a closing boundary.
83
+ def read_boundary_suffix
84
+ # Read the rest of the boundary line to check if it's closing:
85
+ boundary_suffix = @readable.read(2)
86
+ if boundary_suffix == "--"
87
+ @is_closing = true
88
+ @ended = true
89
+ @readable = nil
90
+ elsif boundary_suffix == "\r\n"
91
+ @is_closing = false
92
+ @ended = true
93
+ @readable = nil
94
+ else
95
+ @readable = nil
96
+ raise EOFError, "Unexpected end of stream while reading part data!"
97
+ end
98
+ end
99
+
100
+ # Finishes reading this part's data and advances to the next boundary.
101
+ #
102
+ # @returns [String | Nil] The remaining content of the part, or nil if already finished.
103
+ def finish
104
+ return unless @readable
105
+
106
+ # Read all data until boundary
107
+ data = @readable.read_until("\r\n--#{@boundary}", chomp: true)
108
+
109
+ self.read_boundary_suffix
110
+
111
+ return data
112
+ end
113
+
114
+ # Efficiently discards all data until the next boundary is found.
115
+ # This is used to skip parts without reading their content into memory.
116
+ #
117
+ # @returns [Nil]
118
+ def discard
119
+ # Efficiently discard all data until boundary
120
+ return unless @readable
121
+
122
+ # Discard data until boundary
123
+ @readable.discard_until("\r\n--#{@boundary}")
124
+
125
+ self.read_boundary_suffix
126
+
127
+ return nil
128
+ end
129
+
130
+ # Checks if this part has been completely read.
131
+ #
132
+ # @returns [Boolean] True if this part has been completely read.
133
+ def ended?
134
+ @ended
135
+ end
136
+
137
+ # Checks if this part ends with a closing boundary.
138
+ # A closing boundary indicates that this is the last part in the multipart message.
139
+ #
140
+ # @returns [Boolean] True if this part ends with a closing boundary.
141
+ def closing_boundary?
142
+ @is_closing || (@readable.nil? && @is_closing)
143
+ end
144
+ end
145
+
146
+ # Initialize a new multipart parser.
147
+ #
148
+ # @parameter readable [IO, IO::Stream] The readable stream containing multipart data.
149
+ # @parameter boundary [String] The boundary string that separates the parts.
150
+ def initialize(readable, boundary)
151
+ @readable = IO::Stream(readable)
152
+ @boundary = boundary
153
+
154
+ @boundary_marker = "--#{@boundary}\r\n".freeze
155
+ end
156
+
157
+ # Enumerate through each part in the multipart data.
158
+ # Yields each part for processing. If no block is given, returns an enumerator.
159
+ #
160
+ # @returns [Enumerator, Boolean] An enumerator if no block given, or true when complete.
161
+ def each
162
+ return to_enum unless block_given?
163
+
164
+ # Read lines until we find the first boundary:
165
+ while true
166
+ if line = @readable.gets("\r\n", chomp: false)
167
+ if line == @boundary_marker
168
+ break
169
+ end
170
+ else
171
+ # End of stream reached without finding boundary:
172
+ raise EOFError, "No multipart boundary found in stream!"
173
+ end
174
+ end
175
+
176
+ while true
177
+ part = read_part
178
+ break unless part
179
+
180
+ if part.read_empty_boundary?
181
+ 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
188
+ end
189
+
190
+ # Check if this was the last part:
191
+ break if part.closing_boundary?
192
+ end
193
+
194
+ return true
195
+ end
196
+
197
+ private
198
+
199
+ def read_part
200
+ headers = {}
201
+ value = nil
202
+
203
+ # Read headers until empty line
204
+ while line = @readable.gets("\r\n", chomp: true)
205
+ if line.empty?
206
+ 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*(.*)$/)
214
+ # Parse header line (name: value)
215
+ name = match[1].strip.downcase
216
+ value = match[2].strip
217
+
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
227
+ else
228
+ raise RuntimeError, "Invalid header line: #{line.inspect}"
229
+ end
230
+ end
231
+
232
+ unless line
233
+ raise EOFError, "Unexpected end of stream while reading headers!"
234
+ end
235
+
236
+ return Part.new(@readable, headers, @boundary)
237
+ end
238
+ end
239
+ end
240
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ module Protocol
7
+ module Multipart
8
+ # Represents a part in a multipart message.
9
+ class Part
10
+ # Initialize a new part with the given headers.
11
+ #
12
+ # @parameter headers [Hash] The headers for this part.
13
+ # @returns [void]
14
+ def initialize(headers = {})
15
+ @headers = headers
16
+ end
17
+
18
+ # The headers associated with this part.
19
+ # @attribute [Hash] The headers as name/value pairs.
20
+ attr_accessor :headers
21
+
22
+ # Writes the part to the writable body.
23
+ #
24
+ # @parameter writable [IO] The writable stream to write the part to.
25
+ # @parameter boundary [String] The boundary string used to separate parts.
26
+ def call(writable, boundary = nil)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require_relative "part"
7
+
8
+ module Protocol
9
+ module Multipart
10
+ # A part that contains string content.
11
+ # Represents a multipart part with string data.
12
+ class StringPart < Part
13
+ # Initialize a new StringPart with headers and string content.
14
+ #
15
+ # @parameter headers [Hash] Headers for the part.
16
+ # @parameter content [String] The string content of the part.
17
+ def initialize(headers, content)
18
+ super(headers)
19
+ @content = content
20
+ end
21
+
22
+ # @attribute [String] The content of the part.
23
+ attr_reader :content
24
+
25
+ # Write the part's content to the provided writable stream.
26
+ #
27
+ # @parameter writable [IO] The destination stream to write to.
28
+ # @parameter boundary [String] The boundary string for the multipart message.
29
+ def call(writable, boundary)
30
+ writable.write(@content)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ module Protocol
7
+ module Multipart
8
+ VERSION = "0.1.0"
9
+ end
10
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require_relative "multipart/version"
7
+ require_relative "multipart/parser"
8
+ require_relative "multipart/body"
9
+ require_relative "multipart/part"
10
+
11
+ # @namespace
12
+ module Protocol
13
+ # @namespace
14
+ module Multipart
15
+ end
16
+ end
data/license.md ADDED
@@ -0,0 +1,21 @@
1
+ # MIT License
2
+
3
+ Copyright, 2025, by Samuel Williams.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/readme.md ADDED
@@ -0,0 +1,27 @@
1
+ # `Protocol::Multipart`
2
+
3
+ [![Development Status](https://github.com/socketry/protocol-multipart/workflows/Test/badge.svg)](https://github.com/socketry/protocol-multipart/actions?workflow=Test)
4
+
5
+ ## Releases
6
+
7
+ Please see the [project releases](https://socketry.github.io/protocol-multipart/releases/index) for all releases.
8
+
9
+ ### v0.1.0
10
+
11
+ ## Contributing
12
+
13
+ We welcome contributions to this project.
14
+
15
+ 1. Fork it.
16
+ 2. Create your feature branch (`git checkout -b my-new-feature`).
17
+ 3. Commit your changes (`git commit -am 'Add some feature'`).
18
+ 4. Push to the branch (`git push origin my-new-feature`).
19
+ 5. Create new Pull Request.
20
+
21
+ ### Developer Certificate of Origin
22
+
23
+ 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.
24
+
25
+ ### Community Guidelines
26
+
27
+ This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers.
data/releases.md ADDED
@@ -0,0 +1,3 @@
1
+ # Releases
2
+
3
+ ## v0.1.0
data.tar.gz.sig ADDED
Binary file
metadata ADDED
@@ -0,0 +1,95 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: protocol-multipart
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Samuel Williams
8
+ bindir: bin
9
+ cert_chain:
10
+ - |
11
+ -----BEGIN CERTIFICATE-----
12
+ MIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11
13
+ ZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK
14
+ CZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz
15
+ MjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd
16
+ MBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj
17
+ bzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB
18
+ igKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2
19
+ 9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW
20
+ sGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE
21
+ e5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN
22
+ XibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss
23
+ RZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn
24
+ tUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM
25
+ zp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW
26
+ xm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O
27
+ BBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs
28
+ aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs
29
+ aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE
30
+ cBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl
31
+ xCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/
32
+ c1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp
33
+ 8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws
34
+ JkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP
35
+ eX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt
36
+ Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
37
+ voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
38
+ -----END CERTIFICATE-----
39
+ date: 1980-01-02 00:00:00.000000000 Z
40
+ dependencies:
41
+ - !ruby/object:Gem::Dependency
42
+ name: io-stream
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.8'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.8'
55
+ executables: []
56
+ extensions: []
57
+ extra_rdoc_files: []
58
+ files:
59
+ - lib/protocol/multipart.rb
60
+ - lib/protocol/multipart/boundary.rb
61
+ - lib/protocol/multipart/escape.rb
62
+ - lib/protocol/multipart/form_data.rb
63
+ - lib/protocol/multipart/io_part.rb
64
+ - lib/protocol/multipart/mixed.rb
65
+ - lib/protocol/multipart/parser.rb
66
+ - lib/protocol/multipart/part.rb
67
+ - lib/protocol/multipart/string_part.rb
68
+ - lib/protocol/multipart/version.rb
69
+ - license.md
70
+ - readme.md
71
+ - releases.md
72
+ homepage: https://github.com/socketry/protocol-multipart
73
+ licenses:
74
+ - MIT
75
+ metadata:
76
+ documentation_uri: https://socketry.github.io/protocol-multipart/
77
+ source_code_uri: https://github.com/socketry/protocol-multipart.git
78
+ rdoc_options: []
79
+ require_paths:
80
+ - lib
81
+ required_ruby_version: !ruby/object:Gem::Requirement
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: '3.2'
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ requirements: []
92
+ rubygems_version: 3.6.7
93
+ specification_version: 4
94
+ summary: Provides abstractions to handle the multipart format.
95
+ test_files: []
metadata.gz.sig ADDED
Binary file