1use std::{
89 collections::btree_map::{self, BTreeMap},
90 env, fmt, fs, io,
91 path::{Path, PathBuf},
92 sync::Mutex,
93 time::SystemTime,
94};
95
96use once_cell::sync::Lazy;
97use toml_edit::{Document, Item, Table, TomlError};
98
99#[derive(Debug)]
101pub enum Error {
102 NotFound(PathBuf),
103 CargoManifestDirNotSet,
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::Display for Error {
120 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
121 match self {
122 Error::NotFound(path) => write!(
123 f,
124 "Could not find `Cargo.toml` in manifest dir: `{}`.",
125 path.display()
126 ),
127 Error::CargoManifestDirNotSet => {
128 f.write_str("`CARGO_MANIFEST_DIR` env variable not set.")
129 }
130 Error::CouldNotRead { path, .. } => write!(f, "Could not read `{}`.", path.display()),
131 Error::InvalidToml { .. } => f.write_str("Invalid toml file."),
132 Error::CrateNotFound { crate_name, path } => write!(
133 f,
134 "Could not find `{}` in `dependencies` or `dev-dependencies` in `{}`!",
135 crate_name,
136 path.display(),
137 ),
138 }
139 }
140}
141
142#[derive(Debug, PartialEq, Clone, Eq)]
144pub enum FoundCrate {
145 Itself,
147 Name(String),
149}
150
151type Cache = BTreeMap<String, CacheEntry>;
155
156struct CacheEntry {
157 manifest_ts: SystemTime,
158 crate_names: CrateNames,
159}
160
161type CrateNames = BTreeMap<String, FoundCrate>;
162
163pub fn crate_name(orig_name: &str) -> Result<FoundCrate, Error> {
179 let manifest_dir = env::var("CARGO_MANIFEST_DIR").map_err(|_| Error::CargoManifestDirNotSet)?;
180 let manifest_path = Path::new(&manifest_dir).join("Cargo.toml");
181 let manifest_ts = cargo_toml_timestamp(&manifest_path)?;
182
183 static CACHE: Lazy<Mutex<Cache>> = Lazy::new(Mutex::default);
186 let mut cache = CACHE.lock().unwrap();
187
188 let crate_names = match cache.entry(manifest_dir) {
189 btree_map::Entry::Occupied(entry) => {
190 let cache_entry = entry.into_mut();
191
192 if manifest_ts != cache_entry.manifest_ts {
194 *cache_entry = read_cargo_toml(&manifest_path, manifest_ts)?;
195 }
196
197 &cache_entry.crate_names
198 }
199 btree_map::Entry::Vacant(entry) => {
200 let cache_entry = entry.insert(read_cargo_toml(&manifest_path, manifest_ts)?);
201 &cache_entry.crate_names
202 }
203 };
204
205 Ok(crate_names
206 .get(orig_name)
207 .ok_or_else(|| Error::CrateNotFound {
208 crate_name: orig_name.to_owned(),
209 path: manifest_path,
210 })?
211 .clone())
212}
213
214fn cargo_toml_timestamp(manifest_path: &Path) -> Result<SystemTime, Error> {
215 fs::metadata(manifest_path)
216 .and_then(|meta| meta.modified())
217 .map_err(|source| {
218 if source.kind() == io::ErrorKind::NotFound {
219 Error::NotFound(manifest_path.to_owned())
220 } else {
221 Error::CouldNotRead {
222 path: manifest_path.to_owned(),
223 source,
224 }
225 }
226 })
227}
228
229fn read_cargo_toml(manifest_path: &Path, manifest_ts: SystemTime) -> Result<CacheEntry, Error> {
230 let manifest = open_cargo_toml(manifest_path)?;
231 let crate_names = extract_crate_names(&manifest)?;
232
233 Ok(CacheEntry {
234 manifest_ts,
235 crate_names,
236 })
237}
238
239fn sanitize_crate_name<S: AsRef<str>>(name: S) -> String {
241 name.as_ref().replace('-', "_")
242}
243
244fn open_cargo_toml(path: &Path) -> Result<Document, Error> {
246 let content = fs::read_to_string(path).map_err(|e| Error::CouldNotRead {
247 source: e,
248 path: path.into(),
249 })?;
250 content
251 .parse::<Document>()
252 .map_err(|e| Error::InvalidToml { source: e })
253}
254
255fn extract_crate_names(cargo_toml: &Document) -> Result<CrateNames, Error> {
258 let package_name = extract_package_name(cargo_toml);
259 let root_pkg = package_name.as_ref().map(|name| {
260 let cr = match env::var_os("CARGO_TARGET_TMPDIR") {
261 None => FoundCrate::Itself,
263 Some(_) => FoundCrate::Name(sanitize_crate_name(name)),
265 };
266
267 (name.to_string(), cr)
268 });
269
270 let dep_tables = dep_tables(cargo_toml).chain(target_dep_tables(cargo_toml));
271 let dep_pkgs = dep_tables.flatten().filter_map(move |(dep_name, dep_value)| {
272 let pkg_name = dep_value
273 .get("package")
274 .and_then(|i| i.as_str())
275 .unwrap_or(dep_name);
276
277 if package_name.as_ref().map_or(false, |n| *n == pkg_name) {
278 return None;
279 }
280
281 let cr = FoundCrate::Name(sanitize_crate_name(dep_name));
282
283 Some((pkg_name.to_owned(), cr))
284 });
285
286 Ok(root_pkg.into_iter().chain(dep_pkgs).collect())
287}
288
289fn extract_package_name(cargo_toml: &Document) -> Option<&str> {
290 cargo_toml.get("package")?.get("name")?.as_str()
291}
292
293fn target_dep_tables(cargo_toml: &Document) -> impl Iterator<Item = &Table> {
294 cargo_toml
295 .get("target")
296 .into_iter()
297 .filter_map(Item::as_table)
298 .flat_map(|t| {
299 t.iter()
300 .map(|(_, value)| value)
301 .filter_map(Item::as_table)
302 .flat_map(dep_tables)
303 })
304}
305
306fn dep_tables(table: &Table) -> impl Iterator<Item = &Table> {
307 table
308 .get("dependencies")
309 .into_iter()
310 .chain(table.get("dev-dependencies"))
311 .filter_map(Item::as_table)
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 macro_rules! create_test {
319 (
320 $name:ident,
321 $cargo_toml:expr,
322 $( $result:tt )*
323 ) => {
324 #[test]
325 fn $name() {
326 let cargo_toml = $cargo_toml.parse::<Document>().expect("Parses `Cargo.toml`");
327
328 match extract_crate_names(&cargo_toml).map(|mut map| map.remove("my_crate")) {
329 $( $result )* => (),
330 o => panic!("Invalid result: {:?}", o),
331 }
332 }
333 };
334 }
335
336 create_test! {
337 deps_with_crate,
338 r#"
339 [dependencies]
340 my_crate = "0.1"
341 "#,
342 Ok(Some(FoundCrate::Name(name))) if name == "my_crate"
343 }
344
345 create_test! {
346 dev_deps_with_crate,
347 r#"
348 [dev-dependencies]
349 my_crate = "0.1"
350 "#,
351 Ok(Some(FoundCrate::Name(name))) if name == "my_crate"
352 }
353
354 create_test! {
355 deps_with_crate_renamed,
356 r#"
357 [dependencies]
358 cool = { package = "my_crate", version = "0.1" }
359 "#,
360 Ok(Some(FoundCrate::Name(name))) if name == "cool"
361 }
362
363 create_test! {
364 deps_with_crate_renamed_second,
365 r#"
366 [dependencies.cool]
367 package = "my_crate"
368 version = "0.1"
369 "#,
370 Ok(Some(FoundCrate::Name(name))) if name == "cool"
371 }
372
373 create_test! {
374 deps_empty,
375 r#"
376 [dependencies]
377 "#,
378 Ok(None)
379 }
380
381 create_test! {
382 crate_not_found,
383 r#"
384 [dependencies]
385 serde = "1.0"
386 "#,
387 Ok(None)
388 }
389
390 create_test! {
391 target_dependency,
392 r#"
393 [target.'cfg(target_os="android")'.dependencies]
394 my_crate = "0.1"
395 "#,
396 Ok(Some(FoundCrate::Name(name))) if name == "my_crate"
397 }
398
399 create_test! {
400 target_dependency2,
401 r#"
402 [target.x86_64-pc-windows-gnu.dependencies]
403 my_crate = "0.1"
404 "#,
405 Ok(Some(FoundCrate::Name(name))) if name == "my_crate"
406 }
407
408 create_test! {
409 own_crate,
410 r#"
411 [package]
412 name = "my_crate"
413 "#,
414 Ok(Some(FoundCrate::Itself))
415 }
416
417 create_test! {
418 own_crate_and_in_deps,
419 r#"
420 [package]
421 name = "my_crate"
422
423 [dev-dependencies]
424 my_crate = "0.1"
425 "#,
426 Ok(Some(FoundCrate::Itself))
427 }
428
429 create_test! {
430 multiple_times,
431 r#"
432 [dependencies]
433 my_crate = { version = "0.5" }
434 my-crate-old = { package = "my_crate", version = "0.1" }
435 "#,
436 Ok(Some(FoundCrate::Name(name))) if name == "my_crate_old"
437 }
438}