multipart-parser 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2011,2012 Daniel Abrahamsson
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
data/README ADDED
@@ -0,0 +1,17 @@
1
+ multipart-parser is a simple parser for multipart MIME messages, written in Ruby, based on felixge/node-formidable's parser.
2
+
3
+ Some things to note:
4
+
5
+ * Pure Ruby
6
+
7
+ * Event-driven API
8
+
9
+ * Only supports one level of multipart parsing. Invoke another parser if you need to handle nested messages.
10
+
11
+ * Does not perform I/O.
12
+
13
+ * Does not depend on any other library.
14
+
15
+ Contributors:
16
+ Daniel Abrahamsson
17
+ Jérémy Bobbio
@@ -0,0 +1,246 @@
1
+ module MultipartParser
2
+ # A low level parser for multipart messages,
3
+ # based on the node-formidable parser.
4
+ class Parser
5
+
6
+ def initialize
7
+ @boundary = nil
8
+ @boundary_chars = nil
9
+ @lookbehind = nil
10
+ @state = :parser_uninitialized
11
+ @index = 0 # Index into boundary or header
12
+ @flags = {}
13
+ @marks = {} # Keep track of different parts
14
+ @callbacks = {}
15
+ end
16
+
17
+ # Initializes the parser, using the given boundary
18
+ def init_with_boundary(boundary)
19
+ @boundary = "\r\n--" + boundary
20
+ @lookbehind = "\0"*(@boundary.length + 8)
21
+ @state = :start
22
+
23
+ @boundary_chars = {}
24
+ @boundary.each_byte do |b|
25
+ @boundary_chars[b] = true
26
+ end
27
+ end
28
+
29
+ # Registers a callback to be called when the
30
+ # given event occurs. Each callback is expected to
31
+ # take three parameters: buffer, start_index, and end_index.
32
+ # All of these parameters may be null, depending on the callback.
33
+ # Valid callbacks are:
34
+ # :end
35
+ # :header_field
36
+ # :header_value
37
+ # :header_end
38
+ # :headers_end
39
+ # :part_begin
40
+ # :part_data
41
+ # :part_end
42
+ def on(event, &callback)
43
+ @callbacks[event] = callback
44
+ end
45
+
46
+ # Writes data to the parser.
47
+ # Returns the number of bytes parsed.
48
+ # In practise, this means that if the return value
49
+ # is less than the buffer length, a parse error occured.
50
+ def write(buffer)
51
+ i = 0
52
+ buffer_length = buffer.length
53
+ index = @index
54
+ flags = @flags.dup
55
+ state = @state
56
+ lookbehind = @lookbehind
57
+ boundary = @boundary
58
+ boundary_chars = @boundary_chars
59
+ boundary_length = @boundary.length
60
+ boundary_end = boundary_length - 1
61
+
62
+ while i < buffer_length
63
+ c = buffer[i, 1]
64
+ case state
65
+ when :parser_uninitialized
66
+ return i;
67
+ when :start
68
+ index = 0;
69
+ state = :start_boundary
70
+ when :start_boundary # Differs in that it has no preceeding \r\n
71
+ if index == boundary_length - 2
72
+ return i unless c == "\r"
73
+ index += 1
74
+ elsif index - 1 == boundary_length - 2
75
+ return i unless c == "\n"
76
+ # Boundary read successfully, begin next part
77
+ callback(:part_begin)
78
+ state = :header_field_start
79
+ else
80
+ return i unless c == boundary[index+2, 1] # Unexpected character
81
+ index += 1
82
+ end
83
+ i += 1
84
+ when :header_field_start
85
+ state = :header_field
86
+ @marks[:header_field] = i
87
+ index = 0
88
+ when :header_field
89
+ if c == "\r"
90
+ @marks.delete :header_field
91
+ state = :headers_almost_done
92
+ else
93
+ index += 1
94
+ unless c == "-" # Skip hyphens
95
+ if c == ":"
96
+ return i if index == 1 # Empty header field
97
+ data_callback(:header_field, buffer, i, :clear => true)
98
+ state = :header_value_start
99
+ else
100
+ cl = c.downcase
101
+ return i if cl < "a" || cl > "z"
102
+ end
103
+ end
104
+ end
105
+ i += 1
106
+ when :header_value_start
107
+ if c == " " # Skip spaces
108
+ i += 1
109
+ else
110
+ @marks[:header_value] = i
111
+ state = :header_value
112
+ end
113
+ when :header_value
114
+ if c == "\r"
115
+ data_callback(:header_value, buffer, i, :clear => true)
116
+ callback(:header_end)
117
+ state = :header_value_almost_done
118
+ end
119
+ i += 1
120
+ when :header_value_almost_done
121
+ return i unless c == "\n"
122
+ state = :header_field_start
123
+ i += 1
124
+ when :headers_almost_done
125
+ return i unless c == "\n"
126
+ callback(:headers_end)
127
+ state = :part_data_start
128
+ i += 1
129
+ when :part_data_start
130
+ state = :part_data
131
+ @marks[:part_data] = i
132
+ when :part_data
133
+ prev_index = index
134
+
135
+ if index == 0
136
+ # Boyer-Moore derived algorithm to safely skip non-boundary data
137
+ # See http://debuggable.com/posts/parsing-file-uploads-at-500-
138
+ # mb-s-with-node-js:4c03862e-351c-4faa-bb67-4365cbdd56cb
139
+ while i + boundary_length <= buffer_length
140
+ break if boundary_chars.has_key? buffer[i + boundary_end]
141
+ i += boundary_length
142
+ end
143
+ c = buffer[i, 1]
144
+ end
145
+
146
+ if index < boundary_length
147
+ if boundary[index, 1] == c
148
+ if index == 0
149
+ data_callback(:part_data, buffer, i, :clear => true)
150
+ end
151
+ index += 1
152
+ else # It was not the boundary we found, after all
153
+ index = 0
154
+ end
155
+ elsif index == boundary_length
156
+ index += 1
157
+ if c == "\r"
158
+ flags[:part_boundary] = true
159
+ elsif c == "-"
160
+ flags[:last_boundary] = true
161
+ else # We did not find a boundary after all
162
+ index = 0
163
+ end
164
+ elsif index - 1 == boundary_length
165
+ if flags[:part_boundary]
166
+ index = 0
167
+ if c == "\n"
168
+ flags.delete :part_boundary
169
+ callback(:part_end)
170
+ callback(:part_begin)
171
+ state = :header_field_start
172
+ i += 1
173
+ next # Ugly way to break out of the case statement
174
+ end
175
+ elsif flags[:last_boundary]
176
+ if c == "-"
177
+ callback(:part_end)
178
+ callback(:end)
179
+ state = :end
180
+ else
181
+ index = 0 # False alarm
182
+ end
183
+ else
184
+ index = 0
185
+ end
186
+ end
187
+
188
+ if index > 0
189
+ # When matching a possible boundary, keep a lookbehind
190
+ # reference in case it turns out to be a false lead
191
+ lookbehind[index-1] = c
192
+ elsif prev_index > 0
193
+ # If our boundary turns out to be rubbish,
194
+ # the captured lookbehind belongs to part_data
195
+ callback(:part_data, lookbehind, 0, prev_index)
196
+ @marks[:part_data] = i
197
+
198
+ # Reconsider the current character as it might be the
199
+ # beginning of a new sequence.
200
+ i -= 1
201
+ end
202
+
203
+ i += 1
204
+ when :end
205
+ i += 1
206
+ else
207
+ return i;
208
+ end
209
+ end
210
+
211
+ data_callback(:header_field, buffer, buffer_length)
212
+ data_callback(:header_value, buffer, buffer_length)
213
+ data_callback(:part_data, buffer, buffer_length)
214
+
215
+ @index = index
216
+ @state = state
217
+ @flags = flags
218
+
219
+ return buffer_length
220
+ end
221
+
222
+ private
223
+
224
+ # Issues a callback.
225
+ def callback(event, buffer = nil, start = nil, the_end = nil)
226
+ return if !start.nil? && start == the_end
227
+ if @callbacks.has_key? event
228
+ @callbacks[event].call(buffer, start, the_end)
229
+ end
230
+ end
231
+
232
+ # Issues a data callback,
233
+ # The only valid options is :clear,
234
+ # which, if true, will reset the appropriate mark to 0,
235
+ # If not specified, the mark will be removed.
236
+ def data_callback(data_type, buffer, the_end, options = {})
237
+ return unless @marks.has_key? data_type
238
+ callback(data_type, buffer, @marks[data_type], the_end)
239
+ unless options[:clear]
240
+ @marks[data_type] = 0
241
+ else
242
+ @marks.delete data_type
243
+ end
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,152 @@
1
+ require File.expand_path('../parser', __FILE__)
2
+
3
+ module MultipartParser
4
+ class NotMultipartError < StandardError; end;
5
+
6
+ # A more high level interface to MultipartParser.
7
+ class Reader
8
+
9
+ # Initializes a MultipartReader, that will
10
+ # read a request with the given boundary value.
11
+ def initialize(boundary)
12
+ @parser = Parser.new
13
+ @parser.init_with_boundary(boundary)
14
+ @header_field = ''
15
+ @header_value = ''
16
+ @part = nil
17
+ @ended = false
18
+ @on_error = nil
19
+ @on_part = nil
20
+
21
+ init_parser_callbacks
22
+ end
23
+
24
+ # Returns true if the parser has finished parsing
25
+ def ended?
26
+ @ended
27
+ end
28
+
29
+ # Sets to a code block to call
30
+ # when part headers have been parsed.
31
+ def on_part(&callback)
32
+ @on_part = callback
33
+ end
34
+
35
+ # Sets a code block to call when
36
+ # a parser error occurs.
37
+ def on_error(&callback)
38
+ @on_error = callback
39
+ end
40
+
41
+ # Write data from the given buffer (String)
42
+ # into the reader.
43
+ def write(buffer)
44
+ bytes_parsed = @parser.write(buffer)
45
+ if bytes_parsed != buffer.size
46
+ msg = "Parser error, #{bytes_parsed} of #{buffer.length} bytes parsed"
47
+ @on_error.call(msg) unless @on_error.nil?
48
+ end
49
+ end
50
+
51
+ # Extracts a boundary value from a Content-Type header.
52
+ # Note that it is the header value you provide here.
53
+ # Raises NotMultipartError if content_type is invalid.
54
+ def self.extract_boundary_value(content_type)
55
+ if content_type =~ /multipart/i
56
+ if match = (content_type =~ /boundary=(?:"([^"]+)"|([^;]+))/i)
57
+ $1 || $2
58
+ else
59
+ raise NotMultipartError.new("No multipart boundary")
60
+ end
61
+ else
62
+ raise NotMultipartError.new("Not a multipart content type!")
63
+ end
64
+ end
65
+
66
+ class Part
67
+ attr_accessor :filename, :headers, :name, :mime
68
+
69
+ def initialize
70
+ @headers = {}
71
+ @data_callback = nil
72
+ @end_callback = nil
73
+ end
74
+
75
+ # Calls the data callback with the given data
76
+ def emit_data(data)
77
+ @data_callback.call(data) unless @data_callback.nil?
78
+ end
79
+
80
+ # Calls the end callback
81
+ def emit_end
82
+ @end_callback.call unless @end_callback.nil?
83
+ end
84
+
85
+ # Sets a block to be called when part data
86
+ # is read. The block should take one parameter,
87
+ # namely the read data.
88
+ def on_data(&callback)
89
+ @data_callback = callback
90
+ end
91
+
92
+ # Sets a block to be called when all data
93
+ # for the part has been read.
94
+ def on_end(&callback)
95
+ @end_callback = callback
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ def init_parser_callbacks
102
+ @parser.on(:part_begin) do
103
+ @part = Part.new
104
+ @header_field = ''
105
+ @header_value = ''
106
+ end
107
+
108
+ @parser.on(:header_field) do |b, start, the_end|
109
+ @header_field << b[start...the_end]
110
+ end
111
+
112
+ @parser.on(:header_value) do |b, start, the_end|
113
+ @header_value << b[start...the_end]
114
+ end
115
+
116
+ @parser.on(:header_end) do
117
+ @header_field.downcase!
118
+ @part.headers[@header_field] = @header_value
119
+ if @header_field == 'content-disposition'
120
+ if @header_value =~ /name="([^"]+)"/i
121
+ @part.name = $1
122
+ end
123
+ if @header_value =~ /filename="([^;]+)"/i
124
+ match = $1
125
+ start = (match.rindex("\\") || -1)+1
126
+ @part.filename = match[start...(match.length)]
127
+ end
128
+ elsif @header_field == 'content-type'
129
+ @part.mime = @header_value
130
+ end
131
+ @header_field = ''
132
+ @header_value = ''
133
+ end
134
+
135
+ @parser.on(:headers_end) do
136
+ @on_part.call(@part) unless @on_part.nil?
137
+ end
138
+
139
+ @parser.on(:part_data) do |b, start, the_end|
140
+ @part.emit_data b[start...the_end]
141
+ end
142
+
143
+ @parser.on(:part_end) do
144
+ @part.emit_end
145
+ end
146
+
147
+ @parser.on(:end) do
148
+ @ended = true
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,3 @@
1
+ module MultipartParser
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,27 @@
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+ require 'multipart_parser/version'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'multipart-parser'
6
+ s.version = MultipartParser::VERSION
7
+ s.authors = ['Daniel Abrahamsson']
8
+ s.email = ['hamsson@gmail.com']
9
+ s.homepage = 'https://github.com/danabr/multipart-parser'
10
+ s.summary = %q{simple parser for multipart MIME messages}
11
+ s.description = <<-DESCRIPTION.gsub(/^ */, '')
12
+ multipart-parser is a simple parser for multipart MIME messages, written in
13
+ Ruby, based on felixge/node-formidable's parser.
14
+
15
+ Some things to note:
16
+ - Pure Ruby
17
+ - Event-driven API
18
+ - Only supports one level of multipart parsing. Invoke another parser if
19
+ you need to handle nested messages.
20
+ - Does not perform I/O.
21
+ - Does not depend on any other library.
22
+ DESCRIPTION
23
+
24
+ s.files = `git ls-files`.split("\n")
25
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
26
+ s.require_paths = ['lib']
27
+ end
@@ -0,0 +1,111 @@
1
+ # Contains fixturues to
2
+ module MultipartParser::Fixtures
3
+ # Returns all fixtures in the module
4
+ def fixtures
5
+ [Rfc1867.new, NoTrailingCRLF.new, EmptyHeader.new]
6
+ end
7
+ extend self
8
+
9
+ class Rfc1867
10
+ def boundary
11
+ 'AaB03x'
12
+ end
13
+
14
+ def expect_error
15
+ false
16
+ end
17
+
18
+ def parts
19
+ part1, part2 = {}, {}
20
+ part1[:headers] = {'content-disposition' => 'form-data; name="field1"'}
21
+ part1[:data] = "Joe Blow\r\nalmost tricked you!"
22
+ part2[:headers] = {}
23
+ part2[:headers]['content-disposition'] = 'form-data; name="pics"; ' +
24
+ 'filename="file1.txt"'
25
+ part2[:headers]['Content-Type'] = 'text/plain'
26
+ part2[:data] = "... contents of file1.txt ...\r"
27
+ [part1, part2]
28
+ end
29
+
30
+ def raw
31
+ ['--AaB03x',
32
+ 'content-disposition: form-data; name="field1"',
33
+ '',
34
+ "Joe Blow\r\nalmost tricked you!",
35
+ '--AaB03x',
36
+ 'content-disposition: form-data; name="pics"; filename="file1.txt"',
37
+ 'Content-Type: text/plain',
38
+ '',
39
+ "... contents of file1.txt ...\r",
40
+ '--AaB03x--',
41
+ ''
42
+ ].join("\r\n")
43
+ end
44
+ end
45
+
46
+ class NoTrailingCRLF
47
+ def boundary
48
+ 'AaB03x'
49
+ end
50
+
51
+ def expect_error
52
+ false
53
+ end
54
+
55
+ def parts
56
+ part1, part2 = {}, {}
57
+ part1[:headers] = {'content-disposition' => 'form-data; name="field1"'}
58
+ part1[:data] = "Joe Blow\r\nalmost tricked you!"
59
+ part2[:headers] = {}
60
+ part2[:headers]['content-disposition'] = 'form-data; name="pics"; ' +
61
+ 'filename="file1.txt"'
62
+ part2[:headers]['Content-Type'] = 'text/plain'
63
+ part2[:data] = "... contents of file1.txt ...\r"
64
+ [part1, part2]
65
+ end
66
+
67
+ def raw
68
+ ['--AaB03x',
69
+ 'content-disposition: form-data; name="field1"',
70
+ '',
71
+ "Joe Blow\r\nalmost tricked you!",
72
+ '--AaB03x',
73
+ 'content-disposition: form-data; name="pics"; filename="file1.txt"',
74
+ 'Content-Type: text/plain',
75
+ '',
76
+ "... contents of file1.txt ...\r",
77
+ '--AaB03x--'
78
+ ].join("\r\n")
79
+ end
80
+ end
81
+
82
+ class EmptyHeader
83
+ def boundary
84
+ 'AaB03x'
85
+ end
86
+
87
+ def expect_error
88
+ true
89
+ end
90
+
91
+ def parts
92
+ [] # Should never be called
93
+ end
94
+
95
+ def raw
96
+ ['--AaB03x',
97
+ 'content-disposition: form-data; name="field1"',
98
+ ': foo',
99
+ '',
100
+ "Joe Blow\r\nalmost tricked you!",
101
+ '--AaB03x',
102
+ 'content-disposition: form-data; name="pics"; filename="file1.txt"',
103
+ 'Content-Type: text/plain',
104
+ '',
105
+ "... contents of file1.txt ...\r",
106
+ '--AaB03x--',
107
+ ''
108
+ ].join("\r\n")
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,97 @@
1
+ require 'test/unit'
2
+ require File.dirname(__FILE__) + "/../../lib/multipart_parser/parser"
3
+ require File.dirname(__FILE__) + "/../fixtures/multipart"
4
+
5
+ module MultipartParser
6
+ class ParserTest < Test::Unit::TestCase
7
+ def test_init_with_boundary
8
+ parser = Parser.new
9
+ def parser.boundary; @boundary end
10
+ def parser.boundary_chars; @boundary_chars end
11
+
12
+ parser.init_with_boundary("abc")
13
+ assert_equal "\r\n--abc", parser.boundary
14
+ expected_bc = {13 => true, 10 => true, 45 => true, 97 => true,
15
+ 98 => true, 99 => true}
16
+ assert_equal expected_bc, parser.boundary_chars
17
+ end
18
+
19
+ def test_parser_error
20
+ parser = Parser.new
21
+ parser.init_with_boundary("abc")
22
+ assert_equal 3, parser.write("--ad")
23
+ end
24
+
25
+ def test_fixtures
26
+ parser = Parser.new
27
+ chunk_length = 10
28
+ Fixtures.fixtures.each do |fixture|
29
+ buffer = fixture.raw
30
+ parts = []
31
+ part, header_field, header_value = nil, nil, nil
32
+ end_called = false
33
+ got_error = false
34
+
35
+ parser.init_with_boundary(fixture.boundary)
36
+
37
+ parser.on(:part_begin) do
38
+ part = {:headers => {}, :data => ''}
39
+ parts.push(part)
40
+ header_field = ''
41
+ header_value = ''
42
+ end
43
+
44
+ parser.on(:header_field) do |b, start, the_end|
45
+ header_field += b[start...the_end]
46
+ end
47
+
48
+ parser.on(:header_value) do |b, start, the_end|
49
+ header_value += b[start...the_end]
50
+ end
51
+
52
+ parser.on(:header_end) do
53
+ part[:headers][header_field] = header_value
54
+ header_field = ''
55
+ header_value = ''
56
+ end
57
+
58
+ parser.on(:part_data) do |b, start, the_end|
59
+ part[:data] += b[start...the_end]
60
+ end
61
+
62
+ parser.on(:end) do
63
+ end_called = true
64
+ end
65
+
66
+ offset = 0
67
+ while offset < buffer.length
68
+ if(offset + chunk_length < buffer.length)
69
+ chunk = buffer[offset, chunk_length]
70
+ else
71
+ chunk = buffer[offset...buffer.length]
72
+ end
73
+ offset += chunk_length
74
+
75
+ nparsed = parser.write(chunk)
76
+ if nparsed != chunk.length
77
+ unless fixture.expect_error
78
+ puts "--ERROR--"
79
+ puts chunk
80
+ flunk "#{fixture.class.name}: #{chunk.length} bytes written, " +
81
+ "but only #{nparsed} bytes parsed!"
82
+ else
83
+ got_error = true
84
+ end
85
+ end
86
+ end
87
+ unless got_error
88
+ assert true, end_called
89
+ assert_equal fixture.parts, parts
90
+ else
91
+ assert fixture.expect_error,
92
+ "#{fixture.class.name}: Expected parse error did not happen"
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,74 @@
1
+ require 'test/unit'
2
+ require File.dirname(__FILE__) + "/../../lib/multipart_parser/reader"
3
+ require File.dirname(__FILE__) + "/../fixtures/multipart"
4
+
5
+ module MultipartParser
6
+ class ReaderTest < Test::Unit::TestCase
7
+ def test_extract_boundary_value
8
+ assert_raise(NotMultipartError) do
9
+ not_multipart = "text/plain"
10
+ Reader.extract_boundary_value(not_multipart)
11
+ end
12
+
13
+ assert_raise(NotMultipartError) do
14
+ no_boundary = "multipart/form-data"
15
+ Reader.extract_boundary_value(no_boundary)
16
+ end
17
+
18
+ valid_content_type = "multipart/form-data; boundary=9asdadsdfv"
19
+ boundary = Reader.extract_boundary_value(valid_content_type)
20
+ assert_equal "9asdadsdfv", boundary
21
+ end
22
+
23
+ def test_error_callback
24
+ on_error_called = false
25
+ reader = Reader.new("boundary")
26
+ reader.on_error do |err|
27
+ on_error_called = true
28
+ end
29
+ reader.write("not boundary atleast")
30
+ assert on_error_called
31
+ end
32
+
33
+ def test_success_scenario
34
+ fixture = Fixtures::Rfc1867.new
35
+ reader = Reader.new(fixture.boundary)
36
+ on_error_called = false
37
+ parts = {}
38
+
39
+ reader.on_error do |err|
40
+ on_error_called = true
41
+ end
42
+
43
+ reader.on_part do |part|
44
+ part_entry = {:part => part, :data => '', :ended => false}
45
+ parts[part.name] = part_entry
46
+ part.on_data do |data|
47
+ part_entry[:data] << data
48
+ end
49
+ part.on_end do
50
+ part_entry[:ended] = true
51
+ end
52
+ end
53
+
54
+ reader.write(fixture.raw)
55
+
56
+ assert !on_error_called
57
+ assert reader.ended?
58
+
59
+ assert_equal parts.size, fixture.parts.size
60
+ assert parts.all? {|k, v| v[:ended]}
61
+
62
+ field = parts['field1']
63
+ assert !field.nil?
64
+ assert_equal 'field1', field[:part].name
65
+ assert_equal fixture.parts.first[:data], field[:data]
66
+
67
+ file = parts['pics']
68
+ assert !file.nil?
69
+ assert_equal 'pics', file[:part].name
70
+ assert_equal 'file1.txt', file[:part].filename
71
+ assert_equal fixture.parts.last[:data], file[:data]
72
+ end
73
+ end
74
+ end
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: multipart-parser
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Daniel Abrahamsson
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-03-17 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: ! 'multipart-parser is a simple parser for multipart MIME messages, written
15
+ in
16
+
17
+ Ruby, based on felixge/node-formidable''s parser.
18
+
19
+
20
+ Some things to note:
21
+
22
+ - Pure Ruby
23
+
24
+ - Event-driven API
25
+
26
+ - Only supports one level of multipart parsing. Invoke another parser if
27
+
28
+ you need to handle nested messages.
29
+
30
+ - Does not perform I/O.
31
+
32
+ - Does not depend on any other library.
33
+
34
+ '
35
+ email:
36
+ - hamsson@gmail.com
37
+ executables: []
38
+ extensions: []
39
+ extra_rdoc_files: []
40
+ files:
41
+ - LICENSE
42
+ - README
43
+ - lib/multipart_parser/parser.rb
44
+ - lib/multipart_parser/reader.rb
45
+ - lib/multipart_parser/version.rb
46
+ - multipart-parser.gemspec
47
+ - test/fixtures/multipart.rb
48
+ - test/multipart_parser/parser_test.rb
49
+ - test/multipart_parser/reader.rb
50
+ homepage: https://github.com/danabr/multipart-parser
51
+ licenses: []
52
+ post_install_message:
53
+ rdoc_options: []
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ! '>='
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ requirements: []
69
+ rubyforge_project:
70
+ rubygems_version: 1.8.19
71
+ signing_key:
72
+ specification_version: 3
73
+ summary: simple parser for multipart MIME messages
74
+ test_files:
75
+ - test/fixtures/multipart.rb
76
+ - test/multipart_parser/parser_test.rb
77
+ - test/multipart_parser/reader.rb