⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/process/
cwd.rs

1//! Per-Process Working Directory
2//!
3//! Tracks and resolves the current working directory for each process.
4//! Provides path normalization and resolution of relative paths.
5
6// Per-process working directory
7
8#[cfg(feature = "alloc")]
9extern crate alloc;
10
11#[cfg(feature = "alloc")]
12use alloc::string::String;
13#[cfg(feature = "alloc")]
14use alloc::vec::Vec;
15
16use crate::error::KernelError;
17
18// ---------------------------------------------------------------------------
19// ProcessCwd
20// ---------------------------------------------------------------------------
21
22/// Per-process current working directory state.
23#[cfg(feature = "alloc")]
24pub struct ProcessCwd {
25    /// The current working directory as an absolute path.
26    path: String,
27}
28
29#[cfg(feature = "alloc")]
30impl Default for ProcessCwd {
31    fn default() -> Self {
32        Self::new()
33    }
34}
35
36#[cfg(feature = "alloc")]
37impl ProcessCwd {
38    /// Create a new `ProcessCwd` with the root directory as the default.
39    pub fn new() -> Self {
40        Self {
41            path: String::from("/"),
42        }
43    }
44
45    /// Create a `ProcessCwd` with a specific initial directory.
46    pub fn with_path(path: &str) -> Result<Self, KernelError> {
47        if !path.starts_with('/') {
48            return Err(KernelError::InvalidArgument {
49                name: "path",
50                value: "initial CWD must be an absolute path",
51            });
52        }
53        let normalized = normalize_path(path);
54        Ok(Self { path: normalized })
55    }
56
57    /// Get the current working directory.
58    pub fn get(&self) -> &str {
59        &self.path
60    }
61
62    /// Set the current working directory.
63    ///
64    /// The path must be absolute. It is normalized before storage.
65    pub fn set(&mut self, path: &str) -> Result<(), KernelError> {
66        if !path.starts_with('/') {
67            return Err(KernelError::InvalidArgument {
68                name: "path",
69                value: "CWD must be an absolute path",
70            });
71        }
72
73        if path.is_empty() {
74            return Err(KernelError::InvalidArgument {
75                name: "path",
76                value: "path cannot be empty",
77            });
78        }
79
80        self.path = normalize_path(path);
81        Ok(())
82    }
83
84    /// Resolve a potentially relative path against this CWD.
85    ///
86    /// - Absolute paths (starting with `/`) are returned normalized.
87    /// - Relative paths are joined with the CWD and normalized.
88    pub fn resolve(&self, relative: &str) -> String {
89        resolve_path(relative, &self.path)
90    }
91}
92
93// ---------------------------------------------------------------------------
94// Path Resolution and Normalization (free functions)
95// ---------------------------------------------------------------------------
96
97/// Resolve a potentially relative path against a given working directory.
98///
99/// - If `path` starts with `/`, it is treated as absolute and normalized.
100/// - Otherwise, `path` is appended to `cwd` with a `/` separator and
101///   normalized.
102#[cfg(feature = "alloc")]
103pub fn resolve_path(path: &str, cwd: &str) -> String {
104    if path.starts_with('/') {
105        // Absolute path -- just normalize.
106        normalize_path(path)
107    } else {
108        // Relative path -- join with CWD.
109        let mut combined = String::with_capacity(cwd.len() + 1 + path.len());
110        combined.push_str(cwd);
111        if !cwd.ends_with('/') {
112            combined.push('/');
113        }
114        combined.push_str(path);
115        normalize_path(&combined)
116    }
117}
118
119/// Normalize a path by collapsing redundant separators and resolving `.` and
120/// `..`.
121///
122/// The result is always an absolute path starting with `/`. Trailing slashes
123/// are removed (except for the root `/` itself).
124#[cfg(feature = "alloc")]
125pub fn normalize_path(path: &str) -> String {
126    let mut components: Vec<&str> = Vec::new();
127
128    for component in path.split('/') {
129        match component {
130            "" | "." => {
131                // Skip empty segments (from `//`) and current-dir markers.
132            }
133            ".." => {
134                // Go up one level, but never above root.
135                components.pop();
136            }
137            other => {
138                components.push(other);
139            }
140        }
141    }
142
143    if components.is_empty() {
144        return String::from("/");
145    }
146
147    let mut result = String::with_capacity(path.len());
148    for component in &components {
149        result.push('/');
150        result.push_str(component);
151    }
152
153    result
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    // --- normalize_path tests ---
161
162    #[test]
163    fn test_normalize_root() {
164        assert_eq!(normalize_path("/"), "/");
165    }
166
167    #[test]
168    fn test_normalize_simple() {
169        assert_eq!(normalize_path("/usr/bin"), "/usr/bin");
170    }
171
172    #[test]
173    fn test_normalize_trailing_slash() {
174        assert_eq!(normalize_path("/usr/bin/"), "/usr/bin");
175    }
176
177    #[test]
178    fn test_normalize_double_slash() {
179        assert_eq!(normalize_path("/usr//bin"), "/usr/bin");
180    }
181
182    #[test]
183    fn test_normalize_triple_slash() {
184        assert_eq!(normalize_path("///"), "/");
185    }
186
187    #[test]
188    fn test_normalize_dot() {
189        assert_eq!(normalize_path("/usr/./bin"), "/usr/bin");
190    }
191
192    #[test]
193    fn test_normalize_dotdot() {
194        assert_eq!(normalize_path("/usr/local/../bin"), "/usr/bin");
195    }
196
197    #[test]
198    fn test_normalize_dotdot_at_root() {
199        assert_eq!(normalize_path("/.."), "/");
200    }
201
202    #[test]
203    fn test_normalize_multiple_dotdot() {
204        assert_eq!(normalize_path("/a/b/c/../../d"), "/a/d");
205    }
206
207    #[test]
208    fn test_normalize_complex() {
209        assert_eq!(normalize_path("/usr//local/../bin/./gcc"), "/usr/bin/gcc");
210    }
211
212    #[test]
213    fn test_normalize_all_dotdot() {
214        assert_eq!(normalize_path("/a/b/../../.."), "/");
215    }
216
217    // --- resolve_path tests ---
218
219    #[test]
220    fn test_resolve_absolute() {
221        assert_eq!(resolve_path("/etc/hosts", "/home"), "/etc/hosts");
222    }
223
224    #[test]
225    fn test_resolve_relative_simple() {
226        assert_eq!(resolve_path("foo", "/home"), "/home/foo");
227    }
228
229    #[test]
230    fn test_resolve_relative_nested() {
231        assert_eq!(resolve_path("foo/bar", "/home"), "/home/foo/bar");
232    }
233
234    #[test]
235    fn test_resolve_relative_dotdot() {
236        assert_eq!(resolve_path("../bin", "/usr/local"), "/usr/bin");
237    }
238
239    #[test]
240    fn test_resolve_dot() {
241        assert_eq!(resolve_path(".", "/var/log"), "/var/log");
242    }
243
244    #[test]
245    fn test_resolve_relative_from_root() {
246        assert_eq!(resolve_path("usr/bin", "/"), "/usr/bin");
247    }
248
249    #[test]
250    fn test_resolve_dotdot_past_root() {
251        assert_eq!(resolve_path("../../..", "/a"), "/");
252    }
253
254    // --- ProcessCwd tests ---
255
256    #[test]
257    fn test_process_cwd_default() {
258        let cwd = ProcessCwd::new();
259        assert_eq!(cwd.get(), "/");
260    }
261
262    #[test]
263    fn test_process_cwd_with_path() {
264        let cwd = ProcessCwd::with_path("/home/user").unwrap();
265        assert_eq!(cwd.get(), "/home/user");
266    }
267
268    #[test]
269    fn test_process_cwd_with_path_relative_fails() {
270        let result = ProcessCwd::with_path("relative/path");
271        assert!(result.is_err());
272    }
273
274    #[test]
275    fn test_process_cwd_set() {
276        let mut cwd = ProcessCwd::new();
277        assert!(cwd.set("/home/user").is_ok());
278        assert_eq!(cwd.get(), "/home/user");
279    }
280
281    #[test]
282    fn test_process_cwd_set_normalizes() {
283        let mut cwd = ProcessCwd::new();
284        assert!(cwd.set("/home//user/../admin/./docs").is_ok());
285        assert_eq!(cwd.get(), "/home/admin/docs");
286    }
287
288    #[test]
289    fn test_process_cwd_set_relative_fails() {
290        let mut cwd = ProcessCwd::new();
291        let result = cwd.set("relative");
292        assert!(result.is_err());
293    }
294
295    #[test]
296    fn test_process_cwd_resolve_absolute() {
297        let cwd = ProcessCwd::with_path("/home/user").unwrap();
298        assert_eq!(cwd.resolve("/etc/passwd"), "/etc/passwd");
299    }
300
301    #[test]
302    fn test_process_cwd_resolve_relative() {
303        let cwd = ProcessCwd::with_path("/home/user").unwrap();
304        assert_eq!(
305            cwd.resolve("Documents/file.txt"),
306            "/home/user/Documents/file.txt"
307        );
308    }
309
310    #[test]
311    fn test_process_cwd_resolve_dotdot() {
312        let cwd = ProcessCwd::with_path("/home/user").unwrap();
313        assert_eq!(cwd.resolve("../admin"), "/home/admin");
314    }
315}