blob: ea11b373c93f33879e11bba8eaf442e24180a5eb [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::resolver::{Resolver, ResolverError, ResolverFut},
anyhow::format_err,
cm_fidl_validator,
fidl::endpoints::{ClientEnd, Proxy},
fidl_fuchsia_io::{self as fio, DirectoryMarker},
fidl_fuchsia_sys::LoaderProxy,
fidl_fuchsia_sys2 as fsys,
fuchsia_url::pkg_url::PkgUrl,
std::path::Path,
};
#[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.sys2.ComponentResolver).
///
/// 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.
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<fsys::Component, ResolverError> {
// Parse URL.
let fuchsia_pkg_url = PkgUrl::parse(component_url)
.map_err(|e| ResolverError::url_parse_error(component_url, e))?;
let cm_path = Path::new(
fuchsia_pkg_url
.resource()
.ok_or(ResolverError::url_missing_resource_error(component_url))?,
);
let package_url = fuchsia_pkg_url.root_url().to_string();
// Resolve package.
let package = self
.loader
.load_url(&package_url)
.await
.map_err(|e| ResolverError::component_not_available(component_url, e))?
.ok_or(ResolverError::component_not_available(
component_url,
format_err!("package not available"),
))?;
let dir = package.directory.ok_or(ResolverError::component_not_available(
component_url,
format_err!("package is missing directory handle"),
))?;
// Read component manifest from package.
let dir = ClientEnd::<DirectoryMarker>::new(dir)
.into_proxy()
.expect("failed to create directory proxy");
let file = io_util::open_file(&dir, cm_path, fio::OPEN_RIGHT_READABLE)
.map_err(|e| ResolverError::manifest_not_available(component_url, e))?;
let component_decl = io_util::read_file_fidl(&file).await.map_err(|e| {
match e.downcast_ref::<io_util::file::ReadError>() {
Some(_) => ResolverError::manifest_not_available(component_url, e),
None => ResolverError::manifest_invalid(component_url, e),
}
})?;
// Validate the component manifest
cm_fidl_validator::validate(&component_decl)
.map_err(|e| ResolverError::manifest_invalid(component_url, e))?;
let package_dir = ClientEnd::new(
dir.into_channel().expect("could not convert proxy to channel").into_zx_channel(),
);
let package = fsys::Package {
package_url: Some(package_url),
package_dir: Some(package_dir),
..fsys::Package::EMPTY
};
Ok(fsys::Component {
resolved_url: Some(component_url.to_string()),
decl: Some(component_decl),
package: Some(package),
..fsys::Component::EMPTY
})
}
}
impl Resolver for FuchsiaPkgResolver {
fn resolve<'a>(&'a self, component_url: &'a str) -> ResolverFut {
Box::pin(self.resolve_async(component_url))
}
}
#[cfg(test)]
mod tests {
use {
super::*,
fidl::encoding::encode_persistent,
fidl::endpoints::{self, ServerEnd},
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::pcb::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 = PkgUrl::parse(&package_url).expect("bad url");
// Simulate a package server that only contains the "hello-world" package.
match parsed_url.name() {
"hello-world" => {
let path = Path::new("/pkg");
io_util::connect_in_namespace(
path.to_str().unwrap(),
dir_s,
fio::OPEN_RIGHT_READABLE | fio::OPEN_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 multiple runners being configured.
let sub_dir = pseudo_directory! {
"meta" => pseudo_directory! {
"invalid.cm" => read_only_static(
encode_persistent(&mut fsys::ComponentDecl {
program: None,
uses: Some(vec![
fsys::UseDecl::Runner(
fsys::UseRunnerDecl {
source_name: Some("elf".to_string()),
..fsys::UseRunnerDecl::EMPTY
}
),
fsys::UseDecl::Runner (
fsys::UseRunnerDecl {
source_name: Some("web".to_string()),
..fsys::UseRunnerDecl::EMPTY
}
)
]),
exposes: None,
offers: None,
capabilities: None,
children: None,
collections: None,
environments: None,
facets: None,
..fsys::ComponentDecl::EMPTY
}).unwrap()
),
}
};
sub_dir.open(
ExecutionScope::new(),
fio::OPEN_RIGHT_READABLE,
fio::MODE_TYPE_DIRECTORY,
vfs::path::Path::empty(),
ServerEnd::new(dir_s),
);
return Some(Package {
data: None,
directory: Some(dir_c),
resolved_url: package_url.to_string(),
});
}
_ => return None,
}
}
}
#[fuchsia_async::run_singlethreaded(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.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 fsys::Component { resolved_url, decl, package, .. } = component;
assert_eq!(resolved_url.unwrap(), url);
let program = fdata::Dictionary {
entries: Some(vec![fdata::DictionaryEntry {
key: "binary".to_string(),
value: Some(Box::new(fdata::DictionaryValue::Str("bin/hello_world".to_string()))),
}]),
..fdata::Dictionary::EMPTY
};
let expected_decl = fsys::ComponentDecl {
program: Some(program),
uses: Some(vec![
fsys::UseDecl::Runner(fsys::UseRunnerDecl {
source_name: Some("elf".to_string()),
..fsys::UseRunnerDecl::EMPTY
}),
fsys::UseDecl::Protocol(fsys::UseProtocolDecl {
source: Some(fsys::Ref::Parent(fsys::ParentRef {})),
source_name: Some("fuchsia.logger.LogSink".to_string()),
target_path: Some("/svc/fuchsia.logger.LogSink".to_string()),
..fsys::UseProtocolDecl::EMPTY
}),
]),
exposes: None,
offers: None,
facets: None,
capabilities: None,
children: None,
collections: None,
environments: None,
..fsys::ComponentDecl::EMPTY
};
assert_eq!(decl.unwrap(), expected_decl);
let fsys::Package { package_url, package_dir, .. } = package.unwrap();
assert_eq!(package_url.unwrap(), "fuchsia-pkg://fuchsia.com/hello-world");
let dir_proxy = package_dir.unwrap().into_proxy().unwrap();
let path = Path::new("meta/hello-world.cm");
let file_proxy = io_util::open_file(&dir_proxy, path, fio::OPEN_RIGHT_READABLE)
.expect("could not open cm");
assert_eq!(
io_util::read_file_fidl::<fsys::ComponentDecl>(&file_proxy)
.await
.expect("could not read cm"),
expected_decl
);
// 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")
.await
.expect("failed to open executable file");
}
macro_rules! test_resolve_error {
($resolver:ident, $url:expr, $resolver_error_expected:ident) => {
let url = $url;
let res = $resolver.resolve_async(url).await;
match res.err().expect("unexpected success") {
ResolverError::$resolver_error_expected { url: u, .. } => {
assert_eq!(u, url);
}
e => panic!("unexpected error {:?}", e),
}
};
}
#[fuchsia_async::run_singlethreaded(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.cm",
UrlParseError
);
test_resolve_error!(
resolver,
"fuchsia-pkg://fuchsia.com/hello-world",
UrlMissingResourceError
);
test_resolve_error!(
resolver,
"fuchsia-pkg://fuchsia.com/goodbye-world#meta/hello-world.cm",
ComponentNotAvailable
);
test_resolve_error!(
resolver,
"fuchsia-pkg://fuchsia.com/hello-world#meta/does_not_exist.cm",
ManifestNotAvailable
);
test_resolve_error!(
resolver,
"fuchsia-pkg://fuchsia.com/hello-world#meta/component_manager_tests_invalid.cm",
ManifestInvalid
);
test_resolve_error!(
resolver,
"fuchsia-pkg://fuchsia.com/invalid-cm#meta/invalid.cm",
ManifestInvalid
);
}
}