blob: 4d42b70f00c0e4d5a97b5da0df631b4d380954ca [file] [log] [blame]
// Copyright 2019 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::model::{component::ComponentInstance, resolver::Resolver},
::routing::resolving::{ComponentAddress, ResolvedComponent, ResolverError},
anyhow::format_err,
async_trait::async_trait,
cm_fidl_validator,
cm_rust::FidlIntoNative,
fidl::endpoints::{ClientEnd, Proxy},
fidl_fuchsia_component_decl as fdecl, fidl_fuchsia_component_resolution as fresolution,
fidl_fuchsia_io as fio,
fidl_fuchsia_sys::LoaderProxy,
fuchsia_url::AbsoluteComponentUrl,
std::convert::TryInto,
std::path::Path,
std::sync::Arc,
};
#[allow(unused)]
pub static SCHEME: &str = "fuchsia-pkg";
/// Resolves component URLs with the "fuchsia-pkg" scheme by proxying to an existing
/// fuchsia.sys.Loader service (which is the CFv1 equivalent of fuchsia.component.resolution.Resolver).
///
/// This resolver implementation is used to bridge the v1 and v2 component runtime worlds in
/// situations where the v2 runtime runs under the v1 runtime.
///
/// See the fuchsia_pkg_url crate for URL syntax.
#[derive(Debug)]
pub struct FuchsiaPkgResolver {
loader: LoaderProxy,
}
impl FuchsiaPkgResolver {
pub fn new(loader: LoaderProxy) -> FuchsiaPkgResolver {
FuchsiaPkgResolver { loader }
}
async fn resolve_async<'a>(
&'a self,
component_url: &'a str,
) -> Result<ResolvedComponent, ResolverError> {
// Parse URL.
let component_url =
AbsoluteComponentUrl::parse(component_url).map_err(ResolverError::malformed_url)?;
let cm_path = Path::new(component_url.resource());
let package_url = component_url.package_url().to_string();
// Resolve package.
let package = self
.loader
.load_url(&package_url)
.await
.map_err(|e| ResolverError::package_not_found(e))?
.ok_or(ResolverError::package_not_found(format_err!("package not available")))?;
let dir = package.directory.ok_or(ResolverError::package_not_found(format_err!(
"package is missing directory handle"
)))?;
// Read component manifest from package.
let dir = ClientEnd::<fio::DirectoryMarker>::new(dir)
.into_proxy()
.expect("failed to create directory proxy");
let file = fuchsia_fs::open_file(&dir, cm_path, fio::OpenFlags::RIGHT_READABLE)
.map_err(|e| ResolverError::manifest_not_found(e))?;
let component_decl =
fuchsia_fs::read_file_fidl(&file).await.map_err(|e| match e
.downcast_ref::<fuchsia_fs::file::ReadError>()
{
Some(_) => ResolverError::manifest_not_found(e),
None => ResolverError::manifest_invalid(e),
})?;
// Validate the component manifest
cm_fidl_validator::validate(&component_decl)
.map_err(|e| ResolverError::manifest_invalid(e))?;
// Get config values from the package if needed
let config_values = if let Some(config_decl) = &component_decl.config {
let strategy = config_decl.value_source.as_ref().ok_or_else(|| {
ResolverError::manifest_invalid(anyhow::format_err!(
"missing a strategy for resolving config values"
))
})?;
let config_path = match strategy {
fdecl::ConfigValueSource::PackagePath(path) => Path::new(path),
other => {
return Err(ResolverError::manifest_invalid(anyhow::format_err!(
"unrecognized config value strategy: {:?}",
other
)))
}
};
let file = fuchsia_fs::open_file(&dir, config_path, fio::OpenFlags::RIGHT_READABLE)
.map_err(|e| ResolverError::Io(e.into()))?;
let values_data =
fuchsia_fs::read_file_fidl(&file).await.map_err(|e| ResolverError::Io(e.into()))?;
cm_fidl_validator::validate_values_data(&values_data)
.map_err(|e| ResolverError::config_values_invalid(e))?;
Some(values_data.fidl_into_native())
} else {
None
};
let package_dir = ClientEnd::new(
dir.into_channel().expect("could not convert proxy to channel").into_zx_channel(),
);
let package = fresolution::Package {
url: Some(package_url),
directory: Some(package_dir),
..fresolution::Package::EMPTY
};
Ok(ResolvedComponent {
resolved_by: "FuchsiaPkgResolver".into(),
resolved_url: component_url.to_string(),
context_to_resolve_children: None,
decl: component_decl.fidl_into_native(),
package: Some(package.try_into()?),
config_values,
})
}
}
#[async_trait]
impl Resolver for FuchsiaPkgResolver {
async fn resolve(
&self,
component_address: &ComponentAddress,
_target: &Arc<ComponentInstance>,
) -> Result<ResolvedComponent, ResolverError> {
if component_address.is_relative_path() {
return Err(ResolverError::UnexpectedRelativePath(component_address.url().to_string()));
}
self.resolve_async(component_address.url()).await
}
}
#[cfg(test)]
mod tests {
use {
super::*,
::routing::resolving::ResolvedPackage,
cm_rust::FidlIntoNative,
fidl::encoding::encode_persistent_with_context,
fidl::endpoints::{self, ServerEnd},
fidl_fuchsia_component_config as fconfig, fidl_fuchsia_component_decl as fdecl,
fidl_fuchsia_data as fdata,
fidl_fuchsia_sys::{LoaderMarker, LoaderRequest, Package},
fuchsia_async as fasync, fuchsia_zircon as zx,
futures::TryStreamExt,
std::path::Path,
vfs::{
self, directory::entry::DirectoryEntry, execution_scope::ExecutionScope,
file::vmo::asynchronous::read_only_static, pseudo_directory,
},
};
struct MockLoader {}
impl MockLoader {
fn start() -> LoaderProxy {
let (proxy, server): (_, ServerEnd<LoaderMarker>) = endpoints::create_proxy().unwrap();
fasync::Task::local(async move {
let loader = MockLoader {};
let mut stream = server.into_stream().unwrap();
while let Some(LoaderRequest::LoadUrl { url, responder }) =
stream.try_next().await.expect("failed to read request")
{
let mut package = loader.load_url(&url);
let package = package.as_mut();
responder.send(package).expect("responder failed");
}
})
.detach();
proxy
}
// TODO(fxbug.dev/37534): This can be simplified to no longer need to use the test's real package
// directory once Rust vfs supports OPEN_RIGHT_EXECUTABLE.
fn load_url(&self, package_url: &str) -> Option<Package> {
let (dir_c, dir_s) = zx::Channel::create().unwrap();
let parsed_url = fuchsia_url::AbsolutePackageUrl::parse(&package_url).expect("bad url");
// Simulate a package server that only contains the "hello-world" package.
let invalid_cm_bytes = encode_persistent_with_context(
&fidl::encoding::Context {
wire_format_version: fidl::encoding::WireFormatVersion::V2,
},
&mut fdecl::Component {
program: Some(fdecl::Program {
runner: None,
info: Some(fdata::Dictionary {
entries: Some(vec![]),
..fdata::Dictionary::EMPTY
}),
..fdecl::Program::EMPTY
}),
uses: None,
exposes: None,
offers: None,
capabilities: None,
children: None,
collections: None,
environments: None,
facets: None,
..fdecl::Component::EMPTY
},
)
.unwrap();
let foo_cm_bytes = encode_persistent_with_context(
&fidl::encoding::Context {
wire_format_version: fidl::encoding::WireFormatVersion::V2,
},
&mut fdecl::Component {
config: Some(fdecl::ConfigSchema {
fields: Some(vec![fdecl::ConfigField {
key: Some("test".to_string()),
type_: Some(fdecl::ConfigType {
layout: fdecl::ConfigTypeLayout::Bool,
parameters: Some(vec![]),
constraints: vec![],
}),
..fdecl::ConfigField::EMPTY
}]),
checksum: Some(fdecl::ConfigChecksum::Sha256([0; 32])),
value_source: Some(fdecl::ConfigValueSource::PackagePath(
"config/foo.cvf".to_string(),
)),
..fdecl::ConfigSchema::EMPTY
}),
..fdecl::Component::EMPTY
},
)
.unwrap();
let foo_cvf_bytes = encode_persistent_with_context(
&fidl::encoding::Context {
wire_format_version: fidl::encoding::WireFormatVersion::V2,
},
&mut fconfig::ValuesData {
values: Some(vec![fconfig::ValueSpec {
value: Some(fconfig::Value::Single(fconfig::SingleValue::Bool(false))),
..fconfig::ValueSpec::EMPTY
}]),
checksum: Some(fdecl::ConfigChecksum::Sha256([0; 32])),
..fconfig::ValuesData::EMPTY
},
)
.unwrap();
match parsed_url.name().as_ref() {
"hello-world" => {
let path = Path::new("/pkg");
fuchsia_fs::connect_in_namespace(
path.to_str().unwrap(),
dir_s,
fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::RIGHT_EXECUTABLE,
)
.expect("could not connect to /pkg");
return Some(Package {
data: None,
directory: Some(dir_c),
resolved_url: package_url.to_string(),
});
}
"invalid-cm" => {
// Provide a cm that will fail due to a missing runner.
let sub_dir = pseudo_directory! {
"meta" => pseudo_directory! {
"invalid.cm" => read_only_static(invalid_cm_bytes),
}
};
sub_dir.open(
ExecutionScope::new(),
fio::OpenFlags::RIGHT_READABLE,
fio::MODE_TYPE_DIRECTORY,
vfs::path::Path::dot(),
ServerEnd::new(dir_s),
);
return Some(Package {
data: None,
directory: Some(dir_c),
resolved_url: package_url.to_string(),
});
}
"structured-config" => {
// Provide a cm that will fail due to a missing runner.
let sub_dir = pseudo_directory! {
"meta" => pseudo_directory! {
"foo.cm" => read_only_static(foo_cm_bytes),
},
"config" => pseudo_directory! {
"foo.cvf" => read_only_static(foo_cvf_bytes),
}
};
sub_dir.open(
ExecutionScope::new(),
fio::OpenFlags::RIGHT_READABLE,
fio::MODE_TYPE_DIRECTORY,
vfs::path::Path::dot(),
ServerEnd::new(dir_s),
);
return Some(Package {
data: None,
directory: Some(dir_c),
resolved_url: package_url.to_string(),
});
}
_ => return None,
}
}
}
#[fuchsia::test]
async fn resolve_test() {
let loader = MockLoader::start();
let resolver = FuchsiaPkgResolver::new(loader);
let url = "fuchsia-pkg://fuchsia.com/hello-world#meta/hello-world-rust.cm";
let component = resolver.resolve_async(url).await.expect("resolve failed");
// Check that both the returned component manifest and the component manifest in
// the returned package dir match the expected value. This also tests that
// the resolver returned the right package dir.
let ResolvedComponent {
resolved_by,
resolved_url,
context_to_resolve_children,
decl,
package,
config_values,
} = component;
assert_eq!(resolved_by, "FuchsiaPkgResolver");
assert_eq!(resolved_url, url);
assert_eq!(context_to_resolve_children, None);
assert!(config_values.is_none());
let expected_program = Some(cm_rust::ProgramDecl {
runner: Some("elf".into()),
info: fdata::Dictionary {
entries: Some(vec![
fdata::DictionaryEntry {
key: "binary".to_string(),
value: Some(Box::new(fdata::DictionaryValue::Str(
"bin/hello_world_rust".to_string(),
))),
},
fdata::DictionaryEntry {
key: "forward_stderr_to".to_string(),
value: Some(Box::new(fdata::DictionaryValue::Str("log".to_string()))),
},
fdata::DictionaryEntry {
key: "forward_stdout_to".to_string(),
value: Some(Box::new(fdata::DictionaryValue::Str("log".to_string()))),
},
]),
..fdata::Dictionary::EMPTY
},
});
// no need to check full decl as we just want to make
// sure that we were able to resolve.
assert_eq!(decl.program, expected_program);
let ResolvedPackage { url: package_url, directory: package_dir, .. } = package.unwrap();
assert_eq!(package_url, "fuchsia-pkg://fuchsia.com/hello-world");
let dir_proxy = package_dir.into_proxy().unwrap();
let path = Path::new("meta/hello-world-rust.cm");
let file_proxy = fuchsia_fs::open_file(&dir_proxy, path, fio::OpenFlags::RIGHT_READABLE)
.expect("could not open cm");
let decl = fuchsia_fs::read_file_fidl::<fdecl::Component>(&file_proxy)
.await
.expect("could not read cm");
let decl = decl.fidl_into_native();
assert_eq!(decl.program, expected_program);
// Try to load an executable file, like a binary, reusing the library_loader helper that
// opens with OPEN_RIGHT_EXECUTABLE and gets a VMO with VMO_FLAG_EXEC.
library_loader::load_vmo(&dir_proxy, "bin/hello_world_rust")
.await
.expect("failed to open executable file");
}
#[fuchsia::test]
async fn structured_config() {
let loader = MockLoader::start();
let resolver = FuchsiaPkgResolver::new(loader);
let url = "fuchsia-pkg://fuchsia.com/structured-config#meta/foo.cm";
let component = resolver.resolve_async(url).await.expect("resolve failed");
let ResolvedComponent { decl, config_values, .. } = component;
let expected_config = Some(cm_rust::ConfigDecl {
fields: vec![cm_rust::ConfigField {
key: "test".to_string(),
type_: cm_rust::ConfigValueType::Bool,
}],
checksum: cm_rust::ConfigChecksum::Sha256([0; 32]),
value_source: cm_rust::ConfigValueSource::PackagePath("config/foo.cvf".to_string()),
});
assert_eq!(decl.config, expected_config);
let expected_values = Some(cm_rust::ValuesData {
values: vec![cm_rust::ValueSpec {
value: cm_rust::Value::Single(cm_rust::SingleValue::Bool(false)),
}],
checksum: cm_rust::ConfigChecksum::Sha256([0; 32]),
});
assert_eq!(config_values, expected_values);
}
macro_rules! test_resolve_error {
($resolver:ident, $url:expr, $resolver_error_expected:pat) => {
let res = $resolver.resolve_async($url).await;
match res.err().expect("unexpected success") {
$resolver_error_expected => {}
e => panic!("unexpected error {:?}", e),
}
};
}
#[fuchsia::test]
async fn resolve_errors_test() {
let loader = MockLoader::start();
let resolver = FuchsiaPkgResolver::new(loader);
test_resolve_error!(
resolver,
"fuchsia-pkg:///hello-world#meta/hello-world-rust.cm",
ResolverError::MalformedUrl { .. }
);
test_resolve_error!(
resolver,
"fuchsia-pkg://fuchsia.com/hello-world",
ResolverError::MalformedUrl { .. }
);
test_resolve_error!(
resolver,
"fuchsia-pkg://fuchsia.com/goodbye-world#meta/hello-world-rust.cm",
ResolverError::PackageNotFound { .. }
);
test_resolve_error!(
resolver,
"fuchsia-pkg://fuchsia.com/hello-world#meta/does_not_exist.cm",
ResolverError::ManifestNotFound { .. }
);
test_resolve_error!(
resolver,
"fuchsia-pkg://fuchsia.com/hello-world#meta/component_manager_tests.invalid_cm",
ResolverError::ManifestInvalid { .. }
);
test_resolve_error!(
resolver,
"fuchsia-pkg://fuchsia.com/invalid-cm#meta/invalid.cm",
ResolverError::ManifestInvalid { .. }
);
}
}