Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,80 @@ 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) => {
const num = Number(number);
// If the number is not safe, convert it to a string
if (!Number.isSafeInteger(num)) {
Copy link

Copilot AI Oct 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Converting the string to Number before checking if it's safe defeats the purpose of the check. For integers beyond Number.MAX_SAFE_INTEGER, Number(number) will lose precision during conversion (e.g., Number('9007199254740993') becomes 9007199254740992), making the subsequent safety check unreliable. Instead, compare the absolute value of the integer string against Number.MAX_SAFE_INTEGER without conversion, or use BigInt for comparison.

Suggested change
const num = Number(number);
// If the number is not safe, convert it to a string
if (!Number.isSafeInteger(num)) {
// Use BigInt to check if the integer is outside the safe range
const absBigInt = BigInt(number.startsWith('-') ? number.slice(1) : number);
if (absBigInt > BigInt(Number.MAX_SAFE_INTEGER)) {

Copilot uses AI. Check for mistakes.
// Preserve the prefix character (: or , or [)
const prefix = match.charAt(0);
return `${prefix} "${number}"`;
}
return match;
Comment on lines 359 to 378
Copy link

Copilot AI Oct 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The regex pattern requires a preceding character (:, ,, or [) which means it won't match integers at the start of the JSON string or after an opening brace {. For example, {\"value\": 9007199254740992} would match, but [9007199254740992] or {9007199254740992} (if this were valid JSON) would not match the integer at the start. Consider using a lookbehind assertion or adjust the logic to handle integers that appear immediately after {.

Suggested change
/(?:[:,[])\s*(-?\d+)(?=\s*[,}\]])/g,
(match, number) => {
const num = Number(number);
// If the number is not safe, convert it to a string
if (!Number.isSafeInteger(num)) {
// Preserve the prefix character (: or , or [)
const prefix = match.charAt(0);
return `${prefix} "${number}"`;
}
return match;
/(?<=^|[:,\[{])\s*(-?\d+)(?=\s*[,}\]])/g,
(numberMatch, number, offset, string) => {
const num = Number(number);
// If the number is not safe, convert it to a string
if (!Number.isSafeInteger(num)) {
// Find the prefix character (if any)
const prefixMatch = string.slice(Math.max(0, offset - 1), offset);
const prefix = /[:,\[{]/.test(prefixMatch) ? prefixMatch : '';
return `${prefix} "${number}"`;
}
return numberMatch;

Copilot uses AI. Check for mistakes.
}
);

return JSON.parse(processed);
}

module.exports = {
// General utilities
detectUbuntuCodename,
Expand All @@ -320,5 +394,7 @@ module.exports = {
removeSync,
readJsonSync,

// Version and JSON utilities
compareVersions,
parseJSONWithBigInt,
};
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
31 changes: 2 additions & 29 deletions rosidl_parser/rosidl_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 = [
Expand All @@ -82,7 +55,7 @@ const rosidlParser = {
)
);
} else {
resolve(this._parseJSONObject(stdout));
resolve(parseJSONWithBigInt(stdout));
}
}
);
Expand Down
Loading