blob: 80ac53ff0e968ebc132a141ccc1a965de5b566d9 [file] [log] [blame]
// Copyright 2020 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 {
anyhow::{self, Context},
fidl::endpoints::{ClientEnd, Proxy},
fidl_fuchsia_io::{self as fio, DirectoryProxy},
fidl_fuchsia_sys2::{self as fsys, ComponentResolverRequest, ComponentResolverRequestStream},
fuchsia_async as fasync,
fuchsia_component::server::ServiceFs,
fuchsia_url::{errors::ParseError as PkgUrlParseError, pkg_url::PkgUrl},
fuchsia_zircon::Status,
futures::prelude::*,
log::*,
thiserror::Error,
};
/// Wraps all hosted protocols into a single type that can be matched against
/// and dispatched.
enum IncomingRequest {
ComponentResolver(ComponentResolverRequestStream),
}
#[fasync::run_singlethreaded]
async fn main() -> Result<(), anyhow::Error> {
fuchsia_syslog::init().expect("failed to initialize logging");
let mut service_fs = ServiceFs::new_local();
service_fs.dir("svc").add_fidl_service(IncomingRequest::ComponentResolver);
service_fs.take_and_serve_directory_handle().context("failed to serve outgoing namespace")?;
service_fs
.for_each_concurrent(None, |IncomingRequest::ComponentResolver(stream)| async move {
match serve(stream).await {
Ok(()) => {}
Err(err) => error!("failed to serve resolve request: {:?}", err),
}
})
.await;
Ok(())
}
async fn serve(mut stream: ComponentResolverRequestStream) -> Result<(), anyhow::Error> {
let pkgfs_dir = io_util::open_directory_in_namespace(
"/pkgfs",
fio::OPEN_RIGHT_READABLE | fio::OPEN_RIGHT_EXECUTABLE,
)
.context("failed to open /pkgfs")?;
while let Some(ComponentResolverRequest::Resolve { component_url, responder }) =
stream.try_next().await.context("failed to read request from FIDL stream")?
{
match resolve_component(&component_url, &pkgfs_dir).await {
Ok(result) => responder.send(Status::OK.into_raw(), result),
Err(err) => {
error!("failed to resolve component URL {}: {}", &component_url, &err);
responder.send(err.as_zx_status(), fsys::Component::EMPTY)
}
}
.context("failed sending response")?;
}
Ok(())
}
async fn resolve_component(
component_url: &str,
pkgfs_dir: &DirectoryProxy,
) -> Result<fsys::Component, ResolverError> {
let package_url = PkgUrl::parse(component_url)?;
let cm_path = package_url
.resource()
.ok_or_else(|| ResolverError::InvalidUrl(PkgUrlParseError::InvalidResourcePath))?;
let package_dir = resolve_package(&package_url, pkgfs_dir).await?;
// Read the component manifest (.cm file) from the package directory.
let cm_file = io_util::directory::open_file(&package_dir, cm_path, fio::OPEN_RIGHT_READABLE)
.await
.map_err(ResolverError::ComponentNotFound)?;
let component_decl =
io_util::file::read_fidl(&cm_file).await.map_err(ResolverError::InvalidManifest)?;
let package_dir = ClientEnd::new(
package_dir.into_channel().expect("could not convert proxy to channel").into_zx_channel(),
);
Ok(fsys::Component {
resolved_url: Some(component_url.into()),
decl: Some(component_decl),
package: Some(fsys::Package {
package_url: Some(package_url.root_url().to_string()),
package_dir: Some(package_dir),
..fsys::Package::EMPTY
}),
..fsys::Component::EMPTY
})
}
async fn resolve_package(
package_url: &PkgUrl,
pkgfs_dir: &DirectoryProxy,
) -> Result<DirectoryProxy, ResolverError> {
let root_url = package_url.root_url();
if root_url.host() != "fuchsia.com" {
return Err(ResolverError::UnsupportedRepo);
}
let package_name = io_util::canonicalize_path(root_url.path());
// Package contents are available at `packages/$PACKAGE_NAME/0`.
let dir = io_util::directory::open_directory(
pkgfs_dir,
&format!("packages/{}/0", package_name),
fio::OPEN_RIGHT_READABLE | fio::OPEN_RIGHT_EXECUTABLE,
)
.await
.map_err(ResolverError::PackageNotFound)?;
Ok(dir)
}
#[derive(Error, Debug)]
enum ResolverError {
#[error("invalid component URL: {}", .0)]
InvalidUrl(#[from] PkgUrlParseError),
#[error("the hostname refers to an unsupported repo")]
UnsupportedRepo,
#[error("invalid component manifest: {}", .0)]
InvalidManifest(#[source] anyhow::Error),
#[error("component not found: {}", .0)]
ComponentNotFound(#[source] io_util::node::OpenError),
#[error("package not found: {}", .0)]
PackageNotFound(#[source] io_util::node::OpenError),
}
impl ResolverError {
fn as_zx_status(&self) -> i32 {
match self {
Self::InvalidUrl(_) => Status::INVALID_ARGS.into_raw(),
Self::UnsupportedRepo => Status::NOT_SUPPORTED.into_raw(),
Self::InvalidManifest(_) => Status::IO_INVALID.into_raw(),
Self::ComponentNotFound(_) => Status::NOT_FOUND.into_raw(),
Self::PackageNotFound(_) => Status::NOT_FOUND.into_raw(),
}
}
}
#[cfg(test)]
mod tests {
use {
super::*,
fake_pkgfs::{Entry, MockDir, MockFile},
fidl::encoding::encode_persistent,
fidl::endpoints::{create_proxy, RequestStream, ServerEnd},
fidl_fuchsia_io::{DirectoryMarker, DirectoryObject, NodeInfo, NodeMarker},
matches::assert_matches,
std::sync::Arc,
};
/// A DirectoryEntry implementation that checks whether an expected set of flags
/// are set in the Open request.
struct FlagVerifier(u32);
impl Entry for FlagVerifier {
fn open(
self: Arc<Self>,
flags: u32,
_mode: u32,
_path: &str,
server_end: ServerEnd<NodeMarker>,
) {
let status = if flags & self.0 != self.0 { Status::INVALID_ARGS } else { Status::OK };
let stream = server_end.into_stream().expect("failed to create stream");
let control_handle = stream.control_handle();
control_handle
.send_on_open_(
status.into_raw(),
Some(&mut NodeInfo::Directory(DirectoryObject {})),
)
.expect("failed to send OnOpen event");
control_handle.shutdown_with_epitaph(status);
}
}
fn serve_pkgfs(pseudo_dir: Arc<dyn Entry>) -> Result<DirectoryProxy, anyhow::Error> {
let (proxy, server_end) = create_proxy::<DirectoryMarker>()
.context("failed to create DirectoryProxy/Server pair")?;
pseudo_dir.open(
fio::OPEN_RIGHT_READABLE | fio::OPEN_RIGHT_EXECUTABLE,
fio::MODE_TYPE_DIRECTORY,
".",
ServerEnd::new(server_end.into_channel()),
);
Ok(proxy)
}
#[fasync::run_singlethreaded(test)]
async fn resolves_package_with_executable_rights() {
let pkg_url = PkgUrl::new_package("fuchsia.com".into(), "/test-package".into(), None)
.expect("failed to create test PkgUrl");
let flag_verifier =
Arc::new(FlagVerifier(fio::OPEN_RIGHT_READABLE | fio::OPEN_RIGHT_EXECUTABLE));
let pkgfs_dir =
serve_pkgfs(Arc::new(MockDir::new().add_entry(
"packages",
Arc::new(MockDir::new().add_entry(
"test-package",
Arc::new(MockDir::new().add_entry("0", flag_verifier)),
)),
)))
.expect("failed to serve pkgfs");
let package =
resolve_package(&pkg_url, &pkgfs_dir).await.expect("failed to resolve package");
let event_stream = package.take_event_stream().map_ok(|_| ());
assert_matches!(
event_stream.try_collect::<()>().await,
Err(fidl::Error::ClientChannelClosed { status: Status::OK, .. })
);
}
#[fasync::run_singlethreaded(test)]
async fn fails_to_resolve_package_unsupported_repo() {
let pkg_url = PkgUrl::new_package("fuchsia.ca".into(), "/test-package".into(), None)
.expect("failed to create test PkgUrl");
let pkgfs_dir = serve_pkgfs(Arc::new(MockDir::new())).expect("failed to serve pkgfs");
assert_matches!(
resolve_package(&pkg_url, &pkgfs_dir).await,
Err(ResolverError::UnsupportedRepo)
);
}
#[fasync::run_singlethreaded(test)]
async fn fails_to_resolve_component_invalid_url() {
let pkgfs_dir = serve_pkgfs(Arc::new(MockDir::new())).expect("failed to serve pkgfs");
assert_matches!(
resolve_component("fuchsia://fuchsia.com/foo#meta/bar.cm", &pkgfs_dir).await,
Err(ResolverError::InvalidUrl(_))
);
assert_matches!(
resolve_component("fuchsia-pkg://fuchsia.com/foo", &pkgfs_dir).await,
Err(ResolverError::InvalidUrl(_))
);
assert_matches!(
resolve_component("fuchsia-pkg://fuchsia.com/#meta/bar.cm", &pkgfs_dir).await,
Err(ResolverError::InvalidUrl(_))
);
assert_matches!(
resolve_component("fuchsia-pkg://fuchsia.ca/foo#meta/bar.cm", &pkgfs_dir).await,
Err(ResolverError::UnsupportedRepo)
);
}
#[fasync::run_singlethreaded(test)]
async fn fails_to_resolve_component_package_not_found() {
let pkgfs_dir = serve_pkgfs(build_fake_pkgfs()).expect("failed to serve pkgfs");
assert_matches!(
resolve_component("fuchsia-pkg://fuchsia.com/missing-package#meta/foo.cm", &pkgfs_dir)
.await,
Err(ResolverError::PackageNotFound(_))
);
}
#[fasync::run_singlethreaded(test)]
async fn fails_to_resolve_component_missing_manifest() {
let pkgfs_dir = serve_pkgfs(build_fake_pkgfs()).expect("failed to serve pkgfs");
assert_matches!(
resolve_component("fuchsia-pkg://fuchsia.com/test-package#meta/bar.cm", &pkgfs_dir)
.await,
Err(ResolverError::ComponentNotFound(_))
);
}
#[fasync::run_singlethreaded(test)]
async fn fails_to_resolve_component_invalid_manifest() {
let pkgfs_dir = serve_pkgfs(build_fake_pkgfs()).expect("failed to serve pkgfs");
assert_matches!(
resolve_component("fuchsia-pkg://fuchsia.com/test-package#meta/bad.cm", &pkgfs_dir)
.await,
Err(ResolverError::InvalidManifest(_))
);
}
#[fasync::run_singlethreaded(test)]
async fn resolves_component_successfully() {
fuchsia_syslog::init().unwrap();
fuchsia_syslog::set_severity(fuchsia_syslog::levels::INFO);
let pkgfs_dir = serve_pkgfs(build_fake_pkgfs()).expect("failed to serve pkgfs");
assert_matches!(
resolve_component("fuchsia-pkg://fuchsia.com/test-package#meta/foo.cm", &pkgfs_dir)
.await,
Ok(_)
);
}
fn build_fake_pkgfs() -> Arc<MockDir> {
let cm_bytes = encode_persistent(&mut fsys::ComponentDecl::EMPTY.clone())
.expect("failed to encode ComponentDecl FIDL");
Arc::new(
MockDir::new().add_entry(
"packages",
Arc::new(
MockDir::new().add_entry(
"test-package",
Arc::new(
MockDir::new().add_entry(
"0",
Arc::new(
MockDir::new().add_entry(
"meta",
Arc::new(
MockDir::new()
.add_entry(
"foo.cm",
Arc::new(MockFile::new(cm_bytes)),
)
.add_entry(
"bad.cm",
Arc::new(MockFile::new(b"foo".to_vec())),
),
),
),
),
),
),
),
),
),
)
}
}