1 | // Parse system-deps metadata from Cargo.toml |
2 | |
3 | use std::{fmt, fs, io::Read, path::Path}; |
4 | |
5 | use toml::{map::Map, Value}; |
6 | |
7 | #[derive (Debug, PartialEq)] |
8 | pub(crate) struct MetaData { |
9 | pub(crate) deps: Vec<Dependency>, |
10 | } |
11 | |
12 | #[derive (Debug, Clone, PartialEq)] |
13 | pub(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 | |
24 | impl 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 | |
37 | impl 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)] |
53 | enum VersionOverrideBuilderError { |
54 | MissingVersionField, |
55 | } |
56 | |
57 | impl 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 | |
65 | impl std::error::Error for VersionOverrideBuilderError {} |
66 | |
67 | #[derive (Debug, PartialEq)] |
68 | enum 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 | |
81 | impl 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 | |
104 | impl 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 | |
115 | impl From<cfg_expr::ParseError> for MetadataError { |
116 | fn from(err: cfg_expr::ParseError) -> Self { |
117 | Self::CfgExpr(err) |
118 | } |
119 | } |
120 | |
121 | impl From<toml::de::Error> for MetadataError { |
122 | fn from(err: toml::de::Error) -> Self { |
123 | Self::Toml(err) |
124 | } |
125 | } |
126 | |
127 | impl From<VersionOverrideBuilderError> for MetadataError { |
128 | fn from(err: VersionOverrideBuilderError) -> Self { |
129 | Self::VersionOverrideBuilder(err) |
130 | } |
131 | } |
132 | |
133 | #[derive (Debug, Clone, PartialEq)] |
134 | pub(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 | |
142 | struct 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 | |
150 | impl 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 | |
176 | impl 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 | |
364 | fn 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)] |
377 | pub(crate) enum VersionRange<'a> { |
378 | Range(std::ops::Range<&'a str>), |
379 | RangeFrom(std::ops::RangeFrom<&'a str>), |
380 | } |
381 | |
382 | impl<'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 | |
398 | pub(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)] |
412 | mod 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 | |