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>(
500 executor: &mut crate::executor::Executor<'_>,
501 rules: I,
502) -> Result<(), RemapArg<'r>>
503where
504 I: IntoIterator<Item = RemapArg<'r>>,
505 I::IntoIter: Clone,
506{
507 use crate::executor::spin::{RemapRule, RemapTier};
508 let rules = rules.into_iter();
509 let start = executor.remap_len;
510 for (i, r) in rules.clone().enumerate() {
511 let mut rule = RemapRule {
512 node_name: heapless::String::new(),
513 namespace: heapless::String::new(),
514 from: heapless::String::new(),
515 to: heapless::String::new(),
516 tier: RemapTier::Argv,
517 };
518 let fits = rule.node_name.push_str(r.node.unwrap_or("")).is_ok()
519 && rule.from.push_str(r.from).is_ok()
520 && rule.to.push_str(r.to).is_ok();
521 let slot = executor.remap_table.get_mut(start + i);
522 match (fits, slot) {
523 (true, Some(slot)) => *slot = Some(rule),
524 _ => {
525 for s in &mut executor.remap_table[start..start + i] {
526 *s = None;
527 }
528 return Err(r);
529 }
530 }
531 }
532 executor.remap_len = start + rules.count();
533 Ok(())
534}
535
536#[cfg(any(has_rmw, test))]
537#[derive(Debug, Clone, Copy, PartialEq, Eq)]
539pub enum ApplyError<'a> {
540 Refused(RosArgsError<'a>),
542 DoesNotFit(RemapArg<'a>),
545}
546
547#[cfg(any(has_rmw, test))]
548impl core::fmt::Display for ApplyError<'_> {
549 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
550 match self {
551 Self::Refused(e) => e.fmt(f),
552 Self::DoesNotFit(r) => write!(
553 f,
554 "remap `{}:={}` does not fit: the executor's remap table holds {} rules, \
555 shared with the launch file's; `from`/`to` hold {} bytes, a `node:` \
556 prefix 64",
557 r.from,
558 r.to,
559 crate::executor::spin::MAX_REMAPS,
560 crate::names::MAX_RESOLVED_NAME_LEN,
561 ),
562 }
563 }
564}
565
566#[cfg(any(has_rmw, test))]
567pub fn apply_ros_args<'a, I>(
576 executor: Option<&mut crate::executor::Executor<'_>>,
577 args: I,
578) -> Result<usize, ApplyError<'a>>
579where
580 I: IntoIterator<Item = &'a str>,
581{
582 const N: usize = crate::executor::spin::MAX_REMAPS;
583 let mut rules: [Option<RemapArg<'a>>; N] = [None; N];
584 let mut n = 0usize;
585 let mut overflow = None;
586 parse_ros_args(args, |r| {
587 if n < N {
588 rules[n] = Some(r);
589 n += 1;
590 } else if overflow.is_none() {
591 overflow = Some(r);
592 }
593 })
594 .map_err(ApplyError::Refused)?;
595 if let Some(r) = overflow {
596 return Err(ApplyError::DoesNotFit(r));
597 }
598 if let Some(executor) = executor {
599 install_argv_remaps(executor, rules[..n].iter().flatten().copied())
600 .map_err(ApplyError::DoesNotFit)?;
601 }
602 Ok(n)
603}
604
605#[cfg(all(any(has_rmw, test), feature = "param-store", feature = "alloc"))]
606#[derive(Debug, Clone)]
608pub enum ApplyParamsError<'a> {
609 Remaps(ApplyError<'a>),
611 Unreadable {
613 path: &'a str,
615 why: alloc::string::String,
617 },
618 BadFile {
620 path: &'a str,
622 why: alloc::string::String,
624 },
625 NoStore,
627}
628
629#[cfg(all(any(has_rmw, test), feature = "param-store", feature = "alloc"))]
630impl core::fmt::Display for ApplyParamsError<'_> {
631 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
632 match self {
633 Self::Remaps(e) => e.fmt(f),
634 Self::Unreadable { path, why } => {
635 write!(f, "`--params-file {path}` cannot be read: {why}")
636 }
637 Self::BadFile { path, why } => write!(f, "`--params-file {path}`, {why}"),
638 Self::NoStore => f.write_str(
639 "the parameter store could not be allocated, so the parameter overrides have \
640 nowhere to go",
641 ),
642 }
643 }
644}
645
646#[cfg(all(any(has_rmw, test), feature = "param-store", feature = "alloc"))]
647pub fn apply_ros_args_with_params<'a, I, R>(
658 executor: Option<&mut crate::executor::Executor<'_>>,
659 args: I,
660 mut read_file: R,
661) -> Result<usize, ApplyParamsError<'a>>
662where
663 I: IntoIterator<Item = &'a str> + Clone,
664 R: FnMut(&str) -> Result<alloc::string::String, alloc::string::String>,
665{
666 use alloc::string::String;
667 let mut params: alloc::vec::Vec<(Option<String>, String, String)> = alloc::vec::Vec::new();
670 let mut files: alloc::vec::Vec<&'a str> = alloc::vec::Vec::new();
671 let mut order: alloc::vec::Vec<Result<usize, usize>> = alloc::vec::Vec::new();
672 parse_ros_args_events(args.clone(), |a| match a {
673 RosArg::Param(p) => {
674 order.push(Ok(params.len()));
675 params.push((
676 p.node.map(String::from),
677 String::from(p.name),
678 String::from(p.value),
679 ));
680 }
681 RosArg::ParamsFile(path) => {
682 order.push(Err(files.len()));
683 files.push(path);
684 }
685 RosArg::Remap(_) => {}
686 })
687 .map_err(|e| ApplyParamsError::Remaps(ApplyError::Refused(e)))?;
688 let mut file_params: alloc::vec::Vec<alloc::vec::Vec<(Option<String>, String, String)>> =
690 alloc::vec::Vec::new();
691 for &path in &files {
692 let text = read_file(path).map_err(|why| ApplyParamsError::Unreadable { path, why })?;
693 let mut got = alloc::vec::Vec::new();
694 parse_params_yaml(&text, |p| {
695 got.push((
696 p.node.map(String::from),
697 String::from(p.name),
698 String::from(p.value),
699 ))
700 })
701 .map_err(|e| ApplyParamsError::BadFile {
702 path,
703 why: alloc::format!("{e}"),
704 })?;
705 file_params.push(got);
706 }
707 let remap_args = args.into_iter().scan(false, |skip, a| {
709 if core::mem::take(skip) {
710 return Some(None);
711 }
712 if matches!(a, "-p" | "--param" | "--params-file") {
713 *skip = true;
714 return Some(None);
715 }
716 Some(Some(a))
717 });
718 let remap_args: alloc::vec::Vec<&'a str> = remap_args.flatten().collect();
719 let Some(executor) = executor else {
720 let n =
721 apply_ros_args(None, remap_args.iter().copied()).map_err(ApplyParamsError::Remaps)?;
722 return Ok(n + params.len() + file_params.iter().map(alloc::vec::Vec::len).sum::<usize>());
723 };
724 executor
729 .install_argv_params(core::iter::empty())
730 .map_err(|_| ApplyParamsError::NoStore)?;
731 let n = apply_ros_args(Some(&mut *executor), remap_args.iter().copied())
732 .map_err(ApplyParamsError::Remaps)?;
733 let mut flat: alloc::vec::Vec<&(Option<String>, String, String)> = alloc::vec::Vec::new();
734 for slot in &order {
735 match *slot {
736 Ok(i) => flat.push(¶ms[i]),
737 Err(f) => flat.extend(file_params[f].iter()),
738 }
739 }
740 let m = executor
741 .install_argv_params(flat.iter().map(|(node, name, value)| ParamArg {
742 node: node.as_deref(),
743 name,
744 value,
745 }))
746 .map_err(|_| ApplyParamsError::NoStore)?;
747 Ok(n + m)
748}
749
750#[cfg(any(has_rmw, test))]
751pub fn argv_fallback<'e>(
759 executor: &'e crate::executor::Executor<'_>,
760 node_name: &'e str,
761 namespace: &'e str,
762) -> impl Iterator<Item = (&'e str, &'e str)> + 'e {
763 use crate::executor::spin::{RemapTier, tier_rules};
764 tier_rules(
765 &executor.remap_table[..executor.remap_len],
766 RemapTier::Argv,
767 node_name,
768 namespace,
769 )
770}
771
772#[cfg(test)]
773mod tests {
774 use super::*;
775
776 fn rules<'a>(args: &[&'a str]) -> Result<heapless::Vec<RemapArg<'a>, 8>, RosArgsError<'a>> {
777 let mut out = heapless::Vec::new();
778 parse_ros_args(args.iter().copied(), |r| out.push(r).unwrap())?;
779 Ok(out)
780 }
781
782 fn rule<'a>(node: Option<&'a str>, from: &'a str, to: &'a str) -> RemapArg<'a> {
783 RemapArg { node, from, to }
784 }
785
786 #[test]
787 fn no_scope_reports_nothing_and_leaves_user_arguments_alone() {
788 assert!(
789 rules(&["prog", "-r", "a:=b", "--verbose"])
790 .unwrap()
791 .is_empty()
792 );
793 assert!(rules(&[]).unwrap().is_empty());
794 }
795
796 #[test]
797 fn short_and_long_remap_flags_keep_argv_order() {
798 let got = rules(&[
799 "prog",
800 "--ros-args",
801 "-r",
802 "foo:=bar",
803 "--remap",
804 "bar:=baz",
805 ])
806 .unwrap();
807 assert_eq!(
808 got.as_slice(),
809 &[rule(None, "foo", "bar"), rule(None, "bar", "baz")]
810 );
811 }
812
813 #[test]
814 fn a_node_prefix_scopes_the_rule() {
815 let got = rules(&["--ros-args", "-r", "alice:foo:=~/bar"]).unwrap();
816 assert_eq!(got.as_slice(), &[rule(Some("alice"), "foo", "~/bar")]);
817 }
818
819 #[test]
820 fn double_dash_closes_the_scope_and_a_second_scope_reopens_it() {
821 let got = rules(&[
822 "--ros-args",
823 "-r",
824 "a:=b",
825 "--",
826 "-r",
827 "user:=arg",
828 "--ros-args",
829 "-r",
830 "c:=d",
831 ])
832 .unwrap();
833 assert_eq!(
834 got.as_slice(),
835 &[rule(None, "a", "b"), rule(None, "c", "d")]
836 );
837 }
838
839 #[test]
840 fn parameter_overrides_are_refused_by_name() {
841 for flag in ["-p", "--param", "--params-file"] {
842 assert_eq!(
843 rules(&["--ros-args", flag, "x:=1"]),
844 Err(RosArgsError::Unsupported { flag })
845 );
846 }
847 }
848
849 #[test]
850 fn enclave_and_log_flags_are_refused_by_name() {
851 for flag in [
852 "-e",
853 "--enclave",
854 "--log-level",
855 "--log-config-file",
856 "--disable-rosout-logs",
857 ] {
858 assert_eq!(
859 rules(&["--ros-args", flag]),
860 Err(RosArgsError::Unsupported { flag })
861 );
862 }
863 }
864
865 #[test]
866 fn an_unknown_token_inside_a_scope_is_refused_not_skipped() {
867 assert_eq!(
868 rules(&["--ros-args", "-r", "a:=b", "--frobnicate"]),
869 Err(RosArgsError::Unknown {
870 token: "--frobnicate"
871 })
872 );
873 assert_eq!(
875 rules(&["--ros-args", "--enable-everything"]),
876 Err(RosArgsError::Unknown {
877 token: "--enable-everything"
878 })
879 );
880 }
881
882 #[test]
883 fn a_remap_flag_needs_a_valid_rule_after_it() {
884 assert_eq!(
885 rules(&["--ros-args", "-r"]),
886 Err(RosArgsError::MissingRule { flag: "-r" })
887 );
888 for bad in [
889 "nocolon", ":=b", "a:=", "a b:=c", ":a:=b", "n/s:a:=b", "a:=b:c",
890 ] {
891 assert_eq!(
892 rules(&["--ros-args", "--remap", bad]),
893 Err(RosArgsError::InvalidRule { rule: bad }),
894 "{bad}"
895 );
896 }
897 }
898
899 #[test]
900 fn identity_remaps_are_refused() {
901 for r in ["__node:=x", "__name:=x", "__ns:=/x", "talker:__ns:=/x"] {
902 assert_eq!(
903 rules(&["--ros-args", "-r", r]),
904 Err(RosArgsError::IdentityRemap { rule: r })
905 );
906 }
907 }
908
909 #[cfg(all(feature = "std", not(feature = "rmw-cffi")))]
920 mod chain {
921 use super::super::*;
922 extern crate std;
923 use crate::{executor::Executor, mock::MockSession};
924
925 fn executor() -> Executor<'static> {
926 Executor::from_session(MockSession::new())
927 }
928
929 fn install_argv(exec: &mut Executor<'_>, argv: &[&'static str]) {
932 let mut parsed: std::vec::Vec<RemapArg<'static>> = std::vec::Vec::new();
933 parse_ros_args(argv.iter().copied(), |r| parsed.push(r)).expect("argv parses");
934 install_argv_remaps(exec, parsed.iter().copied()).expect("rules fit");
935 }
936
937 fn resolve(exec: &Executor<'_>, node: &str, source: &str) -> std::string::String {
938 std::string::String::from(
939 exec.resolve_entity_name_for(node, "/", source)
940 .unwrap()
941 .as_str(),
942 )
943 }
944
945 #[test]
946 fn a_launch_rule_wins_over_argv_even_when_argv_was_installed_first() {
947 let mut exec = executor();
951 install_argv(
952 &mut exec,
953 &["prog", "--ros-args", "-r", "chatter:=/from_argv"],
954 );
955 exec.declare_remap("talker", "/", "chatter", "/from_launch")
956 .unwrap();
957 assert_eq!(resolve(&exec, "talker", "chatter"), "/from_launch");
958 }
959
960 #[test]
961 fn an_argv_rule_applies_where_the_launch_projected_none() {
962 let mut exec = executor();
963 install_argv(
964 &mut exec,
965 &["prog", "--ros-args", "-r", "chatter:=/from_argv"],
966 );
967 exec.declare_remap("talker", "/", "chatter", "/from_launch")
968 .unwrap();
969 assert_eq!(resolve(&exec, "listener", "chatter"), "/from_argv");
971 assert_eq!(resolve(&exec, "talker", "other"), "/other");
973 }
974
975 #[test]
976 fn a_node_prefixed_argv_rule_reaches_only_that_node() {
977 let mut exec = executor();
978 install_argv(
979 &mut exec,
980 &["--ros-args", "-r", "talker:chatter:=/only_talker"],
981 );
982 assert_eq!(resolve(&exec, "talker", "chatter"), "/only_talker");
983 assert_eq!(resolve(&exec, "listener", "chatter"), "/chatter");
984 }
985
986 #[test]
987 fn within_argv_the_first_rule_wins() {
988 let mut exec = executor();
990 install_argv(
991 &mut exec,
992 &["--ros-args", "-r", "foo:=bar", "-r", "bar:=baz"],
993 );
994 assert_eq!(resolve(&exec, "n", "foo"), "/bar");
995 }
996
997 #[test]
998 fn argv_fallback_yields_only_argv_rules_for_the_named_node() {
999 let mut exec = executor();
1001 install_argv(
1002 &mut exec,
1003 &["--ros-args", "-r", "a:=/x", "-r", "other:b:=/y"],
1004 );
1005 exec.declare_remap("talker", "/", "a", "/launch").unwrap();
1006 let got: std::vec::Vec<_> = argv_fallback(&exec, "talker", "/").collect();
1007 assert_eq!(got, [("a", "/x")]);
1008 }
1009
1010 #[test]
1011 fn install_is_all_or_nothing() {
1012 let mut exec = executor();
1013 let free = exec.remap_table.len();
1014 for i in 0..free - 1 {
1015 let from = std::format!("/f{i}");
1016 exec.declare_remap("n", "/", &from, "/t").unwrap();
1017 }
1018 let before = exec.remap_len;
1019 let rules = [
1020 RemapArg {
1021 node: None,
1022 from: "fits",
1023 to: "/a",
1024 },
1025 RemapArg {
1026 node: None,
1027 from: "does_not",
1028 to: "/b",
1029 },
1030 ];
1031 assert_eq!(install_argv_remaps(&mut exec, rules), Err(rules[1]));
1032 assert_eq!(
1033 exec.remap_len, before,
1034 "a refused install leaves no rule behind"
1035 );
1036 assert_eq!(resolve(&exec, "n", "fits"), "/fits");
1037 }
1038 }
1039
1040 #[test]
1041 fn kind_qualified_remaps_are_refused() {
1042 let r = "rostopic://foo:=bar";
1043 assert_eq!(
1044 rules(&["--ros-args", "-r", r]),
1045 Err(RosArgsError::Unsupported { flag: r })
1046 );
1047 }
1048
1049 #[test]
1050 fn apply_validates_without_an_executor_and_installs_nothing() {
1051 let ok = [
1052 "prog",
1053 "--ros-args",
1054 "-r",
1055 "a:=b",
1056 "-r",
1057 "n:c:=d",
1058 "--",
1059 "x",
1060 ];
1061 assert_eq!(apply_ros_args(None, ok.iter().copied()), Ok(2));
1062 assert_eq!(
1063 apply_ros_args(None, ["prog", "plain"].iter().copied()),
1064 Ok(0)
1065 );
1066 }
1067
1068 #[test]
1069 fn apply_refuses_by_name_what_the_parse_refuses() {
1070 let bad = ["prog", "--ros-args", "-r", "a:=b", "-p", "x:=1"];
1071 assert_eq!(
1072 apply_ros_args(None, bad.iter().copied()),
1073 Err(ApplyError::Refused(RosArgsError::Unsupported {
1074 flag: "-p"
1075 }))
1076 );
1077 }
1078
1079 #[test]
1080 fn apply_refuses_more_rules_than_the_table_can_ever_hold() {
1081 const N: usize = crate::executor::spin::MAX_REMAPS;
1082 let mut args: heapless::Vec<&str, { 2 + 2 * (N + 1) }> = heapless::Vec::new();
1083 args.extend_from_slice(&["prog", "--ros-args"]).unwrap();
1084 for _ in 0..=N {
1085 args.extend_from_slice(&["-r", "a:=b"]).unwrap();
1086 }
1087 assert!(matches!(
1088 apply_ros_args(None, args.iter().copied()),
1089 Err(ApplyError::DoesNotFit(_))
1090 ));
1091 }
1092}
1093
1094#[cfg(all(test, feature = "alloc"))]
1097mod param_tests {
1098 use super::*;
1099
1100 fn events<'a>(args: &[&'a str]) -> Result<heapless::Vec<RosArg<'a>, 8>, RosArgsError<'a>> {
1101 let mut out = heapless::Vec::new();
1102 parse_ros_args_events(args.iter().copied(), |a| out.push(a).unwrap())?;
1103 Ok(out)
1104 }
1105
1106 #[test]
1107 fn p_and_params_file_are_events_in_argv_order() {
1108 let got = events(&[
1109 "prog",
1110 "--ros-args",
1111 "-p",
1112 "a:=1",
1113 "--params-file",
1114 "x.yaml",
1115 "-r",
1116 "f:=t",
1117 "--param",
1118 "n:b:=two",
1119 ])
1120 .unwrap();
1121 assert_eq!(got.len(), 4);
1122 assert_eq!(
1123 got[0],
1124 RosArg::Param(ParamArg {
1125 node: None,
1126 name: "a",
1127 value: "1"
1128 })
1129 );
1130 assert_eq!(got[1], RosArg::ParamsFile("x.yaml"));
1131 assert!(matches!(got[2], RosArg::Remap(_)));
1132 assert_eq!(
1133 got[3],
1134 RosArg::Param(ParamArg {
1135 node: Some("n"),
1136 name: "b",
1137 value: "two"
1138 })
1139 );
1140 }
1141
1142 #[test]
1143 fn the_remap_only_parse_still_refuses_parameter_flags() {
1144 let r = parse_ros_args(["prog", "--ros-args", "-p", "a:=1"], |_| {});
1145 assert_eq!(r, Err(RosArgsError::Unsupported { flag: "-p" }));
1146 let r = parse_ros_args(["prog", "--ros-args", "--params-file", "x"], |_| {});
1147 assert_eq!(
1148 r,
1149 Err(RosArgsError::Unsupported {
1150 flag: "--params-file"
1151 })
1152 );
1153 }
1154
1155 #[test]
1156 fn malformed_overrides_are_refused_by_name() {
1157 assert_eq!(
1158 events(&["p", "--ros-args", "-p"]).unwrap_err(),
1159 RosArgsError::MissingValue { flag: "-p" }
1160 );
1161 assert_eq!(
1162 events(&["p", "--ros-args", "-p", "novalue"]).unwrap_err(),
1163 RosArgsError::InvalidParam { rule: "novalue" }
1164 );
1165 assert_eq!(
1166 events(&["p", "--ros-args", "--params-file"]).unwrap_err(),
1167 RosArgsError::MissingValue {
1168 flag: "--params-file"
1169 }
1170 );
1171 assert_eq!(
1173 events(&["p", "--ros-args", "-p", "label:="]).unwrap()[0],
1174 RosArg::Param(ParamArg {
1175 node: None,
1176 name: "label",
1177 value: ""
1178 })
1179 );
1180 }
1181
1182 fn yaml(
1183 text: &str,
1184 ) -> Result<
1185 alloc::vec::Vec<(
1186 Option<alloc::string::String>,
1187 alloc::string::String,
1188 alloc::string::String,
1189 )>,
1190 alloc::string::String,
1191 > {
1192 let mut out = alloc::vec::Vec::new();
1193 parse_params_yaml(text, |p| {
1194 out.push((
1195 p.node.map(alloc::string::String::from),
1196 alloc::string::String::from(p.name),
1197 alloc::string::String::from(p.value),
1198 ))
1199 })
1200 .map_err(|e| alloc::format!("{e}"))?;
1201 Ok(out)
1202 }
1203
1204 #[test]
1205 fn a_params_file_reports_dotted_names_per_node() {
1206 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();
1207 assert_eq!(got.len(), 3);
1208 assert_eq!(got[0], (None, "a".into(), "1".into()));
1209 assert_eq!(got[1], (None, "g.b".into(), "x # not a comment".into()));
1210 assert_eq!(got[2], (Some("talker".into()), "c".into(), "true".into()));
1211 }
1212
1213 #[test]
1214 fn what_the_subset_does_not_read_is_refused_with_its_line() {
1215 let e = yaml("/**:\n ros__parameters:\n xs: [1, 2]\n").unwrap_err();
1216 assert!(e.starts_with("line 3:"), "{e}");
1217 let e = yaml("/**:\n params:\n a: 1\n").unwrap_err();
1218 assert!(e.contains("ros__parameters"), "{e}");
1219 let e = yaml("/*/x:\n ros__parameters:\n a: 1\n").unwrap_err();
1220 assert!(e.contains("wildcard"), "{e}");
1221 let e = yaml("/**:\n\tros__parameters:\n").unwrap_err();
1222 assert!(e.contains("tab"), "{e}");
1223 let e = yaml("/**:\n ros__parameters:\n - a\n").unwrap_err();
1224 assert!(e.contains("sequence"), "{e}");
1225 }
1226}