1use std::{
89 collections::btree_map::{self, BTreeMap},
90 env, fmt, fs, io,
91 path::{Path, PathBuf},
92 process::Command,
93 sync::Mutex,
94 time::SystemTime,
95};
96
97use toml_edit::{DocumentMut, Item, TableLike, TomlError};
98
99pub enum Error {
101 NotFound(PathBuf),
102 CargoManifestDirNotSet,
103 FailedGettingWorkspaceManifestPath,
104 CouldNotRead { path: PathBuf, source: io::Error },
105 InvalidToml { source: TomlError },
106 CrateNotFound { crate_name: String, path: PathBuf },
107}
108
109impl std::error::Error for Error {
110 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
111 match self {
112 Error::CouldNotRead { source, .. } => Some(source),
113 Error::InvalidToml { source } => Some(source),
114 _ => None,
115 }
116 }
117}
118
119impl fmt::Debug for Error {
120 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121 fmt::Display::fmt(self, f)
122 }
123}
124
125impl fmt::Display for Error {
126 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
127 match self {
128 Error::NotFound(path) =>
129 write!(f, "Could not find `Cargo.toml` in manifest dir: `{}`.", path.display()),
130 Error::CargoManifestDirNotSet =>
131 f.write_str("`CARGO_MANIFEST_DIR` env variable not set."),
132 Error::CouldNotRead { path, .. } => write!(f, "Could not read `{}`.", path.display()),
133 Error::InvalidToml { .. } => f.write_str("Invalid toml file."),
134 Error::CrateNotFound { crate_name, path } => write!(
135 f,
136 "Could not find `{}` in `dependencies` or `dev-dependencies` in `{}`!",
137 crate_name,
138 path.display(),
139 ),
140 Error::FailedGettingWorkspaceManifestPath =>
141 f.write_str("Failed to get the path of the workspace manifest path."),
142 }
143 }
144}
145
146#[derive(Debug, PartialEq, Clone, Eq)]
148pub enum FoundCrate {
149 Itself,
151 Name(String),
153}
154
155type Cache = BTreeMap<String, CacheEntry>;
159
160struct CacheEntry {
161 manifest_ts: SystemTime,
162 workspace_manifest_ts: SystemTime,
163 workspace_manifest_path: PathBuf,
164 crate_names: CrateNames,
165}
166
167type CrateNames = BTreeMap<String, FoundCrate>;
168
169pub fn crate_name(orig_name: &str) -> Result<FoundCrate, Error> {
185 let manifest_dir = env::var("CARGO_MANIFEST_DIR").map_err(|_| Error::CargoManifestDirNotSet)?;
186 let manifest_path = Path::new(&manifest_dir).join("Cargo.toml");
187
188 let manifest_ts = cargo_toml_timestamp(&manifest_path)?;
189
190 static CACHE: Mutex<Cache> = Mutex::new(BTreeMap::new());
191 let mut cache = CACHE.lock().unwrap();
192
193 let crate_names = match cache.entry(manifest_dir) {
194 btree_map::Entry::Occupied(entry) => {
195 let cache_entry = entry.into_mut();
196 let workspace_manifest_path = cache_entry.workspace_manifest_path.as_path();
197 let workspace_manifest_ts = cargo_toml_timestamp(&workspace_manifest_path)?;
198
199 if manifest_ts != cache_entry.manifest_ts ||
201 workspace_manifest_ts != cache_entry.workspace_manifest_ts
202 {
203 *cache_entry = read_cargo_toml(
204 &manifest_path,
205 &workspace_manifest_path,
206 manifest_ts,
207 workspace_manifest_ts,
208 )?;
209 }
210
211 &cache_entry.crate_names
212 },
213 btree_map::Entry::Vacant(entry) => {
214 let workspace_manifest_path =
219 workspace_manifest_path(&manifest_path)?.unwrap_or_else(|| manifest_path.clone());
220 let workspace_manifest_ts = cargo_toml_timestamp(&workspace_manifest_path)?;
221
222 let cache_entry = entry.insert(read_cargo_toml(
223 &manifest_path,
224 &workspace_manifest_path,
225 manifest_ts,
226 workspace_manifest_ts,
227 )?);
228 &cache_entry.crate_names
229 },
230 };
231
232 Ok(crate_names
233 .get(orig_name)
234 .ok_or_else(|| Error::CrateNotFound {
235 crate_name: orig_name.to_owned(),
236 path: manifest_path,
237 })?
238 .clone())
239}
240
241fn workspace_manifest_path(cargo_toml_manifest: &Path) -> Result<Option<PathBuf>, Error> {
242 let Ok(cargo) = env::var("CARGO") else {
243 return Ok(None);
244 };
245
246 let stdout = Command::new(cargo)
247 .arg("locate-project")
248 .args(&["--workspace", "--message-format=plain"])
249 .arg(format!("--manifest-path={}", cargo_toml_manifest.display()))
250 .output()
251 .map_err(|_| Error::FailedGettingWorkspaceManifestPath)?
252 .stdout;
253
254 String::from_utf8(stdout)
255 .map_err(|_| Error::FailedGettingWorkspaceManifestPath)
256 .map(|s| {
257 let path = s.trim();
258
259 if path.is_empty() {
260 None
261 } else {
262 Some(path.into())
263 }
264 })
265}
266
267fn cargo_toml_timestamp(manifest_path: &Path) -> Result<SystemTime, Error> {
268 fs::metadata(manifest_path).and_then(|meta| meta.modified()).map_err(|source| {
269 if source.kind() == io::ErrorKind::NotFound {
270 Error::NotFound(manifest_path.to_owned())
271 } else {
272 Error::CouldNotRead { path: manifest_path.to_owned(), source }
273 }
274 })
275}
276
277fn read_cargo_toml(
278 manifest_path: &Path,
279 workspace_manifest_path: &Path,
280 manifest_ts: SystemTime,
281 workspace_manifest_ts: SystemTime,
282) -> Result<CacheEntry, Error> {
283 let manifest = open_cargo_toml(manifest_path)?;
284
285 let workspace_dependencies = if manifest_path != workspace_manifest_path {
286 let workspace_manifest = open_cargo_toml(workspace_manifest_path)?;
287 extract_workspace_dependencies(&workspace_manifest)?
288 } else {
289 extract_workspace_dependencies(&manifest)?
290 };
291
292 let crate_names = extract_crate_names(&manifest, workspace_dependencies)?;
293
294 Ok(CacheEntry {
295 manifest_ts,
296 workspace_manifest_ts,
297 crate_names,
298 workspace_manifest_path: workspace_manifest_path.to_path_buf(),
299 })
300}
301
302fn extract_workspace_dependencies(
307 workspace_toml: &DocumentMut,
308) -> Result<BTreeMap<String, String>, Error> {
309 Ok(workspace_dep_tables(&workspace_toml)
310 .into_iter()
311 .map(|t| t.iter())
312 .flatten()
313 .map(move |(dep_name, dep_value)| {
314 let pkg_name = dep_value.get("package").and_then(|i| i.as_str()).unwrap_or(dep_name);
315
316 (dep_name.to_owned(), pkg_name.to_owned())
317 })
318 .collect())
319}
320
321fn workspace_dep_tables(cargo_toml: &DocumentMut) -> Option<&dyn TableLike> {
323 cargo_toml
324 .get("workspace")
325 .and_then(|w| w.as_table_like()?.get("dependencies")?.as_table_like())
326}
327
328fn sanitize_crate_name<S: AsRef<str>>(name: S) -> String {
330 name.as_ref().replace('-', "_")
331}
332
333fn open_cargo_toml(path: &Path) -> Result<DocumentMut, Error> {
335 let content = fs::read_to_string(path)
336 .map_err(|e| Error::CouldNotRead { source: e, path: path.into() })?;
337 content.parse::<DocumentMut>().map_err(|e| Error::InvalidToml { source: e })
338}
339
340fn extract_crate_names(
343 cargo_toml: &DocumentMut,
344 workspace_dependencies: BTreeMap<String, String>,
345) -> Result<CrateNames, Error> {
346 let package_name = extract_package_name(cargo_toml);
347 let root_pkg = package_name.as_ref().map(|name| {
348 let cr = match env::var_os("CARGO_TARGET_TMPDIR") {
349 None => FoundCrate::Itself,
351 Some(_) => FoundCrate::Name(sanitize_crate_name(name)),
353 };
354
355 (name.to_string(), cr)
356 });
357
358 let dep_tables = dep_tables(cargo_toml.as_table()).chain(target_dep_tables(cargo_toml));
359 let dep_pkgs =
360 dep_tables.map(|t| t.iter()).flatten().filter_map(move |(dep_name, dep_value)| {
361 let pkg_name = dep_value.get("package").and_then(|i| i.as_str()).unwrap_or(dep_name);
362
363 if package_name.as_ref().map_or(false, |n| *n == pkg_name) {
365 return None
366 }
367
368 let workspace =
370 dep_value.get("workspace").and_then(|w| w.as_bool()).unwrap_or_default();
371
372 let pkg_name = workspace
373 .then(|| workspace_dependencies.get(pkg_name).map(|p| p.as_ref()))
374 .flatten()
375 .unwrap_or(pkg_name);
376
377 let cr = FoundCrate::Name(sanitize_crate_name(dep_name));
378
379 Some((pkg_name.to_owned(), cr))
380 });
381
382 Ok(root_pkg.into_iter().chain(dep_pkgs).collect())
383}
384
385fn extract_package_name(cargo_toml: &DocumentMut) -> Option<&str> {
386 cargo_toml.get("package")?.get("name")?.as_str()
387}
388
389fn target_dep_tables(cargo_toml: &DocumentMut) -> impl Iterator<Item = &dyn TableLike> {
390 cargo_toml
391 .get("target")
392 .into_iter()
393 .filter_map(Item::as_table_like)
394 .flat_map(|t| {
395 t.iter()
396 .map(|(_, value)| value)
397 .filter_map(Item::as_table_like)
398 .flat_map(dep_tables)
399 })
400}
401
402fn dep_tables(table: &dyn TableLike) -> impl Iterator<Item = &dyn TableLike> {
403 table
404 .get("dependencies")
405 .into_iter()
406 .chain(table.get("dev-dependencies"))
407 .filter_map(Item::as_table_like)
408}
409
410#[cfg(test)]
411mod tests {
412 use super::*;
413
414 macro_rules! create_test {
415 (
416 $name:ident,
417 $cargo_toml:expr,
418 $workspace_toml:expr,
419 $( $result:tt )*
420 ) => {
421 #[test]
422 fn $name() {
423 let cargo_toml = $cargo_toml.parse::<DocumentMut>()
424 .expect("Parses `Cargo.toml`");
425 let workspace_cargo_toml = $workspace_toml.parse::<DocumentMut>()
426 .expect("Parses workspace `Cargo.toml`");
427
428 let workspace_deps = extract_workspace_dependencies(&workspace_cargo_toml)
429 .expect("Extracts workspace dependencies");
430
431 match extract_crate_names(&cargo_toml, workspace_deps)
432 .map(|mut map| map.remove("my_crate"))
433 {
434 $( $result )* => (),
435 o => panic!("Invalid result: {:?}", o),
436 }
437 }
438 };
439 }
440
441 create_test! {
442 deps_with_crate,
443 r#"
444 [dependencies]
445 my_crate = "0.1"
446 "#,
447 "",
448 Ok(Some(FoundCrate::Name(name))) if name == "my_crate"
449 }
450
451 create_test! {
453 deps_with_crate_inline_table,
454 r#"
455 dependencies = { my_crate = "0.1" }
456 "#,
457 "",
458 Ok(Some(FoundCrate::Name(name))) if name == "my_crate"
459 }
460
461 create_test! {
462 dev_deps_with_crate,
463 r#"
464 [dev-dependencies]
465 my_crate = "0.1"
466 "#,
467 "",
468 Ok(Some(FoundCrate::Name(name))) if name == "my_crate"
469 }
470
471 create_test! {
472 deps_with_crate_renamed,
473 r#"
474 [dependencies]
475 cool = { package = "my_crate", version = "0.1" }
476 "#,
477 "",
478 Ok(Some(FoundCrate::Name(name))) if name == "cool"
479 }
480
481 create_test! {
482 deps_with_crate_renamed_second,
483 r#"
484 [dependencies.cool]
485 package = "my_crate"
486 version = "0.1"
487 "#,
488 "",
489 Ok(Some(FoundCrate::Name(name))) if name == "cool"
490 }
491
492 create_test! {
493 deps_empty,
494 r#"
495 [dependencies]
496 "#,
497 "",
498 Ok(None)
499 }
500
501 create_test! {
502 crate_not_found,
503 r#"
504 [dependencies]
505 serde = "1.0"
506 "#,
507 "",
508 Ok(None)
509 }
510
511 create_test! {
512 target_dependency,
513 r#"
514 [target.'cfg(target_os="android")'.dependencies]
515 my_crate = "0.1"
516 "#,
517 "",
518 Ok(Some(FoundCrate::Name(name))) if name == "my_crate"
519 }
520
521 create_test! {
522 target_dependency2,
523 r#"
524 [target.x86_64-pc-windows-gnu.dependencies]
525 my_crate = "0.1"
526 "#,
527 "",
528 Ok(Some(FoundCrate::Name(name))) if name == "my_crate"
529 }
530
531 create_test! {
532 own_crate,
533 r#"
534 [package]
535 name = "my_crate"
536 "#,
537 "",
538 Ok(Some(FoundCrate::Itself))
539 }
540
541 create_test! {
542 own_crate_and_in_deps,
543 r#"
544 [package]
545 name = "my_crate"
546
547 [dev-dependencies]
548 my_crate = "0.1"
549 "#,
550 "",
551 Ok(Some(FoundCrate::Itself))
552 }
553
554 create_test! {
555 multiple_times,
556 r#"
557 [dependencies]
558 my_crate = { version = "0.5" }
559 my-crate-old = { package = "my_crate", version = "0.1" }
560 "#,
561 "",
562 Ok(Some(FoundCrate::Name(name))) if name == "my_crate_old"
563 }
564
565 create_test! {
566 workspace_deps,
567 r#"
568 [dependencies]
569 my_crate_cool = { workspace = true }
570 "#,
571 r#"
572 [workspace.dependencies]
573 my_crate_cool = { package = "my_crate" }
574 "#,
575 Ok(Some(FoundCrate::Name(name))) if name == "my_crate_cool"
576 }
577}