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

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

Issue 25354003: Redo StreamTransformers so they work with Stack traces. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Address comments. Created 7 years, 2 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 | « sdk/lib/io/http_impl.dart ('k') | sdk/lib/utf/utf_stream.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) 2013, the Dart project authors. Please see the AUTHORS file 1 // Copyright (c) 2013, 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 const String _webSocketGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; 7 const String _webSocketGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
8 8
9 class _WebSocketMessageType { 9 class _WebSocketMessageType {
10 static const int NONE = 0; 10 static const int NONE = 0;
(...skipping 23 matching lines...) Expand all
34 34
35 /** 35 /**
36 * The web socket protocol transformer handles the protocol byte stream 36 * The web socket protocol transformer handles the protocol byte stream
37 * which is supplied through the [:handleData:]. As the protocol is processed, 37 * which is supplied through the [:handleData:]. As the protocol is processed,
38 * it'll output frame data as either a List<int> or String. 38 * it'll output frame data as either a List<int> or String.
39 * 39 *
40 * Important infomation about usage: Be sure you use cancelOnError, so the 40 * Important infomation about usage: Be sure you use cancelOnError, so the
41 * socket will be closed when the processer encounter an error. Not using it 41 * socket will be closed when the processer encounter an error. Not using it
42 * will lead to undefined behaviour. 42 * will lead to undefined behaviour.
43 */ 43 */
44 class _WebSocketProtocolTransformer extends StreamEventTransformer { 44 // TODO(ajohnsen): make this transformer reusable?
45 class _WebSocketProtocolTransformer implements StreamTransformer, EventSink {
45 static const int START = 0; 46 static const int START = 0;
46 static const int LEN_FIRST = 1; 47 static const int LEN_FIRST = 1;
47 static const int LEN_REST = 2; 48 static const int LEN_REST = 2;
48 static const int MASK = 3; 49 static const int MASK = 3;
49 static const int PAYLOAD = 4; 50 static const int PAYLOAD = 4;
50 static const int CLOSED = 5; 51 static const int CLOSED = 5;
51 static const int FAILURE = 6; 52 static const int FAILURE = 6;
52 53
53 bool _serverSide; 54 bool _serverSide;
55 EventSink _eventSink;
54 56
55 _WebSocketProtocolTransformer([bool this._serverSide = false]) { 57 _WebSocketProtocolTransformer([bool this._serverSide = false]) {
56 _prepareForNextFrame(); 58 _prepareForNextFrame();
57 _currentMessageType = _WebSocketMessageType.NONE; 59 _currentMessageType = _WebSocketMessageType.NONE;
58 } 60 }
59 61
62 Stream bind(Stream stream) {
63 return new Stream.eventTransformed(
64 stream,
65 (EventSink eventSink) {
66 if (_eventSink != null) {
67 throw new StateError("WebSocket transformer already used.");
68 }
69 _eventSink = eventSink;
70 return this;
71 });
72 }
73
74 void addError(Object error, [StackTrace stackTrace]) {
75 _eventSink.addError(error, stackTrace);
76 }
77
78 void close() => _eventSink.close();
79
60 /** 80 /**
61 * Process data received from the underlying communication channel. 81 * Process data received from the underlying communication channel.
62 */ 82 */
63 void handleData(Uint8List buffer, EventSink sink) { 83 void add(Uint8List buffer) {
64 int count = buffer.length; 84 int count = buffer.length;
65 int index = 0; 85 int index = 0;
66 int lastIndex = count; 86 int lastIndex = count;
67 try { 87 try {
68 if (_state == CLOSED) { 88 if (_state == CLOSED) {
69 throw new WebSocketException("Data on closed connection"); 89 throw new WebSocketException("Data on closed connection");
70 } 90 }
71 if (_state == FAILURE) { 91 if (_state == FAILURE) {
72 throw new WebSocketException("Data on failed connection"); 92 throw new WebSocketException("Data on failed connection");
73 } 93 }
(...skipping 17 matching lines...) Expand all
91 case _WebSocketOpcode.TEXT: 111 case _WebSocketOpcode.TEXT:
92 if (_currentMessageType != _WebSocketMessageType.NONE) { 112 if (_currentMessageType != _WebSocketMessageType.NONE) {
93 throw new WebSocketException("Protocol error"); 113 throw new WebSocketException("Protocol error");
94 } 114 }
95 _currentMessageType = _WebSocketMessageType.TEXT; 115 _currentMessageType = _WebSocketMessageType.TEXT;
96 _controller = new StreamController(sync: true); 116 _controller = new StreamController(sync: true);
97 _controller.stream 117 _controller.stream
98 .transform(UTF8.decoder) 118 .transform(UTF8.decoder)
99 .fold(new StringBuffer(), (buffer, str) => buffer..write(str)) 119 .fold(new StringBuffer(), (buffer, str) => buffer..write(str))
100 .then((buffer) { 120 .then((buffer) {
101 sink.add(buffer.toString()); 121 _eventSink.add(buffer.toString());
102 }, onError: sink.addError); 122 }, onError: _eventSink.addError);
103 break; 123 break;
104 124
105 case _WebSocketOpcode.BINARY: 125 case _WebSocketOpcode.BINARY:
106 if (_currentMessageType != _WebSocketMessageType.NONE) { 126 if (_currentMessageType != _WebSocketMessageType.NONE) {
107 throw new WebSocketException("Protocol error"); 127 throw new WebSocketException("Protocol error");
108 } 128 }
109 _currentMessageType = _WebSocketMessageType.BINARY; 129 _currentMessageType = _WebSocketMessageType.BINARY;
110 _controller = new StreamController(sync: true); 130 _controller = new StreamController(sync: true);
111 _controller.stream 131 _controller.stream
112 .fold(new BytesBuilder(), (buffer, data) => buffer..add(data)) 132 .fold(new BytesBuilder(), (buffer, data) => buffer..add(data))
113 .then((buffer) { 133 .then((buffer) {
114 sink.add(buffer.takeBytes()); 134 _eventSink.add(buffer.takeBytes());
115 }, onError: sink.addError); 135 }, onError: _eventSink.addError);
116 break; 136 break;
117 137
118 case _WebSocketOpcode.CLOSE: 138 case _WebSocketOpcode.CLOSE:
119 case _WebSocketOpcode.PING: 139 case _WebSocketOpcode.PING:
120 case _WebSocketOpcode.PONG: 140 case _WebSocketOpcode.PONG:
121 // Control frames cannot be fragmented. 141 // Control frames cannot be fragmented.
122 if (!_fin) throw new WebSocketException("Protocol error"); 142 if (!_fin) throw new WebSocketException("Protocol error");
123 break; 143 break;
124 144
125 default: 145 default:
126 throw new WebSocketException("Protocol error"); 146 throw new WebSocketException("Protocol error");
127 } 147 }
128 _state = LEN_FIRST; 148 _state = LEN_FIRST;
129 break; 149 break;
130 150
131 case LEN_FIRST: 151 case LEN_FIRST:
132 _masked = (byte & 0x80) != 0; 152 _masked = (byte & 0x80) != 0;
133 _len = byte & 0x7F; 153 _len = byte & 0x7F;
134 if (_isControlFrame() && _len > 125) { 154 if (_isControlFrame() && _len > 125) {
135 throw new WebSocketException("Protocol error"); 155 throw new WebSocketException("Protocol error");
136 } 156 }
137 if (_len < 126) { 157 if (_len < 126) {
138 _lengthDone(sink); 158 _lengthDone();
139 } else if (_len == 126) { 159 } else if (_len == 126) {
140 _len = 0; 160 _len = 0;
141 _remainingLenBytes = 2; 161 _remainingLenBytes = 2;
142 _state = LEN_REST; 162 _state = LEN_REST;
143 } else if (_len == 127) { 163 } else if (_len == 127) {
144 _len = 0; 164 _len = 0;
145 _remainingLenBytes = 8; 165 _remainingLenBytes = 8;
146 _state = LEN_REST; 166 _state = LEN_REST;
147 } 167 }
148 break; 168 break;
149 169
150 case LEN_REST: 170 case LEN_REST:
151 _len = _len << 8 | byte; 171 _len = _len << 8 | byte;
152 _remainingLenBytes--; 172 _remainingLenBytes--;
153 if (_remainingLenBytes == 0) { 173 if (_remainingLenBytes == 0) {
154 _lengthDone(sink); 174 _lengthDone();
155 } 175 }
156 break; 176 break;
157 177
158 case MASK: 178 case MASK:
159 _maskingKey = _maskingKey << 8 | byte; 179 _maskingKey = _maskingKey << 8 | byte;
160 _remainingMaskingKeyBytes--; 180 _remainingMaskingKeyBytes--;
161 if (_remainingMaskingKeyBytes == 0) { 181 if (_remainingMaskingKeyBytes == 0) {
162 _maskDone(sink); 182 _maskDone();
163 } 183 }
164 break; 184 break;
165 185
166 case PAYLOAD: 186 case PAYLOAD:
167 // The payload is not handled one byte at a time but in blocks. 187 // The payload is not handled one byte at a time but in blocks.
168 int payload; 188 int payload;
169 if (lastIndex - index <= _remainingPayloadBytes) { 189 if (lastIndex - index <= _remainingPayloadBytes) {
170 payload = lastIndex - index; 190 payload = lastIndex - index;
171 } else { 191 } else {
172 payload = _remainingPayloadBytes; 192 payload = _remainingPayloadBytes;
(...skipping 15 matching lines...) Expand all
188 // Allocate a buffer for collecting the control frame 208 // Allocate a buffer for collecting the control frame
189 // payload if any. 209 // payload if any.
190 if (_controlPayload == null) { 210 if (_controlPayload == null) {
191 _controlPayload = new List<int>(); 211 _controlPayload = new List<int>();
192 } 212 }
193 _controlPayload.addAll(buffer.sublist(index, index + payload)); 213 _controlPayload.addAll(buffer.sublist(index, index + payload));
194 index += payload; 214 index += payload;
195 } 215 }
196 216
197 if (_remainingPayloadBytes == 0) { 217 if (_remainingPayloadBytes == 0) {
198 _controlFrameEnd(sink); 218 _controlFrameEnd();
199 } 219 }
200 } else { 220 } else {
201 if (_currentMessageType != _WebSocketMessageType.TEXT && 221 if (_currentMessageType != _WebSocketMessageType.TEXT &&
202 _currentMessageType != _WebSocketMessageType.BINARY) { 222 _currentMessageType != _WebSocketMessageType.BINARY) {
203 throw new WebSocketException("Protocol error"); 223 throw new WebSocketException("Protocol error");
204 } 224 }
205 _controller.add( 225 _controller.add(
206 new Uint8List.view(buffer.buffer, index, payload)); 226 new Uint8List.view(buffer.buffer, index, payload));
207 index += payload; 227 index += payload;
208 if (_remainingPayloadBytes == 0) { 228 if (_remainingPayloadBytes == 0) {
209 _messageFrameEnd(sink); 229 _messageFrameEnd();
210 } 230 }
211 } 231 }
212 232
213 // Hack - as we always do index++ below. 233 // Hack - as we always do index++ below.
214 index--; 234 index--;
215 break; 235 break;
216 } 236 }
217 237
218 // Move to the next byte. 238 // Move to the next byte.
219 index++; 239 index++;
220 } 240 }
221 } catch (e) { 241 } catch (e, stackTrace) {
222 _state = FAILURE; 242 _state = FAILURE;
223 sink.addError(e); 243 _eventSink.addError(e, stackTrace);
224 } 244 }
225 } 245 }
226 246
227 void _lengthDone(EventSink sink) { 247 void _lengthDone() {
228 if (_masked) { 248 if (_masked) {
229 if (!_serverSide) { 249 if (!_serverSide) {
230 throw new WebSocketException("Received masked frame from server"); 250 throw new WebSocketException("Received masked frame from server");
231 } 251 }
232 _state = MASK; 252 _state = MASK;
233 _remainingMaskingKeyBytes = 4; 253 _remainingMaskingKeyBytes = 4;
234 } else { 254 } else {
235 if (_serverSide) { 255 if (_serverSide) {
236 throw new WebSocketException("Received unmasked frame from client"); 256 throw new WebSocketException("Received unmasked frame from client");
237 } 257 }
238 _remainingPayloadBytes = _len; 258 _remainingPayloadBytes = _len;
239 _startPayload(sink); 259 _startPayload();
240 } 260 }
241 } 261 }
242 262
243 void _maskDone(EventSink sink) { 263 void _maskDone() {
244 _remainingPayloadBytes = _len; 264 _remainingPayloadBytes = _len;
245 _startPayload(sink); 265 _startPayload();
246 } 266 }
247 267
248 void _startPayload(EventSink sink) { 268 void _startPayload() {
249 // If there is no actual payload perform perform callbacks without 269 // If there is no actual payload perform perform callbacks without
250 // going through the PAYLOAD state. 270 // going through the PAYLOAD state.
251 if (_remainingPayloadBytes == 0) { 271 if (_remainingPayloadBytes == 0) {
252 if (_isControlFrame()) { 272 if (_isControlFrame()) {
253 switch (_opcode) { 273 switch (_opcode) {
254 case _WebSocketOpcode.CLOSE: 274 case _WebSocketOpcode.CLOSE:
255 _state = CLOSED; 275 _state = CLOSED;
256 sink.close(); 276 _eventSink.close();
257 break; 277 break;
258 case _WebSocketOpcode.PING: 278 case _WebSocketOpcode.PING:
259 sink.add(new _WebSocketPing()); 279 _eventSink.add(new _WebSocketPing());
260 break; 280 break;
261 case _WebSocketOpcode.PONG: 281 case _WebSocketOpcode.PONG:
262 sink.add(new _WebSocketPong()); 282 _eventSink.add(new _WebSocketPong());
263 break; 283 break;
264 } 284 }
265 _prepareForNextFrame(); 285 _prepareForNextFrame();
266 } else { 286 } else {
267 _messageFrameEnd(sink); 287 _messageFrameEnd();
268 } 288 }
269 } else { 289 } else {
270 _state = PAYLOAD; 290 _state = PAYLOAD;
271 } 291 }
272 } 292 }
273 293
274 void _messageFrameEnd(EventSink sink) { 294 void _messageFrameEnd() {
275 if (_fin) { 295 if (_fin) {
276 switch (_currentMessageType) { 296 switch (_currentMessageType) {
277 case _WebSocketMessageType.TEXT: 297 case _WebSocketMessageType.TEXT:
278 _controller.close(); 298 _controller.close();
279 break; 299 break;
280 case _WebSocketMessageType.BINARY: 300 case _WebSocketMessageType.BINARY:
281 _controller.close(); 301 _controller.close();
282 break; 302 break;
283 } 303 }
284 _controller = null; 304 _controller = null;
285 _currentMessageType = _WebSocketMessageType.NONE; 305 _currentMessageType = _WebSocketMessageType.NONE;
286 } 306 }
287 _prepareForNextFrame(); 307 _prepareForNextFrame();
288 } 308 }
289 309
290 void _controlFrameEnd(EventSink sink) { 310 void _controlFrameEnd() {
291 switch (_opcode) { 311 switch (_opcode) {
292 case _WebSocketOpcode.CLOSE: 312 case _WebSocketOpcode.CLOSE:
293 closeCode = WebSocketStatus.NO_STATUS_RECEIVED; 313 closeCode = WebSocketStatus.NO_STATUS_RECEIVED;
294 if (_controlPayload.length > 0) { 314 if (_controlPayload.length > 0) {
295 if (_controlPayload.length == 1) { 315 if (_controlPayload.length == 1) {
296 throw new WebSocketException("Protocol error"); 316 throw new WebSocketException("Protocol error");
297 } 317 }
298 closeCode = _controlPayload[0] << 8 | _controlPayload[1]; 318 closeCode = _controlPayload[0] << 8 | _controlPayload[1];
299 if (closeCode == WebSocketStatus.NO_STATUS_RECEIVED) { 319 if (closeCode == WebSocketStatus.NO_STATUS_RECEIVED) {
300 throw new WebSocketException("Protocol error"); 320 throw new WebSocketException("Protocol error");
301 } 321 }
302 if (_controlPayload.length > 2) { 322 if (_controlPayload.length > 2) {
303 closeReason = UTF8.decode(_controlPayload.sublist(2)); 323 closeReason = UTF8.decode(_controlPayload.sublist(2));
304 } 324 }
305 } 325 }
306 _state = CLOSED; 326 _state = CLOSED;
307 sink.close(); 327 _eventSink.close();
308 break; 328 break;
309 329
310 case _WebSocketOpcode.PING: 330 case _WebSocketOpcode.PING:
311 sink.add(new _WebSocketPing(_controlPayload)); 331 _eventSink.add(new _WebSocketPing(_controlPayload));
312 break; 332 break;
313 333
314 case _WebSocketOpcode.PONG: 334 case _WebSocketOpcode.PONG:
315 sink.add(new _WebSocketPong(_controlPayload)); 335 _eventSink.add(new _WebSocketPong(_controlPayload));
316 break; 336 break;
317 } 337 }
318 _prepareForNextFrame(); 338 _prepareForNextFrame();
319 } 339 }
320 340
321 bool _isControlFrame() { 341 bool _isControlFrame() {
322 return _opcode == _WebSocketOpcode.CLOSE || 342 return _opcode == _WebSocketOpcode.CLOSE ||
323 _opcode == _WebSocketOpcode.PING || 343 _opcode == _WebSocketOpcode.PING ||
324 _opcode == _WebSocketOpcode.PONG; 344 _opcode == _WebSocketOpcode.PONG;
325 } 345 }
(...skipping 107 matching lines...) Expand 10 before | Expand all | Expand 10 after
433 } 453 }
434 String key = request.headers.value("Sec-WebSocket-Key"); 454 String key = request.headers.value("Sec-WebSocket-Key");
435 if (key == null) { 455 if (key == null) {
436 return false; 456 return false;
437 } 457 }
438 return true; 458 return true;
439 } 459 }
440 } 460 }
441 461
442 462
443 class _WebSocketOutgoingTransformer extends StreamEventTransformer { 463 // TODO(ajohnsen): Make this transformer reusable.
464 class _WebSocketOutgoingTransformer implements StreamTransformer, EventSink {
444 final _WebSocketImpl webSocket; 465 final _WebSocketImpl webSocket;
466 EventSink _eventSink;
445 467
446 _WebSocketOutgoingTransformer(_WebSocketImpl this.webSocket); 468 _WebSocketOutgoingTransformer(_WebSocketImpl this.webSocket);
447 469
448 void handleData(message, EventSink<List<int>> sink) { 470 Stream bind(Stream stream) {
471 return new Stream.eventTransformed(
472 stream,
473 (EventSink eventSink) {
474 if (_eventSink != null) {
475 throw new StateError("WebSocket transformer already used");
476 }
477 _eventSink = eventSink;
478 return this;
479 });
480 }
481
482 void add(message) {
449 if (message is _WebSocketPong) { 483 if (message is _WebSocketPong) {
450 addFrame(_WebSocketOpcode.PONG, message.payload, sink); 484 addFrame(_WebSocketOpcode.PONG, message.payload);
451 return; 485 return;
452 } 486 }
453 if (message is _WebSocketPing) { 487 if (message is _WebSocketPing) {
454 addFrame(_WebSocketOpcode.PING, message.payload, sink); 488 addFrame(_WebSocketOpcode.PING, message.payload);
455 return; 489 return;
456 } 490 }
457 List<int> data; 491 List<int> data;
458 int opcode; 492 int opcode;
459 if (message != null) { 493 if (message != null) {
460 if (message is String) { 494 if (message is String) {
461 opcode = _WebSocketOpcode.TEXT; 495 opcode = _WebSocketOpcode.TEXT;
462 data = UTF8.encode(message); 496 data = UTF8.encode(message);
463 } else { 497 } else {
464 if (message is !List<int>) { 498 if (message is !List<int>) {
465 throw new ArgumentError(message); 499 throw new ArgumentError(message);
466 } 500 }
467 opcode = _WebSocketOpcode.BINARY; 501 opcode = _WebSocketOpcode.BINARY;
468 data = message; 502 data = message;
469 } 503 }
470 } else { 504 } else {
471 opcode = _WebSocketOpcode.TEXT; 505 opcode = _WebSocketOpcode.TEXT;
472 } 506 }
473 addFrame(opcode, data, sink); 507 addFrame(opcode, data);
474 } 508 }
475 509
476 void handleDone(EventSink<List<int>> sink) { 510 void addError(Object error, [StackTrace stackTrace]) {
511 _eventSink.addError(error, stackTrace);
512 }
513
514 void close() {
477 int code = webSocket._outCloseCode; 515 int code = webSocket._outCloseCode;
478 String reason = webSocket._outCloseReason; 516 String reason = webSocket._outCloseReason;
479 List<int> data; 517 List<int> data;
480 if (code != null) { 518 if (code != null) {
481 data = new List<int>(); 519 data = new List<int>();
482 data.add((code >> 8) & 0xFF); 520 data.add((code >> 8) & 0xFF);
483 data.add(code & 0xFF); 521 data.add(code & 0xFF);
484 if (reason != null) { 522 if (reason != null) {
485 data.addAll(UTF8.encode(reason)); 523 data.addAll(UTF8.encode(reason));
486 } 524 }
487 } 525 }
488 addFrame(_WebSocketOpcode.CLOSE, data, sink); 526 addFrame(_WebSocketOpcode.CLOSE, data);
489 sink.close(); 527 _eventSink.close();
490 } 528 }
491 529
492 void addFrame(int opcode, List<int> data, EventSink<List<int>> sink) { 530 void addFrame(int opcode, List<int> data) {
493 createFrame(opcode, data, webSocket._serverSide).forEach(sink.add); 531 createFrame(opcode, data, webSocket._serverSide).forEach(_eventSink.add);
494 } 532 }
495 533
496 static Iterable createFrame(int opcode, List<int> data, bool serverSide) { 534 static Iterable createFrame(int opcode, List<int> data, bool serverSide) {
497 bool mask = !serverSide; // Masking not implemented for server. 535 bool mask = !serverSide; // Masking not implemented for server.
498 int dataLength = data == null ? 0 : data.length; 536 int dataLength = data == null ? 0 : data.length;
499 // Determine the header size. 537 // Determine the header size.
500 int headerSize = (mask) ? 6 : 2; 538 int headerSize = (mask) ? 6 : 2;
501 if (dataLength > 65535) { 539 if (dataLength > 65535) {
502 headerSize += 8; 540 headerSize += 8;
503 } else if (dataLength > 125) { 541 } else if (dataLength > 125) {
(...skipping 386 matching lines...) Expand 10 before | Expand all | Expand 10 after
890 (code < WebSocketStatus.NORMAL_CLOSURE || 928 (code < WebSocketStatus.NORMAL_CLOSURE ||
891 code == WebSocketStatus.RESERVED_1004 || 929 code == WebSocketStatus.RESERVED_1004 ||
892 code == WebSocketStatus.NO_STATUS_RECEIVED || 930 code == WebSocketStatus.NO_STATUS_RECEIVED ||
893 code == WebSocketStatus.ABNORMAL_CLOSURE || 931 code == WebSocketStatus.ABNORMAL_CLOSURE ||
894 (code > WebSocketStatus.INTERNAL_SERVER_ERROR && 932 (code > WebSocketStatus.INTERNAL_SERVER_ERROR &&
895 code < WebSocketStatus.RESERVED_1015) || 933 code < WebSocketStatus.RESERVED_1015) ||
896 (code >= WebSocketStatus.RESERVED_1015 && 934 (code >= WebSocketStatus.RESERVED_1015 &&
897 code < 3000)); 935 code < 3000));
898 } 936 }
899 } 937 }
OLDNEW
« no previous file with comments | « sdk/lib/io/http_impl.dart ('k') | sdk/lib/utf/utf_stream.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698