Class: Cae::MultipartParser::Parser

Inherits:
Object
  • Object
show all
Defined in:
lib/cae/multipart_parser/parser.rb

Constant Summary collapse

CR =
"\r".freeze
LF =
"\n".freeze
DASH =
"-".freeze
BOUNDARY_PREFIX =
(CR + LF + DASH + DASH).freeze
ContentLengthUnsetError =
Class.new(StandardError)
ParseError =
Class.new(StandardError)

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(opts = {}) ⇒ Parser

Returns a new instance of Parser.



19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/cae/multipart_parser/parser.rb', line 19

def initialize(opts = {})
  # remember offsets into our state in between calls
  @index = 0

  @state = :start

  # default 2MB read buffer
  @read_buffer_size = 2 * 1024 * 1024

  @boundary = BOUNDARY_PREFIX + opts[:boundary]
  @boundary_length = @boundary.length
end

Instance Attribute Details

#read_buffer_sizeObject

Returns the value of attribute read_buffer_size.



17
18
19
# File 'lib/cae/multipart_parser/parser.rb', line 17

def read_buffer_size
  @read_buffer_size
end

Instance Method Details

#parse(io) ⇒ Integer

Parse data from the IO io, calling callbacks appropriately.

Returns:

  • (Integer)

    the number of bytes parsed.



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/cae/multipart_parser/parser.rb', line 35

def parse(io)
  parsed = 0

  buffer = String.new
  while io.read(@read_buffer_size, buffer)
    length = buffer.length
    i = 0
    data_start = 0

    while i < length
      c = buffer[i]

      #p "state=#{@state} index=#{@index} chars=#{buffer[i, 40]}"

      case @state
      when :start
        if @index == @boundary_length - 2
          break unless c == CR
          @index += 1
        elsif @index == @boundary_length - 1
          break unless c == LF
          # reached end of boundary, we're into the first part
          @state = :headers_start
        else
          # there is no leading \r\n on the first boundary, hence index+2
          break unless c == @boundary[@index + 2] # Unexpected character
          @index += 1
        end

      when :headers_start
        @part = Part.new
        @state = :headers
        @index = 0
        @headers = ''
        next # keep i pointing at current char for :headers

      when :headers
        if (c == CR && @index == 0) || (c == LF && @index == 1)
          # keep \r\n to split on later
          @headers << c
          @index += 1
        elsif c == CR && @index == 2
          # don't keep final \r, update state to check for final \n
          @state = :headers_almost_done
        else
          # normal header char, reset index and keep char
          @headers << c
          @index = 0
        end

      when :headers_almost_done
        break unless c == LF # Unexpected character
        @state = :part_start

      when :part_start
        # this must populate #content_length
        @part.parse_header_str @headers

        raise ContentLengthUnsetError if @part.content_length == 0

        chunk_remaining = length - data_start
        cb_len = @part.content_length > chunk_remaining ? chunk_remaining : @part.content_length
        @part.body = Part::Body.new(io, @part.content_length, buffer[i, cb_len])
        yield @part

        i += cb_len

        @index = 0
        @state = :boundary
        next # we've bumped i already, don't increment it

      when :boundary
        break unless c == @boundary[@index] # unexpected character
        @index += 1
        @state = :boundary_almost_done if @index == @boundary_length

      when :boundary_almost_done
        # work out whether this is a part boundary or the final boundary
        if c == CR
          @state = :boundary_part_almost_done
        elsif c == DASH
          @state = :boundary_last_almost_done
        else
          break # unexpected character, this isn't a boundary after all
        end
        @index += 1

      when :boundary_part_almost_done
        # final character of an inter-part boundary must be LF
        break unless c == LF # unexpected character
        @part.callback :end
        @state = :headers_start

      when :boundary_last_almost_done
        # final character of final boundary must be -
        break unless c == DASH # unexpected character
        @part.callback :end
        @state = :end

      end # case

      i += 1

    end # while

    if i != length
      raise ParseError, "unexpected char at char #{parsed+i} (#{c.inspect})"
    end

    parsed += length

  end # while

  parsed
end