OLD | NEW |
(Empty) | |
| 1 // Copyright 2014 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 'use strict'; |
| 6 |
| 7 /** @suppress {duplicate} */ |
| 8 var remoting = remoting || {}; |
| 9 |
| 10 /** |
| 11 * XmppStreamParser is used to parse XMPP stream. Data is fed to the parser |
| 12 * using appendData() method and it calls |onStanzaCallback| and |
| 13 * |onErrorCallback| specified using setCallbacks(). |
| 14 * |
| 15 * @constructor |
| 16 */ |
| 17 remoting.XmppStreamParser = function() { |
| 18 /** @type {function(Element):void} @private */ |
| 19 this.onStanzaCallback_ = function(stanza) {}; |
| 20 /** @type {function(string):void} @private */ |
| 21 this.onErrorCallback_ = function(error) {}; |
| 22 |
| 23 /** |
| 24 * Buffer containing the data that has been received but haven't been parsed. |
| 25 * @private |
| 26 */ |
| 27 this.data_ = new ArrayBuffer(0); |
| 28 |
| 29 /** |
| 30 * Current depth in the XML stream. |
| 31 * @private |
| 32 */ |
| 33 this.depth_ = 0; |
| 34 |
| 35 /** |
| 36 * Set to true after error. |
| 37 * @private |
| 38 */ |
| 39 this.error_ = false; |
| 40 |
| 41 /** |
| 42 * The <stream> opening tag received at the beginning of the stream. |
| 43 * @private |
| 44 */ |
| 45 this.startTag_ = ''; |
| 46 |
| 47 /** |
| 48 * Closing tag matching |startTag_|. |
| 49 * @private |
| 50 */ |
| 51 this.startTagEnd_ = ''; |
| 52 |
| 53 /** |
| 54 * String containing current incomplete stanza. |
| 55 * @private |
| 56 */ |
| 57 this.currentStanza_ = ''; |
| 58 } |
| 59 |
| 60 /** |
| 61 * Sets callbacks to be called on incoming stanzas and on error. |
| 62 * |
| 63 * @param {function(Element):void} onStanzaCallback |
| 64 * @param {function(string):void} onErrorCallback |
| 65 */ |
| 66 remoting.XmppStreamParser.prototype.setCallbacks = |
| 67 function(onStanzaCallback, onErrorCallback) { |
| 68 this.onStanzaCallback_ = onStanzaCallback; |
| 69 this.onErrorCallback_ = onErrorCallback; |
| 70 } |
| 71 |
| 72 /** @param {ArrayBuffer} data */ |
| 73 remoting.XmppStreamParser.prototype.appendData = function(data) { |
| 74 base.debug.assert(!this.error_); |
| 75 |
| 76 if (this.data_.byteLength > 0) { |
| 77 // Concatenate two buffers. |
| 78 var newData = new Uint8Array(this.data_.byteLength + data.byteLength); |
| 79 newData.set(new Uint8Array(this.data_), 0); |
| 80 newData.set(new Uint8Array(data), this.data_.byteLength); |
| 81 this.data_ = newData.buffer; |
| 82 } else { |
| 83 this.data_ = data; |
| 84 } |
| 85 |
| 86 // Check if the newly appended data completes XML tag or a piece of text by |
| 87 // looking for '<' and '>' char codes. This has to be done before converting |
| 88 // data to string because the input may not contain complete UTF-8 sequence. |
| 89 var tagStartCode = '<'.charCodeAt(0); |
| 90 var tagEndCode = '>'.charCodeAt(0); |
| 91 var spaceCode = ' '.charCodeAt(0); |
| 92 var tryAgain = true; |
| 93 while (this.data_.byteLength > 0 && tryAgain && !this.error_) { |
| 94 tryAgain = false; |
| 95 |
| 96 // If we are not currently in a middle of a stanza then skip spaces (server |
| 97 // may send spaces periodically as heartbeats) and make sure that the first |
| 98 // character starts XML tag. |
| 99 if (this.depth_ <= 1) { |
| 100 var view = new DataView(this.data_); |
| 101 var firstChar = view.getUint8(0); |
| 102 if (firstChar == spaceCode) { |
| 103 tryAgain = true; |
| 104 this.data_ = this.data_.slice(1); |
| 105 continue; |
| 106 } else if (firstChar != tagStartCode) { |
| 107 var dataAsText = ''; |
| 108 try { |
| 109 dataAsText = base.decodeUtf8(this.data_); |
| 110 } catch (exception) { |
| 111 dataAsText = 'charCode = ' + firstChar; |
| 112 } |
| 113 this.processError_('Received unexpected text data: ' + dataAsText); |
| 114 return; |
| 115 } |
| 116 } |
| 117 |
| 118 // Iterate over characters in the buffer to find complete tags. |
| 119 var view = new DataView(this.data_); |
| 120 for (var i = 0; i < view.byteLength; ++i) { |
| 121 var currentChar = view.getUint8(i); |
| 122 if (currentChar == tagStartCode) { |
| 123 if (i > 0) { |
| 124 var text = this.extractStringFromBuffer_(i); |
| 125 if (text == null) |
| 126 return; |
| 127 this.processText_(text); |
| 128 tryAgain = true; |
| 129 break; |
| 130 } |
| 131 } else if (currentChar == tagEndCode) { |
| 132 var tag = this.extractStringFromBuffer_(i + 1); |
| 133 if (tag == null) |
| 134 return; |
| 135 if (tag.charAt(0) != '<') { |
| 136 this.processError_('Received \'>\' without \'<\': ' + tag); |
| 137 return; |
| 138 } |
| 139 this.processTag_(tag); |
| 140 tryAgain = true; |
| 141 break; |
| 142 } |
| 143 } |
| 144 } |
| 145 } |
| 146 |
| 147 /** |
| 148 * @param {string} text |
| 149 * @private |
| 150 */ |
| 151 remoting.XmppStreamParser.prototype.processText_ = function(text) { |
| 152 // Tokenization code in appendData() shouldn't allow text tokens in between |
| 153 // stanzas. |
| 154 base.debug.assert(this.depth_ > 1); |
| 155 this.currentStanza_ += text; |
| 156 } |
| 157 |
| 158 /** |
| 159 * @param {string} tag |
| 160 * @private |
| 161 */ |
| 162 remoting.XmppStreamParser.prototype.processTag_ = function(tag) { |
| 163 base.debug.assert(tag.charAt(0) == '<'); |
| 164 base.debug.assert(tag.charAt(tag.length - 1) == '>'); |
| 165 |
| 166 this.currentStanza_ += tag; |
| 167 |
| 168 var openTag = tag.charAt(1) != '/'; |
| 169 if (openTag) { |
| 170 ++this.depth_; |
| 171 if (this.depth_ == 1) { |
| 172 this.startTag_ = this.currentStanza_; |
| 173 this.currentStanza_ = ''; |
| 174 |
| 175 // Create end tag matching the start. |
| 176 var tagName = |
| 177 this.startTag_.substr(1, this.startTag_.length - 2).split(' ', 1)[0]; |
| 178 this.startTagEnd_ = '</' + tagName + '>'; |
| 179 |
| 180 // Try parsing start together with the end |
| 181 var parsed = this.parseTag_(this.startTag_ + this.startTagEnd_); |
| 182 if (!parsed) { |
| 183 this.processError_('Failed to parse start tag: ' + this.startTag_); |
| 184 return; |
| 185 } |
| 186 } |
| 187 } |
| 188 |
| 189 var closingTag = |
| 190 (tag.charAt(1) == '/') || (tag.charAt(tag.length - 2) == '/'); |
| 191 if (closingTag) { |
| 192 // The first start tag is not expected to be closed. |
| 193 if (this.depth_ <= 1) { |
| 194 this.processError_('Unexpected closing tag: ' + tag) |
| 195 return; |
| 196 } |
| 197 --this.depth_; |
| 198 if (this.depth_ == 1) { |
| 199 this.processCompleteStanza_(); |
| 200 this.currentStanza_ = ''; |
| 201 } |
| 202 } |
| 203 } |
| 204 |
| 205 /** |
| 206 * @private |
| 207 */ |
| 208 remoting.XmppStreamParser.prototype.processCompleteStanza_ = function() { |
| 209 var stanza = this.startTag_ + this.currentStanza_ + this.startTagEnd_; |
| 210 var parsed = this.parseTag_(stanza); |
| 211 if (!parsed) { |
| 212 this.processError_('Failed to parse stanza: ' + this.currentStanza_); |
| 213 return; |
| 214 } |
| 215 this.onStanzaCallback_(parsed.firstElementChild); |
| 216 } |
| 217 |
| 218 /** |
| 219 * @param {string} text |
| 220 * @private |
| 221 */ |
| 222 remoting.XmppStreamParser.prototype.processError_ = function(text) { |
| 223 this.onErrorCallback_(text); |
| 224 this.error_ = true; |
| 225 } |
| 226 |
| 227 /** |
| 228 * Helper to extract and decode |bytes| bytes from |data_|. Returns NULL in case |
| 229 * the buffer contains invalidUTF-8. |
| 230 * |
| 231 * @param {number} bytes Specifies how many bytes should be extracted. |
| 232 * @returns {string?} |
| 233 * @private |
| 234 */ |
| 235 remoting.XmppStreamParser.prototype.extractStringFromBuffer_ = function(bytes) { |
| 236 var result = ''; |
| 237 try { |
| 238 result = base.decodeUtf8(this.data_.slice(0, bytes)); |
| 239 } catch (exception) { |
| 240 this.processError_('Received invalid UTF-8 data.'); |
| 241 result = null; |
| 242 } |
| 243 this.data_ = this.data_.slice(bytes); |
| 244 return result; |
| 245 } |
| 246 |
| 247 /** |
| 248 * @param {string} text |
| 249 * @return {Element} |
| 250 * @private |
| 251 */ |
| 252 remoting.XmppStreamParser.prototype.parseTag_ = function(text) { |
| 253 /** @type {Document} */ |
| 254 var result = new DOMParser().parseFromString(text, 'text/xml'); |
| 255 if (result.querySelector('parsererror') != null) |
| 256 return null; |
| 257 return result.firstElementChild; |
| 258 } |
OLD | NEW |