Skip to main content

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::fuchsia_pkg::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 { epitaph, .. }))
58                        if epitaph == 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 omaha_client::cup_ecdsa::test_support::{
105        make_default_json_public_keys_for_test, make_default_public_keys_for_test,
106    };
107
108    #[fuchsia::test]
109    async fn not_found_tmp() {
110        let configs = EagerPackageConfigs::from_path("/tmp/not-found").await.unwrap();
111        assert_eq!(configs.packages, vec![]);
112    }
113
114    #[fuchsia::test]
115    async fn not_found_pkg() {
116        let configs = EagerPackageConfigs::from_path("/pkg/not-found").await.unwrap();
117        assert_eq!(configs.packages, vec![]);
118    }
119
120    #[fuchsia::test]
121    async fn success() {
122        let json = serde_json::json!({
123            "packages":[
124                {
125                    "url": "fuchsia-pkg://example.com/package_service_1",
126                    "public_keys": make_default_json_public_keys_for_test(),
127                    "minimum_required_version": "1.2.3.4"
128                }
129            ]
130        });
131        assert_eq!(
132            EagerPackageConfigs::from_json(json.to_string().as_bytes()).unwrap(),
133            EagerPackageConfigs {
134                packages: vec![EagerPackageConfig {
135                    url: "fuchsia-pkg://example.com/package_service_1".parse().unwrap(),
136                    executable: false,
137                    public_keys: make_default_public_keys_for_test(),
138                    minimum_required_version: [1, 2, 3, 4].into(),
139                    cache_fallback: true,
140                }]
141            }
142        );
143    }
144
145    #[fuchsia::test]
146    async fn duplicate_path() {
147        let json = serde_json::json!({
148            "packages":[
149                {
150                    "url": "fuchsia-pkg://example.com/package_service_1",
151                    "public_keys": make_default_json_public_keys_for_test(),
152                    "minimum_required_version": "1.2.3.4"
153                },
154                {
155                    "url": "fuchsia-pkg://another-example.com/package_service_1",
156                    "public_keys": make_default_json_public_keys_for_test(),
157                    "minimum_required_version": "1.2.3.4"
158                }
159            ]
160        });
161        assert_matches!(
162            EagerPackageConfigs::from_json(json.to_string().as_bytes()),
163            Err(EagerPackageConfigsError::DuplicatePath)
164        );
165    }
166}