| // Copyright 2021 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use { |
| crate::{error::Error, util}, |
| serde_json, |
| std::{fs, path::PathBuf}, |
| }; |
| |
| enum ComponentManifest { |
| Cml(cml::Document), |
| Cmx(serde_json::Value), |
| } |
| |
| // These runners respect `program.binary`. We only validate the `program` section for components |
| // using them. |
| // TODO(fxbug.dev/67151) |
| const RUNNER_VALIDATE_LIST: [&'static str; 6] = [ |
| "driver", |
| "elf", |
| "elf_test_runner", |
| "go_test_runner", |
| "gtest_runner", |
| "rust_test_runner", |
| // TODO(fxbug.dev/68608): Add support for Dart components. |
| ]; |
| |
| /// Validates that a component manifest file is consistent with the content |
| /// of its package. Checks included in this function are: |
| /// 1. If provided program binary in component manifest matches with |
| /// executable target declared in package manifest. |
| /// If all checks pass, this function returns Ok(()). |
| pub fn validate( |
| component_manifest_path: &PathBuf, |
| package_manifest_path: &PathBuf, |
| gn_label: Option<&String>, |
| ) -> Result<(), Error> { |
| let component_manifest = read_component_manifest(component_manifest_path)?; |
| match get_component_runner(&component_manifest).as_deref() { |
| Some(runner) => { |
| if !RUNNER_VALIDATE_LIST.iter().any(|r| &runner == r) { |
| return Ok(()); |
| } |
| } |
| None => {} |
| } |
| |
| let package_manifest = read_package_manifest(package_manifest_path)?; |
| let package_targets = get_package_targets(&package_manifest); |
| let program_binary = get_program_binary(&component_manifest); |
| |
| if program_binary.is_none() { |
| return Ok(()); |
| } |
| |
| let program_binary = program_binary.unwrap(); |
| |
| if package_targets.contains(&program_binary) { |
| return Ok(()); |
| } |
| |
| // Legacy package.gni supports the "disabled test" feature that |
| // intentionally breaks component manifests. |
| if program_binary.starts_with("test/") |
| && package_targets.contains(&format!("test/disabled/{}", &program_binary[5..])) |
| { |
| return Ok(()); |
| } |
| |
| Err(missing_binary_error(component_manifest_path, program_binary, package_targets, gn_label)) |
| } |
| |
| fn read_package_manifest(path: &PathBuf) -> Result<String, Error> { |
| fs::read_to_string(path).map_err(|e| { |
| Error::parse(format!("Couldn't parse file {:?}: {}", path, e), None, Some(path)) |
| }) |
| } |
| |
| fn read_component_manifest(path: &PathBuf) -> Result<ComponentManifest, Error> { |
| const BAD_EXTENSION: &str = "Input file does not have a component manifest extension \ |
| (.cml or .cmx)"; |
| let ext = path.extension().and_then(|e| e.to_str()); |
| Ok(match ext { |
| Some("cmx") => ComponentManifest::Cmx(util::read_cmx(path)?), |
| Some("cml") => ComponentManifest::Cml(util::read_cml(path)?), |
| _ => { |
| return Err(Error::invalid_args(BAD_EXTENSION)); |
| } |
| }) |
| } |
| |
| fn get_package_targets(package_manifest: &str) -> Vec<String> { |
| package_manifest |
| .lines() |
| .map(|line| line.split("=")) |
| .map(|mut parts| parts.next()) |
| .filter(Option::is_some) |
| // Safe to unwrap because Option(None) values have been filtered out. |
| .map(Option::unwrap) |
| .map(str::to_owned) |
| .collect() |
| } |
| |
| fn get_program_binary(component_manifest: &ComponentManifest) -> Option<String> { |
| let program = match component_manifest { |
| ComponentManifest::Cml(document) => match &document.program { |
| Some(program) => &program.info, |
| None => { |
| return None; |
| } |
| }, |
| ComponentManifest::Cmx(body) => match body.get("program") { |
| Some(program) => match program.as_object() { |
| Some(program) => program, |
| None => { |
| return None; |
| } |
| }, |
| None => { |
| return None; |
| } |
| }, |
| }; |
| |
| match program.get("binary") { |
| Some(binary) => match binary.as_str() { |
| Some(value) => Some(value.to_owned()), |
| None => None, |
| }, |
| None => None, |
| } |
| } |
| |
| fn get_component_runner(component_manifest: &ComponentManifest) -> Option<String> { |
| match component_manifest { |
| ComponentManifest::Cml(document) => { |
| document.program.as_ref().and_then(|p| p.runner.as_ref().map(|s| s.as_str().to_owned())) |
| } |
| ComponentManifest::Cmx(payload) => { |
| payload.get("runner").and_then(|r| r.as_str().map(|s| s.to_owned())) |
| } |
| } |
| } |
| |
| fn missing_binary_error( |
| component_manifest_path: &PathBuf, |
| program_binary: String, |
| package_targets: Vec<String>, |
| gn_label: Option<&String>, |
| ) -> Error { |
| let header = gen_header(component_manifest_path, gn_label); |
| if package_targets.is_empty() { |
| return Error::validate(format!("{}\n\tPackage deps is empty!", header)); |
| } |
| |
| // We couldn't find the binary, let's get the nearest match. |
| let nearest_match = get_nearest_match(&program_binary, &package_targets); |
| |
| Error::validate(format!( |
| r"{} |
| program.binary={} but {} is not provided by deps! |
| |
| Did you mean {}? |
| |
| Try any of the following: |
| {}", |
| header, |
| program_binary, |
| program_binary, |
| nearest_match, |
| package_targets.join("\n") |
| )) |
| } |
| |
| fn gen_header(component_manifest_path: &PathBuf, gn_label: Option<&String>) -> String { |
| match gn_label { |
| Some(label) => format!( |
| "Error found in: {}\n\tFailed to validate manifest: {:#?}", |
| label, component_manifest_path |
| ), |
| None => format!("Failed to validate manifest: {:#?}", component_manifest_path), |
| } |
| } |
| |
| fn get_nearest_match<'a>(reference: &'a str, candidates: &'a Vec<String>) -> &'a str { |
| let mut nearest = &candidates[0]; |
| let mut min_distance = strsim::levenshtein(reference, &candidates[0]); |
| for candidate in candidates.iter().skip(1) { |
| let distance = strsim::levenshtein(reference, candidate); |
| if distance < min_distance { |
| min_distance = distance; |
| nearest = candidate; |
| } |
| } |
| &nearest |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use { |
| super::*, |
| matches::assert_matches, |
| serde_json::json, |
| std::{fmt::Display, fs::File, io::Write}, |
| tempfile::TempDir, |
| }; |
| |
| fn tmp_file(tmp_dir: &TempDir, name: &str, contents: impl Display) -> PathBuf { |
| let path = tmp_dir.path().join(name); |
| File::create(tmp_dir.path().join(name)) |
| .unwrap() |
| .write_all(format!("{:#}", contents).as_bytes()) |
| .unwrap(); |
| return path; |
| } |
| |
| macro_rules! fini_file { |
| ( $( $line:literal ),* ) => { |
| { |
| let mut buf: Vec<&str> = Vec::new(); |
| $( |
| buf.push($line); |
| )* |
| buf.join("\n") |
| } |
| }; |
| } |
| |
| #[test] |
| fn validate_returns_ok_if_program_binary_empty() { |
| let tmp_dir = TempDir::new().unwrap(); |
| let component_manifest = tmp_file(&tmp_dir, "test.cmx", json!({})); |
| let package_manifest = tmp_file( |
| &tmp_dir, |
| "test.fini", |
| fini_file!("bin/hello_world=hello_world", "lib/foo=foo"), |
| ); |
| |
| assert_matches!(validate(&component_manifest, &package_manifest, None), Ok(())); |
| } |
| |
| #[test] |
| fn validate_returns_ok_for_proper_cmx() { |
| let tmp_dir = TempDir::new().unwrap(); |
| let component_manifest = tmp_file( |
| &tmp_dir, |
| "test.cmx", |
| json!({ |
| "program": { |
| "binary": "bin/hello_world" |
| }, |
| "runner": "elf" |
| }), |
| ); |
| let package_manifest = tmp_file( |
| &tmp_dir, |
| "test.fini", |
| fini_file!("bin/hello_world=hello_world", "lib/foo=foo"), |
| ); |
| |
| assert_matches!(validate(&component_manifest, &package_manifest, None), Ok(())); |
| } |
| |
| #[test] |
| fn validate_returns_ok_for_proper_cml() { |
| let tmp_dir = TempDir::new().unwrap(); |
| let component_manifest = tmp_file( |
| &tmp_dir, |
| "test.cml", |
| r#"{ |
| // JSON5, which .cml uses, allows for comments. |
| program: { |
| runner: "elf", |
| binary: "bin/hello_world", |
| }, |
| }"#, |
| ); |
| let package_manifest = tmp_file( |
| &tmp_dir, |
| "test.fini", |
| fini_file!("bin/hello_world=hello_world", "lib/foo=foo"), |
| ); |
| |
| assert_matches!(validate(&component_manifest, &package_manifest, None), Ok(())); |
| } |
| |
| #[test] |
| fn validate_returns_ok_for_test_binaries() { |
| let tmp_dir = TempDir::new().unwrap(); |
| let component_manifest = tmp_file( |
| &tmp_dir, |
| "test.cmx", |
| json!({ |
| "program": { |
| "binary": "test/hello_world" |
| }, |
| "runner": "elf" |
| }), |
| ); |
| let package_manifest = tmp_file( |
| &tmp_dir, |
| "test.fini", |
| fini_file!("test/hello_world=hello_world", "lib/foo=foo"), |
| ); |
| |
| assert_matches!(validate(&component_manifest, &package_manifest, None), Ok(())); |
| } |
| |
| #[test] |
| fn validate_returns_ok_for_disabled_test_binaries() { |
| let tmp_dir = TempDir::new().unwrap(); |
| let component_manifest = tmp_file( |
| &tmp_dir, |
| "test.cmx", |
| json!({ |
| "program": { |
| "binary": "test/hello_world" |
| }, |
| "runner": "elf" |
| }), |
| ); |
| let package_manifest = tmp_file( |
| &tmp_dir, |
| "test.fini", |
| fini_file!("test/disabled/hello_world=hello_world", "lib/foo=foo"), |
| ); |
| |
| assert_matches!(validate(&component_manifest, &package_manifest, None), Ok(())); |
| } |
| |
| #[test] |
| fn validate_returns_ok_for_ignored_runners() { |
| let tmp_dir = TempDir::new().unwrap(); |
| let package_manifest = tmp_file(&tmp_dir, "test.fini", ""); |
| |
| // Normally, an empty package manifest file would yield an error. |
| // However, the following runners are ignored during validation. |
| for runner in [ |
| "fuchsia-pkg://fuchsia.com/guest-runner#meta/guest_runner.cmx", |
| "fuchsia-pkg://fuchsia.com/appmgr_mock_runner#meta/appmgr_mock_runner.cmx", |
| "fuchsia-pkg://fuchsia.com/netemul-runner#meta/netemul-runner.cmx", |
| "fuchsia-pkg://fuchsia.com/dart_jit_runner#meta/dart_jit_runner.cmx", |
| ] |
| .iter() |
| { |
| let component_manifest = tmp_file( |
| &tmp_dir, |
| "test.cmx", |
| json!({ |
| "program": { |
| "binary": "bin/hello_world" |
| }, |
| "runner": runner |
| }), |
| ); |
| |
| assert_matches!(validate(&component_manifest, &package_manifest, None), Ok(())); |
| } |
| } |
| |
| #[test] |
| fn validate_returns_ok_if_runner_not_provided() { |
| let tmp_dir = TempDir::new().unwrap(); |
| let package_manifest = tmp_file(&tmp_dir, "test.fini", fini_file!("bin/hello_world")); |
| |
| let component_manifest = tmp_file( |
| &tmp_dir, |
| "test.cmx", |
| json!({ |
| "program": { |
| "binary": "bin/hello_world" |
| } |
| }), |
| ); |
| |
| assert_matches!(validate(&component_manifest, &package_manifest, None), Ok(())); |
| } |
| |
| #[test] |
| fn validate_returns_err_if_runner_not_provided_and_binary_not_found() { |
| let tmp_dir = TempDir::new().unwrap(); |
| let package_manifest = tmp_file(&tmp_dir, "test.fini", fini_file!("bin/hello_world")); |
| |
| let component_manifest = tmp_file( |
| &tmp_dir, |
| "test.cmx", |
| json!({ |
| "program": { |
| "binary": "test/hello_world" |
| } |
| }), |
| ); |
| |
| assert_matches!(validate(&component_manifest, &package_manifest, None), |
| Err(Error::Validate { schema_name: None, err, .. }) if err.contains("test/hello_world is not provided by deps!") && err.contains("Did you mean bin/hello_world?")); |
| } |
| |
| #[test] |
| fn validate_returns_err_if_package_manifest_is_empty() { |
| let tmp_dir = TempDir::new().unwrap(); |
| let component_manifest = tmp_file( |
| &tmp_dir, |
| "test.cmx", |
| json!({ |
| "program": { |
| "binary": "bin/hello_world" |
| }, |
| "runner": "elf" |
| }), |
| ); |
| let package_manifest = tmp_file(&tmp_dir, "test.fini", ""); |
| |
| assert_matches!(validate(&component_manifest, &package_manifest, None), Err(_)); |
| } |
| |
| #[test] |
| fn validate_returns_err_if_binary_not_found() { |
| let tmp_dir = TempDir::new().unwrap(); |
| let component_manifest = tmp_file( |
| &tmp_dir, |
| "test.cmx", |
| json!({ |
| "program": { |
| "binary": "bin/not_a_listed_binary" |
| }, |
| "runner": "elf" |
| }), |
| ); |
| let package_manifest = tmp_file( |
| &tmp_dir, |
| "test.fini", |
| fini_file!("bin/hello_world=hello_world", "lib/foo=foo"), |
| ); |
| |
| assert_matches!(validate(&component_manifest, &package_manifest, None), Err(_)); |
| } |
| |
| #[test] |
| fn validate_returns_err_if_component_manifest_has_unknown_extension() { |
| let tmp_dir = TempDir::new().unwrap(); |
| let component_manifest = tmp_file( |
| &tmp_dir, |
| "test.txt", |
| r#"{ |
| // JSON5, which .cml uses, allows for comments. |
| program: { |
| binary: "bin/hello_world", |
| }, |
| }"#, |
| ); |
| let package_manifest = tmp_file( |
| &tmp_dir, |
| "test.fini", |
| fini_file!("bin/hello_world=hello_world", "lib/foo=foo"), |
| ); |
| |
| assert_matches!(validate(&component_manifest, &package_manifest, None), Err(_)); |
| } |
| |
| #[test] |
| fn validate_returns_err_if_cml_is_malformed() { |
| let tmp_dir = TempDir::new().unwrap(); |
| let component_manifest = tmp_file( |
| &tmp_dir, |
| "test.cml", |
| r#" |
| // With no opening '{', the json5 parser should yield an error. |
| program: { |
| binary: "bin/hello_world", |
| } |
| "#, |
| ); |
| let package_manifest = tmp_file( |
| &tmp_dir, |
| "test.fini", |
| fini_file!("bin/hello_world=hello_world", "lib/foo=foo"), |
| ); |
| |
| assert_matches!(validate(&component_manifest, &package_manifest, None), Err(_)); |
| } |
| |
| #[test] |
| fn validate_returns_err_if_cmx_is_malformed() { |
| let tmp_dir = TempDir::new().unwrap(); |
| let component_manifest = tmp_file( |
| &tmp_dir, |
| "test.cmx", |
| // json parser should break when encountering unquoted keys. |
| r#"{ |
| program: { |
| binary: "bin/hello_world" |
| } |
| runner: "elf", |
| }"#, |
| ); |
| let package_manifest = tmp_file( |
| &tmp_dir, |
| "test.fini", |
| fini_file!("bin/hello_world=hello_world", "lib/foo=foo"), |
| ); |
| |
| assert_matches!(validate(&component_manifest, &package_manifest, None), Err(_)); |
| } |
| |
| #[test] |
| fn validate_returns_err_if_package_manifest_is_bad_file() { |
| let tmp_dir = TempDir::new().unwrap(); |
| let component_manifest = tmp_file( |
| &tmp_dir, |
| "test.cml", |
| r#"{ |
| // JSON5, which .cml uses, allows for comments. |
| program: { |
| binary: "bin/hello_world", |
| }, |
| use: [{ runner: "elf", }], |
| }"#, |
| ); |
| |
| assert_matches!( |
| validate(&component_manifest, &PathBuf::from("file/doesnt/exist"), None), |
| Err(_) |
| ); |
| } |
| |
| #[test] |
| fn get_nearest_match_returns_correct_value() { |
| assert_eq!( |
| get_nearest_match("foo", &vec!["lib/bar".to_string(), "bin/foo".to_string()]), |
| "bin/foo" |
| ); |
| assert_eq!( |
| get_nearest_match("foo", &vec!["bin/foo".to_string(), "lib/bar".to_string()]), |
| "bin/foo" |
| ); |
| } |
| } |