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