OLD | NEW |
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 | |
5 library mime.multipart_transformer; | 4 library mime.multipart_transformer; |
6 | 5 |
7 import 'dart:async'; | 6 import 'dart:async'; |
8 import 'dart:convert'; | |
9 import 'dart:typed_data'; | 7 import 'dart:typed_data'; |
10 | 8 |
11 /** | 9 import 'bound_multipart_stream.dart'; |
12 * A Mime Multipart class representing each part parsed by | 10 import 'mime_shared.dart'; |
13 * [MimeMultipartTransformer]. The data is streamed in as it become available. | 11 import 'char_code.dart'; |
14 */ | |
15 abstract class MimeMultipart extends Stream<List<int>> { | |
16 Map<String, String> get headers; | |
17 } | |
18 | 12 |
19 class _MimeMultipart extends MimeMultipart { | |
20 final Map<String, String> headers; | |
21 final Stream<List<int>> _stream; | |
22 | 13 |
23 _MimeMultipart(this.headers, this._stream); | 14 Uint8List _getBoundary(String boundary) { |
| 15 var charCodes = boundary.codeUnits; |
24 | 16 |
25 StreamSubscription<List<int>> listen(void onData(List<int> data), | 17 var boundaryList = new Uint8List(4 + charCodes.length); |
26 {void onDone(), | 18 // Set-up the matching boundary preceding it with CRLF and two |
27 Function onError, | 19 // dashes. |
28 bool cancelOnError}) { | 20 boundaryList[0] = CharCode.CR; |
29 return _stream.listen(onData, | 21 boundaryList[1] = CharCode.LF; |
30 onDone: onDone, | 22 boundaryList[2] = CharCode.DASH; |
31 onError: onError, | 23 boundaryList[3] = CharCode.DASH; |
32 cancelOnError: cancelOnError); | 24 boundaryList.setRange(4, 4 + charCodes.length, charCodes); |
33 } | 25 return boundaryList; |
34 } | |
35 | |
36 class _CharCode { | |
37 static const int HT = 9; | |
38 static const int LF = 10; | |
39 static const int CR = 13; | |
40 static const int SP = 32; | |
41 static const int DASH = 45; | |
42 static const int COLON = 58; | |
43 } | 26 } |
44 | 27 |
45 /** | 28 /** |
46 * Parser for MIME multipart types of data as described in RFC 2046 | 29 * Parser for MIME multipart types of data as described in RFC 2046 |
47 * section 5.1.1. The data is transformed into [MimeMultipart] objects, each | 30 * section 5.1.1. The data is transformed into [MimeMultipart] objects, each |
48 * of them streaming the multipart data. | 31 * of them streaming the multipart data. |
49 */ | 32 */ |
50 class MimeMultipartTransformer | 33 class MimeMultipartTransformer |
51 implements StreamTransformer<List<int>, MimeMultipart> { | 34 implements StreamTransformer<List<int>, MimeMultipart> { |
52 static const int _START = 0; | |
53 static const int _FIRST_BOUNDARY_ENDING = 111; | |
54 static const int _FIRST_BOUNDARY_END = 112; | |
55 static const int _BOUNDARY_ENDING = 1; | |
56 static const int _BOUNDARY_END = 2; | |
57 static const int _HEADER_START = 3; | |
58 static const int _HEADER_FIELD = 4; | |
59 static const int _HEADER_VALUE_START = 5; | |
60 static const int _HEADER_VALUE = 6; | |
61 static const int _HEADER_VALUE_FOLDING_OR_ENDING = 7; | |
62 static const int _HEADER_VALUE_FOLD_OR_END = 8; | |
63 static const int _HEADER_ENDING = 9; | |
64 static const int _CONTENT = 10; | |
65 static const int _LAST_BOUNDARY_DASH2 = 11; | |
66 static const int _LAST_BOUNDARY_ENDING = 12; | |
67 static const int _LAST_BOUNDARY_END = 13; | |
68 static const int _DONE = 14; | |
69 static const int _FAILURE = 15; | |
70 | 35 |
71 // Bytes for '()<>@,;:\\"/[]?={} \t'. | 36 final List<int> _boundary; |
72 static const _SEPARATORS = const [40, 41, 60, 62, 64, 44, 59, 58, 92, 34, 47, | |
73 91, 93, 63, 61, 123, 125, 32, 9]; | |
74 | |
75 StreamController _controller; | |
76 StreamSubscription _subscription; | |
77 | |
78 StreamController _multipartController; | |
79 Map<String, String> _headers; | |
80 | |
81 List<int> _boundary; | |
82 int _state = _START; | |
83 int _boundaryIndex = 2; | |
84 | |
85 // Current index in the data buffer. If index is negative then it | |
86 // is the index into the artificial prefix of the boundary string. | |
87 int _index; | |
88 List<int> _buffer; | |
89 | |
90 List<int> _headerField = []; | |
91 List<int> _headerValue = []; | |
92 | 37 |
93 /** | 38 /** |
94 * Construct a new MIME multipart parser with the boundary | 39 * Construct a new MIME multipart parser with the boundary |
95 * [boundary]. The boundary should be as specified in the content | 40 * [boundary]. The boundary should be as specified in the content |
96 * type parameter, that is without the -- prefix. | 41 * type parameter, that is without the -- prefix. |
97 */ | 42 */ |
98 MimeMultipartTransformer(String boundary) { | 43 MimeMultipartTransformer(String boundary) |
99 List<int> charCodes = boundary.codeUnits; | 44 : _boundary = _getBoundary(boundary); |
100 _boundary = new Uint8List(4 + charCodes.length); | |
101 // Set-up the matching boundary preceding it with CRLF and two | |
102 // dashes. | |
103 _boundary[0] = _CharCode.CR; | |
104 _boundary[1] = _CharCode.LF; | |
105 _boundary[2] = _CharCode.DASH; | |
106 _boundary[3] = _CharCode.DASH; | |
107 _boundary.setRange(4, 4 + charCodes.length, charCodes); | |
108 } | |
109 | |
110 void _resumeStream() { | |
111 _subscription.resume(); | |
112 } | |
113 | |
114 void _pauseStream() { | |
115 _subscription.pause(); | |
116 } | |
117 | 45 |
118 Stream<MimeMultipart> bind(Stream<List<int>> stream) { | 46 Stream<MimeMultipart> bind(Stream<List<int>> stream) { |
119 _controller = new StreamController( | 47 return new BoundMultipartStream(_boundary, stream).stream; |
120 sync: true, | |
121 onPause: _pauseStream, | |
122 onResume:_resumeStream, | |
123 onCancel: () { | |
124 _subscription.cancel(); | |
125 }, | |
126 onListen: () { | |
127 _subscription = stream.listen( | |
128 (data) { | |
129 assert(_buffer == null); | |
130 _pauseStream(); | |
131 _buffer = data; | |
132 _index = 0; | |
133 _parse(); | |
134 }, | |
135 onDone: () { | |
136 if (_state != _DONE) { | |
137 _controller.addError( | |
138 new MimeMultipartException("Bad multipart ending")); | |
139 } | |
140 _controller.close(); | |
141 }, | |
142 onError: _controller.addError); | |
143 }); | |
144 return _controller.stream; | |
145 } | |
146 | |
147 void _parse() { | |
148 // Number of boundary bytes to artificially place before the supplied data. | |
149 int boundaryPrefix = 0; | |
150 // Position where content starts. Will be null if no known content | |
151 // start exists. Will be negative of the content starts in the | |
152 // boundary prefix. Will be zero or position if the content starts | |
153 // in the current buffer. | |
154 int contentStartIndex; | |
155 | |
156 // Function to report content data for the current part. The data | |
157 // reported is from the current content start index up til the | |
158 // current index. As the data can be artificially prefixed with a | |
159 // prefix of the boundary both the content start index and index | |
160 // can be negative. | |
161 void reportData() { | |
162 if (contentStartIndex < 0) { | |
163 var contentLength = boundaryPrefix + _index - _boundaryIndex; | |
164 if (contentLength <= boundaryPrefix) { | |
165 _multipartController.add( | |
166 _boundary.sublist(0, contentLength)); | |
167 } else { | |
168 _multipartController.add( | |
169 _boundary.sublist(0, boundaryPrefix)); | |
170 _multipartController.add( | |
171 _buffer.sublist(0, contentLength - boundaryPrefix)); | |
172 } | |
173 } else { | |
174 var contentEndIndex = _index - _boundaryIndex; | |
175 _multipartController.add( | |
176 _buffer.sublist(contentStartIndex, contentEndIndex)); | |
177 } | |
178 } | |
179 | |
180 if (_state == _CONTENT && _boundaryIndex == 0) { | |
181 contentStartIndex = 0; | |
182 } else { | |
183 contentStartIndex = null; | |
184 } | |
185 // The data to parse might be "artificially" prefixed with a | |
186 // partial match of the boundary. | |
187 boundaryPrefix = _boundaryIndex; | |
188 | |
189 while ((_index < _buffer.length) && _state != _FAILURE && _state != _DONE) { | |
190 if (_multipartController != null && _multipartController.isPaused) { | |
191 return; | |
192 } | |
193 int byte; | |
194 if (_index < 0) { | |
195 byte = _boundary[boundaryPrefix + _index]; | |
196 } else { | |
197 byte = _buffer[_index]; | |
198 } | |
199 switch (_state) { | |
200 case _START: | |
201 if (byte == _boundary[_boundaryIndex]) { | |
202 _boundaryIndex++; | |
203 if (_boundaryIndex == _boundary.length) { | |
204 _state = _FIRST_BOUNDARY_ENDING; | |
205 _boundaryIndex = 0; | |
206 } | |
207 } else { | |
208 // Restart matching of the boundary. | |
209 _index = _index - _boundaryIndex; | |
210 _boundaryIndex = 0; | |
211 } | |
212 break; | |
213 | |
214 case _FIRST_BOUNDARY_ENDING: | |
215 if (byte == _CharCode.CR) { | |
216 _state = _FIRST_BOUNDARY_END; | |
217 } else { | |
218 _expectWS(byte); | |
219 } | |
220 break; | |
221 | |
222 case _FIRST_BOUNDARY_END: | |
223 _expect(byte, _CharCode.LF); | |
224 _state = _HEADER_START; | |
225 break; | |
226 | |
227 case _BOUNDARY_ENDING: | |
228 if (byte == _CharCode.CR) { | |
229 _state = _BOUNDARY_END; | |
230 } else if (byte == _CharCode.DASH) { | |
231 _state = _LAST_BOUNDARY_DASH2; | |
232 } else { | |
233 _expectWS(byte); | |
234 } | |
235 break; | |
236 | |
237 case _BOUNDARY_END: | |
238 _expect(byte, _CharCode.LF); | |
239 _multipartController.close(); | |
240 _multipartController = null; | |
241 _state = _HEADER_START; | |
242 break; | |
243 | |
244 case _HEADER_START: | |
245 _headers = new Map<String, String>(); | |
246 if (byte == _CharCode.CR) { | |
247 _state = _HEADER_ENDING; | |
248 } else { | |
249 // Start of new header field. | |
250 _headerField.add(_toLowerCase(byte)); | |
251 _state = _HEADER_FIELD; | |
252 } | |
253 break; | |
254 | |
255 case _HEADER_FIELD: | |
256 if (byte == _CharCode.COLON) { | |
257 _state = _HEADER_VALUE_START; | |
258 } else { | |
259 if (!_isTokenChar(byte)) { | |
260 throw new MimeMultipartException("Invalid header field name"); | |
261 } | |
262 _headerField.add(_toLowerCase(byte)); | |
263 } | |
264 break; | |
265 | |
266 case _HEADER_VALUE_START: | |
267 if (byte == _CharCode.CR) { | |
268 _state = _HEADER_VALUE_FOLDING_OR_ENDING; | |
269 } else if (byte != _CharCode.SP && byte != _CharCode.HT) { | |
270 // Start of new header value. | |
271 _headerValue.add(byte); | |
272 _state = _HEADER_VALUE; | |
273 } | |
274 break; | |
275 | |
276 case _HEADER_VALUE: | |
277 if (byte == _CharCode.CR) { | |
278 _state = _HEADER_VALUE_FOLDING_OR_ENDING; | |
279 } else { | |
280 _headerValue.add(byte); | |
281 } | |
282 break; | |
283 | |
284 case _HEADER_VALUE_FOLDING_OR_ENDING: | |
285 _expect(byte, _CharCode.LF); | |
286 _state = _HEADER_VALUE_FOLD_OR_END; | |
287 break; | |
288 | |
289 case _HEADER_VALUE_FOLD_OR_END: | |
290 if (byte == _CharCode.SP || byte == _CharCode.HT) { | |
291 _state = _HEADER_VALUE_START; | |
292 } else { | |
293 String headerField = UTF8.decode(_headerField); | |
294 String headerValue = UTF8.decode(_headerValue); | |
295 _headers[headerField.toLowerCase()] = headerValue; | |
296 _headerField = []; | |
297 _headerValue = []; | |
298 if (byte == _CharCode.CR) { | |
299 _state = _HEADER_ENDING; | |
300 } else { | |
301 // Start of new header field. | |
302 _headerField.add(_toLowerCase(byte)); | |
303 _state = _HEADER_FIELD; | |
304 } | |
305 } | |
306 break; | |
307 | |
308 case _HEADER_ENDING: | |
309 _expect(byte, _CharCode.LF); | |
310 _multipartController = new StreamController( | |
311 sync: true, | |
312 onPause: () { | |
313 _pauseStream(); | |
314 }, | |
315 onResume: () { | |
316 _resumeStream(); | |
317 _parse(); | |
318 }); | |
319 _controller.add( | |
320 new _MimeMultipart(_headers, _multipartController.stream)); | |
321 _headers = null; | |
322 _state = _CONTENT; | |
323 contentStartIndex = _index + 1; | |
324 break; | |
325 | |
326 case _CONTENT: | |
327 if (byte == _boundary[_boundaryIndex]) { | |
328 _boundaryIndex++; | |
329 if (_boundaryIndex == _boundary.length) { | |
330 if (contentStartIndex != null) { | |
331 _index++; | |
332 reportData(); | |
333 _index--; | |
334 } | |
335 _multipartController.close(); | |
336 _boundaryIndex = 0; | |
337 _state = _BOUNDARY_ENDING; | |
338 } | |
339 } else { | |
340 // Restart matching of the boundary. | |
341 _index = _index - _boundaryIndex; | |
342 if (contentStartIndex == null) contentStartIndex = _index; | |
343 _boundaryIndex = 0; | |
344 } | |
345 break; | |
346 | |
347 case _LAST_BOUNDARY_DASH2: | |
348 _expect(byte, _CharCode.DASH); | |
349 _state = _LAST_BOUNDARY_ENDING; | |
350 break; | |
351 | |
352 case _LAST_BOUNDARY_ENDING: | |
353 if (byte == _CharCode.CR) { | |
354 _state = _LAST_BOUNDARY_END; | |
355 } else { | |
356 _expectWS(byte); | |
357 } | |
358 break; | |
359 | |
360 case _LAST_BOUNDARY_END: | |
361 _expect(byte, _CharCode.LF); | |
362 _multipartController.close(); | |
363 _multipartController = null; | |
364 _state = _DONE; | |
365 break; | |
366 | |
367 default: | |
368 // Should be unreachable. | |
369 assert(false); | |
370 break; | |
371 } | |
372 | |
373 // Move to the next byte. | |
374 _index++; | |
375 } | |
376 | |
377 // Report any known content. | |
378 if (_state == _CONTENT && contentStartIndex != null) { | |
379 reportData(); | |
380 } | |
381 | |
382 // Resume if at end. | |
383 if (_index == _buffer.length) { | |
384 _buffer = null; | |
385 _index = null; | |
386 _resumeStream(); | |
387 } | |
388 } | |
389 | |
390 bool _isTokenChar(int byte) { | |
391 return byte > 31 && byte < 128 && _SEPARATORS.indexOf(byte) == -1; | |
392 } | |
393 | |
394 int _toLowerCase(int byte) { | |
395 final int aCode = "A".codeUnitAt(0); | |
396 final int zCode = "Z".codeUnitAt(0); | |
397 final int delta = "a".codeUnitAt(0) - aCode; | |
398 return (aCode <= byte && byte <= zCode) ? byte + delta : byte; | |
399 } | |
400 | |
401 void _expect(int val1, int val2) { | |
402 if (val1 != val2) { | |
403 throw new MimeMultipartException("Failed to parse multipart mime 1"); | |
404 } | |
405 } | |
406 | |
407 void _expectWS(int byte) { | |
408 if (byte != _CharCode.SP && byte != _CharCode.HT) { | |
409 throw new MimeMultipartException("Failed to parse multipart mime 2"); | |
410 } | |
411 } | 48 } |
412 } | 49 } |
413 | |
414 | |
415 class MimeMultipartException implements Exception { | |
416 final String message; | |
417 | |
418 const MimeMultipartException([String this.message = ""]); | |
419 | |
420 String toString() => "MimeMultipartException: $message"; | |
421 } | |
OLD | NEW |