cargo_docs_md/source/
locator.rs1use std::path::{Path, PathBuf};
7
8use cargo_metadata::{Metadata, MetadataCommand, Package};
9
10use crate::error::Error;
11
12#[derive(Debug)]
17pub struct SourceLocator {
18 registry_path: PathBuf,
21
22 metadata: Option<Metadata>,
24}
25
26impl SourceLocator {
27 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 #[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 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 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 pub fn locate(&self, name: &str, version: &str) -> Result<PathBuf, Error> {
97 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 self.locate_in_registry(name, version)
106 }
107
108 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 pkg.manifest_path.parent().map(|p| p.to_path_buf().into())
114 } else {
115 None
116 }
117 })
118 }
119
120 fn locate_in_registry(&self, name: &str, version: &str) -> Result<PathBuf, Error> {
122 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 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 #[must_use]
164 pub fn packages(&self) -> Option<&[Package]> {
165 self.metadata.as_ref().map(|m| m.packages.as_slice())
166 }
167
168 #[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 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 if metadata.workspace_members.contains(&pkg.id) {
196 continue;
197 }
198
199 let version = pkg.version.to_string();
200
201 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 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 pub fn parse_crate_dir_name(dir_name: &str) -> Option<(&str, &str)> {
266 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 assert_eq!(ParseUtils::parse_crate_dir_name("nocrate"), None);
313 assert_eq!(ParseUtils::parse_crate_dir_name("a-1"), Some(("a", "1")));
314 }
315}