1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct RemapArg<'a> {
57 pub node: Option<&'a str>,
61 pub from: &'a str,
63 pub to: &'a str,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub struct ParamArg<'a> {
72 pub node: Option<&'a str>,
75 pub name: &'a str,
77 pub value: &'a str,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum RosArg<'a> {
85 Remap(RemapArg<'a>),
87 Param(ParamArg<'a>),
89 ParamsFile(&'a str),
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum RosArgsError<'a> {
97 Unsupported {
100 flag: &'a str,
102 },
103 Unknown {
105 token: &'a str,
107 },
108 MissingRule {
110 flag: &'a str,
112 },
113 InvalidRule {
115 rule: &'a str,
117 },
118 IdentityRemap {
120 rule: &'a str,
122 },
123 MissingValue {
125 flag: &'a str,
127 },
128 InvalidParam {
130 rule: &'a str,
132 },
133}
134
135impl core::fmt::Display for RosArgsError<'_> {
136 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
137 match self {
138 Self::Unsupported { flag } => write!(
139 f,
140 "`{flag}` is a ROS argument nano-ros does not honour here (enclaves and log \
141 configuration have no counterpart; `-p` / `--params-file` are honoured only \
142 where an executor can hold parameter overrides)"
143 ),
144 Self::Unknown { token } => {
145 write!(
146 f,
147 "`{token}` is not a ROS argument (rclcpp refuses it too, as UnknownROSArgsError)"
148 )
149 }
150 Self::MissingRule { flag } => {
151 write!(f, "`{flag}` must be followed by a rule `[node:]from:=to`")
152 }
153 Self::InvalidRule { rule } => {
154 write!(
155 f,
156 "`{rule}` is not a remap rule; expected `[node:]from:=to`"
157 )
158 }
159 Self::MissingValue { flag } => {
160 write!(f, "`{flag}` must be followed by a value")
161 }
162 Self::InvalidParam { rule } => write!(
163 f,
164 "`{rule}` is not a parameter override; expected `[node:]name:=value`"
165 ),
166 Self::IdentityRemap { rule } => write!(
167 f,
168 "`{rule}` remaps a node's identity; the launch file is the authority on a node's \
169 name and namespace (RFC-0046), and an argv rung beneath it is not implemented"
170 ),
171 }
172 }
173}
174
175const UNSUPPORTED_FLAGS: &[&str] = &["-e", "--enclave", "--log-level", "--log-config-file"];
178
179const LOG_SWITCH_SUFFIXES: &[&str] = &["stdout-logs", "rosout-logs", "external-lib-logs"];
182
183fn is_log_switch(token: &str) -> bool {
184 let rest = token
185 .strip_prefix("--enable-")
186 .or_else(|| token.strip_prefix("--disable-"));
187 matches!(rest, Some(s) if LOG_SWITCH_SUFFIXES.contains(&s))
188}
189
190pub fn parse_remap_rule(rule: &str) -> Result<RemapArg<'_>, RosArgsError<'_>> {
192 let invalid = RosArgsError::InvalidRule { rule };
193 let (lhs, to) = rule.split_once(":=").ok_or(invalid)?;
194 if lhs.contains("://") || to.contains("://") {
195 return Err(RosArgsError::Unsupported { flag: rule });
197 }
198 let (node, from) = match lhs.split_once(':') {
199 Some((node, from)) => (Some(node), from),
200 None => (None, lhs),
201 };
202 if from.starts_with("__") {
203 return Err(RosArgsError::IdentityRemap { rule });
204 }
205 let bad = |s: &str| s.is_empty() || s.contains(':') || s.chars().any(char::is_whitespace);
206 if bad(from) || bad(to) {
207 return Err(invalid);
208 }
209 if let Some(n) = node
210 && (bad(n) || n.contains('/') || n.contains('~'))
211 {
212 return Err(invalid);
213 }
214 Ok(RemapArg { node, from, to })
215}
216
217pub fn parse_param_rule(rule: &str) -> Result<ParamArg<'_>, RosArgsError<'_>> {
220 let invalid = RosArgsError::InvalidParam { rule };
221 let (lhs, value) = rule.split_once(":=").ok_or(invalid)?;
222 let (node, name) = match lhs.split_once(':') {
223 Some((node, name)) => (Some(node), name),
224 None => (None, lhs),
225 };
226 let bad = |s: &str| s.is_empty() || s.contains(':') || s.chars().any(char::is_whitespace);
227 if bad(name) {
228 return Err(invalid);
229 }
230 if let Some(n) = node
231 && (bad(n) || n.contains('/') || n.contains('~'))
232 {
233 return Err(invalid);
234 }
235 Ok(ParamArg { node, name, value })
236}
237
238pub fn parse_ros_args<'a, I, F>(args: I, mut on_remap: F) -> Result<(), RosArgsError<'a>>
251where
252 I: IntoIterator<Item = &'a str>,
253 F: FnMut(RemapArg<'a>),
254{
255 parse_impl(args, false, |a| {
256 if let RosArg::Remap(r) = a {
257 on_remap(r);
258 }
259 })
260}
261
262pub fn parse_ros_args_events<'a, I, F>(args: I, on_arg: F) -> Result<(), RosArgsError<'a>>
265where
266 I: IntoIterator<Item = &'a str>,
267 F: FnMut(RosArg<'a>),
268{
269 parse_impl(args, true, on_arg)
270}
271
272fn parse_impl<'a, I, F>(args: I, params: bool, mut on_arg: F) -> Result<(), RosArgsError<'a>>
273where
274 I: IntoIterator<Item = &'a str>,
275 F: FnMut(RosArg<'a>),
276{
277 let mut in_scope = false;
278 let mut it = args.into_iter();
279 while let Some(arg) = it.next() {
280 if arg == "--ros-args" {
281 in_scope = true;
282 continue;
283 }
284 if !in_scope {
285 continue;
286 }
287 match arg {
288 "--" => in_scope = false,
289 "-r" | "--remap" => {
290 let rule = it.next().ok_or(RosArgsError::MissingRule { flag: arg })?;
291 on_arg(RosArg::Remap(parse_remap_rule(rule)?));
292 }
293 "-p" | "--param" | "--params-file" if !params => {
294 return Err(RosArgsError::Unsupported { flag: arg });
295 }
296 "-p" | "--param" => {
297 let rule = it.next().ok_or(RosArgsError::MissingValue { flag: arg })?;
298 on_arg(RosArg::Param(parse_param_rule(rule)?));
299 }
300 "--params-file" => {
301 let path = it.next().ok_or(RosArgsError::MissingValue { flag: arg })?;
302 on_arg(RosArg::ParamsFile(path));
303 }
304 flag if UNSUPPORTED_FLAGS.contains(&flag) || is_log_switch(flag) => {
305 return Err(RosArgsError::Unsupported { flag });
306 }
307 token => return Err(RosArgsError::Unknown { token }),
308 }
309 }
310 Ok(())
311}
312
313#[derive(Debug, Clone, Copy, PartialEq, Eq)]
318pub struct ParamsYamlError<'t> {
319 pub line: usize,
321 pub text: &'t str,
323 pub reason: &'static str,
325}
326
327impl core::fmt::Display for ParamsYamlError<'_> {
328 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
329 write!(f, "line {}: {}: `{}`", self.line, self.reason, self.text)
330 }
331}
332
333pub const MAX_PARAMS_FILE_NAME_LEN: usize = 128;
335const MAX_PARAMS_FILE_DEPTH: usize = 8;
336
337pub fn parse_params_yaml<'t, F>(text: &'t str, mut on_param: F) -> Result<(), ParamsYamlError<'t>>
361where
362 F: for<'n> FnMut(ParamArg<'n>),
363{
364 let mut stack: heapless::Vec<(usize, &'t str), MAX_PARAMS_FILE_DEPTH> = heapless::Vec::new();
365 for (i, raw) in text.lines().enumerate() {
366 let err = |reason| ParamsYamlError {
367 line: i + 1,
368 text: raw.trim(),
369 reason,
370 };
371 let content = yaml_strip_comment(raw);
372 let body = content.trim();
373 if body.is_empty() || body == "---" || body == "..." {
374 continue;
375 }
376 let lead = &content[..content.len() - content.trim_start().len()];
377 if lead.contains('\t') {
378 return Err(err("a tab is not YAML indentation"));
379 }
380 let indent = lead.len();
381 if body == "-" || body.starts_with("- ") {
382 return Err(err("a sequence is not read from a parameter file"));
383 }
384 let (key, rest) =
385 yaml_split_key(body).ok_or_else(|| err("expected `key:` or `key: value`"))?;
386 let key = yaml_unquote(key.trim());
387 while let Some(&(ind, _)) = stack.last() {
388 if ind >= indent {
389 stack.pop();
390 } else {
391 break;
392 }
393 }
394 let rest = rest.trim();
395 if rest.is_empty() {
396 stack
397 .push((indent, key))
398 .map_err(|_| err("nested deeper than a parameter file needs"))?;
399 continue;
400 }
401 if stack.len() < 2 {
402 return Err(err(
403 "a value must sit under `<node>:` then `ros__parameters:`",
404 ));
405 }
406 if stack[1].1 != "ros__parameters" {
407 return Err(err("the level under a node key must be `ros__parameters`"));
408 }
409 if rest.starts_with(['[', '{', '|', '>', '&', '*', '!']) {
410 return Err(err(
411 "an array, inline map, block scalar, anchor or tag is not read from a parameter file",
412 ));
413 }
414 let node = yaml_node_selector(stack[0].1)
415 .ok_or_else(|| err("only the `/**` wildcard is supported"))?;
416 let mut name: heapless::String<MAX_PARAMS_FILE_NAME_LEN> = heapless::String::new();
417 for &(_, group) in &stack[2..] {
418 name.push_str(group)
419 .and_then(|_| name.push('.'))
420 .map_err(|_| err("parameter name longer than MAX_PARAMS_FILE_NAME_LEN"))?;
421 }
422 name.push_str(key)
423 .map_err(|_| err("parameter name longer than MAX_PARAMS_FILE_NAME_LEN"))?;
424 on_param(ParamArg {
425 node,
426 name: &name,
427 value: yaml_unquote(rest),
428 });
429 }
430 Ok(())
431}
432
433fn yaml_strip_comment(line: &str) -> &str {
435 let mut quote = None;
436 let mut prev_space = true;
437 for (i, c) in line.char_indices() {
438 match (quote, c) {
439 (None, '"' | '\'') => quote = Some(c),
440 (Some(q), c) if c == q => quote = None,
441 (None, '#') if prev_space => return &line[..i],
442 _ => {}
443 }
444 prev_space = c.is_whitespace();
445 }
446 line
447}
448
449fn yaml_split_key(body: &str) -> Option<(&str, &str)> {
452 let mut quote = None;
453 let bytes = body.as_bytes();
454 for (i, c) in body.char_indices() {
455 match (quote, c) {
456 (None, '"' | '\'') => quote = Some(c),
457 (Some(q), c) if c == q => quote = None,
458 (None, ':') if i + 1 == bytes.len() || bytes[i + 1] == b' ' => {
459 return Some((&body[..i], &body[i + 1..]));
460 }
461 _ => {}
462 }
463 }
464 None
465}
466
467fn yaml_unquote(s: &str) -> &str {
468 let b = s.as_bytes();
469 if b.len() >= 2 && (b[0] == b'"' || b[0] == b'\'') && b[b.len() - 1] == b[0] {
470 &s[1..s.len() - 1]
471 } else {
472 s
473 }
474}
475
476#[allow(clippy::option_option)]
479fn yaml_node_selector(key: &str) -> Option<Option<&str>> {
480 match key {
481 "/**" | "**" => Some(None),
482 k if k.contains('*') => None,
483 k => Some(Some(k.rsplit('/').next().unwrap_or(k))),
484 }
485}
486
487#[cfg(any(has_rmw, test))]
488pub fn install_argv_remaps<'r, I>(
501 executor: &mut crate::executor::Executor<'_>,
502 rules: I,
503) -> Result<(), RemapArg<'r>>
504where
505 I: IntoIterator<Item = RemapArg<'r>>,
506 I::IntoIter: Clone,
507{
508 use crate::executor::spin::{RemapRule, RemapTier};
509 let rules = rules.into_iter();
510 let start = executor.remap_len;
511 for (i, r) in rules.clone().enumerate() {
512 let mut rule = RemapRule {
513 node_name: heapless::String::new(),
514 namespace: heapless::String::new(),
515 from: heapless::String::new(),
516 to: heapless::String::new(),
517 tier: RemapTier::Argv,
518 };
519 let fits = rule.node_name.push_str(r.node.unwrap_or("")).is_ok()
520 && rule.from.push_str(r.from).is_ok()
521 && rule.to.push_str(r.to).is_ok();
522 let slot = executor.remap_table.get_mut(start + i);
523 match (fits, slot) {
524 (true, Some(slot)) => *slot = Some(rule),
525 _ => {
526 for s in &mut executor.remap_table[start..start + i] {
527 *s = None;
528 }
529 return Err(r);
530 }
531 }
532 }
533 executor.remap_len = start + rules.count();
534 Ok(())
535}
536
537#[cfg(any(has_rmw, test))]
538#[derive(Debug, Clone, Copy, PartialEq, Eq)]
540pub enum ApplyError<'a> {
541 Refused(RosArgsError<'a>),
543 DoesNotFit(RemapArg<'a>),
547}
548
549#[cfg(any(has_rmw, test))]
550impl core::fmt::Display for ApplyError<'_> {
551 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
552 match self {
553 Self::Refused(e) => e.fmt(f),
554 Self::DoesNotFit(r) => write!(
555 f,
556 "remap `{}:={}` does not fit: the executor's runtime remap table holds {} \
557 rules (NROS_EXECUTOR_RUNTIME_REMAPS; 0 on bare metal, which has no argv), \
558 shared with hand-declared rules; `from`/`to` hold {} bytes, a `node:` \
559 prefix 64",
560 r.from,
561 r.to,
562 crate::executor::spin::MAX_REMAPS,
563 crate::names::MAX_RESOLVED_NAME_LEN,
564 ),
565 }
566 }
567}
568
569#[cfg(any(has_rmw, test))]
570#[allow(clippy::absurd_extreme_comparisons)]
581pub fn apply_ros_args<'a, I>(
582 executor: Option<&mut crate::executor::Executor<'_>>,
583 args: I,
584) -> Result<usize, ApplyError<'a>>
585where
586 I: IntoIterator<Item = &'a str>,
587{
588 const N: usize = crate::executor::spin::MAX_REMAPS;
589 let mut rules: [Option<RemapArg<'a>>; N] = [None; N];
590 let mut n = 0usize;
591 let mut overflow = None;
592 parse_ros_args(args, |r| {
593 if n < N {
594 rules[n] = Some(r);
595 n += 1;
596 } else if overflow.is_none() {
597 overflow = Some(r);
598 }
599 })
600 .map_err(ApplyError::Refused)?;
601 if let Some(r) = overflow {
602 return Err(ApplyError::DoesNotFit(r));
603 }
604 if let Some(executor) = executor {
605 install_argv_remaps(executor, rules[..n].iter().flatten().copied())
606 .map_err(ApplyError::DoesNotFit)?;
607 }
608 Ok(n)
609}
610
611#[cfg(all(any(has_rmw, test), feature = "param-store", feature = "alloc"))]
612#[derive(Debug, Clone)]
614pub enum ApplyParamsError<'a> {
615 Remaps(ApplyError<'a>),
617 Unreadable {
619 path: &'a str,
621 why: alloc::string::String,
623 },
624 BadFile {
626 path: &'a str,
628 why: alloc::string::String,
630 },
631 NoStore,
633}
634
635#[cfg(all(any(has_rmw, test), feature = "param-store", feature = "alloc"))]
636impl core::fmt::Display for ApplyParamsError<'_> {
637 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
638 match self {
639 Self::Remaps(e) => e.fmt(f),
640 Self::Unreadable { path, why } => {
641 write!(f, "`--params-file {path}` cannot be read: {why}")
642 }
643 Self::BadFile { path, why } => write!(f, "`--params-file {path}`, {why}"),
644 Self::NoStore => f.write_str(
645 "the parameter store could not be allocated, so the parameter overrides have \
646 nowhere to go",
647 ),
648 }
649 }
650}
651
652#[cfg(all(any(has_rmw, test), feature = "param-store", feature = "alloc"))]
653pub fn apply_ros_args_with_params<'a, I, R>(
664 executor: Option<&mut crate::executor::Executor<'_>>,
665 args: I,
666 mut read_file: R,
667) -> Result<usize, ApplyParamsError<'a>>
668where
669 I: IntoIterator<Item = &'a str> + Clone,
670 R: FnMut(&str) -> Result<alloc::string::String, alloc::string::String>,
671{
672 use alloc::string::String;
673 let mut params: alloc::vec::Vec<(Option<String>, String, String)> = alloc::vec::Vec::new();
676 let mut files: alloc::vec::Vec<&'a str> = alloc::vec::Vec::new();
677 let mut order: alloc::vec::Vec<Result<usize, usize>> = alloc::vec::Vec::new();
678 parse_ros_args_events(args.clone(), |a| match a {
679 RosArg::Param(p) => {
680 order.push(Ok(params.len()));
681 params.push((
682 p.node.map(String::from),
683 String::from(p.name),
684 String::from(p.value),
685 ));
686 }
687 RosArg::ParamsFile(path) => {
688 order.push(Err(files.len()));
689 files.push(path);
690 }
691 RosArg::Remap(_) => {}
692 })
693 .map_err(|e| ApplyParamsError::Remaps(ApplyError::Refused(e)))?;
694 let mut file_params: alloc::vec::Vec<alloc::vec::Vec<(Option<String>, String, String)>> =
696 alloc::vec::Vec::new();
697 for &path in &files {
698 let text = read_file(path).map_err(|why| ApplyParamsError::Unreadable { path, why })?;
699 let mut got = alloc::vec::Vec::new();
700 parse_params_yaml(&text, |p| {
701 got.push((
702 p.node.map(String::from),
703 String::from(p.name),
704 String::from(p.value),
705 ))
706 })
707 .map_err(|e| ApplyParamsError::BadFile {
708 path,
709 why: alloc::format!("{e}"),
710 })?;
711 file_params.push(got);
712 }
713 let remap_args = args.into_iter().scan(false, |skip, a| {
715 if core::mem::take(skip) {
716 return Some(None);
717 }
718 if matches!(a, "-p" | "--param" | "--params-file") {
719 *skip = true;
720 return Some(None);
721 }
722 Some(Some(a))
723 });
724 let remap_args: alloc::vec::Vec<&'a str> = remap_args.flatten().collect();
725 let Some(executor) = executor else {
726 let n =
727 apply_ros_args(None, remap_args.iter().copied()).map_err(ApplyParamsError::Remaps)?;
728 return Ok(n + params.len() + file_params.iter().map(alloc::vec::Vec::len).sum::<usize>());
729 };
730 executor
735 .install_argv_params(core::iter::empty())
736 .map_err(|_| ApplyParamsError::NoStore)?;
737 let n = apply_ros_args(Some(&mut *executor), remap_args.iter().copied())
738 .map_err(ApplyParamsError::Remaps)?;
739 let mut flat: alloc::vec::Vec<&(Option<String>, String, String)> = alloc::vec::Vec::new();
740 for slot in &order {
741 match *slot {
742 Ok(i) => flat.push(¶ms[i]),
743 Err(f) => flat.extend(file_params[f].iter()),
744 }
745 }
746 let m = executor
747 .install_argv_params(flat.iter().map(|(node, name, value)| ParamArg {
748 node: node.as_deref(),
749 name,
750 value,
751 }))
752 .map_err(|_| ApplyParamsError::NoStore)?;
753 Ok(n + m)
754}
755
756#[cfg(any(has_rmw, test))]
757pub fn argv_fallback<'e>(
765 executor: &'e crate::executor::Executor<'_>,
766 node_name: &'e str,
767 namespace: &'e str,
768) -> impl Iterator<Item = (&'e str, &'e str)> + 'e {
769 use crate::executor::spin::{RemapTier, tier_rules};
770 tier_rules(
771 &executor.remap_table[..executor.remap_len],
772 RemapTier::Argv,
773 node_name,
774 namespace,
775 )
776}
777
778#[cfg(test)]
779mod tests {
780 use super::*;
781
782 fn rules<'a>(args: &[&'a str]) -> Result<heapless::Vec<RemapArg<'a>, 8>, RosArgsError<'a>> {
783 let mut out = heapless::Vec::new();
784 parse_ros_args(args.iter().copied(), |r| out.push(r).unwrap())?;
785 Ok(out)
786 }
787
788 fn rule<'a>(node: Option<&'a str>, from: &'a str, to: &'a str) -> RemapArg<'a> {
789 RemapArg { node, from, to }
790 }
791
792 #[test]
793 fn no_scope_reports_nothing_and_leaves_user_arguments_alone() {
794 assert!(
795 rules(&["prog", "-r", "a:=b", "--verbose"])
796 .unwrap()
797 .is_empty()
798 );
799 assert!(rules(&[]).unwrap().is_empty());
800 }
801
802 #[test]
803 fn short_and_long_remap_flags_keep_argv_order() {
804 let got = rules(&[
805 "prog",
806 "--ros-args",
807 "-r",
808 "foo:=bar",
809 "--remap",
810 "bar:=baz",
811 ])
812 .unwrap();
813 assert_eq!(
814 got.as_slice(),
815 &[rule(None, "foo", "bar"), rule(None, "bar", "baz")]
816 );
817 }
818
819 #[test]
820 fn a_node_prefix_scopes_the_rule() {
821 let got = rules(&["--ros-args", "-r", "alice:foo:=~/bar"]).unwrap();
822 assert_eq!(got.as_slice(), &[rule(Some("alice"), "foo", "~/bar")]);
823 }
824
825 #[test]
826 fn double_dash_closes_the_scope_and_a_second_scope_reopens_it() {
827 let got = rules(&[
828 "--ros-args",
829 "-r",
830 "a:=b",
831 "--",
832 "-r",
833 "user:=arg",
834 "--ros-args",
835 "-r",
836 "c:=d",
837 ])
838 .unwrap();
839 assert_eq!(
840 got.as_slice(),
841 &[rule(None, "a", "b"), rule(None, "c", "d")]
842 );
843 }
844
845 #[test]
846 fn parameter_overrides_are_refused_by_name() {
847 for flag in ["-p", "--param", "--params-file"] {
848 assert_eq!(
849 rules(&["--ros-args", flag, "x:=1"]),
850 Err(RosArgsError::Unsupported { flag })
851 );
852 }
853 }
854
855 #[test]
856 fn enclave_and_log_flags_are_refused_by_name() {
857 for flag in [
858 "-e",
859 "--enclave",
860 "--log-level",
861 "--log-config-file",
862 "--disable-rosout-logs",
863 ] {
864 assert_eq!(
865 rules(&["--ros-args", flag]),
866 Err(RosArgsError::Unsupported { flag })
867 );
868 }
869 }
870
871 #[test]
872 fn an_unknown_token_inside_a_scope_is_refused_not_skipped() {
873 assert_eq!(
874 rules(&["--ros-args", "-r", "a:=b", "--frobnicate"]),
875 Err(RosArgsError::Unknown {
876 token: "--frobnicate"
877 })
878 );
879 assert_eq!(
881 rules(&["--ros-args", "--enable-everything"]),
882 Err(RosArgsError::Unknown {
883 token: "--enable-everything"
884 })
885 );
886 }
887
888 #[test]
889 fn a_remap_flag_needs_a_valid_rule_after_it() {
890 assert_eq!(
891 rules(&["--ros-args", "-r"]),
892 Err(RosArgsError::MissingRule { flag: "-r" })
893 );
894 for bad in [
895 "nocolon", ":=b", "a:=", "a b:=c", ":a:=b", "n/s:a:=b", "a:=b:c",
896 ] {
897 assert_eq!(
898 rules(&["--ros-args", "--remap", bad]),
899 Err(RosArgsError::InvalidRule { rule: bad }),
900 "{bad}"
901 );
902 }
903 }
904
905 #[test]
906 fn identity_remaps_are_refused() {
907 for r in ["__node:=x", "__name:=x", "__ns:=/x", "talker:__ns:=/x"] {
908 assert_eq!(
909 rules(&["--ros-args", "-r", r]),
910 Err(RosArgsError::IdentityRemap { rule: r })
911 );
912 }
913 }
914
915 #[cfg(all(feature = "alloc", not(feature = "rmw-cffi")))]
926 mod chain {
927 use super::super::*;
928 extern crate std;
929 use crate::{executor::Executor, mock::MockSession};
930
931 fn executor() -> Executor<'static> {
932 Executor::from_session(MockSession::new())
933 }
934
935 fn install_argv(exec: &mut Executor<'_>, argv: &[&'static str]) {
938 let mut parsed: std::vec::Vec<RemapArg<'static>> = std::vec::Vec::new();
939 parse_ros_args(argv.iter().copied(), |r| parsed.push(r)).expect("argv parses");
940 install_argv_remaps(exec, parsed.iter().copied()).expect("rules fit");
941 }
942
943 fn resolve(exec: &Executor<'_>, node: &str, source: &str) -> std::string::String {
944 std::string::String::from(
945 exec.resolve_entity_name_for(node, "/", source)
946 .unwrap()
947 .as_str(),
948 )
949 }
950
951 #[test]
952 fn a_launch_rule_wins_over_argv_even_when_argv_was_installed_first() {
953 let mut exec = executor();
957 install_argv(
958 &mut exec,
959 &["prog", "--ros-args", "-r", "chatter:=/from_argv"],
960 );
961 exec.declare_remap("talker", "/", "chatter", "/from_launch")
962 .unwrap();
963 assert_eq!(resolve(&exec, "talker", "chatter"), "/from_launch");
964 }
965
966 #[test]
967 fn an_argv_rule_applies_where_the_launch_projected_none() {
968 let mut exec = executor();
969 install_argv(
970 &mut exec,
971 &["prog", "--ros-args", "-r", "chatter:=/from_argv"],
972 );
973 exec.declare_remap("talker", "/", "chatter", "/from_launch")
974 .unwrap();
975 assert_eq!(resolve(&exec, "listener", "chatter"), "/from_argv");
977 assert_eq!(resolve(&exec, "talker", "other"), "/other");
979 }
980
981 static LAUNCH: [crate::executor::LaunchRemap; 1] = [crate::executor::LaunchRemap::new(
984 c"talker",
985 c"/",
986 c"chatter",
987 c"/from_flash",
988 )];
989
990 #[test]
991 fn a_flash_launch_rule_wins_over_argv() {
992 let mut exec = executor();
993 install_argv(
994 &mut exec,
995 &["prog", "--ros-args", "-r", "chatter:=/from_argv"],
996 );
997 exec.set_launch_remaps(&LAUNCH);
998 assert_eq!(resolve(&exec, "talker", "chatter"), "/from_flash");
999 assert_eq!(resolve(&exec, "listener", "chatter"), "/from_argv");
1001 }
1002
1003 #[test]
1004 fn a_flash_launch_rule_takes_no_runtime_slot() {
1005 let mut exec = executor();
1006 exec.set_launch_remaps(&LAUNCH);
1007 assert_eq!(exec.remap_len, 0);
1008 assert_eq!(resolve(&exec, "talker", "chatter"), "/from_flash");
1009 }
1010
1011 #[test]
1012 fn a_node_prefixed_argv_rule_reaches_only_that_node() {
1013 let mut exec = executor();
1014 install_argv(
1015 &mut exec,
1016 &["--ros-args", "-r", "talker:chatter:=/only_talker"],
1017 );
1018 assert_eq!(resolve(&exec, "talker", "chatter"), "/only_talker");
1019 assert_eq!(resolve(&exec, "listener", "chatter"), "/chatter");
1020 }
1021
1022 #[test]
1023 fn within_argv_the_first_rule_wins() {
1024 let mut exec = executor();
1026 install_argv(
1027 &mut exec,
1028 &["--ros-args", "-r", "foo:=bar", "-r", "bar:=baz"],
1029 );
1030 assert_eq!(resolve(&exec, "n", "foo"), "/bar");
1031 }
1032
1033 #[test]
1034 fn argv_fallback_yields_only_argv_rules_for_the_named_node() {
1035 let mut exec = executor();
1037 install_argv(
1038 &mut exec,
1039 &["--ros-args", "-r", "a:=/x", "-r", "other:b:=/y"],
1040 );
1041 exec.declare_remap("talker", "/", "a", "/launch").unwrap();
1042 let got: std::vec::Vec<_> = argv_fallback(&exec, "talker", "/").collect();
1043 assert_eq!(got, [("a", "/x")]);
1044 }
1045
1046 #[test]
1047 fn install_is_all_or_nothing() {
1048 let mut exec = executor();
1049 let free = exec.remap_table.len();
1050 for i in 0..free - 1 {
1051 let from = std::format!("/f{i}");
1052 exec.declare_remap("n", "/", &from, "/t").unwrap();
1053 }
1054 let before = exec.remap_len;
1055 let rules = [
1056 RemapArg {
1057 node: None,
1058 from: "fits",
1059 to: "/a",
1060 },
1061 RemapArg {
1062 node: None,
1063 from: "does_not",
1064 to: "/b",
1065 },
1066 ];
1067 assert_eq!(install_argv_remaps(&mut exec, rules), Err(rules[1]));
1068 assert_eq!(
1069 exec.remap_len, before,
1070 "a refused install leaves no rule behind"
1071 );
1072 assert_eq!(resolve(&exec, "n", "fits"), "/fits");
1073 }
1074 }
1075
1076 #[test]
1077 fn kind_qualified_remaps_are_refused() {
1078 let r = "rostopic://foo:=bar";
1079 assert_eq!(
1080 rules(&["--ros-args", "-r", r]),
1081 Err(RosArgsError::Unsupported { flag: r })
1082 );
1083 }
1084
1085 #[test]
1086 fn apply_validates_without_an_executor_and_installs_nothing() {
1087 let ok = [
1088 "prog",
1089 "--ros-args",
1090 "-r",
1091 "a:=b",
1092 "-r",
1093 "n:c:=d",
1094 "--",
1095 "x",
1096 ];
1097 assert_eq!(apply_ros_args(None, ok.iter().copied()), Ok(2));
1098 assert_eq!(
1099 apply_ros_args(None, ["prog", "plain"].iter().copied()),
1100 Ok(0)
1101 );
1102 }
1103
1104 #[test]
1105 fn apply_refuses_by_name_what_the_parse_refuses() {
1106 let bad = ["prog", "--ros-args", "-r", "a:=b", "-p", "x:=1"];
1107 assert_eq!(
1108 apply_ros_args(None, bad.iter().copied()),
1109 Err(ApplyError::Refused(RosArgsError::Unsupported {
1110 flag: "-p"
1111 }))
1112 );
1113 }
1114
1115 #[test]
1116 fn apply_refuses_more_rules_than_the_table_can_ever_hold() {
1117 const N: usize = crate::executor::spin::MAX_REMAPS;
1118 let mut args: heapless::Vec<&str, { 2 + 2 * (N + 1) }> = heapless::Vec::new();
1119 args.extend_from_slice(&["prog", "--ros-args"]).unwrap();
1120 for _ in 0..=N {
1121 args.extend_from_slice(&["-r", "a:=b"]).unwrap();
1122 }
1123 assert!(matches!(
1124 apply_ros_args(None, args.iter().copied()),
1125 Err(ApplyError::DoesNotFit(_))
1126 ));
1127 }
1128}
1129
1130#[cfg(all(test, feature = "alloc"))]
1133mod param_tests {
1134 use super::*;
1135
1136 fn events<'a>(args: &[&'a str]) -> Result<heapless::Vec<RosArg<'a>, 8>, RosArgsError<'a>> {
1137 let mut out = heapless::Vec::new();
1138 parse_ros_args_events(args.iter().copied(), |a| out.push(a).unwrap())?;
1139 Ok(out)
1140 }
1141
1142 #[test]
1143 fn p_and_params_file_are_events_in_argv_order() {
1144 let got = events(&[
1145 "prog",
1146 "--ros-args",
1147 "-p",
1148 "a:=1",
1149 "--params-file",
1150 "x.yaml",
1151 "-r",
1152 "f:=t",
1153 "--param",
1154 "n:b:=two",
1155 ])
1156 .unwrap();
1157 assert_eq!(got.len(), 4);
1158 assert_eq!(
1159 got[0],
1160 RosArg::Param(ParamArg {
1161 node: None,
1162 name: "a",
1163 value: "1"
1164 })
1165 );
1166 assert_eq!(got[1], RosArg::ParamsFile("x.yaml"));
1167 assert!(matches!(got[2], RosArg::Remap(_)));
1168 assert_eq!(
1169 got[3],
1170 RosArg::Param(ParamArg {
1171 node: Some("n"),
1172 name: "b",
1173 value: "two"
1174 })
1175 );
1176 }
1177
1178 #[test]
1179 fn the_remap_only_parse_still_refuses_parameter_flags() {
1180 let r = parse_ros_args(["prog", "--ros-args", "-p", "a:=1"], |_| {});
1181 assert_eq!(r, Err(RosArgsError::Unsupported { flag: "-p" }));
1182 let r = parse_ros_args(["prog", "--ros-args", "--params-file", "x"], |_| {});
1183 assert_eq!(
1184 r,
1185 Err(RosArgsError::Unsupported {
1186 flag: "--params-file"
1187 })
1188 );
1189 }
1190
1191 #[test]
1192 fn malformed_overrides_are_refused_by_name() {
1193 assert_eq!(
1194 events(&["p", "--ros-args", "-p"]).unwrap_err(),
1195 RosArgsError::MissingValue { flag: "-p" }
1196 );
1197 assert_eq!(
1198 events(&["p", "--ros-args", "-p", "novalue"]).unwrap_err(),
1199 RosArgsError::InvalidParam { rule: "novalue" }
1200 );
1201 assert_eq!(
1202 events(&["p", "--ros-args", "--params-file"]).unwrap_err(),
1203 RosArgsError::MissingValue {
1204 flag: "--params-file"
1205 }
1206 );
1207 assert_eq!(
1209 events(&["p", "--ros-args", "-p", "label:="]).unwrap()[0],
1210 RosArg::Param(ParamArg {
1211 node: None,
1212 name: "label",
1213 value: ""
1214 })
1215 );
1216 }
1217
1218 fn yaml(
1219 text: &str,
1220 ) -> Result<
1221 alloc::vec::Vec<(
1222 Option<alloc::string::String>,
1223 alloc::string::String,
1224 alloc::string::String,
1225 )>,
1226 alloc::string::String,
1227 > {
1228 let mut out = alloc::vec::Vec::new();
1229 parse_params_yaml(text, |p| {
1230 out.push((
1231 p.node.map(alloc::string::String::from),
1232 alloc::string::String::from(p.name),
1233 alloc::string::String::from(p.value),
1234 ))
1235 })
1236 .map_err(|e| alloc::format!("{e}"))?;
1237 Ok(out)
1238 }
1239
1240 #[test]
1241 fn a_params_file_reports_dotted_names_per_node() {
1242 let got = yaml("/**:\n ros__parameters:\n a: 1 # c\n g:\n b: 'x # not a comment'\n/ns/talker:\n ros__parameters:\n c: true\n").unwrap();
1243 assert_eq!(got.len(), 3);
1244 assert_eq!(got[0], (None, "a".into(), "1".into()));
1245 assert_eq!(got[1], (None, "g.b".into(), "x # not a comment".into()));
1246 assert_eq!(got[2], (Some("talker".into()), "c".into(), "true".into()));
1247 }
1248
1249 #[test]
1250 fn what_the_subset_does_not_read_is_refused_with_its_line() {
1251 let e = yaml("/**:\n ros__parameters:\n xs: [1, 2]\n").unwrap_err();
1252 assert!(e.starts_with("line 3:"), "{e}");
1253 let e = yaml("/**:\n params:\n a: 1\n").unwrap_err();
1254 assert!(e.contains("ros__parameters"), "{e}");
1255 let e = yaml("/*/x:\n ros__parameters:\n a: 1\n").unwrap_err();
1256 assert!(e.contains("wildcard"), "{e}");
1257 let e = yaml("/**:\n\tros__parameters:\n").unwrap_err();
1258 assert!(e.contains("tab"), "{e}");
1259 let e = yaml("/**:\n ros__parameters:\n - a\n").unwrap_err();
1260 assert!(e.contains("sequence"), "{e}");
1261 }
1262}