blob: 8af8228825cd8898e14d28c925fe0b9a5c667c53 [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::Error,
cm_fidl_validator,
fidl::endpoints::{ClientEnd, Proxy},
fidl_fuchsia_io::{self as fio, DirectoryProxy},
fidl_fuchsia_sys2 as fsys,
fuchsia_url::boot_url::BootUrl,
std::path::{Path, PathBuf},
};
pub static SCHEME: &str = "fuchsia-boot";
/// Resolves component URLs with the "fuchsia-boot" scheme, which supports loading components from
/// the /boot directory in component_manager's namespace.
///
/// On a typical system, this /boot directory is the bootfs served from the contents of the
/// 'ZBI_TYPE_STORAGE_BOOTFS' ZBI item by bootsvc, the process which starts component_manager.
///
/// URL syntax:
/// - fuchsia-boot:///path/within/bootfs#meta/component.cm
pub struct FuchsiaBootResolver {
boot_proxy: DirectoryProxy,
}
impl FuchsiaBootResolver {
/// Create a new FuchsiaBootResolver. This first checks whether a /boot directory is present in
/// the namespace, and returns Ok(None) if not present. This is generally the case in unit and
/// integration tests where this resolver is unused.
pub fn new() -> Result<Option<FuchsiaBootResolver>, Error> {
// Note that this check is synchronous. The async executor also likely is not being polled
// yet, since this is called during startup.
let bootfs_dir = Path::new("/boot");
if !bootfs_dir.exists() {
return Ok(None);
}
let proxy = io_util::open_directory_in_namespace(
bootfs_dir.to_str().unwrap(),
fio::OPEN_RIGHT_READABLE | fio::OPEN_RIGHT_EXECUTABLE,
)?;
Ok(Some(Self::new_from_directory(proxy)))
}
/// Create a new FuchsiaBootResolver that resolves URLs within the given directory. Used for
/// injection in unit tests.
fn new_from_directory(proxy: DirectoryProxy) -> FuchsiaBootResolver {
FuchsiaBootResolver { boot_proxy: proxy }
}
async fn resolve_async<'a>(
&'a self,
component_url: &'a str,
) -> Result<fsys::Component, ResolverError> {
// Parse URL.
let url = BootUrl::parse(component_url)
.map_err(|e| ResolverError::component_not_available(component_url, e))?;
// Package path is 'canonicalized' to ensure that it is relative, since absolute paths will
// be (inconsistently) rejected by fuchsia.io methods.
let package_path = Path::new(io_util::canonicalize_path(url.path()));
let res = url.resource().ok_or(ResolverError::url_missing_resource_error(component_url))?;
let res_path = match package_path.to_str() {
Some(".") => PathBuf::from(res),
_ => package_path.join(res),
};
// Read component manifest from resource into a component decl.
let cm_file = io_util::open_file(&self.boot_proxy, &res_path, fio::OPEN_RIGHT_READABLE)
.map_err(|e| ResolverError::manifest_not_available(component_url, e))?;
let component_decl = io_util::read_file_fidl(&cm_file)
.await
.map_err(|e| ResolverError::manifest_not_available(component_url, e))?;
// Validate the component manifest
cm_fidl_validator::validate(&component_decl)
.map_err(|e| ResolverError::manifest_invalid(component_url, e))?;
// Set up the fuchsia-boot path as the component's "package" namespace.
let path_proxy = io_util::open_directory(
&self.boot_proxy,
package_path,
fio::OPEN_RIGHT_READABLE | fio::OPEN_RIGHT_EXECUTABLE,
)
.map_err(|e| {
ResolverError::component_not_available(
component_url,
e.context("failed to open package directory"),
)
})?;
let package = fsys::Package {
package_url: Some(url.root_url().to_string()),
package_dir: Some(ClientEnd::new(path_proxy.into_channel().unwrap().into_zx_channel())),
..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 FuchsiaBootResolver {
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::{create_proxy_and_stream, ServerEnd},
fidl_fuchsia_data as fdata,
fidl_fuchsia_io::{DirectoryMarker, DirectoryRequest, NodeMarker},
fidl_fuchsia_sys2::ComponentDecl,
fuchsia_async as fasync,
futures::prelude::*,
std::path::PathBuf,
vfs::{
self, directory::entry::DirectoryEntry, execution_scope::ExecutionScope,
file::pcb::asynchronous::read_only_static, pseudo_directory,
},
};
// Simulate a fake bootfs Directory service that only contains a single directory
// ("packages/hello-world"), using our own package directory (hosted by the real pkgfs) as the
// contents.
// TODO(fxbug.dev/37534): This is implemented by manually handling the Directory.Open and forwarding
// to the test's real package directory because Rust vfs does not yet support
// OPEN_RIGHT_EXECUTABLE. Simplify in the future.
// TODO: Switch this test to use a hardcoded manifest string & consider removing this test
// manifest from the test package completely (after cleaning up other test dependencies).
struct FakeBootfs;
impl FakeBootfs {
pub fn new() -> DirectoryProxy {
let (proxy, mut stream) = create_proxy_and_stream::<DirectoryMarker>().unwrap();
fasync::Task::local(async move {
while let Some(request) = stream.try_next().await.unwrap() {
match request {
DirectoryRequest::Open {
flags,
mode: _,
path,
object,
control_handle: _,
} => Self::handle_open(&path, flags, object),
_ => panic!("Fake doesn't support request: {:?}", request),
}
}
})
.detach();
proxy
}
fn handle_open(path_str: &str, flags: u32, server_end: ServerEnd<NodeMarker>) {
if path_str.is_empty() {
// We don't support this in this fake, drop the server_end
return;
}
let path = Path::new(path_str);
let mut path_iter = path.iter();
match path_iter.next().unwrap().to_str().unwrap() {
"packages" => {
// The test URLs used below have "packages/" as the first path component
match path_iter.next().unwrap().to_str().unwrap() {
"hello-world" => {
// Connect the server_end by forwarding to our real package directory, which can handle
// OPEN_RIGHT_EXECUTABLE. Also, pass through the input flags here to ensure that we
// don't artificially pass the test (i.e. the resolver needs to ask for the appropriate
// rights).
let mut open_path = PathBuf::from("/pkg");
open_path.extend(path_iter);
io_util::connect_in_namespace(
open_path.to_str().unwrap(),
server_end.into_channel(),
flags,
)
.expect("failed to open path in namespace");
}
_ => return,
}
}
"meta" => {
// Provide a cm that will fail due to multiple runners being configured.
let out_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()
),
}
};
out_dir.open(
ExecutionScope::new(),
flags,
fio::MODE_TYPE_FILE,
vfs::path::Path::validate_and_split(path_str)
.expect("received invalid path"),
server_end,
);
}
_ => return,
}
}
}
#[fasync::run_singlethreaded(test)]
async fn hello_world_test() -> Result<(), Error> {
let resolver = FuchsiaBootResolver::new_from_directory(FakeBootfs::new());
let url = "fuchsia-boot:///packages/hello-world#meta/hello-world.cm";
let component = resolver.resolve_async(url).await?;
// 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!(url, resolved_url.unwrap());
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 = 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,
..ComponentDecl::EMPTY
};
assert_eq!(decl.unwrap(), expected_decl);
let fsys::Package { package_url, package_dir, .. } = package.unwrap();
assert_eq!(package_url.unwrap(), "fuchsia-boot:///packages/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::<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");
Ok(())
}
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 resolver = FuchsiaBootResolver::new_from_directory(FakeBootfs::new());
test_resolve_error!(resolver, "fuchsia-boot:///#meta/invalid.cm", ManifestInvalid);
}
}