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 | |
Jamie
2014/08/29 02:14:09
There is no onStartCallback.
Sergey Ulanov
2014/08/29 23:40:31
Done.
| |
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>} */ | |
Jamie
2014/08/29 02:14:09
Please add descriptions of these members.
Sergey Ulanov
2014/08/29 23:40:31
Done.
| |
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) { | |
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.
| |
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); | |
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
| |
59 continue; | |
60 } else if (firstChar != tagStartCode) { | |
61 this.processError_('Received unexpected text data: ' + | |
62 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
| |
63 return; | |
64 } | |
65 } | |
66 | |
67 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.
| |
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]); | |
kelvinp
2014/08/29 01:37:10
childNodes also contains all nodes, including text
Sergey Ulanov
2014/08/29 23:40:31
Done.
| |
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) { | |
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_.
| |
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); | |
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
| |
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 |