use anyhow::{anyhow, Error};
use fidl::endpoints::ServerEnd;
use fidl_fuchsia_pkg::{
self as fpkg, PackageResolverMarker, PackageResolverProxy, PackageResolverRequestStream,
PackageResolverResolveResponder,
};
use fuchsia_sync::Mutex;
use futures::channel::oneshot;
use futures::prelude::*;
use std::collections::HashMap;
use std::fs::{self, create_dir};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tempfile::TempDir;
use {fidl_fuchsia_io as fio, fuchsia_async as fasync};
const PACKAGE_CONTENTS_PATH: &str = "package_contents";
const META_FAR_MERKLE_ROOT_PATH: &str = "meta";
#[derive(Debug)]
pub struct TestPackage {
root: PathBuf,
}
impl TestPackage {
fn new(root: PathBuf) -> Self {
TestPackage { root }
}
pub fn add_file(self, path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Self {
fs::write(self.root.join(PACKAGE_CONTENTS_PATH).join(path), contents)
.expect("create fake package file");
self
}
fn serve_on(&self, dir_request: ServerEnd<fio::DirectoryMarker>) {
let (backing_dir_proxy, server_end) =
fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
fuchsia_fs::directory::open_channel_in_namespace(
self.root.to_str().unwrap(),
fio::PERM_READABLE,
server_end,
)
.expect("open channel in namespace failed");
fasync::Task::spawn(handle_package_directory_stream(
dir_request.into_stream(),
backing_dir_proxy,
))
.detach();
}
}
fn should_redirect_request_to_merkle_file(path: &str, flags: fio::OpenFlags) -> bool {
let file_flag = flags.intersects(fio::OpenFlags::NOT_DIRECTORY);
let dir_flag = flags.intersects(fio::OpenFlags::DIRECTORY);
let path_flag = flags.intersects(fio::OpenFlags::NODE_REFERENCE);
let open_as_file = file_flag;
let open_as_directory = dir_flag || path_flag;
path == "meta" && (open_as_file || !open_as_directory)
}
pub async fn handle_package_directory_stream(
mut stream: fio::DirectoryRequestStream,
backing_dir_proxy: fio::DirectoryProxy,
) {
async move {
let (package_contents_dir_proxy, package_contents_dir_server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
backing_dir_proxy
.open3(
PACKAGE_CONTENTS_PATH,
fio::Flags::PROTOCOL_DIRECTORY | fio::PERM_READABLE,
&fio::Options::default(),
package_contents_dir_server_end.into_channel(),
)
.unwrap();
while let Some(req) = stream.next().await {
match req.unwrap() {
fio::DirectoryRequest::Open { flags, mode, path, object, control_handle: _ } => {
if path == "." {
panic!(
"Client would escape mock resolver directory redirects by opening '.', which might break further requests to /meta as a file"
)
}
if should_redirect_request_to_merkle_file(&path, flags) {
backing_dir_proxy.open(flags, mode, &path, object).unwrap();
} else {
package_contents_dir_proxy.open(flags, mode, &path, object).unwrap();
}
}
fio::DirectoryRequest::Open3 { path, flags, options, object, control_handle: _ } => {
if path == "." {
panic!(
"Client would escape mock resolver directory redirects by opening '.', which might break further requests to /meta as a file"
)
}
let open_meta_as_file = flags.intersects(fio::Flags::PROTOCOL_FILE) || !flags.intersects(fio::Flags::PROTOCOL_DIRECTORY | fio::Flags::PROTOCOL_NODE);
if path == "meta" && open_meta_as_file {
backing_dir_proxy.open3(&path, flags, &options, object).expect("open3 wire call failed.");
} else {
package_contents_dir_proxy.open3(&path, flags, &options, object).expect("open3 wire call failed.");
}
}
fio::DirectoryRequest::ReadDirents { max_bytes, responder } => {
let results = package_contents_dir_proxy
.read_dirents(max_bytes)
.await
.expect("read package contents dir");
responder.send(results.0, &results.1).expect("send ReadDirents response");
}
fio::DirectoryRequest::Rewind { responder } => {
responder
.send(
package_contents_dir_proxy
.rewind()
.await
.expect("rewind to package_contents dir"),
)
.expect("could send Rewind Response");
}
fio::DirectoryRequest::Close { responder } => {
responder.send(Ok(())).expect("send Close response")
}
other => panic!("unhandled request type: {other:?}"),
}
}
}.await;
}
#[derive(Debug)]
enum Expectation {
ImmediateConstant(Result<TestPackage, fidl_fuchsia_pkg::ResolveError>),
ImmediateVec(Vec<Result<TestPackage, fidl_fuchsia_pkg::ResolveError>>),
BlockOnce(Option<oneshot::Sender<PendingResolve>>),
}
pub struct MockResolverService {
expectations: Mutex<HashMap<String, Expectation>>,
resolve_hook: Box<dyn Fn(&str) + Send + Sync>,
packages_dir: tempfile::TempDir,
}
impl MockResolverService {
#[allow(clippy::type_complexity)]
pub fn new(resolve_hook: Option<Box<dyn Fn(&str) + Send + Sync>>) -> Self {
let packages_dir = TempDir::new().expect("create packages tempdir");
Self {
packages_dir,
resolve_hook: resolve_hook.unwrap_or_else(|| Box::new(|_| ())),
expectations: Mutex::new(HashMap::new()),
}
}
pub fn register_custom_package(
&self,
name_for_url: impl AsRef<str>,
meta_far_name: impl AsRef<str>,
merkle: impl AsRef<str>,
domain: &str,
) -> TestPackage {
let name_for_url = name_for_url.as_ref();
let merkle = merkle.as_ref();
let meta_far_name = meta_far_name.as_ref();
let url = format!("fuchsia-pkg://{domain}/{name_for_url}");
let pkg = self.package(meta_far_name, merkle);
self.url(url).resolve(&pkg);
pkg
}
pub fn register_package(&self, name: impl AsRef<str>, merkle: impl AsRef<str>) -> TestPackage {
self.register_custom_package(&name, &name, merkle, "fuchsia.com")
}
pub fn mock_resolve_failure(
&self,
url: impl Into<String>,
error: fidl_fuchsia_pkg::ResolveError,
) {
self.url(url).fail(error);
}
pub fn package(&self, name: impl AsRef<str>, merkle: impl AsRef<str>) -> TestPackage {
let name = name.as_ref();
let merkle = merkle.as_ref();
let root = self.packages_dir.path().join(merkle);
create_dir(&root).expect("package to not yet exist");
create_dir(root.join(PACKAGE_CONTENTS_PATH))
.expect("package_contents dir to not yet exist");
create_dir(root.join(PACKAGE_CONTENTS_PATH).join("meta"))
.expect("meta dir to not yet exist");
std::fs::write(root.join(META_FAR_MERKLE_ROOT_PATH), merkle)
.expect("create fake package file");
TestPackage::new(root)
.add_file("meta/package", format!("{{\"name\": \"{name}\", \"version\": \"0\"}}"))
}
pub fn path(&self, path: impl AsRef<str>) -> ForUrl<'_> {
self.url(format!("fuchsia-pkg://fuchsia.com/{}", path.as_ref()))
}
pub fn url(&self, url: impl Into<String>) -> ForUrl<'_> {
ForUrl { svc: self, url: url.into() }
}
pub fn spawn_resolver_service(self: Arc<Self>) -> PackageResolverProxy {
let (proxy, stream) = fidl::endpoints::create_proxy_and_stream::<PackageResolverMarker>();
fasync::Task::spawn(self.run_resolver_service(stream).unwrap_or_else(|e| {
panic!("error running package resolver service: {:#}", anyhow!(e))
}))
.detach();
proxy
}
pub async fn run_resolver_service(
self: Arc<Self>,
mut stream: PackageResolverRequestStream,
) -> Result<(), Error> {
while let Some(event) = stream.try_next().await.expect("received request") {
match event {
fidl_fuchsia_pkg::PackageResolverRequest::Resolve {
package_url,
dir,
responder,
} => self.handle_resolve(package_url, dir, responder).await?,
fidl_fuchsia_pkg::PackageResolverRequest::ResolveWithContext {
package_url: _,
context: _,
dir: _,
responder: _,
} => panic!("ResolveWithContext not implemented"),
fidl_fuchsia_pkg::PackageResolverRequest::GetHash {
package_url: _,
responder: _,
} => panic!("GetHash not implemented"),
}
}
Ok(())
}
async fn handle_resolve(
&self,
package_url: String,
dir: ServerEnd<fio::DirectoryMarker>,
responder: PackageResolverResolveResponder,
) -> Result<(), Error> {
(*self.resolve_hook)(&package_url);
match self.expectations.lock().get_mut(&package_url).unwrap_or(
&mut Expectation::ImmediateConstant(Err(
fidl_fuchsia_pkg::ResolveError::PackageNotFound,
)),
) {
Expectation::ImmediateConstant(Ok(package)) => {
package.serve_on(dir);
responder.send(Ok(&fpkg::ResolutionContext { bytes: vec![] }))?;
}
Expectation::ImmediateConstant(Err(error)) => {
responder.send(Err(*error))?;
}
Expectation::BlockOnce(handler) => {
let handler = handler.take().unwrap();
handler.send(PendingResolve { responder, dir_request: dir }).unwrap();
}
Expectation::ImmediateVec(expected_results) => {
if expected_results.is_empty() {
panic!("expected_results should be >= number of resolve requests");
}
match expected_results.remove(0) {
Ok(package) => {
package.serve_on(dir);
responder.send(Ok(&fpkg::ResolutionContext { bytes: vec![] }))?;
}
Err(e) => {
responder.send(Err(e))?;
}
};
}
}
Ok(())
}
}
#[must_use]
pub struct ForUrl<'a> {
svc: &'a MockResolverService,
url: String,
}
impl<'a> ForUrl<'a> {
pub fn fail(self, error: fidl_fuchsia_pkg::ResolveError) {
self.svc.expectations.lock().insert(self.url, Expectation::ImmediateConstant(Err(error)));
}
pub fn resolve(self, pkg: &TestPackage) {
let pkg = TestPackage::new(pkg.root.clone());
self.svc.expectations.lock().insert(self.url, Expectation::ImmediateConstant(Ok(pkg)));
}
pub fn block_once(self) -> ResolveHandler {
let (send, recv) = oneshot::channel();
self.svc.expectations.lock().insert(self.url, Expectation::BlockOnce(Some(send)));
ResolveHandler::Waiting(recv)
}
pub fn respond_serially(
self,
responses: Vec<Result<TestPackage, fidl_fuchsia_pkg::ResolveError>>,
) {
self.svc.expectations.lock().insert(self.url, Expectation::ImmediateVec(responses));
}
}
#[derive(Debug)]
pub struct PendingResolve {
responder: PackageResolverResolveResponder,
dir_request: ServerEnd<fio::DirectoryMarker>,
}
#[derive(Debug)]
pub enum ResolveHandler {
Waiting(oneshot::Receiver<PendingResolve>),
Blocked(PendingResolve),
}
impl ResolveHandler {
pub async fn wait(&mut self) {
match self {
ResolveHandler::Waiting(receiver) => {
*self = ResolveHandler::Blocked(receiver.await.unwrap());
}
ResolveHandler::Blocked(_) => {}
}
}
async fn into_pending(self) -> PendingResolve {
match self {
ResolveHandler::Waiting(receiver) => receiver.await.unwrap(),
ResolveHandler::Blocked(pending) => pending,
}
}
pub async fn fail(self, error: fidl_fuchsia_pkg::ResolveError) {
self.into_pending().await.responder.send(Err(error)).unwrap();
}
pub async fn resolve(self, pkg: &TestPackage) {
let PendingResolve { responder, dir_request } = self.into_pending().await;
pkg.serve_on(dir_request);
responder.send(Ok(&fpkg::ResolutionContext { bytes: vec![] })).unwrap();
}
}
#[cfg(test)]
mod tests {
use super::*;
use assert_matches::assert_matches;
use fidl_fuchsia_pkg::ResolveError;
async fn read_file(dir_proxy: &fio::DirectoryProxy, path: &str) -> String {
let file_proxy =
fuchsia_fs::directory::open_file(dir_proxy, path, fio::PERM_READABLE).await.unwrap();
fuchsia_fs::file::read_to_string(&file_proxy).await.unwrap()
}
fn do_resolve(
proxy: &PackageResolverProxy,
url: &str,
) -> impl Future<Output = Result<(fio::DirectoryProxy, fpkg::ResolutionContext), ResolveError>>
{
let (package_dir, package_dir_server_end) = fidl::endpoints::create_proxy();
let fut = proxy.resolve(url, package_dir_server_end);
async move {
let resolve_context = fut.await.unwrap()?;
Ok((package_dir, resolve_context))
}
}
#[fasync::run_singlethreaded(test)]
async fn test_mock_resolver() {
let resolved_urls = Arc::new(Mutex::new(vec![]));
let resolved_urls_clone = resolved_urls.clone();
let resolver =
Arc::new(MockResolverService::new(Some(Box::new(move |resolved_url: &str| {
resolved_urls_clone.lock().push(resolved_url.to_owned())
}))));
let resolver_proxy = Arc::clone(&resolver).spawn_resolver_service();
resolver
.register_package("update", "upd4t3")
.add_file(
"packages",
"system_image/0=42ade6f4fd51636f70c68811228b4271ed52c4eb9a647305123b4f4d0741f296\n",
)
.add_file("zbi", "fake zbi");
assert_eq!(*resolved_urls.lock(), Vec::<String>::new());
let (package_dir, _resolved_context) =
do_resolve(&resolver_proxy, "fuchsia-pkg://fuchsia.com/update").await.unwrap();
let meta_contents = read_file(&package_dir, "meta").await;
assert_eq!(meta_contents, "upd4t3");
let package_info = read_file(&package_dir, "meta/package").await;
assert_eq!(package_info, "{\"name\": \"update\", \"version\": \"0\"}");
let zbi_contents = read_file(&package_dir, "zbi").await;
assert_eq!(zbi_contents, "fake zbi");
assert_eq!(*resolved_urls.lock(), vec!["fuchsia-pkg://fuchsia.com/update"]);
}
#[fasync::run_singlethreaded(test)]
async fn block_once_blocks() {
let resolver = Arc::new(MockResolverService::new(None));
let mut handle_first = resolver.url("fuchsia-pkg://fuchsia.com/first").block_once();
let handle_second = resolver.path("second").block_once();
let proxy = Arc::clone(&resolver).spawn_resolver_service();
let first_fut = do_resolve(&proxy, "fuchsia-pkg://fuchsia.com/first");
let second_fut = do_resolve(&proxy, "fuchsia-pkg://fuchsia.com/second");
handle_first.wait().await;
handle_second.fail(fidl_fuchsia_pkg::ResolveError::PackageNotFound).await;
assert_matches!(second_fut.await, Err(fidl_fuchsia_pkg::ResolveError::PackageNotFound));
let pkg = resolver.package("second", "fake merkle");
handle_first.resolve(&pkg).await;
let (first_pkg, _resolved_context) = first_fut.await.unwrap();
assert_eq!(read_file(&first_pkg, "meta").await, "fake merkle");
}
#[fasync::run_singlethreaded(test)]
async fn multiple_predefined_responses() {
let resolver = Arc::new(MockResolverService::new(None));
let resolver_proxy = Arc::clone(&resolver).spawn_resolver_service();
resolver.url("fuchsia-pkg://fuchsia.com/update").respond_serially(vec![
Err(ResolveError::NoSpace),
Ok(resolver.package("update", "upd4t3")),
]);
assert_matches!(
do_resolve(&resolver_proxy, "fuchsia-pkg://fuchsia.com/update").await,
Err(ResolveError::NoSpace)
);
let (package_dir, _resolved_context) =
do_resolve(&resolver_proxy, "fuchsia-pkg://fuchsia.com/update").await.unwrap();
let meta_contents = read_file(&package_dir, "meta").await;
assert_eq!(meta_contents, "upd4t3");
}
#[fasync::run_singlethreaded(test)]
#[should_panic(expected = "expected_results should be >= number of resolve requests")]
async fn panics_when_not_enough_predefined_responses() {
let resolver = Arc::new(MockResolverService::new(None));
let resolver_proxy = Arc::clone(&resolver).spawn_resolver_service();
resolver.url("fuchsia-pkg://fuchsia.com/update").respond_serially(vec![]);
let _ = do_resolve(&resolver_proxy, "fuchsia-pkg://fuchsia.com/update").await;
}
}