1use anyhow::{anyhow, Error};
6use fidl::endpoints::ServerEnd;
7use fidl_fuchsia_pkg::{
8 self as fpkg, PackageResolverMarker, PackageResolverProxy, PackageResolverRequestStream,
9 PackageResolverResolveResponder,
10};
11use fuchsia_sync::Mutex;
12use futures::channel::oneshot;
13use futures::prelude::*;
14use std::collections::HashMap;
15use std::fs::{self, create_dir};
16use std::path::{Path, PathBuf};
17use std::sync::Arc;
18use tempfile::TempDir;
19use {fidl_fuchsia_io as fio, fuchsia_async as fasync};
20
21const PACKAGE_CONTENTS_PATH: &str = "package_contents";
22const META_FAR_MERKLE_ROOT_PATH: &str = "meta";
23
24#[derive(Debug)]
25pub struct TestPackage {
26 root: PathBuf,
27}
28
29impl TestPackage {
30 fn new(root: PathBuf) -> Self {
31 TestPackage { root }
32 }
33
34 pub fn add_file(self, path: impl AsRef<Path>, contents: impl AsRef<[u8]>) -> Self {
35 fs::write(self.root.join(PACKAGE_CONTENTS_PATH).join(path), contents)
36 .expect("create fake package file");
37 self
38 }
39
40 fn serve_on(&self, dir_request: ServerEnd<fio::DirectoryMarker>) {
41 let (backing_dir_proxy, server_end) =
43 fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
44 fuchsia_fs::directory::open_channel_in_namespace(
45 self.root.to_str().unwrap(),
46 fio::PERM_READABLE,
47 server_end,
48 )
49 .expect("open channel in namespace failed");
50
51 fasync::Task::spawn(handle_package_directory_stream(
55 dir_request.into_stream(),
56 backing_dir_proxy,
57 ))
58 .detach();
59 }
60}
61
62pub async fn handle_package_directory_stream(
65 mut stream: fio::DirectoryRequestStream,
66 backing_dir_proxy: fio::DirectoryProxy,
67) {
68 async move {
69 let (package_contents_dir_proxy, package_contents_dir_server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
70 backing_dir_proxy.open(PACKAGE_CONTENTS_PATH, fio::Flags::PROTOCOL_DIRECTORY | fio::PERM_READABLE, &fio::Options::default(), package_contents_dir_server_end.into_channel())
71 .unwrap();
72
73 while let Some(req) = stream.next().await {
74 match req.unwrap() {
75 fio::DirectoryRequest::Open { path, flags, options, object, control_handle: _ } => {
76 if path == "." {
80 panic!(
81 "Client would escape mock resolver directory redirects by opening '.', which might break further requests to /meta as a file"
82 )
83 }
84
85 let open_meta_as_file = flags.intersects(fio::Flags::PROTOCOL_FILE) || !flags.intersects(fio::Flags::PROTOCOL_DIRECTORY | fio::Flags::PROTOCOL_NODE);
86
87 if path == "meta" && open_meta_as_file {
88 backing_dir_proxy.open(&path, flags, &options, object).expect("open3 wire call failed.");
90 } else {
91 package_contents_dir_proxy.open(&path, flags, &options, object).expect("open3 wire call failed.");
92 }
93 }
94 fio::DirectoryRequest::ReadDirents { max_bytes, responder } => {
95 let results = package_contents_dir_proxy
96 .read_dirents(max_bytes)
97 .await
98 .expect("read package contents dir");
99 responder.send(results.0, &results.1).expect("send ReadDirents response");
100 }
101 fio::DirectoryRequest::Rewind { responder } => {
102 responder
103 .send(
104 package_contents_dir_proxy
105 .rewind()
106 .await
107 .expect("rewind to package_contents dir"),
108 )
109 .expect("could send Rewind Response");
110 }
111 fio::DirectoryRequest::Close { responder } => {
112 responder.send(Ok(())).expect("send Close response")
114 }
115 other => panic!("unhandled request type: {other:?}"),
116 }
117 }
118 }.await;
119}
120
121#[derive(Debug)]
122enum Expectation {
123 ImmediateConstant(Result<TestPackage, fidl_fuchsia_pkg::ResolveError>),
124 ImmediateVec(Vec<Result<TestPackage, fidl_fuchsia_pkg::ResolveError>>),
125 BlockOnce(Option<oneshot::Sender<PendingResolve>>),
126}
127
128pub struct MockResolverService {
132 expectations: Mutex<HashMap<String, Expectation>>,
133 resolve_hook: Box<dyn Fn(&str) + Send + Sync>,
134 packages_dir: tempfile::TempDir,
135}
136
137impl MockResolverService {
138 #[allow(clippy::type_complexity)]
139 pub fn new(resolve_hook: Option<Box<dyn Fn(&str) + Send + Sync>>) -> Self {
140 let packages_dir = TempDir::new().expect("create packages tempdir");
141 Self {
142 packages_dir,
143 resolve_hook: resolve_hook.unwrap_or_else(|| Box::new(|_| ())),
144 expectations: Mutex::new(HashMap::new()),
145 }
146 }
147
148 pub fn register_custom_package(
150 &self,
151 name_for_url: impl AsRef<str>,
152 meta_far_name: impl AsRef<str>,
153 merkle: impl AsRef<str>,
154 domain: &str,
155 ) -> TestPackage {
156 let name_for_url = name_for_url.as_ref();
157 let merkle = merkle.as_ref();
158 let meta_far_name = meta_far_name.as_ref();
159
160 let url = format!("fuchsia-pkg://{domain}/{name_for_url}");
161 let pkg = self.package(meta_far_name, merkle);
162 self.url(url).resolve(&pkg);
163 pkg
164 }
165
166 pub fn register_package(&self, name: impl AsRef<str>, merkle: impl AsRef<str>) -> TestPackage {
167 self.register_custom_package(&name, &name, merkle, "fuchsia.com")
168 }
169
170 pub fn mock_resolve_failure(
171 &self,
172 url: impl Into<String>,
173 error: fidl_fuchsia_pkg::ResolveError,
174 ) {
175 self.url(url).fail(error);
176 }
177
178 pub fn package(&self, name: impl AsRef<str>, merkle: impl AsRef<str>) -> TestPackage {
184 let name = name.as_ref();
185 let merkle = merkle.as_ref();
186
187 let root = self.packages_dir.path().join(merkle);
188
189 create_dir(&root).expect("package to not yet exist");
191 create_dir(root.join(PACKAGE_CONTENTS_PATH))
192 .expect("package_contents dir to not yet exist");
193 create_dir(root.join(PACKAGE_CONTENTS_PATH).join("meta"))
194 .expect("meta dir to not yet exist");
195
196 std::fs::write(root.join(META_FAR_MERKLE_ROOT_PATH), merkle)
198 .expect("create fake package file");
199
200 TestPackage::new(root)
201 .add_file("meta/package", format!("{{\"name\": \"{name}\", \"version\": \"0\"}}"))
202 }
203
204 pub fn path(&self, path: impl AsRef<str>) -> ForUrl<'_> {
206 self.url(format!("fuchsia-pkg://fuchsia.com/{}", path.as_ref()))
207 }
208
209 pub fn url(&self, url: impl Into<String>) -> ForUrl<'_> {
211 ForUrl { svc: self, url: url.into() }
212 }
213
214 pub fn spawn_resolver_service(self: Arc<Self>) -> PackageResolverProxy {
215 let (proxy, stream) = fidl::endpoints::create_proxy_and_stream::<PackageResolverMarker>();
216
217 fasync::Task::spawn(self.run_resolver_service(stream).unwrap_or_else(|e| {
218 panic!("error running package resolver service: {:#}", anyhow!(e))
219 }))
220 .detach();
221
222 proxy
223 }
224
225 pub async fn run_resolver_service(
227 self: Arc<Self>,
228 mut stream: PackageResolverRequestStream,
229 ) -> Result<(), Error> {
230 while let Some(event) = stream.try_next().await.expect("received request") {
231 match event {
232 fidl_fuchsia_pkg::PackageResolverRequest::Resolve {
233 package_url,
234 dir,
235 responder,
236 } => self.handle_resolve(package_url, dir, responder).await?,
237 fidl_fuchsia_pkg::PackageResolverRequest::ResolveWithContext {
238 package_url: _,
239 context: _,
240 dir: _,
241 responder: _,
242 } => panic!("ResolveWithContext not implemented"),
243 fidl_fuchsia_pkg::PackageResolverRequest::GetHash {
244 package_url: _,
245 responder: _,
246 } => panic!("GetHash not implemented"),
247 }
248 }
249 Ok(())
250 }
251
252 async fn handle_resolve(
253 &self,
254 package_url: String,
255 dir: ServerEnd<fio::DirectoryMarker>,
256 responder: PackageResolverResolveResponder,
257 ) -> Result<(), Error> {
258 (*self.resolve_hook)(&package_url);
259
260 match self.expectations.lock().get_mut(&package_url).unwrap_or(
261 &mut Expectation::ImmediateConstant(Err(
262 fidl_fuchsia_pkg::ResolveError::PackageNotFound,
263 )),
264 ) {
265 Expectation::ImmediateConstant(Ok(package)) => {
266 package.serve_on(dir);
267 responder.send(Ok(&fpkg::ResolutionContext { bytes: vec![] }))?;
268 }
269 Expectation::ImmediateConstant(Err(error)) => {
270 responder.send(Err(*error))?;
271 }
272 Expectation::BlockOnce(handler) => {
273 let handler = handler.take().unwrap();
274 handler.send(PendingResolve { responder, dir_request: dir }).unwrap();
275 }
276 Expectation::ImmediateVec(expected_results) => {
277 if expected_results.is_empty() {
278 panic!("expected_results should be >= number of resolve requests");
279 }
280 match expected_results.remove(0) {
281 Ok(package) => {
282 package.serve_on(dir);
283 responder.send(Ok(&fpkg::ResolutionContext { bytes: vec![] }))?;
284 }
285 Err(e) => {
286 responder.send(Err(e))?;
287 }
288 };
289 }
290 }
291 Ok(())
292 }
293}
294
295#[must_use]
296pub struct ForUrl<'a> {
297 svc: &'a MockResolverService,
298 url: String,
299}
300
301impl ForUrl<'_> {
302 pub fn fail(self, error: fidl_fuchsia_pkg::ResolveError) {
304 self.svc.expectations.lock().insert(self.url, Expectation::ImmediateConstant(Err(error)));
305 }
306
307 pub fn resolve(self, pkg: &TestPackage) {
309 let pkg = TestPackage::new(pkg.root.clone());
313 self.svc.expectations.lock().insert(self.url, Expectation::ImmediateConstant(Ok(pkg)));
314 }
315
316 pub fn block_once(self) -> ResolveHandler {
319 let (send, recv) = oneshot::channel();
320
321 self.svc.expectations.lock().insert(self.url, Expectation::BlockOnce(Some(send)));
322 ResolveHandler::Waiting(recv)
323 }
324
325 pub fn respond_serially(
333 self,
334 responses: Vec<Result<TestPackage, fidl_fuchsia_pkg::ResolveError>>,
335 ) {
336 self.svc.expectations.lock().insert(self.url, Expectation::ImmediateVec(responses));
337 }
338}
339
340#[derive(Debug)]
341pub struct PendingResolve {
342 responder: PackageResolverResolveResponder,
343 dir_request: ServerEnd<fio::DirectoryMarker>,
344}
345
346#[derive(Debug)]
347pub enum ResolveHandler {
348 Waiting(oneshot::Receiver<PendingResolve>),
349 Blocked(PendingResolve),
350}
351
352impl ResolveHandler {
353 pub async fn wait(&mut self) {
355 match self {
356 ResolveHandler::Waiting(receiver) => {
357 *self = ResolveHandler::Blocked(receiver.await.unwrap());
358 }
359 ResolveHandler::Blocked(_) => {}
360 }
361 }
362
363 async fn into_pending(self) -> PendingResolve {
364 match self {
365 ResolveHandler::Waiting(receiver) => receiver.await.unwrap(),
366 ResolveHandler::Blocked(pending) => pending,
367 }
368 }
369
370 pub async fn fail(self, error: fidl_fuchsia_pkg::ResolveError) {
372 self.into_pending().await.responder.send(Err(error)).unwrap();
373 }
374
375 pub async fn resolve(self, pkg: &TestPackage) {
377 let PendingResolve { responder, dir_request } = self.into_pending().await;
378
379 pkg.serve_on(dir_request);
380 responder.send(Ok(&fpkg::ResolutionContext { bytes: vec![] })).unwrap();
381 }
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387 use assert_matches::assert_matches;
388 use fidl_fuchsia_pkg::ResolveError;
389
390 async fn read_file(dir_proxy: &fio::DirectoryProxy, path: &str) -> String {
391 let file_proxy =
392 fuchsia_fs::directory::open_file(dir_proxy, path, fio::PERM_READABLE).await.unwrap();
393
394 fuchsia_fs::file::read_to_string(&file_proxy).await.unwrap()
395 }
396
397 fn do_resolve(
398 proxy: &PackageResolverProxy,
399 url: &str,
400 ) -> impl Future<Output = Result<(fio::DirectoryProxy, fpkg::ResolutionContext), ResolveError>>
401 {
402 let (package_dir, package_dir_server_end) = fidl::endpoints::create_proxy();
403 let fut = proxy.resolve(url, package_dir_server_end);
404
405 async move {
406 let resolve_context = fut.await.unwrap()?;
407 Ok((package_dir, resolve_context))
408 }
409 }
410
411 #[fasync::run_singlethreaded(test)]
412 async fn test_mock_resolver() {
413 let resolved_urls = Arc::new(Mutex::new(vec![]));
414 let resolved_urls_clone = resolved_urls.clone();
415 let resolver =
416 Arc::new(MockResolverService::new(Some(Box::new(move |resolved_url: &str| {
417 resolved_urls_clone.lock().push(resolved_url.to_owned())
418 }))));
419
420 let resolver_proxy = Arc::clone(&resolver).spawn_resolver_service();
421
422 resolver
423 .register_package("update", "upd4t3")
424 .add_file(
425 "packages",
426 "system_image/0=42ade6f4fd51636f70c68811228b4271ed52c4eb9a647305123b4f4d0741f296\n",
427 )
428 .add_file("zbi", "fake zbi");
429
430 assert_eq!(*resolved_urls.lock(), Vec::<String>::new());
432
433 let (package_dir, _resolved_context) =
434 do_resolve(&resolver_proxy, "fuchsia-pkg://fuchsia.com/update").await.unwrap();
435
436 let meta_contents = read_file(&package_dir, "meta").await;
438 assert_eq!(meta_contents, "upd4t3");
439
440 let package_info = read_file(&package_dir, "meta/package").await;
442 assert_eq!(package_info, "{\"name\": \"update\", \"version\": \"0\"}");
443
444 let zbi_contents = read_file(&package_dir, "zbi").await;
446 assert_eq!(zbi_contents, "fake zbi");
447
448 assert_eq!(*resolved_urls.lock(), vec!["fuchsia-pkg://fuchsia.com/update"]);
450 }
451
452 #[fasync::run_singlethreaded(test)]
453 async fn block_once_blocks() {
454 let resolver = Arc::new(MockResolverService::new(None));
455 let mut handle_first = resolver.url("fuchsia-pkg://fuchsia.com/first").block_once();
456 let handle_second = resolver.path("second").block_once();
457
458 let proxy = Arc::clone(&resolver).spawn_resolver_service();
459
460 let first_fut = do_resolve(&proxy, "fuchsia-pkg://fuchsia.com/first");
461 let second_fut = do_resolve(&proxy, "fuchsia-pkg://fuchsia.com/second");
462
463 handle_first.wait().await;
464
465 handle_second.fail(fidl_fuchsia_pkg::ResolveError::PackageNotFound).await;
466 assert_matches!(second_fut.await, Err(fidl_fuchsia_pkg::ResolveError::PackageNotFound));
467
468 let pkg = resolver.package("second", "fake merkle");
469 handle_first.resolve(&pkg).await;
470
471 let (first_pkg, _resolved_context) = first_fut.await.unwrap();
472 assert_eq!(read_file(&first_pkg, "meta").await, "fake merkle");
473 }
474
475 #[fasync::run_singlethreaded(test)]
476 async fn multiple_predefined_responses() {
477 let resolver = Arc::new(MockResolverService::new(None));
478 let resolver_proxy = Arc::clone(&resolver).spawn_resolver_service();
479
480 resolver.url("fuchsia-pkg://fuchsia.com/update").respond_serially(vec![
481 Err(ResolveError::NoSpace),
482 Ok(resolver.package("update", "upd4t3")),
483 ]);
484
485 assert_matches!(
487 do_resolve(&resolver_proxy, "fuchsia-pkg://fuchsia.com/update").await,
488 Err(ResolveError::NoSpace)
489 );
490
491 let (package_dir, _resolved_context) =
493 do_resolve(&resolver_proxy, "fuchsia-pkg://fuchsia.com/update").await.unwrap();
494 let meta_contents = read_file(&package_dir, "meta").await;
495 assert_eq!(meta_contents, "upd4t3");
496 }
497
498 #[fasync::run_singlethreaded(test)]
499 #[should_panic(expected = "expected_results should be >= number of resolve requests")]
500 async fn panics_when_not_enough_predefined_responses() {
501 let resolver = Arc::new(MockResolverService::new(None));
502 let resolver_proxy = Arc::clone(&resolver).spawn_resolver_service();
503
504 resolver.url("fuchsia-pkg://fuchsia.com/update").respond_serially(vec![]);
505
506 let _ = do_resolve(&resolver_proxy, "fuchsia-pkg://fuchsia.com/update").await;
508 }
509}