cae-multipart_parser 1.1.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +19 -10
- data/lib/cae/multipart_parser/parser.rb +8 -18
- data/lib/cae/multipart_parser/part.rb +11 -7
- data/lib/cae/multipart_parser/part/body.rb +49 -0
- data/lib/cae/multipart_parser/version.rb +1 -1
- data/spec/parser_spec.rb +13 -29
- data/spec/part_body_spec.rb +69 -0
- data/spec/spec_helper.rb +8 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ba2747bd66a2c381c5d6b0845d5005af0c193638
|
4
|
+
data.tar.gz: 59fa00307201bb96569afcf18146e0466abc3dad
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 69c00279283004fa1f1145a8b0b66afc915ef510628f8fd530d60c881fb01755ceed5114705d1a94a0d535cd11d2f55c03e2f4d94931d509c920d2790c45095a
|
7
|
+
data.tar.gz: 2accc6e0407347dfebe6154a06dc484a3d2ab67bd918722218d26ffddc9df6a4c4b5417d18f7fec30af787aaeb55ae485fefec6a90952b78d1d7d477ffd2a17a
|
data/README.md
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
# Event-driven HTTP Multipart Parser
|
2
2
|
|
3
|
-
This is based on https://github.com/danabr/multipart-parser, with
|
4
|
-
modifications to suit my use-case.
|
3
|
+
This is based on https://github.com/danabr/multipart-parser, with modifications to suit my use-case.
|
5
4
|
|
6
5
|
It currently depends on finding a `Content-Length` part header to avoid having to scan the entire body. It will raise `Cae::MultipartParser::Parser::ContentLengthUnsetError` if this header is not present.
|
7
6
|
|
@@ -12,15 +11,25 @@ It currently depends on finding a `Content-Length` part header to avoid having t
|
|
12
11
|
parser = Cae::MultipartParser::Parser.new(boundary: boundary)
|
13
12
|
|
14
13
|
parser.parse fh do |part|
|
15
|
-
part.
|
16
|
-
|
14
|
+
# part.headers and part.content_length should be set now
|
15
|
+
# headers are underscored and uppercased:
|
16
|
+
if part.headers['CONTENT_TYPE'] == 'text/html'
|
17
|
+
# ...
|
17
18
|
end
|
18
|
-
|
19
|
-
|
20
|
-
|
19
|
+
|
20
|
+
# content_length is an integer:
|
21
|
+
if part.content_length < 1024
|
22
|
+
# ...
|
21
23
|
end
|
22
|
-
|
23
|
-
|
24
|
+
|
25
|
+
while part.body.read(chunksize, buf)
|
26
|
+
# buf contains up to chunksize bytes of data.
|
27
|
+
# Do not assume if less than chunksize is returned, you're done,
|
28
|
+
# for internal as-yet-to-be-fixed reasons.
|
29
|
+
# Only stop when #read returns nil. Note that #read is NOT IO#read,
|
30
|
+
# but is mostly compatible.
|
24
31
|
end
|
32
|
+
|
33
|
+
# all the part body has now been read
|
25
34
|
end
|
26
|
-
```
|
35
|
+
```
|
@@ -44,6 +44,8 @@ module Cae
|
|
44
44
|
while i < length
|
45
45
|
c = buffer[i]
|
46
46
|
|
47
|
+
#p "state=#{@state} index=#{@index} chars=#{buffer[i, 40]}"
|
48
|
+
|
47
49
|
case @state
|
48
50
|
when :start
|
49
51
|
if @index == @boundary_length - 2
|
@@ -88,29 +90,17 @@ module Cae
|
|
88
90
|
# this must populate #content_length
|
89
91
|
@part.parse_header_str @headers
|
90
92
|
|
91
|
-
|
92
|
-
raise ContentLengthUnsetError if @part_data_remaining == 0
|
93
|
-
|
94
|
-
# allow caller to setup callbacks
|
95
|
-
yield @part
|
93
|
+
raise ContentLengthUnsetError if @part.content_length == 0
|
96
94
|
|
97
|
-
@part.callback :headers, @part.headers
|
98
|
-
|
99
|
-
data_start = i
|
100
|
-
@index = 0
|
101
|
-
@state = :part_data
|
102
|
-
next # keep i pointing at current char for :part_data
|
103
|
-
|
104
|
-
when :part_data
|
105
95
|
chunk_remaining = length - data_start
|
106
|
-
cb_len = @
|
107
|
-
|
108
|
-
@part
|
96
|
+
cb_len = @part.content_length > chunk_remaining ? chunk_remaining : @part.content_length
|
97
|
+
@part.body = Part::Body.new(io, @part.content_length, buffer[i, cb_len])
|
98
|
+
yield @part
|
109
99
|
|
110
|
-
@part_data_remaining -= cb_len
|
111
100
|
i += cb_len
|
112
101
|
|
113
|
-
@
|
102
|
+
@index = 0
|
103
|
+
@state = :boundary
|
114
104
|
next # we've bumped i already, don't increment it
|
115
105
|
|
116
106
|
when :boundary
|
@@ -1,30 +1,34 @@
|
|
1
1
|
# vim: et sw=2 ts=2 sts=2
|
2
|
+
|
3
|
+
require 'cae/multipart_parser/part/body'
|
4
|
+
|
2
5
|
module Cae
|
3
6
|
module MultipartParser
|
4
7
|
class Part
|
5
8
|
|
6
9
|
attr_reader :headers
|
10
|
+
attr_accessor :body
|
7
11
|
|
8
12
|
def initialize
|
9
13
|
@callbacks = {}
|
10
14
|
@headers = {}
|
15
|
+
@body = nil
|
11
16
|
end
|
12
17
|
|
13
18
|
def parse_header_str(str)
|
14
|
-
#
|
19
|
+
# Munge multiline headers back into one line.
|
15
20
|
str = str.gsub /\r\n\s+/, ' '
|
16
21
|
|
17
|
-
#
|
18
|
-
str.split(/\r\n/).
|
19
|
-
key, value =
|
22
|
+
# Split header string into a hash. Returns the initial hash.
|
23
|
+
str.split(/\r\n/).each_with_object(@headers) do |line, headers|
|
24
|
+
key, value = line.split ':'
|
20
25
|
|
21
|
-
# normalize
|
26
|
+
# normalize Content-Length -> CONTENT_LENGTH
|
22
27
|
key.upcase!
|
23
28
|
key.tr! '-', '_'
|
24
29
|
|
25
|
-
|
30
|
+
headers[key] = value.lstrip
|
26
31
|
end
|
27
|
-
@headers
|
28
32
|
end
|
29
33
|
|
30
34
|
def content_length
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# vim: et sw=2 ts=2 sts=2
|
2
|
+
module Cae
|
3
|
+
module MultipartParser
|
4
|
+
class Part
|
5
|
+
class Body
|
6
|
+
|
7
|
+
def initialize(fh, read_limit, read_buffer = nil)
|
8
|
+
# Backing filehandle
|
9
|
+
@fh = fh
|
10
|
+
|
11
|
+
# After we've read this amount, act like the backing filehandle is empty
|
12
|
+
@read_limit = read_limit
|
13
|
+
|
14
|
+
# If anything is in this, we'll return it on the first read()
|
15
|
+
@read_buffer = read_buffer
|
16
|
+
end
|
17
|
+
|
18
|
+
def read(length, outbuf = nil)
|
19
|
+
# Check we're not trying to read more bytes than are available
|
20
|
+
length = @read_limit > length ? length : @read_limit
|
21
|
+
|
22
|
+
# Early nil return if there's nothing available. This technically
|
23
|
+
# breaks compatibility with IO#read, but I don't care.
|
24
|
+
# (IO#read returns an empty string if passed length is 0; we'll return nil)
|
25
|
+
return nil if length == 0
|
26
|
+
|
27
|
+
# if there's anything in @read_buffer, return it before doing a real read
|
28
|
+
if @read_buffer
|
29
|
+
# initialise outbuf if it wasn't passed in.
|
30
|
+
outbuf ||= String.new
|
31
|
+
|
32
|
+
# copy contents into outbuf, being careful NOT to change the object_id
|
33
|
+
outbuf.clear
|
34
|
+
outbuf << @read_buffer[0, length]
|
35
|
+
|
36
|
+
# advance buffer pointer; if there's none left, @read_buffer will be nil
|
37
|
+
@read_limit -= outbuf.length
|
38
|
+
@read_buffer = @read_buffer[length, @read_limit]
|
39
|
+
|
40
|
+
return outbuf
|
41
|
+
end
|
42
|
+
|
43
|
+
@fh.read(length, outbuf).tap{|o| @read_limit -= o.length }
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/spec/parser_spec.rb
CHANGED
@@ -18,56 +18,40 @@ describe Cae::MultipartParser::Parser do
|
|
18
18
|
body = generate_body(boundary, [part])
|
19
19
|
fh = StringIO.new body
|
20
20
|
|
21
|
-
r = parser.parse
|
22
|
-
end
|
23
|
-
|
21
|
+
r = parser.parse(fh){ }
|
24
22
|
r.must_equal body.length
|
25
23
|
end
|
26
24
|
|
27
|
-
it "
|
25
|
+
it "sets part#headers" do
|
28
26
|
part = SecureRandom.random_bytes(1024) # random data
|
29
27
|
fh = StringIO.new generate_body(boundary, [part])
|
30
28
|
|
31
29
|
headers = nil
|
32
|
-
parser.parse
|
33
|
-
part.on(:headers){|h| headers = h }
|
34
|
-
end
|
30
|
+
parser.parse(fh){|part| headers = part.headers }
|
35
31
|
headers.must_be_kind_of Hash
|
36
32
|
end
|
37
33
|
|
38
|
-
it "
|
34
|
+
it "passes the original data to the part#body handle" do
|
39
35
|
part = SecureRandom.random_bytes(1024 * 1024) # 1MB of random data
|
40
36
|
fh = StringIO.new generate_body(boundary, [part])
|
41
37
|
ret = ''
|
42
38
|
parser.parse fh do |part|
|
43
|
-
part.
|
39
|
+
part.body.must_be_kind_of Cae::MultipartParser::Part::Body
|
40
|
+
while x = part.body.read(1024)
|
41
|
+
ret << x
|
42
|
+
end
|
44
43
|
end
|
45
44
|
|
46
45
|
ret.must_equal part
|
47
46
|
end
|
48
47
|
|
49
|
-
it "
|
48
|
+
it "yields for each part" do
|
50
49
|
part = SecureRandom.random_bytes(1024) # random data
|
51
|
-
|
50
|
+
parts = [part, part]
|
51
|
+
fh = StringIO.new generate_body(boundary, parts)
|
52
52
|
done = 0
|
53
|
-
parser.parse
|
54
|
-
|
55
|
-
end
|
56
|
-
|
57
|
-
done.must_equal 1
|
58
|
-
end
|
59
|
-
|
60
|
-
it "calls callbacks after each part is done" do
|
61
|
-
part = SecureRandom.random_bytes(1024) # random data
|
62
|
-
fh = StringIO.new generate_body(boundary, [part, part])
|
63
|
-
headers, done = 0, 0
|
64
|
-
parser.parse fh do |part|
|
65
|
-
part.on(:headers){ headers += 1 }
|
66
|
-
part.on(:end){ done += 1 }
|
67
|
-
end
|
68
|
-
|
69
|
-
headers.must_equal 2
|
70
|
-
done.must_equal 2
|
53
|
+
parser.parse(fh){|part| done += 1 }
|
54
|
+
done.must_equal parts.count
|
71
55
|
end
|
72
56
|
end
|
73
57
|
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# vim: et sw=2 ts=2 sts=2
|
2
|
+
|
3
|
+
require File.expand_path("spec_helper", File.dirname(__FILE__))
|
4
|
+
|
5
|
+
def read_all(fh, chunksize, outbuf = nil)
|
6
|
+
str = String.new
|
7
|
+
outbuf = String.new
|
8
|
+
while fh.read(chunksize, outbuf)
|
9
|
+
str << outbuf
|
10
|
+
end
|
11
|
+
str
|
12
|
+
end
|
13
|
+
|
14
|
+
describe Cae::MultipartParser::Part::Body do
|
15
|
+
let(:fh_data) { '12345' }
|
16
|
+
let(:initial_buffer) { nil }
|
17
|
+
let(:size_limit) { 5 }
|
18
|
+
let(:expected) { ((initial_buffer || '') + fh_data)[0, size_limit] }
|
19
|
+
let(:fh) { StringIO.new fh_data }
|
20
|
+
let(:body) do
|
21
|
+
Cae::MultipartParser::Part::Body.new(fh, size_limit, initial_buffer)
|
22
|
+
end
|
23
|
+
|
24
|
+
(1..15).each do |chunksize|
|
25
|
+
it "should work with read chunk size #{chunksize}" do
|
26
|
+
read_all(body, chunksize).must_equal expected
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "with an initial buffer" do
|
31
|
+
let(:initial_buffer) { 'abcde' }
|
32
|
+
let(:size_limit) { 10 }
|
33
|
+
|
34
|
+
(1..15).each do |chunksize|
|
35
|
+
it "should work with read chunk size #{chunksize}" do
|
36
|
+
read_all(body, chunksize).must_equal expected
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should not reallocate if passed an outbuf" do
|
41
|
+
outbuf = String.new
|
42
|
+
refute_changes(->{outbuf.object_id}) do
|
43
|
+
while body.read(1, outbuf)
|
44
|
+
# no-op
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should return nil when empty" do
|
50
|
+
read_all(body, size_limit) # empty the "file"
|
51
|
+
body.read(1).must_equal nil
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "with a shorter size_limit than data available" do
|
55
|
+
let(:size_limit) { 5 }
|
56
|
+
it "should stop returning data when size_limit is hit" do
|
57
|
+
read_all(body, 1).must_equal expected
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should return nil when 'empty'" do
|
61
|
+
read_all(body, size_limit) # empty the "file"
|
62
|
+
body.read(1).must_equal nil
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
end
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cae-multipart_parser
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chris Elsworth
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-09-
|
11
|
+
date: 2015-09-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: multipart-post
|
@@ -54,8 +54,10 @@ files:
|
|
54
54
|
- lib/cae/multipart_parser.rb
|
55
55
|
- lib/cae/multipart_parser/parser.rb
|
56
56
|
- lib/cae/multipart_parser/part.rb
|
57
|
+
- lib/cae/multipart_parser/part/body.rb
|
57
58
|
- lib/cae/multipart_parser/version.rb
|
58
59
|
- spec/parser_spec.rb
|
60
|
+
- spec/part_body_spec.rb
|
59
61
|
- spec/part_spec.rb
|
60
62
|
- spec/spec_helper.rb
|
61
63
|
homepage: https://github.com/celsworth/cae-multipart_parser
|
@@ -84,6 +86,7 @@ specification_version: 4
|
|
84
86
|
summary: Event-driven HTTP Multipart parser
|
85
87
|
test_files:
|
86
88
|
- spec/parser_spec.rb
|
89
|
+
- spec/part_body_spec.rb
|
87
90
|
- spec/part_spec.rb
|
88
91
|
- spec/spec_helper.rb
|
89
92
|
has_rdoc:
|