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 split data received over XMPP connection into |
| 12 * individual stanzas. Data is fed to the parser using appendData() method. |
| 13 * onStartCallback is called once for the stream header and after that |
| 14 * onStanzaCallback is called for each complete stanza. |
| 15 * |
| 16 * @param {function(Element):void} onStanzaCallback |
| 17 * @param {function(string):void} onErrorCallback |
| 18 * @constructor |
| 19 */ |
| 20 remoting.XmppStreamParser = function(onStanzaCallback, onErrorCallback) { |
| 21 this.onStanzaCallback_ = onStanzaCallback; |
| 22 this.onErrorCallback_ = onErrorCallback; |
| 23 |
| 24 |
| 25 /** @type {Array.<ArrayBuffer>} */ |
| 26 this.data_ = []; |
| 27 this.depth_ = 0; |
| 28 this.error_ = false; |
| 29 this.startTag_ = ''; |
| 30 this.startTagEnd_ = ''; |
| 31 this.currentStanza_ = ''; |
| 32 } |
| 33 |
| 34 /** @param {ArrayBuffer} data */ |
| 35 remoting.XmppStreamParser.prototype.appendData = function(data) { |
| 36 base.debug.assert(!this.error_); |
| 37 |
| 38 this.data_.push(data); |
| 39 |
| 40 // Check if the newly appended data completes XML tag or a piece of text by |
| 41 // looking for '<' and '>' char codes. This has to be done before converting |
| 42 // data to string because the input may not contain complete UTF-8 sequence. |
| 43 var tagStartCode = '<'.charCodeAt(0); |
| 44 var tagEndCode = '>'.charCodeAt(0); |
| 45 var spaceCode = ' '.charCodeAt(0); |
| 46 var tryAgain = true; |
| 47 while (this.data_.length > 0 && tryAgain && !this.error_) { |
| 48 tryAgain = false; |
| 49 |
| 50 // If we are not currently in a middle of a stanza then skip spaces (server |
| 51 // may send spaces periodically as heartbeats) and make sure that the first |
| 52 // character starts XML tag. |
| 53 if (this.depth_ <= 1) { |
| 54 var view = new DataView(this.data_[0]); |
| 55 var firstChar = view.getUint8(0); |
| 56 if (firstChar == spaceCode) { |
| 57 tryAgain = true; |
| 58 this.extractToken_(1); |
| 59 continue; |
| 60 } else if (firstChar != tagStartCode) { |
| 61 this.processError_('Received unexpected text data: ' + |
| 62 base.decodeUtf8(this.data_[0])); |
| 63 return; |
| 64 } |
| 65 } |
| 66 |
| 67 var view = new DataView(this.data_[this.data_.length - 1]); |
| 68 for (var i = 0; i < view.byteLength; ++i) { |
| 69 var currentChar = view.getUint8(i); |
| 70 if (currentChar == tagStartCode) { |
| 71 if (this.data_.length > 1 || i > 0) { |
| 72 this.processText_(this.extractToken_(i)); |
| 73 tryAgain = true; |
| 74 break; |
| 75 } |
| 76 } else if (currentChar == tagEndCode) { |
| 77 var tag = this.extractToken_(i + 1); |
| 78 if (tag.charAt(0) != '<') { |
| 79 this.processError_('Received \'>\' without \'<\': ' + tag); |
| 80 return; |
| 81 } |
| 82 this.processTag_(tag); |
| 83 tryAgain = true; |
| 84 break; |
| 85 } |
| 86 } |
| 87 } |
| 88 } |
| 89 |
| 90 /** @param {string} text */ |
| 91 remoting.XmppStreamParser.prototype.processText_ = function(text) { |
| 92 // Tokenization code in appendData() shouldn't allow text tokens in between |
| 93 // stanzas. |
| 94 base.debug.assert(this.depth_ > 1); |
| 95 this.currentStanza_ += text; |
| 96 } |
| 97 |
| 98 /** @param {string} tag */ |
| 99 remoting.XmppStreamParser.prototype.processTag_ = function(tag) { |
| 100 base.debug.assert(tag.charAt(0) == '<'); |
| 101 base.debug.assert(tag.charAt(tag.length - 1) == '>'); |
| 102 |
| 103 this.currentStanza_ += tag; |
| 104 |
| 105 var openTag = tag.charAt(1) != '/'; |
| 106 if (openTag) { |
| 107 ++this.depth_; |
| 108 if (this.depth_ == 1) { |
| 109 this.startTag_ = this.currentStanza_; |
| 110 this.currentStanza_ = ''; |
| 111 |
| 112 // Create end tag matching the start. |
| 113 var tagName = |
| 114 this.startTag_.substr(1, this.startTag_.length - 2).split(' ', 1)[0]; |
| 115 this.startTagEnd_ = '</' + tagName + '>'; |
| 116 |
| 117 // Try parsing start together with the end |
| 118 var parsed = this.parseTag_(this.startTag_ + this.startTagEnd_); |
| 119 if (!parsed) { |
| 120 this.processError_('Failed to parse start tag: ' + this.startTag_); |
| 121 return; |
| 122 } |
| 123 } |
| 124 } |
| 125 |
| 126 var closingTag = |
| 127 (tag.charAt(1) == '/') || (tag.charAt(tag.length - 2) == '/'); |
| 128 if (closingTag) { |
| 129 // The first start tag is not expected to be closed. |
| 130 if (this.depth_ <= 1) { |
| 131 this.processError_('Unexpected closing tag: ' + tag) |
| 132 return; |
| 133 } |
| 134 --this.depth_; |
| 135 if (this.depth_ == 1) { |
| 136 this.processCompleteStanza_(); |
| 137 this.currentStanza_ = ''; |
| 138 } |
| 139 } |
| 140 } |
| 141 |
| 142 remoting.XmppStreamParser.prototype.processCompleteStanza_ = function() { |
| 143 var stanza = this.startTag_ + this.currentStanza_ + this.startTagEnd_; |
| 144 var parsed = this.parseTag_(stanza); |
| 145 if (!parsed) { |
| 146 this.processError_('Failed to parse stanza: ' + this.currentStanza_); |
| 147 return; |
| 148 } |
| 149 this.onStanzaCallback_(parsed.childNodes[0]); |
| 150 } |
| 151 |
| 152 /** @param {string} text */ |
| 153 remoting.XmppStreamParser.prototype.processError_ = function(text) { |
| 154 this.onErrorCallback_(text); |
| 155 this.error_ = true; |
| 156 } |
| 157 |
| 158 /** |
| 159 * Helper that assembles block of data from pieces in this.data_ up to specified |
| 160 * position in the last buffer and then removes that data from this.data_ . |
| 161 * |
| 162 * @param {number} endPos Specifies how many bytes should be taken from the last |
| 163 * buffer in this.data_ . |
| 164 * @returns {string} |
| 165 */ |
| 166 remoting.XmppStreamParser.prototype.extractToken_ = function(endPos) { |
| 167 var size = endPos; |
| 168 for (var i = 0; i < this.data_.length - 1; ++i) { |
| 169 size += this.data_[i].byteLength; |
| 170 } |
| 171 |
| 172 var buffer = new Uint8Array(size); |
| 173 var pos = 0; |
| 174 for (var i = 0; i < this.data_.length - 1; ++i) { |
| 175 buffer.set(new Uint8Array(this.data_[i]), pos); |
| 176 pos += this.data_[i].byteLength; |
| 177 } |
| 178 if (endPos > 0) { |
| 179 buffer.set(new Uint8Array(this.data_[this.data_.length - 1], 0, endPos), |
| 180 pos); |
| 181 pos += endPos; |
| 182 } |
| 183 base.debug.assert(pos == size); |
| 184 |
| 185 // Remove copied data from |data_|. |
| 186 var dataLeft = this.data_[this.data_.length - 1].slice(endPos); |
| 187 if (dataLeft.byteLength == 0) { |
| 188 this.data_ = [] |
| 189 } else { |
| 190 this.data_ = [dataLeft]; |
| 191 } |
| 192 |
| 193 return base.decodeUtf8(buffer.buffer); |
| 194 } |
| 195 |
| 196 /** |
| 197 * @param {string} text |
| 198 * @return {Element} |
| 199 */ |
| 200 remoting.XmppStreamParser.prototype.parseTag_ = function(text) { |
| 201 /** @type {Document} */ |
| 202 var result = new DOMParser().parseFromString(text, 'text/xml'); |
| 203 if (result.querySelector('parsererror') != null) |
| 204 return null; |
| 205 return result.firstChild; |
| 206 } |
OLD | NEW |