1use std::{
60 collections::HashMap,
61 env,
62 fs::File,
63 io::{self, Read},
64 path::{Path, PathBuf},
65};
66
67use toml::{self, value::Table};
68
69type CargoToml = HashMap<String, toml::Value>;
70
71#[derive(Debug, thiserror::Error)]
73pub enum Error {
74 #[error("Could not find `Cargo.toml` in manifest dir: `{0}`.")]
75 NotFound(PathBuf),
76 #[error("`CARGO_MANIFEST_DIR` env variable not set.")]
77 CargoManifestDirNotSet,
78 #[error("Could not read `{path}`.")]
79 CouldNotRead { path: PathBuf, source: io::Error },
80 #[error("Invalid toml file.")]
81 InvalidToml { source: toml::de::Error },
82 #[error("Could not find `{crate_name}` in `dependencies` or `dev-dependencies` in `{path}`!")]
83 CrateNotFound { crate_name: String, path: PathBuf },
84}
85
86#[derive(Debug, PartialEq, Clone, Eq)]
88pub enum FoundCrate {
89 Itself,
91 Name(String),
93}
94
95pub fn crate_name(orig_name: &str) -> Result<FoundCrate, Error> {
111 let manifest_dir =
112 PathBuf::from(env::var("CARGO_MANIFEST_DIR").map_err(|_| Error::CargoManifestDirNotSet)?);
113
114 let cargo_toml_path = manifest_dir.join("Cargo.toml");
115
116 if !cargo_toml_path.exists() {
117 return Err(Error::NotFound(manifest_dir.into()));
118 }
119
120 let cargo_toml = open_cargo_toml(&cargo_toml_path)?;
121
122 extract_crate_name(orig_name, cargo_toml, &cargo_toml_path)
123}
124
125fn sanitize_crate_name(name: String) -> String {
127 name.replace("-", "_")
128}
129
130fn open_cargo_toml(path: &Path) -> Result<CargoToml, Error> {
132 let mut content = String::new();
133 File::open(path)
134 .map_err(|e| Error::CouldNotRead {
135 source: e,
136 path: path.into(),
137 })?
138 .read_to_string(&mut content)
139 .map_err(|e| Error::CouldNotRead {
140 source: e,
141 path: path.into(),
142 })?;
143 toml::from_str(&content).map_err(|e| Error::InvalidToml { source: e })
144}
145
146fn extract_crate_name(
152 orig_name: &str,
153 mut cargo_toml: CargoToml,
154 cargo_toml_path: &Path,
155) -> Result<FoundCrate, Error> {
156 if let Some(toml::Value::Table(t)) = cargo_toml.get("package") {
157 if let Some(toml::Value::String(s)) = t.get("name") {
158 if s == orig_name {
159 return Ok(FoundCrate::Itself);
160 }
161 }
162 }
163
164 if let Some(name) = ["dependencies", "dev-dependencies"]
165 .iter()
166 .find_map(|k| search_crate_at_key(k, orig_name, &mut cargo_toml))
167 {
168 return Ok(FoundCrate::Name(sanitize_crate_name(name)));
169 }
170
171 if let Some(name) = cargo_toml
173 .remove("target")
174 .and_then(|t| t.try_into::<Table>().ok())
175 .and_then(|t| {
176 t.values()
177 .filter_map(|v| v.as_table())
178 .filter_map(|t| t.get("dependencies").and_then(|t| t.as_table()))
179 .find_map(|t| extract_crate_name_from_deps(orig_name, t.clone()))
180 })
181 {
182 return Ok(FoundCrate::Name(sanitize_crate_name(name)));
183 }
184
185 Err(Error::CrateNotFound {
186 crate_name: orig_name.into(),
187 path: cargo_toml_path.into(),
188 })
189}
190
191fn search_crate_at_key(key: &str, orig_name: &str, cargo_toml: &mut CargoToml) -> Option<String> {
193 cargo_toml
194 .remove(key)
195 .and_then(|v| v.try_into::<Table>().ok())
196 .and_then(|t| extract_crate_name_from_deps(orig_name, t))
197}
198
199fn extract_crate_name_from_deps(orig_name: &str, deps: Table) -> Option<String> {
204 for (key, value) in deps.into_iter() {
205 let renamed = value
206 .try_into::<Table>()
207 .ok()
208 .and_then(|t| t.get("package").cloned())
209 .map(|t| t.as_str() == Some(orig_name))
210 .unwrap_or(false);
211
212 if key == orig_name || renamed {
213 return Some(key.clone());
214 }
215 }
216
217 None
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 macro_rules! create_test {
225 (
226 $name:ident,
227 $cargo_toml:expr,
228 $( $result:tt )*
229 ) => {
230 #[test]
231 fn $name() {
232 let cargo_toml = toml::from_str($cargo_toml).expect("Parses `Cargo.toml`");
233 let path = PathBuf::from("test-path");
234
235 match extract_crate_name("my_crate", cargo_toml, &path) {
236 $( $result )* => (),
237 o => panic!("Invalid result: {:?}", o),
238 }
239 }
240 };
241 }
242
243 create_test! {
244 deps_with_crate,
245 r#"
246 [dependencies]
247 my_crate = "0.1"
248 "#,
249 Ok(FoundCrate::Name(name)) if name == "my_crate"
250 }
251
252 create_test! {
253 dev_deps_with_crate,
254 r#"
255 [dev-dependencies]
256 my_crate = "0.1"
257 "#,
258 Ok(FoundCrate::Name(name)) if name == "my_crate"
259 }
260
261 create_test! {
262 deps_with_crate_renamed,
263 r#"
264 [dependencies]
265 cool = { package = "my_crate", version = "0.1" }
266 "#,
267 Ok(FoundCrate::Name(name)) if name == "cool"
268 }
269
270 create_test! {
271 deps_with_crate_renamed_second,
272 r#"
273 [dependencies.cool]
274 package = "my_crate"
275 version = "0.1"
276 "#,
277 Ok(FoundCrate::Name(name)) if name == "cool"
278 }
279
280 create_test! {
281 deps_empty,
282 r#"
283 [dependencies]
284 "#,
285 Err(Error::CrateNotFound {
286 crate_name,
287 path,
288 }) if crate_name == "my_crate" && path.display().to_string() == "test-path"
289 }
290
291 create_test! {
292 crate_not_found,
293 r#"
294 [dependencies]
295 serde = "1.0"
296 "#,
297 Err(Error::CrateNotFound {
298 crate_name,
299 path,
300 }) if crate_name == "my_crate" && path.display().to_string() == "test-path"
301 }
302
303 create_test! {
304 target_dependency,
305 r#"
306 [target.'cfg(target_os="android")'.dependencies]
307 my_crate = "0.1"
308 "#,
309 Ok(FoundCrate::Name(name)) if name == "my_crate"
310 }
311
312 create_test! {
313 target_dependency2,
314 r#"
315 [target.x86_64-pc-windows-gnu.dependencies]
316 my_crate = "0.1"
317 "#,
318 Ok(FoundCrate::Name(name)) if name == "my_crate"
319 }
320
321 create_test! {
322 own_crate,
323 r#"
324 [package]
325 name = "my_crate"
326 "#,
327 Ok(FoundCrate::Itself)
328 }
329}