blob: faf9f83bdcf70ccfa382dd2bd909ddaf8114fff2 [file] [log] [blame]
// Copyright 2022 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},
base_resolver_config::Config,
fidl::endpoints::{ClientEnd, Proxy},
fidl_fuchsia_component_decl as fdecl,
fidl_fuchsia_component_resolution::{
self as fresolution, ResolverRequest, ResolverRequestStream,
},
fidl_fuchsia_io as fio,
fidl_fuchsia_pkg::PackageCacheMarker,
fidl_fuchsia_pkg_ext::BasePackageIndex,
fuchsia_component::{client::connect_to_protocol, server::ServiceFs},
fuchsia_pkg::{
transitional::{context_bytes_from_subpackages_map, subpackages_map_from_context_bytes},
PackageDirectory,
},
fuchsia_url::{ComponentUrl, PackageUrl},
futures::prelude::*,
log::*,
};
pub(crate) async fn main() -> anyhow::Result<()> {
info!("started");
// Record configuration to inspect
let config = Config::take_from_startup_handle();
let inspector = fuchsia_inspect::component::inspector();
inspector.root().record_child("config", |config_node| config.record_inspect(config_node));
let mut service_fs = ServiceFs::new_local();
service_fs.dir("svc").add_fidl_service(Services::BaseResolver);
service_fs.take_and_serve_directory_handle().context("failed to serve outgoing namespace")?;
let () = service_fs
.for_each_concurrent(None, |request| async {
match request {
Services::BaseResolver(stream) => {
serve(stream, &config)
.unwrap_or_else(|e| {
error!("failed to serve base resolver request: {:#}", e)
})
.await
}
}
})
.await;
Ok(())
}
enum Services {
BaseResolver(ResolverRequestStream),
}
async fn serve(mut stream: ResolverRequestStream, config: &Config) -> anyhow::Result<()> {
let pkg_cache =
connect_to_protocol::<PackageCacheMarker>().context("error connecting to package cache")?;
let base_package_index = BasePackageIndex::from_proxy(&pkg_cache)
.await
.context("failed to load base package index")?;
let packages_dir = fuchsia_fs::open_directory_in_namespace(
"/pkgfs/packages",
fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::RIGHT_EXECUTABLE,
)
.context("failed to open /pkgfs/packages")?;
while let Some(request) =
stream.try_next().await.context("failed to read request from FIDL stream")?
{
match request {
ResolverRequest::Resolve { component_url, responder } => {
let mut result =
resolve_component(&component_url, &packages_dir).await.map_err(|err| {
let fidl_err = (&err).into();
error!(
"failed to resolve component URL {}: {:#}",
&component_url,
anyhow::anyhow!(err)
);
fidl_err
});
responder.send(&mut result).context("failed sending response")?;
}
ResolverRequest::ResolveWithContext { component_url, context, responder } => {
if config.enable_subpackages {
let mut result = resolve_component_with_context(
&component_url,
&context,
&packages_dir,
&base_package_index,
)
.await
.map_err(|err| {
let fidl_err = (&err).into();
error!(
"failed to resolve component URL {} with context {:?}: {:#}",
&component_url,
&context,
anyhow::anyhow!(err)
);
fidl_err
});
responder.send(&mut result).context("failed sending response")?;
} else {
error!(
"base-resolver ResolveWithContext is disabled. Config value `enable_subpackages` is false. Cannot resolve component URL {:?} with context {:?}",
component_url,
context
);
responder
.send(&mut Err(fresolution::ResolverError::Internal))
.context("failed sending response")?;
}
}
};
}
Ok(())
}
async fn resolve_component(
component_url: &str,
packages_dir: &fio::DirectoryProxy,
) -> Result<fresolution::Component, crate::ResolverError> {
resolve_component_async(component_url, None, packages_dir, None).await
}
async fn resolve_component_with_context(
component_url: &str,
context: &Vec<u8>,
packages_dir: &fio::DirectoryProxy,
base_package_index: &BasePackageIndex,
) -> Result<fresolution::Component, crate::ResolverError> {
resolve_component_async(component_url, Some(context), packages_dir, Some(base_package_index))
.await
}
async fn resolve_component_async(
component_url_str: &str,
some_incoming_context: Option<&Vec<u8>>,
packages_dir: &fio::DirectoryProxy,
some_base_package_index: Option<&BasePackageIndex>,
) -> Result<fresolution::Component, crate::ResolverError> {
let component_url = ComponentUrl::parse(component_url_str)?;
let package = resolve_package_async(
component_url.package_url(),
some_incoming_context,
packages_dir,
some_base_package_index,
)
.await?;
let data = mem_util::open_file_data(&package.dir, &component_url.resource())
.await
.map_err(crate::ResolverError::ComponentNotFound)?;
let raw_bytes = mem_util::bytes_from_data(&data).map_err(crate::ResolverError::ReadManifest)?;
let decl: fdecl::Component = fidl::encoding::decode_persistent(&raw_bytes[..])
.map_err(crate::ResolverError::ParsingManifest)?;
let config_values = if let Some(config_decl) = decl.config.as_ref() {
// if we have a config declaration, we need to read the value file from the package dir
let strategy =
config_decl.value_source.as_ref().ok_or(crate::ResolverError::InvalidConfigSource)?;
let config_path = match strategy {
fdecl::ConfigValueSource::PackagePath(path) => path,
other => return Err(crate::ResolverError::UnsupportedConfigSource(other.to_owned())),
};
Some(
mem_util::open_file_data(&package.dir, &config_path)
.await
.map_err(crate::ResolverError::ConfigValuesNotFound)?,
)
} else {
None
};
let package_dir = ClientEnd::new(
package.dir.into_channel().expect("could not convert proxy to channel").into_zx_channel(),
);
Ok(fresolution::Component {
url: Some(component_url_str.to_string()),
resolution_context: Some(package.context),
decl: Some(data),
package: Some(fresolution::Package {
url: Some(component_url.package_url().to_string()),
directory: Some(package_dir),
..fresolution::Package::EMPTY
}),
config_values,
..fresolution::Component::EMPTY
})
}
#[derive(Debug)]
struct ResolvedPackage {
dir: fio::DirectoryProxy,
context: Vec<u8>,
}
async fn resolve_package_async(
package_url: &PackageUrl,
some_incoming_context: Option<&Vec<u8>>,
packages_dir: &fio::DirectoryProxy,
some_base_package_index: Option<&BasePackageIndex>,
) -> Result<ResolvedPackage, crate::ResolverError> {
let (package_name, some_variant) = match package_url {
PackageUrl::Relative(relative) => {
let context = some_incoming_context.ok_or_else(|| {
crate::ResolverError::RelativeUrlMissingContext(package_url.to_string())
})?;
// TODO(fxbug.dev/101492): Update base-resolver to use blobfs directly,
// and allow subpackage lookup to resolve subpackages from blobfs via
// the blobid (package hash). Then base-resolver will no longer need
// access to pkgfs-packages or to the package index (from PackageCache).
let base_package_index =
some_base_package_index.ok_or_else(|| crate::ResolverError::Internal)?;
let subpackage_hashes = subpackages_map_from_context_bytes(context)
.map_err(|err| crate::ResolverError::ReadingContext(err))?;
let hash = subpackage_hashes.get(relative).ok_or_else(|| {
crate::ResolverError::SubpackageNotFound(anyhow::format_err!(
"Subpackage '{}' not found in context: {:?}",
relative,
subpackage_hashes
))
})?;
let absolute = base_package_index.get_url(&(*hash).into()).ok_or_else(|| {
crate::ResolverError::SubpackageNotInBase(anyhow::format_err!(
"Subpackage '{}' with hash '{}' is not in base",
relative,
hash
))
})?;
(absolute.name(), absolute.variant())
}
PackageUrl::Absolute(absolute) => {
if absolute.host() != "fuchsia.com" {
return Err(crate::ResolverError::UnsupportedRepo);
}
if absolute.hash().is_some() {
return Err(crate::ResolverError::PackageHashNotSupported);
}
(absolute.name(), absolute.variant())
}
};
// Package contents are available at `packages/$PACKAGE_NAME/0`.
let dir = fuchsia_fs::directory::open_directory(
packages_dir,
&format!("{}/{}", package_name.as_ref(), some_variant.map(|v| v.as_ref()).unwrap_or("0")),
fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::RIGHT_EXECUTABLE,
)
.await
.map_err(crate::ResolverError::PackageNotFound)?;
let package_dir = PackageDirectory::from_proxy(dir);
let context = fabricate_package_context(&package_dir)
.map_err(|err| crate::ResolverError::CreatingContext(anyhow::anyhow!(err)))
.await?;
Ok(ResolvedPackage { dir: package_dir.into_proxy(), context })
}
async fn fabricate_package_context(package_dir: &PackageDirectory) -> anyhow::Result<Vec<u8>> {
let meta = package_dir.meta_subpackages().await?;
Ok(context_bytes_from_subpackages_map(&meta.into_subpackages())?.unwrap_or(vec![]))
}
#[cfg(test)]
mod tests {
use {
super::*,
assert_matches::assert_matches,
fidl::encoding::encode_persistent_with_context,
fidl::endpoints::{create_proxy, ServerEnd},
fidl::prelude::*,
fidl_fuchsia_component_config as fconfig, fidl_fuchsia_component_decl as fdecl,
fidl_fuchsia_mem as fmem,
fidl_fuchsia_pkg_ext::BlobId,
fuchsia_hash::Hash,
fuchsia_pkg::MetaSubpackages,
fuchsia_url::{RelativePackageUrl, UnpinnedAbsolutePackageUrl},
fuchsia_zircon::Status,
maplit::hashmap,
std::{iter::FromIterator, str::FromStr, sync::Arc},
};
const SUBPACKAGE_NAME: &'static str = "my_subpackage";
const SUBPACKAGE_HASH: &'static str =
"facefacefacefacefacefacefacefacefacefacefacefacefacefacefaceface";
const OTHER_PACKAGE_HASH: &'static str =
"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
/// A DirectoryEntry implementation that checks whether an expected set of flags
/// are set in the Open request.
struct FlagVerifier(fio::OpenFlags);
impl vfs::directory::entry::DirectoryEntry for FlagVerifier {
fn open(
self: Arc<Self>,
_scope: vfs::execution_scope::ExecutionScope,
flags: fio::OpenFlags,
_mode: u32,
_path: vfs::path::Path,
server_end: ServerEnd<fio::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 fio::NodeInfo::Directory(fio::DirectoryObject {})),
)
.expect("failed to send OnOpen event");
control_handle.shutdown_with_epitaph(status);
}
fn entry_info(&self) -> vfs::directory::entry::EntryInfo {
vfs::directory::entry::EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory)
}
}
/// Serve a pseudo_dir with `RIGHT_EXECUTABLE` permissions, which can
/// emulate `/pkgfs/packages` for tests.
fn serve_executable_dir(
pseudo_dir: Arc<dyn vfs::directory::entry::DirectoryEntry>,
) -> fio::DirectoryProxy {
let (proxy, server_end) = create_proxy::<fio::DirectoryMarker>()
.expect("failed to create DirectoryProxy/Server pair");
let () = pseudo_dir.open(
vfs::execution_scope::ExecutionScope::new(),
fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::RIGHT_EXECUTABLE,
0,
vfs::path::Path::dot(),
ServerEnd::new(server_end.into_channel()),
);
proxy
}
#[fuchsia::test]
async fn fails_to_resolve_package_unsupported_repo() {
let packages_dir = serve_executable_dir(vfs::pseudo_directory! {});
assert_matches!(
resolve_component("fuchsia-pkg://fuchsia.ca/test-package#meta/foo.cm", &packages_dir)
.await,
Err(crate::ResolverError::UnsupportedRepo)
);
}
#[fuchsia::test]
async fn fails_to_resolve_component_invalid_url() {
let packages_dir = serve_executable_dir(vfs::pseudo_directory! {});
assert_matches!(
resolve_component("fuchsia://fuchsia.com/foo#meta/bar.cm", &packages_dir).await,
Err(crate::ResolverError::InvalidUrl(_))
);
assert_matches!(
resolve_component("fuchsia-pkg://fuchsia.com/foo", &packages_dir).await,
Err(crate::ResolverError::InvalidUrl(_))
);
assert_matches!(
resolve_component("fuchsia-pkg://fuchsia.com/#meta/bar.cm", &packages_dir).await,
Err(crate::ResolverError::InvalidUrl(_))
);
assert_matches!(
resolve_component("fuchsia-pkg://fuchsia.ca/foo#meta/bar.cm", &packages_dir).await,
Err(crate::ResolverError::UnsupportedRepo)
);
let url_with_hash = concat!(
"fuchsia-pkg://fuchsia.com/test-package",
"?hash=f241b31d5913b66c90a44d44537d6bec62672e1f05dbc4c4f22b863b01c68749",
"#meta/test.cm"
);
assert_matches!(
resolve_component(url_with_hash, &packages_dir).await,
Err(crate::ResolverError::PackageHashNotSupported)
);
}
#[fuchsia::test]
async fn fails_to_resolve_component_package_not_found() {
let packages_dir = serve_executable_dir(build_fake_packages_dir());
assert_matches!(
resolve_component(
"fuchsia-pkg://fuchsia.com/missing-package#meta/foo.cm",
&packages_dir
)
.await,
Err(crate::ResolverError::PackageNotFound(_))
);
}
#[fuchsia::test]
async fn fails_to_resolve_component_missing_manifest() {
let packages_dir = serve_executable_dir(build_fake_packages_dir());
assert_matches!(
resolve_component("fuchsia-pkg://fuchsia.com/test-package#meta/bar.cm", &packages_dir)
.await,
Err(crate::ResolverError::ComponentNotFound(_))
);
}
#[fuchsia::test]
async fn resolves_component_vmo_manifest() {
let packages_dir = serve_executable_dir(build_fake_packages_dir());
assert_matches!(
resolve_component("fuchsia-pkg://fuchsia.com/test-package#meta/vmo.cm", &packages_dir)
.await,
Ok(fresolution::Component { decl: Some(fmem::Data::Buffer(_)), .. })
);
}
#[fuchsia::test]
async fn resolves_component_file_manifest() {
let packages_dir = serve_executable_dir(build_fake_packages_dir());
assert_matches!(
resolve_component("fuchsia-pkg://fuchsia.com/test-package#meta/foo.cm", &packages_dir)
.await,
Ok(fresolution::Component {
decl: Some(fidl_fuchsia_mem::Data::Buffer(fidl_fuchsia_mem::Buffer { .. })),
..
})
);
}
#[fuchsia::test]
async fn resolves_component_in_subpackage() {
let parent_component_url = "fuchsia-pkg://fuchsia.com/test-package#meta/foo.cm";
let subpackaged_component_url = SUBPACKAGE_NAME.to_string() + "#meta/subfoo.cm";
// Set up the base package index with the subpackage's hash that will
// be requested
let subpackage_as_base_package_url: UnpinnedAbsolutePackageUrl =
"fuchsia-pkg://fuchsia.com/toplevel-subpackage".parse().unwrap();
let subpackage_blob_id = BlobId::parse(SUBPACKAGE_HASH).unwrap();
let other_package_as_base_package_url: UnpinnedAbsolutePackageUrl =
"fuchsia-pkg://fuchsia.com/some-other-package".parse().unwrap();
let other_package_blob_id = BlobId::parse(OTHER_PACKAGE_HASH).unwrap();
let index = hashmap! {
subpackage_as_base_package_url => subpackage_blob_id,
other_package_as_base_package_url => other_package_blob_id,
};
let base_package_index = BasePackageIndex::create_mock(index);
let packages_dir = serve_executable_dir(build_fake_packages_dir());
let parent_component = resolve_component(parent_component_url, &packages_dir)
.await
.expect("failed to resolve parent_component");
assert_matches!(parent_component.resolution_context, Some(..));
assert_matches!(
resolve_component_with_context(
&subpackaged_component_url,
parent_component.resolution_context.as_ref().unwrap(),
&packages_dir,
&base_package_index,
)
.await,
Ok(fresolution::Component { decl: Some(..), .. }),
"Could not resolve subpackaged component '{}' from context '{:?}'",
subpackaged_component_url,
parent_component.resolution_context
);
}
#[fuchsia::test]
async fn fails_to_resolve_component_in_subpackage_not_in_base() {
let parent_component_url = "fuchsia-pkg://fuchsia.com/test-package#meta/foo.cm";
let subpackaged_component_url = SUBPACKAGE_NAME.to_string() + "#meta/other_subfoo.cm";
// Set up the base package index WITHOUT the subpackage's hash that will
// be requested
let other_package_as_base_package_url: UnpinnedAbsolutePackageUrl =
"fuchsia-pkg://fuchsia.com/some-other-package".parse().unwrap();
let other_package_blob_id = BlobId::parse(OTHER_PACKAGE_HASH).unwrap();
let index = hashmap! {
other_package_as_base_package_url => other_package_blob_id,
};
let base_package_index = BasePackageIndex::create_mock(index);
assert!(base_package_index.contains_package(&other_package_blob_id));
let packages_dir = serve_executable_dir(build_fake_packages_dir());
let parent_component = resolve_component(parent_component_url, &packages_dir)
.await
.expect("failed to resolve parent_component");
assert_matches!(parent_component.resolution_context, Some(..));
assert_matches!(
resolve_component_with_context(
&subpackaged_component_url,
parent_component.resolution_context.as_ref().unwrap(),
&packages_dir,
&base_package_index,
)
.await,
Err(crate::ResolverError::SubpackageNotInBase(..))
);
}
#[fuchsia::test]
async fn fails_to_resolve_subpackage_name_not_in_parent_subpackages() {
let parent_component_url = "fuchsia-pkg://fuchsia.com/test-package#meta/foo.cm";
let subpackaged_component_url = "subpackage_not_in_parent#meta/other_subfoo.cm";
// Set up the base package index WITHOUT the subpackage's hash that will
// be requested
let other_package_as_base_package_url: UnpinnedAbsolutePackageUrl =
"fuchsia-pkg://fuchsia.com/some-other-package".parse().unwrap();
let other_package_blob_id = BlobId::parse(OTHER_PACKAGE_HASH).unwrap();
let index = hashmap! {
other_package_as_base_package_url => other_package_blob_id,
};
let base_package_index = BasePackageIndex::create_mock(index);
assert!(base_package_index.contains_package(&other_package_blob_id));
let packages_dir = serve_executable_dir(build_fake_packages_dir());
let parent_component = resolve_component(parent_component_url, &packages_dir)
.await
.expect("failed to resolve parent_component");
assert_matches!(parent_component.resolution_context, Some(..));
assert_matches!(
resolve_component_with_context(
&subpackaged_component_url,
parent_component.resolution_context.as_ref().unwrap(),
&packages_dir,
&base_package_index,
)
.await,
Err(crate::ResolverError::SubpackageNotFound(..))
);
}
#[fuchsia::test]
async fn resolves_component_with_config() {
let packages_dir = serve_executable_dir(build_fake_packages_dir());
let component = resolve_component(
"fuchsia-pkg://fuchsia.com/test-package#meta/foo-with-config.cm",
&packages_dir,
)
.await
.unwrap();
assert_matches!(
component,
fresolution::Component { decl: Some(..), config_values: Some(..), .. }
);
}
#[fuchsia::test]
async fn fails_to_resolve_component_missing_config_values() {
let packages_dir = serve_executable_dir(build_fake_packages_dir());
let error = resolve_component(
"fuchsia-pkg://fuchsia.com/test-package#meta/foo-without-config.cm",
&packages_dir,
)
.await
.unwrap_err();
assert_matches!(error, crate::ResolverError::ConfigValuesNotFound(..));
}
#[fuchsia::test]
async fn fails_to_resolve_component_bad_config_source() {
let packages_dir = serve_executable_dir(build_fake_packages_dir());
let error = resolve_component(
"fuchsia-pkg://fuchsia.com/test-package#meta/foo-with-bad-config.cm",
&packages_dir,
)
.await
.unwrap_err();
assert_matches!(error, crate::ResolverError::InvalidConfigSource);
}
fn build_fake_packages_dir() -> Arc<dyn vfs::directory::entry::DirectoryEntry> {
let subpackage = build_fake_subpackage_dir();
let parent_package = build_fake_package_dir();
vfs::pseudo_directory! {
"test-package" => vfs::pseudo_directory! {
"0" => parent_package,
},
"toplevel-subpackage" => vfs::pseudo_directory! {
"0" => subpackage,
},
}
}
fn build_fake_package_dir() -> Arc<dyn vfs::directory::entry::DirectoryEntry> {
let cm_bytes = encode_persistent_with_context(
&fidl::encoding::Context { wire_format_version: fidl::encoding::WireFormatVersion::V2 },
&mut fdecl::Component::EMPTY.clone(),
)
.expect("failed to encode ComponentDecl FIDL");
let subpackages = MetaSubpackages::from_iter(vec![(
RelativePackageUrl::parse(SUBPACKAGE_NAME).unwrap(),
Hash::from_str(SUBPACKAGE_HASH).unwrap(),
)]);
vfs::pseudo_directory! {
"meta" => vfs::pseudo_directory! {
"fuchsia.pkg" => vfs::pseudo_directory! {
"subpackages" => vfs::file::vmo::asynchronous::read_only_const(&serde_json::to_vec(&subpackages).unwrap()),
},
"foo.cm" => vfs::file::vmo::asynchronous::read_only_const(&cm_bytes),
"foo-with-config.cm" => vfs::file::vmo::asynchronous::read_only_const(
&encode_persistent_with_context(
&fidl::encoding::Context {
wire_format_version: fidl::encoding::WireFormatVersion::V2
},
&mut fdecl::Component {
config: Some(fdecl::ConfigSchema {
value_source: Some(
fdecl::ConfigValueSource::PackagePath(
"meta/foo-with-config.cvf".to_string(),
),
),
..fdecl::ConfigSchema::EMPTY
}),
..fdecl::Component::EMPTY
}
).unwrap()
),
"foo-with-config.cvf" => vfs::file::vmo::asynchronous::read_only_const(
&encode_persistent_with_context(
&fidl::encoding::Context {
wire_format_version: fidl::encoding::WireFormatVersion::V2
},
&mut fconfig::ValuesData {
..fconfig::ValuesData::EMPTY
}
).unwrap()
),
"foo-with-bad-config.cm" => vfs::file::vmo::asynchronous::read_only_const(
&encode_persistent_with_context(
&fidl::encoding::Context {
wire_format_version: fidl::encoding::WireFormatVersion::V2
},
&mut fdecl::Component {
config: Some(fdecl::ConfigSchema {
..fdecl::ConfigSchema::EMPTY
}),
..fdecl::Component::EMPTY
}
).unwrap()
),
"foo-without-config.cm" => vfs::file::vmo::asynchronous::read_only_const(
&encode_persistent_with_context(
&fidl::encoding::Context {
wire_format_version: fidl::encoding::WireFormatVersion::V2
},
&mut fdecl::Component {
config: Some(fdecl::ConfigSchema {
value_source: Some(
fdecl::ConfigValueSource::PackagePath(
"doesnt-exist.cvf".to_string(),
),
),
..fdecl::ConfigSchema::EMPTY
}),
..fdecl::Component::EMPTY
}
).unwrap()
),
"vmo.cm" => vfs::file::vmo::asynchronous::read_only_const(&cm_bytes),
}
}
}
fn build_fake_subpackage_dir() -> Arc<dyn vfs::directory::entry::DirectoryEntry> {
let cm_bytes = encode_persistent_with_context(
&fidl::encoding::Context { wire_format_version: fidl::encoding::WireFormatVersion::V2 },
&mut fdecl::Component::EMPTY.clone(),
)
.expect("failed to encode ComponentDecl FIDL");
vfs::pseudo_directory! {
"meta" => vfs::pseudo_directory! {
"subfoo.cm" => vfs::file::vmo::asynchronous::read_only_const(&cm_bytes),
}
}
}
}