| // 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::{ |
| capability::{BuiltinCapability, CapabilityProvider, InternalCapabilityProvider}, |
| model::{ |
| component::WeakComponentInstance, |
| resolver::{self, Resolver}, |
| }, |
| }, |
| anyhow::{format_err, Error}, |
| async_trait::async_trait, |
| fidl::endpoints::{ClientEnd, Proxy, ServerEnd}, |
| fidl_fuchsia_component_decl as fdecl, fidl_fuchsia_component_resolution as fresolution, |
| fidl_fuchsia_io as fio, |
| fuchsia_pkg::PackagePath, |
| fuchsia_url::{boot_url::BootUrl, PackageName, PackageVariant}, |
| fuchsia_zircon as zx, |
| futures::TryStreamExt, |
| routing::capability_source::InternalCapability, |
| routing::resolving::{ComponentAddress, ResolvedComponent, ResolverError}, |
| std::path::Path, |
| std::str::FromStr, |
| std::sync::Arc, |
| system_image::{Bootfs, PathHashMapping}, |
| version_history::AbiRevision, |
| }; |
| |
| pub static SCHEME: &str = "fuchsia-boot"; |
| |
| /// The path for the bootfs package index relative to root of |
| /// the /boot directory. |
| pub static BOOT_PACKAGE_INDEX: &str = "data/bootfs_packages"; |
| |
| /// The subdirectory of /boot that holds all merkle-root named |
| /// blobs used by package resolution. |
| static BOOTFS_BLOB_DIR: &str = "blob"; |
| |
| /// 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. |
| /// |
| /// For unit and integration tests, the /pkg directory in component_manager's namespace may be used |
| /// to load components. |
| /// |
| /// URL syntax: |
| /// - fuchsia-boot:///path/within/bootfs#meta/component.cm |
| #[derive(Debug)] |
| pub struct FuchsiaBootResolver { |
| boot_proxy: fio::DirectoryProxy, |
| boot_package_resolver: Option<BootPackageResolver>, |
| } |
| |
| impl FuchsiaBootResolver { |
| /// Create a new FuchsiaBootResolver. This first checks whether the path passed in is present in |
| /// the namespace, and returns Ok(None) if not present. For unit and integration tests, this |
| /// path may point to /pkg. |
| async fn new(path: &'static str) -> Result<Option<Arc<Self>>, Error> { |
| let bootfs_dir = Path::new(path); |
| |
| // TODO(97517): Remove this check if there is never a case for starting component manager |
| // without a /boot dir in namespace. |
| if !bootfs_dir.exists() { |
| return Ok(None); |
| } |
| |
| let boot_proxy = fuchsia_fs::directory::open_in_namespace( |
| bootfs_dir.to_str().unwrap(), |
| fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::RIGHT_EXECUTABLE, |
| )?; |
| |
| Ok(Some(Self::new_from_directory(boot_proxy).await?)) |
| } |
| |
| /// Create a new FuchsiaBootResolver that resolves URLs within the given directory. Used for |
| /// injection in unit tests. |
| async fn new_from_directory(proxy: fio::DirectoryProxy) -> Result<Arc<Self>, Error> { |
| let boot_package_resolver = BootPackageResolver::try_instantiate(&proxy).await?; |
| |
| Ok(Arc::new(Self { boot_proxy: proxy, boot_package_resolver })) |
| } |
| |
| async fn resolve_unpackaged_component( |
| &self, |
| boot_url: BootUrl, |
| ) -> Result<fresolution::Component, fresolution::ResolverError> { |
| // When a component is unpacked, the root of its namespace is the root |
| // of the /boot directory. |
| let namespace_root = "."; |
| |
| // Set up the fuchsia-boot path as the component's "package" namespace. |
| let path_proxy = fuchsia_fs::directory::open_directory_no_describe( |
| &self.boot_proxy, |
| namespace_root, |
| fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::RIGHT_EXECUTABLE, |
| ) |
| .map_err(|_| fresolution::ResolverError::Internal)?; |
| |
| // unpackaged components resolved from the zbi are assigned the platform |
| // abi revision |
| let abi_revision = version_history::HISTORY.get_abi_revision_for_platform_components(); |
| |
| self.construct_component(path_proxy, boot_url, Some(abi_revision)).await |
| } |
| |
| async fn resolve_packaged_component( |
| &self, |
| boot_url: BootUrl, |
| ) -> Result<fresolution::Component, fresolution::ResolverError> { |
| // Package path is 'canonicalized' to ensure that it is relative, since absolute paths will |
| // be (inconsistently) rejected by fuchsia.io methods. |
| let canonicalized_package_path = fuchsia_fs::canonicalize_path(boot_url.path()); |
| match &self.boot_package_resolver { |
| Some(boot_package_resolver) => { |
| let package_dir_proxy = |
| boot_package_resolver.setup_package_dir(canonicalized_package_path).await?; |
| // TODO(97517): when all bootfs components are packaged, abi_revision setting can be moved |
| // into `construct_component()`. |
| let abi_revision = fidl_fuchsia_component_abi_ext::read_abi_revision_optional( |
| &package_dir_proxy, |
| AbiRevision::PATH, |
| ) |
| .await?; |
| self.construct_component(package_dir_proxy, boot_url, abi_revision).await |
| } |
| _ => { |
| tracing::warn!( |
| "Encountered a packaged bootfs component, but bootfs has no package index: {:?}", |
| canonicalized_package_path); |
| return Err(fresolution::ResolverError::PackageNotFound); |
| } |
| } |
| } |
| |
| async fn construct_component( |
| &self, |
| proxy: fio::DirectoryProxy, |
| boot_url: BootUrl, |
| abi_revision: Option<AbiRevision>, |
| ) -> Result<fresolution::Component, fresolution::ResolverError> { |
| let manifest = boot_url.resource().ok_or(fresolution::ResolverError::InvalidArgs)?; |
| |
| // Read the component manifest (.cm file) from the package-root. |
| let data = mem_util::open_file_data(&proxy, &manifest) |
| .await |
| .map_err(|_| fresolution::ResolverError::ManifestNotFound)?; |
| |
| let decl_bytes = |
| mem_util::bytes_from_data(&data).map_err(|_| fresolution::ResolverError::Io)?; |
| |
| let decl: fdecl::Component = fidl::unpersist(&decl_bytes[..]) |
| .map_err(|_| fresolution::ResolverError::InvalidManifest)?; |
| |
| let config_values = if let Some(config_decl) = decl.config.as_ref() { |
| let strategy = config_decl |
| .value_source |
| .as_ref() |
| .ok_or(fresolution::ResolverError::InvalidManifest)?; |
| match strategy { |
| // If we have to read the source from a package, do so. |
| fdecl::ConfigValueSource::PackagePath(path) => Some( |
| mem_util::open_file_data(&proxy, path) |
| .await |
| .map_err(|_| fresolution::ResolverError::ConfigValuesNotFound)?, |
| ), |
| // We don't have to do anything for capability routing. |
| fdecl::ConfigValueSource::Capabilities(_) => None, |
| fdecl::ConfigValueSourceUnknown!() => { |
| return Err(fresolution::ResolverError::InvalidManifest); |
| } |
| } |
| } else { |
| None |
| }; |
| Ok(fresolution::Component { |
| url: Some(boot_url.to_string().into()), |
| resolution_context: None, |
| decl: Some(data), |
| package: Some(fresolution::Package { |
| // This call just strips the boot_url of the resource. |
| url: Some(boot_url.root_url().to_string()), |
| directory: Some(ClientEnd::new(proxy.into_channel().unwrap().into_zx_channel())), |
| ..Default::default() |
| }), |
| config_values, |
| abi_revision: abi_revision.map(Into::into), |
| ..Default::default() |
| }) |
| } |
| |
| async fn resolve_async( |
| &self, |
| component_url: &str, |
| ) -> Result<fresolution::Component, fresolution::ResolverError> { |
| // Parse URL. |
| let url = |
| BootUrl::parse(component_url).map_err(|_| fresolution::ResolverError::InvalidArgs)?; |
| // Package path is 'canonicalized' to ensure that it is relative, since absolute paths will |
| // be (inconsistently) rejected by fuchsia.io methods. |
| let canonicalized_path = fuchsia_fs::canonicalize_path(url.path()); |
| |
| match canonicalized_path { |
| "." => { |
| return self.resolve_unpackaged_component(url).await; |
| } |
| _ => { |
| return self.resolve_packaged_component(url).await; |
| } |
| } |
| } |
| |
| pub async fn serve( |
| self: Arc<Self>, |
| mut stream: fresolution::ResolverRequestStream, |
| ) -> Result<(), Error> { |
| while let Some(request) = stream.try_next().await? { |
| match request { |
| fresolution::ResolverRequest::Resolve { component_url, responder } => { |
| responder.send(self.resolve_async(&component_url).await)?; |
| } |
| fresolution::ResolverRequest::ResolveWithContext { |
| component_url, |
| context: _, |
| responder, |
| } => { |
| // FuchsiaBootResolver ResolveWithContext currently ignores |
| // context, but should still resolve absolute URLs. |
| responder.send(self.resolve_async(&component_url).await)?; |
| } |
| } |
| } |
| Ok(()) |
| } |
| } |
| |
| pub struct FuchsiaBootResolverBuiltinCapability { |
| host: Arc<FuchsiaBootResolver>, |
| } |
| |
| impl FuchsiaBootResolverBuiltinCapability { |
| pub async fn new(path: &'static str) -> Result<Option<Self>, Error> { |
| Ok(FuchsiaBootResolver::new(path).await?.map(|host| Self { host })) |
| } |
| |
| pub fn host(&self) -> &Arc<FuchsiaBootResolver> { |
| &self.host |
| } |
| } |
| |
| impl BuiltinCapability for FuchsiaBootResolverBuiltinCapability { |
| fn matches(&self, capability: &InternalCapability) -> bool { |
| matches!(capability, InternalCapability::Resolver(n) if n.as_str() == "boot_resolver") |
| } |
| |
| fn new_provider(&self, _target: WeakComponentInstance) -> Box<dyn CapabilityProvider> { |
| Box::new(ComponentResolverCapabilityProvider::new(self.host.clone())) |
| } |
| } |
| |
| struct ComponentResolverCapabilityProvider { |
| component_resolver: Arc<FuchsiaBootResolver>, |
| } |
| |
| impl ComponentResolverCapabilityProvider { |
| pub fn new(component_resolver: Arc<FuchsiaBootResolver>) -> Self { |
| Self { component_resolver } |
| } |
| } |
| |
| #[async_trait] |
| impl InternalCapabilityProvider for ComponentResolverCapabilityProvider { |
| async fn open_protocol(self: Box<Self>, server_end: zx::Channel) { |
| let server_end = ServerEnd::<fresolution::ResolverMarker>::new(server_end); |
| if let Err(error) = self.component_resolver.serve(server_end.into_stream().unwrap()).await { |
| tracing::warn!(%error, "FuchsiaBootResolver::serve failed"); |
| } |
| } |
| } |
| |
| #[derive(Debug)] |
| struct BootPackageResolver { |
| // Blobfs client exposing the bootfs |
| // /boot/blob directory to package-directory interface. |
| // TODO(97517): Refactor to an impl of NonMetaStorage. |
| boot_blob_storage: fio::DirectoryProxy, |
| // PathHashMapping encoding the index for boot package resolution. |
| boot_package_index: PathHashMapping<Bootfs>, |
| } |
| |
| impl BootPackageResolver { |
| // Attempts to instantiate a BootPackageResolver. |
| // |
| // - The absence of a /boot/blob dir implies that there are no packages in the BootFS, |
| // and boot resolver setup should still succeed. |
| // |
| // - The presence of a /boot/blob dir, but absence of a package index implies incorrect |
| // bootfs assembly, and produces a FuchsiaBootResolver instantiation error. |
| async fn try_instantiate(proxy: &fio::DirectoryProxy) -> Result<Option<Self>, Error> { |
| // Check for the existence of a /boot/blob directory. Until we've started our migration, |
| // it's a valid state for no packages to exist in the bootfs, in which case no blobs will |
| // exist. |
| if !fuchsia_fs::directory::dir_contains(proxy, BOOTFS_BLOB_DIR).await? { |
| return Ok(None); |
| } |
| |
| let boot_blob_storage = fuchsia_fs::directory::open_directory_no_describe( |
| &proxy, |
| BOOTFS_BLOB_DIR, |
| fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::RIGHT_EXECUTABLE, |
| ) |
| .map_err(|err| format_err!("Bootfs blob directory existed, but converting it into a blob client for package resolution failed: {:?}", err))?; |
| |
| let boot_package_index = |
| BootPackageResolver::extract_bootfs_index(&proxy).await.map_err(|err| { |
| format_err!( |
| "Failed to extract a package index from a bootfs that contains packages: {:?}", |
| err |
| ) |
| })?; |
| |
| Ok(Some(BootPackageResolver { boot_blob_storage, boot_package_index })) |
| } |
| |
| /// Load `data/bootfs_packages` from /boot, if present. |
| async fn extract_bootfs_index( |
| boot_proxy: &fio::DirectoryProxy, |
| ) -> Result<PathHashMapping<Bootfs>, Error> { |
| let bootfs_package_index = fuchsia_fs::directory::open_file_no_describe( |
| &boot_proxy, |
| BOOT_PACKAGE_INDEX, |
| fio::OpenFlags::RIGHT_READABLE, |
| )?; |
| |
| let bootfs_package_contents = fuchsia_fs::file::read(&bootfs_package_index).await?; |
| |
| PathHashMapping::<Bootfs>::deserialize(&(*bootfs_package_contents)) |
| .map_err(|e| format_err!("Parsing bootfs index failed: {:?}", e)) |
| } |
| |
| async fn setup_package_dir( |
| &self, |
| canonicalized_package_path: &str, |
| ) -> Result<fio::DirectoryProxy, fresolution::ResolverError> { |
| let package_path = match PackageName::from_str(canonicalized_package_path) { |
| Ok(package_name) => { |
| PackagePath::from_name_and_variant(package_name, PackageVariant::zero()) |
| } |
| Err(e) => { |
| tracing::warn!("Bootfs package paths should be a single named segment: {:?}", e); |
| return Err(fresolution::ResolverError::InvalidArgs); |
| } |
| }; |
| |
| let meta_hash = self |
| .boot_package_index |
| .hash_for_package(&package_path) |
| .ok_or(fresolution::ResolverError::PackageNotFound)?; |
| |
| let (proxy, server) = fidl::endpoints::create_proxy() |
| .map_err(|_| fresolution::ResolverError::InvalidManifest)?; |
| |
| let blob_proxy = fuchsia_fs::directory::clone_no_describe(&self.boot_blob_storage, None) |
| .map_err(|e| { |
| tracing::warn!( |
| "Creating duplicate connection to /boot/blob directory failed: {:?}", |
| e |
| ); |
| fresolution::ResolverError::Internal |
| })?; |
| |
| let () = package_directory::serve( |
| // scope is used to spawn an async task, which will continue until all features complete. |
| package_directory::ExecutionScope::new(), |
| blob_proxy, |
| meta_hash, |
| fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::RIGHT_EXECUTABLE, |
| server, |
| ) |
| .await |
| .map_err(|_| fresolution::ResolverError::Internal)?; |
| |
| Ok(proxy) |
| } |
| } |
| |
| #[async_trait] |
| impl Resolver for FuchsiaBootResolver { |
| async fn resolve( |
| &self, |
| component_address: &ComponentAddress, |
| ) -> Result<ResolvedComponent, ResolverError> { |
| if component_address.is_relative_path() { |
| return Err(ResolverError::UnexpectedRelativePath(component_address.url().to_string())); |
| } |
| let fresolution::Component { url, decl, package, config_values, abi_revision, .. } = |
| self.resolve_async(component_address.url()).await?; |
| let resolved_url = url.unwrap(); |
| let decl = decl.ok_or_else(|| { |
| ResolverError::ManifestInvalid( |
| anyhow::format_err!("missing manifest from resolved component").into(), |
| ) |
| })?; |
| let decl = resolver::read_and_validate_manifest(&decl)?; |
| let config_values = if let Some(cv) = config_values { |
| Some(resolver::read_and_validate_config_values(&cv)?) |
| } else { |
| None |
| }; |
| Ok(ResolvedComponent { |
| resolved_url, |
| context_to_resolve_children: None, |
| decl, |
| package: package.map(|p| p.try_into()).transpose()?, |
| config_values, |
| abi_revision: abi_revision.map(Into::into), |
| }) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use { |
| super::*, |
| crate::model::{ |
| component::ComponentInstance, context::ModelContext, environment::Environment, |
| }, |
| ::routing::resolving::ResolvedPackage, |
| assert_matches::assert_matches, |
| cm_rust::{FidlIntoNative, NativeIntoFidl}, |
| cm_util::TaskGroup, |
| fidl::endpoints::{create_endpoints, create_proxy}, |
| fidl::persist, |
| fidl_fuchsia_component_decl as fdecl, fidl_fuchsia_data as fdata, |
| fuchsia_async::Task, |
| fuchsia_fs::directory::open_in_namespace, |
| std::sync::Weak, |
| vfs::{ |
| directory::{entry::OpenRequest, entry_container::Directory}, |
| execution_scope::ExecutionScope, |
| file::vmo::read_only, |
| path::Path as VfsPath, |
| pseudo_directory, ToObjectRequest, |
| }, |
| }; |
| |
| fn serve_vfs_dir(root: Arc<impl Directory>) -> (Task<()>, fio::DirectoryProxy) { |
| let fs_scope = ExecutionScope::new(); |
| let (client, server) = create_proxy::<fio::DirectoryMarker>().unwrap(); |
| root.open( |
| fs_scope.clone(), |
| fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::RIGHT_EXECUTABLE, |
| VfsPath::dot(), |
| ServerEnd::new(server.into_channel()), |
| ); |
| |
| let vfs_task = Task::spawn(async move { fs_scope.wait().await }); |
| |
| (vfs_task, client) |
| } |
| |
| #[fuchsia::test] |
| async fn hello_world_test() -> Result<(), Error> { |
| let bootfs = open_in_namespace( |
| "/pkg", |
| fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::RIGHT_EXECUTABLE, |
| ) |
| .unwrap(); |
| let resolver = FuchsiaBootResolver::new_from_directory(bootfs).await.unwrap(); |
| |
| let url = "fuchsia-boot:///#meta/hello-world-rust.cm".parse().unwrap(); |
| let component = resolver.resolve(&ComponentAddress::from_absolute_url(&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 ResolvedComponent { resolved_url, decl, package, abi_revision, .. } = component; |
| assert_eq!(url, resolved_url); |
| version_history::HISTORY |
| .check_abi_revision_for_runtime( |
| abi_revision.expect("boot component should present ABI revision"), |
| ) |
| .expect("ABI revision should be supported for boot component"); |
| |
| let expected_program = Some(cm_rust::ProgramDecl { |
| runner: Some("elf".parse().unwrap()), |
| 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()))), |
| }, |
| ]), |
| ..Default::default() |
| }, |
| }); |
| |
| // 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-boot:///"); |
| |
| let dir_proxy = package_dir.into_proxy().unwrap(); |
| let path = "meta/hello-world-rust.cm"; |
| let file_proxy = fuchsia_fs::directory::open_file_no_describe( |
| &dir_proxy, |
| path, |
| fio::OpenFlags::RIGHT_READABLE, |
| ) |
| .expect("could not open cm"); |
| |
| let decl = fuchsia_fs::file::read_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 VmoFlags::EXECUTE. |
| library_loader::load_vmo(&dir_proxy, "bin/hello_world_rust") |
| .await |
| .expect("failed to open executable file"); |
| |
| let url = "fuchsia-boot:///contains/a/package#meta/hello-world-rust.cm".parse().unwrap(); |
| let err = resolver.resolve(&ComponentAddress::from_absolute_url(&url)?).await.unwrap_err(); |
| assert_matches!(err, ResolverError::PackageNotFound { .. }); |
| Ok(()) |
| } |
| |
| #[fuchsia::test] |
| async fn capability_provider_test() { |
| // Create a CapabilityProvider to serve fuchsia boot resolver requests. |
| let bootfs = open_in_namespace( |
| "/pkg", |
| fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::RIGHT_EXECUTABLE, |
| ) |
| .unwrap(); |
| let resolver = FuchsiaBootResolver::new_from_directory(bootfs).await.unwrap(); |
| let resolver_provider = |
| Box::new(ComponentResolverCapabilityProvider::new(resolver.clone())); |
| let (client_channel, server_channel) = create_endpoints::<fresolution::ResolverMarker>(); |
| let task_group = TaskGroup::new(); |
| let scope = ExecutionScope::new(); |
| let mut object_request = fio::OpenFlags::empty().to_object_request(server_channel); |
| resolver_provider |
| .open( |
| task_group.clone(), |
| OpenRequest::new( |
| scope.clone(), |
| fio::OpenFlags::empty(), |
| VfsPath::dot(), |
| &mut object_request, |
| ), |
| ) |
| .await |
| .expect("failed to open capability"); |
| // Create a client-side resolver proxy to submit resolve requests with. |
| let resolver_proxy = |
| client_channel.into_proxy().expect("failed converting endpoint into proxy"); |
| // Test that the client resolve request is served by the CapabilityProvider successfully. |
| assert!(resolver_proxy.resolve("fuchsia-boot:///#meta/hello-world-rust.cm").await.is_ok()); |
| } |
| |
| #[fuchsia::test] |
| async fn config_works() { |
| let fake_checksum = cm_rust::ConfigChecksum::Sha256([0; 32]); |
| let manifest = fdecl::Component { |
| config: Some( |
| cm_rust::ConfigDecl { |
| value_source: cm_rust::ConfigValueSource::PackagePath( |
| "meta/has_config.cvf".to_string(), |
| ), |
| fields: vec![cm_rust::ConfigField { |
| key: "foo".to_string(), |
| type_: cm_rust::ConfigValueType::String { max_size: 100 }, |
| mutability: Default::default(), |
| }], |
| checksum: fake_checksum.clone(), |
| } |
| .native_into_fidl(), |
| ), |
| ..Default::default() |
| }; |
| let values_data = fdecl::ConfigValuesData { |
| values: Some(vec![fdecl::ConfigValueSpec { |
| value: Some(fdecl::ConfigValue::Single(fdecl::ConfigSingleValue::String( |
| "hello, world!".to_string(), |
| ))), |
| ..Default::default() |
| }]), |
| checksum: Some(fake_checksum.clone().native_into_fidl()), |
| ..Default::default() |
| }; |
| let manifest_encoded = persist(&manifest).unwrap(); |
| let values_data_encoded = persist(&values_data).unwrap(); |
| let root = pseudo_directory! { |
| "meta" => pseudo_directory! { |
| "has_config.cm" => read_only(manifest_encoded), |
| "has_config.cvf" => read_only(values_data_encoded), |
| } |
| }; |
| let (_task, bootfs) = serve_vfs_dir(root); |
| let resolver = FuchsiaBootResolver::new_from_directory(bootfs).await.unwrap(); |
| |
| let url = "fuchsia-boot:///#meta/has_config.cm".parse().unwrap(); |
| let component = |
| resolver.resolve(&ComponentAddress::from_absolute_url(&url).unwrap()).await.unwrap(); |
| |
| let ResolvedComponent { resolved_url, decl, config_values, .. } = component; |
| assert_eq!(url, resolved_url); |
| |
| let config_decl = decl.config.unwrap(); |
| let config_values = config_values.unwrap(); |
| |
| let observed_fields = |
| config_encoder::ConfigFields::resolve(&config_decl, config_values, None).unwrap(); |
| let expected_fields = config_encoder::ConfigFields { |
| fields: vec![config_encoder::ConfigField { |
| key: "foo".to_string(), |
| value: cm_rust::ConfigValue::Single(cm_rust::ConfigSingleValue::String( |
| "hello, world!".to_string(), |
| )), |
| mutability: Default::default(), |
| }], |
| checksum: fake_checksum, |
| }; |
| assert_eq!(observed_fields, expected_fields); |
| } |
| |
| #[fuchsia::test] |
| async fn config_requires_values() { |
| let manifest = fdecl::Component { |
| config: Some( |
| cm_rust::ConfigDecl { |
| value_source: cm_rust::ConfigValueSource::PackagePath( |
| "meta/has_config.cvf".to_string(), |
| ), |
| fields: vec![cm_rust::ConfigField { |
| key: "foo".to_string(), |
| type_: cm_rust::ConfigValueType::String { max_size: 100 }, |
| mutability: Default::default(), |
| }], |
| checksum: cm_rust::ConfigChecksum::Sha256([0; 32]), |
| } |
| .native_into_fidl(), |
| ), |
| ..Default::default() |
| }; |
| let manifest_encoded = persist(&manifest).unwrap(); |
| let root = pseudo_directory! { |
| "meta" => pseudo_directory! { |
| "has_config.cm" => read_only(manifest_encoded), |
| } |
| }; |
| let (_task, bootfs) = serve_vfs_dir(root); |
| let resolver = FuchsiaBootResolver::new_from_directory(bootfs).await.unwrap(); |
| |
| let root = ComponentInstance::new_root( |
| Environment::empty(), |
| Arc::new(ModelContext::new_for_test()), |
| Weak::new(), |
| "fuchsia-boot:///#meta/root.cm".parse().unwrap(), |
| ) |
| .await; |
| |
| let url = "fuchsia-boot:///#meta/has_config.cm".parse().unwrap(); |
| let err = resolver |
| .resolve(&ComponentAddress::from(&url, &root).await.unwrap()) |
| .await |
| .unwrap_err(); |
| assert_matches!(err, ResolverError::ConfigValuesIo { .. }); |
| } |
| |
| macro_rules! test_resolve_error { |
| ($resolver:ident, $url:expr, $target:ident, $resolver_error_expected:ident) => { |
| let url = $url.parse().unwrap(); |
| let res = |
| $resolver.resolve(&ComponentAddress::from(&url, &$target).await.unwrap()).await; |
| match res.err().expect("unexpected success") { |
| ResolverError::$resolver_error_expected { .. } => {} |
| e => panic!("unexpected error {:?}", e), |
| } |
| }; |
| } |
| |
| #[fuchsia::test] |
| async fn resolve_errors_test() { |
| let manifest_encoded = persist(&fdecl::Component { |
| program: Some(fdecl::Program { |
| runner: None, |
| info: Some(fdata::Dictionary { entries: Some(vec![]), ..Default::default() }), |
| ..Default::default() |
| }), |
| ..Default::default() |
| }) |
| .unwrap(); |
| let root = pseudo_directory! { |
| "meta" => pseudo_directory! { |
| // Provide a cm that will fail due to a missing runner. |
| "invalid.cm" => read_only(manifest_encoded), |
| }, |
| }; |
| let (_task, bootfs) = serve_vfs_dir(root); |
| let resolver = FuchsiaBootResolver::new_from_directory(bootfs).await.unwrap(); |
| let root = ComponentInstance::new_root( |
| Environment::empty(), |
| Arc::new(ModelContext::new_for_test()), |
| Weak::new(), |
| "fuchsia-boot:///#meta/root.cm".parse().unwrap(), |
| ) |
| .await; |
| test_resolve_error!(resolver, "fuchsia-boot:///#meta/invalid.cm", root, ManifestInvalid); |
| } |
| } |