5 var keys = require('./keys');
6 var hasBinary = require('has-binary2');
7 var sliceBuffer = require('arraybuffer.slice');
8 var after = require('after');
9 var utf8 = require('./utf8');
12 if (typeof ArrayBuffer !== 'undefined') {
13 base64encoder = require('base64-arraybuffer');
17 * Check if we are running an android browser. That requires us to use
18 * ArrayBuffer with polling transports...
20 * http://ghinda.net/jpeg-blob-ajax-android/
23 var isAndroid = typeof navigator !== 'undefined' && /Android/i.test(navigator.userAgent);
26 * Check if we are running in PhantomJS.
27 * Uploading a Blob with PhantomJS does not work correctly, as reported here:
28 * https://github.com/ariya/phantomjs/issues/11395
31 var isPhantomJS = typeof navigator !== 'undefined' && /PhantomJS/i.test(navigator.userAgent);
34 * When true, avoids using Blobs to encode payloads.
37 var dontSendBlobs = isAndroid || isPhantomJS;
40 * Current protocol version.
49 var packets = exports.packets = {
59 var packetslist = keys(packets);
62 * Premade error packet.
65 var err = { type: 'error', data: 'parser error' };
68 * Create a blob api even for blob builder when vendor prefixes exist
71 var Blob = require('blob');
76 * <packet type id> [ <data> ]
84 * Binary is encoded in an identical principle
89 exports.encodePacket = function (packet, supportsBinary, utf8encode, callback) {
90 if (typeof supportsBinary === 'function') {
91 callback = supportsBinary;
92 supportsBinary = false;
95 if (typeof utf8encode === 'function') {
96 callback = utf8encode;
100 var data = (packet.data === undefined)
102 : packet.data.buffer || packet.data;
104 if (typeof ArrayBuffer !== 'undefined' && data instanceof ArrayBuffer) {
105 return encodeArrayBuffer(packet, supportsBinary, callback);
106 } else if (typeof Blob !== 'undefined' && data instanceof Blob) {
107 return encodeBlob(packet, supportsBinary, callback);
110 // might be an object with { base64: true, data: dataAsBase64String }
111 if (data && data.base64) {
112 return encodeBase64Object(packet, callback);
115 // Sending data as a utf-8 string
116 var encoded = packets[packet.type];
118 // data fragment is optional
119 if (undefined !== packet.data) {
120 encoded += utf8encode ? utf8.encode(String(packet.data), { strict: false }) : String(packet.data);
123 return callback('' + encoded);
127 function encodeBase64Object(packet, callback) {
128 // packet data is an object { base64: true, data: dataAsBase64String }
129 var message = 'b' + exports.packets[packet.type] + packet.data.data;
130 return callback(message);
134 * Encode packet helpers for binary types
137 function encodeArrayBuffer(packet, supportsBinary, callback) {
138 if (!supportsBinary) {
139 return exports.encodeBase64Packet(packet, callback);
142 var data = packet.data;
143 var contentArray = new Uint8Array(data);
144 var resultBuffer = new Uint8Array(1 + data.byteLength);
146 resultBuffer[0] = packets[packet.type];
147 for (var i = 0; i < contentArray.length; i++) {
148 resultBuffer[i+1] = contentArray[i];
151 return callback(resultBuffer.buffer);
154 function encodeBlobAsArrayBuffer(packet, supportsBinary, callback) {
155 if (!supportsBinary) {
156 return exports.encodeBase64Packet(packet, callback);
159 var fr = new FileReader();
160 fr.onload = function() {
161 exports.encodePacket({ type: packet.type, data: fr.result }, supportsBinary, true, callback);
163 return fr.readAsArrayBuffer(packet.data);
166 function encodeBlob(packet, supportsBinary, callback) {
167 if (!supportsBinary) {
168 return exports.encodeBase64Packet(packet, callback);
172 return encodeBlobAsArrayBuffer(packet, supportsBinary, callback);
175 var length = new Uint8Array(1);
176 length[0] = packets[packet.type];
177 var blob = new Blob([length.buffer, packet.data]);
179 return callback(blob);
183 * Encodes a packet with binary data in a base64 string
185 * @param {Object} packet, has `type` and `data`
186 * @return {String} base64 encoded message
189 exports.encodeBase64Packet = function(packet, callback) {
190 var message = 'b' + exports.packets[packet.type];
191 if (typeof Blob !== 'undefined' && packet.data instanceof Blob) {
192 var fr = new FileReader();
193 fr.onload = function() {
194 var b64 = fr.result.split(',')[1];
195 callback(message + b64);
197 return fr.readAsDataURL(packet.data);
202 b64data = String.fromCharCode.apply(null, new Uint8Array(packet.data));
204 // iPhone Safari doesn't let you apply with typed arrays
205 var typed = new Uint8Array(packet.data);
206 var basic = new Array(typed.length);
207 for (var i = 0; i < typed.length; i++) {
210 b64data = String.fromCharCode.apply(null, basic);
212 message += btoa(b64data);
213 return callback(message);
217 * Decodes a packet. Changes format to Blob if requested.
219 * @return {Object} with `type` and `data` (if any)
223 exports.decodePacket = function (data, binaryType, utf8decode) {
224 if (data === undefined) {
228 if (typeof data === 'string') {
229 if (data.charAt(0) === 'b') {
230 return exports.decodeBase64Packet(data.substr(1), binaryType);
234 data = tryDecode(data);
235 if (data === false) {
239 var type = data.charAt(0);
241 if (Number(type) != type || !packetslist[type]) {
245 if (data.length > 1) {
246 return { type: packetslist[type], data: data.substring(1) };
248 return { type: packetslist[type] };
252 var asArray = new Uint8Array(data);
253 var type = asArray[0];
254 var rest = sliceBuffer(data, 1);
255 if (Blob && binaryType === 'blob') {
256 rest = new Blob([rest]);
258 return { type: packetslist[type], data: rest };
261 function tryDecode(data) {
263 data = utf8.decode(data, { strict: false });
271 * Decodes a packet encoded in a base64 string
273 * @param {String} base64 encoded message
274 * @return {Object} with `type` and `data` (if any)
277 exports.decodeBase64Packet = function(msg, binaryType) {
278 var type = packetslist[msg.charAt(0)];
279 if (!base64encoder) {
280 return { type: type, data: { base64: true, data: msg.substr(1) } };
283 var data = base64encoder.decode(msg.substr(1));
285 if (binaryType === 'blob' && Blob) {
286 data = new Blob([data]);
289 return { type: type, data: data };
293 * Encodes multiple messages (payload).
301 * If any contents are binary, they will be encoded as base64 strings. Base64
302 * encoded strings are marked with a b before the length specifier
304 * @param {Array} packets
308 exports.encodePayload = function (packets, supportsBinary, callback) {
309 if (typeof supportsBinary === 'function') {
310 callback = supportsBinary;
311 supportsBinary = null;
314 var isBinary = hasBinary(packets);
316 if (supportsBinary && isBinary) {
317 if (Blob && !dontSendBlobs) {
318 return exports.encodePayloadAsBlob(packets, callback);
321 return exports.encodePayloadAsArrayBuffer(packets, callback);
324 if (!packets.length) {
325 return callback('0:');
328 function setLengthHeader(message) {
329 return message.length + ':' + message;
332 function encodeOne(packet, doneCallback) {
333 exports.encodePacket(packet, !isBinary ? false : supportsBinary, false, function(message) {
334 doneCallback(null, setLengthHeader(message));
338 map(packets, encodeOne, function(err, results) {
339 return callback(results.join(''));
344 * Async array map using after
347 function map(ary, each, done) {
348 var result = new Array(ary.length);
349 var next = after(ary.length, done);
351 var eachWithIndex = function(i, el, cb) {
352 each(el, function(error, msg) {
358 for (var i = 0; i < ary.length; i++) {
359 eachWithIndex(i, ary[i], next);
364 * Decodes data when a payload is maybe expected. Possible binary contents are
365 * decoded from their base64 representation
367 * @param {String} data, callback method
371 exports.decodePayload = function (data, binaryType, callback) {
372 if (typeof data !== 'string') {
373 return exports.decodePayloadAsBinary(data, binaryType, callback);
376 if (typeof binaryType === 'function') {
377 callback = binaryType;
383 // parser error - ignoring payload
384 return callback(err, 0, 1);
387 var length = '', n, msg;
389 for (var i = 0, l = data.length; i < l; i++) {
390 var chr = data.charAt(i);
397 if (length === '' || (length != (n = Number(length)))) {
398 // parser error - ignoring payload
399 return callback(err, 0, 1);
402 msg = data.substr(i + 1, n);
404 if (length != msg.length) {
405 // parser error - ignoring payload
406 return callback(err, 0, 1);
410 packet = exports.decodePacket(msg, binaryType, false);
412 if (err.type === packet.type && err.data === packet.data) {
413 // parser error in individual packet - ignoring payload
414 return callback(err, 0, 1);
417 var ret = callback(packet, i + n, l);
418 if (false === ret) return;
427 // parser error - ignoring payload
428 return callback(err, 0, 1);
434 * Encodes multiple messages (payload) as binary.
436 * <1 = binary, 0 = string><number from 0-9><number from 0-9>[...]<number
440 * 1 3 255 1 2 3, if the binary contents are interpreted as 8 bit integers
442 * @param {Array} packets
443 * @return {ArrayBuffer} encoded payload
447 exports.encodePayloadAsArrayBuffer = function(packets, callback) {
448 if (!packets.length) {
449 return callback(new ArrayBuffer(0));
452 function encodeOne(packet, doneCallback) {
453 exports.encodePacket(packet, true, true, function(data) {
454 return doneCallback(null, data);
458 map(packets, encodeOne, function(err, encodedPackets) {
459 var totalLength = encodedPackets.reduce(function(acc, p) {
461 if (typeof p === 'string'){
466 return acc + len.toString().length + len + 2; // string/binary identifier + separator = 2
469 var resultArray = new Uint8Array(totalLength);
472 encodedPackets.forEach(function(p) {
473 var isString = typeof p === 'string';
476 var view = new Uint8Array(p.length);
477 for (var i = 0; i < p.length; i++) {
478 view[i] = p.charCodeAt(i);
483 if (isString) { // not true binary
484 resultArray[bufferIndex++] = 0;
485 } else { // true binary
486 resultArray[bufferIndex++] = 1;
489 var lenStr = ab.byteLength.toString();
490 for (var i = 0; i < lenStr.length; i++) {
491 resultArray[bufferIndex++] = parseInt(lenStr[i]);
493 resultArray[bufferIndex++] = 255;
495 var view = new Uint8Array(ab);
496 for (var i = 0; i < view.length; i++) {
497 resultArray[bufferIndex++] = view[i];
501 return callback(resultArray.buffer);
509 exports.encodePayloadAsBlob = function(packets, callback) {
510 function encodeOne(packet, doneCallback) {
511 exports.encodePacket(packet, true, true, function(encoded) {
512 var binaryIdentifier = new Uint8Array(1);
513 binaryIdentifier[0] = 1;
514 if (typeof encoded === 'string') {
515 var view = new Uint8Array(encoded.length);
516 for (var i = 0; i < encoded.length; i++) {
517 view[i] = encoded.charCodeAt(i);
519 encoded = view.buffer;
520 binaryIdentifier[0] = 0;
523 var len = (encoded instanceof ArrayBuffer)
527 var lenStr = len.toString();
528 var lengthAry = new Uint8Array(lenStr.length + 1);
529 for (var i = 0; i < lenStr.length; i++) {
530 lengthAry[i] = parseInt(lenStr[i]);
532 lengthAry[lenStr.length] = 255;
535 var blob = new Blob([binaryIdentifier.buffer, lengthAry.buffer, encoded]);
536 doneCallback(null, blob);
541 map(packets, encodeOne, function(err, results) {
542 return callback(new Blob(results));
547 * Decodes data when a payload is maybe expected. Strings are decoded by
548 * interpreting each byte as a key code for entries marked to start with 0. See
549 * description of encodePayloadAsBinary
551 * @param {ArrayBuffer} data, callback method
555 exports.decodePayloadAsBinary = function (data, binaryType, callback) {
556 if (typeof binaryType === 'function') {
557 callback = binaryType;
561 var bufferTail = data;
564 while (bufferTail.byteLength > 0) {
565 var tailArray = new Uint8Array(bufferTail);
566 var isString = tailArray[0] === 0;
569 for (var i = 1; ; i++) {
570 if (tailArray[i] === 255) break;
572 // 310 = char length of Number.MAX_VALUE
573 if (msgLength.length > 310) {
574 return callback(err, 0, 1);
577 msgLength += tailArray[i];
580 bufferTail = sliceBuffer(bufferTail, 2 + msgLength.length);
581 msgLength = parseInt(msgLength);
583 var msg = sliceBuffer(bufferTail, 0, msgLength);
586 msg = String.fromCharCode.apply(null, new Uint8Array(msg));
588 // iPhone Safari doesn't let you apply to typed arrays
589 var typed = new Uint8Array(msg);
591 for (var i = 0; i < typed.length; i++) {
592 msg += String.fromCharCode(typed[i]);
598 bufferTail = sliceBuffer(bufferTail, msgLength);
601 var total = buffers.length;
602 buffers.forEach(function(buffer, i) {
603 callback(exports.decodePacket(buffer, binaryType, true), i, total);