Roll src/third_party/WebKit d9c6159:8139f33 (svn 201974:201975)
[chromium-blink-merge.git] / remoting / webapp / base / js / xmpp_stream_parser.js
blob81ad5bf7f112aa506d3422110101eda6abd8adbe
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.
5 'use strict';
7 /** @suppress {duplicate} */
8 var remoting = remoting || {};
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().
15 * @constructor
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) {};
23 /**
24 * Buffer containing the data that has been received but haven't been parsed.
25 * @private
27 this.data_ = new ArrayBuffer(0);
29 /**
30 * Current depth in the XML stream.
31 * @private
33 this.depth_ = 0;
35 /**
36 * Set to true after error.
37 * @private
39 this.error_ = false;
41 /**
42 * The <stream> opening tag received at the beginning of the stream.
43 * @private
45 this.startTag_ = '';
47 /**
48 * Closing tag matching |startTag_|.
49 * @private
51 this.startTagEnd_ = '';
53 /**
54 * String containing current incomplete stanza.
55 * @private
57 this.currentStanza_ = '';
60 /**
61 * Sets callbacks to be called on incoming stanzas and on error.
63 * @param {function(Element):void} onStanzaCallback
64 * @param {function(string):void} onErrorCallback
66 remoting.XmppStreamParser.prototype.setCallbacks =
67 function(onStanzaCallback, onErrorCallback) {
68 this.onStanzaCallback_ = onStanzaCallback;
69 this.onErrorCallback_ = onErrorCallback;
72 /** @param {ArrayBuffer} data */
73 remoting.XmppStreamParser.prototype.appendData = function(data) {
74 console.assert(!this.error_, 'appendData() called in error state.');
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;
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;
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;
113 this.processError_('Received unexpected text data: ' + dataAsText);
114 return;
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;
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;
139 this.processTag_(tag);
140 tryAgain = true;
141 break;
148 * @param {string} text
149 * @private
151 remoting.XmppStreamParser.prototype.processText_ = function(text) {
152 // Tokenization code in appendData() shouldn't allow text tokens in between
153 // stanzas.
154 console.assert(this.depth_ > 1, 'Bad depth: ' + this.depth_ + '.');
155 this.currentStanza_ += text;
159 * @param {string} tag
160 * @private
162 remoting.XmppStreamParser.prototype.processTag_ = function(tag) {
163 console.assert(tag.charAt(0) == '<' && tag.charAt(tag.length - 1) == '>',
164 'Malformed tag: ' + tag);
166 this.currentStanza_ += tag;
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_ = '';
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 + '>';
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;
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;
197 --this.depth_;
198 if (this.depth_ == 1) {
199 this.processCompleteStanza_();
200 this.currentStanza_ = '';
206 * @private
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;
215 this.onStanzaCallback_(parsed.firstElementChild);
219 * @param {string} text
220 * @private
222 remoting.XmppStreamParser.prototype.processError_ = function(text) {
223 this.onErrorCallback_(text);
224 this.error_ = true;
228 * Helper to extract and decode |bytes| bytes from |data_|. Returns NULL in case
229 * the buffer contains invalidUTF-8.
231 * @param {number} bytes Specifies how many bytes should be extracted.
232 * @returns {string?}
233 * @private
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;
243 this.data_ = this.data_.slice(bytes);
244 return result;
248 * @param {string} text
249 * @return {Element}
250 * @private
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;