Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(201)

Side by Side Diff: sdk/lib/io/mime_multipart_parser.dart

Issue 14905003: Update mime_multipart_parser to be a StreamTransformer, public available. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 7 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | tests/standalone/io/mime_multipart_parser_test.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file 1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
2 // for details. All rights reserved. Use of this source code is governed by a 2 // for details. All rights reserved. Use of this source code is governed by a
3 // BSD-style license that can be found in the LICENSE file. 3 // BSD-style license that can be found in the LICENSE file.
4 4
5 part of dart.io; 5 part of dart.io;
6 6
7
8 /**
9 * A Mime Multipart class representing each part parsed by
10 * [MimeMultipartTransformer]. The data is streamed in as it become available.
Søren Gjesse 2013/05/03 08:30:48 Maybe add some code to show how to use it to colle
Anders Johnsen 2013/05/03 10:22:54 That's actually quite complicated (header parsing,
11 */
12 abstract class MimeMultipart extends Stream<List<int>> {
13 Map<String, String> get headers;
14 }
15
16 class _MimeMultipartImpl extends MimeMultipart {
17 final Map<String, String> headers;
18 final Stream<List<int>> _stream;
19
20 _MimeMultipartImpl(this.headers, this._stream);
21
22 StreamSubscription<List<int>> listen(void onData(List<int> data),
23 {void onDone(),
24 void onError(error),
25 bool cancelOnError}) {
26 return _stream.listen(onData,
27 onDone: onDone,
28 onError: onError,
29 cancelOnError: cancelOnError);
30 }
31 }
32
7 /** 33 /**
8 * Parser for MIME multipart types of data as described in RFC 2046 34 * Parser for MIME multipart types of data as described in RFC 2046
9 * section 5.1.1. The data to parse is supplied through the [:update:] 35 * section 5.1.1. The data is transformed into [MimeMultipart] objects, each
10 * method. As the data is parsed the following callbacks are called: 36 * of them streaming the multipart data.
11 *
12 * [:partStart;
13 * [:headerReceived;
14 * [:headersComplete;
15 * [:partDataReceived;
16 * [:partEnd;
17 * [:error:]
18 */ 37 */
38 class MimeMultipartTransformer
39 implements StreamTransformer<List<int>, MimeMultipart> {
40 static const int _START = 0;
41 static const int _FIRST_BOUNDARY_ENDING = 111;
42 static const int _FIRST_BOUNDARY_END = 112;
43 static const int _BOUNDARY_ENDING = 1;
44 static const int _BOUNDARY_END = 2;
45 static const int _HEADER_START = 3;
46 static const int _HEADER_FIELD = 4;
47 static const int _HEADER_VALUE_START = 5;
48 static const int _HEADER_VALUE = 6;
49 static const int _HEADER_VALUE_FOLDING_OR_ENDING = 7;
50 static const int _HEADER_VALUE_FOLD_OR_END = 8;
51 static const int _HEADER_ENDING = 9;
52 static const int _CONTENT = 10;
53 static const int _LAST_BOUNDARY_DASH2 = 11;
54 static const int _LAST_BOUNDARY_ENDING = 12;
55 static const int _LAST_BOUNDARY_END = 13;
56 static const int _DONE = 14;
57 static const int _FAILURE = 15;
19 58
20 class _MimeMultipartParser { 59 StreamController _controller;
21 const int _START = 0; 60 StreamSubscription _subscription;
22 const int _FIRST_BOUNDARY_ENDING = 111;
23 const int _FIRST_BOUNDARY_END = 112;
24 const int _BOUNDARY_ENDING = 1;
25 const int _BOUNDARY_END = 2;
26 const int _HEADER_START = 3;
27 const int _HEADER_FIELD = 4;
28 const int _HEADER_VALUE_START = 5;
29 const int _HEADER_VALUE = 6;
30 const int _HEADER_VALUE_FOLDING_OR_ENDING = 7;
31 const int _HEADER_VALUE_FOLD_OR_END = 8;
32 const int _HEADER_ENDING = 9;
33 const int _CONTENT = 10;
34 const int _LAST_BOUNDARY_DASH2 = 11;
35 const int _LAST_BOUNDARY_ENDING = 12;
36 const int _LAST_BOUNDARY_END = 13;
37 const int _DONE = 14;
38 const int _FAILURE = 15;
39 61
40 // Construct a new MIME multipart parser with the boundary 62 StreamController _multipartController;
41 // [boundary]. The boundary should be as specified in the content 63 Map<String, String> _headers;
42 // type parameter, that is without the -- prefix. 64
43 _MimeMultipartParser(String boundary) { 65 List<int> _boundary;
66 int _state = _START;
67 int _boundaryIndex = 0;
68
69 // Current index in the data buffer. If index is negative then it
70 // is the index into the artificial prefix of the boundary string.
71 int _index;
72 List<int> _buffer;
73
74 StringBuffer _headerField = new StringBuffer();
75 StringBuffer _headerValue = new StringBuffer();
76
77 /**
78 * Construct a new MIME multipart parser with the boundary
79 * [boundary]. The boundary should be as specified in the content
80 * type parameter, that is without the -- prefix.
81 */
82 MimeMultipartTransformer(String boundary) {
44 List<int> charCodes = boundary.codeUnits; 83 List<int> charCodes = boundary.codeUnits;
45 _boundary = new List<int>(4 + charCodes.length); 84 _boundary = new Uint8List(4 + charCodes.length);
46 // Set-up the matching boundary preceding it with CRLF and two 85 // Set-up the matching boundary preceding it with CRLF and two
47 // dashes. 86 // dashes.
48 _boundary[0] = _CharCode.CR; 87 _boundary[0] = _CharCode.CR;
49 _boundary[1] = _CharCode.LF; 88 _boundary[1] = _CharCode.LF;
50 _boundary[2] = _CharCode.DASH; 89 _boundary[2] = _CharCode.DASH;
51 _boundary[3] = _CharCode.DASH; 90 _boundary[3] = _CharCode.DASH;
52 _boundary.setRange(4, 4 + charCodes.length, charCodes); 91 _boundary.setRange(4, 4 + charCodes.length, charCodes);
53 _state = _START;
54 _headerField = new StringBuffer();
55 _headerValue = new StringBuffer();
56 } 92 }
57 93
58 int update(List<int> buffer, int offset, int count) { 94 void _resumeStream() {
59 // Current index in the data buffer. If index is negative then it 95 _subscription.resume();
60 // is the index into the artificial prefix of the boundary string. 96 }
61 int index; 97
98 void _pauseStream() {
99 _subscription.pause();
100 }
101
102 Stream<MimeMultipart> bind(Stream<List<int>> stream) {
103 _controller = new StreamController(
104 onPause: () {
105 _pauseStream();
106 },
107 onResume: () {
108 _resumeStream();
109 },
110 onListen: () {
111 _subscription = stream.listen(
112 (data) {
113 assert(_buffer == null);
114 _pauseStream();
115 _buffer = data;
116 _index = 0;
117 _parse();
118 },
119 onDone: () {
120 if (_state != _DONE) {
121 _controller.addError(
122 new MimeParserException("Bad multipart ending"));
123 }
124 _controller.close();
125 },
126 onError: (error) {
127 _controller.addError(error);
128 });
129 },
130 onCancel: () {
Søren Gjesse 2013/05/03 08:30:48 Do you need to specify this empty onCancel closure
Anders Johnsen 2013/05/03 10:22:54 Done.
131 });
132 return _controller.stream;
133 }
134
135 void _parse() {
62 // Number of boundary bytes to artificially place before the supplied data. 136 // Number of boundary bytes to artificially place before the supplied data.
63 int boundaryPrefix = 0; 137 int boundaryPrefix = 0;
64 // Position where content starts. Will be null if no known content 138 // Position where content starts. Will be null if no known content
65 // start exists. Will be negative of the content starts in the 139 // start exists. Will be negative of the content starts in the
66 // boundary prefix. Will be zero or position if the content starts 140 // boundary prefix. Will be zero or position if the content starts
67 // in the current buffer. 141 // in the current buffer.
68 int contentStartIndex; 142 int contentStartIndex;
69 143
70 // Function to report content data for the current part. The data 144 // Function to report content data for the current part. The data
71 // reported is from the current content start index up til the 145 // reported is from the current content start index up til the
72 // current index. As the data can be artificially prefixed with a 146 // current index. As the data can be artificially prefixed with a
73 // prefix of the boundary both the content start index and index 147 // prefix of the boundary both the content start index and index
74 // can be negative. 148 // can be negative.
75 void reportData() { 149 void reportData() {
76 if (partDataReceived == null) return;
77
78 if (contentStartIndex < 0) { 150 if (contentStartIndex < 0) {
79 var contentLength = boundaryPrefix + index - _boundaryIndex; 151 var contentLength = boundaryPrefix + _index - _boundaryIndex;
80 if (contentLength <= boundaryPrefix) { 152 if (contentLength <= boundaryPrefix) {
81 partDataReceived( 153 _multipartController.add(
82 _boundary.sublist(0, contentLength)); 154 _boundary.sublist(0, contentLength));
83 } else { 155 } else {
84 partDataReceived( 156 _multipartController.add(
85 _boundary.sublist(0, boundaryPrefix)); 157 _boundary.sublist(0, boundaryPrefix));
86 partDataReceived( 158 _multipartController.add(
87 buffer.sublist(0, contentLength - boundaryPrefix)); 159 _buffer.sublist(0, contentLength - boundaryPrefix));
88 } 160 }
89 } else { 161 } else {
90 var contentEndIndex = index - _boundaryIndex; 162 var contentEndIndex = _index - _boundaryIndex;
91 partDataReceived( 163 _multipartController.add(
92 buffer.sublist(contentStartIndex, contentEndIndex)); 164 _buffer.sublist(contentStartIndex, contentEndIndex));
93 } 165 }
94 } 166 }
95 167
96 // Prepare for processing the buffer.
97 index = offset;
98 int lastIndex = offset + count;
99 if (_state == _CONTENT && _boundaryIndex == 0) { 168 if (_state == _CONTENT && _boundaryIndex == 0) {
100 contentStartIndex = 0; 169 contentStartIndex = 0;
101 } else { 170 } else {
102 contentStartIndex = null; 171 contentStartIndex = null;
103 } 172 }
104 // The data to parse might be "artificially" prefixed with a 173 // The data to parse might be "artificially" prefixed with a
105 // partial match of the boundary. 174 // partial match of the boundary.
106 boundaryPrefix = _boundaryIndex; 175 boundaryPrefix = _boundaryIndex;
107 176
108 while ((index < lastIndex) && _state != _FAILURE && _state != _DONE) { 177 while ((_index < _buffer.length) && _state != _FAILURE && _state != _DONE) {
178 if (_multipartController != null && _multipartController.isPaused) {
179 return;
180 }
109 int byte; 181 int byte;
110 if (index < 0) { 182 if (_index < 0) {
111 byte = _boundary[boundaryPrefix + index]; 183 byte = _boundary[boundaryPrefix + _index];
112 } else { 184 } else {
113 byte = buffer[index]; 185 byte = _buffer[_index];
114 } 186 }
115 switch (_state) { 187 switch (_state) {
116 case _START: 188 case _START:
117 if (_toLowerCase(byte) == _toLowerCase(_boundary[_boundaryIndex])) { 189 if (_toLowerCase(byte) == _toLowerCase(_boundary[_boundaryIndex])) {
118 _boundaryIndex++; 190 _boundaryIndex++;
119 if (_boundaryIndex == _boundary.length) { 191 if (_boundaryIndex == _boundary.length) {
120 _state = _FIRST_BOUNDARY_ENDING; 192 _state = _FIRST_BOUNDARY_ENDING;
121 _boundaryIndex = 0; 193 _boundaryIndex = 0;
122 } 194 }
123 } else { 195 } else {
124 // Restart matching of the boundary. 196 // Restart matching of the boundary.
125 index = index - _boundaryIndex; 197 _index = _index - _boundaryIndex;
126 _boundaryIndex = 0; 198 _boundaryIndex = 0;
127 } 199 }
128 break; 200 break;
129 201
130 case _FIRST_BOUNDARY_ENDING: 202 case _FIRST_BOUNDARY_ENDING:
131 if (byte == _CharCode.CR) { 203 if (byte == _CharCode.CR) {
132 _state = _FIRST_BOUNDARY_END; 204 _state = _FIRST_BOUNDARY_END;
133 } else { 205 } else {
134 _expectWS(byte); 206 _expectWS(byte);
135 } 207 }
136 break; 208 break;
137 209
138 case _FIRST_BOUNDARY_END: 210 case _FIRST_BOUNDARY_END:
139 _expect(byte, _CharCode.LF); 211 _expect(byte, _CharCode.LF);
140 _state = _HEADER_START; 212 _state = _HEADER_START;
141 break; 213 break;
142 214
143 case _BOUNDARY_ENDING: 215 case _BOUNDARY_ENDING:
144 if (byte == _CharCode.CR) { 216 if (byte == _CharCode.CR) {
145 _state = _BOUNDARY_END; 217 _state = _BOUNDARY_END;
146 } else if (byte == _CharCode.DASH) { 218 } else if (byte == _CharCode.DASH) {
147 _state = _LAST_BOUNDARY_DASH2; 219 _state = _LAST_BOUNDARY_DASH2;
148 } else { 220 } else {
149 _expectWS(byte); 221 _expectWS(byte);
150 } 222 }
151 break; 223 break;
152 224
153 case _BOUNDARY_END: 225 case _BOUNDARY_END:
154 _expect(byte, _CharCode.LF); 226 _expect(byte, _CharCode.LF);
155 if (partEnd != null) { 227 _multipartController.close();
156 partEnd(false); 228 _multipartController = null;
157 }
158 _state = _HEADER_START; 229 _state = _HEADER_START;
159 break; 230 break;
160 231
161 case _HEADER_START: 232 case _HEADER_START:
233 _headers = new Map<String, String>();
162 if (byte == _CharCode.CR) { 234 if (byte == _CharCode.CR) {
163 _state = _HEADER_ENDING; 235 _state = _HEADER_ENDING;
236 } else {
237 // Start of new header field.
238 _headerField.writeCharCode(_toLowerCase(byte));
239 _state = _HEADER_FIELD;
240 }
241 break;
242
243 case _HEADER_FIELD:
244 if (byte == _CharCode.COLON) {
245 _state = _HEADER_VALUE_START;
246 } else {
247 if (!_isTokenChar(byte)) {
248 throw new MimeParserException("Invalid header field name");
249 }
250 _headerField.writeCharCode(_toLowerCase(byte));
251 }
252 break;
253
254 case _HEADER_VALUE_START:
255 if (byte == _CharCode.CR) {
256 _state = _HEADER_VALUE_FOLDING_OR_ENDING;
257 } else if (byte != _CharCode.SP && byte != _CharCode.HT) {
258 // Start of new header value.
259 _headerValue.writeCharCode(byte);
260 _state = _HEADER_VALUE;
261 }
262 break;
263
264 case _HEADER_VALUE:
265 if (byte == _CharCode.CR) {
266 _state = _HEADER_VALUE_FOLDING_OR_ENDING;
267 } else {
268 _headerValue.writeCharCode(byte);
269 }
270 break;
271
272 case _HEADER_VALUE_FOLDING_OR_ENDING:
273 _expect(byte, _CharCode.LF);
274 _state = _HEADER_VALUE_FOLD_OR_END;
275 break;
276
277 case _HEADER_VALUE_FOLD_OR_END:
278 if (byte == _CharCode.SP || byte == _CharCode.HT) {
279 _state = _HEADER_VALUE_START;
280 } else {
281 String headerField = _headerField.toString();
282 String headerValue =_headerValue.toString();
283 _headers[headerField] = headerValue;
284 _headerField = new StringBuffer();
285 _headerValue = new StringBuffer();
286 if (byte == _CharCode.CR) {
287 _state = _HEADER_ENDING;
164 } else { 288 } else {
165 // Start of new header field. 289 // Start of new header field.
166 _headerField.writeCharCode(_toLowerCase(byte)); 290 _headerField.writeCharCode(_toLowerCase(byte));
167 _state = _HEADER_FIELD; 291 _state = _HEADER_FIELD;
168 } 292 }
169 break; 293 }
294 break;
170 295
171 case _HEADER_FIELD: 296 case _HEADER_ENDING:
172 if (byte == _CharCode.COLON) { 297 _expect(byte, _CharCode.LF);
173 _state = _HEADER_VALUE_START; 298 _multipartController = new StreamController(
174 } else { 299 onPause: () {
175 if (!_isTokenChar(byte)) { 300 _pauseStream();
176 throw new MimeParserException("Invalid header field name"); 301 },
302 onResume: () {
303 _resumeStream();
304 _parse();
305 });
306 _controller.add(
307 new _MimeMultipartImpl(_headers, _multipartController.stream));
308 _headers = null;
309 _state = _CONTENT;
310 contentStartIndex = _index + 1;
311 break;
312
313 case _CONTENT:
314 if (_toLowerCase(byte) == _toLowerCase(_boundary[_boundaryIndex])) {
Søren Gjesse 2013/05/03 08:30:48 I know I worte this code, but looking at RFC2046 I
Anders Johnsen 2013/05/03 10:22:54 Done.
315 _boundaryIndex++;
316 if (_boundaryIndex == _boundary.length) {
317 if (contentStartIndex != null) {
318 _index++;
319 reportData();
320 _index--;
177 } 321 }
178 _headerField.writeCharCode(_toLowerCase(byte)); 322 _multipartController.close();
323 _boundaryIndex = 0;
324 _state = _BOUNDARY_ENDING;
179 } 325 }
180 break; 326 } else {
181 327 // Restart matching of the boundary.
182 case _HEADER_VALUE_START: 328 _index = _index - _boundaryIndex;
183 if (byte == _CharCode.CR) { 329 if (contentStartIndex == null) contentStartIndex = _index;
184 _state = _HEADER_VALUE_FOLDING_OR_ENDING; 330 _boundaryIndex = 0;
185 } else if (byte != _CharCode.SP && byte != _CharCode.HT) { 331 }
186 // Start of new header value. 332 break;
187 _headerValue.writeCharCode(byte);
188 _state = _HEADER_VALUE;
189 }
190 break;
191
192 case _HEADER_VALUE:
193 if (byte == _CharCode.CR) {
194 _state = _HEADER_VALUE_FOLDING_OR_ENDING;
195 } else {
196 _headerValue.writeCharCode(byte);
197 }
198 break;
199
200 case _HEADER_VALUE_FOLDING_OR_ENDING:
201 _expect(byte, _CharCode.LF);
202 _state = _HEADER_VALUE_FOLD_OR_END;
203 break;
204
205 case _HEADER_VALUE_FOLD_OR_END:
206 if (byte == _CharCode.SP || byte == _CharCode.HT) {
207 _state = _HEADER_VALUE_START;
208 } else {
209 String headerField = _headerField.toString();
210 String headerValue =_headerValue.toString();
211 if (headerReceived != null) {
212 headerReceived(headerField, headerValue);
213 }
214 _headerField = new StringBuffer();
215 _headerValue = new StringBuffer();
216 if (byte == _CharCode.CR) {
217 _state = _HEADER_ENDING;
218 } else {
219 // Start of new header field.
220 _headerField.writeCharCode(_toLowerCase(byte));
221 _state = _HEADER_FIELD;
222 }
223 }
224 break;
225
226 case _HEADER_ENDING:
227 _expect(byte, _CharCode.LF);
228 if (headersComplete != null) headersComplete();
229 _state = _CONTENT;
230 contentStartIndex = index + 1;
231 break;
232
233 case _CONTENT:
234 if (_toLowerCase(byte) == _toLowerCase(_boundary[_boundaryIndex])) {
235 _boundaryIndex++;
236 if (_boundaryIndex == _boundary.length) {
237 if (contentStartIndex != null) {
238 index++;
239 reportData();
240 index--;
241 }
242 _boundaryIndex = 0;
243 _state = _BOUNDARY_ENDING;
244 }
245 } else {
246 // Restart matching of the boundary.
247 index = index - _boundaryIndex;
248 if (contentStartIndex == null) contentStartIndex = index;
249 _boundaryIndex = 0;
250 }
251 break;
252 333
253 case _LAST_BOUNDARY_DASH2: 334 case _LAST_BOUNDARY_DASH2:
254 _expect(byte, _CharCode.DASH); 335 _expect(byte, _CharCode.DASH);
255 _state = _LAST_BOUNDARY_ENDING; 336 _state = _LAST_BOUNDARY_ENDING;
256 break; 337 break;
257 338
258 case _LAST_BOUNDARY_ENDING: 339 case _LAST_BOUNDARY_ENDING:
259 if (byte == _CharCode.CR) { 340 if (byte == _CharCode.CR) {
260 _state = _LAST_BOUNDARY_END; 341 _state = _LAST_BOUNDARY_END;
261 } else { 342 } else {
262 _expectWS(byte); 343 _expectWS(byte);
263 } 344 }
264 break; 345 break;
265 346
266 case _LAST_BOUNDARY_END: 347 case _LAST_BOUNDARY_END:
267 _expect(byte, _CharCode.LF); 348 _expect(byte, _CharCode.LF);
268 if (partEnd != null) { 349 _multipartController.close();
269 partEnd(true); 350 _multipartController = null;
270 }
271 _state = _DONE; 351 _state = _DONE;
272 break; 352 break;
273 353
274 default: 354 default:
275 // Should be unreachable. 355 // Should be unreachable.
276 assert(false); 356 assert(false);
277 break; 357 break;
278 } 358 }
279 359
280 // Move to the next byte. 360 // Move to the next byte.
281 index++; 361 _index++;
282 } 362 }
283 363
284 // Report any known content. 364 // Report any known content.
285 if (_state == _CONTENT && contentStartIndex != null) { 365 if (_state == _CONTENT && contentStartIndex != null) {
286 reportData(); 366 reportData();
287 } 367 }
288 return index - offset; 368
369 // Resume if at end.
370 if (_index == _buffer.length) {
371 _buffer = null;
372 _index = null;
373 _resumeStream();
374 }
289 } 375 }
290 376
291 bool _isTokenChar(int byte) { 377 bool _isTokenChar(int byte) {
292 return byte > 31 && byte < 128 && _Const.SEPARATORS.indexOf(byte) == -1; 378 return byte > 31 && byte < 128 && _Const.SEPARATORS.indexOf(byte) == -1;
293 } 379 }
294 380
295 int _toLowerCase(int byte) { 381 int _toLowerCase(int byte) {
296 final int aCode = "A".codeUnitAt(0); 382 final int aCode = "A".codeUnitAt(0);
297 final int zCode = "Z".codeUnitAt(0); 383 final int zCode = "Z".codeUnitAt(0);
298 final int delta = "a".codeUnitAt(0) - aCode; 384 final int delta = "a".codeUnitAt(0) - aCode;
299 return (aCode <= byte && byte <= zCode) ? byte + delta : byte; 385 return (aCode <= byte && byte <= zCode) ? byte + delta : byte;
300 } 386 }
301 387
302 void _expect(int val1, int val2) { 388 void _expect(int val1, int val2) {
303 if (val1 != val2) { 389 if (val1 != val2) {
390 print("BAD");
Søren Gjesse 2013/05/03 08:30:48 Debug print.
Anders Johnsen 2013/05/03 10:22:54 Done.
304 throw new MimeParserException("Failed to parse multipart mime 1"); 391 throw new MimeParserException("Failed to parse multipart mime 1");
305 } 392 }
306 } 393 }
307 394
308 void _expectWS(int byte) { 395 void _expectWS(int byte) {
309 if (byte != _CharCode.SP && byte != _CharCode.HT) { 396 if (byte != _CharCode.SP && byte != _CharCode.HT) {
397 print("BAD");
Søren Gjesse 2013/05/03 08:30:48 Debug print.
Anders Johnsen 2013/05/03 10:22:54 Done.
310 throw new MimeParserException("Failed to parse multipart mime 2"); 398 throw new MimeParserException("Failed to parse multipart mime 2");
311 } 399 }
312 } 400 }
313
314 List<int> _boundary;
315 int _state;
316 int _boundaryIndex = 0;
317
318 StringBuffer _headerField;
319 StringBuffer _headerValue;
320
321 Function partStart;
322 Function headerReceived;
323 Function headersComplete;
324 Function partDataReceived;
325 Function partEnd;
326 } 401 }
327 402
328 403
329 class MimeParserException implements Exception { 404 class MimeParserException implements Exception {
330 const MimeParserException([String this.message = ""]); 405 const MimeParserException([String this.message = ""]);
331 String toString() => "MimeParserException: $message"; 406 String toString() => "MimeParserException: $message";
332 final String message; 407 final String message;
333 } 408 }
OLDNEW
« no previous file with comments | « no previous file | tests/standalone/io/mime_multipart_parser_test.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698