Skip to main content

nut_shell/tree/
path.rs

1//! Path parsing and navigation.
2//!
3//! Unix-style path resolution with `.` and `..` support.
4
5use crate::error::CliError;
6
7/// Unix-style path parser and representation.
8///
9/// Handles absolute and relative paths with `.` and `..` navigation.
10/// Zero-allocation parsing using string slices.
11///
12/// # Path Syntax
13///
14/// - **Absolute paths**: Start with `/` (e.g., `/system/reboot`)
15/// - **Relative paths**: No leading `/` (e.g., `network/status`, `../hw`)
16/// - **Parent navigation**: `..` goes up one level
17/// - **Current directory**: `.` stays at current level
18///
19/// # Memory
20///
21/// Uses `MAX_DEPTH` const generic to limit nesting depth.
22/// All parsing is zero-allocation, working with string slices.
23#[derive(Debug, PartialEq)]
24pub struct Path<'a, const MAX_DEPTH: usize> {
25    /// Whether path starts with `/`
26    is_absolute: bool,
27
28    /// Path segments (includes `.` and `..` for resolution)
29    segments: heapless::Vec<&'a str, MAX_DEPTH>,
30}
31
32impl<'a, const MAX_DEPTH: usize> Path<'a, MAX_DEPTH> {
33    /// Parse path string into Path structure.
34    ///
35    /// Supports absolute (`/system/reboot`), relative (`cmd`, `./cmd`), and parent (`..`) paths.
36    ///
37    /// Returns `InvalidPath` for empty input or `PathTooDeep` if MAX_DEPTH exceeded.
38    pub fn parse(input: &'a str) -> Result<Self, CliError> {
39        // Handle empty path
40        if input.is_empty() {
41            return Err(CliError::InvalidPath);
42        }
43
44        // Check if absolute (starts with /)
45        let is_absolute = input.starts_with('/');
46
47        // Remove leading slash for parsing
48        let path_str = if is_absolute { &input[1..] } else { input };
49
50        // Parse segments
51        let mut segments = heapless::Vec::new();
52
53        // Empty path after removing leading slash means root directory
54        if path_str.is_empty() {
55            // Absolute path "/" refers to root
56            if is_absolute {
57                return Ok(Self {
58                    is_absolute,
59                    segments,
60                });
61            } else {
62                // Relative empty path is invalid
63                return Err(CliError::InvalidPath);
64            }
65        }
66
67        // Split by '/' and filter empty segments
68        for segment in path_str.split('/') {
69            // Skip empty segments (e.g., from "//" or trailing "/")
70            if segment.is_empty() {
71                continue;
72            }
73
74            // Add segment
75            segments.push(segment).map_err(|_| CliError::PathTooDeep)?;
76        }
77
78        Ok(Self {
79            is_absolute,
80            segments,
81        })
82    }
83
84    /// Returns true if this is an absolute path (starts with `/`).
85    pub fn is_absolute(&self) -> bool {
86        self.is_absolute
87    }
88
89    /// Get path segments as slice.
90    pub fn segments(&self) -> &[&'a str] {
91        &self.segments
92    }
93
94    /// Returns the number of segments in this path.
95    pub fn segment_count(&self) -> usize {
96        self.segments.len()
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use crate::config::{DefaultConfig, MinimalConfig, ShellConfig};
104
105    // Use DefaultConfig's MAX_PATH_DEPTH = 8 for most tests
106    type TestPath<'a> = Path<'a, { DefaultConfig::MAX_PATH_DEPTH }>;
107
108    #[test]
109    fn test_empty_path_is_invalid() {
110        let result = TestPath::parse("");
111        assert_eq!(result, Err(CliError::InvalidPath));
112    }
113
114    #[test]
115    fn test_absolute_root() {
116        let path = TestPath::parse("/").unwrap();
117        assert!(path.is_absolute());
118        assert_eq!(path.segments(), &[] as &[&str]);
119        assert_eq!(path.segment_count(), 0);
120    }
121
122    #[test]
123    fn test_absolute_single_segment() {
124        let path = TestPath::parse("/system").unwrap();
125        assert!(path.is_absolute());
126        assert_eq!(path.segments(), &["system"]);
127        assert_eq!(path.segment_count(), 1);
128    }
129
130    #[test]
131    fn test_absolute_multiple_segments() {
132        let path = TestPath::parse("/system/network/status").unwrap();
133        assert!(path.is_absolute());
134        assert_eq!(path.segments(), &["system", "network", "status"]);
135        assert_eq!(path.segment_count(), 3);
136    }
137
138    #[test]
139    fn test_relative_single_segment() {
140        let path = TestPath::parse("help").unwrap();
141        assert!(!path.is_absolute());
142        assert_eq!(path.segments(), &["help"]);
143    }
144
145    #[test]
146    fn test_relative_multiple_segments() {
147        let path = TestPath::parse("system/network").unwrap();
148        assert!(!path.is_absolute());
149        assert_eq!(path.segments(), &["system", "network"]);
150    }
151
152    #[test]
153    fn test_parent_navigation() {
154        let path = TestPath::parse("..").unwrap();
155        assert!(!path.is_absolute());
156        assert_eq!(path.segments(), &[".."]);
157
158        let path = TestPath::parse("../system").unwrap();
159        assert_eq!(path.segments(), &["..", "system"]);
160
161        let path = TestPath::parse("../../hw/led").unwrap();
162        assert_eq!(path.segments(), &["..", "..", "hw", "led"]);
163    }
164
165    #[test]
166    fn test_current_directory() {
167        let path = TestPath::parse(".").unwrap();
168        assert!(!path.is_absolute());
169        assert_eq!(path.segments(), &["."]);
170
171        let path = TestPath::parse("./cmd").unwrap();
172        assert_eq!(path.segments(), &[".", "cmd"]);
173    }
174
175    #[test]
176    fn test_mixed_navigation() {
177        let path = TestPath::parse("../system/./network").unwrap();
178        assert_eq!(path.segments(), &["..", "system", ".", "network"]);
179    }
180
181    #[test]
182    fn test_trailing_slash_ignored() {
183        let path = TestPath::parse("/system/").unwrap();
184        assert!(path.is_absolute());
185        assert_eq!(path.segments(), &["system"]);
186
187        let path = TestPath::parse("network/").unwrap();
188        assert!(!path.is_absolute());
189        assert_eq!(path.segments(), &["network"]);
190    }
191
192    #[test]
193    fn test_double_slash_treated_as_single() {
194        let path = TestPath::parse("/system//network").unwrap();
195        assert_eq!(path.segments(), &["system", "network"]);
196
197        let path = TestPath::parse("//system").unwrap();
198        assert!(path.is_absolute());
199        assert_eq!(path.segments(), &["system"]);
200    }
201
202    #[test]
203    fn test_path_too_deep() {
204        // Build a path that exceeds MAX_PATH_DEPTH (8 for DefaultConfig)
205        let deep_path = "a/b/c/d/e/f/g/h/i/j/k";
206        let result = TestPath::parse(deep_path);
207        assert_eq!(result, Err(CliError::PathTooDeep));
208    }
209
210    #[test]
211    fn test_max_depth_exactly() {
212        // MAX_PATH_DEPTH = 8 for DefaultConfig
213        let path = TestPath::parse("a/b/c/d/e/f/g/h").unwrap();
214        assert_eq!(path.segment_count(), 8);
215    }
216
217    #[test]
218    fn test_absolute_path_with_parent() {
219        let path = TestPath::parse("/../system").unwrap();
220        assert!(path.is_absolute());
221        assert_eq!(path.segments(), &["..", "system"]);
222    }
223
224    #[test]
225    fn test_minimal_config_respects_depth() {
226        type MinimalPath<'a> = Path<'a, { MinimalConfig::MAX_PATH_DEPTH }>;
227
228        // MinimalConfig has MAX_PATH_DEPTH = 4
229        // Exactly 4 segments should succeed
230        let path = MinimalPath::parse("a/b/c/d").unwrap();
231        assert_eq!(path.segment_count(), 4);
232
233        // 5 segments should fail
234        let result = MinimalPath::parse("a/b/c/d/e");
235        assert_eq!(result, Err(CliError::PathTooDeep));
236    }
237
238    #[test]
239    fn test_default_config_allows_deeper_paths() {
240        // DefaultConfig has MAX_PATH_DEPTH = 8
241        let path = TestPath::parse("a/b/c/d/e/f/g/h").unwrap();
242        assert_eq!(path.segment_count(), 8);
243
244        // But MinimalConfig (depth=4) doesn't allow this
245        type MinimalPath<'a> = Path<'a, { MinimalConfig::MAX_PATH_DEPTH }>;
246        let result = MinimalPath::parse("a/b/c/d/e/f/g/h");
247        assert_eq!(result, Err(CliError::PathTooDeep));
248    }
249}