diff --git a/lib/utils.js b/lib/utils.js index e851c84d..7bd17ae0 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -298,6 +298,90 @@ function compareVersions(version1, version2, operator) { } } +// Feature detection for JSON.parse context parameter support +// Test once at module load time to avoid repeated checks +let isContextSupported = false; +try { + // Try to use context parameter with a simple test + JSON.parse('{"test":1}', (key, value, context) => { + if (context && context.source !== undefined) { + isContextSupported = true; + } + return value; + }); +} catch { + // Context parameter not supported, will use regex fallback + isContextSupported = false; +} + +/** + * Parse JSON string with support for large integers (beyond Number.MAX_SAFE_INTEGER). + * Large integers are automatically converted to strings to preserve precision. + * + * Uses feature detection to determine the best parsing strategy: + * - If context parameter is supported (Node.js >= 21.0.0, modern runtimes): + * Uses native JSON.parse() with context parameter for optimal performance + * - Otherwise (older environments): + * Preprocesses JSON string with regex to convert unsafe integers before parsing + * + * @param {string} str - JSON string to parse + * @returns {any} Parsed JSON object with unsafe integers as strings + * + * @example + * parseJSONWithBigInt('{"value": 9007199254740992}') + * // Returns: { value: '9007199254740992' } + * + * parseJSONWithBigInt('{"value": 100}') + * // Returns: { value: 100 } + */ +function parseJSONWithBigInt(str) { + // For environments supporting context parameter, use it + // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse + if (isContextSupported) { + return JSON.parse(str, (key, value, context) => { + if ( + Number.isInteger(value) && + !Number.isSafeInteger(Number(context.source)) + ) { + return context.source; + } + return value; + }); + } + + // For environments without context parameter support, preprocess the JSON string + // to convert large integers (outside the safe integer range) to strings before parsing. + // + // The regex matches integers that are actual JSON values, not inside strings. + // It matches after: colon (:), comma (,), or open bracket ([) + // and before: comma (,), close brace (}), or close bracket (]) + const processed = str.replace( + /(?:[:,[])\s*(-?\d+)(?=\s*[,}\]])/g, + (match, number) => { + // Use BigInt to check if the number is safe without losing precision + // BigInt can accurately represent arbitrarily large integers + try { + const bigIntValue = BigInt(number); + // Check if the value is outside the safe integer range + if ( + bigIntValue < BigInt(Number.MIN_SAFE_INTEGER) || + bigIntValue > BigInt(Number.MAX_SAFE_INTEGER) + ) { + // Preserve the prefix character (: or , or [) + const prefix = match.charAt(0); + return `${prefix} "${number}"`; + } + } catch { + // If BigInt conversion fails, keep the original match + return match; + } + return match; + } + ); + + return JSON.parse(processed); +} + module.exports = { // General utilities detectUbuntuCodename, @@ -320,5 +404,7 @@ module.exports = { removeSync, readJsonSync, + // Version and JSON utilities compareVersions, + parseJSONWithBigInt, }; diff --git a/package.json b/package.json index 090893bb..c420092f 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,6 @@ "@rclnodejs/ref-struct-di": "^1.1.1", "bindings": "^1.5.0", "debug": "^4.4.0", - "json-bigint": "^1.0.0", "node-addon-api": "^8.3.1", "walk": "^2.3.15" }, diff --git a/rosidl_parser/rosidl_parser.js b/rosidl_parser/rosidl_parser.js index 04e2e52b..e921bde5 100644 --- a/rosidl_parser/rosidl_parser.js +++ b/rosidl_parser/rosidl_parser.js @@ -14,20 +14,12 @@ 'use strict'; -const { compareVersions } = require('../lib/utils.js'); +const { compareVersions, parseJSONWithBigInt } = require('../lib/utils'); const path = require('path'); const execFile = require('child_process').execFile; const pythonExecutable = require('./py_utils').getPythonExecutable('python3'); -const contextSupportedVersion = '21.0.0.0'; -const currentVersion = process.version; -const isContextSupported = compareVersions( - currentVersion.substring(1, currentVersion.length), - contextSupportedVersion, - '>=' -); - const rosidlParser = { parseMessageFile(packageName, filePath) { return this._parseFile('parse_message_file', packageName, filePath); @@ -41,25 +33,6 @@ const rosidlParser = { return this._parseFile('parse_action_file', packageName, filePath); }, - _parseJSONObject(str) { - // For nodejs >= `contextSupportedVersion`, we leverage context parameter to - // convert unsafe integer to string, otherwise, json-bigint is used. - // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse - if (isContextSupported) { - return JSON.parse(str, (key, value, context) => { - if ( - Number.isInteger(value) && - !Number.isSafeInteger(Number(context.source)) - ) { - return context.source; - } - return value; - }); - } - const JSONbigString = require('json-bigint')({ storeAsString: true }); - return JSONbigString.parse(str); - }, - _parseFile(command, packageName, filePath) { return new Promise((resolve, reject) => { const args = [ @@ -82,7 +55,7 @@ const rosidlParser = { ) ); } else { - resolve(this._parseJSONObject(stdout)); + resolve(parseJSONWithBigInt(stdout)); } } );