blob: 5846502cdf57f3c211d9cb80e8af5c774f37858d [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::common_utils::common::get_proxy_or_connect;
use anyhow::{bail, Error};
use fidl_fuchsia_paver::{PaverMarker, PaverProxy};
use fuchsia_zircon::Status;
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use super::types::{Asset, Configuration, ConfigurationStatus};
/// Facade providing access to paver service.
#[derive(Debug)]
pub struct PaverFacade {
proxy: RwLock<Option<PaverProxy>>,
}
impl PaverFacade {
/// Creates a new [PaverFacade] with no active connection to the paver service.
pub fn new() -> Self {
Self { proxy: RwLock::new(None) }
}
#[cfg(test)]
fn new_with_proxy(proxy: PaverProxy) -> Self {
Self { proxy: RwLock::new(Some(proxy)) }
}
/// Return a cached connection to the paver service, or try to connect and cache the connection
/// for later.
fn proxy(&self) -> Result<PaverProxy, Error> {
get_proxy_or_connect::<PaverMarker>(&self.proxy)
}
/// Queries the active boot configuration, if the current bootloader supports it.
///
/// # Errors
///
/// Returns an Err(_) if
/// * connecting to the paver service fails, or
/// * the paver service returns an unexpected error
pub(super) async fn query_active_configuration(
&self,
) -> Result<QueryActiveConfigurationResult, Error> {
let (boot_manager, boot_manager_server_end) = fidl::endpoints::create_proxy()?;
self.proxy()?.find_boot_manager(boot_manager_server_end)?;
match boot_manager.query_active_configuration().await {
Ok(Ok(config)) => Ok(QueryActiveConfigurationResult::Success(config.into())),
Ok(Err(err)) => bail!("unexpected failure status: {}", err),
Err(fidl::Error::ClientChannelClosed { status: Status::NOT_SUPPORTED, .. }) => {
Ok(QueryActiveConfigurationResult::NotSupported)
}
Err(err) => bail!("unexpected failure status: {}", err),
}
}
/// Queries the current boot configuration, if the current bootloader supports it.
///
/// # Errors
///
/// Returns an Err(_) if
/// * connecting to the paver service fails, or
/// * the paver service returns an unexpected error
pub(super) async fn query_current_configuration(
&self,
) -> Result<QueryCurrentConfigurationResult, Error> {
let (boot_manager, boot_manager_server_end) = fidl::endpoints::create_proxy()?;
self.proxy()?.find_boot_manager(boot_manager_server_end)?;
match boot_manager.query_current_configuration().await {
Ok(Ok(config)) => Ok(QueryCurrentConfigurationResult::Success(config.into())),
Ok(Err(err)) => bail!("unexpected failure status: {}", err),
Err(fidl::Error::ClientChannelClosed { status: Status::NOT_SUPPORTED, .. }) => {
Ok(QueryCurrentConfigurationResult::NotSupported)
}
Err(err) => bail!("unexpected failure status: {}", err),
}
}
/// Queries the bootable status of the given configuration, if the current bootloader supports
/// it.
///
/// # Errors
///
/// Returns an Err(_) if
/// * connecting to the paver service fails, or
/// * the paver service returns an unexpected error
pub(super) async fn query_configuration_status(
&self,
args: QueryConfigurationStatusRequest,
) -> Result<QueryConfigurationStatusResult, Error> {
let (boot_manager, boot_manager_server_end) = fidl::endpoints::create_proxy()?;
self.proxy()?.find_boot_manager(boot_manager_server_end)?;
match boot_manager.query_configuration_status(args.configuration.into()).await {
Ok(Ok(status)) => Ok(QueryConfigurationStatusResult::Success(status.into())),
Ok(Err(err)) => bail!("unexpected failure status: {}", err),
Err(fidl::Error::ClientChannelClosed { status: Status::NOT_SUPPORTED, .. }) => {
Ok(QueryConfigurationStatusResult::NotSupported)
}
Err(err) => bail!("unexpected failure status: {}", err),
}
}
/// Given a configuration and asset identifier, read that image and return it as a base64
/// encoded String.
///
/// # Errors
///
/// Returns an Err(_) if
/// * connecting to the paver service fails, or
/// * the paver service returns an unexpected error
pub(super) async fn read_asset(&self, args: ReadAssetRequest) -> Result<String, Error> {
let (data_sink, data_sink_server_end) = fidl::endpoints::create_proxy()?;
self.proxy()?.find_data_sink(data_sink_server_end)?;
let buffer = data_sink
.read_asset(args.configuration.into(), args.asset.into())
.await?
.map_err(Status::from_raw)?;
let mut res = vec![0; buffer.size as usize];
buffer.vmo.read(&mut res[..], 0)?;
Ok(base64::encode(&res))
}
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub(super) enum QueryActiveConfigurationResult {
Success(Configuration),
NotSupported,
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub(super) enum QueryCurrentConfigurationResult {
Success(Configuration),
NotSupported,
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
pub(super) struct QueryConfigurationStatusRequest {
configuration: Configuration,
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub(super) enum QueryConfigurationStatusResult {
Success(ConfigurationStatus),
NotSupported,
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
pub(super) struct ReadAssetRequest {
configuration: Configuration,
asset: Asset,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::common_utils::test::assert_value_round_trips_as;
use fidl_fuchsia_paver::{
BootManagerRequest, BootManagerRequestStream, DataSinkRequest, DataSinkRequestStream,
PaverRequest,
};
use futures::{future::Future, join, stream::StreamExt};
use matches::assert_matches;
use serde_json::json;
#[test]
fn serde_query_active_configuration_result() {
assert_value_round_trips_as(
QueryActiveConfigurationResult::NotSupported,
json!("not_supported"),
);
assert_value_round_trips_as(
QueryActiveConfigurationResult::Success(Configuration::A),
json!({"success": "a"}),
);
}
#[test]
fn serde_query_current_configuration_result() {
assert_value_round_trips_as(
QueryCurrentConfigurationResult::NotSupported,
json!("not_supported"),
);
assert_value_round_trips_as(
QueryCurrentConfigurationResult::Success(Configuration::A),
json!({"success": "a"}),
);
}
#[test]
fn serde_query_configuration_status_result() {
assert_value_round_trips_as(
QueryConfigurationStatusResult::NotSupported,
json!("not_supported"),
);
assert_value_round_trips_as(
QueryConfigurationStatusResult::Success(ConfigurationStatus::Healthy),
json!({"success": "healthy"}),
);
}
#[test]
fn serde_query_configuration_request() {
assert_value_round_trips_as(
QueryConfigurationStatusRequest { configuration: Configuration::Recovery },
json!({"configuration": "recovery"}),
);
}
#[test]
fn serde_read_asset_request() {
assert_value_round_trips_as(
ReadAssetRequest {
configuration: Configuration::A,
asset: Asset::VerifiedBootMetadata,
},
json!({"configuration": "a", "asset": "verified_boot_metadata"}),
);
}
struct MockBootManagerBuilder {
expected: Vec<Box<dyn FnOnce(BootManagerRequest) + Send + 'static>>,
}
impl MockBootManagerBuilder {
fn new() -> Self {
Self { expected: vec![] }
}
fn push(mut self, request: impl FnOnce(BootManagerRequest) + Send + 'static) -> Self {
self.expected.push(Box::new(request));
self
}
fn expect_query_active_configuration(self, res: Result<Configuration, Status>) -> Self {
self.push(move |req| match req {
BootManagerRequest::QueryActiveConfiguration { responder } => {
responder.send(&mut res.map(Into::into).map_err(|e| e.into_raw())).unwrap()
}
req => panic!("unexpected request: {:?}", req),
})
}
fn expect_query_current_configuration(self, res: Result<Configuration, Status>) -> Self {
self.push(move |req| match req {
BootManagerRequest::QueryCurrentConfiguration { responder } => {
responder.send(&mut res.map(Into::into).map_err(|e| e.into_raw())).unwrap()
}
req => panic!("unexpected request: {:?}", req),
})
}
fn expect_query_configuration_status(
self,
config: Configuration,
res: Result<ConfigurationStatus, Status>,
) -> Self {
self.push(move |req| match req {
BootManagerRequest::QueryConfigurationStatus { configuration, responder } => {
assert_eq!(Configuration::from(configuration), config);
responder.send(&mut res.map(Into::into).map_err(|e| e.into_raw())).unwrap()
}
req => panic!("unexpected request: {:?}", req),
})
}
fn build(self, mut stream: BootManagerRequestStream) -> impl Future<Output = ()> {
async move {
for expected in self.expected {
expected(stream.next().await.unwrap().unwrap());
}
assert_matches!(stream.next().await, None);
}
}
}
struct MockDataSinkBuilder {
expected: Vec<Box<dyn FnOnce(DataSinkRequest) + Send + 'static>>,
}
impl MockDataSinkBuilder {
fn new() -> Self {
Self { expected: vec![] }
}
fn push(mut self, request: impl FnOnce(DataSinkRequest) + Send + 'static) -> Self {
self.expected.push(Box::new(request));
self
}
fn expect_read_asset(
self,
expected_request: ReadAssetRequest,
response: &'static [u8],
) -> Self {
let buf = fidl_fuchsia_mem::Buffer {
vmo: fuchsia_zircon::Vmo::create(response.len() as u64).unwrap(),
size: response.len() as u64,
};
buf.vmo.write(response, 0).unwrap();
self.push(move |req| match req {
DataSinkRequest::ReadAsset { configuration, asset, responder } => {
let request = ReadAssetRequest {
configuration: configuration.into(),
asset: asset.into(),
};
assert_eq!(request, expected_request);
responder.send(&mut Ok(buf)).unwrap()
}
req => panic!("unexpected request: {:?}", req),
})
}
fn build(self, mut stream: DataSinkRequestStream) -> impl Future<Output = ()> {
async move {
for expected in self.expected {
expected(stream.next().await.unwrap().unwrap());
}
assert_matches!(stream.next().await, None);
}
}
}
struct MockPaverBuilder {
expected: Vec<Box<dyn FnOnce(PaverRequest) + 'static>>,
}
impl MockPaverBuilder {
fn new() -> Self {
Self { expected: vec![] }
}
fn push(mut self, request: impl FnOnce(PaverRequest) + 'static) -> Self {
self.expected.push(Box::new(request));
self
}
fn expect_find_boot_manager(self, mock: Option<MockBootManagerBuilder>) -> Self {
self.push(move |req| match req {
PaverRequest::FindBootManager { boot_manager, .. } => {
if let Some(mock) = mock {
let stream = boot_manager.into_stream().unwrap();
fuchsia_async::Task::spawn(async move {
mock.build(stream).await;
})
.detach();
} else {
boot_manager.close_with_epitaph(Status::NOT_SUPPORTED).unwrap();
}
}
req => panic!("unexpected request: {:?}", req),
})
}
fn expect_find_data_sink(self, mock: MockDataSinkBuilder) -> Self {
self.push(move |req| match req {
PaverRequest::FindDataSink { data_sink, .. } => {
let stream = data_sink.into_stream().unwrap();
fuchsia_async::Task::spawn(async move {
mock.build(stream).await;
})
.detach();
}
req => panic!("unexpected request: {:?}", req),
})
}
fn build(self) -> (PaverFacade, impl Future<Output = ()>) {
let (proxy, mut stream) =
fidl::endpoints::create_proxy_and_stream::<PaverMarker>().unwrap();
let fut = async move {
for expected in self.expected {
expected(stream.next().await.unwrap().unwrap());
}
assert_matches!(stream.next().await, None);
};
(PaverFacade::new_with_proxy(proxy), fut)
}
}
#[fuchsia_async::run_singlethreaded(test)]
async fn query_active_configuration_ok() {
let (facade, paver) = MockPaverBuilder::new()
.expect_find_boot_manager(Some(
MockBootManagerBuilder::new()
.expect_query_active_configuration(Ok(Configuration::A)),
))
.expect_find_boot_manager(Some(
MockBootManagerBuilder::new()
.expect_query_active_configuration(Ok(Configuration::B)),
))
.build();
let test = async move {
assert_matches!(
facade.query_active_configuration().await,
Ok(QueryActiveConfigurationResult::Success(Configuration::A))
);
assert_matches!(
facade.query_active_configuration().await,
Ok(QueryActiveConfigurationResult::Success(Configuration::B))
);
};
join!(paver, test);
}
#[fuchsia_async::run_singlethreaded(test)]
async fn query_active_configuration_not_supported() {
let (facade, paver) = MockPaverBuilder::new().expect_find_boot_manager(None).build();
let test = async move {
assert_matches!(
facade.query_active_configuration().await,
Ok(QueryActiveConfigurationResult::NotSupported)
);
};
join!(paver, test);
}
#[fuchsia_async::run_singlethreaded(test)]
async fn query_current_configuration_ok() {
let (facade, paver) = MockPaverBuilder::new()
.expect_find_boot_manager(Some(
MockBootManagerBuilder::new()
.expect_query_current_configuration(Ok(Configuration::A)),
))
.expect_find_boot_manager(Some(
MockBootManagerBuilder::new()
.expect_query_current_configuration(Ok(Configuration::B)),
))
.build();
let test = async move {
assert_matches!(
facade.query_current_configuration().await,
Ok(QueryCurrentConfigurationResult::Success(Configuration::A))
);
assert_matches!(
facade.query_current_configuration().await,
Ok(QueryCurrentConfigurationResult::Success(Configuration::B))
);
};
join!(paver, test);
}
#[fuchsia_async::run_singlethreaded(test)]
async fn query_current_configuration_not_supported() {
let (facade, paver) = MockPaverBuilder::new().expect_find_boot_manager(None).build();
let test = async move {
assert_matches!(
facade.query_current_configuration().await,
Ok(QueryCurrentConfigurationResult::NotSupported)
);
};
join!(paver, test);
}
#[fuchsia_async::run_singlethreaded(test)]
async fn query_configuration_status_ok() {
let (facade, paver) = MockPaverBuilder::new()
.expect_find_boot_manager(Some(
MockBootManagerBuilder::new().expect_query_configuration_status(
Configuration::A,
Ok(ConfigurationStatus::Healthy),
),
))
.expect_find_boot_manager(Some(
MockBootManagerBuilder::new().expect_query_configuration_status(
Configuration::B,
Ok(ConfigurationStatus::Unbootable),
),
))
.build();
let test = async move {
assert_matches!(
facade
.query_configuration_status(QueryConfigurationStatusRequest {
configuration: Configuration::A
})
.await,
Ok(QueryConfigurationStatusResult::Success(ConfigurationStatus::Healthy))
);
assert_matches!(
facade
.query_configuration_status(QueryConfigurationStatusRequest {
configuration: Configuration::B
})
.await,
Ok(QueryConfigurationStatusResult::Success(ConfigurationStatus::Unbootable))
);
};
join!(paver, test);
}
#[fuchsia_async::run_singlethreaded(test)]
async fn query_configuration_status_not_supported() {
let (facade, paver) = MockPaverBuilder::new().expect_find_boot_manager(None).build();
let test = async move {
assert_matches!(
facade
.query_configuration_status(QueryConfigurationStatusRequest {
configuration: Configuration::A
})
.await,
Ok(QueryConfigurationStatusResult::NotSupported)
);
};
join!(paver, test);
}
#[fuchsia_async::run_singlethreaded(test)]
async fn read_asset_ok() {
const FILE_CONTENTS: &[u8] = b"hello world!";
const FILE_CONTENTS_AS_BASE64: &str = "aGVsbG8gd29ybGQh";
let request = ReadAssetRequest {
configuration: Configuration::A,
asset: Asset::VerifiedBootMetadata,
};
let (facade, paver) = MockPaverBuilder::new()
.expect_find_data_sink(
MockDataSinkBuilder::new().expect_read_asset(request.clone(), FILE_CONTENTS),
)
.build();
let test = async move {
assert_matches!(
facade.read_asset(request).await,
Ok(s) if s == FILE_CONTENTS_AS_BASE64
);
};
join!(paver, test);
}
}