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: &String) => write!(f, "missing key `{}`", k),
85 Self::NotATable(k: &String) => write!(f, "`{}` is not a table", k),
86 Self::NestedCfg(k: &String) => write!(f, "`{}`: cfg() cannot be nested", k),
87 Self::NotString(k: &String) => write!(f, "`{}`: not a string", k),
88 Self::NotStringOrTable(k: &String) => write!(f, "`{}`: not a string or a table", k),
89 Self::CfgExpr(e: &ParseError) => write!(f, "{}", e),
90 Self::Toml(e: &Error) => write!(f, "error parsing TOML: {}", e),
91 Self::UnexpectedVersionSetting(n: &String, k: &String, t: &String) => {
92 write!(
93 f,
94 "{}: unexpected version settings key: {} type: {}",
95 n, k, t
96 )
97 }
98 Self::UnexpectedKey(n: &String, k: &String, t: &String) => write!(f, "{}: unexpected key {} type {}", n, k, t),
99 Self::VersionOverrideBuilder(e: &VersionOverrideBuilderError) => 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: &ParseError) => Some(e),
108 Self::Toml(e: &Error) => Some(e),
109 Self::VersionOverrideBuilder(e: &VersionOverrideBuilderError) => 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: &str, max: &str)) = version.split_once(delimiter:',') {
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: &Range<&str>) => r.start_bound(),
386 VersionRange::RangeFrom(r: &RangeFrom<&str>) => r.start_bound(),
387 }
388 }
389
390 fn end_bound(&self) -> std::ops::Bound<&&'a str> {
391 match self {
392 VersionRange::Range(r: &Range<&str>) => r.end_bound(),
393 VersionRange::RangeFrom(r: &RangeFrom<&str>) => r.end_bound(),
394 }
395 }
396}
397
398pub(crate) fn parse_version(version: &str) -> VersionRange {
399 if let Some((min: &str, max: &str)) = version.split_once(delimiter:',') {
400 // Format checked when parsing
401 let min: &str = min.trim_start().strip_prefix(">=").unwrap().trim();
402 let max: &str = max.trim_start().strip_prefix('<').unwrap().trim();
403 VersionRange::Range(min..max)
404 } else if let Some(min: &str) = 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}
668