ceec0fce2a
Supported SSL. Closes #71 Added setZeroTimeout. Now downloading files when tab in background and improved performance Multiple file parts download in parallel TLDeserialization.fetchBytes returns typed array
594 lines
16 KiB
JavaScript
594 lines
16 KiB
JavaScript
/*!
|
|
* Webogram v0.3.2 - messaging web application for MTProto
|
|
* https://github.com/zhukov/webogram
|
|
* Copyright (C) 2014 Igor Zhukov <igor.beatle@gmail.com>
|
|
* https://github.com/zhukov/webogram/blob/master/LICENSE
|
|
*/
|
|
|
|
function TLSerialization (options) {
|
|
options = options || {};
|
|
this.maxLength = options.startMaxLength || 2048; // 2Kb
|
|
this.offset = 0; // in bytes
|
|
|
|
this.createBuffer();
|
|
|
|
// this.debug = options.debug !== undefined ? options.debug : Config.Modes.debug;
|
|
this.mtproto = options.mtproto || false;
|
|
return this;
|
|
}
|
|
|
|
TLSerialization.prototype.createBuffer = function () {
|
|
this.buffer = new ArrayBuffer(this.maxLength);
|
|
this.intView = new Int32Array(this.buffer);
|
|
this.byteView = new Uint8Array(this.buffer);
|
|
};
|
|
|
|
TLSerialization.prototype.getArray = function () {
|
|
var resultBuffer = new ArrayBuffer(this.offset);
|
|
var resultArray = new Int32Array(resultBuffer);
|
|
|
|
resultArray.set(this.intView.subarray(0, this.offset / 4));
|
|
|
|
return resultArray;
|
|
};
|
|
|
|
TLSerialization.prototype.getBuffer = function () {
|
|
return this.getArray().buffer;
|
|
};
|
|
|
|
TLSerialization.prototype.getBytes = function (typed) {
|
|
if (typed) {
|
|
var resultBuffer = new ArrayBuffer(this.offset);
|
|
var resultArray = new Uint8Array(resultBuffer);
|
|
|
|
resultArray.set(this.byteView.subarray(0, this.offset));
|
|
|
|
return resultArray;
|
|
}
|
|
|
|
var bytes = [];
|
|
for (var i = 0; i < this.offset; i++) {
|
|
bytes.push(this.byteView[i]);
|
|
}
|
|
return bytes;
|
|
};
|
|
|
|
TLSerialization.prototype.checkLength = function (needBytes) {
|
|
if (this.offset + needBytes < this.maxLength) {
|
|
return;
|
|
}
|
|
|
|
console.trace('Increase buffer', this.offset, needBytes, this.maxLength);
|
|
this.maxLength = Math.ceil(Math.max(this.maxLength * 2, this.offset + needBytes + 16) / 4) * 4;
|
|
var previousBuffer = this.buffer,
|
|
previousArray = new Int32Array(previousBuffer);
|
|
|
|
this.createBuffer();
|
|
|
|
new Int32Array(this.buffer).set(previousArray);
|
|
};
|
|
|
|
TLSerialization.prototype.writeInt = function (i, field) {
|
|
this.debug && console.log('>>>', i.toString(16), i, field);
|
|
|
|
this.checkLength(4);
|
|
this.intView[this.offset / 4] = i;
|
|
this.offset += 4;
|
|
};
|
|
|
|
TLSerialization.prototype.storeInt = function (i, field) {
|
|
this.writeInt(i, (field || '') + ':int');
|
|
};
|
|
|
|
TLSerialization.prototype.storeBool = function (i, field) {
|
|
if (i) {
|
|
this.writeInt(0x997275b5, (field || '') + ':bool');
|
|
} else {
|
|
this.writeInt(0xbc799737, (field || '') + ':bool');
|
|
}
|
|
};
|
|
|
|
TLSerialization.prototype.storeLongP = function (iHigh, iLow, field) {
|
|
this.writeInt(iLow, (field || '') + ':long[low]');
|
|
this.writeInt(iHigh, (field || '') + ':long[high]');
|
|
};
|
|
|
|
TLSerialization.prototype.storeLong = function (sLong, field) {
|
|
if (angular.isArray(sLong)) {
|
|
if (sLong.length == 2) {
|
|
return this.storeLongP(sLong[0], sLong[1], field);
|
|
} else {
|
|
return this.storeIntBytes(sLong, 64, field);
|
|
}
|
|
}
|
|
|
|
var divRem = bigStringInt(sLong).divideAndRemainder(bigint(0x100000000));
|
|
|
|
this.writeInt(intToUint(divRem[1].intValue()), (field || '') + ':long[low]');
|
|
this.writeInt(intToUint(divRem[0].intValue()), (field || '') + ':long[high]');
|
|
};
|
|
|
|
TLSerialization.prototype.storeDouble = function (f) {
|
|
var buffer = new ArrayBuffer(8);
|
|
var intView = new Int32Array(buffer);
|
|
var doubleView = new Float64Array(buffer);
|
|
|
|
doubleView[0] = f;
|
|
|
|
this.writeInt(intView[0], (field || '') + ':double[low]');
|
|
this.writeInt(intView[1], (field || '') + ':double[high]');
|
|
};
|
|
|
|
TLSerialization.prototype.storeString = function (s, field) {
|
|
this.debug && console.log('>>>', s, (field || '') + ':string');
|
|
|
|
var sUTF8 = unescape(encodeURIComponent(s));
|
|
|
|
this.checkLength(sUTF8.length + 8);
|
|
|
|
|
|
var len = sUTF8.length;
|
|
if (len <= 253) {
|
|
this.byteView[this.offset++] = len;
|
|
} else {
|
|
this.byteView[this.offset++] = 254;
|
|
this.byteView[this.offset++] = len & 0xFF;
|
|
this.byteView[this.offset++] = (len & 0xFF00) >> 8;
|
|
this.byteView[this.offset++] = (len & 0xFF0000) >> 16;
|
|
}
|
|
for (var i = 0; i < len; i++) {
|
|
this.byteView[this.offset++] = sUTF8.charCodeAt(i);
|
|
}
|
|
|
|
// Padding
|
|
while (this.offset % 4) {
|
|
this.byteView[this.offset++] = 0;
|
|
}
|
|
}
|
|
|
|
|
|
TLSerialization.prototype.storeBytes = function (bytes, field) {
|
|
this.debug && console.log('>>>', bytesToHex(bytes), (field || '') + ':bytes');
|
|
|
|
var len = bytes.byteLength || bytes.length;
|
|
this.checkLength(len + 8);
|
|
if (len <= 253) {
|
|
this.byteView[this.offset++] = len;
|
|
} else {
|
|
this.byteView[this.offset++] = 254;
|
|
this.byteView[this.offset++] = len & 0xFF;
|
|
this.byteView[this.offset++] = (len & 0xFF00) >> 8;
|
|
this.byteView[this.offset++] = (len & 0xFF0000) >> 16;
|
|
}
|
|
this.byteView.set(bytes, this.offset);
|
|
this.offset += len;
|
|
|
|
// Padding
|
|
while (this.offset % 4) {
|
|
this.byteView[this.offset++] = 0;
|
|
}
|
|
}
|
|
|
|
TLSerialization.prototype.storeIntBytes = function (bytes, bits, field) {
|
|
if (bytes instanceof ArrayBuffer) {
|
|
bytes = new Uint8Array(bytes);
|
|
}
|
|
var len = bytes.length;
|
|
if ((bits % 32) || (len * 8) != bits) {
|
|
throw new Error('Invalid bits: ' + bits + ', ' + bytes.length);
|
|
}
|
|
|
|
this.debug && console.log('>>>', bytesToHex(bytes), (field || '') + ':int' + bits);
|
|
this.checkLength(len);
|
|
|
|
this.byteView.set(bytes, this.offset);
|
|
this.offset += len;
|
|
};
|
|
|
|
TLSerialization.prototype.storeRawBytes = function (bytes, field) {
|
|
if (bytes instanceof ArrayBuffer) {
|
|
bytes = new Uint8Array(bytes);
|
|
}
|
|
var len = bytes.length;
|
|
|
|
this.debug && console.log('>>>', bytesToHex(bytes), (field || ''));
|
|
this.checkLength(len);
|
|
|
|
this.byteView.set(bytes, this.offset);
|
|
this.offset += len;
|
|
};
|
|
|
|
|
|
TLSerialization.prototype.storeMethod = function (methodName, params) {
|
|
var schema = this.mtproto ? Config.Schema.MTProto : Config.Schema.API,
|
|
methodData = false,
|
|
i;
|
|
|
|
for (i = 0; i < schema.methods.length; i++) {
|
|
if (schema.methods[i].method == methodName) {
|
|
methodData = schema.methods[i];
|
|
break
|
|
}
|
|
}
|
|
if (!methodData) {
|
|
throw new Error('No method ' + methodName + ' found');
|
|
}
|
|
|
|
this.storeInt(intToUint(methodData.id), methodName + '[id]');
|
|
|
|
var self = this;
|
|
angular.forEach(methodData.params, function (param) {
|
|
self.storeObject(params[param.name], param.type, methodName + '[' + param.name + ']');
|
|
});
|
|
|
|
return methodData.type;
|
|
};
|
|
|
|
TLSerialization.prototype.storeObject = function (obj, type, field) {
|
|
switch (type) {
|
|
case 'int': return this.storeInt(obj, field);
|
|
case 'long': return this.storeLong(obj, field);
|
|
case 'int128': return this.storeIntBytes(obj, 128, field);
|
|
case 'int256': return this.storeIntBytes(obj, 256, field);
|
|
case 'int512': return this.storeIntBytes(obj, 512, field);
|
|
case 'string': return this.storeString(obj, field);
|
|
case 'bytes': return this.storeBytes(obj, field);
|
|
case 'double': return this.storeDouble(obj, field);
|
|
case 'Bool': return this.storeBool(obj, field);
|
|
}
|
|
|
|
if (angular.isArray(obj)) {
|
|
if (type.substr(0, 6) == 'Vector') {
|
|
this.writeInt(0x1cb5c415, field + '[id]');
|
|
}
|
|
else if (type.substr(0, 6) != 'vector') {
|
|
throw new Error('Invalid vector type ' + type);
|
|
}
|
|
var itemType = type.substr(7, type.length - 8); // for "Vector<itemType>"
|
|
this.writeInt(obj.length, field + '[count]');
|
|
for (var i = 0; i < obj.length; i++) {
|
|
this.storeObject(obj[i], itemType, field + '[' + i + ']');
|
|
}
|
|
return true;
|
|
}
|
|
else if (type.substr(0, 6).toLowerCase() == 'vector') {
|
|
throw new Error('Invalid vector object');
|
|
}
|
|
|
|
if (!angular.isObject(obj)) {
|
|
throw new Error('Invalid object for type ' + type);
|
|
}
|
|
|
|
var schema = this.mtproto ? Config.Schema.MTProto : Config.Schema.API,
|
|
predicate = obj['_'],
|
|
isBare = false,
|
|
constructorData = false,
|
|
i;
|
|
|
|
if (isBare = (type.charAt(0) == '%')) {
|
|
type = type.substr(1);
|
|
}
|
|
|
|
for (i = 0; i < schema.constructors.length; i++) {
|
|
if (schema.constructors[i].predicate == predicate) {
|
|
constructorData = schema.constructors[i];
|
|
break
|
|
}
|
|
}
|
|
if (!constructorData) {
|
|
throw new Error('No predicate ' + predicate + ' found');
|
|
}
|
|
|
|
if (predicate == type) {
|
|
isBare = true;
|
|
}
|
|
|
|
if (!isBare) {
|
|
this.writeInt(intToUint(constructorData.id), field + '[' + predicate + '][id]');
|
|
}
|
|
|
|
var self = this;
|
|
angular.forEach(constructorData.params, function (param) {
|
|
self.storeObject(obj[param.name], param.type, field + '[' + predicate + '][' + param.name + ']');
|
|
});
|
|
|
|
return constructorData.type;
|
|
};
|
|
|
|
|
|
|
|
function TLDeserialization (buffer, options) {
|
|
options = options || {};
|
|
|
|
this.offset = 0; // in bytes
|
|
this.override = options.override || {};
|
|
|
|
this.buffer = buffer;
|
|
this.intView = new Uint32Array(this.buffer);
|
|
this.byteView = new Uint8Array(this.buffer);
|
|
|
|
// this.debug = options.debug !== undefined ? options.debug : Config.Modes.debug;
|
|
this.mtproto = options.mtproto || false;
|
|
return this;
|
|
}
|
|
|
|
TLDeserialization.prototype.readInt = function (field) {
|
|
if (this.offset >= this.intView.length * 4) {
|
|
throw new Error('Nothing to fetch: ' + field);
|
|
}
|
|
|
|
var i = this.intView[this.offset / 4];
|
|
|
|
this.debug && console.log('<<<', i.toString(16), i, field);
|
|
|
|
this.offset += 4;
|
|
|
|
return i;
|
|
};
|
|
|
|
TLDeserialization.prototype.fetchInt = function (field) {
|
|
return this.readInt((field || '') + ':int');
|
|
}
|
|
|
|
TLDeserialization.prototype.fetchDouble = function (field) {
|
|
var buffer = new ArrayBuffer(8);
|
|
var intView = new Int32Array(buffer);
|
|
var doubleView = new Float64Array(buffer);
|
|
|
|
intView[0] = this.readInt((field || '') + ':double[low]'),
|
|
intView[1] = this.readInt((field || '') + ':double[high]');
|
|
|
|
return doubleView[0];
|
|
};
|
|
|
|
TLDeserialization.prototype.fetchLong = function (field) {
|
|
var iLow = this.readInt((field || '') + ':long[low]'),
|
|
iHigh = this.readInt((field || '') + ':long[high]');
|
|
|
|
var longDec = bigint(iHigh).shiftLeft(32).add(bigint(iLow)).toString();
|
|
|
|
return longDec;
|
|
}
|
|
|
|
TLDeserialization.prototype.fetchBool = function (field) {
|
|
var i = this.readInt((field || '') + ':bool');
|
|
if (i == 0x997275b5) {
|
|
return true;
|
|
} else if (i == 0xbc799737) {
|
|
return false
|
|
}
|
|
|
|
this.offset -= 4;
|
|
return this.fetchObject('Object', field);
|
|
}
|
|
|
|
TLDeserialization.prototype.fetchString = function (field) {
|
|
var len = this.byteView[this.offset++];
|
|
|
|
if (len == 254) {
|
|
var len = this.byteView[this.offset++] |
|
|
(this.byteView[this.offset++] << 8) |
|
|
(this.byteView[this.offset++] << 16);
|
|
}
|
|
|
|
var sUTF8 = '';
|
|
for (var i = 0; i < len; i++) {
|
|
sUTF8 += String.fromCharCode(this.byteView[this.offset++]);
|
|
}
|
|
|
|
// Padding
|
|
while (this.offset % 4) {
|
|
this.offset++;
|
|
}
|
|
|
|
try {
|
|
var s = decodeURIComponent(escape(sUTF8));
|
|
} catch (e) {
|
|
var s = sUTF8;
|
|
}
|
|
|
|
this.debug && console.log('<<<', s, (field || '') + ':string');
|
|
|
|
return s;
|
|
}
|
|
|
|
|
|
TLDeserialization.prototype.fetchBytes = function (field) {
|
|
var len = this.byteView[this.offset++];
|
|
|
|
if (len == 254) {
|
|
var len = this.byteView[this.offset++] |
|
|
(this.byteView[this.offset++] << 8) |
|
|
(this.byteView[this.offset++] << 16);
|
|
}
|
|
|
|
var bytes = this.byteView.subarray(this.offset, this.offset + len);
|
|
this.offset += len;
|
|
|
|
// Padding
|
|
while (this.offset % 4) {
|
|
this.offset++;
|
|
}
|
|
|
|
this.debug && console.log('<<<', bytesToHex(bytes), (field || '') + ':bytes');
|
|
|
|
return bytes;
|
|
}
|
|
|
|
TLDeserialization.prototype.fetchIntBytes = function (bits, typed, field) {
|
|
if (bits % 32) {
|
|
throw new Error('Invalid bits: ' + bits);
|
|
}
|
|
|
|
var len = bits / 8;
|
|
if (typed) {
|
|
var result = this.byteView.subarray(this.offset, this.offset + len);
|
|
this.offset += len;
|
|
return result;
|
|
}
|
|
|
|
var bytes = [];
|
|
for (var i = 0; i < len; i++) {
|
|
bytes.push(this.byteView[this.offset++]);
|
|
}
|
|
|
|
this.debug && console.log('<<<', bytesToHex(bytes), (field || '') + ':int' + bits);
|
|
|
|
return bytes;
|
|
};
|
|
|
|
|
|
TLDeserialization.prototype.fetchRawBytes = function (len, typed, field) {
|
|
if (len === false) {
|
|
len = this.readInt((field || '') + '_length');
|
|
}
|
|
|
|
if (typed) {
|
|
var bytes = new Uint8Array(len);
|
|
bytes.set(this.byteView.subarray(this.offset, this.offset + len));
|
|
this.offset += len;
|
|
return bytes;
|
|
}
|
|
|
|
var bytes = [];
|
|
for (var i = 0; i < len; i++) {
|
|
bytes.push(this.byteView[this.offset++]);
|
|
}
|
|
|
|
this.debug && console.log('<<<', bytesToHex(bytes), (field || ''));
|
|
|
|
return bytes;
|
|
};
|
|
|
|
TLDeserialization.prototype.fetchObject = function (type, field) {
|
|
switch (type) {
|
|
case 'int': return this.fetchInt(field);
|
|
case 'long': return this.fetchLong(field);
|
|
case 'int128': return this.fetchIntBytes(128, false, field);
|
|
case 'int256': return this.fetchIntBytes(256, false, field);
|
|
case 'int512': return this.fetchIntBytes(512, false, field);
|
|
case 'string': return this.fetchString(field);
|
|
case 'bytes': return this.fetchBytes(field);
|
|
case 'double': return this.fetchDouble(field);
|
|
case 'Bool': return this.fetchBool(field);
|
|
}
|
|
|
|
field = field || type || 'Object';
|
|
|
|
if (type.substr(0, 6) == 'Vector' || type.substr(0, 6) == 'vector') {
|
|
if (type.charAt(0) == 'V') {
|
|
var constructor = this.readInt(field + '[id]');
|
|
if (constructor != 0x1cb5c415) {
|
|
throw new Error('Invalid vector constructor ' + constructor);
|
|
}
|
|
}
|
|
var len = this.readInt(field + '[count]');
|
|
var result = [];
|
|
if (len > 0) {
|
|
var itemType = type.substr(7, type.length - 8); // for "Vector<itemType>"
|
|
for (var i = 0; i < len; i++) {
|
|
result.push(this.fetchObject(itemType, field + '[' + i + ']'))
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
var schema = this.mtproto ? Config.Schema.MTProto : Config.Schema.API,
|
|
predicate = false,
|
|
constructorData = false;
|
|
|
|
if (type.charAt(0) == '%') {
|
|
var checkType = type.substr(1);
|
|
for (i = 0; i < schema.constructors.length; i++) {
|
|
if (schema.constructors[i].type == checkType) {
|
|
constructorData = schema.constructors[i];
|
|
break
|
|
}
|
|
}
|
|
if (!constructorData) {
|
|
throw new Error('Constructor not found for type: ' + type);
|
|
}
|
|
}
|
|
else if (type.charAt(0) >= 97 && type.charAt(0) <= 122) {
|
|
for (i = 0; i < schema.constructors.length; i++) {
|
|
if (schema.constructors[i].predicate == type) {
|
|
constructorData = schema.constructors[i];
|
|
break
|
|
}
|
|
}
|
|
if (!constructorData) {
|
|
throw new Error('Constructor not found for predicate: ' + type);
|
|
}
|
|
}
|
|
else {
|
|
var constructor = this.readInt(field + '[id]'),
|
|
constructorCmp = uintToInt(constructor);
|
|
|
|
if (constructorCmp == 0x3072cfa1) { // Gzip packed
|
|
var compressed = this.fetchBytes(field + '[packed_string]'),
|
|
uncompressed = gzipUncompress(compressed),
|
|
buffer = bytesToArrayBuffer(uncompressed),
|
|
newDeserializer = (new TLDeserialization(buffer));
|
|
|
|
return newDeserializer.fetchObject(type, field);
|
|
}
|
|
|
|
for (i = 0; i < schema.constructors.length; i++) {
|
|
if (schema.constructors[i].id == constructorCmp) {
|
|
constructorData = schema.constructors[i];
|
|
break;
|
|
}
|
|
}
|
|
|
|
var fallback = false;
|
|
if (!constructorData && this.mtproto) {
|
|
var schemaFallback = Config.Schema.API;
|
|
for (i = 0; i < schemaFallback.constructors.length; i++) {
|
|
if (schemaFallback.constructors[i].id == constructorCmp) {
|
|
constructorData = schemaFallback.constructors[i];
|
|
|
|
delete this.mtproto;
|
|
fallback = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!constructorData) {
|
|
throw new Error('Constructor not found: ' + constructor);
|
|
}
|
|
}
|
|
|
|
predicate = constructorData.predicate;
|
|
|
|
var result = {'_': predicate},
|
|
overrideKey = (this.mtproto ? 'mt_' : '') + predicate,
|
|
self = this;
|
|
|
|
|
|
if (this.override[overrideKey]) {
|
|
this.override[overrideKey].apply(this, [result, field + '[' + predicate + ']']);
|
|
} else {
|
|
angular.forEach(constructorData.params, function (param) {
|
|
result[param.name] = self.fetchObject(param.type, field + '[' + predicate + '][' + param.name + ']');
|
|
});
|
|
}
|
|
|
|
if (fallback) {
|
|
this.mtproto = true;
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
TLDeserialization.prototype.getOffset = function () {
|
|
return this.offset;
|
|
};
|
|
|
|
TLDeserialization.prototype.fetchEnd = function () {
|
|
if (this.offset != this.byteView.length) {
|
|
throw new Error('Fetch end with non-empty buffer');
|
|
}
|
|
return true;
|
|
};
|