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 self.current_path = new_path;
794 #[cfg(feature = "history")]
795 return Ok(Response::success("")
796 .without_history()
797 .without_postfix_newline());
798 #[cfg(not(feature = "history"))]
799 return Ok(Response::success("").without_postfix_newline());
800 }
801 Some(Node::Command(cmd_meta)) => {
802 if let Some(user) = &self.current_user
805 && user.access_level < cmd_meta.access_level
806 {
807 return Err(CliError::InvalidPath);
808 }
809
810 if args.len() < cmd_meta.min_args || args.len() > cmd_meta.max_args {
812 return Err(CliError::InvalidArgumentCount {
813 expected_min: cmd_meta.min_args,
814 expected_max: cmd_meta.max_args,
815 received: args.len(),
816 });
817 }
818
819 match cmd_meta.kind {
821 CommandKind::Sync => {
822 self.handler.execute_sync(cmd_meta.id, args)
824 }
825 #[cfg(feature = "async")]
826 CommandKind::Async => {
827 Err(CliError::AsyncInSyncContext)
829 }
830 }
831 }
832 }
833 }
834
835 #[cfg(feature = "async")]
847 async fn execute_tree_path_async(&mut self, input: &str) -> Result<Response<C>, CliError> {
848 let parts: heapless::Vec<&str, 17> = input.split_whitespace().collect();
851 if parts.is_empty() {
852 return Err(CliError::CommandNotFound);
853 }
854
855 let path_str = parts[0];
856 let args = &parts[1..];
857
858 let (target_node, new_path) = self.resolve_path(path_str)?;
860
861 match target_node {
863 None | Some(Node::Directory(_)) => {
864 self.current_path = new_path;
866 #[cfg(feature = "history")]
867 return Ok(Response::success("")
868 .without_history()
869 .without_postfix_newline());
870 #[cfg(not(feature = "history"))]
871 return Ok(Response::success("").without_postfix_newline());
872 }
873 Some(Node::Command(cmd_meta)) => {
874 if let Some(user) = &self.current_user
877 && user.access_level < cmd_meta.access_level
878 {
879 return Err(CliError::InvalidPath);
880 }
881
882 if args.len() < cmd_meta.min_args || args.len() > cmd_meta.max_args {
884 return Err(CliError::InvalidArgumentCount {
885 expected_min: cmd_meta.min_args,
886 expected_max: cmd_meta.max_args,
887 received: args.len(),
888 });
889 }
890
891 match cmd_meta.kind {
893 CommandKind::Sync => {
894 self.handler.execute_sync(cmd_meta.id, args)
896 }
897 CommandKind::Async => {
898 self.handler.execute_async(cmd_meta.id, args).await
900 }
901 }
902 }
903 }
904 }
905
906 fn resolve_path(
912 &self,
913 path_str: &str,
914 ) -> Result<(Option<&'tree Node<L>>, heapless::Vec<usize, 8>), CliError> {
915 let mut working_path: heapless::Vec<usize, 8> = if path_str.starts_with('/') {
918 heapless::Vec::new() } else {
920 self.current_path.clone() };
922
923 let segments: heapless::Vec<&str, 8> = path_str
926 .trim_start_matches('/')
927 .split('/')
928 .filter(|s| !s.is_empty() && *s != ".")
929 .collect();
930
931 for (seg_idx, segment) in segments.iter().enumerate() {
933 if *segment == ".." {
934 working_path.pop();
936 continue;
937 }
938
939 let is_last_segment = seg_idx == segments.len() - 1;
940
941 let current_dir = self.get_dir_at_path(&working_path)?;
943 let mut found = false;
944
945 for (index, child) in current_dir.children.iter().enumerate() {
946 let node_level = match child {
948 Node::Command(cmd) => cmd.access_level,
949 Node::Directory(dir) => dir.access_level,
950 };
951
952 if let Some(user) = &self.current_user
953 && user.access_level < node_level
954 {
955 continue; }
957
958 if child.name() == *segment {
959 if child.is_directory() {
961 working_path
963 .push(index)
964 .map_err(|_| CliError::PathTooDeep)?;
965 } else {
966 if is_last_segment {
968 return Ok((Some(child), working_path));
969 } else {
970 return Err(CliError::InvalidPath);
972 }
973 }
974 found = true;
975 break;
976 }
977 }
978
979 if !found {
980 return Err(CliError::CommandNotFound);
981 }
982 }
983
984 if working_path.is_empty() {
987 return Ok((None, working_path));
989 }
990
991 let dir_node = self.get_node_at_path(&working_path)?;
992 Ok((Some(dir_node), working_path))
993 }
994
995 fn get_dir_at_path(
998 &self,
999 path: &heapless::Vec<usize, 8>,
1000 ) -> Result<&'tree Directory<L>, CliError> {
1001 let mut current: &Directory<L> = self.tree;
1002
1003 for &index in path.iter() {
1004 match current.children.get(index) {
1005 Some(Node::Directory(dir)) => current = dir,
1006 Some(Node::Command(_)) | None => return Err(CliError::InvalidPath),
1007 }
1008 }
1009
1010 Ok(current)
1011 }
1012
1013 fn get_node_at_path(&self, path: &heapless::Vec<usize, 8>) -> Result<&'tree Node<L>, CliError> {
1016 if path.is_empty() {
1017 return Err(CliError::InvalidPath);
1020 }
1021
1022 let parent_path: heapless::Vec<usize, 8> =
1024 path.iter().take(path.len() - 1).copied().collect();
1025 let parent_dir = self.get_dir_at_path(&parent_path)?;
1026
1027 let last_index = *path.last().ok_or(CliError::InvalidPath)?;
1028 parent_dir
1029 .children
1030 .get(last_index)
1031 .ok_or(CliError::InvalidPath)
1032 }
1033
1034 fn handle_tab(&mut self) -> Result<(), IO::Error> {
1036 #[cfg(feature = "completion")]
1037 {
1038 let current_dir = match self.get_current_dir() {
1040 Ok(dir) => dir,
1041 Err(_) => return self.generate_and_write_prompt(), };
1043
1044 let result = suggest_completions::<L, 16>(
1046 current_dir,
1047 self.input_buffer.as_str(),
1048 self.current_user.as_ref(),
1049 );
1050
1051 match result {
1052 Ok(crate::tree::completion::CompletionResult::Single { completion, .. }) => {
1053 self.input_buffer.clear();
1055 match self.input_buffer.push_str(&completion) {
1056 Ok(()) => {
1057 self.io.write_str("\r")?; let prompt = self.generate_prompt();
1060 self.io.write_str(prompt.as_str())?;
1061 self.io.write_str(self.input_buffer.as_str())?;
1062 }
1063 Err(_) => {
1064 self.io.put_char('\x07')?;
1066 }
1067 }
1068 }
1069 Ok(crate::tree::completion::CompletionResult::Multiple { all_matches, .. }) => {
1070 self.io.write_str("\r\n")?;
1072 for m in all_matches.iter() {
1073 self.io.write_str(" ")?; self.io.write_str(m.as_str())?;
1075 self.io.write_str(" ")?;
1076 }
1077 self.io.write_str("\r\n")?;
1078 self.generate_and_write_prompt()?;
1079 self.io.write_str(self.input_buffer.as_str())?;
1080 }
1081 _ => {
1082 self.io.put_char('\x07')?; }
1085 }
1086 }
1087
1088 #[cfg(not(feature = "completion"))]
1089 {
1090 self.io.put_char('\x07')?; }
1093
1094 Ok(())
1095 }
1096
1097 fn handle_history(&mut self, direction: HistoryDirection) -> Result<(), IO::Error> {
1099 #[cfg(feature = "history")]
1100 {
1101 let history_entry = match direction {
1102 HistoryDirection::Previous => self.history.previous_command(),
1103 HistoryDirection::Next => self.history.next_command(),
1104 };
1105
1106 if let Some(entry) = history_entry {
1107 self.input_buffer = entry;
1109 self.clear_line_and_redraw()?;
1111 }
1112 }
1113
1114 #[cfg(not(feature = "history"))]
1115 {
1116 let _ = direction; }
1119
1120 Ok(())
1121 }
1122
1123 fn show_help(&mut self) -> Result<(), IO::Error> {
1125 self.io.write_str(" ? - List global commands\r\n")?;
1126 self.io
1127 .write_str(" ls - List directory contents\r\n")?;
1128
1129 #[cfg(feature = "authentication")]
1130 self.io.write_str(" logout - End session\r\n")?;
1131
1132 self.io.write_str(" clear - Clear screen\r\n")?;
1133 self.io.write_str(" ESC ESC - Clear input buffer\r\n")?;
1134
1135 Ok(())
1136 }
1137
1138 fn show_ls(&mut self) -> Result<(), IO::Error> {
1140 let current_dir = match self.get_current_dir() {
1141 Ok(dir) => dir,
1142 Err(_) => {
1143 self.io.write_str("Error accessing directory\r\n")?;
1144 return Ok(());
1145 }
1146 };
1147
1148 for child in current_dir.children.iter() {
1149 let node_level = match child {
1151 Node::Command(cmd) => cmd.access_level,
1152 Node::Directory(dir) => dir.access_level,
1153 };
1154
1155 if let Some(user) = &self.current_user
1156 && user.access_level < node_level
1157 {
1158 continue; }
1160
1161 match child {
1163 Node::Command(cmd) => {
1164 self.io.write_str(" ")?;
1165 self.io.write_str(cmd.name)?;
1166 self.io.write_str(" - ")?;
1167 self.io.write_str(cmd.description)?;
1168 self.io.write_str("\r\n")?;
1169 }
1170 Node::Directory(dir) => {
1171 self.io.write_str(" ")?;
1172 self.io.write_str(dir.name)?;
1173 self.io.write_str("/ - Directory\r\n")?;
1174 }
1175 }
1176 }
1177
1178 Ok(())
1179 }
1180
1181 fn clear_line_and_redraw(&mut self) -> Result<(), IO::Error> {
1183 self.io.write_str("\r\x1b[K")?; self.generate_and_write_prompt()?;
1185 self.io.write_str(self.input_buffer.as_str())?;
1186 Ok(())
1187 }
1188
1189 pub fn io(&self) -> &IO {
1195 &self.io
1196 }
1197
1198 pub fn io_mut(&mut self) -> &mut IO {
1200 &mut self.io
1201 }
1202}
1203
1204#[cfg(test)]
1209mod tests {
1210 use super::*;
1211 use crate::auth::AccessLevel;
1212 use crate::config::DefaultConfig;
1213 use crate::io::CharIo;
1214 use crate::tree::{CommandKind, CommandMeta, Directory, Node};
1215
1216 #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
1218 enum MockLevel {
1219 User = 0,
1220 }
1221
1222 impl AccessLevel for MockLevel {
1223 fn from_str(s: &str) -> Option<Self> {
1224 match s {
1225 "User" => Some(Self::User),
1226 _ => None,
1227 }
1228 }
1229
1230 fn as_str(&self) -> &'static str {
1231 "User"
1232 }
1233 }
1234
1235 struct MockIo {
1237 output: heapless::String<512>,
1238 }
1239 impl MockIo {
1240 fn new() -> Self {
1241 Self {
1242 output: heapless::String::new(),
1243 }
1244 }
1245
1246 #[allow(dead_code)]
1247 fn get_output(&self) -> &str {
1248 &self.output
1249 }
1250 }
1251 impl CharIo for MockIo {
1252 type Error = ();
1253 fn get_char(&mut self) -> Result<Option<char>, ()> {
1254 Ok(None)
1255 }
1256 fn put_char(&mut self, c: char) -> Result<(), ()> {
1257 self.output.push(c).map_err(|_| ())
1258 }
1259 fn write_str(&mut self, s: &str) -> Result<(), ()> {
1260 self.output.push_str(s).map_err(|_| ())
1261 }
1262 }
1263
1264 struct MockHandler;
1266 impl CommandHandler<DefaultConfig> for MockHandler {
1267 fn execute_sync(
1268 &self,
1269 _id: &str,
1270 _args: &[&str],
1271 ) -> Result<crate::response::Response<DefaultConfig>, crate::error::CliError> {
1272 Err(crate::error::CliError::CommandNotFound)
1273 }
1274
1275 #[cfg(feature = "async")]
1276 async fn execute_async(
1277 &self,
1278 _id: &str,
1279 _args: &[&str],
1280 ) -> Result<crate::response::Response<DefaultConfig>, crate::error::CliError> {
1281 Err(crate::error::CliError::CommandNotFound)
1282 }
1283 }
1284
1285 const CMD_TEST: CommandMeta<MockLevel> = CommandMeta {
1287 id: "test-cmd",
1288 name: "test-cmd",
1289 description: "Test command",
1290 access_level: MockLevel::User,
1291 kind: CommandKind::Sync,
1292 min_args: 0,
1293 max_args: 0,
1294 };
1295
1296 const CMD_REBOOT: CommandMeta<MockLevel> = CommandMeta {
1297 id: "reboot",
1298 name: "reboot",
1299 description: "Reboot the system",
1300 access_level: MockLevel::User,
1301 kind: CommandKind::Sync,
1302 min_args: 0,
1303 max_args: 0,
1304 };
1305
1306 const CMD_STATUS: CommandMeta<MockLevel> = CommandMeta {
1307 id: "status",
1308 name: "status",
1309 description: "Show status",
1310 access_level: MockLevel::User,
1311 kind: CommandKind::Sync,
1312 min_args: 0,
1313 max_args: 0,
1314 };
1315
1316 const CMD_LED: CommandMeta<MockLevel> = CommandMeta {
1317 id: "led",
1318 name: "led",
1319 description: "Control LED",
1320 access_level: MockLevel::User,
1321 kind: CommandKind::Sync,
1322 min_args: 1,
1323 max_args: 1,
1324 };
1325
1326 const CMD_NETWORK_STATUS: CommandMeta<MockLevel> = CommandMeta {
1327 id: "network_status",
1328 name: "status",
1329 description: "Network status",
1330 access_level: MockLevel::User,
1331 kind: CommandKind::Sync,
1332 min_args: 0,
1333 max_args: 0,
1334 };
1335
1336 const DIR_HARDWARE: Directory<MockLevel> = Directory {
1338 name: "hardware",
1339 children: &[Node::Command(&CMD_LED)],
1340 access_level: MockLevel::User,
1341 };
1342
1343 const DIR_NETWORK: Directory<MockLevel> = Directory {
1344 name: "network",
1345 children: &[Node::Command(&CMD_NETWORK_STATUS)],
1346 access_level: MockLevel::User,
1347 };
1348
1349 const DIR_SYSTEM: Directory<MockLevel> = Directory {
1350 name: "system",
1351 children: &[
1352 Node::Command(&CMD_REBOOT),
1353 Node::Command(&CMD_STATUS),
1354 Node::Directory(&DIR_HARDWARE),
1355 Node::Directory(&DIR_NETWORK),
1356 ],
1357 access_level: MockLevel::User,
1358 };
1359
1360 const TEST_TREE: Directory<MockLevel> = Directory {
1362 name: "/",
1363 children: &[Node::Command(&CMD_TEST), Node::Directory(&DIR_SYSTEM)],
1364 access_level: MockLevel::User,
1365 };
1366
1367 #[test]
1368 fn test_request_command_no_args() {
1369 let mut path = heapless::String::<128>::new();
1370 path.push_str("help").unwrap();
1371 let args = heapless::Vec::new();
1372 #[cfg(feature = "history")]
1373 let original = {
1374 let mut s = heapless::String::<128>::new();
1375 s.push_str("help").unwrap();
1376 s
1377 };
1378
1379 let request = Request::<DefaultConfig>::Command {
1380 path,
1381 args,
1382 #[cfg(feature = "history")]
1383 original,
1384 _phantom: core::marker::PhantomData,
1385 };
1386
1387 match request {
1388 Request::Command { path, args, .. } => {
1389 assert_eq!(path.as_str(), "help");
1390 assert_eq!(args.len(), 0);
1391 }
1392 #[allow(unreachable_patterns)]
1393 _ => panic!("Expected Command variant"),
1394 }
1395 }
1396
1397 #[test]
1398 fn test_request_command_with_args() {
1399 let mut path = heapless::String::<128>::new();
1400 path.push_str("echo").unwrap();
1401
1402 let mut args = heapless::Vec::new();
1403 let mut hello = heapless::String::<128>::new();
1404 hello.push_str("hello").unwrap();
1405 let mut world = heapless::String::<128>::new();
1406 world.push_str("world").unwrap();
1407 args.push(hello).unwrap();
1408 args.push(world).unwrap();
1409
1410 #[cfg(feature = "history")]
1411 let original = {
1412 let mut s = heapless::String::<128>::new();
1413 s.push_str("echo hello world").unwrap();
1414 s
1415 };
1416
1417 let request = Request::<DefaultConfig>::Command {
1418 path,
1419 args,
1420 #[cfg(feature = "history")]
1421 original,
1422 _phantom: core::marker::PhantomData,
1423 };
1424
1425 match request {
1426 Request::Command { path, args, .. } => {
1427 assert_eq!(path.as_str(), "echo");
1428 assert_eq!(args.len(), 2);
1429 assert_eq!(args[0].as_str(), "hello");
1430 assert_eq!(args[1].as_str(), "world");
1431 }
1432 #[allow(unreachable_patterns)]
1433 _ => panic!("Expected Command variant"),
1434 }
1435 }
1436
1437 #[test]
1438 #[cfg(feature = "history")]
1439 fn test_request_command_with_original() {
1440 let mut path = heapless::String::<128>::new();
1441 path.push_str("reboot").unwrap();
1442 let mut original = heapless::String::<128>::new();
1443 original.push_str("reboot").unwrap();
1444
1445 let request = Request::<DefaultConfig>::Command {
1446 path,
1447 args: heapless::Vec::new(),
1448 original,
1449 _phantom: core::marker::PhantomData,
1450 };
1451
1452 match request {
1453 Request::Command { path, original, .. } => {
1454 assert_eq!(path.as_str(), "reboot");
1455 assert_eq!(original.as_str(), "reboot");
1456 }
1457 #[allow(unreachable_patterns)]
1458 _ => panic!("Expected Command variant"),
1459 }
1460 }
1461
1462 #[test]
1463 #[cfg(feature = "authentication")]
1464 fn test_request_login() {
1465 let mut username = heapless::String::<32>::new();
1466 username.push_str("admin").unwrap();
1467 let mut password = heapless::String::<64>::new();
1468 password.push_str("secret123").unwrap();
1469
1470 let request = Request::<DefaultConfig>::Login { username, password };
1471
1472 match request {
1473 Request::Login { username, password } => {
1474 assert_eq!(username.as_str(), "admin");
1475 assert_eq!(password.as_str(), "secret123");
1476 }
1477 #[allow(unreachable_patterns)]
1478 _ => panic!("Expected Login variant"),
1479 }
1480 }
1481
1482 #[test]
1483 #[cfg(feature = "authentication")]
1484 fn test_request_invalid_login() {
1485 let request = Request::<DefaultConfig>::InvalidLogin;
1486
1487 match request {
1488 Request::InvalidLogin => {}
1489 #[allow(unreachable_patterns)]
1490 _ => panic!("Expected InvalidLogin variant"),
1491 }
1492 }
1493
1494 #[test]
1495 #[cfg(feature = "completion")]
1496 fn test_request_tab_complete() {
1497 let mut path = heapless::String::<128>::new();
1498 path.push_str("sys").unwrap();
1499
1500 let request = Request::<DefaultConfig>::TabComplete { path };
1501
1502 match request {
1503 Request::TabComplete { path } => {
1504 assert_eq!(path.as_str(), "sys");
1505 }
1506 #[allow(unreachable_patterns)]
1507 _ => panic!("Expected TabComplete variant"),
1508 }
1509 }
1510
1511 #[test]
1512 #[cfg(feature = "completion")]
1513 fn test_request_tab_complete_empty() {
1514 let request = Request::<DefaultConfig>::TabComplete {
1515 path: heapless::String::new(),
1516 };
1517
1518 match request {
1519 Request::TabComplete { path } => {
1520 assert_eq!(path.as_str(), "");
1521 }
1522 #[allow(unreachable_patterns)]
1523 _ => panic!("Expected TabComplete variant"),
1524 }
1525 }
1526
1527 #[test]
1528 #[cfg(feature = "history")]
1529 fn test_request_history_previous() {
1530 let mut buffer = heapless::String::<128>::new();
1531 buffer.push_str("current input").unwrap();
1532
1533 let request = Request::<DefaultConfig>::History {
1534 direction: HistoryDirection::Previous,
1535 buffer,
1536 };
1537
1538 match request {
1539 Request::History { direction, buffer } => {
1540 assert_eq!(direction, HistoryDirection::Previous);
1541 assert_eq!(buffer.as_str(), "current input");
1542 }
1543 #[allow(unreachable_patterns)]
1544 _ => panic!("Expected History variant"),
1545 }
1546 }
1547
1548 #[test]
1549 #[cfg(feature = "history")]
1550 fn test_request_history_next() {
1551 let request = Request::<DefaultConfig>::History {
1552 direction: HistoryDirection::Next,
1553 buffer: heapless::String::new(),
1554 };
1555
1556 match request {
1557 Request::History { direction, buffer } => {
1558 assert_eq!(direction, HistoryDirection::Next);
1559 assert_eq!(buffer.as_str(), "");
1560 }
1561 #[allow(unreachable_patterns)]
1562 _ => panic!("Expected History variant"),
1563 }
1564 }
1565
1566 #[test]
1567 fn test_request_variants_match_features() {
1568 let _cmd = Request::<DefaultConfig>::Command {
1569 path: heapless::String::new(),
1570 args: heapless::Vec::new(),
1571 #[cfg(feature = "history")]
1572 original: heapless::String::new(),
1573 _phantom: core::marker::PhantomData,
1574 };
1575
1576 #[cfg(feature = "authentication")]
1577 let _login = Request::<DefaultConfig>::Login {
1578 username: heapless::String::new(),
1579 password: heapless::String::new(),
1580 };
1581
1582 #[cfg(feature = "completion")]
1583 let _complete = Request::<DefaultConfig>::TabComplete {
1584 path: heapless::String::new(),
1585 };
1586
1587 #[cfg(feature = "history")]
1588 let _history = Request::<DefaultConfig>::History {
1589 direction: HistoryDirection::Previous,
1590 buffer: heapless::String::new(),
1591 };
1592 }
1593
1594 #[test]
1595 fn test_activate_deactivate_lifecycle() {
1596 let io = MockIo::new();
1597 let handler = MockHandler;
1598
1599 #[cfg(feature = "authentication")]
1601 {
1602 use crate::auth::CredentialProvider;
1603 struct MockProvider;
1604 impl CredentialProvider<MockLevel> for MockProvider {
1605 type Error = ();
1606 fn find_user(
1607 &self,
1608 _username: &str,
1609 ) -> Result<Option<crate::auth::User<MockLevel>>, ()> {
1610 Ok(None)
1611 }
1612 fn verify_password(
1613 &self,
1614 _user: &crate::auth::User<MockLevel>,
1615 _password: &str,
1616 ) -> bool {
1617 false
1618 }
1619 }
1620 let provider = MockProvider;
1621 let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1622 Shell::new(&TEST_TREE, handler, &provider, io);
1623
1624 assert_eq!(shell.state, CliState::Inactive);
1626 assert!(shell.current_user.is_none());
1627
1628 shell.activate().unwrap();
1630 assert_eq!(shell.state, CliState::LoggedOut);
1631
1632 shell.deactivate();
1634 assert_eq!(shell.state, CliState::Inactive);
1635 assert!(shell.current_user.is_none());
1636 assert!(shell.input_buffer.is_empty());
1637 assert!(shell.current_path.is_empty());
1638 }
1639
1640 #[cfg(not(feature = "authentication"))]
1641 {
1642 let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1643 Shell::new(&TEST_TREE, handler, io);
1644
1645 assert_eq!(shell.state, CliState::Inactive);
1647
1648 shell.activate().unwrap();
1650 assert_eq!(shell.state, CliState::LoggedIn);
1651
1652 shell.deactivate();
1654 assert_eq!(shell.state, CliState::Inactive);
1655 assert!(shell.current_user.is_none());
1656 assert!(shell.input_buffer.is_empty());
1657 assert!(shell.current_path.is_empty());
1658 }
1659 }
1660
1661 #[test]
1662 #[cfg(not(feature = "authentication"))]
1663 fn test_write_formatted_response_default() {
1664 let io = MockIo::new();
1666 let handler = MockHandler;
1667 let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1668 Shell::new(&TEST_TREE, handler, io);
1669
1670 let response = crate::response::Response::<DefaultConfig>::success("Test message");
1671 shell.write_formatted_response(&response).unwrap();
1672
1673 assert_eq!(shell.io.get_output(), "Test message\r\n");
1675 }
1676
1677 #[test]
1678 #[cfg(not(feature = "authentication"))]
1679 fn test_write_formatted_response_with_prefix_newline() {
1680 let io = MockIo::new();
1681 let handler = MockHandler;
1682 let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1683 Shell::new(&TEST_TREE, handler, io);
1684
1685 let response =
1686 crate::response::Response::<DefaultConfig>::success("Test").with_prefix_newline();
1687 shell.write_formatted_response(&response).unwrap();
1688
1689 assert_eq!(shell.io.get_output(), "\r\nTest\r\n");
1691 }
1692
1693 #[test]
1694 #[cfg(not(feature = "authentication"))]
1695 fn test_write_formatted_response_indented() {
1696 let io = MockIo::new();
1697 let handler = MockHandler;
1698 let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1699 Shell::new(&TEST_TREE, handler, io);
1700
1701 let response =
1702 crate::response::Response::<DefaultConfig>::success("Line 1\r\nLine 2").indented();
1703 shell.write_formatted_response(&response).unwrap();
1704
1705 assert_eq!(shell.io.get_output(), " Line 1\r\n Line 2\r\n");
1707 }
1708
1709 #[test]
1710 #[cfg(not(feature = "authentication"))]
1711 fn test_write_formatted_response_indented_single_line() {
1712 let io = MockIo::new();
1713 let handler = MockHandler;
1714 let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1715 Shell::new(&TEST_TREE, handler, io);
1716
1717 let response =
1718 crate::response::Response::<DefaultConfig>::success("Single line").indented();
1719 shell.write_formatted_response(&response).unwrap();
1720
1721 assert_eq!(shell.io.get_output(), " Single line\r\n");
1723 }
1724
1725 #[test]
1726 #[cfg(not(feature = "authentication"))]
1727 fn test_write_formatted_response_without_postfix_newline() {
1728 let io = MockIo::new();
1729 let handler = MockHandler;
1730 let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1731 Shell::new(&TEST_TREE, handler, io);
1732
1733 let response = crate::response::Response::<DefaultConfig>::success("No newline")
1734 .without_postfix_newline();
1735 shell.write_formatted_response(&response).unwrap();
1736
1737 assert_eq!(shell.io.get_output(), "No newline");
1739 }
1740
1741 #[test]
1742 #[cfg(not(feature = "authentication"))]
1743 fn test_write_formatted_response_combined_flags() {
1744 let io = MockIo::new();
1745 let handler = MockHandler;
1746 let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1747 Shell::new(&TEST_TREE, handler, io);
1748
1749 let response = crate::response::Response::<DefaultConfig>::success("Multi\r\nLine")
1750 .with_prefix_newline()
1751 .indented();
1752 shell.write_formatted_response(&response).unwrap();
1753
1754 assert_eq!(shell.io.get_output(), "\r\n Multi\r\n Line\r\n");
1756 }
1757
1758 #[test]
1759 #[cfg(not(feature = "authentication"))]
1760 fn test_write_formatted_response_all_flags_off() {
1761 let io = MockIo::new();
1762 let handler = MockHandler;
1763 let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1764 Shell::new(&TEST_TREE, handler, io);
1765
1766 let response =
1767 crate::response::Response::<DefaultConfig>::success("Raw").without_postfix_newline();
1768 shell.write_formatted_response(&response).unwrap();
1769
1770 assert_eq!(shell.io.get_output(), "Raw");
1772 }
1773
1774 #[test]
1775 #[cfg(not(feature = "authentication"))]
1776 fn test_write_formatted_response_empty_message() {
1777 let io = MockIo::new();
1778 let handler = MockHandler;
1779 let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1780 Shell::new(&TEST_TREE, handler, io);
1781
1782 let response = crate::response::Response::<DefaultConfig>::success("");
1783 shell.write_formatted_response(&response).unwrap();
1784
1785 assert_eq!(shell.io.get_output(), "\r\n");
1787 }
1788
1789 #[test]
1790 #[cfg(not(feature = "authentication"))]
1791 fn test_write_formatted_response_indented_multiline() {
1792 let io = MockIo::new();
1793 let handler = MockHandler;
1794 let mut shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1795 Shell::new(&TEST_TREE, handler, io);
1796
1797 let response = crate::response::Response::<DefaultConfig>::success("A\r\nB\r\nC\r\nD")
1798 .indented()
1799 .without_postfix_newline();
1800 shell.write_formatted_response(&response).unwrap();
1801
1802 assert_eq!(shell.io.get_output(), " A\r\n B\r\n C\r\n D");
1804 }
1805
1806 #[test]
1807 #[cfg(not(feature = "authentication"))]
1808 fn test_inline_message_flag() {
1809 let response =
1811 crate::response::Response::<DefaultConfig>::success("... processing").inline();
1812
1813 assert!(
1814 response.inline_message,
1815 "inline() should set inline_message flag"
1816 );
1817
1818 }
1822
1823 #[test]
1824 #[cfg(not(feature = "authentication"))]
1825 fn test_resolve_path_cannot_navigate_through_command() {
1826 let io = MockIo::new();
1828 let handler = MockHandler;
1829 let shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1830 Shell::new(&TEST_TREE, handler, io);
1831
1832 let result = shell.resolve_path("test-cmd");
1834 assert!(result.is_ok(), "Should resolve path to command");
1835 if let Ok((node, _)) = result {
1836 assert!(node.is_some());
1837 if let Some(Node::Command(cmd)) = node {
1838 assert_eq!(cmd.name, "test-cmd");
1839 } else {
1840 panic!("Expected Command node");
1841 }
1842 }
1843
1844 let result = shell.resolve_path("test-cmd/invalid");
1846 assert!(
1847 result.is_err(),
1848 "Should fail when navigating through command"
1849 );
1850 assert_eq!(
1851 result.unwrap_err(),
1852 CliError::InvalidPath,
1853 "Should return InvalidPath when trying to navigate through command"
1854 );
1855
1856 let result = shell.resolve_path("test-cmd/extra/path");
1858 assert!(
1859 result.is_err(),
1860 "Should fail with multiple segments after command"
1861 );
1862 assert_eq!(
1863 result.unwrap_err(),
1864 CliError::InvalidPath,
1865 "Should return InvalidPath for multiple segments after command"
1866 );
1867 }
1868
1869 #[test]
1870 #[cfg(not(feature = "authentication"))]
1871 fn test_resolve_path_comprehensive() {
1872 let io = MockIo::new();
1873 let handler = MockHandler;
1874 let shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1875 Shell::new(&TEST_TREE, handler, io);
1876
1877 let result = shell.resolve_path("test-cmd");
1879 assert!(result.is_ok(), "Should resolve root-level command");
1880 if let Ok((node, _)) = result {
1881 assert!(node.is_some());
1882 if let Some(Node::Command(cmd)) = node {
1883 assert_eq!(cmd.name, "test-cmd");
1884 }
1885 }
1886
1887 let result = shell.resolve_path("system/reboot");
1889 assert!(result.is_ok(), "Should resolve system/reboot");
1890 if let Ok((node, _)) = result {
1891 assert!(node.is_some());
1892 if let Some(Node::Command(cmd)) = node {
1893 assert_eq!(cmd.name, "reboot");
1894 assert_eq!(cmd.description, "Reboot the system");
1895 assert_eq!(cmd.access_level, MockLevel::User);
1896 assert_eq!(cmd.kind, CommandKind::Sync);
1897 }
1898 }
1899
1900 let result = shell.resolve_path("system/status");
1902 assert!(result.is_ok(), "Should resolve system/status");
1903 if let Ok((node, _)) = result {
1904 assert!(node.is_some());
1905 if let Some(Node::Command(cmd)) = node {
1906 assert_eq!(cmd.name, "status");
1907 assert_eq!(cmd.id, "status");
1908 }
1910 }
1911
1912 let result = shell.resolve_path("system/network/status");
1914 assert!(result.is_ok(), "Should resolve system/network/status");
1915 if let Ok((node, _)) = result {
1916 assert!(node.is_some());
1917 if let Some(Node::Command(cmd)) = node {
1918 assert_eq!(cmd.name, "status");
1919 }
1920 }
1921
1922 let result = shell.resolve_path("system/hardware/led");
1924 assert!(result.is_ok(), "Should resolve system/hardware/led");
1925 if let Ok((node, _)) = result {
1926 assert!(node.is_some());
1927 if let Some(Node::Command(cmd)) = node {
1928 assert_eq!(cmd.name, "led");
1929 assert_eq!(cmd.min_args, 1);
1930 assert_eq!(cmd.max_args, 1);
1931 }
1932 }
1933
1934 let result = shell.resolve_path("nonexistent");
1936 assert!(result.is_err(), "Should fail for non-existent command");
1937 assert_eq!(
1938 result.unwrap_err(),
1939 CliError::CommandNotFound,
1940 "Should return CommandNotFound for non-existent command"
1941 );
1942
1943 let result = shell.resolve_path("invalid/path/command");
1945 assert!(result.is_err(), "Should fail for nonexistent path");
1946 assert_eq!(
1947 result.unwrap_err(),
1948 CliError::CommandNotFound,
1949 "Should return CommandNotFound when first segment doesn't exist"
1950 );
1951
1952 let result = shell.resolve_path("test-cmd/something");
1954 assert!(
1955 result.is_err(),
1956 "Should fail when navigating through command"
1957 );
1958 assert_eq!(
1959 result.unwrap_err(),
1960 CliError::InvalidPath,
1961 "Should return InvalidPath when trying to navigate through a command"
1962 );
1963
1964 let result = shell.resolve_path("system");
1966 assert!(result.is_ok(), "Should resolve directory path");
1967 if let Ok((node, _)) = result {
1968 assert!(node.is_some());
1969 if let Some(Node::Directory(dir)) = node {
1970 assert_eq!(dir.name, "system");
1971 }
1972 }
1973
1974 let result = shell.resolve_path("system/network");
1976 assert!(result.is_ok(), "Should resolve nested directory");
1977 if let Ok((node, _)) = result {
1978 assert!(node.is_some());
1979 if let Some(Node::Directory(dir)) = node {
1980 assert_eq!(dir.name, "network");
1981 }
1982 }
1983 }
1984
1985 #[test]
1986 #[cfg(not(feature = "authentication"))]
1987 fn test_resolve_path_parent_directory() {
1988 let io = MockIo::new();
1989 let handler = MockHandler;
1990 let shell: Shell<MockLevel, MockIo, MockHandler, DefaultConfig> =
1991 Shell::new(&TEST_TREE, handler, io);
1992
1993 let result = shell.resolve_path("system/network/status");
1996 assert!(result.is_ok(), "Should resolve system/network/status");
1997 let (_, path) = result.unwrap();
1998 assert_eq!(
2002 path.len(),
2003 2,
2004 "Path should have 2 elements (system, network)"
2005 );
2006 assert_eq!(path[0], 1, "system should be at index 1 in root");
2007 assert_eq!(path[1], 3, "network should be at index 3 in system");
2008
2009 let result = shell.resolve_path("system/network/..");
2011 assert!(result.is_ok(), "Should resolve system/network/..");
2012 if let Ok((node, path)) = result {
2013 assert!(node.is_some());
2014 if let Some(Node::Directory(dir)) = node {
2015 assert_eq!(dir.name, "system", "Should be back at system directory");
2016 }
2017 assert_eq!(path.len(), 1, "Path should have 1 element");
2018 assert_eq!(path[0], 1, "system should be at index 1 in root");
2019 }
2020
2021 let result = shell.resolve_path("system/network/../..");
2023 assert!(result.is_ok(), "Should resolve system/network/../..");
2024 if let Ok((node, path)) = result {
2025 assert_eq!(path.len(), 0, "Path should be empty (at root)");
2026 assert!(node.is_none(), "Node should be None (representing root)");
2027 }
2028
2029 let result = shell.resolve_path("..");
2031 assert!(result.is_ok(), "Should handle .. at root");
2032 if let Ok((node, path)) = result {
2033 assert_eq!(path.len(), 0, "Path should stay at root");
2034 assert!(node.is_none(), "Node should be None (representing root)");
2035 }
2036 }
2037}