system_deps/
metadata.rs

1// Parse system-deps metadata from Cargo.toml
2
3use std::{fmt, fs, io::Read, path::Path};
4
5use toml::{map::Map, Value};
6
7#[derive(Debug, PartialEq)]
8pub(crate) struct MetaData {
9    pub(crate) deps: Vec<Dependency>,
10}
11
12#[derive(Debug, Clone, PartialEq)]
13pub(crate) struct Dependency {
14    pub(crate) key: String,
15    pub(crate) version: Option<String>,
16    pub(crate) name: Option<String>,
17    pub(crate) fallback_names: Option<Vec<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
24impl Dependency {
25    fn new(name: &str) -> Self {
26        Self {
27            key: name.to_string(),
28            ..Default::default()
29        }
30    }
31
32    pub(crate) fn lib_name(&self) -> &str {
33        self.name.as_ref().unwrap_or(&self.key)
34    }
35}
36
37impl Default for Dependency {
38    fn default() -> Self {
39        Self {
40            key: "".to_string(),
41            version: None,
42            name: None,
43            fallback_names: None,
44            feature: None,
45            optional: false,
46            cfg: None,
47            version_overrides: Vec::new(),
48        }
49    }
50}
51
52#[derive(Debug, PartialEq)]
53enum VersionOverrideBuilderError {
54    MissingVersionField,
55}
56
57impl fmt::Display for VersionOverrideBuilderError {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        match *self {
60            Self::MissingVersionField => write!(f, "missing version field"),
61        }
62    }
63}
64
65impl std::error::Error for VersionOverrideBuilderError {}
66
67#[derive(Debug, PartialEq)]
68enum MetadataError {
69    MissingKey(String),
70    NotATable(String),
71    NestedCfg(String),
72    NotStringOrTable(String),
73    NotString(String),
74    CfgExpr(cfg_expr::ParseError),
75    Toml(toml::de::Error),
76    UnexpectedVersionSetting(String, String, String),
77    UnexpectedKey(String, String, String),
78    VersionOverrideBuilder(VersionOverrideBuilderError),
79}
80
81impl fmt::Display for MetadataError {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        match self {
84            Self::MissingKey(k) => write!(f, "missing key `{}`", k),
85            Self::NotATable(k) => write!(f, "`{}` is not a table", k),
86            Self::NestedCfg(k) => write!(f, "`{}`: cfg() cannot be nested", k),
87            Self::NotString(k) => write!(f, "`{}`: not a string", k),
88            Self::NotStringOrTable(k) => write!(f, "`{}`: not a string or a table", k),
89            Self::CfgExpr(e) => write!(f, "{}", e),
90            Self::Toml(e) => write!(f, "error parsing TOML: {}", e),
91            Self::UnexpectedVersionSetting(n, k, t) => {
92                write!(
93                    f,
94                    "{}: unexpected version settings key: {} type: {}",
95                    n, k, t
96                )
97            }
98            Self::UnexpectedKey(n, k, t) => write!(f, "{}: unexpected key {} type {}", n, k, t),
99            Self::VersionOverrideBuilder(e) => write!(f, "{}", e),
100        }
101    }
102}
103
104impl std::error::Error for MetadataError {
105    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
106        match self {
107            Self::CfgExpr(e) => Some(e),
108            Self::Toml(e) => Some(e),
109            Self::VersionOverrideBuilder(e) => Some(e),
110            _ => None,
111        }
112    }
113}
114
115impl From<cfg_expr::ParseError> for MetadataError {
116    fn from(err: cfg_expr::ParseError) -> Self {
117        Self::CfgExpr(err)
118    }
119}
120
121impl From<toml::de::Error> for MetadataError {
122    fn from(err: toml::de::Error) -> Self {
123        Self::Toml(err)
124    }
125}
126
127impl From<VersionOverrideBuilderError> for MetadataError {
128    fn from(err: VersionOverrideBuilderError) -> Self {
129        Self::VersionOverrideBuilder(err)
130    }
131}
132
133#[derive(Debug, Clone, PartialEq)]
134pub(crate) struct VersionOverride {
135    pub(crate) key: String,
136    pub(crate) version: String,
137    pub(crate) name: Option<String>,
138    pub(crate) fallback_names: Option<Vec<String>>,
139    pub(crate) optional: Option<bool>,
140}
141
142struct VersionOverrideBuilder {
143    version_id: String,
144    version: Option<String>,
145    full_name: Option<String>,
146    fallback_names: Option<Vec<String>>,
147    optional: Option<bool>,
148}
149
150impl VersionOverrideBuilder {
151    fn new(version_id: &str) -> Self {
152        Self {
153            version_id: version_id.to_string(),
154            version: None,
155            full_name: None,
156            fallback_names: None,
157            optional: None,
158        }
159    }
160
161    fn build(self) -> Result<VersionOverride, VersionOverrideBuilderError> {
162        let version = self
163            .version
164            .ok_or(VersionOverrideBuilderError::MissingVersionField)?;
165
166        Ok(VersionOverride {
167            key: self.version_id,
168            version,
169            name: self.full_name,
170            fallback_names: self.fallback_names,
171            optional: self.optional,
172        })
173    }
174}
175
176impl MetaData {
177    pub(crate) fn from_file(path: &Path) -> Result<Self, crate::Error> {
178        let mut manifest = fs::File::open(path).map_err(|e| {
179            crate::Error::FailToRead(format!("error opening {}", path.display()), e)
180        })?;
181
182        let mut manifest_str = String::new();
183        manifest.read_to_string(&mut manifest_str).map_err(|e| {
184            crate::Error::FailToRead(format!("error reading {}", path.display()), e)
185        })?;
186
187        Self::from_str(manifest_str)
188            .map_err(|e| crate::Error::InvalidMetadata(format!("{}: {}", path.display(), e)))
189    }
190
191    fn from_str(manifest_str: String) -> Result<Self, MetadataError> {
192        let toml = manifest_str.parse::<toml::Value>()?;
193        let key = "package.metadata.system-deps";
194        let meta = toml
195            .get("package")
196            .and_then(|v| v.get("metadata"))
197            .and_then(|v| v.get("system-deps"))
198            .ok_or_else(|| MetadataError::MissingKey(key.to_owned()))?;
199
200        let deps = Self::parse_deps_table(meta, key, true)?;
201
202        Ok(MetaData { deps })
203    }
204
205    fn parse_deps_table(
206        table: &Value,
207        key: &str,
208        allow_cfg: bool,
209    ) -> Result<Vec<Dependency>, MetadataError> {
210        let table = table
211            .as_table()
212            .ok_or_else(|| MetadataError::NotATable(key.to_owned()))?;
213
214        let mut deps = Vec::new();
215
216        for (name, value) in table {
217            if name.starts_with("cfg(") {
218                if allow_cfg {
219                    let cfg_exp = cfg_expr::Expression::parse(name)?;
220
221                    for mut dep in
222                        Self::parse_deps_table(value, &format!("{}.{}", key, name), false)?
223                    {
224                        dep.cfg = Some(cfg_exp.clone());
225                        deps.push(dep);
226                    }
227                } else {
228                    return Err(MetadataError::NestedCfg(format!("{}.{}", key, name)));
229                }
230            } else {
231                let dep = Self::parse_dep(key, name, value)?;
232                deps.push(dep);
233            }
234        }
235
236        Ok(deps)
237    }
238
239    fn parse_dep(key: &str, name: &str, value: &Value) -> Result<Dependency, MetadataError> {
240        let mut dep = Dependency::new(name);
241
242        match value {
243            // somelib = "1.0"
244            toml::Value::String(ref s) => {
245                if !validate_version(s) {
246                    return Err(MetadataError::UnexpectedVersionSetting(
247                        key.into(),
248                        name.into(),
249                        value.type_str().to_owned(),
250                    ));
251                }
252
253                dep.version = Some(s.clone());
254            }
255            toml::Value::Table(ref t) => {
256                Self::parse_dep_table(key, name, &mut dep, t)?;
257            }
258            _ => {
259                return Err(MetadataError::NotStringOrTable(format!("{}.{}", key, name)));
260            }
261        }
262
263        Ok(dep)
264    }
265
266    fn parse_dep_table(
267        p_key: &str,
268        name: &str,
269        dep: &mut Dependency,
270        t: &Map<String, Value>,
271    ) -> Result<(), MetadataError> {
272        for (key, value) in t {
273            match (key.as_str(), value) {
274                ("feature", toml::Value::String(s)) => {
275                    dep.feature = Some(s.clone());
276                }
277                ("version", toml::Value::String(s)) => {
278                    if !validate_version(s) {
279                        return Err(MetadataError::UnexpectedVersionSetting(
280                            format!("{}.{}", p_key, name),
281                            key.into(),
282                            value.type_str().to_owned(),
283                        ));
284                    }
285
286                    dep.version = Some(s.clone());
287                }
288                ("name", toml::Value::String(s)) => {
289                    dep.name = Some(s.clone());
290                }
291                ("fallback-names", toml::Value::Array(values)) => {
292                    let key = format!("{}.{}", p_key, name);
293                    dep.fallback_names = Some(Self::parse_name_list(&key, values)?);
294                }
295                ("optional", &toml::Value::Boolean(optional)) => {
296                    dep.optional = optional;
297                }
298                (version_feature, toml::Value::Table(version_settings))
299                    if version_feature.starts_with('v') =>
300                {
301                    let mut builder = VersionOverrideBuilder::new(version_feature);
302
303                    for (k, v) in version_settings {
304                        match (k.as_str(), v) {
305                            ("version", toml::Value::String(feat_vers)) => {
306                                if !validate_version(feat_vers) {
307                                    return Err(MetadataError::UnexpectedVersionSetting(
308                                        format!("{}.{}", p_key, name),
309                                        k.into(),
310                                        v.type_str().to_owned(),
311                                    ));
312                                }
313
314                                builder.version = Some(feat_vers.into());
315                            }
316                            ("name", toml::Value::String(feat_name)) => {
317                                builder.full_name = Some(feat_name.into());
318                            }
319                            ("fallback-names", toml::Value::Array(values)) => {
320                                let key = format!("{}.{}.{}", p_key, name, version_feature);
321                                builder.fallback_names = Some(Self::parse_name_list(&key, values)?);
322                            }
323                            ("optional", &toml::Value::Boolean(optional)) => {
324                                builder.optional = Some(optional);
325                            }
326                            _ => {
327                                return Err(MetadataError::UnexpectedVersionSetting(
328                                    format!("{}.{}", p_key, name),
329                                    k.to_owned(),
330                                    v.type_str().to_owned(),
331                                ));
332                            }
333                        }
334                    }
335
336                    dep.version_overrides.push(builder.build()?);
337                }
338                _ => {
339                    return Err(MetadataError::UnexpectedKey(
340                        format!("{}.{}", p_key, name),
341                        key.to_owned(),
342                        value.type_str().to_owned(),
343                    ));
344                }
345            }
346        }
347        Ok(())
348    }
349
350    fn parse_name_list(key: &str, values: &[Value]) -> Result<Vec<String>, MetadataError> {
351        values
352            .iter()
353            .enumerate()
354            .map(|(i, value)| {
355                value
356                    .as_str()
357                    .map(|x| x.to_owned())
358                    .ok_or_else(|| MetadataError::NotString(format!("{}[{}]", key, i)))
359            })
360            .collect()
361    }
362}
363
364fn validate_version(version: &str) -> bool {
365    if let Some((min, max)) = version.split_once(',') {
366        if !min.trim_start().starts_with(">=") || !max.trim_start().starts_with('<') {
367            return false;
368        }
369
370        true
371    } else {
372        true
373    }
374}
375
376#[derive(Debug, Clone)]
377pub(crate) enum VersionRange<'a> {
378    Range(std::ops::Range<&'a str>),
379    RangeFrom(std::ops::RangeFrom<&'a str>),
380}
381
382impl<'a> std::ops::RangeBounds<&'a str> for VersionRange<'a> {
383    fn start_bound(&self) -> std::ops::Bound<&&'a str> {
384        match self {
385            VersionRange::Range(r) => r.start_bound(),
386            VersionRange::RangeFrom(r) => r.start_bound(),
387        }
388    }
389
390    fn end_bound(&self) -> std::ops::Bound<&&'a str> {
391        match self {
392            VersionRange::Range(r) => r.end_bound(),
393            VersionRange::RangeFrom(r) => r.end_bound(),
394        }
395    }
396}
397
398pub(crate) fn parse_version(version: &str) -> VersionRange {
399    if let Some((min, max)) = version.split_once(',') {
400        // Format checked when parsing
401        let min = min.trim_start().strip_prefix(">=").unwrap().trim();
402        let max = max.trim_start().strip_prefix('<').unwrap().trim();
403        VersionRange::Range(min..max)
404    } else if let Some(min) = version.trim_start().strip_prefix(">=") {
405        VersionRange::RangeFrom(min..)
406    } else {
407        VersionRange::RangeFrom(version..)
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use super::*;
414    use assert_matches::assert_matches;
415    use cfg_expr::Expression;
416    use std::{env, path::PathBuf};
417
418    fn parse_file(dir: &str) -> Result<MetaData, crate::Error> {
419        let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
420        let mut p: PathBuf = manifest_dir.into();
421        p.push("src");
422        p.push("tests");
423        p.push(dir);
424        p.push("Cargo.toml");
425        assert!(p.exists());
426
427        MetaData::from_file(&p)
428    }
429
430    #[test]
431    fn parse_good() {
432        let m = parse_file("toml-good").unwrap();
433
434        assert_eq!(
435            m,
436            MetaData {
437                deps: vec![
438                    Dependency {
439                        key: "testdata".into(),
440                        version: Some("4".into()),
441                        ..Default::default()
442                    },
443                    Dependency {
444                        key: "testlib".into(),
445                        version: Some("1".into()),
446                        feature: Some("test-feature".into()),
447                        ..Default::default()
448                    },
449                    Dependency {
450                        key: "testmore".into(),
451                        version: Some("2".into()),
452                        feature: Some("another-test-feature".into()),
453                        ..Default::default()
454                    }
455                ]
456            }
457        )
458    }
459
460    #[test]
461    fn parse_feature_not_string() {
462        assert_matches!(
463            parse_file("toml-feature-not-string"),
464            Err(crate::Error::InvalidMetadata(_))
465        );
466    }
467
468    #[test]
469    fn parse_override_name() {
470        let m = parse_file("toml-override-name").unwrap();
471
472        assert_eq!(
473            m,
474            MetaData {
475                deps: vec![Dependency {
476                    key: "test_lib".into(),
477                    version: Some("1.0".into()),
478                    name: Some("testlib".into()),
479                    version_overrides: vec![VersionOverride {
480                        key: "v1_2".into(),
481                        version: "1.2".into(),
482                        name: None,
483                        fallback_names: None,
484                        optional: None,
485                    }],
486                    ..Default::default()
487                },]
488            }
489        )
490    }
491
492    #[test]
493    fn parse_feature_versions() {
494        let m = parse_file("toml-feature-versions").unwrap();
495
496        assert_eq!(
497            m,
498            MetaData {
499                deps: vec![Dependency {
500                    key: "testdata".into(),
501                    version: Some("4".into()),
502                    version_overrides: vec![
503                        VersionOverride {
504                            key: "v5".into(),
505                            version: "5".into(),
506                            name: None,
507                            fallback_names: None,
508                            optional: None,
509                        },
510                        VersionOverride {
511                            key: "v6".into(),
512                            version: "6".into(),
513                            name: None,
514                            fallback_names: None,
515                            optional: None,
516                        },
517                    ],
518                    ..Default::default()
519                },]
520            }
521        )
522    }
523
524    #[test]
525    fn parse_fallback_names() {
526        let m = parse_file("toml-fallback-names").unwrap();
527
528        assert_eq!(
529            m,
530            MetaData {
531                deps: vec![Dependency {
532                    key: "test_lib".into(),
533                    version: Some("1.0".into()),
534                    name: Some("nosuchlib".into()),
535                    fallback_names: Some(vec![
536                        "also-no-such-lib".into(),
537                        "testlib".into(),
538                        "should-not-get-here".into(),
539                    ]),
540                    version_overrides: vec![],
541                    ..Default::default()
542                }]
543            }
544        )
545    }
546
547    #[test]
548    fn parse_version_fallback_names() {
549        let m = parse_file("toml-version-fallback-names").unwrap();
550
551        assert_eq!(
552            m,
553            MetaData {
554                deps: vec![Dependency {
555                    key: "test_lib".into(),
556                    version: Some("0.1".into()),
557                    name: Some("nosuchlib".into()),
558                    fallback_names: Some(vec![
559                        "also-no-such-lib".into(),
560                        "testlib".into(),
561                        "should-not-get-here".into(),
562                    ]),
563                    version_overrides: vec![
564                        VersionOverride {
565                            key: "v1".into(),
566                            version: "1.0".into(),
567                            name: None,
568                            fallback_names: None,
569                            optional: None,
570                        },
571                        VersionOverride {
572                            key: "v2".into(),
573                            version: "2.0".into(),
574                            name: None,
575                            fallback_names: Some(vec!["testlib-2.0".into()]),
576                            optional: None,
577                        },
578                        VersionOverride {
579                            key: "v99".into(),
580                            version: "99.0".into(),
581                            name: None,
582                            fallback_names: Some(vec![]),
583                            optional: None,
584                        },
585                    ],
586                    ..Default::default()
587                }]
588            }
589        )
590    }
591
592    #[test]
593    fn parse_optional() {
594        let m = parse_file("toml-optional").unwrap();
595
596        assert_eq!(
597            m,
598            MetaData {
599                deps: vec![
600                    Dependency {
601                        key: "testbadger".into(),
602                        version: Some("1".into()),
603                        optional: true,
604                        ..Default::default()
605                    },
606                    Dependency {
607                        key: "testlib".into(),
608                        version: Some("1.0".into()),
609                        optional: true,
610                        version_overrides: vec![VersionOverride {
611                            key: "v5".into(),
612                            version: "5.0".into(),
613                            name: Some("testlib-5.0".into()),
614                            fallback_names: None,
615                            optional: Some(false),
616                        },],
617                        ..Default::default()
618                    },
619                    Dependency {
620                        key: "testmore".into(),
621                        version: Some("2".into()),
622                        version_overrides: vec![VersionOverride {
623                            key: "v3".into(),
624                            version: "3.0".into(),
625                            name: None,
626                            fallback_names: None,
627                            optional: Some(true),
628                        },],
629                        ..Default::default()
630                    },
631                ]
632            }
633        )
634    }
635
636    #[test]
637    fn parse_os_specific() {
638        let m = parse_file("toml-os-specific").unwrap();
639
640        assert_eq!(
641            m,
642            MetaData {
643                deps: vec![
644                    Dependency {
645                        key: "testlib".into(),
646                        version: Some("1".into()),
647                        cfg: Some(Expression::parse("not(target_os = \"macos\")").unwrap()),
648                        ..Default::default()
649                    },
650                    Dependency {
651                        key: "testdata".into(),
652                        version: Some("1".into()),
653                        cfg: Some(Expression::parse("target_os = \"linux\"").unwrap()),
654                        ..Default::default()
655                    },
656                    Dependency {
657                        key: "testanotherlib".into(),
658                        version: Some("1".into()),
659                        cfg: Some(Expression::parse("unix").unwrap()),
660                        optional: true,
661                        ..Default::default()
662                    },
663                ]
664            }
665        )
666    }
667}