cargo_docs_md/source/
locator.rs

1//! Source locator for finding crate sources in the Cargo registry.
2//!
3//! This module provides utilities to locate crate source code in
4//! `~/.cargo/registry/src/` based on crate name and version.
5
6use std::path::{Path, PathBuf};
7
8use cargo_metadata::{Metadata, MetadataCommand, Package};
9
10use crate::error::Error;
11
12/// Locates crate sources in the Cargo registry.
13///
14/// The locator can find sources either by scanning the registry directly
15/// or by using `cargo metadata` to find exact paths for dependencies.
16#[derive(Debug)]
17pub struct SourceLocator {
18    /// Path to the cargo registry source directory.
19    /// Typically `~/.cargo/registry/src/`.
20    registry_path: PathBuf,
21
22    /// Cached cargo metadata (if loaded from a project).
23    metadata: Option<Metadata>,
24}
25
26impl SourceLocator {
27    /// Create a new `SourceLocator` with the default registry path.
28    ///
29    /// # Errors
30    ///
31    /// Returns an error if the home directory cannot be determined.
32    pub fn new() -> Result<Self, Error> {
33        let home = std::env::var("HOME")
34            .or_else(|_| std::env::var("USERPROFILE"))
35            .map_err(|_| Error::SourceLocator("Could not determine home directory".into()))?;
36
37        let registry_path = PathBuf::from(home).join(".cargo/registry/src");
38
39        Ok(Self {
40            registry_path,
41            metadata: None,
42        })
43    }
44
45    /// Create a `SourceLocator` with a custom registry path.
46    #[must_use]
47    pub const fn with_registry_path(registry_path: PathBuf) -> Self {
48        Self {
49            registry_path,
50            metadata: None,
51        }
52    }
53
54    /// Load cargo metadata from a project directory.
55    ///
56    /// This enables more accurate source location by using the exact
57    /// paths from cargo's dependency resolution.
58    ///
59    /// # Errors
60    ///
61    /// Returns an error if cargo metadata cannot be loaded.
62    pub fn load_metadata(&mut self, manifest_path: &Path) -> Result<(), Error> {
63        let metadata = MetadataCommand::new()
64            .manifest_path(manifest_path)
65            .exec()
66            .map_err(|e| Error::SourceLocator(format!("Failed to load cargo metadata: {e}")))?;
67
68        self.metadata = Some(metadata);
69
70        Ok(())
71    }
72
73    /// Load cargo metadata from the current directory.
74    ///
75    /// # Errors
76    ///
77    /// Returns an error if cargo metadata cannot be loaded.
78    pub fn load_metadata_from_current_dir(&mut self) -> Result<(), Error> {
79        let metadata = MetadataCommand::new()
80            .exec()
81            .map_err(|e| Error::SourceLocator(format!("Failed to load cargo metadata: {e}")))?;
82
83        self.metadata = Some(metadata);
84
85        Ok(())
86    }
87
88    /// Locate the source directory for a crate by name and version.
89    ///
90    /// First tries to use cargo metadata if available, then falls back
91    /// to scanning the registry directory.
92    ///
93    /// # Errors
94    ///
95    /// Returns an error if the source cannot be found.
96    pub fn locate(&self, name: &str, version: &str) -> Result<PathBuf, Error> {
97        // Try metadata first (most accurate)
98        if let Some(ref metadata) = self.metadata
99            && let Some(path) = Self::locate_from_metadata(metadata, name, version)
100        {
101            return Ok(path);
102        }
103
104        // Fall back to registry scan
105        self.locate_in_registry(name, version)
106    }
107
108    /// Locate source using cargo metadata.
109    fn locate_from_metadata(metadata: &Metadata, name: &str, version: &str) -> Option<PathBuf> {
110        metadata.packages.iter().find_map(|pkg| {
111            if pkg.name == name && pkg.version.to_string() == version {
112                // The manifest_path points to Cargo.toml, we want the parent directory
113                pkg.manifest_path.parent().map(|p| p.to_path_buf().into())
114            } else {
115                None
116            }
117        })
118    }
119
120    /// Locate source by scanning the registry directory.
121    fn locate_in_registry(&self, name: &str, version: &str) -> Result<PathBuf, Error> {
122        // Registry structure: ~/.cargo/registry/src/{index-hash}/{name}-{version}/
123        // The index hash varies (e.g., "index.crates.io-1949cf8c6b5b557f")
124
125        if !self.registry_path.exists() {
126            return Err(Error::SourceLocator(format!(
127                "Cargo registry not found at {}",
128                self.registry_path.display()
129            )));
130        }
131
132        let target_dir_name = format!("{name}-{version}");
133
134        // Scan all index directories
135        for entry in std::fs::read_dir(&self.registry_path)
136            .map_err(|e| Error::SourceLocator(format!("Failed to read registry: {e}")))?
137        {
138            let entry =
139                entry.map_err(|e| Error::SourceLocator(format!("Failed to read entry: {e}")))?;
140            let index_path = entry.path();
141
142            if index_path.is_dir() {
143                let crate_path = index_path.join(&target_dir_name);
144
145                if crate_path.exists()
146                    && crate_path.is_dir()
147                    && (crate_path.join("src").exists() || crate_path.join("lib.rs").exists())
148                {
149                    return Ok(crate_path);
150                }
151            }
152        }
153
154        Err(Error::SourceLocator(format!(
155            "Source not found for {name} v{version} in {}",
156            self.registry_path.display()
157        )))
158    }
159
160    /// Get all packages from the loaded metadata.
161    ///
162    /// Returns `None` if metadata hasn't been loaded.
163    #[must_use]
164    pub fn packages(&self) -> Option<&[Package]> {
165        self.metadata.as_ref().map(|m| m.packages.as_slice())
166    }
167
168    /// Get the workspace root from loaded metadata.
169    ///
170    /// Returns `None` if metadata hasn't been loaded.
171    #[must_use]
172    pub fn workspace_root(&self) -> Option<&Path> {
173        self.metadata
174            .as_ref()
175            .map(|m| m.workspace_root.as_std_path())
176    }
177
178    /// Find all dependency sources for a workspace.
179    ///
180    /// Returns a list of (name, version, path) tuples for all dependencies
181    /// that have sources in the registry.
182    ///
183    /// # Errors
184    ///
185    /// Returns an error if metadata hasn't been loaded.
186    pub fn all_dependency_sources(&self) -> Result<Vec<(String, String, PathBuf)>, Error> {
187        let metadata = self.metadata.as_ref().ok_or_else(|| {
188            Error::SourceLocator("Metadata not loaded. Call load_metadata first.".into())
189        })?;
190
191        let mut sources = Vec::new();
192
193        for pkg in &metadata.packages {
194            // Skip workspace members (they're not in the registry)
195            if metadata.workspace_members.contains(&pkg.id) {
196                continue;
197            }
198
199            let version = pkg.version.to_string();
200
201            // Try to locate the source
202            if let Ok(path) = self.locate(&pkg.name, &version) {
203                sources.push((pkg.name.to_string(), version, path));
204            }
205        }
206
207        Ok(sources)
208    }
209
210    /// List all available crate versions in the registry.
211    ///
212    /// Returns a list of (name, version) tuples.
213    ///
214    /// # Errors
215    ///
216    /// Returns an error if the registry cannot be read.
217    pub fn list_registry_crates(&self) -> Result<Vec<(String, String)>, Error> {
218        if !self.registry_path.exists() {
219            return Err(Error::SourceLocator(format!(
220                "Cargo registry not found at {}",
221                self.registry_path.display()
222            )));
223        }
224
225        let mut crates = Vec::new();
226
227        for index_entry in std::fs::read_dir(&self.registry_path)
228            .map_err(|e| Error::SourceLocator(format!("Failed to read registry: {e}")))?
229        {
230            let index_entry = index_entry
231                .map_err(|e| Error::SourceLocator(format!("Failed to read entry: {e}")))?;
232            let index_path = index_entry.path();
233
234            if !index_path.is_dir() {
235                continue;
236            }
237
238            for crate_entry in std::fs::read_dir(&index_path)
239                .map_err(|e| Error::SourceLocator(format!("Failed to read index: {e}")))?
240            {
241                let crate_entry = crate_entry
242                    .map_err(|e| Error::SourceLocator(format!("Failed to read entry: {e}")))?;
243                let crate_path = crate_entry.path();
244
245                if !crate_path.is_dir() {
246                    continue;
247                }
248
249                if let Some(dir_name) = crate_path.file_name().and_then(|n| n.to_str())
250                    && let Some((name, version)) = ParseUtils::parse_crate_dir_name(dir_name)
251                {
252                    crates.push((name.to_string(), version.to_string()));
253                }
254            }
255        }
256
257        Ok(crates)
258    }
259}
260
261struct ParseUtils;
262
263impl ParseUtils {
264    /// Parse a crate directory name like "serde-1.0.228" into (name, version).
265    pub fn parse_crate_dir_name(dir_name: &str) -> Option<(&str, &str)> {
266        // Find the last hyphen followed by a digit (start of version)
267        let bytes = dir_name.as_bytes();
268        let mut last_hyphen_before_version = None;
269
270        for (i, &byte) in bytes.iter().enumerate() {
271            if byte == b'-' && i + 1 < bytes.len() && bytes[i + 1].is_ascii_digit() {
272                last_hyphen_before_version = Some(i);
273            }
274        }
275
276        last_hyphen_before_version.map(|pos| {
277            let name = &dir_name[..pos];
278            let version = &dir_name[pos + 1..];
279
280            (name, version)
281        })
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use crate::source::locator::ParseUtils;
288
289    #[test]
290    fn test_parse_crate_dir_name() {
291        assert_eq!(
292            ParseUtils::parse_crate_dir_name("serde-1.0.228"),
293            Some(("serde", "1.0.228"))
294        );
295
296        assert_eq!(
297            ParseUtils::parse_crate_dir_name("serde_json-1.0.145"),
298            Some(("serde_json", "1.0.145"))
299        );
300
301        assert_eq!(
302            ParseUtils::parse_crate_dir_name("my-crate-name-0.1.0"),
303            Some(("my-crate-name", "0.1.0"))
304        );
305
306        assert_eq!(
307            ParseUtils::parse_crate_dir_name("tokio-1.48.0"),
308            Some(("tokio", "1.48.0"))
309        );
310
311        // Edge cases
312        assert_eq!(ParseUtils::parse_crate_dir_name("nocrate"), None);
313        assert_eq!(ParseUtils::parse_crate_dir_name("a-1"), Some(("a", "1")));
314    }
315}