1use crate::auth::{AccessLevel, User};
8use crate::config::ShellConfig;
9use crate::error::CliError;
10use crate::io::CharIo;
11use crate::response::Response;
12use crate::tree::{CommandKind, Directory, Node};
13use core::marker::PhantomData;
14
15#[cfg(feature = "completion")]
16use crate::tree::completion::suggest_completions;
17
18pub mod decoder;
20pub mod handler;
21pub mod history;
22
23pub use decoder::{InputDecoder, InputEvent};
25pub use handler::CommandHandler;
26pub use history::CommandHistory;
27
28#[repr(u8)]
32#[derive(Debug, Copy, Clone, PartialEq, Eq)]
33pub enum HistoryDirection {
34 Previous = 0,
36
37 Next = 1,
39}
40
41#[derive(Debug, Copy, Clone, PartialEq, Eq)]
46pub enum CliState {
47 Inactive,
49
50 #[cfg(feature = "authentication")]
52 LoggedOut,
53
54 LoggedIn,
56}
57
58#[derive(Debug, Clone)]
63#[allow(clippy::large_enum_variant)]
64pub enum Request<C: ShellConfig> {
65 #[cfg(feature = "authentication")]
67 Login {
68 username: heapless::String<32>,
70 password: heapless::String<64>,
72 },
73
74 #[cfg(feature = "authentication")]
76 InvalidLogin,
77
78 Command {
80 path: heapless::String<128>, args: heapless::Vec<heapless::String<128>, 16>, #[cfg(feature = "history")]
86 original: heapless::String<128>, _phantom: PhantomData<C>,
89 },
90
91 #[cfg(feature = "completion")]
93 TabComplete {
94 path: heapless::String<128>, },
97
98 #[cfg(feature = "history")]
100 History {
101 direction: HistoryDirection,
103 buffer: heapless::String<128>, },
106}
107
108pub struct Shell<'tree, L, IO, H, C>
113where
114 L: AccessLevel,
115 IO: CharIo,
116 H: CommandHandler<C>,
117 C: ShellConfig,
118{
119 tree: &'tree Directory<L>,
121
122 current_user: Option<User<L>>,
124
125 state: CliState,
127
128 input_buffer: heapless::String<128>,
130
131 current_path: heapless::Vec<usize, 8>,
133
134 decoder: InputDecoder,
136
137 #[cfg_attr(not(feature = "history"), allow(dead_code))]
139 history: CommandHistory<10, 128>,
140
141 io: IO,
143
144 handler: H,
146
147 #[cfg(feature = "authentication")]
149 credential_provider: &'tree (dyn crate::auth::CredentialProvider<L, Error = ()> + 'tree),
150
151 _config: PhantomData<C>,
153}
154
155impl<'tree, L, IO, H, C> core::fmt::Debug for Shell<'tree, L, IO, H, C>
160where
161 L: AccessLevel,
162 IO: CharIo,
163 H: CommandHandler<C>,
164 C: ShellConfig,
165{
166 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
167 let mut debug_struct = f.debug_struct("Shell");
168 debug_struct
169 .field("state", &self.state)
170 .field("input_buffer", &self.input_buffer.as_str())
171 .field("current_path", &self.current_path);
172
173 if let Some(user) = &self.current_user {
174 debug_struct.field("current_user", &user.username.as_str());
175 } else {
176 debug_struct.field("current_user", &"None");
177 }
178
179 #[cfg(feature = "authentication")]
180 debug_struct.field("credential_provider", &"<dyn CredentialProvider>");
181
182 debug_struct.finish_non_exhaustive()
183 }
184}
185
186#[cfg(feature = "authentication")]
191impl<'tree, L, IO, H, C> Shell<'tree, L, IO, H, C>
192where
193 L: AccessLevel,
194 IO: CharIo,
195 H: CommandHandler<C>,
196 C: ShellConfig,
197{
198 pub fn new(
201 tree: &'tree Directory<L>,
202 handler: H,
203 credential_provider: &'tree (dyn crate::auth::CredentialProvider<L, Error = ()> + 'tree),
204 io: IO,
205 ) -> Self {
206 Self {
207 tree,
208 handler,
209 current_user: None,
210 state: CliState::Inactive,
211 input_buffer: heapless::String::new(),
212 current_path: heapless::Vec::new(),
213 decoder: InputDecoder::new(),
214 history: CommandHistory::new(),
215 io,
216 credential_provider,
217 _config: PhantomData,
218 }
219 }
220}
221
222#[cfg(not(feature = "authentication"))]
223impl<'tree, L, IO, H, C> Shell<'tree, L, IO, H, C>
224where
225 L: AccessLevel,
226 IO: CharIo,
227 H: CommandHandler<C>,
228 C: ShellConfig,
229{
230 pub fn new(tree: &'tree Directory<L>, handler: H, io: IO) -> Self {
234 Self {
235 tree,
236 handler,
237 current_user: None,
238 state: CliState::Inactive,
239 input_buffer: heapless::String::new(),
240 current_path: heapless::Vec::new(),
241 decoder: InputDecoder::new(),
242 history: CommandHistory::new(),
243 io,
244 _config: PhantomData,
245 }
246 }
247}
248
249impl<'tree, L, IO, H, C> Shell<'tree, L, IO, H, C>
254where
255 L: AccessLevel,
256 IO: CharIo,
257 H: CommandHandler<C>,
258 C: ShellConfig,
259{
260 pub fn activate(&mut self) -> Result<(), IO::Error> {
264 self.io.write_str(C::MSG_WELCOME)?;
265 self.io.write_str("\r\n")?;
266
267 #[cfg(feature = "authentication")]
268 {
269 self.state = CliState::LoggedOut;
270 self.io.write_str(C::MSG_LOGIN_PROMPT)?;
271 }
272
273 #[cfg(not(feature = "authentication"))]
274 {
275 self.state = CliState::LoggedIn;
276 self.generate_and_write_prompt()?;
277 }
278
279 Ok(())
280 }
281
282 pub fn deactivate(&mut self) {
285 self.state = CliState::Inactive;
286 self.current_user = None;
287 self.input_buffer.clear();
288 self.current_path.clear();
289 }
290
291 pub fn process_char(&mut self, c: char) -> Result<(), IO::Error> {
293 let event = self.decoder.decode_char(c);
295
296 match event {
297 InputEvent::None => Ok(()), InputEvent::Char(ch) => {
300 match self.input_buffer.push(ch) {
302 Ok(_) => {
303 let echo_char = self.get_echo_char(ch);
305 self.io.put_char(echo_char)?;
306 Ok(())
307 }
308 Err(_) => {
309 self.io.put_char('\x07')?; Ok(())
312 }
313 }
314 }
315
316 InputEvent::Backspace => {
317 if !self.input_buffer.is_empty() {
319 self.input_buffer.pop();
320 self.io.write_str("\x08 \x08")?;
322 }
323 Ok(())
324 }
325
326 InputEvent::DoubleEsc => {
327 self.input_buffer.clear();
329 self.clear_line_and_redraw()
330 }
331
332 InputEvent::Enter => self.handle_enter(),
333
334 InputEvent::Tab => self.handle_tab(),
335
336 InputEvent::UpArrow => self.handle_history(HistoryDirection::Previous),
337
338 InputEvent::DownArrow => self.handle_history(HistoryDirection::Next),
339 }
340 }
341
342 #[cfg(feature = "async")]
345 pub async fn process_char_async(&mut self, c: char) -> Result<(), IO::Error> {
346 let event = self.decoder.decode_char(c);
348
349 match event {
350 InputEvent::None => Ok(()), InputEvent::Char(ch) => {
353 match self.input_buffer.push(ch) {
355 Ok(_) => {
356 let echo_char = self.get_echo_char(ch);
358 self.io.put_char(echo_char)?;
359 Ok(())
360 }
361 Err(_) => {
362 self.io.put_char('\x07')?; Ok(())
365 }
366 }
367 }
368
369 InputEvent::Backspace => {
370 if !self.input_buffer.is_empty() {
372 self.input_buffer.pop();
373 self.io.write_str("\x08 \x08")?;
375 }
376 Ok(())
377 }
378
379 InputEvent::DoubleEsc => {
380 self.input_buffer.clear();
382 self.clear_line_and_redraw()
383 }
384
385 InputEvent::Enter => self.handle_enter_async().await,
386
387 InputEvent::Tab => self.handle_tab(),
388
389 InputEvent::UpArrow => self.handle_history(HistoryDirection::Previous),
390
391 InputEvent::DownArrow => self.handle_history(HistoryDirection::Next),
392 }
393 }
394
395 pub fn poll(&mut self) -> Result<(), IO::Error> {
398 if let Some(c) = self.io.get_char()? {
399 self.process_char(c)?;
400 }
401 Ok(())
402 }
403
404 fn get_echo_char(&self, ch: char) -> char {
408 #[cfg(feature = "authentication")]
409 {
410 if self.state == CliState::LoggedOut {
412 let colon_count = self.input_buffer.matches(':').count();
414
415 if colon_count == 0 || (colon_count == 1 && ch == ':') {
420 return ch; } else {
422 return '*'; }
424 }
425 }
426
427 ch
429 }
430
431 fn generate_prompt(&self) -> heapless::String<128> {
436 let mut prompt = heapless::String::new();
437
438 if let Some(user) = &self.current_user {
440 prompt.push_str(user.username.as_str()).ok();
441 }
442 prompt.push('@').ok();
443
444 prompt.push('/').ok();
446 if !self.current_path.is_empty()
447 && let Ok(path_str) = self.get_current_path_string()
448 {
449 prompt.push_str(&path_str).ok();
450 }
451
452 prompt.push_str("> ").ok();
453 prompt
454 }
455
456 fn generate_and_write_prompt(&mut self) -> Result<(), IO::Error> {
458 let prompt = self.generate_prompt();
459 self.io.write_str(prompt.as_str())
460 }
461
462 fn write_formatted_response(&mut self, response: &Response<C>) -> Result<(), IO::Error> {
467 if response.prefix_newline {
469 self.io.write_str("\r\n")?;
470 }
471
472 if response.indent_message {
474 for (i, line) in response.message.split("\r\n").enumerate() {
476 if i > 0 {
477 self.io.write_str("\r\n")?;
478 }
479 self.io.write_str(" ")?; self.io.write_str(line)?;
481 }
482 } else {
483 self.io.write_str(&response.message)?;
485 }
486
487 if response.postfix_newline {
489 self.io.write_str("\r\n")?;
490 }
491
492 Ok(())
493 }
494
495 fn format_error(error: &CliError) -> heapless::String<256> {
501 use core::fmt::Write;
502 let mut buffer = heapless::String::new();
503 let _ = write!(&mut buffer, "{}", error);
506 buffer
507 }
508
509 fn get_current_dir(&self) -> Result<&'tree Directory<L>, CliError> {
511 let mut current: &Directory<L> = self.tree;
512
513 for &index in self.current_path.iter() {
514 match current.children.get(index) {
515 Some(Node::Directory(dir)) => current = dir,
516 Some(Node::Command(_)) | None => return Err(CliError::InvalidPath),
517 }
518 }
519
520 Ok(current)
521 }
522
523 fn get_current_path_string(&self) -> Result<heapless::String<128>, CliError> {
526 let mut path_str = heapless::String::new();
527 let mut current: &Directory<L> = self.tree;
528
529 for (i, &index) in self.current_path.iter().enumerate() {
530 match current.children.get(index) {
531 Some(Node::Directory(dir)) => {
532 if i > 0 {
533 path_str.push('/').map_err(|_| CliError::BufferFull)?;
534 }
535 path_str
536 .push_str(dir.name)
537 .map_err(|_| CliError::BufferFull)?;
538 current = dir;
539 }
540 _ => return Err(CliError::InvalidPath),
541 }
542 }
543
544 Ok(path_str)
545 }
546
547 fn handle_enter(&mut self) -> Result<(), IO::Error> {
549 let input = self.input_buffer.clone();
553 self.input_buffer.clear();
554
555 match self.state {
556 CliState::Inactive => Ok(()),
557
558 #[cfg(feature = "authentication")]
559 CliState::LoggedOut => self.handle_login_input(&input),
560
561 CliState::LoggedIn => self.handle_input_line(&input),
562 }
563 }
564
565 #[cfg(feature = "async")]
569 async fn handle_enter_async(&mut self) -> Result<(), IO::Error> {
570 let input = self.input_buffer.clone();
574 self.input_buffer.clear();
575
576 match self.state {
577 CliState::Inactive => Ok(()),
578
579 #[cfg(feature = "authentication")]
580 CliState::LoggedOut => self.handle_login_input(&input),
581
582 CliState::LoggedIn => self.handle_input_line_async(&input).await,
583 }
584 }
585
586 #[cfg(feature = "authentication")]
588 fn handle_login_input(&mut self, input: &str) -> Result<(), IO::Error> {
589 self.io.write_str("\r\n ")?;
591
592 if input.contains(':') {
593 let parts: heapless::Vec<&str, 2> = input.splitn(2, ':').collect();
595 if parts.len() == 2 {
596 let username = parts[0];
597 let password = parts[1];
598
599 match self.credential_provider.find_user(username) {
601 Ok(Some(user)) if self.credential_provider.verify_password(&user, password) => {
602 self.current_user = Some(user);
604 self.state = CliState::LoggedIn;
605 self.io.write_str(C::MSG_LOGIN_SUCCESS)?;
606 self.io.write_str("\r\n")?;
607 self.generate_and_write_prompt()?;
608 }
609 _ => {
610 self.io.write_str(C::MSG_LOGIN_FAILED)?;
612 self.io.write_str("\r\n")?;
613 self.io.write_str(C::MSG_LOGIN_PROMPT)?;
614 }
615 }
616 } else {
617 self.io.write_str(C::MSG_INVALID_LOGIN_FORMAT)?;
618 self.io.write_str("\r\n")?;
619 self.io.write_str(C::MSG_LOGIN_PROMPT)?;
620 }
621 } else {
622 self.io.write_str(C::MSG_INVALID_LOGIN_FORMAT)?;
624 self.io.write_str("\r\n")?;
625 self.io.write_str(C::MSG_LOGIN_PROMPT)?;
626 }
627
628 Ok(())
629 }
630
631 fn handle_global_commands(&mut self, input: &str) -> Result<bool, IO::Error> {
635 match input.trim() {
638 "?" => {
639 self.io.write_str("\r\n")?;
640 self.show_help()?;
641 self.generate_and_write_prompt()?;
642 Ok(true)
643 }
644 "ls" => {
645 self.io.write_str("\r\n")?;
646 self.show_ls()?;
647 self.generate_and_write_prompt()?;
648 Ok(true)
649 }
650 "clear" => {
651 self.io.write_str("\x1b[2J\x1b[H")?; self.generate_and_write_prompt()?;
654 Ok(true)
655 }
656 #[cfg(feature = "authentication")]
657 "logout" => {
658 self.io.write_str("\r\n ")?;
659 self.current_user = None;
660 self.state = CliState::LoggedOut;
661 self.current_path.clear();
662 self.io.write_str(C::MSG_LOGOUT)?;
663 self.io.write_str("\r\n")?;
664 self.io.write_str(C::MSG_LOGIN_PROMPT)?;
665 Ok(true)
666 }
667 _ => Ok(false),
668 }
669 }
670
671 fn write_response_and_prompt(
673 &mut self,
674 response: Response<C>,
675 #[cfg_attr(not(feature = "history"), allow(unused_variables))] input: &str,
676 ) -> Result<(), IO::Error> {
677 if !response.inline_message {
679 self.io.write_str("\r\n")?;
680 }
681
682 self.write_formatted_response(&response)?;
684
685 #[cfg(feature = "history")]
687 if !response.exclude_from_history {
688 self.history.add(input);
689 }
690
691 if response.show_prompt {
693 self.generate_and_write_prompt()?;
694 }
695
696 Ok(())
697 }
698
699 fn write_error_and_prompt(&mut self, error: CliError) -> Result<(), IO::Error> {
701 self.io.write_str("\r\n ")?;
703
704 self.io.write_str("Error: ")?;
706 let error_msg = Self::format_error(&error);
707 self.io.write_str(error_msg.as_str())?;
708 self.io.write_str("\r\n")?;
709 self.generate_and_write_prompt()?;
710
711 Ok(())
712 }
713
714 fn handle_input_line(&mut self, input: &str) -> Result<(), IO::Error> {
721 if input.trim().is_empty() {
723 self.io.write_str("\r\n")?;
724 self.generate_and_write_prompt()?;
725 return Ok(());
726 }
727
728 if self.handle_global_commands(input)? {
730 return Ok(());
731 }
732
733 match self.execute_tree_path(input) {
735 Ok(response) => self.write_response_and_prompt(response, input),
736 Err(e) => self.write_error_and_prompt(e),
737 }
738 }
739
740 #[cfg(feature = "async")]
747 async fn handle_input_line_async(&mut self, input: &str) -> Result<(), IO::Error> {
748 if input.trim().is_empty() {
750 self.io.write_str("\r\n")?;
751 self.generate_and_write_prompt()?;
752 return Ok(());
753 }
754
755 if self.handle_global_commands(input)? {
757 return Ok(());
758 }
759
760 match self.execute_tree_path_async(input).await {
762 Ok(response) => self.write_response_and_prompt(response, input),
763 Err(e) => self.write_error_and_prompt(e),
764 }
765 }
766
767 fn execute_tree_path(&mut self, input: &str) -> Result<Response<C>, CliError> {
776 let parts: heapless::Vec<&str, 17> = input.split_whitespace().collect();
779 if parts.is_empty() {
780 return Err(CliError::CommandNotFound);
781 }
782
783 let path_str = parts[0];
784 let args = &parts[1..];
785
786 let (target_node, new_path) = self.resolve_path(path_str)?;
788
789 match target_node {
791 None | Some(Node::Directory(_)) => {
792 if !args.is_empty() {
793 return Err(CliError::InvalidArgumentCount {
794 expected_min: 0,
795 expected_max: 0,
796 received: args.len(),
797 });
798 }
799 self.current_path = new_path;
801 #[cfg(feature = "history")]
802 return Ok(Response::success("")
803 .without_history()
804 .without_postfix_newline());
805 #[cfg(not(feature = "history"))]
806 return Ok(Response::success("").without_postfix_newline());
807 }
808 Some(Node::Command(cmd_meta)) => {
809 if let Some(user) = &self.current_user
812 && user.access_level < cmd_meta.access_level
813 {
814 return Err(CliError::InvalidPath);
815 }
816
817 if args.len() < cmd_meta.min_args || args.len() > cmd_meta.max_args {
819 return Err(CliError::InvalidArgumentCount {
820 expected_min: cmd_meta.min_args,
821 expected_max: cmd_meta.max_args,
822 received: args.len(),
823 });
824 }
825
826 match cmd_meta.kind {
828 CommandKind::Sync => {
829 self.handler.execute_sync(cmd_meta.id, args)
831 }
832 #[cfg(feature = "async")]
833 CommandKind::Async => {
834 Err(CliError::AsyncInSyncContext)
836 }
837 }
838 }
839 }
840 }
841
842 #[cfg(feature = "async")]
854 async fn execute_tree_path_async(&mut self, input: &str) -> Result<Response<C>, CliError> {
855 let parts: heapless::Vec<&str, 17> = input.split_whitespace().collect();
858 if parts.is_empty() {
859 return Err(CliError::CommandNotFound);
860 }
861
862 let path_str = parts[0];
863 let args = &parts[1..];
864
865 let (target_node, new_path) = self.resolve_path(path_str)?;
867
868 match target_node {
870 None | Some(Node::Directory(_)) => {
871 if !args.is_empty() {
872 return Err(CliError::InvalidArgumentCount {
873 expected_min: 0,
874 expected_max: 0,
875 received: args.len(),
876 });
877 }
878 self.current_path = new_path;
880 #[cfg(feature = "history")]
881 return Ok(Response::success("")
882 .without_history()
883 .without_postfix_newline());
884 #[cfg(not(feature = "history"))]
885 return Ok(Response::success("").without_postfix_newline());
886 }
887 Some(Node::Command(cmd_meta)) => {
888 if let Some(user) = &self.current_user
891 && user.access_level < cmd_meta.access_level
892 {
893 return Err(CliError::InvalidPath);
894 }
895
896 if args.len() < cmd_meta.min_args || args.len() > cmd_meta.max_args {
898 return Err(CliError::InvalidArgumentCount {
899 expected_min: cmd_meta.min_args,
900 expected_max: cmd_meta.max_args,
901 received: args.len(),
902 });
903 }
904
905 match cmd_meta.kind {
907 CommandKind::Sync => {
908 self.handler.execute_sync(cmd_meta.id, args)
910 }
911 CommandKind::Async => {
912 self.handler.execute_async(cmd_meta.id, args).await
914 }
915 }
916 }
917 }
918 }
919
920 fn resolve_path(
926 &self,
927 path_str: &str,
928 ) -> Result<(Option<&'tree Node<L>>, heapless::Vec<usize, 8>), CliError> {
929 let mut working_path: heapless::Vec<usize, 8> = if path_str.starts_with('/') {
932 heapless::Vec::new() } else {
934 self.current_path.clone() };
936
937 let segments: heapless::Vec<&str, 8> = path_str
940 .trim_start_matches('/')
941 .split('/')
942 .filter(|s| !s.is_empty() && *s != ".")
943 .collect();
944
945 for (seg_idx, segment) in segments.iter().enumerate() {
947 if *segment == ".." {
948 working_path.pop();
950 continue;
951 }
952
953 let is_last_segment = seg_idx == segments.len() - 1;
954
955 let current_dir = self.get_dir_at_path(&working_path)?;
957 let mut found = false;
958
959 for (index, child) in current_dir.children.iter().enumerate() {
960 let node_level = match child {
962 Node::Command(cmd) => cmd.access_level,
963 Node::Directory(dir) => dir.access_level,
964 };
965
966 if let Some(user) = &self.current_user
967 && user.access_level < node_level
968 {
969 continue; }
971
972 if child.name() == *segment {
973 if child.is_directory() {
975 working_path
977 .push(index)
978 .map_err(|_| CliError::PathTooDeep)?;
979 } else {
980 if is_last_segment {
982 return Ok((Some(child), working_path));
983 } else {
984 return Err(CliError::InvalidPath);
986 }
987 }
988 found = true;
989 break;
990 }
991 }
992
993 if !found {
994 return Err(CliError::CommandNotFound);
995 }
996 }
997
998 if working_path.is_empty() {
1001 return Ok((None, working_path));
1003 }
1004
1005 let dir_node = self.get_node_at_path(&working_path)?;
1006 Ok((Some(dir_node), working_path))
1007 }
1008
1009 fn get_dir_at_path(
1012 &self,
1013 path: &heapless::Vec<usize, 8>,
1014 ) -> Result<&'tree Directory<L>, CliError> {
1015 let mut current: &Directory<L> = self.tree;
1016
1017 for &index in path.iter() {
1018 match current.children.get(index) {
1019 Some(Node::Directory(dir)) => current = dir,
1020 Some(Node::Command(_)) | None => return Err(CliError::InvalidPath),
1021 }
1022 }
1023
1024 Ok(current)
1025 }
1026
1027 fn get_node_at_path(&self, path: &heapless::Vec<usize, 8>) -> Result<&'tree Node<L>, CliError> {
1030 if path.is_empty() {
1031 return Err(CliError::InvalidPath);
1034 }
1035
1036 let parent_path: heapless::Vec<usize, 8> =
1038 path.iter().take(path.len() - 1).copied().collect();
1039 let parent_dir = self.get_dir_at_path(&parent_path)?;
1040
1041 let last_index = *path.last().ok_or(CliError::InvalidPath)?;
1042 parent_dir
1043 .children
1044 .get(last_index)
1045 .ok_or(CliError::InvalidPath)
1046 }
1047
1048 fn handle_tab(&mut self) -> Result<(), IO::Error> {
1050 #[cfg(feature = "completion")]
1051 {
1052 let current_dir = match self.get_current_dir() {
1054 Ok(dir) => dir,
1055 Err(_) => return self.generate_and_write_prompt(), };
1057
1058 let result = suggest_completions::<L, 16>(
1060 current_dir,
1061 self.input_buffer.as_str(),
1062 self.current_user.as_ref(),
1063 );
1064
1065 match result {
1066 Ok(crate::tree::completion::CompletionResult::Single { completion, .. }) => {
1067 self.input_buffer.clear();
1069 match self.input_buffer.push_str(&completion) {
1070 Ok(()) => {
1071 self.io.write_str("\r")?; let prompt = self.generate_prompt();
1074 self.io.write_str(prompt.as_str())?;
1075 self.io.write_str(self.input_buffer.as_str())?;
1076 }
1077 Err(_) => {
1078 self.io.put_char('\x07')?;
1080 }
1081 }
1082 }
1083 Ok(crate::tree::completion::CompletionResult::Multiple { all_matches, .. }) => {
1084 self.io.write_str("\r\n")?;
1086 for m in all_matches.iter() {
1087 self.io.write_str(" ")?; self.io.write_str(m.as_str())?;
1089 self.io.write_str(" ")?;
1090 }
1091 self.io.write_str("\r\n")?;
1092 self.generate_and_write_prompt()?;
1093 self.io.write_str(self.input_buffer.as_str())?;
1094 }
1095 _ => {
1096 self.io.put_char('\x07')?; }
1099 }
1100 }
1101
1102 #[cfg(not(feature = "completion"))]
1103 {
1104 self.io.put_char('\x07')?; }
1107
1108 Ok(())
1109 }
1110
1111 fn handle_history(&mut self, direction: HistoryDirection) -> Result<(), IO::Error> {
1113 #[cfg(feature = "history")]
1114 {
1115 let history_entry = match direction {
1116 HistoryDirection::Previous => self.history.previous_command(),
1117 HistoryDirection::Next => self.history.next_command(),
1118 };
1119
1120 if let Some(entry) = history_entry {
1121 self.input_buffer = entry;
1123 self.clear_line_and_redraw()?;
1125 }
1126 }
1127
1128 #[cfg(not(feature = "history"))]
1129 {
1130 let _ = direction; }
1133
1134 Ok(())
1135 }
1136
1137 fn show_help(&mut self) -> Result<(), IO::Error> {
1139 self.io.write_str(" ? - List global commands\r\n")?;
1140 self.io
1141 .write_str(" ls - List directory contents\r\n")?;
1142
1143 #[cfg(feature = "authentication")]
1144 self.io.write_str(" logout - End session\r\n")?;
1145
1146 self.io.write_str(" clear - Clear screen\r\n")?;
1147 self.io.write_str(" ESC ESC - Clear input buffer\r\n")?;
1148
1149 Ok(())
1150 }
1151
1152 fn show_ls(&mut self) -> Result<(), IO::Error> {
1154 let current_dir = match self.get_current_dir() {
1155 Ok(dir) => dir,
1156 Err(_) => {
1157 self.io.write_str("Error accessing directory\r\n")?;
1158 return Ok(());
1159 }
1160 };
1161
1162 for child in current_dir.children.iter() {
1163 let node_level = match child {
1165 Node::Command(cmd) => cmd.access_level,
1166 Node::Directory(dir) => dir.access_level,
1167 };
1168
1169 if let Some(user) = &self.current_user
1170 && user.access_level < node_level
1171 {
1172 continue; }
1174
1175 match child {
1177 Node::Command(cmd) => {
1178 self.io.write_str(" ")?;
1179 self.io.write_str(cmd.name)?;
1180 self.io.write_str(" - ")?;
1181 self.io.write_str(cmd.description)?;
1182 self.io.write_str("\r\n")?;
1183 }
1184 Node::Directory(dir) => {
1185 self.io.write_str(" ")?;
1186 self.io.write_str(dir.name)?;
1187 self.io.write_str("/ - Directory\r\n")?;
1188 }
1189 }
1190 }
1191
1192 Ok(())
1193 }
1194
1195 fn clear_line_and_redraw(&mut self) -> Result<(), IO::Error> {
1197 self.io.write_str("\r\x1b[K")?; self.generate_and_write_prompt()?;
1199 self.io.write_str(self.input_buffer.as_str())?;
1200 Ok(())
1201 }
1202
1203 pub fn io(&self) -> &IO {
1209 &self.io
1210 }
1211
1212 pub fn io_mut(&mut self) -> &mut IO {
1214 &mut self.io
1215 }
1216}
1217
1218#[cfg(test)]
1223mod tests {
1224 use super::*;
1225 use crate::auth::AccessLevel;
1226 use crate::config::DefaultConfig;
1227 use crate::io::CharIo;
1228 use crate::tree::{CommandKind, CommandMeta, Directory, Node};
1229
1230 #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
1232 enum MockLevel {
1233 User = 0,
1234 }
1235
1236 impl AccessLevel for MockLevel {
1237 fn from_str(s: &str) -> Option<Self> {
1238 match s {
1239 "User" => Some(Self::User),
1240 _ => None,
1241 }
1242 }
1243
1244 fn as_str(&self) -> &'static str {
1245 "User"
1246 }
1247 }
1248
1249 struct MockIo {
1251 output: heapless::String<512>,
1252 }
1253 impl MockIo {
1254 fn new() -> Self {
1255 Self {
1256 output: heapless::String::new(),
1257 }
1258 }
1259
1260 #[allow(dead_code)]
1261 fn get_output(&self) -> &str {
1262 &self.output
1263 }
1264 }
1265 impl CharIo for MockIo {
1266 type Error = ();
1267 fn get_char(&mut self) -> Result<Option<char>, ()> {
1268 Ok(None)
1269 }
1270 fn put_char(&mut self, c: char) -> Result<(), ()> {
1271 self.output.push(c).map_err(|_| ())
1272 }
1273 fn write_str(&mut self, s: &str) -> Result<(), ()> {
1274 self.output.push_str(s).map_err(|_| ())
1275 }
1276 }
1277
1278 struct MockHandler;
1280 impl CommandHandler<DefaultConfig> for MockHandler {
1281 fn execute_sync(
1282 &self,
1283 _id: &str,
1284 _args: &[&str],
1285 ) -> Result<crate::response::Response<DefaultConfig>, crate::error::CliError> {
1286 Err(crate::error::CliError::CommandNotFound)
1287 }
1288
1289 #[cfg(feature = "async")]
1290 async fn execute_async(
1291 &self,
1292 _id: &str,
1293 _args: &[&str],
1294 ) -> Result<crate::response::Response<DefaultConfig>, crate::error::CliError> {
1295 Err(crate::error::CliError::CommandNotFound)
1296 }
1297 }
1298
1299 const CMD_TEST: CommandMeta<MockLevel> = CommandMeta {
1301 id: "test-cmd",
1302 name: "test-cmd",
1303 description: "Test command",
1304 access_level: MockLevel::User,
1305 kind: CommandKind::Sync,
1306 min_args: 0,
1307 max_args: 0,
1308 };
1309
1310 const CMD_REBOOT: CommandMeta<MockLevel> = CommandMeta {
1311 id: "reboot",
1312 name: "reboot",
1313 description: "Reboot the system",
1314 access_level: MockLevel::User,
1315 kind: CommandKind::Sync,
1316 min_args: 0,
1317 max_args: 0,
1318 };
1319
1320 const CMD_STATUS: CommandMeta<MockLevel> = CommandMeta {
1321 id: "status",
1322 name: "status",
1323 description: "Show status",
1324 access_level: MockLevel::User,
1325 kind: CommandKind::Sync,
1326 min_args: 0,
1327 max_args: 0,
1328 };
1329
1330 const CMD_LED: CommandMeta<MockLevel> = CommandMeta {
1331 id: "led",
1332 name: "led",
1333 description: "Control LED",
1334 access_level: MockLevel::User,
1335 kind: CommandKind::Sync,
1336 min_args: 1,
1337 max_args: 1,
1338 };
1339
1340 const CMD_NETWORK_STATUS: CommandMeta<MockLevel> = CommandMeta {
1341 id: "network_status",
1342 name: "status",
1343 description: "Network status",
1344 access_level: MockLevel::User,
1345 kind: CommandKind::Sync,
1346 min_args: 0,
1347 max_args: 0,
1348 };
1349
1350 const DIR_HARDWARE: Directory<MockLevel> = Directory {
1352 name: "hardware",
1353 children: &[Node::Command(&CMD_LED)],
1354 access_level: MockLevel::User,
1355 };
1356
1357 const DIR_NETWORK: Directory<MockLevel> = Directory {
1358 name: "network",
1359 children: &[Node::Command(&CMD_NETWORK_STATUS)],
1360 access_level: MockLevel::User,
1361 };
1362
1363 const DIR_SYSTEM: Directory<MockLevel> = Directory {
1364 name: "system",
1365 children: &[
1366 Node::Command(&CMD_REBOOT),
1367 Node::Command(&CMD_STATUS),
1368 Node::Directory(&DIR_HARDWARE),
1369 Node::Directory(&DIR_NETWORK),
1370 ],
1371 access_level: MockLevel::User,
1372 };
1373
1374 const TEST_TREE: Directory<MockLevel> = Directory {
1376 name: "/",
1377 children: &[Node::Command(&CMD_TEST), Node::Directory(&DIR_SYSTEM)],
1378 access_level: MockLevel::User,
1379 };
1380
1381 #[test]
1382 fn test_request_command_no_args() {
1383 let mut path = heapless::String::<128>::new();
1384 path.push_str("help").unwrap();
1385 let args = heapless::Vec::new();
1386 #[cfg(feature = "history")]
1387 let original = {
1388 let mut s = heapless::String::<128>::new();
1389 s.push_str("help").unwrap();
1390 s
1391 };
1392
1393 let request = Request::<DefaultConfig>::Command {
1394 path,
1395 args,
1396 #[cfg(feature = "history")]
1397 original,
1398 _phantom: core::marker::PhantomData,
1399 };
1400
1401 match request {
1402 Request::Command { path, args, .. } => {
1403 assert_eq!(path.as_str(), "help");
1404 assert_eq!(args.len(), 0);
1405 }
1406 #[allow(unreachable_patterns)]
1407 _ => panic!("Expected Command variant"),
1408 }
1409 }
1410
1411 #[test]
1412 fn test_request_command_with_args() {
1413 let mut path = heapless::String::<128>::new();
1414 path.push_str("echo").unwrap();
1415
1416 let mut args = heapless::Vec::new();
1417 let mut hello = heapless::String::<128>::new();
1418 hello.push_str("hello").unwrap();
1419 let mut world = heapless::String::<128>::new();
1420 world.push_str("world").unwrap();
1421 args.push(hello).unwrap();
1422 args.push(world).unwrap();
1423
1424 #[cfg(feature = "history")]
1425 let original = {
1426 let mut s = heapless::String::<128>::new();
1427 s.push_str("echo hello world").unwrap();
1428 s
1429 };
1430
1431 let request = Request::<DefaultConfig>::Command {
1432 path,
1433 args,
1434 #[cfg(feature = "history")]
1435 original,
1436 _phantom: core::marker::PhantomData,
1437 };
1438
1439 match request {
1440 Request::Command { path, args, .. } => {
1441 assert_eq!(path.as_str(), "echo");
1442 assert_eq!(args.len(), 2);
1443 assert_eq!(args[0].as_str(), "hello");
1444 assert_eq!(args[1].as_str(), "world");
1445 }
1446 #[allow(unreachable_patterns)]
1447 _ => panic!("Expected Command variant"),
1448 }
1449 }
1450
1451 #[test]
1452 #[cfg(feature = "history")]
1453 fn test_request_command_with_original() {
1454 let mut path = heapless::String::<128>::new();
1455 path.push_str("reboot").unwrap();
1456 let mut original = heapless::String::<128>::new();
1457 original.push_str("reboot").unwrap();
1458
1459 let request = Request::<DefaultConfig>::Command {
1460 path,
1461 args: heapless::Vec::new(),
1462 original,
1463 _phantom: core::marker::PhantomData,
1464 };
1465
1466 match request {
1467 Request::Command { path, original, .. } => {
1468 assert_eq!(path.as_str(), "reboot");
1469 assert_eq!(original.as_str(), "reboot");
1470 }
1471 #[allow(unreachable_patterns)]
1472 _ => panic!("Expected Command variant"),
1473 }
1474 }
1475
1476 #[test]
1477 #[cfg(feature = "authentication")]
1478 fn test_request_login() {
1479 let mut username = heapless::String::<32>::new();
1480 username.push_str("admin").unwrap();
1481 let mut password = heapless::String::<64>::new();
1482 password.push_str("secret123").unwrap();
1483
1484 let request = Request::<DefaultConfig>::Login { username, password };
1485
1486 match request {
1487 Request::Login { username, password } => {
1488 assert_eq!(username.as_str(), "admin");
1489 assert_eq!(password.as_str(), "secret123");
1490 }
1491 #[allow(unreachable_patterns)]
1492 _ => panic!("Expected Login variant"),
1493 }
1494 }
1495
1496 #[test]
1497 #[cfg(feature = "authentication")]
1498 fn test_request_invalid_login() {
1499 let request = Request::<DefaultConfig>::InvalidLogin;
1500
1501 match request {
1502 Request::InvalidLogin => {}
1503 #[allow(unreachable_patterns)]
1504 _ => panic!("Expected InvalidLogin variant"),
1505 }
1506 }
1507
1508 #[test]
1509 #[cfg(feature = "completion")]
1510 fn test_request_tab_complete() {
1511 let mut path = heapless::String::<128>::new();
1512 path.push_str("sys").unwrap();
1513
1514 let request = Request::<DefaultConfig>::TabComplete { path };
1515
1516 match request {
1517 Request::TabComplete { path } => {
1518 assert_eq!(path.as_str(), "sys");
1519 }
1520 #[allow(unreachable_patterns)]
1521 _ => panic!("Expected TabComplete variant"),
1522 }
1523 }
1524
1525 #[test]
1526 #[cfg(feature = "completion")]
1527 fn test_request_tab_complete_empty() {
1528 let request = Request::<DefaultConfig>::TabComplete {
1529 path: heapless::String::new(),
1530 };
1531
1532 match request {
1533 Request::TabComplete { path } => {
1534 assert_eq!(path.as_str(), "");
1535 }
1536 #[allow(unreachable_patterns)]
1537 _ => panic!("Expected TabComplete variant"),
1538 }
1539 }
1540
1541 #[test]
1542 #[cfg(feature = "history")]
1543 fn test_request_history_previous() {
1544 let mut buffer = heapless::String::<128>::new();
1545 buffer.push_str("current input").unwrap();
1546
1547 let request = Request::<DefaultConfig>::History {
1548 direction: HistoryDirection::Previous,
1549 buffer,
1550 };
1551
1552 match request {
1553 Request::History { direction, buffer } => {
1554 assert_eq!(direction, HistoryDirection::Previous);
1555 assert_eq!(buffer.as_str(), "current input");
1556 }
1557 #[allow(unreachable_patterns)]
1558 _ => panic!("Expected History variant"),
1559 }
1560 }
1561
1562 #[test]
1563 #[cfg(feature = "history")]
1564 fn test_request_history_next() {
1565 let request = Request::<DefaultConfig>::History {
1566 direction: HistoryDirection::Next,
1567 buffer: heapless::String::new(),
1568 };
1569
1570 match request {
1571 Request::History { direction, buffer } => {
1572 assert_eq!(direction, HistoryDirection::Next);
1573 assert_eq!(buffer.as_str(), "");
1574 }
1575 #[allow(unreachable_patterns)]
1576 _ => panic!("Expected History variant"),
1577 }
1578 }
1579
1580 #[test]
1581 fn test_request_variants_match_features() {
1582 let _cmd = Request::<DefaultConfig>::Command {
1583 path: heapless::String::new(),
1584 args: heapless::Vec::new(),
1585 #[cfg(feature = "history")]
1586 original: heapless::String::new(),
1587 _phantom: core::marker::PhantomData,
1588 };
1589
1590 #[cfg(feature = "authentication")]
1591 let _login = Request::<DefaultConfig>::Login {
1592 username: heapless::String::new(),
1593 password: heapless::String::new(),
1594 };
1595
1596 #[cfg(feature = "completion")]
1597 let _complete = Request::<DefaultConfig>::TabComplete {
1598 path: heapless::String::new(),
1599 };
1600
1601 #[cfg(feature = "history")]
1602 let _history = Request::<DefaultConfig>::History {
1603 direction: HistoryDirection::Previous,
1604 buffer: heapless::String::new(),
1605 };
1606 }
1607
1608 #[test]
1609 fn test_activate_deactivate_lifecycle() {
1610 let io = MockIo::new();
1611 let handler = MockHandler;
1612
1613 #[cfg(feature = "authentication")]
1615 {
1616 use crate::auth::CredentialProvider;
1617 struct MockProvider;
1618 impl CredentialProvider<MockLevel> for MockProvider {
1619 type Error = ();
1620 fn find_user(
1621 &self,
1622 _username: &str,
1623 ) -> Result<Option<crate::auth::User<MockLevel>>, ()> {
1624 Ok(None)
1625 }
1626 fn verify_password(
1627 &self,
1628 _user: &crate::auth::User<MockLevel>,
1629 _password: &str,
1630 ) -> bool {
1631 false
1632 }
1633 }
1634 let provider = MockProvider;
1635 let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1636 Shell::new(&TEST_TREE, handler, &provider, io);
1637
1638 assert_eq!(shell.state, CliState::Inactive);
1640 assert!(shell.current_user.is_none());
1641
1642 shell.activate().unwrap();
1644 assert_eq!(shell.state, CliState::LoggedOut);
1645
1646 shell.deactivate();
1648 assert_eq!(shell.state, CliState::Inactive);
1649 assert!(shell.current_user.is_none());
1650 assert!(shell.input_buffer.is_empty());
1651 assert!(shell.current_path.is_empty());
1652 }
1653
1654 #[cfg(not(feature = "authentication"))]
1655 {
1656 let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1657 Shell::new(&TEST_TREE, handler, io);
1658
1659 assert_eq!(shell.state, CliState::Inactive);
1661
1662 shell.activate().unwrap();
1664 assert_eq!(shell.state, CliState::LoggedIn);
1665
1666 shell.deactivate();
1668 assert_eq!(shell.state, CliState::Inactive);
1669 assert!(shell.current_user.is_none());
1670 assert!(shell.input_buffer.is_empty());
1671 assert!(shell.current_path.is_empty());
1672 }
1673 }
1674
1675 #[test]
1676 #[cfg(not(feature = "authentication"))]
1677 fn test_write_formatted_response_default() {
1678 let io = MockIo::new();
1680 let handler = MockHandler;
1681 let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1682 Shell::new(&TEST_TREE, handler, io);
1683
1684 let response = crate::response::Response::<DefaultConfig>::success("Test message");
1685 shell.write_formatted_response(&response).unwrap();
1686
1687 assert_eq!(shell.io.get_output(), "Test message\r\n");
1689 }
1690
1691 #[test]
1692 #[cfg(not(feature = "authentication"))]
1693 fn test_write_formatted_response_with_prefix_newline() {
1694 let io = MockIo::new();
1695 let handler = MockHandler;
1696 let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1697 Shell::new(&TEST_TREE, handler, io);
1698
1699 let response =
1700 crate::response::Response::<DefaultConfig>::success("Test").with_prefix_newline();
1701 shell.write_formatted_response(&response).unwrap();
1702
1703 assert_eq!(shell.io.get_output(), "\r\nTest\r\n");
1705 }
1706
1707 #[test]
1708 #[cfg(not(feature = "authentication"))]
1709 fn test_write_formatted_response_indented() {
1710 let io = MockIo::new();
1711 let handler = MockHandler;
1712 let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1713 Shell::new(&TEST_TREE, handler, io);
1714
1715 let response =
1716 crate::response::Response::<DefaultConfig>::success("Line 1\r\nLine 2").indented();
1717 shell.write_formatted_response(&response).unwrap();
1718
1719 assert_eq!(shell.io.get_output(), " Line 1\r\n Line 2\r\n");
1721 }
1722
1723 #[test]
1724 #[cfg(not(feature = "authentication"))]
1725 fn test_write_formatted_response_indented_single_line() {
1726 let io = MockIo::new();
1727 let handler = MockHandler;
1728 let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1729 Shell::new(&TEST_TREE, handler, io);
1730
1731 let response =
1732 crate::response::Response::<DefaultConfig>::success("Single line").indented();
1733 shell.write_formatted_response(&response).unwrap();
1734
1735 assert_eq!(shell.io.get_output(), " Single line\r\n");
1737 }
1738
1739 #[test]
1740 #[cfg(not(feature = "authentication"))]
1741 fn test_write_formatted_response_without_postfix_newline() {
1742 let io = MockIo::new();
1743 let handler = MockHandler;
1744 let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1745 Shell::new(&TEST_TREE, handler, io);
1746
1747 let response = crate::response::Response::<DefaultConfig>::success("No newline")
1748 .without_postfix_newline();
1749 shell.write_formatted_response(&response).unwrap();
1750
1751 assert_eq!(shell.io.get_output(), "No newline");
1753 }
1754
1755 #[test]
1756 #[cfg(not(feature = "authentication"))]
1757 fn test_write_formatted_response_combined_flags() {
1758 let io = MockIo::new();
1759 let handler = MockHandler;
1760 let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1761 Shell::new(&TEST_TREE, handler, io);
1762
1763 let response = crate::response::Response::<DefaultConfig>::success("Multi\r\nLine")
1764 .with_prefix_newline()
1765 .indented();
1766 shell.write_formatted_response(&response).unwrap();
1767
1768 assert_eq!(shell.io.get_output(), "\r\n Multi\r\n Line\r\n");
1770 }
1771
1772 #[test]
1773 #[cfg(not(feature = "authentication"))]
1774 fn test_write_formatted_response_all_flags_off() {
1775 let io = MockIo::new();
1776 let handler = MockHandler;
1777 let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1778 Shell::new(&TEST_TREE, handler, io);
1779
1780 let response =
1781 crate::response::Response::<DefaultConfig>::success("Raw").without_postfix_newline();
1782 shell.write_formatted_response(&response).unwrap();
1783
1784 assert_eq!(shell.io.get_output(), "Raw");
1786 }
1787
1788 #[test]
1789 #[cfg(not(feature = "authentication"))]
1790 fn test_write_formatted_response_empty_message() {
1791 let io = MockIo::new();
1792 let handler = MockHandler;
1793 let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1794 Shell::new(&TEST_TREE, handler, io);
1795
1796 let response = crate::response::Response::<DefaultConfig>::success("");
1797 shell.write_formatted_response(&response).unwrap();
1798
1799 assert_eq!(shell.io.get_output(), "\r\n");
1801 }
1802
1803 #[test]
1804 #[cfg(not(feature = "authentication"))]
1805 fn test_write_formatted_response_indented_multiline() {
1806 let io = MockIo::new();
1807 let handler = MockHandler;
1808 let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1809 Shell::new(&TEST_TREE, handler, io);
1810
1811 let response = crate::response::Response::<DefaultConfig>::success("A\r\nB\r\nC\r\nD")
1812 .indented()
1813 .without_postfix_newline();
1814 shell.write_formatted_response(&response).unwrap();
1815
1816 assert_eq!(shell.io.get_output(), " A\r\n B\r\n C\r\n D");
1818 }
1819
1820 #[test]
1821 #[cfg(not(feature = "authentication"))]
1822 fn test_inline_message_flag() {
1823 let response =
1825 crate::response::Response::<DefaultConfig>::success("... processing").inline();
1826
1827 assert!(
1828 response.inline_message,
1829 "inline() should set inline_message flag"
1830 );
1831
1832 }
1836
1837 #[test]
1838 #[cfg(not(feature = "authentication"))]
1839 fn test_resolve_path_cannot_navigate_through_command() {
1840 let io = MockIo::new();
1842 let handler = MockHandler;
1843 let shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1844 Shell::new(&TEST_TREE, handler, io);
1845
1846 let result = shell.resolve_path("test-cmd");
1848 assert!(result.is_ok(), "Should resolve path to command");
1849 if let Ok((node, _)) = result {
1850 assert!(node.is_some());
1851 if let Some(Node::Command(cmd)) = node {
1852 assert_eq!(cmd.name, "test-cmd");
1853 } else {
1854 panic!("Expected Command node");
1855 }
1856 }
1857
1858 let result = shell.resolve_path("test-cmd/invalid");
1860 assert!(
1861 result.is_err(),
1862 "Should fail when navigating through command"
1863 );
1864 assert_eq!(
1865 result.unwrap_err(),
1866 CliError::InvalidPath,
1867 "Should return InvalidPath when trying to navigate through command"
1868 );
1869
1870 let result = shell.resolve_path("test-cmd/extra/path");
1872 assert!(
1873 result.is_err(),
1874 "Should fail with multiple segments after command"
1875 );
1876 assert_eq!(
1877 result.unwrap_err(),
1878 CliError::InvalidPath,
1879 "Should return InvalidPath for multiple segments after command"
1880 );
1881 }
1882
1883 #[test]
1884 #[cfg(not(feature = "authentication"))]
1885 fn test_resolve_path_comprehensive() {
1886 let io = MockIo::new();
1887 let handler = MockHandler;
1888 let shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1889 Shell::new(&TEST_TREE, handler, io);
1890
1891 let result = shell.resolve_path("test-cmd");
1893 assert!(result.is_ok(), "Should resolve root-level command");
1894 if let Ok((node, _)) = result {
1895 assert!(node.is_some());
1896 if let Some(Node::Command(cmd)) = node {
1897 assert_eq!(cmd.name, "test-cmd");
1898 }
1899 }
1900
1901 let result = shell.resolve_path("system/reboot");
1903 assert!(result.is_ok(), "Should resolve system/reboot");
1904 if let Ok((node, _)) = result {
1905 assert!(node.is_some());
1906 if let Some(Node::Command(cmd)) = node {
1907 assert_eq!(cmd.name, "reboot");
1908 assert_eq!(cmd.description, "Reboot the system");
1909 assert_eq!(cmd.access_level, MockLevel::User);
1910 assert_eq!(cmd.kind, CommandKind::Sync);
1911 }
1912 }
1913
1914 let result = shell.resolve_path("system/status");
1916 assert!(result.is_ok(), "Should resolve system/status");
1917 if let Ok((node, _)) = result {
1918 assert!(node.is_some());
1919 if let Some(Node::Command(cmd)) = node {
1920 assert_eq!(cmd.name, "status");
1921 assert_eq!(cmd.id, "status");
1922 }
1924 }
1925
1926 let result = shell.resolve_path("system/network/status");
1928 assert!(result.is_ok(), "Should resolve system/network/status");
1929 if let Ok((node, _)) = result {
1930 assert!(node.is_some());
1931 if let Some(Node::Command(cmd)) = node {
1932 assert_eq!(cmd.name, "status");
1933 }
1934 }
1935
1936 let result = shell.resolve_path("system/hardware/led");
1938 assert!(result.is_ok(), "Should resolve system/hardware/led");
1939 if let Ok((node, _)) = result {
1940 assert!(node.is_some());
1941 if let Some(Node::Command(cmd)) = node {
1942 assert_eq!(cmd.name, "led");
1943 assert_eq!(cmd.min_args, 1);
1944 assert_eq!(cmd.max_args, 1);
1945 }
1946 }
1947
1948 let result = shell.resolve_path("nonexistent");
1950 assert!(result.is_err(), "Should fail for non-existent command");
1951 assert_eq!(
1952 result.unwrap_err(),
1953 CliError::CommandNotFound,
1954 "Should return CommandNotFound for non-existent command"
1955 );
1956
1957 let result = shell.resolve_path("invalid/path/command");
1959 assert!(result.is_err(), "Should fail for nonexistent path");
1960 assert_eq!(
1961 result.unwrap_err(),
1962 CliError::CommandNotFound,
1963 "Should return CommandNotFound when first segment doesn't exist"
1964 );
1965
1966 let result = shell.resolve_path("test-cmd/something");
1968 assert!(
1969 result.is_err(),
1970 "Should fail when navigating through command"
1971 );
1972 assert_eq!(
1973 result.unwrap_err(),
1974 CliError::InvalidPath,
1975 "Should return InvalidPath when trying to navigate through a command"
1976 );
1977
1978 let result = shell.resolve_path("system");
1980 assert!(result.is_ok(), "Should resolve directory path");
1981 if let Ok((node, _)) = result {
1982 assert!(node.is_some());
1983 if let Some(Node::Directory(dir)) = node {
1984 assert_eq!(dir.name, "system");
1985 }
1986 }
1987
1988 let result = shell.resolve_path("system/network");
1990 assert!(result.is_ok(), "Should resolve nested directory");
1991 if let Ok((node, _)) = result {
1992 assert!(node.is_some());
1993 if let Some(Node::Directory(dir)) = node {
1994 assert_eq!(dir.name, "network");
1995 }
1996 }
1997 }
1998
1999 #[test]
2000 #[cfg(not(feature = "authentication"))]
2001 fn test_resolve_path_parent_directory() {
2002 let io = MockIo::new();
2003 let handler = MockHandler;
2004 let shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
2005 Shell::new(&TEST_TREE, handler, io);
2006
2007 let result = shell.resolve_path("system/network/status");
2010 assert!(result.is_ok(), "Should resolve system/network/status");
2011 let (_, path) = result.unwrap();
2012 assert_eq!(
2016 path.len(),
2017 2,
2018 "Path should have 2 elements (system, network)"
2019 );
2020 assert_eq!(path[0], 1, "system should be at index 1 in root");
2021 assert_eq!(path[1], 3, "network should be at index 3 in system");
2022
2023 let result = shell.resolve_path("system/network/..");
2025 assert!(result.is_ok(), "Should resolve system/network/..");
2026 if let Ok((node, path)) = result {
2027 assert!(node.is_some());
2028 if let Some(Node::Directory(dir)) = node {
2029 assert_eq!(dir.name, "system", "Should be back at system directory");
2030 }
2031 assert_eq!(path.len(), 1, "Path should have 1 element");
2032 assert_eq!(path[0], 1, "system should be at index 1 in root");
2033 }
2034
2035 let result = shell.resolve_path("system/network/../..");
2037 assert!(result.is_ok(), "Should resolve system/network/../..");
2038 if let Ok((node, path)) = result {
2039 assert_eq!(path.len(), 0, "Path should be empty (at root)");
2040 assert!(node.is_none(), "Node should be None (representing root)");
2041 }
2042
2043 let result = shell.resolve_path("..");
2045 assert!(result.is_ok(), "Should handle .. at root");
2046 if let Ok((node, path)) = result {
2047 assert_eq!(path.len(), 0, "Path should stay at root");
2048 assert!(node.is_none(), "Node should be None (representing root)");
2049 }
2050 }
2051}