Skip to main content

mock_resolver/
lib.rs

1// Copyright 2020 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use 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        // Connect to the backing directory which we'll proxy _most_ requests to.
43        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        // Open the package directory using the directory request given by the client
53        // asking to resolve the package, but proxy it through our handler so that we can
54        // intercept requests for /meta.
55        fasync::Task::spawn(handle_package_directory_stream(
56            dir_request.into_stream(),
57            backing_dir_proxy,
58        ))
59        .detach();
60    }
61}
62
63/// Handles a stream of requests for a package directory,
64/// redirecting file-mode Open requests for /meta to an internal file.
65pub 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 the client is trying to read the meta directory as a file, redirect them
78                    // to the file which actually holds the merkle for the purposes of these tests.
79                    // Otherwise, redirect to the real package contents.
80                    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                        // Should redirect request to merkle file
90                        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                    // Don't do anything with this for now.
114                    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
129/// Mock package resolver which returns package directories that behave
130/// roughly as if they're being served from pkgfs: /meta can be
131/// opened as both a directory and a file.
132pub 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    /// Consider using Self::package/Self::url instead to clarify the usage of these 4 str params.
150    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    /// Registers a package with the given name and merkle root, returning a handle to add files to
180    /// the package.
181    ///
182    /// This method does not register the package to be served by any fuchsia-pkg URLs. See
183    /// [`MockResolverService::url`]
184    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 the package directory and the meta directory for the fake package.
191        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        // Create the file which holds the merkle root of the package, to redirect requests for 'meta' to.
198        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    /// Equivalent to `self.url(format!("fuchsia-pkg://fuchsia.com/{}", path))`
206    pub fn path(&self, path: impl AsRef<str>) -> ForUrl<'_> {
207        self.url(format!("fuchsia-pkg://fuchsia.com/{}", path.as_ref()))
208    }
209
210    /// Returns an object to configure the handler for the given URL.
211    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    /// Serves the fuchsia.pkg.PackageResolver protocol on the given request stream.
227    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    /// Fail resolve requests for the given URL with the given error status.
304    pub fn fail(self, error: fidl_fuchsia_pkg::ResolveError) {
305        self.svc.expectations.lock().insert(self.url, Expectation::ImmediateConstant(Err(error)));
306    }
307
308    /// Succeed resolve requests for the given URL by serving the given package.
309    pub fn resolve(self, pkg: &TestPackage) {
310        // Manually construct a new TestPackage referring to the same root dir. Note that it would
311        // be invalid for TestPackage to impl Clone, as add_file would affect all Clones of a
312        // package.
313        let pkg = TestPackage::new(pkg.root.clone());
314        self.svc.expectations.lock().insert(self.url, Expectation::ImmediateConstant(Ok(pkg)));
315    }
316
317    /// Blocks requests for the given URL once, allowing the returned handler control the response.
318    /// Panics on further requests for that URL.
319    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    /// Respond to resolve requests serially with a list of pre-defined immediate responses. This is
327    /// useful if the caller wants to make several resolve calls for the same url and have each
328    /// resolve call return something different.
329    ///
330    /// This API is different from the other ForUrl APIs because the mock resolver will use each
331    /// response exactly once. In the other APIs, the resolver will always return the given response
332    /// for a url regardless of how many times resolve() is called.
333    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    /// Waits for the mock package resolver to receive a resolve request for this handler.
355    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    /// Wait for the request and fail the resolve with the given status.
372    pub async fn fail(self, error: fidl_fuchsia_pkg::ResolveError) {
373        self.into_pending().await.responder.send(Err(error)).unwrap();
374    }
375
376    /// Wait for the request and succeed the resolve by serving the given package.
377    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        // We should have no URLs resolved yet.
432        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        // Check that we can read from /meta (meta-as-file mode)
438        let meta_contents = read_file(&package_dir, "meta").await;
439        assert_eq!(meta_contents, "upd4t3");
440
441        // Check that we can read a file _within_ /meta (meta-as-dir mode)
442        let package_info = read_file(&package_dir, "meta/package").await;
443        assert_eq!(package_info, "{\"name\": \"update\", \"version\": \"0\"}");
444
445        // Check that we can read files we expect to be in the package.
446        let zbi_contents = read_file(&package_dir, "zbi").await;
447        assert_eq!(zbi_contents, "fake zbi");
448
449        // Make sure that our resolve hook was called properly
450        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        // First resolve should fail with the error.
487        assert_matches!(
488            do_resolve(&resolver_proxy, "fuchsia-pkg://fuchsia.com/update").await,
489            Err(ResolveError::NoSpace)
490        );
491
492        // Second resolve should succeed and give us the expected package dir.
493        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        // Since there are no expected responses, the mock resolver should panic.
508        let _ = do_resolve(&resolver_proxy, "fuchsia-pkg://fuchsia.com/update").await;
509    }
510}