probe_code/path_resolver/
rust.rs1use super::PathResolver;
4use serde_json::Value;
5use std::path::PathBuf;
6use std::process::Command;
7
8pub struct RustPathResolver;
10
11impl Default for RustPathResolver {
12 fn default() -> Self {
13 Self::new()
14 }
15}
16
17impl RustPathResolver {
18 pub fn new() -> Self {
20 RustPathResolver
21 }
22
23 fn get_crate_path(&self, crate_name: &str) -> Result<PathBuf, String> {
25 let output = Command::new("cargo")
27 .args(["metadata", "--format-version=1"])
28 .output()
29 .map_err(|e| format!("Failed to execute 'cargo metadata': {e}"))?;
30
31 if !output.status.success() {
32 return Err(format!(
33 "Error running 'cargo metadata': {}",
34 String::from_utf8_lossy(&output.stderr)
35 ));
36 }
37
38 let json_str = String::from_utf8_lossy(&output.stdout);
39 let json: Value = serde_json::from_str(&json_str)
40 .map_err(|e| format!("Failed to parse JSON output from 'cargo metadata': {e}"))?;
41
42 if let Some(packages) = json["packages"].as_array() {
44 for package in packages {
45 if let Some(name) = package["name"].as_str() {
46 if name == crate_name {
47 if let Some(manifest_path) = package["manifest_path"].as_str() {
48 let path = PathBuf::from(manifest_path);
50 return path.parent().map_or(
51 Err(format!(
52 "Could not determine parent directory of {manifest_path}"
53 )),
54 |parent| Ok(parent.to_path_buf()),
55 );
56 }
57 }
58 }
59 }
60 }
61
62 if let Some(packages) = json["packages"].as_array() {
64 for package in packages {
65 if let Some(deps) = package["dependencies"].as_array() {
66 for dep in deps {
67 if let Some(name) = dep["name"].as_str() {
68 if name == crate_name {
69 if let Some(source) = dep["source"].as_str() {
71 if source.starts_with("registry+") {
73 return self.find_in_registry_cache(crate_name);
74 }
75 }
76 }
77 }
78 }
79 }
80 }
81 }
82
83 self.find_in_registry_cache(crate_name)
85 }
86
87 fn find_in_registry_cache(&self, crate_name: &str) -> Result<PathBuf, String> {
89 let cargo_home = std::env::var("CARGO_HOME")
91 .or_else(|_| {
92 let home = std::env::var("HOME")
93 .map_err(|e| format!("Failed to get HOME environment variable: {e}"))?;
94 Ok::<String, String>(format!("{home}/.cargo"))
95 })
96 .map_err(|e| format!("Failed to determine CARGO_HOME: {e}"))?;
97
98 let registry_dir = PathBuf::from(cargo_home).join("registry").join("src");
100
101 if !registry_dir.exists() {
102 return Err(format!(
103 "Cargo registry directory not found: {registry_dir:?}"
104 ));
105 }
106
107 let registry_indices = std::fs::read_dir(®istry_dir)
109 .map_err(|e| format!("Failed to read registry directory: {e}"))?;
110
111 for index_entry in registry_indices {
112 let index_dir = index_entry
113 .map_err(|e| format!("Failed to read registry index entry: {e}"))?
114 .path();
115
116 if !index_dir.is_dir() {
117 continue;
118 }
119
120 let crates = std::fs::read_dir(&index_dir)
122 .map_err(|e| format!("Failed to read index directory: {e}"))?;
123
124 for crate_entry in crates {
125 let crate_dir = crate_entry
126 .map_err(|e| format!("Failed to read crate entry: {e}"))?
127 .path();
128
129 if !crate_dir.is_dir() {
130 continue;
131 }
132
133 let dir_name = crate_dir
135 .file_name()
136 .ok_or_else(|| "Invalid directory name".to_string())?
137 .to_string_lossy();
138
139 if dir_name.starts_with(&format!("{crate_name}-")) {
140 return Ok(crate_dir);
142 }
143 }
144 }
145
146 Err(format!("Could not find Rust crate: {crate_name}"))
147 }
148}
149
150impl PathResolver for RustPathResolver {
151 fn prefix(&self) -> &'static str {
152 "rust:"
153 }
154
155 fn split_module_and_subpath(
156 &self,
157 full_path_after_prefix: &str,
158 ) -> Result<(String, Option<String>), String> {
159 if full_path_after_prefix.is_empty() {
160 return Err("Rust path (to Cargo.toml) cannot be empty".to_string());
161 }
162
163 Ok((full_path_after_prefix.to_string(), None))
166 }
167
168 fn resolve(&self, crate_name: &str) -> Result<PathBuf, String> {
169 let path = PathBuf::from(crate_name);
171 if path.exists()
172 && path.is_file()
173 && path.file_name().is_some_and(|name| name == "Cargo.toml")
174 {
175 return path.parent().map_or(
177 Err("Could not determine parent directory of Cargo.toml".to_string()),
178 |parent| Ok(parent.to_path_buf()),
179 );
180 }
181
182 let crate_dir = PathBuf::from(crate_name);
184 let cargo_toml = crate_dir.join("Cargo.toml");
185 if crate_dir.exists() && crate_dir.is_dir() && cargo_toml.exists() {
186 return Ok(crate_dir);
187 }
188
189 self.get_crate_path(crate_name)
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197 use std::fs;
198
199 #[test]
200 fn test_rust_path_resolver_with_directory() {
201 let temp_dir = tempfile::tempdir().unwrap();
203 let cargo_toml_path = temp_dir.path().join("Cargo.toml");
204
205 fs::write(
207 &cargo_toml_path,
208 r#"[package]
209name = "test-crate"
210version = "0.1.0"
211edition = "2021"
212"#,
213 )
214 .expect("Failed to write Cargo.toml");
215
216 let resolver = RustPathResolver::new();
217 let result = resolver.resolve(temp_dir.path().to_str().unwrap());
218
219 assert!(
220 result.is_ok(),
221 "Failed to resolve directory with Cargo.toml: {result:?}"
222 );
223 assert_eq!(result.unwrap(), temp_dir.path());
224 }
225
226 #[test]
227 fn test_rust_path_resolver_with_cargo_toml() {
228 let temp_dir = tempfile::tempdir().unwrap();
230 let cargo_toml_path = temp_dir.path().join("Cargo.toml");
231
232 fs::write(
234 &cargo_toml_path,
235 r#"[package]
236name = "test-crate"
237version = "0.1.0"
238edition = "2021"
239"#,
240 )
241 .expect("Failed to write Cargo.toml");
242
243 let resolver = RustPathResolver::new();
244 let result = resolver.resolve(cargo_toml_path.to_str().unwrap());
245
246 assert!(result.is_ok(), "Failed to resolve Cargo.toml: {result:?}");
247 assert_eq!(result.unwrap(), temp_dir.path());
248 }
249
250 #[test]
251 fn test_rust_path_resolver_crate() {
252 if Command::new("cargo").arg("--version").output().is_err() {
254 println!("Skipping test_rust_path_resolver_crate: cargo is not installed");
255 return;
256 }
257
258 let resolver = RustPathResolver::new();
261
262 let cargo_toml = std::fs::read_to_string("Cargo.toml").expect("Failed to read Cargo.toml");
264
265 let re = regex::Regex::new(r#"name\s*=\s*"([^"]+)""#).unwrap();
267 let crate_name = re
268 .captures(&cargo_toml)
269 .map(|cap| cap[1].to_string())
270 .unwrap_or_else(|| "probe".to_string()); let result = resolver.resolve(&crate_name);
273
274 if let Ok(path) = result {
276 assert!(path.exists(), "Path does not exist: {path:?}");
277
278 let cargo_toml_path = path.join("Cargo.toml");
280 assert!(
281 cargo_toml_path.exists(),
282 "Cargo.toml not found: {cargo_toml_path:?}"
283 );
284 } else {
285 println!("Skipping assertion for '{crate_name}': Crate not found");
286 }
287 }
288}