1 // Parse system-deps metadata from Cargo.toml
2 
3 use std::{fs, io::Read, path::Path};
4 
5 use anyhow::{anyhow, bail, Error};
6 use toml::{map::Map, Value};
7 
8 #[derive(Debug, PartialEq)]
9 pub(crate) struct MetaData {
10     pub(crate) deps: Vec<Dependency>,
11 }
12 
13 #[derive(Debug, PartialEq)]
14 pub(crate) struct Dependency {
15     pub(crate) key: String,
16     pub(crate) version: Option<String>,
17     pub(crate) name: Option<String>,
18     pub(crate) feature: Option<String>,
19     pub(crate) optional: bool,
20     pub(crate) cfg: Option<cfg_expr::Expression>,
21     pub(crate) version_overrides: Vec<VersionOverride>,
22 }
23 
24 impl Dependency {
new(name: &str) -> Self25     fn new(name: &str) -> Self {
26         Self {
27             key: name.to_string(),
28             ..Default::default()
29         }
30     }
31 
lib_name(&self) -> String32     pub(crate) fn lib_name(&self) -> String {
33         self.name.as_ref().unwrap_or(&self.key).to_string()
34     }
35 }
36 
37 impl Default for Dependency {
default() -> Self38     fn default() -> Self {
39         Self {
40             key: "".to_string(),
41             version: None,
42             name: None,
43             feature: None,
44             optional: false,
45             cfg: None,
46             version_overrides: Vec::new(),
47         }
48     }
49 }
50 
51 #[derive(Debug, PartialEq)]
52 pub(crate) struct VersionOverride {
53     pub(crate) key: String,
54     pub(crate) version: String,
55     pub(crate) name: Option<String>,
56     pub(crate) optional: Option<bool>,
57 }
58 
59 struct VersionOverrideBuilder {
60     version_id: String,
61     version: Option<String>,
62     full_name: Option<String>,
63     optional: Option<bool>,
64 }
65 
66 impl VersionOverrideBuilder {
new(version_id: &str) -> Self67     fn new(version_id: &str) -> Self {
68         Self {
69             version_id: version_id.to_string(),
70             version: None,
71             full_name: None,
72             optional: None,
73         }
74     }
75 
build(self) -> Result<VersionOverride, Error>76     fn build(self) -> Result<VersionOverride, Error> {
77         let version = self
78             .version
79             .ok_or_else(|| anyhow!("missing version field"))?;
80 
81         Ok(VersionOverride {
82             key: self.version_id,
83             version,
84             name: self.full_name,
85             optional: self.optional,
86         })
87     }
88 }
89 
90 impl MetaData {
from_file(path: &Path) -> Result<Self, crate::Error>91     pub(crate) fn from_file(path: &Path) -> Result<Self, crate::Error> {
92         let mut manifest = fs::File::open(&path).map_err(|e| {
93             crate::Error::FailToRead(format!("error opening {}", path.display()), e)
94         })?;
95 
96         let mut manifest_str = String::new();
97         manifest.read_to_string(&mut manifest_str).map_err(|e| {
98             crate::Error::FailToRead(format!("error reading {}", path.display()), e)
99         })?;
100 
101         Self::from_str(manifest_str)
102             .map_err(|e| crate::Error::InvalidMetadata(format!("{}: {}", path.display(), e)))
103     }
104 
from_str(manifest_str: String) -> Result<Self, Error>105     fn from_str(manifest_str: String) -> Result<Self, Error> {
106         let toml = manifest_str
107             .parse::<toml::Value>()
108             .map_err(|e| anyhow!("error parsing TOML: {:?}", e))?;
109 
110         let key = "package.metadata.system-deps";
111         let meta = toml
112             .get("package")
113             .and_then(|v| v.get("metadata"))
114             .and_then(|v| v.get("system-deps"))
115             .ok_or_else(|| anyhow!("no {}", key))?;
116 
117         let deps = Self::parse_deps_table(meta, key, true)?;
118 
119         Ok(MetaData { deps })
120     }
121 
parse_deps_table( table: &Value, key: &str, allow_cfg: bool, ) -> Result<Vec<Dependency>, Error>122     fn parse_deps_table(
123         table: &Value,
124         key: &str,
125         allow_cfg: bool,
126     ) -> Result<Vec<Dependency>, Error> {
127         let table = table
128             .as_table()
129             .ok_or_else(|| anyhow!("{} not a table", key))?;
130 
131         let mut deps = Vec::new();
132 
133         for (name, value) in table {
134             if name.starts_with("cfg(") {
135                 if allow_cfg {
136                     let cfg_exp = cfg_expr::Expression::parse(name)?;
137 
138                     for mut dep in
139                         Self::parse_deps_table(value, &format!("{}.{}", key, name), false)?
140                     {
141                         dep.cfg = Some(cfg_exp.clone());
142                         deps.push(dep);
143                     }
144                 } else {
145                     bail!("{}.{}: cfg() cannot be nested", key, name);
146                 }
147             } else {
148                 let dep =
149                     Self::parse_dep(name, value).map_err(|e| anyhow!("{}.{}: {}", key, name, e))?;
150                 deps.push(dep);
151             }
152         }
153 
154         Ok(deps)
155     }
156 
parse_dep(name: &str, value: &Value) -> Result<Dependency, Error>157     fn parse_dep(name: &str, value: &Value) -> Result<Dependency, Error> {
158         let mut dep = Dependency::new(name);
159 
160         match value {
161             // somelib = "1.0"
162             toml::Value::String(ref s) => {
163                 dep.version = Some(s.clone());
164             }
165             toml::Value::Table(ref t) => {
166                 Self::parse_dep_table(&mut dep, t)?;
167             }
168             _ => {
169                 bail!("not a string or table");
170             }
171         }
172 
173         Ok(dep)
174     }
175 
parse_dep_table(dep: &mut Dependency, t: &Map<String, Value>) -> Result<(), Error>176     fn parse_dep_table(dep: &mut Dependency, t: &Map<String, Value>) -> Result<(), Error> {
177         for (key, value) in t {
178             match (key.as_str(), value) {
179                 ("feature", &toml::Value::String(ref s)) => {
180                     dep.feature = Some(s.clone());
181                 }
182                 ("version", &toml::Value::String(ref s)) => {
183                     dep.version = Some(s.clone());
184                 }
185                 ("name", &toml::Value::String(ref s)) => {
186                     dep.name = Some(s.clone());
187                 }
188                 ("optional", &toml::Value::Boolean(optional)) => {
189                     dep.optional = optional;
190                 }
191                 (version_feature, &toml::Value::Table(ref version_settings))
192                     if version_feature.starts_with('v') =>
193                 {
194                     let mut builder = VersionOverrideBuilder::new(version_feature);
195 
196                     for (k, v) in version_settings {
197                         match (k.as_str(), v) {
198                             ("version", &toml::Value::String(ref feat_vers)) => {
199                                 builder.version = Some(feat_vers.into());
200                             }
201                             ("name", &toml::Value::String(ref feat_name)) => {
202                                 builder.full_name = Some(feat_name.into());
203                             }
204                             ("optional", &toml::Value::Boolean(optional)) => {
205                                 builder.optional = Some(optional);
206                             }
207                             _ => {
208                                 bail!(
209                                     "unexpected version settings key: {} type: {}",
210                                     k,
211                                     v.type_str()
212                                 )
213                             }
214                         }
215                     }
216 
217                     dep.version_overrides.push(builder.build()?);
218                 }
219                 _ => {
220                     bail!("unexpected key {} type {}", key, value.type_str());
221                 }
222             }
223         }
224         Ok(())
225     }
226 }
227 
228 #[cfg(test)]
229 mod tests {
230     use super::*;
231     use assert_matches::assert_matches;
232     use cfg_expr::Expression;
233     use std::{env, path::PathBuf};
234 
parse_file(dir: &str) -> Result<MetaData, crate::Error>235     fn parse_file(dir: &str) -> Result<MetaData, crate::Error> {
236         let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
237         let mut p: PathBuf = manifest_dir.into();
238         p.push("src");
239         p.push("tests");
240         p.push(dir);
241         p.push("Cargo.toml");
242         assert!(p.exists());
243 
244         MetaData::from_file(&p)
245     }
246 
247     #[test]
parse_good()248     fn parse_good() {
249         let m = parse_file("toml-good").unwrap();
250 
251         assert_eq!(
252             m,
253             MetaData {
254                 deps: vec![
255                     Dependency {
256                         key: "testdata".into(),
257                         version: Some("4".into()),
258                         ..Default::default()
259                     },
260                     Dependency {
261                         key: "testlib".into(),
262                         version: Some("1".into()),
263                         feature: Some("test-feature".into()),
264                         ..Default::default()
265                     },
266                     Dependency {
267                         key: "testmore".into(),
268                         version: Some("2".into()),
269                         feature: Some("another-test-feature".into()),
270                         ..Default::default()
271                     }
272                 ]
273             }
274         )
275     }
276 
277     #[test]
parse_feature_not_string()278     fn parse_feature_not_string() {
279         assert_matches!(
280             parse_file("toml-feature-not-string"),
281             Err(crate::Error::InvalidMetadata(_))
282         );
283     }
284 
285     #[test]
parse_override_name()286     fn parse_override_name() {
287         let m = parse_file("toml-override-name").unwrap();
288 
289         assert_eq!(
290             m,
291             MetaData {
292                 deps: vec![Dependency {
293                     key: "test_lib".into(),
294                     version: Some("1.0".into()),
295                     name: Some("testlib".into()),
296                     version_overrides: vec![VersionOverride {
297                         key: "v1_2".into(),
298                         version: "1.2".into(),
299                         name: None,
300                         optional: None,
301                     }],
302                     ..Default::default()
303                 },]
304             }
305         )
306     }
307 
308     #[test]
parse_feature_versions()309     fn parse_feature_versions() {
310         let m = parse_file("toml-feature-versions").unwrap();
311 
312         assert_eq!(
313             m,
314             MetaData {
315                 deps: vec![Dependency {
316                     key: "testdata".into(),
317                     version: Some("4".into()),
318                     version_overrides: vec![
319                         VersionOverride {
320                             key: "v5".into(),
321                             version: "5".into(),
322                             name: None,
323                             optional: None,
324                         },
325                         VersionOverride {
326                             key: "v6".into(),
327                             version: "6".into(),
328                             name: None,
329                             optional: None,
330                         },
331                     ],
332                     ..Default::default()
333                 },]
334             }
335         )
336     }
337 
338     #[test]
parse_optional()339     fn parse_optional() {
340         let m = parse_file("toml-optional").unwrap();
341 
342         assert_eq!(
343             m,
344             MetaData {
345                 deps: vec![
346                     Dependency {
347                         key: "testbadger".into(),
348                         version: Some("1".into()),
349                         optional: true,
350                         ..Default::default()
351                     },
352                     Dependency {
353                         key: "testlib".into(),
354                         version: Some("1.0".into()),
355                         optional: true,
356                         version_overrides: vec![VersionOverride {
357                             key: "v5".into(),
358                             version: "5.0".into(),
359                             name: Some("testlib-5.0".into()),
360                             optional: Some(false),
361                         },],
362                         ..Default::default()
363                     },
364                     Dependency {
365                         key: "testmore".into(),
366                         version: Some("2".into()),
367                         version_overrides: vec![VersionOverride {
368                             key: "v3".into(),
369                             version: "3.0".into(),
370                             name: None,
371                             optional: Some(true),
372                         },],
373                         ..Default::default()
374                     },
375                 ]
376             }
377         )
378     }
379 
380     #[test]
parse_os_specific()381     fn parse_os_specific() {
382         let m = parse_file("toml-os-specific").unwrap();
383 
384         assert_eq!(
385             m,
386             MetaData {
387                 deps: vec![
388                     Dependency {
389                         key: "testlib".into(),
390                         version: Some("1".into()),
391                         cfg: Some(Expression::parse("not(target_os = \"macos\")").unwrap()),
392                         ..Default::default()
393                     },
394                     Dependency {
395                         key: "testdata".into(),
396                         version: Some("1".into()),
397                         cfg: Some(Expression::parse("target_os = \"linux\"").unwrap()),
398                         ..Default::default()
399                     },
400                     Dependency {
401                         key: "testanotherlib".into(),
402                         version: Some("1".into()),
403                         cfg: Some(Expression::parse("unix").unwrap()),
404                         optional: true,
405                         ..Default::default()
406                     },
407                 ]
408             }
409         )
410     }
411 }
412