eager_package_config/
pkg_resolver.rs

1// Copyright 2022 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 fuchsia_url::UnpinnedAbsolutePackageUrl;
6use omaha_client::cup_ecdsa::PublicKeys;
7use omaha_client::version::Version;
8use serde::{Deserialize, Serialize};
9
10#[cfg(target_os = "fuchsia")]
11use {fidl_fuchsia_io as fio, futures::stream::StreamExt as _, std::collections::HashSet};
12
13#[cfg(target_os = "fuchsia")]
14const EAGER_PACKAGE_CONFIG_PATH: &str = "/config/data/eager_package_config.json";
15
16#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
17pub struct EagerPackageConfigs {
18    pub packages: Vec<EagerPackageConfig>,
19}
20
21#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
22pub struct EagerPackageConfig {
23    pub url: UnpinnedAbsolutePackageUrl,
24    #[serde(default, skip_serializing_if = "is_false")]
25    pub executable: bool,
26    pub public_keys: PublicKeys,
27    pub minimum_required_version: Version,
28    #[serde(default = "return_true", skip_serializing_if = "bool::clone")]
29    pub cache_fallback: bool,
30}
31
32#[cfg(target_os = "fuchsia")]
33impl EagerPackageConfigs {
34    /// Read eager config from namespace. Returns an empty instance of `EagerPackageConfigs` in
35    /// case config was not found.
36    pub async fn from_namespace() -> Result<Self, EagerPackageConfigsError> {
37        Self::from_path(EAGER_PACKAGE_CONFIG_PATH).await
38    }
39
40    async fn from_path(path: &str) -> Result<Self, EagerPackageConfigsError> {
41        let proxy = fuchsia_fs::file::open_in_namespace(
42            path,
43            fio::PERM_READABLE | fio::Flags::FLAG_SEND_REPRESENTATION,
44        )
45        .map_err(EagerPackageConfigsError::Open)?;
46        match fuchsia_fs::file::read(&proxy).await {
47            Ok(json) => Self::from_json(&json),
48            Err(e) => {
49                match proxy.take_event_stream().next().await {
50                    // If not found, Open1 sends the NOT_FOUND status in an OnOpen event.
51                    Some(Ok(fio::FileEvent::OnOpen_ { s, .. }))
52                        if s == zx::Status::NOT_FOUND.into_raw() =>
53                    {
54                        Ok(EagerPackageConfigs { packages: Vec::new() })
55                    }
56                    // If not found, Open3 sends the will close the channel with NOT_FOUND status.
57                    Some(Err(fidl::Error::ClientChannelClosed { status, .. }))
58                        if status == zx::Status::NOT_FOUND =>
59                    {
60                        Ok(EagerPackageConfigs { packages: Vec::new() })
61                    }
62                    _ => Err(EagerPackageConfigsError::Read(e)),
63                }
64            }
65        }
66    }
67
68    fn from_json(json: &[u8]) -> Result<Self, EagerPackageConfigsError> {
69        let configs: Self = serde_json::from_slice(json)?;
70        if configs.packages.iter().map(|config| config.url.path()).collect::<HashSet<_>>().len()
71            < configs.packages.len()
72        {
73            return Err(EagerPackageConfigsError::DuplicatePath);
74        }
75        Ok(configs)
76    }
77}
78
79#[cfg(target_os = "fuchsia")]
80#[derive(Debug, thiserror::Error)]
81pub enum EagerPackageConfigsError {
82    #[error("open eager package config")]
83    Open(#[from] fuchsia_fs::node::OpenError),
84    #[error("read eager package config")]
85    Read(#[from] fuchsia_fs::file::ReadError),
86    #[error("parse eager package config from json")]
87    Json(#[from] serde_json::Error),
88    #[error("eager package URL must have unique path")]
89    DuplicatePath,
90}
91
92fn return_true() -> bool {
93    true
94}
95
96fn is_false(b: &bool) -> bool {
97    !b
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use assert_matches::assert_matches;
104    use fuchsia_async as fasync;
105    use omaha_client::cup_ecdsa::test_support::{
106        make_default_json_public_keys_for_test, make_default_public_keys_for_test,
107    };
108
109    #[fasync::run_singlethreaded(test)]
110    async fn not_found_tmp() {
111        let configs = EagerPackageConfigs::from_path("/tmp/not-found").await.unwrap();
112        assert_eq!(configs.packages, vec![]);
113    }
114
115    #[fasync::run_singlethreaded(test)]
116    async fn not_found_pkg() {
117        let configs = EagerPackageConfigs::from_path("/pkg/not-found").await.unwrap();
118        assert_eq!(configs.packages, vec![]);
119    }
120
121    #[fasync::run_singlethreaded(test)]
122    async fn success() {
123        let json = serde_json::json!({
124            "packages":[
125                {
126                    "url": "fuchsia-pkg://example.com/package_service_1",
127                    "public_keys": make_default_json_public_keys_for_test(),
128                    "minimum_required_version": "1.2.3.4"
129                }
130            ]
131        });
132        assert_eq!(
133            EagerPackageConfigs::from_json(json.to_string().as_bytes()).unwrap(),
134            EagerPackageConfigs {
135                packages: vec![EagerPackageConfig {
136                    url: "fuchsia-pkg://example.com/package_service_1".parse().unwrap(),
137                    executable: false,
138                    public_keys: make_default_public_keys_for_test(),
139                    minimum_required_version: [1, 2, 3, 4].into(),
140                    cache_fallback: true,
141                }]
142            }
143        );
144    }
145
146    #[fasync::run_singlethreaded(test)]
147    async fn duplicate_path() {
148        let json = serde_json::json!({
149            "packages":[
150                {
151                    "url": "fuchsia-pkg://example.com/package_service_1",
152                    "public_keys": make_default_json_public_keys_for_test(),
153                    "minimum_required_version": "1.2.3.4"
154                },
155                {
156                    "url": "fuchsia-pkg://another-example.com/package_service_1",
157                    "public_keys": make_default_json_public_keys_for_test(),
158                    "minimum_required_version": "1.2.3.4"
159                }
160            ]
161        });
162        assert_matches!(
163            EagerPackageConfigs::from_json(json.to_string().as_bytes()),
164            Err(EagerPackageConfigsError::DuplicatePath)
165        );
166    }
167}