1use 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#[derive(Debug)]
17pub struct PaverFacade {
18 proxy: LazyProxy<PaverMarker>,
19}
20
21impl PaverFacade {
22 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 fn proxy(&self) -> Result<PaverProxy, Error> {
37 self.proxy.get_or_connect()
38 }
39
40 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 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 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 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}