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