Chromium Code Reviews| 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; |
| +} |