Files
InfiniteAPI/WAProto/fix-imports.js
T
Renato Alcara f0ee205f96 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>
2026-04-25 15:06:16 -03:00

90 lines
3.9 KiB
JavaScript

import { readFileSync, writeFileSync } from 'fs';
import { exit } from 'process';
const filePath = './index.js'
try {
let content = readFileSync(filePath, 'utf8')
content = content.replace(/import \* as (\$protobuf) from/g, 'import $1 from')
content = content.replace(/(['"])protobufjs\/minimal(['"])/g, '$1protobufjs/minimal.js$2')
const marker = 'const $root = $protobuf.roots["default"] || ($protobuf.roots["default"] = {});\n\n'
const longToStringHelper =
'function longToString(value, unsigned) {\n' +
'\tif (typeof value === "string") {\n' +
'\t\treturn value;\n' +
'\t}\n' +
'\tif (typeof value === "number") {\n' +
'\t\treturn String(value);\n' +
'\t}\n' +
'\t// Fast path: convert Long {low, high} directly via native BigInt\n' +
'\t// BigInt.toString() is a native C++ operation, much faster than Long\'s pure JS division loops\n' +
'\tif (value && typeof value.low === "number" && typeof value.high === "number") {\n' +
'\t\t// Normalize high to signed int32 so the sign-bit check works for inputs\n' +
'\t\t// where high is stored unsigned (e.g. raw {low, high} JSON).\n' +
'\t\tconst high = value.high | 0;\n' +
'\t\tconst lo = BigInt(value.low >>> 0);\n' +
'\t\tconst hi = BigInt(value.high >>> 0);\n' +
'\t\tconst combined = (hi << 32n) | lo;\n' +
'\t\tif (!unsigned && high < 0) {\n' +
'\t\t\treturn (combined - (1n << 64n)).toString();\n' +
'\t\t}\n' +
'\t\treturn combined.toString();\n' +
'\t}\n' +
'\treturn String(value);\n' +
'}\n\n'
const longToNumberHelper =
'function longToNumber(value, unsigned) {\n' +
'\tif (typeof value === "number") {\n' +
'\t\treturn value;\n' +
'\t}\n' +
'\tif (typeof value === "string") {\n' +
'\t\treturn Number(value);\n' +
'\t}\n' +
'\t// Fast path: convert Long {low, high} directly via native BigInt\n' +
'\tif (value && typeof value.low === "number" && typeof value.high === "number") {\n' +
'\t\tconst high = value.high | 0;\n' +
'\t\tconst lo = BigInt(value.low >>> 0);\n' +
'\t\tconst hi = BigInt(value.high >>> 0);\n' +
'\t\tconst combined = (hi << 32n) | lo;\n' +
'\t\tif (!unsigned && high < 0) {\n' +
'\t\t\treturn Number(combined - (1n << 64n));\n' +
'\t\t}\n' +
'\t\treturn Number(combined);\n' +
'\t}\n' +
'\treturn Number(value);\n' +
'}\n\n'
if (!content.includes('function longToString(')) {
const markerIndex = content.indexOf(marker)
if (markerIndex === -1) {
throw new Error('Unable to inject Long helpers: marker not found in WAProto index output')
}
content = content.replace(marker, `${marker}${longToStringHelper}${longToNumberHelper}`)
} else {
const longToStringRegex = /function longToString\(value, unsigned\) {\n[\s\S]*?\n}\n\n/
const longToNumberRegex = /function longToNumber\(value, unsigned\) {\n[\s\S]*?\n}\n\n/
if (!longToStringRegex.test(content) || !longToNumberRegex.test(content)) {
throw new Error('Unable to update Long helpers: existing definitions not found')
}
content = content.replace(longToStringRegex, longToStringHelper)
content = content.replace(longToNumberRegex, longToNumberHelper)
}
const longPattern = /([ \t]+d\.(\w+) = )o\.longs === String \? \$util\.Long\.prototype\.toString\.call\(m\.\2\) : o\.longs === Number \? new \$util\.LongBits\(m\.\2\.low >>> 0, m\.\2\.high >>> 0\)\.toNumber\((true)?\) : m\.\2;/g
content = content.replace(longPattern, (_match, prefix, field, unsignedFlag) => {
const unsignedArg = unsignedFlag ? ', true' : ''
return `${prefix}o.longs === String ? longToString(m.${field}${unsignedArg}) : o.longs === Number ? longToNumber(m.${field}${unsignedArg}) : m.${field};`
})
writeFileSync(filePath, content, 'utf8')
console.log(`✅ Fixed imports in ${filePath}`)
} catch (error) {
console.error(`❌ Error fixing imports: ${error.message}`)
exit(1)
}