1use anyhow::{bail, Context, Error};
6use fidl_fuchsia_boot::{ArgumentsMarker, ArgumentsProxy};
7use fidl_fuchsia_buildinfo::{ProviderMarker as BuildInfoMarker, ProviderProxy as BuildInfoProxy};
8use serde::{Deserialize, Serialize};
9use std::fs::File;
10use std::io::BufReader;
11
12const PATH_TO_RECOVERY_CONFIG: &'static str = "/config/data/recovery-config.json";
13const DEFAULT_OMAHA_SERVICE_URL: &'static str =
14 "https://clients2.google.com/service/update2/fuchsia/json";
15
16#[derive(Debug, PartialEq)]
17pub struct RecoveryUpdateConfig {
18 pub channel: String,
19 pub update_type: UpdateType,
20 pub version: String,
21}
22
23#[derive(Debug, PartialEq)]
24pub enum UpdateType {
25 Omaha { app_id: String, service_url: String },
30 Tuf,
32}
33
34impl RecoveryUpdateConfig {
35 pub async fn resolve_update_config() -> Result<RecoveryUpdateConfig, Error> {
38 let json_config =
39 JsonUpdateConfig::load_json_config_data().context("Failed to load json config data")?;
40
41 let boot_args = {
42 let arguments_proxy =
43 fuchsia_component::client::connect_to_protocol::<ArgumentsMarker>()
44 .context("Could not load boot arguments.")?;
45 BootloaderArgs::load_from_proxy(arguments_proxy)
46 .await
47 .context("Unable to connect to fuchsia.boot.Arguments.")
48 }
49 .map_err(|e| {
50 eprintln!(
52 "Error: Error collecting boot argument: '{:?}'. Continuing without boot args.",
53 e
54 );
55 })
56 .unwrap_or_default();
57
58 let build_info_proxy = fuchsia_component::client::connect_to_protocol::<BuildInfoMarker>()?;
59 let resolved_version = Self::resolve_version_from_proxy(&json_config, build_info_proxy)
60 .await
61 .context("Failed to load version")?;
62
63 Self::resolve_update_config_from_structs(boot_args, json_config, resolved_version)
64 }
65
66 async fn resolve_version_from_proxy(
67 json_config: &JsonUpdateConfig,
68 build_info_proxy: BuildInfoProxy,
69 ) -> Result<String, Error> {
70 let version = match &json_config.override_version {
71 Some(version) => version.clone(),
72 None => {
73 let build_info =
74 build_info_proxy.get_build_info().await.context("Failed to read build info")?;
75 build_info.version.context("No version string provided by build info component")?
76 }
77 };
78 Ok(version)
79 }
80
81 fn resolve_update_config_from_structs(
85 boot_args: BootloaderArgs,
86 json_config: JsonUpdateConfig,
87 resolved_version: String,
88 ) -> Result<RecoveryUpdateConfig, Error> {
89 let channel = boot_args.ota_channel.unwrap_or(json_config.default_channel);
91
92 let update_type: UpdateType = match json_config.update_type {
94 JsonUpdateType::Omaha(json_app_id, json_service_url) => {
95 UpdateType::Omaha {
96 app_id: boot_args.omaha_app_id.unwrap_or(json_app_id),
98 service_url: boot_args
100 .omaha_url
101 .or(json_service_url)
102 .unwrap_or_else(|| String::from(DEFAULT_OMAHA_SERVICE_URL)),
103 }
104 }
105 JsonUpdateType::Tuf => UpdateType::Tuf,
106 };
107
108 Ok(RecoveryUpdateConfig {
109 channel: channel,
110 update_type: update_type,
111 version: resolved_version,
112 })
113 }
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
120pub struct JsonUpdateConfig {
121 pub default_channel: String,
122 pub update_type: JsonUpdateType,
123 pub override_version: Option<String>,
124}
125
126#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
128#[serde(rename_all = "snake_case")]
129pub enum JsonUpdateType {
130 Omaha(String, Option<String>),
135 Tuf,
137}
138
139impl JsonUpdateConfig {
140 fn load_json_config_data() -> Result<JsonUpdateConfig, Error> {
141 let ota_config: JsonUpdateConfig = serde_json::from_reader(BufReader::new(
142 File::open(PATH_TO_RECOVERY_CONFIG).context("Failed to find update config data")?,
143 ))?;
144 Ok(ota_config)
145 }
146}
147
148#[derive(Debug, Default, PartialEq)]
152struct BootloaderArgs {
153 omaha_app_id: Option<String>,
154 omaha_url: Option<String>,
155 ota_channel: Option<String>,
156}
157
158impl BootloaderArgs {
159 async fn load_from_proxy(arguments_proxy: ArgumentsProxy) -> Result<BootloaderArgs, Error> {
160 let keys = &["omaha_app_id".to_owned(), "omaha_url".to_owned(), "ota_channel".to_owned()];
161 let num_keys = keys.len();
162 let boot_args: Vec<Option<String>> =
163 arguments_proxy.get_strings(keys).await.context("No boot args available.")?;
164 if boot_args.len() != num_keys {
165 bail!("Boot args returned {} values, expected {}", boot_args.len(), num_keys);
166 }
167 Ok(BootloaderArgs {
168 omaha_app_id: boot_args[0].clone(),
169 omaha_url: boot_args[1].clone(),
170 ota_channel: boot_args[2].clone(),
171 })
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use assert_matches::assert_matches;
179 use fidl::endpoints::create_proxy_and_stream;
180 use fidl_fuchsia_buildinfo::{BuildInfo, ProviderRequest as BuildInfoRequest};
181 use fuchsia_async as fasync;
182 use futures::prelude::*;
183 use maplit::hashmap;
184 use mock_boot_arguments::MockBootArgumentsService;
185 use pretty_assertions::assert_eq;
186 use std::collections::HashMap;
187 use std::sync::Arc;
188
189 fn empty_bootloader_args() -> BootloaderArgs {
190 BootloaderArgs { omaha_app_id: None, omaha_url: None, ota_channel: None }
191 }
192
193 #[test]
195 fn resolve_config_expected_in_field_use_case() {
196 let boot_args = BootloaderArgs {
197 omaha_app_id: Some("avb_app_id".to_string()),
198 omaha_url: None,
199 ota_channel: Some("avb_channel".to_string()),
200 };
201 let json_config = JsonUpdateConfig {
202 default_channel: "json_channel".to_string(),
203 update_type: JsonUpdateType::Omaha("json_app_id".to_string(), None),
204 override_version: None,
205 };
206
207 let resolved_config = RecoveryUpdateConfig::resolve_update_config_from_structs(
208 boot_args,
209 json_config,
210 "resolved_version".to_string(),
211 );
212 assert_eq!(
213 resolved_config.unwrap(),
214 RecoveryUpdateConfig {
215 channel: "avb_channel".to_string(),
216 update_type: UpdateType::Omaha {
217 app_id: "avb_app_id".to_string(),
218 service_url: DEFAULT_OMAHA_SERVICE_URL.to_string(),
219 },
220 version: "resolved_version".to_string(),
221 }
222 );
223 }
224
225 #[test]
226 fn resolve_tuf_config_without_boot_args() {
227 let boot_args = empty_bootloader_args();
228 let json_config = JsonUpdateConfig {
229 default_channel: "json_channel".to_string(),
230 update_type: JsonUpdateType::Tuf,
231 override_version: None,
232 };
233
234 let resolved_config = RecoveryUpdateConfig::resolve_update_config_from_structs(
235 boot_args,
236 json_config,
237 "resolved_version".to_string(),
238 );
239 assert_eq!(
240 resolved_config.unwrap(),
241 RecoveryUpdateConfig {
242 channel: "json_channel".to_string(),
243 update_type: UpdateType::Tuf,
244 version: "resolved_version".to_string(),
245 }
246 );
247 }
248
249 #[test]
250 fn resolve_tuf_config_with_boot_override() {
251 let boot_args = BootloaderArgs {
252 omaha_app_id: None,
253 omaha_url: None,
254 ota_channel: Some("avb_channel".to_string()),
255 };
256 let json_config = JsonUpdateConfig {
257 default_channel: "json_channel".to_string(),
258 update_type: JsonUpdateType::Tuf,
259 override_version: Some("json_version".to_string()),
260 };
261
262 let resolved_config = RecoveryUpdateConfig::resolve_update_config_from_structs(
263 boot_args,
264 json_config,
265 "resolved_version".to_string(),
266 );
267 assert_eq!(
268 resolved_config.unwrap(),
269 RecoveryUpdateConfig {
270 channel: "avb_channel".to_string(),
271 update_type: UpdateType::Tuf,
272 version: "resolved_version".to_string(),
273 }
274 );
275 }
276
277 #[test]
278 fn resolve_omaha_config_without_boot_args() {
279 let boot_args = empty_bootloader_args();
280 let json_config = JsonUpdateConfig {
281 default_channel: "json_channel".to_string(),
282 update_type: JsonUpdateType::Omaha("json_app_id".to_string(), None),
283 override_version: None,
284 };
285
286 let resolved_config = RecoveryUpdateConfig::resolve_update_config_from_structs(
287 boot_args,
288 json_config,
289 "resolved_version".to_string(),
290 );
291 assert_eq!(
292 resolved_config.unwrap(),
293 RecoveryUpdateConfig {
294 channel: "json_channel".to_string(),
295 update_type: UpdateType::Omaha {
296 app_id: "json_app_id".to_string(),
297 service_url: DEFAULT_OMAHA_SERVICE_URL.to_string(),
298 },
299 version: "resolved_version".to_string(),
300 }
301 );
302 }
303
304 #[test]
305 fn resolve_omaha_json_version_and_url() {
306 let boot_args = empty_bootloader_args();
307 let json_config = JsonUpdateConfig {
308 default_channel: "json_channel".to_string(),
309 update_type: JsonUpdateType::Omaha(
310 "json_app_id".to_string(),
311 Some("json_url".to_string()),
312 ),
313 override_version: Some("json_version".to_string()),
314 };
315
316 let resolved_config = RecoveryUpdateConfig::resolve_update_config_from_structs(
317 boot_args,
318 json_config,
319 "resolved_version".to_string(),
320 );
321 assert_eq!(
322 resolved_config.unwrap(),
323 RecoveryUpdateConfig {
324 channel: "json_channel".to_string(),
325 update_type: UpdateType::Omaha {
326 app_id: "json_app_id".to_string(),
327 service_url: "json_url".to_string(),
328 },
329 version: "resolved_version".to_string(),
330 }
331 );
332 }
333
334 #[test]
335 fn resolve_omaha_all_boot_args() {
336 let boot_args = BootloaderArgs {
337 omaha_app_id: Some("avb_app_id".to_string()),
338 omaha_url: Some("avb_url".to_string()),
339 ota_channel: Some("avb_channel".to_string()),
340 };
341 let json_config = JsonUpdateConfig {
342 default_channel: "json_channel".to_string(),
343 update_type: JsonUpdateType::Omaha("json_app_id".to_string(), None),
344 override_version: None,
345 };
346
347 let resolved_config = RecoveryUpdateConfig::resolve_update_config_from_structs(
348 boot_args,
349 json_config,
350 "resolved_version".to_string(),
351 );
352 assert_eq!(
353 resolved_config.unwrap(),
354 RecoveryUpdateConfig {
355 channel: "avb_channel".to_string(),
356 update_type: UpdateType::Omaha {
357 app_id: "avb_app_id".to_string(),
358 service_url: "avb_url".to_string(),
359 },
360 version: "resolved_version".to_string(),
361 }
362 );
363 }
364
365 #[test]
366 fn resolve_omaha_all_boot_args_with_json_url() {
367 let boot_args = BootloaderArgs {
368 omaha_app_id: Some("avb_app_id".to_string()),
369 omaha_url: Some("avb_url".to_string()),
370 ota_channel: Some("avb_channel".to_string()),
371 };
372 let json_config = JsonUpdateConfig {
373 default_channel: "json_channel".to_string(),
374 update_type: JsonUpdateType::Omaha(
375 "json_app_id".to_string(),
376 Some("json_url".to_string()),
377 ),
378 override_version: None,
379 };
380
381 let resolved_config = RecoveryUpdateConfig::resolve_update_config_from_structs(
382 boot_args,
383 json_config,
384 "resolved_version".to_string(),
385 );
386 assert_eq!(
387 resolved_config.unwrap(),
388 RecoveryUpdateConfig {
389 channel: "avb_channel".to_string(),
390 update_type: UpdateType::Omaha {
391 app_id: "avb_app_id".to_string(),
392 service_url: "avb_url".to_string(),
393 },
394 version: "resolved_version".to_string(),
395 }
396 );
397 }
398
399 async fn spawn_boot_arg_server_with_values(
401 args: HashMap<String, Option<String>>,
402 ) -> ArgumentsProxy {
403 let mock = Arc::new(MockBootArgumentsService::new(args));
404 let (proxy, stream) = create_proxy_and_stream::<ArgumentsMarker>();
405 fasync::Task::spawn(mock.handle_request_stream(stream)).detach();
406 proxy
407 }
408
409 #[fuchsia::test]
410 async fn full_boot_args_from_proxy() {
411 let proxy = spawn_boot_arg_server_with_values(hashmap! {
412 "omaha_app_id".to_string() => Some("avb_app_id".to_string()),
413 "ota_channel".to_string() => Some("avb_channel".to_string()),
414 "omaha_url".to_string() => Some("avb_url".to_string()),
415 })
416 .await;
417
418 let args = BootloaderArgs::load_from_proxy(proxy).await.unwrap();
419
420 assert_eq!(
421 args,
422 BootloaderArgs {
423 omaha_app_id: Some("avb_app_id".to_string()),
424 omaha_url: Some("avb_url".to_string()),
425 ota_channel: Some("avb_channel".to_string()),
426 }
427 );
428 }
429
430 #[fuchsia::test]
431 async fn partial_boot_args_from_proxy() {
432 let proxy = spawn_boot_arg_server_with_values(hashmap! {
433 "omaha_url".to_string() => Some("avb_url".to_string())
434 })
435 .await;
436 let args = BootloaderArgs::load_from_proxy(proxy).await.unwrap();
437
438 assert_eq!(
439 args,
440 BootloaderArgs {
441 omaha_app_id: None,
442 omaha_url: Some("avb_url".to_string()),
443 ota_channel: None,
444 }
445 );
446 }
447
448 #[fuchsia::test]
449 async fn empty_boot_args_from_proxy() {
450 let proxy = spawn_boot_arg_server_with_values(hashmap! {}).await;
451 let args = BootloaderArgs::load_from_proxy(proxy).await.unwrap();
452
453 assert_eq!(args, BootloaderArgs { omaha_app_id: None, omaha_url: None, ota_channel: None });
454 }
455
456 #[fuchsia::test]
458 async fn resolve_version_from_proxy() {
459 let (proxy, mut stream) = create_proxy_and_stream::<BuildInfoMarker>();
460 fasync::Task::local(async move {
461 match stream.next().await.unwrap() {
462 Ok(BuildInfoRequest::GetBuildInfo { responder }) => {
463 responder
464 .send(&BuildInfo {
465 version: Some("proxy_version".to_string()),
466 ..Default::default()
467 })
468 .unwrap();
469 }
470 request => panic!("Unexpected request: {:?}", request),
471 }
472 })
473 .detach();
474
475 let json_config = JsonUpdateConfig {
476 default_channel: "json_channel".to_string(),
477 update_type: JsonUpdateType::Tuf,
478 override_version: None,
479 };
480 let version =
481 RecoveryUpdateConfig::resolve_version_from_proxy(&json_config, proxy).await.unwrap();
482
483 assert_eq!(version, "proxy_version".to_string());
484 }
485
486 #[fuchsia::test]
487 async fn resolve_version_from_json_override() {
488 let (build_info_proxy, mut stream) = create_proxy_and_stream::<BuildInfoMarker>();
489 fasync::Task::local(async move {
490 let _ = stream.next().await.unwrap();
491 panic!("Runtime version should not be queried if override provided");
492 })
493 .detach();
494 let json_config = JsonUpdateConfig {
495 default_channel: "json_channel".to_string(),
496 update_type: JsonUpdateType::Tuf,
497 override_version: Some("json_version".to_string()),
498 };
499 let version =
500 RecoveryUpdateConfig::resolve_version_from_proxy(&json_config, build_info_proxy)
501 .await
502 .unwrap();
503
504 assert_eq!(version, "json_version".to_string());
505 }
506
507 #[fuchsia::test]
508 async fn error_when_no_version_available() {
509 let (build_info_proxy, mut stream) = create_proxy_and_stream::<BuildInfoMarker>();
510 fasync::Task::local(async move {
511 match stream.next().await.unwrap() {
512 Ok(BuildInfoRequest::GetBuildInfo { responder }) => {
513 responder.send(&BuildInfo::default()).unwrap();
514 }
515 request => panic!("Unexpected request: {:?}", request),
516 }
517 })
518 .detach();
519
520 let json_config = JsonUpdateConfig {
521 default_channel: "json_channel".to_string(),
522 update_type: JsonUpdateType::Tuf,
523 override_version: None,
524 };
525
526 let version_result =
527 RecoveryUpdateConfig::resolve_version_from_proxy(&json_config, build_info_proxy).await;
528 assert_matches!(version_result, Err(_));
529 }
530
531 #[test]
533 fn test_no_json_channel() {
534 let string_version = r#"
535 {
536 "override_version": "1.2.3.4",
537 "update_type": "tuf"
538 }"#;
539 let res: Result<JsonUpdateConfig, serde_json::Error> = serde_json::from_str(string_version);
540 assert_matches!(res, Err(_));
541 }
542
543 #[test]
544 fn test_omaha_config_new_url() {
545 let a = JsonUpdateConfig {
546 default_channel: "some_channel".to_string(),
547 override_version: None,
548 update_type: JsonUpdateType::Omaha(
549 "app_id_here".to_string(),
550 Some("https://override.google.com".to_string()),
551 ),
552 };
553 let string_version = r#"{
554 "default_channel": "some_channel",
555 "update_type": {
556 "omaha": [
557 "app_id_here",
558 "https://override.google.com"
559 ]
560 }
561 }"#;
562 assert_eq!(a, serde_json::from_str(string_version).unwrap());
563 }
564
565 #[test]
566 fn test_omaha_config() {
567 let a = JsonUpdateConfig {
568 default_channel: "channel_from_json".to_string(),
569 override_version: None,
570 update_type: JsonUpdateType::Omaha("app_id_here".to_string(), None),
571 };
572 let string_version = r#"{
573 "default_channel": "channel_from_json",
574 "update_type": {
575 "omaha": [
576 "app_id_here", null
577 ]
578 }
579 }"#;
580 assert_eq!(a, serde_json::from_str(string_version).unwrap());
581 }
582
583 #[test]
584 fn test_tuf_config() {
585 let a = JsonUpdateConfig {
586 default_channel: "channel_from_json".to_string(),
587 override_version: Some("1.2.3.4".to_string()),
588 update_type: JsonUpdateType::Tuf,
589 };
590 let string_version = r#"
591 {
592 "default_channel": "channel_from_json",
593 "override_version": "1.2.3.4",
594 "update_type": "tuf"
595 }"#;
596 assert_eq!(a, serde_json::from_str(string_version).unwrap());
597 }
598}