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 +7 -0
- checksums.yaml.gz.sig +0 -0
- data/lib/protocol/multipart/boundary.rb +20 -0
- data/lib/protocol/multipart/escape.rb +22 -0
- data/lib/protocol/multipart/form_data.rb +41 -0
- data/lib/protocol/multipart/io_part.rb +60 -0
- data/lib/protocol/multipart/mixed.rb +84 -0
- data/lib/protocol/multipart/parser.rb +240 -0
- data/lib/protocol/multipart/part.rb +30 -0
- data/lib/protocol/multipart/string_part.rb +34 -0
- data/lib/protocol/multipart/version.rb +10 -0
- data/lib/protocol/multipart.rb +16 -0
- data/license.md +21 -0
- data/readme.md +27 -0
- data/releases.md +3 -0
- data.tar.gz.sig +0 -0
- metadata +95 -0
- metadata.gz.sig +0 -0
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,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
|
+
[](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
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
|