diff --git a/src/rcl_context_bindings.cpp b/src/rcl_context_bindings.cpp index 4a75b305..cb8ca1ec 100644 --- a/src/rcl_context_bindings.cpp +++ b/src/rcl_context_bindings.cpp @@ -73,6 +73,11 @@ Napi::Value Init(const Napi::CallbackInfo& info) { rcl_init(argc, argc > 0 ? argv : nullptr, &init_options, context), rcl_get_error_string().str); + ThrowIfUnparsedROSArgs(env, jsArgv, context->global_arguments); + if (env.IsExceptionPending()) { + return env.Undefined(); + } + THROW_ERROR_IF_NOT_EQUAL( RCL_RET_OK, rcl_logging_configure(&context->global_arguments, &allocator), rcl_get_error_string().str); diff --git a/src/rcl_node_bindings.cpp b/src/rcl_node_bindings.cpp index 8a14c402..5d1b74e6 100644 --- a/src/rcl_node_bindings.cpp +++ b/src/rcl_node_bindings.cpp @@ -193,12 +193,17 @@ Napi::Value CreateNode(const Napi::CallbackInfo& info) { rcl_arguments_t arguments = rcl_get_zero_initialized_arguments(); rcl_ret_t ret = rcl_parse_arguments(argc, argv, rcl_get_default_allocator(), &arguments); - if ((ret != RCL_RET_OK) || HasUnparsedROSArgs(arguments)) { + if (ret != RCL_RET_OK) { Napi::Error::New(env, "failed to parse arguments") .ThrowAsJavaScriptException(); return env.Undefined(); } + ThrowIfUnparsedROSArgs(env, jsArgv, arguments); + if (env.IsExceptionPending()) { + return env.Undefined(); + } + RCPPUTILS_SCOPE_EXIT({ if (RCL_RET_OK != rcl_arguments_fini(&arguments)) { Napi::Error::New(env, "failed to fini arguments") diff --git a/src/rcl_utilities.cpp b/src/rcl_utilities.cpp index d33aeb8e..fc331a52 100644 --- a/src/rcl_utilities.cpp +++ b/src/rcl_utilities.cpp @@ -21,6 +21,8 @@ #include #include +#include +// NOLINTNEXTLINE #include namespace { @@ -344,9 +346,51 @@ void FreeArgs(char** argv, size_t argc) { } } -bool HasUnparsedROSArgs(const rcl_arguments_t& rcl_args) { +void ThrowIfUnparsedROSArgs(Napi::Env env, const Napi::Array& jsArgv, + const rcl_arguments_t& rcl_args) { int unparsed_ros_args_count = rcl_arguments_get_count_unparsed_ros(&rcl_args); - return unparsed_ros_args_count != 0; + + if (unparsed_ros_args_count < 0) { + Napi::Error::New(env, "Failed to count unparsed arguments") + .ThrowAsJavaScriptException(); + return; + } + if (0 == unparsed_ros_args_count) { + return; + } + + rcl_allocator_t allocator = rcl_get_default_allocator(); + int* unparsed_indices_c = nullptr; + rcl_ret_t ret = + rcl_arguments_get_unparsed_ros(&rcl_args, allocator, &unparsed_indices_c); + if (RCL_RET_OK != ret) { + Napi::Error::New(env, "Failed to get unparsed arguments") + .ThrowAsJavaScriptException(); + return; + } + + RCPPUTILS_SCOPE_EXIT({ + allocator.deallocate(unparsed_indices_c, allocator.state); + }); + + std::string unparsed_args_str = "["; + for (int i = 0; i < unparsed_ros_args_count; ++i) { + int index = unparsed_indices_c[i]; + if (index < 0 || static_cast(index) >= jsArgv.Length()) { + Napi::Error::New(env, "Got invalid unparsed ROS arg index") + .ThrowAsJavaScriptException(); + return; + } + std::string arg = jsArgv.Get(index).As().Utf8Value(); + unparsed_args_str += "'" + arg + "'"; + if (i < unparsed_ros_args_count - 1) { + unparsed_args_str += ", "; + } + } + unparsed_args_str += "]"; + + Napi::Error::New(env, "Unknown ROS arguments: " + unparsed_args_str) + .ThrowAsJavaScriptException(); } } // namespace rclnodejs diff --git a/src/rcl_utilities.h b/src/rcl_utilities.h index 5f46c8ff..568824ea 100644 --- a/src/rcl_utilities.h +++ b/src/rcl_utilities.h @@ -63,7 +63,8 @@ char** AbstractArgsFromNapiArray(const Napi::Array& jsArgv); // `AbstractArgsFromNapiArray` and `FreeArgs` must be called in pairs. void FreeArgs(char** argv, size_t argc); -bool HasUnparsedROSArgs(const rcl_arguments_t& rcl_args); +void ThrowIfUnparsedROSArgs(Napi::Env env, const Napi::Array& jsArgv, + const rcl_arguments_t& rcl_args); } // namespace rclnodejs diff --git a/test/test-unparsed-args.js b/test/test-unparsed-args.js new file mode 100644 index 00000000..2aa8da76 --- /dev/null +++ b/test/test-unparsed-args.js @@ -0,0 +1,59 @@ +// Copyright (c) 2025, The Robot Web Tools Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const assert = require('assert'); +const rclnodejs = require('../index.js'); + +describe('rclnodejs unparsed ROS args', function () { + this.timeout(10 * 1000); + + beforeEach(function () { + rclnodejs.shutdown(); + }); + + afterEach(function () { + rclnodejs.shutdown(); + }); + + it('should throw error when initializing context with unknown ROS args', async function () { + const args = ['--ros-args', '--unknown-arg']; + try { + await rclnodejs.init(rclnodejs.Context.defaultContext(), args); + assert.fail('Should have thrown an error'); + } catch (e) { + assert.ok(e.message.includes('Unknown ROS arguments')); + assert.ok(e.message.includes('--unknown-arg')); + } + }); + + it('should throw error when creating node with unknown ROS args', async function () { + await rclnodejs.init(); + const args = ['--ros-args', '--unknown-arg']; + try { + rclnodejs.createNode( + 'test_node', + '', + rclnodejs.Context.defaultContext(), + rclnodejs.NodeOptions.defaultOptions, + args + ); + assert.fail('Should have thrown an error'); + } catch (e) { + assert.ok(e.message.includes('Unknown ROS arguments')); + assert.ok(e.message.includes('--unknown-arg')); + } + }); +});