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