2021-11-23 00:03:59 +03:00
|
|
|
import Long from 'long'
|
|
|
|
|
2022-06-30 16:32:56 +03:00
|
|
|
import { gzipInflate } from './platform/gzip'
|
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
const TWO_PWR_32_DBL = (1 << 16) * (1 << 16)
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Mapping of TL object IDs to reader functions.
|
|
|
|
*
|
|
|
|
* Note that these types should never be present in the map
|
|
|
|
* and are parsed manually inside `.object()`:
|
|
|
|
* - `0x1cb5c415` aka `vector`
|
|
|
|
* - `0x3072cfa1` aka `gzip_packed`
|
|
|
|
* - `0xbc799737` aka `boolFalse`
|
|
|
|
* - `0x997275b5` aka `boolTrue`
|
|
|
|
* - `0x3fedd339` aka `true`
|
|
|
|
* - `0x56730bcc` aka `null`
|
|
|
|
*/
|
2023-06-05 03:30:48 +03:00
|
|
|
// avoid unnecessary type complexity
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
2023-07-20 22:07:07 +03:00
|
|
|
export type TlReaderMap = Record<number, (r: any) => unknown> & {
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
_results?: Record<string, (r: any) => unknown>
|
|
|
|
}
|
2021-11-23 00:03:59 +03:00
|
|
|
|
2022-08-29 16:22:57 +03:00
|
|
|
/**
|
|
|
|
* Reader for TL objects.
|
|
|
|
*/
|
2021-11-23 00:03:59 +03:00
|
|
|
export class TlBinaryReader {
|
2022-08-29 16:22:57 +03:00
|
|
|
/**
|
|
|
|
* Underlying buffer.
|
|
|
|
*/
|
2021-04-08 12:19:38 +03:00
|
|
|
data: Buffer
|
2022-08-29 16:22:57 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Position in the buffer.
|
|
|
|
*/
|
2021-04-08 12:19:38 +03:00
|
|
|
pos = 0
|
|
|
|
|
2022-08-29 16:22:57 +03:00
|
|
|
/**
|
|
|
|
* @param objectsMap Readers map
|
|
|
|
* @param data Buffer to read from
|
|
|
|
* @param start Position to start reading from
|
|
|
|
*/
|
2023-09-24 01:32:22 +03:00
|
|
|
constructor(readonly objectsMap: TlReaderMap | undefined, data: Buffer, start = 0) {
|
2021-04-08 12:19:38 +03:00
|
|
|
this.data = data
|
|
|
|
this.pos = start
|
|
|
|
}
|
|
|
|
|
2022-08-29 16:22:57 +03:00
|
|
|
/**
|
|
|
|
* Create a new reader without objects map for manual usage
|
|
|
|
*
|
|
|
|
* @param data Buffer to read from
|
|
|
|
* @param start Position to start reading from
|
|
|
|
*/
|
2021-11-23 00:03:59 +03:00
|
|
|
static manual(data: Buffer, start = 0): TlBinaryReader {
|
|
|
|
return new TlBinaryReader(undefined, data, start)
|
2021-04-08 12:19:38 +03:00
|
|
|
}
|
|
|
|
|
2022-08-29 16:22:57 +03:00
|
|
|
/**
|
|
|
|
* Deserialize a single object
|
|
|
|
*
|
|
|
|
* @param objectsMap Readers map
|
|
|
|
* @param data Buffer to read from
|
|
|
|
* @param start Position to start reading from
|
|
|
|
*/
|
2023-09-24 01:32:22 +03:00
|
|
|
static deserializeObject<T>(objectsMap: TlReaderMap, data: Buffer, start = 0): T {
|
2023-06-05 03:30:48 +03:00
|
|
|
return new TlBinaryReader(objectsMap, data, start).object() as T
|
2021-11-23 00:03:59 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
int(): number {
|
2021-04-08 12:19:38 +03:00
|
|
|
const res = this.data.readInt32LE(this.pos)
|
|
|
|
this.pos += 4
|
2023-06-05 03:30:48 +03:00
|
|
|
|
2021-04-08 12:19:38 +03:00
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
uint(): number {
|
2021-04-08 12:19:38 +03:00
|
|
|
const res = this.data.readUInt32LE(this.pos)
|
|
|
|
this.pos += 4
|
2023-06-05 03:30:48 +03:00
|
|
|
|
2021-04-08 12:19:38 +03:00
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
2022-08-29 16:22:57 +03:00
|
|
|
/**
|
|
|
|
* Get the next {@link uint} without advancing the reader cursor
|
|
|
|
*/
|
2021-11-23 00:03:59 +03:00
|
|
|
peekUint(): number {
|
|
|
|
// e.g. for checking ctor number
|
|
|
|
return this.data.readUInt32LE(this.pos)
|
|
|
|
}
|
2021-05-21 22:15:25 +03:00
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
int53(): number {
|
|
|
|
// inlined toNumber from Long
|
2023-09-24 01:32:22 +03:00
|
|
|
const res = (this.data.readInt32LE(this.pos) >>> 0) + TWO_PWR_32_DBL * this.data.readInt32LE(this.pos + 4)
|
2021-04-08 12:19:38 +03:00
|
|
|
this.pos += 8
|
2023-06-05 03:30:48 +03:00
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
return res
|
2021-04-08 12:19:38 +03:00
|
|
|
}
|
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
long(unsigned = false): Long {
|
|
|
|
const lo = this.data.readInt32LE(this.pos)
|
|
|
|
const hi = this.data.readInt32LE(this.pos + 4)
|
2021-04-08 12:19:38 +03:00
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
this.pos += 8
|
2023-06-05 03:30:48 +03:00
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
return new Long(lo, hi, unsigned)
|
2021-04-08 12:19:38 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
float(): number {
|
|
|
|
const res = this.data.readFloatLE(this.pos)
|
|
|
|
this.pos += 4
|
2023-06-05 03:30:48 +03:00
|
|
|
|
2021-04-08 12:19:38 +03:00
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
|
|
|
double(): number {
|
|
|
|
const res = this.data.readDoubleLE(this.pos)
|
|
|
|
this.pos += 8
|
2023-06-05 03:30:48 +03:00
|
|
|
|
2021-04-08 12:19:38 +03:00
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
|
|
|
boolean(): boolean {
|
2021-11-23 00:03:59 +03:00
|
|
|
const val = this.uint()
|
2021-04-08 12:19:38 +03:00
|
|
|
if (val === 0xbc799737) return false
|
|
|
|
if (val === 0x997275b5) return true
|
2023-09-24 01:32:22 +03:00
|
|
|
throw new Error(`Expected either boolTrue or boolFalse, got 0x${val.toString(16)}`)
|
2021-04-08 12:19:38 +03:00
|
|
|
}
|
|
|
|
|
2022-08-29 16:22:57 +03:00
|
|
|
/**
|
|
|
|
* Read raw bytes of the given length
|
|
|
|
* @param bytes Length of the buffer to read
|
|
|
|
*/
|
2021-04-08 12:19:38 +03:00
|
|
|
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++]
|
2023-07-20 22:07:07 +03:00
|
|
|
let length
|
|
|
|
let padding
|
2023-06-05 03:30:48 +03:00
|
|
|
|
2021-04-08 12:19:38 +03:00
|
|
|
if (firstByte === 254) {
|
2023-09-24 01:32:22 +03:00
|
|
|
length = this.data[this.pos++] | (this.data[this.pos++] << 8) | (this.data[this.pos++] << 16)
|
2021-04-08 12:19:38 +03:00
|
|
|
padding = length % 4
|
|
|
|
} else {
|
|
|
|
length = firstByte
|
|
|
|
padding = (length + 1) % 4
|
|
|
|
}
|
|
|
|
|
|
|
|
const data = this.raw(length)
|
2021-11-23 00:03:59 +03:00
|
|
|
if (padding > 0) this.pos += 4 - padding
|
2021-04-08 12:19:38 +03:00
|
|
|
|
|
|
|
return data
|
|
|
|
}
|
|
|
|
|
|
|
|
string(): string {
|
|
|
|
return this.bytes().toString('utf-8')
|
|
|
|
}
|
|
|
|
|
2023-06-05 03:30:48 +03:00
|
|
|
object(): unknown {
|
2021-11-23 00:03:59 +03:00
|
|
|
const id = this.uint()
|
2021-04-08 12:19:38 +03:00
|
|
|
|
2023-07-20 22:07:07 +03:00
|
|
|
if (id === 0x1cb5c415 /* vector */) {
|
|
|
|
return this.vector(this.object, true)
|
|
|
|
}
|
2021-04-08 12:19:38 +03:00
|
|
|
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
|
|
|
|
|
2023-06-05 03:30:48 +03:00
|
|
|
// hot path, avoid additional runtime checks
|
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
const reader = this.objectsMap![id]
|
2023-06-05 03:30:48 +03:00
|
|
|
|
2021-04-08 12:19:38 +03:00
|
|
|
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(
|
2023-09-24 01:32:22 +03:00
|
|
|
`Unknown object id: 0x${id.toString(16)}. Content: ${this.raw().toString('hex')}`,
|
2021-04-08 12:19:38 +03:00
|
|
|
)
|
|
|
|
this.pos = pos
|
|
|
|
throw error
|
|
|
|
}
|
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
return reader(this)
|
2021-04-08 12:19:38 +03:00
|
|
|
}
|
|
|
|
|
2023-06-05 03:30:48 +03:00
|
|
|
gzip(): unknown {
|
2023-09-24 01:32:22 +03:00
|
|
|
return new TlBinaryReader(this.objectsMap, gzipInflate(this.bytes())).object()
|
2021-04-08 12:19:38 +03:00
|
|
|
}
|
|
|
|
|
2023-06-05 03:30:48 +03:00
|
|
|
vector(reader = this.object, bare = false): unknown[] {
|
2021-04-08 12:19:38 +03:00
|
|
|
if (!bare) {
|
2023-06-05 03:30:48 +03:00
|
|
|
if (this.uint() !== 0x1cb5c415) {
|
2023-09-24 01:32:22 +03:00
|
|
|
throw new Error('Invalid object code, expected 0x1cb5c415 (vector)')
|
2023-06-05 03:30:48 +03:00
|
|
|
}
|
2021-04-08 12:19:38 +03:00
|
|
|
}
|
|
|
|
|
2021-11-23 00:03:59 +03:00
|
|
|
const length = this.uint()
|
2021-04-08 12:19:38 +03:00
|
|
|
const ret = []
|
|
|
|
for (let i = 0; i < length; i++) ret.push(reader.call(this))
|
2023-06-05 03:30:48 +03:00
|
|
|
|
2021-04-08 12:19:38 +03:00
|
|
|
return ret
|
|
|
|
}
|
|
|
|
|
2022-08-29 16:22:57 +03:00
|
|
|
/**
|
|
|
|
* Advance the reader cursor by the given amount of bytes
|
|
|
|
*
|
|
|
|
* @param delta Amount of bytes to advance (can be negative)
|
|
|
|
*/
|
2021-04-08 12:19:38 +03:00
|
|
|
seek(delta: number): void {
|
|
|
|
this.seekTo(this.pos + delta)
|
|
|
|
}
|
|
|
|
|
2022-08-29 16:22:57 +03:00
|
|
|
/**
|
|
|
|
* Seek to the given position
|
|
|
|
*
|
|
|
|
* @param pos Position to seek to
|
|
|
|
*/
|
2021-04-08 12:19:38 +03:00
|
|
|
seekTo(pos: number): void {
|
2023-07-20 22:07:07 +03:00
|
|
|
if (pos >= this.data.length || pos < 0) {
|
|
|
|
throw new RangeError('New position is out of range')
|
|
|
|
}
|
2021-04-08 12:19:38 +03:00
|
|
|
this.pos = pos
|
|
|
|
}
|
|
|
|
}
|