fix(waproto): normalize high to int32 + tighten roundtrip test
Addresses 2 review comments on PR #385: 1. CodeRabbit Major — longToString/longToNumber checked `value.high < 0` on the raw input. When a Long-like object carries high as an unsigned 32-bit value (e.g. raw {low, high} from JSON deserialization, not constructed via Long.fromBits/fromValue which normalize via `| 0`), the sign-bit detection fails and the value is interpreted as unsigned. Example: {low: 0, high: 0xFFFFFFFF} signed should be -4294967296 but returned 18446744069414584320. Fix: `const high = value.high | 0` before the sign check. Applied to both helpers in WAProto/index.js (generated artifact) and WAProto/fix-imports.js (template). Diverges from upstream Baileys PR #2333 which has the same latent bug. Added 2 test cases in bigint-validation.test.ts for the raw {low, high} scenario (Long.fromBits would mask the bug because it normalizes internally). 2. CodeRabbit Nitpick — the encode/decode roundtrip test in proto-tojson-long.test.ts used safe integers (1700000000 and 123456789), which would silently pass even if longToString fell back to Number(value). Switched to uint64 values above MAX_SAFE_INTEGER (max uint64 and 9999999999999999999) so the roundtrip actually exercises the BigInt fast path. Test suite: 789/789 passing (+2 cases for the unsigned-high scenario). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,10 +19,11 @@ 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 && value.high < 0) {
|
||||
if (!unsigned && high < 0) {
|
||||
return (combined - (1n << 64n)).toString()
|
||||
}
|
||||
|
||||
@@ -50,10 +51,11 @@ 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 && value.high < 0) {
|
||||
if (!unsigned && high < 0) {
|
||||
return Number(combined - (1n << 64n))
|
||||
}
|
||||
|
||||
@@ -158,6 +160,27 @@ describe('BigInt vs Long equivalence validation', () => {
|
||||
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', () => {
|
||||
|
||||
@@ -90,17 +90,21 @@ describe('proto serialization', () => {
|
||||
expect(json.message?.imageMessage?.fileLength).toBe('1')
|
||||
})
|
||||
|
||||
it('roundtrips encode/decode with Long fields preserving values', () => {
|
||||
it('roundtrips encode/decode with Long fields preserving values (> MAX_SAFE_INTEGER)', () => {
|
||||
// Use uint64 values above Number.MAX_SAFE_INTEGER (2^53 - 1) so the
|
||||
// roundtrip actually exercises the BigInt fast path on decode->toJSON.
|
||||
// Safe integers would silently work even if longToString fell back to
|
||||
// Number(value).
|
||||
const original = proto.WebMessageInfo.fromObject({
|
||||
key: {
|
||||
remoteJid: '123@s.whatsapp.net',
|
||||
id: 'ABC123',
|
||||
fromMe: false
|
||||
},
|
||||
messageTimestamp: 1700000000,
|
||||
messageTimestamp: Long.fromString('18446744073709551615', true),
|
||||
message: {
|
||||
imageMessage: {
|
||||
fileLength: 123456789
|
||||
fileLength: Long.fromString('9999999999999999999', true)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -109,7 +113,7 @@ describe('proto serialization', () => {
|
||||
const decoded = proto.WebMessageInfo.decode(encoded)
|
||||
const json = decoded.toJSON()
|
||||
|
||||
expect(json.messageTimestamp).toBe('1700000000')
|
||||
expect(json.message?.imageMessage?.fileLength).toBe('123456789')
|
||||
expect(json.messageTimestamp).toBe('18446744073709551615')
|
||||
expect(json.message?.imageMessage?.fileLength).toBe('9999999999999999999')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user