import {bigint, bigStringInt, bytesToHex, isObject} from '../bin_utils'; import Schema from './schema'; /// #if MTPROTO_WORKER // @ts-ignore import {gzipUncompress} from '../crypto/crypto_utils'; /// #else // @ts-ignore import {gzipUncompress} from '../bin_utils'; /// #endif const boolFalse = +Schema.API.constructors.find(c => c.predicate == 'boolFalse').id >>> 0; const boolTrue = +Schema.API.constructors.find(c => c.predicate == 'boolTrue').id >>> 0; const vector = +Schema.API.constructors.find(c => c.predicate == 'vector').id >>> 0; const gzipPacked = +Schema.MTProto.constructors.find(c => c.predicate == 'gzip_packed').id >>> 0; //console.log('boolFalse', boolFalse == 0xbc799737); class TLSerialization { public maxLength = 2048; // 2Kb public offset = 0; // in bytes public mtproto = false; private debug = false;//Modes.debug; public buffer: ArrayBuffer; public intView: Int32Array; public byteView: Uint8Array; constructor(options: any = {}) { this.maxLength = options.startMaxLength || 2048 // 2Kb this.mtproto = options.mtproto || false; this.createBuffer(); } public createBuffer() { this.buffer = new ArrayBuffer(this.maxLength); this.intView = new Int32Array(this.buffer); this.byteView = new Uint8Array(this.buffer); } public getArray() { let resultBuffer = new ArrayBuffer(this.offset); let resultArray = new Int32Array(resultBuffer); resultArray.set(this.intView.subarray(0, this.offset / 4)); return resultArray; } public getBuffer() { return this.getArray().buffer; } public getBytes(typed: true): Uint8Array; public getBytes(typed?: false): number[]; public getBytes(typed?: boolean): number[] | Uint8Array { if(typed) { let resultBuffer = new ArrayBuffer(this.offset); let resultArray = new Uint8Array(resultBuffer); resultArray.set(this.byteView.subarray(0, this.offset)); return resultArray; } let bytes: number[] = []; for(var i = 0; i < this.offset; i++) { bytes.push(this.byteView[i]); } return bytes; } public checkLength(needBytes: number) { 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; var previousArray = new Int32Array(previousBuffer); this.createBuffer(); new Int32Array(this.buffer).set(previousArray); } public writeInt(i: number, field: string) { this.debug && console.log('>>>', i.toString(16), i, field); this.checkLength(4); this.intView[this.offset / 4] = i; this.offset += 4; } public storeInt(i: number, field?: string) { this.writeInt(i, (field || '') + ':int'); } public storeBool(i: boolean, field?: string) { if(i) { this.writeInt(boolTrue, (field || '') + ':bool'); } else { this.writeInt(boolFalse, (field || '') + ':bool'); } } public storeLongP(iHigh: number, iLow: number, field?: string) { this.writeInt(iLow, (field || '') + ':long[low]'); this.writeInt(iHigh, (field || '') + ':long[high]'); } public storeLong(sLong: Array | string | number, field?: string) { if(Array.isArray(sLong)) { if(sLong.length == 2) { return this.storeLongP(sLong[0], sLong[1], field); } else { return this.storeIntBytes(sLong, 64, field); } } if(typeof sLong != 'string') { sLong = sLong ? sLong.toString() : '0'; } var divRem = bigStringInt(sLong).divideAndRemainder(bigint(0x100000000)); this.writeInt(divRem[1].intValue(), (field || '') + ':long[low]'); this.writeInt(divRem[0].intValue(), (field || '') + ':long[high]'); } public storeDouble(f: any, field?: string) { 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]'); } public storeString(s: string, field?: string) { this.debug && console.log('>>>', s, (field || '') + ':string'); if(s === undefined) { s = ''; } 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; } } public storeBytes(bytes: any, field?: string) { if(bytes instanceof ArrayBuffer) { bytes = new Uint8Array(bytes); } else if(bytes === undefined) { bytes = []; } this.debug && console.log('>>>', bytesToHex(bytes), (field || '') + ':bytes'); // if uint8array were json.stringified, then will be: {'0': 123, '1': 123} 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; } } public storeIntBytes(bytes: any, bits: any, field?: string) { 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; } public storeRawBytes(bytes: any, field?: string) { 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; } public storeMethod(methodName: string, params: any) { var schema = this.mtproto ? Schema.MTProto : Schema.API; var methodData: any = 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(methodData.id, methodName + '[id]'); var param, type; var i, condType; var fieldBit; var len = methodData.params.length; //console.log('storeMethod', len, methodData); for(i = 0; i < len; i++) { param = methodData.params[i]; type = param.type; if(type.indexOf('?') !== -1) { condType = type.split('?'); fieldBit = condType[0].split('.'); if(!(params[fieldBit[0]] & (1 << fieldBit[1]))) { continue; } type = condType[1]; } this.storeObject(params[param.name], type, methodName + '[' + param.name + ']'); } return methodData.type; } public storeObject(obj: any, type: string, field?: string) { //console.log('storeObject', obj, type, field, this.offset, this.getBytes(true).hex); switch(type) { case '#': 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); case 'true': return } if(Array.isArray(obj)) { if(type.substr(0, 6) == 'Vector') { this.writeInt(vector, 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" 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(!isObject(obj)) { throw new Error('Invalid object for type ' + type); } var schema = this.mtproto ? Schema.MTProto : Schema.API; var predicate = obj['_']; var isBare = false; var constructorData: any = false; 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(constructorData.id, field + '[' + predicate + '][id]'); } var param, type: string; var condType; var fieldBit; var len = constructorData.params.length; //console.log('storeObject', len, constructorData); for(i = 0; i < len; i++) { param = constructorData.params[i]; type = param.type; //console.log('storeObject', param, type); if(type.indexOf('?') !== -1) { condType = type.split('?'); fieldBit = condType[0].split('.'); //console.log('storeObject fieldBit', fieldBit, obj[fieldBit[0]]); if(!(obj[fieldBit[0]] & (1 << +fieldBit[1]))) { continue; } type = condType[1]; } //console.log('storeObject', param, type); this.storeObject(obj[param.name], type, field + '[' + predicate + '][' + param.name + ']'); } return constructorData.type; } } class TLDeserialization { public offset = 0; // in bytes public override: any; public buffer: ArrayBuffer; //public intView: Uint32Array; public byteView: Uint8Array; // this.debug = public mtproto: boolean = false; private debug: boolean; constructor(buffer: ArrayBuffer | Uint8Array, options: any = {}) { //buffer = addPadding(buffer, 4, true); // fix 21.01.2020 for wss if(buffer instanceof ArrayBuffer) { this.buffer = buffer; this.byteView = new Uint8Array(this.buffer); } else { this.buffer = buffer.buffer; this.byteView = buffer; } //console.log("TCL: TLDeserialization -> constructor -> buffer", buffer, this.byteView, this.byteView.hex); /* this.buffer = buffer; //this.intView = new Uint32Array(this.buffer); this.byteView = new Uint8Array(this.buffer); */ //console.log(this.intView); this.override = 'override' in options ? options.override : {}; this.mtproto = 'mtproto' in options ? options.mtproto : false; this.debug = options.debug !== undefined ? options.debug : /* Modes.debug */false; } public readInt(field: string) { //if(this.offset >= this.intView.length * 4) { if((this.byteView.length - this.offset) < 4) { console.error(this.byteView, this.offset); throw new Error('Nothing to fetch: ' + field); } //var i = this.intView[this.offset / 4]; let i = new Uint32Array(this.byteView.buffer.slice(this.offset, this.offset + 4))[0]; this.debug/* || field.includes('[dialog][read_outbox_max_id]') */ && console.log('<<<', i.toString(16), i, field, this.byteView.slice(this.offset - 16, this.offset + 16), this.byteView.slice(this.offset - 16, this.offset + 16).hex); this.offset += 4; return i; } public fetchInt(field?: string) { return this.readInt((field || '') + ':int'); } public fetchDouble(field?: string) { 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]; } public fetchLong(field?: string) { var iLow = this.readInt((field || '') + ':long[low]'); var iHigh = this.readInt((field || '') + ':long[high]'); var longDec = bigint(iHigh).shiftLeft(32).add(bigint(iLow)).toString(); return longDec; } public fetchBool(field?: string) { var i = this.readInt((field || '') + ':bool'); if(i == boolTrue) { return true; } else if(i == boolFalse) { return false; } this.offset -= 4; return this.fetchObject('Object', field); } public fetchString(field?: string) { 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; } public fetchBytes(field?: string) { var len = this.byteView[this.offset++]; if(len == 254) { 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; } public fetchIntBytes(bits: number, typed: true, field?: string): Uint8Array; public fetchIntBytes(bits: number, typed?: false, field?: string): number[]; public fetchIntBytes(bits: number, typed?: boolean, field?: string) { 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; } public fetchRawBytes(len: any, typed: true, field: string): Uint8Array; public fetchRawBytes(len: any, typed: false, field: string): number[]; public fetchRawBytes(len: any, typed: boolean, field: string) { if(len === false) { len = this.readInt((field || '') + '_length'); if(len > this.byteView.byteLength) { throw new Error('Invalid raw bytes length: ' + len + ', buffer len: ' + this.byteView.byteLength); } } if(typed) { let 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; } public fetchObject(type: any, field?: string): any { switch(type) { case '#': 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); case 'true': return true; } 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]'); var constructorCmp = constructor; if(constructorCmp == gzipPacked) { // Gzip packed var compressed = this.fetchBytes(field + '[packed_string]'); var uncompressed = gzipUncompress(compressed); var newDeserializer = new TLDeserialization(uncompressed); return newDeserializer.fetchObject(type, field); } if(constructorCmp != vector) { throw new Error('Invalid vector constructor ' + constructor); } } var len = this.readInt(field + '[count]'); var result: any = []; if(len > 0) { var itemType = type.substr(7, type.length - 8); // for "Vector" for(var i = 0; i < len; i++) { result.push(this.fetchObject(itemType, field + '[' + i + ']')); } } return result; } var schema = this.mtproto ? Schema.MTProto : Schema.API; var predicate = false; var constructorData: any = false; if(type.charAt(0) == '%') { var checkType = type.substr(1); for(var 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(var 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]'); var constructorCmp = constructor; if(constructorCmp == gzipPacked) { // Gzip packed var compressed = this.fetchBytes(field + '[packed_string]'); var uncompressed = gzipUncompress(compressed); var newDeserializer = new TLDeserialization(uncompressed); return newDeserializer.fetchObject(type, field); } var index = schema.constructorsIndex; if(!index) { schema.constructorsIndex = index = {}; for(var i = 0; i < schema.constructors.length; i++) { index[schema.constructors[i].id] = i; } } var i = index[constructorCmp]; if(i) { constructorData = schema.constructors[i]; } var fallback = false; if(!constructorData && this.mtproto) { var schemaFallback = 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 + ' ' + this.fetchInt() + ' ' + this.fetchInt() + ' ' + field); } } predicate = constructorData.predicate; var result: any = {'_': predicate}; var overrideKey = (this.mtproto ? 'mt_' : '') + predicate; var self = this; if(this.override[overrideKey]) { this.override[overrideKey].apply(this, [result, field + '[' + predicate + ']']); } else { var i: number, param; var type, isCond; var condType, fieldBit; var value; var len: number = constructorData.params.length; for(i = 0; i < len; i++) { param = constructorData.params[i]; type = param.type; if(type == '#' && result.pFlags === undefined) { result.pFlags = {}; } if(isCond = (type.indexOf('?') !== -1)) { condType = type.split('?'); fieldBit = condType[0].split('.'); if(!(result[fieldBit[0]] & (1 << fieldBit[1]))) { //console.log('fetchObject bad', constructorData, result[fieldBit[0]], fieldBit); continue; } //console.log('fetchObject good', constructorData, result[fieldBit[0]], fieldBit); type = condType[1]; } value = self.fetchObject(type, field + '[' + predicate + '][' + param.name + ']'); if(isCond && type === 'true') { result.pFlags[param.name] = value; } else { /* if(param.name == 'read_outbox_max_id') { console.log(result, param.name, value, field + '[' + predicate + '][' + param.name + ']'); } */ result[param.name] = value; } } } if(fallback) { this.mtproto = true; } return result; } public getOffset() { return this.offset; } public fetchEnd() { if(this.offset != this.byteView.length) { throw new Error('Fetch end with non-empty buffer'); } return true; } } export {TLDeserialization, TLSerialization};