Skip to main content

mcp_servers/
workspace_paths.rs

1use std::path::{Path, PathBuf};
2
3pub fn current_dir() -> PathBuf {
4    std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
5}
6
7/// Resolves `path` against `root`, leaving absolute paths untouched.
8pub fn resolve_path(root: &Path, path: PathBuf) -> PathBuf {
9    if path.is_absolute() { path } else { root.join(path) }
10}
11
12pub fn resolve_root(base: &Path, root: Option<PathBuf>) -> PathBuf {
13    root.map_or_else(|| base.to_path_buf(), |path| resolve_path(base, path))
14}
15
16pub fn relative_path(root: &Path, path: &str) -> String {
17    Path::new(path)
18        .strip_prefix(root)
19        .map_or_else(|_| path.to_string(), |relative| relative.to_string_lossy().to_string())
20}
21
22#[derive(Debug, thiserror::Error)]
23#[error("file path is required and cannot be empty")]
24pub struct EmptyFilePathError;
25
26/// Resolves a required file path against `root`. Whitespace-only input is
27/// rejected; otherwise the text (including any surrounding spaces) is preserved
28/// so unusual-but-valid file names round-trip unchanged.
29pub fn resolve_file(root: &Path, raw: &str) -> Result<PathBuf, EmptyFilePathError> {
30    if raw.trim().is_empty() {
31        return Err(EmptyFilePathError);
32    }
33    Ok(resolve_path(root, PathBuf::from(raw)))
34}
35
36#[cfg(test)]
37mod tests {
38    use super::*;
39
40    #[test]
41    fn relative_path_strips_root_prefix() {
42        assert_eq!(relative_path(Path::new("/workspace"), "/workspace/src/main.rs"), "src/main.rs");
43        assert_eq!(relative_path(Path::new("/workspace"), "/elsewhere/lib.rs"), "/elsewhere/lib.rs");
44    }
45
46    #[test]
47    fn resolves_relative_path_against_root() {
48        assert_eq!(
49            resolve_path(Path::new("/workspace"), PathBuf::from("src/main.rs")),
50            PathBuf::from("/workspace/src/main.rs")
51        );
52    }
53
54    #[test]
55    fn root_override_resolves_against_base() {
56        assert_eq!(resolve_root(Path::new("/workspace"), None), PathBuf::from("/workspace"));
57        assert_eq!(resolve_root(Path::new("/workspace"), Some("sub".into())), PathBuf::from("/workspace/sub"));
58        assert_eq!(resolve_root(Path::new("/workspace"), Some("/abs".into())), PathBuf::from("/abs"));
59    }
60
61    #[test]
62    fn leaves_absolute_path_untouched() {
63        assert_eq!(resolve_path(Path::new("/workspace"), PathBuf::from("/etc/passwd")), PathBuf::from("/etc/passwd"));
64    }
65
66    #[test]
67    fn preserves_non_empty_file_path_text() {
68        let resolved = resolve_file(Path::new("/workspace"), " leading space.txt ").unwrap();
69        assert_eq!(resolved, PathBuf::from("/workspace/ leading space.txt "));
70    }
71
72    #[test]
73    fn rejects_whitespace_only_required_file_path() {
74        let err = resolve_file(Path::new("/workspace"), "  \t ").unwrap_err();
75        assert!(err.to_string().contains("file path is required"));
76    }
77}