probe_code/path_resolver/
rust.rs

1//! Rust-specific path resolver implementation.
2
3use super::PathResolver;
4use serde_json::Value;
5use std::path::PathBuf;
6use std::process::Command;
7
8/// A path resolver for Rust crates.
9pub struct RustPathResolver;
10
11impl Default for RustPathResolver {
12    fn default() -> Self {
13        Self::new()
14    }
15}
16
17impl RustPathResolver {
18    /// Creates a new Rust path resolver.
19    pub fn new() -> Self {
20        RustPathResolver
21    }
22
23    /// Gets the path to a Rust crate using cargo metadata.
24    fn get_crate_path(&self, crate_name: &str) -> Result<PathBuf, String> {
25        // Run `cargo metadata --format-version=1`
26        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        // Find the package in the packages array
43        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                            // The manifest_path points to Cargo.toml, we want the directory
49                            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 we couldn't find it in the current workspace, try to find it in the dependencies
63        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                                // For dependencies, we need to look at the source
70                                if let Some(source) = dep["source"].as_str() {
71                                    // For registry dependencies, we need to look in the registry cache
72                                    if source.starts_with("registry+") {
73                                        return self.find_in_registry_cache(crate_name);
74                                    }
75                                }
76                            }
77                        }
78                    }
79                }
80            }
81        }
82
83        // If we still couldn't find it, try to find it in the registry cache directly
84        self.find_in_registry_cache(crate_name)
85    }
86
87    /// Finds a crate in the Cargo registry cache.
88    fn find_in_registry_cache(&self, crate_name: &str) -> Result<PathBuf, String> {
89        // Get the cargo home directory
90        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        // The registry cache is in $CARGO_HOME/registry/src
99        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        // Look for the crate in all registry indices
108        let registry_indices = std::fs::read_dir(&registry_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            // Look for directories that contain the crate name
121            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                // Check if this directory contains our crate
134                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                    // Found a matching crate directory
141                    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        // For Rust, the entire path is treated as the module identifier
164        // We don't split into module/subpath for Rust paths
165        Ok((full_path_after_prefix.to_string(), None))
166    }
167
168    fn resolve(&self, crate_name: &str) -> Result<PathBuf, String> {
169        // First, check if this is a path to a Cargo.toml file
170        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            // If it's a Cargo.toml file, return its directory
176            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        // If it's a directory containing Cargo.toml, return the directory
183        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        // Otherwise, try to resolve it as a crate name
190        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        // Create a temporary directory with a Cargo.toml file
202        let temp_dir = tempfile::tempdir().unwrap();
203        let cargo_toml_path = temp_dir.path().join("Cargo.toml");
204
205        // Write a minimal Cargo.toml
206        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        // Create a temporary directory with a Cargo.toml file
229        let temp_dir = tempfile::tempdir().unwrap();
230        let cargo_toml_path = temp_dir.path().join("Cargo.toml");
231
232        // Write a minimal Cargo.toml
233        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        // Skip this test if cargo is not installed
253        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        // This test is more complex as it requires cargo to be installed
259        // We'll try to resolve the current crate
260        let resolver = RustPathResolver::new();
261
262        // Get the name of the current crate from Cargo.toml
263        let cargo_toml = std::fs::read_to_string("Cargo.toml").expect("Failed to read Cargo.toml");
264
265        // Extract the package name using a simple regex
266        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()); // Default to "probe" if not found
271
272        let result = resolver.resolve(&crate_name);
273
274        // The result should be Ok and point to the current directory or a valid path
275        if let Ok(path) = result {
276            assert!(path.exists(), "Path does not exist: {path:?}");
277
278            // Check if it contains a Cargo.toml
279            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}