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.
7 /** @suppress {duplicate} */
8 var remoting
= remoting
|| {};
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().
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
) {};
24 * Buffer containing the data that has been received but haven't been parsed.
27 this.data_
= new ArrayBuffer(0);
30 * Current depth in the XML stream.
36 * Set to true after error.
42 * The <stream> opening tag received at the beginning of the stream.
48 * Closing tag matching |startTag_|.
51 this.startTagEnd_
= '';
54 * String containing current incomplete stanza.
57 this.currentStanza_
= '';
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
;
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);
93 while (this.data_
.byteLength
> 0 && tryAgain
&& !this.error_
) {
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
) {
104 this.data_
= this.data_
.slice(1);
106 } else if (firstChar
!= tagStartCode
) {
109 dataAsText
= base
.decodeUtf8(this.data_
);
110 } catch (exception
) {
111 dataAsText
= 'charCode = ' + firstChar
;
113 this.processError_('Received unexpected text data: ' + dataAsText
);
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
) {
124 var text
= this.extractStringFromBuffer_(i
);
127 this.processText_(text
);
131 } else if (currentChar
== tagEndCode
) {
132 var tag
= this.extractStringFromBuffer_(i
+ 1);
135 if (tag
.charAt(0) != '<') {
136 this.processError_('Received \'>\' without \'<\': ' + tag
);
139 this.processTag_(tag
);
148 * @param {string} text
151 remoting
.XmppStreamParser
.prototype.processText_ = function(text
) {
152 // Tokenization code in appendData() shouldn't allow text tokens in between
154 console
.assert(this.depth_
> 1, 'Bad depth: ' + this.depth_
+ '.');
155 this.currentStanza_
+= text
;
159 * @param {string} tag
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) != '/';
171 if (this.depth_
== 1) {
172 this.startTag_
= this.currentStanza_
;
173 this.currentStanza_
= '';
175 // Create end tag matching the start.
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_
);
183 this.processError_('Failed to parse start tag: ' + this.startTag_
);
190 (tag
.charAt(1) == '/') || (tag
.charAt(tag
.length
- 2) == '/');
192 // The first start tag is not expected to be closed.
193 if (this.depth_
<= 1) {
194 this.processError_('Unexpected closing tag: ' + tag
)
198 if (this.depth_
== 1) {
199 this.processCompleteStanza_();
200 this.currentStanza_
= '';
208 remoting
.XmppStreamParser
.prototype.processCompleteStanza_ = function() {
209 var stanza
= this.startTag_
+ this.currentStanza_
+ this.startTagEnd_
;
210 var parsed
= this.parseTag_(stanza
);
212 this.processError_('Failed to parse stanza: ' + this.currentStanza_
);
215 this.onStanzaCallback_(parsed
.firstElementChild
);
219 * @param {string} text
222 remoting
.XmppStreamParser
.prototype.processError_ = function(text
) {
223 this.onErrorCallback_(text
);
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.
235 remoting
.XmppStreamParser
.prototype.extractStringFromBuffer_ = function(bytes
) {
238 result
= base
.decodeUtf8(this.data_
.slice(0, bytes
));
239 } catch (exception
) {
240 this.processError_('Received invalid UTF-8 data.');
243 this.data_
= this.data_
.slice(bytes
);
248 * @param {string} text
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)
257 return result
.firstElementChild
;