Index: remoting/webapp/xmpp_stream_parser.js |
diff --git a/remoting/webapp/xmpp_stream_parser.js b/remoting/webapp/xmpp_stream_parser.js |
new file mode 100644 |
index 0000000000000000000000000000000000000000..9a9881f2fcb863ce14993dacbf519a11c67a0b6b |
--- /dev/null |
+++ b/remoting/webapp/xmpp_stream_parser.js |
@@ -0,0 +1,206 @@ |
+// Copyright 2014 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+'use strict'; |
+ |
+/** @suppress {duplicate} */ |
+var remoting = remoting || {}; |
+ |
+/** |
+ * XmppStreamParser is used to split data received over XMPP connection into |
+ * individual stanzas. Data is fed to the parser using appendData() method. |
+ * onStartCallback is called once for the stream header and after that |
Jamie
2014/08/29 02:14:09
There is no onStartCallback.
Sergey Ulanov
2014/08/29 23:40:31
Done.
|
+ * onStanzaCallback is called for each complete stanza. |
+ * |
+ * @param {function(Element):void} onStanzaCallback |
+ * @param {function(string):void} onErrorCallback |
+ * @constructor |
+ */ |
+remoting.XmppStreamParser = function(onStanzaCallback, onErrorCallback) { |
+ this.onStanzaCallback_ = onStanzaCallback; |
+ this.onErrorCallback_ = onErrorCallback; |
+ |
+ |
+ /** @type {Array.<ArrayBuffer>} */ |
Jamie
2014/08/29 02:14:09
Please add descriptions of these members.
Sergey Ulanov
2014/08/29 23:40:31
Done.
|
+ this.data_ = []; |
+ this.depth_ = 0; |
+ this.error_ = false; |
+ this.startTag_ = ''; |
+ this.startTagEnd_ = ''; |
+ this.currentStanza_ = ''; |
+} |
+ |
+/** @param {ArrayBuffer} data */ |
+remoting.XmppStreamParser.prototype.appendData = function(data) { |
+ base.debug.assert(!this.error_); |
+ |
+ this.data_.push(data); |
+ |
+ // Check if the newly appended data completes XML tag or a piece of text by |
+ // looking for '<' and '>' char codes. This has to be done before converting |
+ // data to string because the input may not contain complete UTF-8 sequence. |
+ var tagStartCode = '<'.charCodeAt(0); |
+ var tagEndCode = '>'.charCodeAt(0); |
+ var spaceCode = ' '.charCodeAt(0); |
+ var tryAgain = true; |
+ while (this.data_.length > 0 && tryAgain && !this.error_) { |
+ tryAgain = false; |
+ |
+ // If we are not currently in a middle of a stanza then skip spaces (server |
+ // may send spaces periodically as heartbeats) and make sure that the first |
+ // character starts XML tag. |
+ if (this.depth_ <= 1) { |
kelvinp
2014/08/29 01:37:10
base.debug.assert(this.depth_ > 0);
Sergey Ulanov
2014/08/29 23:40:30
depth_ can be 0 here.
|
+ var view = new DataView(this.data_[0]); |
+ var firstChar = view.getUint8(0); |
+ if (firstChar == spaceCode) { |
+ tryAgain = true; |
+ this.extractToken_(1); |
Jamie
2014/08/29 02:14:09
This doesn't look right. It seems to be testing wh
Sergey Ulanov
2014/08/29 23:40:31
That's for catching this. I've simplified this cod
|
+ continue; |
+ } else if (firstChar != tagStartCode) { |
+ this.processError_('Received unexpected text data: ' + |
+ base.decodeUtf8(this.data_[0])); |
Jamie
2014/08/29 02:14:10
Your earlier comment stated that this might be inc
Sergey Ulanov
2014/08/29 23:40:31
Good point. decodeUtf8() throws in that case. I ch
|
+ return; |
+ } |
+ } |
+ |
+ var view = new DataView(this.data_[this.data_.length - 1]); |
Jamie
2014/08/29 02:14:10
It's not obvious what this second loop is doing. I
Sergey Ulanov
2014/08/29 23:40:31
Done.
|
+ for (var i = 0; i < view.byteLength; ++i) { |
+ var currentChar = view.getUint8(i); |
+ if (currentChar == tagStartCode) { |
+ if (this.data_.length > 1 || i > 0) { |
+ this.processText_(this.extractToken_(i)); |
+ tryAgain = true; |
+ break; |
+ } |
+ } else if (currentChar == tagEndCode) { |
+ var tag = this.extractToken_(i + 1); |
+ if (tag.charAt(0) != '<') { |
+ this.processError_('Received \'>\' without \'<\': ' + tag); |
+ return; |
+ } |
+ this.processTag_(tag); |
+ tryAgain = true; |
+ break; |
+ } |
+ } |
+ } |
+} |
+ |
+/** @param {string} text */ |
+remoting.XmppStreamParser.prototype.processText_ = function(text) { |
+ // Tokenization code in appendData() shouldn't allow text tokens in between |
+ // stanzas. |
+ base.debug.assert(this.depth_ > 1); |
+ this.currentStanza_ += text; |
+} |
+ |
+/** @param {string} tag */ |
+remoting.XmppStreamParser.prototype.processTag_ = function(tag) { |
+ base.debug.assert(tag.charAt(0) == '<'); |
+ base.debug.assert(tag.charAt(tag.length - 1) == '>'); |
+ |
+ this.currentStanza_ += tag; |
+ |
+ var openTag = tag.charAt(1) != '/'; |
+ if (openTag) { |
+ ++this.depth_; |
+ if (this.depth_ == 1) { |
+ this.startTag_ = this.currentStanza_; |
+ this.currentStanza_ = ''; |
+ |
+ // Create end tag matching the start. |
+ var tagName = |
+ this.startTag_.substr(1, this.startTag_.length - 2).split(' ', 1)[0]; |
+ this.startTagEnd_ = '</' + tagName + '>'; |
+ |
+ // Try parsing start together with the end |
+ var parsed = this.parseTag_(this.startTag_ + this.startTagEnd_); |
+ if (!parsed) { |
+ this.processError_('Failed to parse start tag: ' + this.startTag_); |
+ return; |
+ } |
+ } |
+ } |
+ |
+ var closingTag = |
+ (tag.charAt(1) == '/') || (tag.charAt(tag.length - 2) == '/'); |
+ if (closingTag) { |
+ // The first start tag is not expected to be closed. |
+ if (this.depth_ <= 1) { |
+ this.processError_('Unexpected closing tag: ' + tag) |
+ return; |
+ } |
+ --this.depth_; |
+ if (this.depth_ == 1) { |
+ this.processCompleteStanza_(); |
+ this.currentStanza_ = ''; |
+ } |
+ } |
+} |
+ |
+remoting.XmppStreamParser.prototype.processCompleteStanza_ = function() { |
+ var stanza = this.startTag_ + this.currentStanza_ + this.startTagEnd_; |
+ var parsed = this.parseTag_(stanza); |
+ if (!parsed) { |
+ this.processError_('Failed to parse stanza: ' + this.currentStanza_); |
+ return; |
+ } |
+ this.onStanzaCallback_(parsed.childNodes[0]); |
kelvinp
2014/08/29 01:37:10
childNodes also contains all nodes, including text
Sergey Ulanov
2014/08/29 23:40:31
Done.
|
+} |
+ |
+/** @param {string} text */ |
+remoting.XmppStreamParser.prototype.processError_ = function(text) { |
+ this.onErrorCallback_(text); |
+ this.error_ = true; |
+} |
+ |
+/** |
+ * Helper that assembles block of data from pieces in this.data_ up to specified |
+ * position in the last buffer and then removes that data from this.data_ . |
+ * |
+ * @param {number} endPos Specifies how many bytes should be taken from the last |
+ * buffer in this.data_ . |
+ * @returns {string} |
+ */ |
+remoting.XmppStreamParser.prototype.extractToken_ = function(endPos) { |
Jamie
2014/08/29 02:14:09
What is a "token" in this context? This seems to b
Sergey Ulanov
2014/08/29 23:40:31
Renamed extractStringFromBuffer_.
|
+ var size = endPos; |
+ for (var i = 0; i < this.data_.length - 1; ++i) { |
+ size += this.data_[i].byteLength; |
+ } |
+ |
+ var buffer = new Uint8Array(size); |
+ var pos = 0; |
+ for (var i = 0; i < this.data_.length - 1; ++i) { |
+ buffer.set(new Uint8Array(this.data_[i]), pos); |
+ pos += this.data_[i].byteLength; |
+ } |
+ if (endPos > 0) { |
+ buffer.set(new Uint8Array(this.data_[this.data_.length - 1], 0, endPos), |
+ pos); |
+ pos += endPos; |
+ } |
+ base.debug.assert(pos == size); |
+ |
+ // Remove copied data from |data_|. |
+ var dataLeft = this.data_[this.data_.length - 1].slice(endPos); |
Jamie
2014/08/29 02:14:10
I suggest calling this dataRemaining, since it's a
Sergey Ulanov
2014/08/29 23:40:31
Done, though some people would say that arrays sta
|
+ if (dataLeft.byteLength == 0) { |
+ this.data_ = [] |
+ } else { |
+ this.data_ = [dataLeft]; |
+ } |
+ |
+ return base.decodeUtf8(buffer.buffer); |
+} |
+ |
+/** |
+ * @param {string} text |
+ * @return {Element} |
+ */ |
+remoting.XmppStreamParser.prototype.parseTag_ = function(text) { |
+ /** @type {Document} */ |
+ var result = new DOMParser().parseFromString(text, 'text/xml'); |
+ if (result.querySelector('parsererror') != null) |
+ return null; |
+ return result.firstChild; |
+} |