9525f42cb2
* perf: optimize history sync memory and CPU usage Aligns with Baileys upstream PR #2333 . Surgical port of 4 micro-optimizations + 2 test files. No collision with InfiniteAPI's LID/PN, carousel, buttons, Bad MAC or app state sync resilience code. Changes: 1. WAProto longToString/longToNumber: use native BigInt instead of Long library division loops. ~4.6x faster per call (237ns -> 52ns). BigInt.toString() delegates to V8 native C++. Same change applied to fix-imports.js (template) and index.js (generated artifact) so future regenerations stay in sync. 2. downloadHistory: stream-pipe createInflate instead of buffering the full compressed payload then inflating. Cuts RSS peak ~50% on 50MB history payloads. 3. processHistoryMessage: drop the { ...chat } shallow copy before pushing to chats[]. The decoded protobuf object is not referenced after the push. 4. downloadEncryptedContent transform: skip Buffer.concat when remainingBytes is empty (the common case — chunks usually arrive AES-aligned). ~19x faster per chunk (78ms -> 4ms over 25k chunks). Tests: - bigint-validation.test.ts (NEW): 32 parameterized cases covering zero, MAX_SAFE_INTEGER boundary, max int64/uint64, negative values, tricky bit patterns and powers of 2; plus 200-iter fuzz comparing old Long vs new BigInt implementations for equivalence. - proto-tojson-long.test.ts: kept the existing fork-only test for string-in-Long-field graceful handling, appended 4 upstream tests covering Long->string fast path, large unsigned > MAX_SAFE_INTEGER, zero/small values and encode/decode roundtrip.
210 lines
7.9 KiB
TypeScript
210 lines
7.9 KiB
TypeScript
import Long from 'long'
|
|
import $protobuf from 'protobufjs/minimal.js'
|
|
|
|
const $util = $protobuf.util
|
|
|
|
// proto implementation
|
|
function longToStringOld(value: any, unsigned?: boolean): string {
|
|
if (typeof value === 'string') return value
|
|
if (typeof value === 'number') return String(value)
|
|
if (!$util.Long) return String(value)
|
|
const normalized = ($util.Long as any).fromValue(value)
|
|
const prepared =
|
|
unsigned && normalized && typeof normalized.toUnsigned === 'function' ? normalized.toUnsigned() : normalized
|
|
return prepared.toString()
|
|
}
|
|
|
|
// bigint implementation
|
|
function longToStringNew(value: any, unsigned?: boolean): string {
|
|
if (typeof value === 'string') return value
|
|
if (typeof value === 'number') return String(value)
|
|
if (value && typeof value.low === 'number' && typeof value.high === 'number') {
|
|
const high = value.high | 0
|
|
const lo = BigInt(value.low >>> 0)
|
|
const hi = BigInt(value.high >>> 0)
|
|
const combined = (hi << 32n) | lo
|
|
if (!unsigned && high < 0) {
|
|
return (combined - (1n << 64n)).toString()
|
|
}
|
|
|
|
return combined.toString()
|
|
}
|
|
|
|
return String(value)
|
|
}
|
|
|
|
function longToNumberOld(value: any, unsigned?: boolean): number {
|
|
if (typeof value === 'number') return value
|
|
if (typeof value === 'string') return Number(value)
|
|
if (!$util.Long) return Number(value)
|
|
const normalized = ($util.Long as any).fromValue(value)
|
|
const prepared =
|
|
unsigned && normalized && typeof normalized.toUnsigned === 'function'
|
|
? normalized.toUnsigned()
|
|
: typeof normalized.toSigned === 'function'
|
|
? normalized.toSigned()
|
|
: normalized
|
|
return prepared.toNumber()
|
|
}
|
|
|
|
function longToNumberNew(value: any, unsigned?: boolean): number {
|
|
if (typeof value === 'number') return value
|
|
if (typeof value === 'string') return Number(value)
|
|
if (value && typeof value.low === 'number' && typeof value.high === 'number') {
|
|
const high = value.high | 0
|
|
const lo = BigInt(value.low >>> 0)
|
|
const hi = BigInt(value.high >>> 0)
|
|
const combined = (hi << 32n) | lo
|
|
if (!unsigned && high < 0) {
|
|
return Number(combined - (1n << 64n))
|
|
}
|
|
|
|
return Number(combined)
|
|
}
|
|
|
|
return Number(value)
|
|
}
|
|
|
|
describe('BigInt vs Long equivalence validation', () => {
|
|
// Test cases: [description, Long value, unsigned flag]
|
|
const testCases: [string, Long, boolean][] = [
|
|
// Basic values
|
|
['zero unsigned', Long.fromNumber(0, true), true],
|
|
['zero signed', Long.fromNumber(0, false), false],
|
|
['one unsigned', Long.fromNumber(1, true), true],
|
|
['one signed', Long.fromNumber(1, false), false],
|
|
|
|
// Typical WhatsApp timestamps (seconds since epoch)
|
|
['timestamp 2023', Long.fromNumber(1700000000, true), true],
|
|
['timestamp 2025', Long.fromNumber(1750000000, true), true],
|
|
['timestamp as signed', Long.fromNumber(1700000000, false), false],
|
|
|
|
// File sizes
|
|
['small file', Long.fromNumber(1024, true), true],
|
|
['medium file 10MB', Long.fromNumber(10485760, true), true],
|
|
['large file 2GB', Long.fromNumber(2147483648, true), true],
|
|
|
|
// Boundary values - 32-bit
|
|
['max int32', Long.fromNumber(2147483647, false), false],
|
|
['min int32', Long.fromNumber(-2147483648, false), false],
|
|
['max uint32', Long.fromNumber(4294967295, true), true],
|
|
|
|
// Around MAX_SAFE_INTEGER
|
|
['MAX_SAFE_INTEGER', Long.fromString('9007199254740991', false), false],
|
|
['MAX_SAFE_INTEGER + 1', Long.fromString('9007199254740992', false), false],
|
|
['MAX_SAFE_INTEGER unsigned', Long.fromString('9007199254740991', true), true],
|
|
|
|
// Large unsigned values
|
|
['large unsigned', Long.fromString('9999999999999999999', true), true],
|
|
['max uint64', Long.fromString('18446744073709551615', true), true],
|
|
['max uint64 - 1', Long.fromString('18446744073709551614', true), true],
|
|
|
|
// Signed negative values
|
|
['negative one', Long.fromNumber(-1, false), false],
|
|
['negative small', Long.fromNumber(-100, false), false],
|
|
['negative large', Long.fromNumber(-2147483648, false), false],
|
|
['min int64', Long.fromString('-9223372036854775808', false), false],
|
|
['max int64', Long.fromString('9223372036854775807', false), false],
|
|
['-1 as signed Long', Long.fromBits(-1, -1, false), false],
|
|
|
|
// Tricky bit patterns
|
|
['high=1, low=0', Long.fromBits(0, 1, true), true],
|
|
['high=0, low=-1 (0xFFFFFFFF)', Long.fromBits(-1, 0, true), true],
|
|
['high=-1, low=-1 unsigned (max uint64)', Long.fromBits(-1, -1, true), true],
|
|
['high=0x7FFFFFFF, low=0xFFFFFFFF (max int64)', Long.fromBits(-1, 0x7fffffff, false), false],
|
|
['high=0x80000000, low=0 (min int64)', Long.fromBits(0, -2147483648, false), false],
|
|
|
|
// Powers of 2
|
|
['2^32', Long.fromString('4294967296', true), true],
|
|
['2^32 signed', Long.fromString('4294967296', false), false],
|
|
['2^48', Long.fromString('281474976710656', true), true],
|
|
['2^63', Long.fromString('9223372036854775808', true), true]
|
|
]
|
|
|
|
describe('longToString equivalence', () => {
|
|
for (const [desc, longVal, unsigned] of testCases) {
|
|
it(`${desc}: Long(${longVal.low}, ${longVal.high}, ${unsigned})`, () => {
|
|
const oldResult = longToStringOld(longVal, unsigned)
|
|
const newResult = longToStringNew(longVal, unsigned)
|
|
expect(newResult).toBe(oldResult)
|
|
})
|
|
}
|
|
})
|
|
|
|
describe('longToNumber equivalence', () => {
|
|
for (const [desc, longVal, unsigned] of testCases) {
|
|
it(`${desc}: Long(${longVal.low}, ${longVal.high}, ${unsigned})`, () => {
|
|
const oldResult = longToNumberOld(longVal, unsigned)
|
|
const newResult = longToNumberNew(longVal, unsigned)
|
|
// For large values, both may lose precision equally (Number limitation)
|
|
// So we compare string representations
|
|
expect(newResult.toString()).toBe(oldResult.toString())
|
|
})
|
|
}
|
|
})
|
|
|
|
describe('non-Long inputs (fallback paths)', () => {
|
|
it('string passthrough', () => {
|
|
expect(longToStringNew('12345')).toBe('12345')
|
|
expect(longToStringNew('0')).toBe('0')
|
|
})
|
|
it('number passthrough', () => {
|
|
expect(longToStringNew(42)).toBe('42')
|
|
expect(longToStringNew(0)).toBe('0')
|
|
expect(longToStringNew(-1)).toBe('-1')
|
|
})
|
|
it('null/undefined fallback', () => {
|
|
expect(longToStringNew(null)).toBe('null')
|
|
expect(longToStringNew(undefined)).toBe('undefined')
|
|
})
|
|
it('object without low/high', () => {
|
|
expect(longToStringNew({ foo: 'bar' })).toBe('[object Object]')
|
|
})
|
|
|
|
// Defensive: a raw {low, high} JSON object can carry `high` as an
|
|
// unsigned 32-bit value (Long.fromBits would normalize via `| 0`,
|
|
// but plain JSON deserialization does not). Without `value.high | 0`
|
|
// the sign-bit check fails and the result is interpreted as unsigned.
|
|
// Upstream Baileys PR #2333 has this latent bug; InfiniteAPI fixes it.
|
|
it('signed sign-bit detection on raw unsigned high (-1 as {low: -1, high: 4294967295})', () => {
|
|
const raw = { low: -1, high: 0xffffffff }
|
|
// signed: -1
|
|
expect(longToStringNew(raw, false)).toBe('-1')
|
|
// unsigned: max uint64
|
|
expect(longToStringNew(raw, true)).toBe('18446744073709551615')
|
|
})
|
|
|
|
it('signed sign-bit detection on raw unsigned high (-4294967296 as {low: 0, high: 4294967295})', () => {
|
|
const raw = { low: 0, high: 0xffffffff }
|
|
// signed: -4294967296 (high word = -1 after normalization)
|
|
expect(longToStringNew(raw, false)).toBe('-4294967296')
|
|
// unsigned: 0xFFFFFFFF00000000 = 18446744069414584320
|
|
expect(longToStringNew(raw, true)).toBe('18446744069414584320')
|
|
})
|
|
})
|
|
|
|
describe('fuzz: random Long values', () => {
|
|
it('100 random unsigned values match', () => {
|
|
for (let i = 0; i < 100; i++) {
|
|
const low = (Math.random() * 0xffffffff) | 0
|
|
const high = (Math.random() * 0xffffffff) | 0
|
|
const longVal = Long.fromBits(low, high, true)
|
|
const oldResult = longToStringOld(longVal, true)
|
|
const newResult = longToStringNew(longVal, true)
|
|
expect(newResult).toBe(oldResult)
|
|
}
|
|
})
|
|
|
|
it('100 random signed values match', () => {
|
|
for (let i = 0; i < 100; i++) {
|
|
const low = (Math.random() * 0xffffffff) | 0
|
|
const high = (Math.random() * 0xffffffff) | 0
|
|
const longVal = Long.fromBits(low, high, false)
|
|
const oldResult = longToStringOld(longVal, false)
|
|
const newResult = longToStringNew(longVal, false)
|
|
expect(newResult).toBe(oldResult)
|
|
}
|
|
})
|
|
})
|
|
})
|