Skip to main content

sl4f_lib/paver/
facade.rs

1// Copyright 2019 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 crate::common_utils::common::LazyProxy;
6use anyhow::{Error, bail};
7use base64::engine::Engine as _;
8use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
9use fidl_fuchsia_paver::{PaverMarker, PaverProxy};
10use serde::{Deserialize, Serialize};
11use zx::Status;
12
13use super::types::{Asset, Configuration, ConfigurationStatus};
14
15/// Facade providing access to paver service.
16#[derive(Debug)]
17pub struct PaverFacade {
18    proxy: LazyProxy<PaverMarker>,
19}
20
21impl PaverFacade {
22    /// Creates a new [PaverFacade] with no active connection to the paver service.
23    pub fn new() -> Self {
24        Self { proxy: Default::default() }
25    }
26
27    #[cfg(test)]
28    fn new_with_proxy(proxy: PaverProxy) -> Self {
29        let new = Self::new();
30        new.proxy.set(proxy).expect("newly created facade should have empty proxy");
31        new
32    }
33
34    /// Return a cached connection to the paver service, or try to connect and cache the connection
35    /// for later.
36    fn proxy(&self) -> Result<PaverProxy, Error> {
37        self.proxy.get_or_connect()
38    }
39
40    /// Queries the active boot configuration, if the current bootloader supports it.
41    ///
42    /// # Errors
43    ///
44    /// Returns an Err(_) if
45    ///  * connecting to the paver service fails, or
46    ///  * the paver service returns an unexpected error
47    pub(super) async fn query_active_configuration(
48        &self,
49    ) -> Result<QueryActiveConfigurationResult, Error> {
50        let (boot_manager, boot_manager_server_end) = fidl::endpoints::create_proxy();
51
52        self.proxy()?.find_boot_manager(boot_manager_server_end)?;
53
54        match boot_manager.query_active_configuration().await {
55            Ok(Ok(config)) => Ok(QueryActiveConfigurationResult::Success(config.into())),
56            Ok(Err(err)) => bail!("unexpected failure status: {}", err),
57            Err(fidl::Error::ClientChannelClosed { epitaph, .. })
58                if epitaph == Status::NOT_SUPPORTED =>
59            {
60                Ok(QueryActiveConfigurationResult::NotSupported)
61            }
62            Err(err) => bail!("unexpected failure status: {}", err),
63        }
64    }
65
66    /// Queries the current boot configuration, if the current bootloader supports it.
67    ///
68    /// # Errors
69    ///
70    /// Returns an Err(_) if
71    ///  * connecting to the paver service fails, or
72    ///  * the paver service returns an unexpected error
73    pub(super) async fn query_current_configuration(
74        &self,
75    ) -> Result<QueryCurrentConfigurationResult, Error> {
76        let (boot_manager, boot_manager_server_end) = fidl::endpoints::create_proxy();
77
78        self.proxy()?.find_boot_manager(boot_manager_server_end)?;
79
80        match boot_manager.query_current_configuration().await {
81            Ok(Ok(config)) => Ok(QueryCurrentConfigurationResult::Success(config.into())),
82            Ok(Err(err)) => bail!("unexpected failure status: {}", err),
83            Err(fidl::Error::ClientChannelClosed { epitaph, .. })
84                if epitaph == Status::NOT_SUPPORTED =>
85            {
86                Ok(QueryCurrentConfigurationResult::NotSupported)
87            }
88            Err(err) => bail!("unexpected failure status: {}", err),
89        }
90    }
91
92    /// Queries the bootable status of the given configuration, if the current bootloader supports
93    /// it.
94    ///
95    /// # Errors
96    ///
97    /// Returns an Err(_) if
98    ///  * connecting to the paver service fails, or
99    ///  * the paver service returns an unexpected error
100    pub(super) async fn query_configuration_status(
101        &self,
102        args: QueryConfigurationStatusRequest,
103    ) -> Result<QueryConfigurationStatusResult, Error> {
104        let (boot_manager, boot_manager_server_end) = fidl::endpoints::create_proxy();
105
106        self.proxy()?.find_boot_manager(boot_manager_server_end)?;
107
108        match boot_manager.query_configuration_status(args.configuration.into()).await {
109            Ok(Ok(status)) => Ok(QueryConfigurationStatusResult::Success(status.into())),
110            Ok(Err(err)) => bail!("unexpected failure status: {}", err),
111            Err(fidl::Error::ClientChannelClosed { epitaph, .. })
112                if epitaph == Status::NOT_SUPPORTED =>
113            {
114                Ok(QueryConfigurationStatusResult::NotSupported)
115            }
116            Err(err) => bail!("unexpected failure status: {}", err),
117        }
118    }
119
120    /// Given a configuration and asset identifier, read that image and return it as a base64
121    /// encoded String.
122    ///
123    /// # Errors
124    ///
125    /// Returns an Err(_) if
126    ///  * connecting to the paver service fails, or
127    ///  * the paver service returns an unexpected error
128    pub(super) async fn read_asset(&self, args: ReadAssetRequest) -> Result<String, Error> {
129        let (data_sink, data_sink_server_end) = fidl::endpoints::create_proxy();
130
131        self.proxy()?.find_data_sink(data_sink_server_end)?;
132
133        let buffer = data_sink
134            .read_asset(args.configuration.into(), args.asset.into())
135            .await?
136            .map_err(Status::from_raw)?;
137
138        let mut res = vec![0; buffer.size as usize];
139        buffer.vmo.read(&mut res[..], 0)?;
140        Ok(BASE64_STANDARD.encode(&res))
141    }
142}
143
144#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
145#[serde(rename_all = "snake_case")]
146pub(super) enum QueryActiveConfigurationResult {
147    Success(Configuration),
148    NotSupported,
149}
150
151#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
152#[serde(rename_all = "snake_case")]
153pub(super) enum QueryCurrentConfigurationResult {
154    Success(Configuration),
155    NotSupported,
156}
157
158#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
159pub(super) struct QueryConfigurationStatusRequest {
160    configuration: Configuration,
161}
162
163#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
164#[serde(rename_all = "snake_case")]
165pub(super) enum QueryConfigurationStatusResult {
166    Success(ConfigurationStatus),
167    NotSupported,
168}
169
170#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
171pub(super) struct ReadAssetRequest {
172    configuration: Configuration,
173    asset: Asset,
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179    use crate::common_utils::test::assert_value_round_trips_as;
180    use assert_matches::assert_matches;
181    use fidl_fuchsia_paver::{
182        BootManagerRequest, BootManagerRequestStream, DataSinkRequest, DataSinkRequestStream,
183        PaverRequest,
184    };
185    use futures::future::Future;
186    use futures::join;
187    use futures::stream::StreamExt;
188    use serde_json::json;
189
190    #[test]
191    fn serde_query_active_configuration_result() {
192        assert_value_round_trips_as(
193            QueryActiveConfigurationResult::NotSupported,
194            json!("not_supported"),
195        );
196        assert_value_round_trips_as(
197            QueryActiveConfigurationResult::Success(Configuration::A),
198            json!({"success": "a"}),
199        );
200    }
201
202    #[test]
203    fn serde_query_current_configuration_result() {
204        assert_value_round_trips_as(
205            QueryCurrentConfigurationResult::NotSupported,
206            json!("not_supported"),
207        );
208        assert_value_round_trips_as(
209            QueryCurrentConfigurationResult::Success(Configuration::A),
210            json!({"success": "a"}),
211        );
212    }
213
214    #[test]
215    fn serde_query_configuration_status_result() {
216        assert_value_round_trips_as(
217            QueryConfigurationStatusResult::NotSupported,
218            json!("not_supported"),
219        );
220        assert_value_round_trips_as(
221            QueryConfigurationStatusResult::Success(ConfigurationStatus::Healthy),
222            json!({"success": "healthy"}),
223        );
224    }
225
226    #[test]
227    fn serde_query_configuration_request() {
228        assert_value_round_trips_as(
229            QueryConfigurationStatusRequest { configuration: Configuration::Recovery },
230            json!({"configuration": "recovery"}),
231        );
232    }
233
234    #[test]
235    fn serde_read_asset_request() {
236        assert_value_round_trips_as(
237            ReadAssetRequest {
238                configuration: Configuration::A,
239                asset: Asset::VerifiedBootMetadata,
240            },
241            json!({"configuration": "a", "asset": "verified_boot_metadata"}),
242        );
243    }
244
245    struct MockBootManagerBuilder {
246        expected: Vec<Box<dyn FnOnce(BootManagerRequest) + Send + 'static>>,
247    }
248
249    impl MockBootManagerBuilder {
250        fn new() -> Self {
251            Self { expected: vec![] }
252        }
253
254        fn push(mut self, request: impl FnOnce(BootManagerRequest) + Send + 'static) -> Self {
255            self.expected.push(Box::new(request));
256            self
257        }
258
259        fn expect_query_active_configuration(self, res: Result<Configuration, Status>) -> Self {
260            self.push(move |req| match req {
261                BootManagerRequest::QueryActiveConfiguration { responder } => {
262                    responder.send(res.map(Into::into).map_err(|e| e.into_raw())).unwrap()
263                }
264                req => panic!("unexpected request: {:?}", req),
265            })
266        }
267
268        fn expect_query_current_configuration(self, res: Result<Configuration, Status>) -> Self {
269            self.push(move |req| match req {
270                BootManagerRequest::QueryCurrentConfiguration { responder } => {
271                    responder.send(res.map(Into::into).map_err(|e| e.into_raw())).unwrap()
272                }
273                req => panic!("unexpected request: {:?}", req),
274            })
275        }
276
277        fn expect_query_configuration_status(
278            self,
279            config: Configuration,
280            res: Result<ConfigurationStatus, Status>,
281        ) -> Self {
282            self.push(move |req| match req {
283                BootManagerRequest::QueryConfigurationStatus { configuration, responder } => {
284                    assert_eq!(Configuration::from(configuration), config);
285                    responder.send(res.map(Into::into).map_err(|e| e.into_raw())).unwrap()
286                }
287                req => panic!("unexpected request: {:?}", req),
288            })
289        }
290
291        fn build(self, mut stream: BootManagerRequestStream) -> impl Future<Output = ()> {
292            async move {
293                for expected in self.expected {
294                    expected(stream.next().await.unwrap().unwrap());
295                }
296                assert_matches!(stream.next().await, None);
297            }
298        }
299    }
300
301    struct MockDataSinkBuilder {
302        expected: Vec<Box<dyn FnOnce(DataSinkRequest) + Send + 'static>>,
303    }
304
305    impl MockDataSinkBuilder {
306        fn new() -> Self {
307            Self { expected: vec![] }
308        }
309
310        fn push(mut self, request: impl FnOnce(DataSinkRequest) + Send + 'static) -> Self {
311            self.expected.push(Box::new(request));
312            self
313        }
314
315        fn expect_read_asset(
316            self,
317            expected_request: ReadAssetRequest,
318            response: &'static [u8],
319        ) -> Self {
320            let buf = fidl_fuchsia_mem::Buffer {
321                vmo: zx::Vmo::create(response.len() as u64).unwrap(),
322                size: response.len() as u64,
323            };
324            buf.vmo.write(response, 0).unwrap();
325
326            self.push(move |req| match req {
327                DataSinkRequest::ReadAsset { configuration, asset, responder } => {
328                    let request = ReadAssetRequest {
329                        configuration: configuration.into(),
330                        asset: asset.into(),
331                    };
332                    assert_eq!(request, expected_request);
333
334                    responder.send(Ok(buf)).unwrap()
335                }
336                req => panic!("unexpected request: {:?}", req),
337            })
338        }
339
340        fn build(self, mut stream: DataSinkRequestStream) -> impl Future<Output = ()> {
341            async move {
342                for expected in self.expected {
343                    expected(stream.next().await.unwrap().unwrap());
344                }
345                assert_matches!(stream.next().await, None);
346            }
347        }
348    }
349
350    struct MockPaverBuilder {
351        expected: Vec<Box<dyn FnOnce(PaverRequest) + 'static>>,
352    }
353
354    impl MockPaverBuilder {
355        fn new() -> Self {
356            Self { expected: vec![] }
357        }
358
359        fn push(mut self, request: impl FnOnce(PaverRequest) + 'static) -> Self {
360            self.expected.push(Box::new(request));
361            self
362        }
363
364        fn expect_find_boot_manager(self, mock: Option<MockBootManagerBuilder>) -> Self {
365            self.push(move |req| match req {
366                PaverRequest::FindBootManager { boot_manager, .. } => {
367                    if let Some(mock) = mock {
368                        let stream = boot_manager.into_stream();
369                        fuchsia_async::Task::spawn(async move {
370                            mock.build(stream).await;
371                        })
372                        .detach();
373                    } else {
374                        boot_manager.close_with_epitaph(Status::NOT_SUPPORTED).unwrap();
375                    }
376                }
377                req => panic!("unexpected request: {:?}", req),
378            })
379        }
380
381        fn expect_find_data_sink(self, mock: MockDataSinkBuilder) -> Self {
382            self.push(move |req| match req {
383                PaverRequest::FindDataSink { data_sink, .. } => {
384                    let stream = data_sink.into_stream();
385                    fuchsia_async::Task::spawn(async move {
386                        mock.build(stream).await;
387                    })
388                    .detach();
389                }
390                req => panic!("unexpected request: {:?}", req),
391            })
392        }
393
394        fn build(self) -> (PaverFacade, impl Future<Output = ()>) {
395            let (proxy, mut stream) = fidl::endpoints::create_proxy_and_stream::<PaverMarker>();
396            let fut = async move {
397                for expected in self.expected {
398                    expected(stream.next().await.unwrap().unwrap());
399                }
400                assert_matches!(stream.next().await, None);
401            };
402
403            (PaverFacade::new_with_proxy(proxy), fut)
404        }
405    }
406
407    #[fuchsia_async::run_singlethreaded(test)]
408    async fn query_active_configuration_ok() {
409        let (facade, paver) = MockPaverBuilder::new()
410            .expect_find_boot_manager(Some(
411                MockBootManagerBuilder::new()
412                    .expect_query_active_configuration(Ok(Configuration::A)),
413            ))
414            .expect_find_boot_manager(Some(
415                MockBootManagerBuilder::new()
416                    .expect_query_active_configuration(Ok(Configuration::B)),
417            ))
418            .build();
419
420        let test = async move {
421            assert_matches!(
422                facade.query_active_configuration().await,
423                Ok(QueryActiveConfigurationResult::Success(Configuration::A))
424            );
425            assert_matches!(
426                facade.query_active_configuration().await,
427                Ok(QueryActiveConfigurationResult::Success(Configuration::B))
428            );
429        };
430
431        join!(paver, test);
432    }
433
434    #[fuchsia_async::run_singlethreaded(test)]
435    async fn query_active_configuration_not_supported() {
436        let (facade, paver) = MockPaverBuilder::new().expect_find_boot_manager(None).build();
437
438        let test = async move {
439            assert_matches!(
440                facade.query_active_configuration().await,
441                Ok(QueryActiveConfigurationResult::NotSupported)
442            );
443        };
444
445        join!(paver, test);
446    }
447
448    #[fuchsia_async::run_singlethreaded(test)]
449    async fn query_current_configuration_ok() {
450        let (facade, paver) = MockPaverBuilder::new()
451            .expect_find_boot_manager(Some(
452                MockBootManagerBuilder::new()
453                    .expect_query_current_configuration(Ok(Configuration::A)),
454            ))
455            .expect_find_boot_manager(Some(
456                MockBootManagerBuilder::new()
457                    .expect_query_current_configuration(Ok(Configuration::B)),
458            ))
459            .build();
460
461        let test = async move {
462            assert_matches!(
463                facade.query_current_configuration().await,
464                Ok(QueryCurrentConfigurationResult::Success(Configuration::A))
465            );
466            assert_matches!(
467                facade.query_current_configuration().await,
468                Ok(QueryCurrentConfigurationResult::Success(Configuration::B))
469            );
470        };
471
472        join!(paver, test);
473    }
474
475    #[fuchsia_async::run_singlethreaded(test)]
476    async fn query_current_configuration_not_supported() {
477        let (facade, paver) = MockPaverBuilder::new().expect_find_boot_manager(None).build();
478
479        let test = async move {
480            assert_matches!(
481                facade.query_current_configuration().await,
482                Ok(QueryCurrentConfigurationResult::NotSupported)
483            );
484        };
485
486        join!(paver, test);
487    }
488
489    #[fuchsia_async::run_singlethreaded(test)]
490    async fn query_configuration_status_ok() {
491        let (facade, paver) = MockPaverBuilder::new()
492            .expect_find_boot_manager(Some(
493                MockBootManagerBuilder::new().expect_query_configuration_status(
494                    Configuration::A,
495                    Ok(ConfigurationStatus::Healthy),
496                ),
497            ))
498            .expect_find_boot_manager(Some(
499                MockBootManagerBuilder::new().expect_query_configuration_status(
500                    Configuration::B,
501                    Ok(ConfigurationStatus::Unbootable),
502                ),
503            ))
504            .build();
505
506        let test = async move {
507            assert_matches!(
508                facade
509                    .query_configuration_status(QueryConfigurationStatusRequest {
510                        configuration: Configuration::A
511                    })
512                    .await,
513                Ok(QueryConfigurationStatusResult::Success(ConfigurationStatus::Healthy))
514            );
515            assert_matches!(
516                facade
517                    .query_configuration_status(QueryConfigurationStatusRequest {
518                        configuration: Configuration::B
519                    })
520                    .await,
521                Ok(QueryConfigurationStatusResult::Success(ConfigurationStatus::Unbootable))
522            );
523        };
524
525        join!(paver, test);
526    }
527
528    #[fuchsia_async::run_singlethreaded(test)]
529    async fn query_configuration_status_not_supported() {
530        let (facade, paver) = MockPaverBuilder::new().expect_find_boot_manager(None).build();
531
532        let test = async move {
533            assert_matches!(
534                facade
535                    .query_configuration_status(QueryConfigurationStatusRequest {
536                        configuration: Configuration::A
537                    })
538                    .await,
539                Ok(QueryConfigurationStatusResult::NotSupported)
540            );
541        };
542
543        join!(paver, test);
544    }
545
546    #[fuchsia_async::run_singlethreaded(test)]
547    async fn read_asset_ok() {
548        const FILE_CONTENTS: &[u8] = b"hello world!";
549        const FILE_CONTENTS_AS_BASE64: &str = "aGVsbG8gd29ybGQh";
550
551        let request = ReadAssetRequest {
552            configuration: Configuration::A,
553            asset: Asset::VerifiedBootMetadata,
554        };
555
556        let (facade, paver) = MockPaverBuilder::new()
557            .expect_find_data_sink(
558                MockDataSinkBuilder::new().expect_read_asset(request.clone(), FILE_CONTENTS),
559            )
560            .build();
561
562        let test = async move {
563            assert_matches!(
564                facade.read_asset(request).await,
565                Ok(s) if s == FILE_CONTENTS_AS_BASE64
566            );
567        };
568
569        join!(paver, test);
570    }
571}