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