2021-04-08 12:19:38 +03:00
|
|
|
import bigInt, { BigInteger } from 'big-integer'
|
|
|
|
import { inflate } from 'pako'
|
|
|
|
import { typedArrayToBuffer } from '../buffer-utils'
|
|
|
|
import readerMap, { ITlBinaryReader } from '@mtcute/tl/binary/reader'
|
2021-05-21 22:15:25 +03:00
|
|
|
import { bufferToBigInt, longToUlong, ulongToLong } from '../bigint-utils'
|
2021-04-08 12:19:38 +03:00
|
|
|
import { tl } from '@mtcute/tl'
|
|
|
|
|
2021-05-21 22:15:25 +03:00
|
|
|
const isNativeBigIntAvailable = typeof BigInt !== 'undefined' && 'readBigInt64LE' in Buffer.prototype
|
|
|
|
|
2021-04-08 12:19:38 +03:00
|
|
|
export class BinaryReader implements ITlBinaryReader {
|
|
|
|
data: Buffer
|
|
|
|
pos = 0
|
|
|
|
|
|
|
|
_objectsMap = readerMap
|
|
|
|
|
|
|
|
constructor(data: Buffer, start = 0) {
|
|
|
|
this.data = data
|
|
|
|
this.pos = start
|
|
|
|
}
|
|
|
|
|
|
|
|
static deserializeObject<T = tl.TlObject>(data: Buffer, start = 0): T {
|
|
|
|
return new BinaryReader(data, start).object()
|
|
|
|
}
|
|
|
|
|
|
|
|
int32(): number {
|
|
|
|
const res = this.data.readInt32LE(this.pos)
|
|
|
|
this.pos += 4
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
|
|
|
uint32(): number {
|
|
|
|
const res = this.data.readUInt32LE(this.pos)
|
|
|
|
this.pos += 4
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
|
|
|
long(unsigned = false): BigInteger {
|
2021-05-21 22:15:25 +03:00
|
|
|
let big: BigInteger
|
|
|
|
if (isNativeBigIntAvailable) {
|
|
|
|
const val = this.data.readBigInt64LE(this.pos)
|
|
|
|
big = bigInt(val)
|
|
|
|
|
|
|
|
if (unsigned) big = longToUlong(big)
|
|
|
|
} else {
|
|
|
|
big = bufferToBigInt(this.data, this.pos, 8, true)
|
|
|
|
if (!unsigned) big = ulongToLong(big)
|
|
|
|
}
|
|
|
|
|
2021-04-08 12:19:38 +03:00
|
|
|
this.pos += 8
|
|
|
|
|
|
|
|
return big
|
|
|
|
}
|
|
|
|
|
|
|
|
// for use in generated code
|
|
|
|
ulong(): BigInteger {
|
|
|
|
return this.long(true)
|
|
|
|
}
|
|
|
|
|
|
|
|
rawLong(): Buffer {
|
|
|
|
return this.data.slice(this.pos, (this.pos += 8))
|
|
|
|
}
|
|
|
|
|
|
|
|
float(): number {
|
|
|
|
const res = this.data.readFloatLE(this.pos)
|
|
|
|
this.pos += 4
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
|
|
|
double(): number {
|
|
|
|
const res = this.data.readDoubleLE(this.pos)
|
|
|
|
this.pos += 8
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
|
|
|
boolean(): boolean {
|
|
|
|
const val = this.uint32()
|
|
|
|
if (val === 0xbc799737) return false
|
|
|
|
if (val === 0x997275b5) return true
|
|
|
|
throw new Error(
|
|
|
|
`Expected either boolTrue or boolFalse, got 0x${val.toString(16)}`
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
raw(bytes = -1): Buffer {
|
|
|
|
if (bytes === -1) bytes = this.data.length - this.pos
|
|
|
|
|
|
|
|
return this.data.slice(this.pos, (this.pos += bytes))
|
|
|
|
}
|
|
|
|
|
|
|
|
int128(): Buffer {
|
|
|
|
return this.data.slice(this.pos, (this.pos += 16))
|
|
|
|
}
|
|
|
|
|
|
|
|
int256(): Buffer {
|
|
|
|
return this.data.slice(this.pos, (this.pos += 32))
|
|
|
|
}
|
|
|
|
|
|
|
|
bytes(): Buffer {
|
|
|
|
const firstByte = this.data[this.pos++]
|
|
|
|
let length, padding
|
|
|
|
if (firstByte === 254) {
|
|
|
|
length =
|
|
|
|
this.data[this.pos++] |
|
|
|
|
(this.data[this.pos++] << 8) |
|
|
|
|
(this.data[this.pos++] << 16)
|
|
|
|
padding = length % 4
|
|
|
|
} else {
|
|
|
|
length = firstByte
|
|
|
|
padding = (length + 1) % 4
|
|
|
|
}
|
|
|
|
|
|
|
|
const data = this.raw(length)
|
|
|
|
if (padding > 0) this.raw(4 - padding)
|
|
|
|
|
|
|
|
return data
|
|
|
|
}
|
|
|
|
|
|
|
|
string(): string {
|
|
|
|
return this.bytes().toString('utf-8')
|
|
|
|
}
|
|
|
|
|
|
|
|
object(): any {
|
|
|
|
const id = this.uint32()
|
|
|
|
|
|
|
|
if (id === 0x1cb5c415 /* vector */)
|
|
|
|
return this.vector(this.object, true)
|
|
|
|
if (id === 0x3072cfa1 /* gzip_packed */) return this.gzip()
|
|
|
|
if (id === 0xbc799737 /* boolFalse */) return false
|
|
|
|
if (id === 0x997275b5 /* boolTrue */) return true
|
|
|
|
// unsure if it is actually used in the wire, seems like it's only used for boolean flags
|
|
|
|
if (id === 0x3fedd339 /* true */) return true
|
|
|
|
// never used in the actual schema, but whatever
|
|
|
|
if (id === 0x56730bcc /* null */) return null
|
|
|
|
|
|
|
|
const reader = this._objectsMap[id]
|
|
|
|
if (!reader) {
|
|
|
|
// unknown type. bruh moment.
|
|
|
|
// mtproto sucks and there's no way we can just skip it
|
|
|
|
this.seek(-4)
|
|
|
|
const pos = this.pos
|
|
|
|
const error = new TypeError(
|
|
|
|
`Unknown object id: 0x${id.toString(
|
|
|
|
16
|
|
|
|
)}. Content: ${this.raw().toString('hex')}`
|
|
|
|
)
|
|
|
|
this.pos = pos
|
|
|
|
throw error
|
|
|
|
}
|
|
|
|
|
|
|
|
return reader.call(this)
|
|
|
|
}
|
|
|
|
|
|
|
|
gzip(): any {
|
|
|
|
return new BinaryReader(
|
|
|
|
typedArrayToBuffer(inflate(this.bytes()))
|
|
|
|
).object()
|
|
|
|
}
|
|
|
|
|
|
|
|
vector(reader = this.object, bare = false): any[] {
|
|
|
|
if (!bare) {
|
|
|
|
if (this.uint32() !== 0x1cb5c415)
|
|
|
|
throw new Error(
|
|
|
|
'Invalid object code, expected 0x1cb5c415 (vector)'
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
const length = this.uint32()
|
|
|
|
const ret = []
|
|
|
|
for (let i = 0; i < length; i++) ret.push(reader.call(this))
|
|
|
|
return ret
|
|
|
|
}
|
|
|
|
|
|
|
|
seek(delta: number): void {
|
|
|
|
this.seekTo(this.pos + delta)
|
|
|
|
}
|
|
|
|
|
|
|
|
seekTo(pos: number): void {
|
|
|
|
if (pos >= this.data.length || pos < 0)
|
|
|
|
throw new RangeError('New position is out of range')
|
|
|
|
this.pos = pos
|
|
|
|
}
|
|
|
|
}
|