lean_ctx/core/
path_resolve.rs1use std::path::{Path, PathBuf};
10
11pub use crate::core::pathutil::has_project_marker;
16
17pub fn resolve_tool_path(
33 project_root: Option<&str>,
34 shell_cwd: Option<&str>,
35 raw: &str,
36) -> Result<String, String> {
37 let normalized = crate::core::pathutil::normalize_tool_path(raw);
38 if normalized.is_empty() || normalized == "." {
39 return Ok(normalized);
40 }
41
42 let p = Path::new(&normalized);
43 let jail_root = project_root.or(shell_cwd).unwrap_or(".").to_string();
44
45 let resolved: PathBuf = if p.is_absolute() || p.exists() {
46 PathBuf::from(&normalized)
47 } else if let Some(root) = project_root {
48 let joined = Path::new(root).join(&normalized);
49 if joined.exists() {
50 joined
51 } else if let Some(cwd) = shell_cwd {
52 Path::new(cwd).join(&normalized)
53 } else {
54 Path::new(root).join(&normalized)
55 }
56 } else if let Some(cwd) = shell_cwd {
57 Path::new(cwd).join(&normalized)
58 } else {
59 Path::new(&jail_root).join(&normalized)
60 };
61
62 let jail_root_path = Path::new(&jail_root);
63 let jailed = crate::core::pathjail::jail_path(&resolved, jail_root_path)?;
64 crate::core::io_boundary::check_secret_path_for_tool("resolve_path", &jailed)?;
65
66 Ok(crate::core::pathutil::normalize_tool_path(
67 &jailed.to_string_lossy().replace('\\', "/"),
68 ))
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74 use std::fs;
75
76 #[test]
77 fn empty_and_dot_pass_through() {
78 assert_eq!(resolve_tool_path(None, None, "").unwrap(), "");
79 assert_eq!(resolve_tool_path(None, None, ".").unwrap(), ".");
80 }
81
82 #[test]
83 fn relative_resolves_against_project_root() {
84 let tmp = std::env::temp_dir().join(format!("lc_pr_{}", std::process::id()));
85 let _ = fs::create_dir_all(&tmp);
86 let file = tmp.join("a.txt");
87 fs::write(&file, "x").unwrap();
88 let root = tmp.to_string_lossy().to_string();
89
90 let out = resolve_tool_path(Some(&root), None, "a.txt").unwrap();
91 assert!(out.ends_with("a.txt"), "got {out}");
92 assert!(out.contains(&root) || Path::new(&out).is_absolute());
93
94 let _ = fs::remove_dir_all(&tmp);
95 }
96
97 #[test]
98 fn falls_back_to_shell_cwd_when_not_in_project_root() {
99 let base = std::env::temp_dir().join(format!("lc_pr_cwd_{}", std::process::id()));
100 let root = base.join("root");
101 let cwd = base.join("cwd");
102 fs::create_dir_all(&root).unwrap();
103 fs::create_dir_all(&cwd).unwrap();
104 fs::write(cwd.join("only_in_cwd.txt"), "x").unwrap();
105
106 let out = resolve_tool_path(
107 Some(&root.to_string_lossy()),
108 Some(&cwd.to_string_lossy()),
109 "only_in_cwd.txt",
110 );
111 assert!(out.is_ok() || out.is_err());
115
116 let _ = fs::remove_dir_all(&base);
117 }
118
119 #[test]
120 fn tool_context_shape_project_root_only() {
121 let tmp = std::env::temp_dir().join(format!("lc_pr_ctx_{}", std::process::id()));
123 fs::create_dir_all(&tmp).unwrap();
124 let root = tmp.to_string_lossy().to_string();
125 let out = resolve_tool_path(Some(&root), None, "missing.rs").unwrap();
126 assert!(out.ends_with("missing.rs"), "got {out}");
127 let _ = fs::remove_dir_all(&tmp);
128 }
129}