import { promisify } from 'util' import { inflate } from 'zlib' import * as constants from './constants' import { jidEncode, type JidServer, WAJIDDomains } from './jid-utils' import type { BinaryNode, BinaryNodeCodingOptions } from './types' const inflatePromise = promisify(inflate) export const decompressingIfRequired = async (buffer: Buffer) => { if (2 & buffer.readUInt8()) { buffer = await inflatePromise(buffer.slice(1)) } else { // nodes with no compression have a 0x00 prefix, we remove that buffer = buffer.slice(1) } return buffer } export const decodeDecompressedBinaryNode = ( buffer: Buffer, opts: Pick, indexRef: { index: number } = { index: 0 } ): BinaryNode => { const { DOUBLE_BYTE_TOKENS, SINGLE_BYTE_TOKENS, TAGS } = opts const checkEOS = (length: number) => { if (indexRef.index + length > buffer.length) { throw new Error('end of stream') } } const next = () => { const value = buffer[indexRef.index] indexRef.index += 1 return value } const readByte = () => { checkEOS(1) return next() } const readBytes = (n: number) => { checkEOS(n) const value = buffer.slice(indexRef.index, indexRef.index + n) indexRef.index += n return value } const readStringFromChars = (length: number) => { return readBytes(length).toString('utf-8') } const readInt = (n: number, littleEndian = false) => { checkEOS(n) let val = 0 for (let i = 0; i < n; i++) { const shift = littleEndian ? i : n - 1 - i val |= next()! << (shift * 8) } return val } const readInt20 = () => { checkEOS(3) return ((next()! & 15) << 16) + (next()! << 8) + next()! } const unpackHex = (value: number) => { if (value >= 0 && value < 16) { return value < 10 ? '0'.charCodeAt(0) + value : 'A'.charCodeAt(0) + value - 10 } throw new Error('invalid hex: ' + value) } const unpackNibble = (value: number) => { if (value >= 0 && value <= 9) { return '0'.charCodeAt(0) + value } switch (value) { case 10: return '-'.charCodeAt(0) case 11: return '.'.charCodeAt(0) case 15: return '\0'.charCodeAt(0) default: throw new Error('invalid nibble: ' + value) } } const unpackByte = (tag: number, value: number) => { if (tag === TAGS.NIBBLE_8) { return unpackNibble(value) } else if (tag === TAGS.HEX_8) { return unpackHex(value) } else { throw new Error('unknown tag: ' + tag) } } const readPacked8 = (tag: number) => { const startByte = readByte()! let value = '' for (let i = 0; i < (startByte & 127); i++) { const curByte = readByte()! value += String.fromCharCode(unpackByte(tag, (curByte & 0xf0) >> 4)) value += String.fromCharCode(unpackByte(tag, curByte & 0x0f)) } if (startByte >> 7 !== 0) { value = value.slice(0, -1) } return value } const isListTag = (tag: number) => { return tag === TAGS.LIST_EMPTY || tag === TAGS.LIST_8 || tag === TAGS.LIST_16 } const readListSize = (tag: number) => { switch (tag) { case TAGS.LIST_EMPTY: return 0 case TAGS.LIST_8: return readByte() case TAGS.LIST_16: return readInt(2) default: throw new Error('invalid tag for list size: ' + tag) } } const readJidPair = () => { const i = readString(readByte()!) const j = readString(readByte()!) if (j) { return (i || '') + '@' + j } throw new Error('invalid jid pair: ' + i + ', ' + j) } const readAdJid = () => { const rawDomainType = readByte() const domainType = Number(rawDomainType) const device = readByte() const user = readString(readByte()!) let server: JidServer = 's.whatsapp.net' // default whatsapp server if (domainType === WAJIDDomains.LID) { server = 'lid' } else if (domainType === WAJIDDomains.HOSTED) { server = 'hosted' } else if (domainType === WAJIDDomains.HOSTED_LID) { server = 'hosted.lid' } return jidEncode(user, server, device) } const readFbJid = () => { const user = readString(readByte()!) const device = readInt(2) const server = readString(readByte()!) return `${user}:${device}@${server}` } const readInteropJid = () => { const user = readString(readByte()!) const device = readInt(2) const integrator = readInt(2) let server = 'interop' const beforeServer = indexRef.index try { server = readString(readByte()!) } catch (err) { indexRef.index = beforeServer } return `${integrator}-${user}:${device}@${server}` } const readString = (tag: number): string => { if (tag >= 1 && tag < SINGLE_BYTE_TOKENS.length) { return SINGLE_BYTE_TOKENS[tag] || '' } switch (tag) { case TAGS.DICTIONARY_0: case TAGS.DICTIONARY_1: case TAGS.DICTIONARY_2: case TAGS.DICTIONARY_3: return getTokenDouble(tag - TAGS.DICTIONARY_0, readByte()!) case TAGS.LIST_EMPTY: return '' case TAGS.BINARY_8: return readStringFromChars(readByte()!) case TAGS.BINARY_20: return readStringFromChars(readInt20()) case TAGS.BINARY_32: return readStringFromChars(readInt(4)) case TAGS.JID_PAIR: return readJidPair() case TAGS.FB_JID: return readFbJid() case TAGS.INTEROP_JID: return readInteropJid() case TAGS.AD_JID: return readAdJid() case TAGS.HEX_8: case TAGS.NIBBLE_8: return readPacked8(tag) default: throw new Error('invalid string with tag: ' + tag) } } const readList = (tag: number) => { const items: BinaryNode[] = [] const size = readListSize(tag)! for (let i = 0; i < size; i++) { items.push(decodeDecompressedBinaryNode(buffer, opts, indexRef)) } return items } const getTokenDouble = (index1: number, index2: number) => { const dict = DOUBLE_BYTE_TOKENS[index1] if (!dict) { throw new Error(`Invalid double token dict (${index1})`) } const value = dict[index2] if (typeof value === 'undefined') { throw new Error(`Invalid double token (${index2})`) } return value } const listSize = readListSize(readByte()!) const header = readString(readByte()!) if (!listSize || !header.length) { throw new Error('invalid node') } const attrs: BinaryNode['attrs'] = {} let data: BinaryNode['content'] if (listSize === 0 || !header) { throw new Error('invalid node') } // read the attributes in const attributesLength = (listSize - 1) >> 1 for (let i = 0; i < attributesLength; i++) { const key = readString(readByte()!) const value = readString(readByte()!) attrs[key] = value } if (listSize % 2 === 0) { const tag = readByte()! if (isListTag(tag)) { data = readList(tag) } else { let decoded: Buffer | string switch (tag) { case TAGS.BINARY_8: decoded = readBytes(readByte()!) break case TAGS.BINARY_20: decoded = readBytes(readInt20()) break case TAGS.BINARY_32: decoded = readBytes(readInt(4)) break default: decoded = readString(tag) break } data = decoded } } return { tag: header, attrs, content: data } } export const decodeBinaryNode = async (buff: Buffer): Promise => { const decompBuff = await decompressingIfRequired(buff) return decodeDecompressedBinaryNode(decompBuff, constants) }