use std::{
collections::HashMap,
env,
fs::File,
io::{self, Read},
path::{Path, PathBuf},
};
use toml::{self, value::Table};
type CargoToml = HashMap<String, toml::Value>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Could not find `Cargo.toml` in manifest dir: `{0}`.")]
NotFound(PathBuf),
#[error("`CARGO_MANIFEST_DIR` env variable not set.")]
CargoManifestDirNotSet,
#[error("Could not read `{path}`.")]
CouldNotRead { path: PathBuf, source: io::Error },
#[error("Invalid toml file.")]
InvalidToml { source: toml::de::Error },
#[error("Could not find `{crate_name}` in `dependencies` or `dev-dependencies` in `{path}`!")]
CrateNotFound { crate_name: String, path: PathBuf },
}
#[derive(Debug, PartialEq, Clone, Eq)]
pub enum FoundCrate {
Itself,
Name(String),
}
pub fn crate_name(orig_name: &str) -> Result<FoundCrate, Error> {
let manifest_dir =
PathBuf::from(env::var("CARGO_MANIFEST_DIR").map_err(|_| Error::CargoManifestDirNotSet)?);
let cargo_toml_path = manifest_dir.join("Cargo.toml");
if !cargo_toml_path.exists() {
return Err(Error::NotFound(manifest_dir.into()));
}
let cargo_toml = open_cargo_toml(&cargo_toml_path)?;
extract_crate_name(orig_name, cargo_toml, &cargo_toml_path)
}
fn sanitize_crate_name<S: AsRef<str>>(name: S) -> String {
name.as_ref().replace("-", "_")
}
fn open_cargo_toml(path: &Path) -> Result<CargoToml, Error> {
let mut content = String::new();
File::open(path)
.map_err(|e| Error::CouldNotRead {
source: e,
path: path.into(),
})?
.read_to_string(&mut content)
.map_err(|e| Error::CouldNotRead {
source: e,
path: path.into(),
})?;
toml::from_str(&content).map_err(|e| Error::InvalidToml { source: e })
}
fn extract_crate_name(
orig_name: &str,
mut cargo_toml: CargoToml,
cargo_toml_path: &Path,
) -> Result<FoundCrate, Error> {
if let Some(toml::Value::Table(t)) = cargo_toml.get("package") {
if let Some(toml::Value::String(s)) = t.get("name") {
if s == orig_name {
if std::env::var_os("CARGO_TARGET_TMPDIR").is_none() {
return Ok(FoundCrate::Itself);
} else {
return Ok(FoundCrate::Name(sanitize_crate_name(orig_name)));
}
}
}
}
if let Some(name) = ["dependencies", "dev-dependencies"]
.iter()
.find_map(|k| search_crate_at_key(k, orig_name, &mut cargo_toml))
{
return Ok(FoundCrate::Name(sanitize_crate_name(name)));
}
if let Some(name) = cargo_toml
.remove("target")
.and_then(|t| t.try_into::<Table>().ok())
.and_then(|t| {
t.values()
.filter_map(|v| v.as_table())
.filter_map(|t| t.get("dependencies").or_else(|| t.get("dev-dependencies")))
.filter_map(|t| t.as_table())
.find_map(|t| extract_crate_name_from_deps(orig_name, t.clone()))
})
{
return Ok(FoundCrate::Name(sanitize_crate_name(name)));
}
Err(Error::CrateNotFound {
crate_name: orig_name.into(),
path: cargo_toml_path.into(),
})
}
fn search_crate_at_key(key: &str, orig_name: &str, cargo_toml: &mut CargoToml) -> Option<String> {
cargo_toml
.remove(key)
.and_then(|v| v.try_into::<Table>().ok())
.and_then(|t| extract_crate_name_from_deps(orig_name, t))
}
fn extract_crate_name_from_deps(orig_name: &str, deps: Table) -> Option<String> {
for (key, value) in deps.into_iter() {
let renamed = value
.try_into::<Table>()
.ok()
.and_then(|t| t.get("package").cloned())
.map(|t| t.as_str() == Some(orig_name))
.unwrap_or(false);
if key == orig_name || renamed {
return Some(key.clone());
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! create_test {
(
$name:ident,
$cargo_toml:expr,
$( $result:tt )*
) => {
#[test]
fn $name() {
let cargo_toml = toml::from_str($cargo_toml).expect("Parses `Cargo.toml`");
let path = PathBuf::from("test-path");
match extract_crate_name("my_crate", cargo_toml, &path) {
$( $result )* => (),
o => panic!("Invalid result: {:?}", o),
}
}
};
}
create_test! {
deps_with_crate,
r#"
[dependencies]
my_crate = "0.1"
"#,
Ok(FoundCrate::Name(name)) if name == "my_crate"
}
create_test! {
dev_deps_with_crate,
r#"
[dev-dependencies]
my_crate = "0.1"
"#,
Ok(FoundCrate::Name(name)) if name == "my_crate"
}
create_test! {
deps_with_crate_renamed,
r#"
[dependencies]
cool = { package = "my_crate", version = "0.1" }
"#,
Ok(FoundCrate::Name(name)) if name == "cool"
}
create_test! {
deps_with_crate_renamed_second,
r#"
[dependencies.cool]
package = "my_crate"
version = "0.1"
"#,
Ok(FoundCrate::Name(name)) if name == "cool"
}
create_test! {
deps_empty,
r#"
[dependencies]
"#,
Err(Error::CrateNotFound {
crate_name,
path,
}) if crate_name == "my_crate" && path.display().to_string() == "test-path"
}
create_test! {
crate_not_found,
r#"
[dependencies]
serde = "1.0"
"#,
Err(Error::CrateNotFound {
crate_name,
path,
}) if crate_name == "my_crate" && path.display().to_string() == "test-path"
}
create_test! {
target_dependency,
r#"
[target.'cfg(target_os="android")'.dependencies]
my_crate = "0.1"
"#,
Ok(FoundCrate::Name(name)) if name == "my_crate"
}
create_test! {
target_dependency2,
r#"
[target.x86_64-pc-windows-gnu.dependencies]
my_crate = "0.1"
"#,
Ok(FoundCrate::Name(name)) if name == "my_crate"
}
create_test! {
own_crate,
r#"
[package]
name = "my_crate"
"#,
Ok(FoundCrate::Itself)
}
}