Skip to main content

system_image/
cache_packages.rs

1// Copyright 2021 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 crate::CachePackagesInitError;
6use fuchsia_hash::Hash;
7use fuchsia_inspect::{self as finspect, ArrayProperty as _};
8use fuchsia_url::fuchsia_pkg::{
9    AbsolutePackageUrl, PinnedAbsolutePackageUrl, UnpinnedAbsolutePackageUrl,
10};
11use futures::FutureExt as _;
12use futures::future::BoxFuture;
13use serde::{Deserialize, Serialize};
14use std::sync::Arc;
15
16#[derive(Debug, PartialEq, Eq)]
17pub struct CachePackages {
18    contents: Vec<PinnedAbsolutePackageUrl>,
19}
20
21impl CachePackages {
22    /// Create a new instance of `CachePackages` containing entries provided.
23    pub fn from_entries(entries: Vec<PinnedAbsolutePackageUrl>) -> Self {
24        CachePackages { contents: entries }
25    }
26
27    /// Create a new instance of `CachePackages` from parsing a json.
28    /// If there are no cache packages, `file_contents` must be empty.
29    pub(crate) fn from_json(file_contents: &[u8]) -> Result<Self, CachePackagesInitError> {
30        if file_contents.is_empty() {
31            return Ok(CachePackages { contents: vec![] });
32        }
33        let contents = parse_json(file_contents)?;
34        if contents.is_empty() {
35            return Err(CachePackagesInitError::NoCachePackages);
36        }
37        Ok(CachePackages { contents })
38    }
39
40    /// Iterator over the contents of the mapping.
41    pub fn contents(&self) -> impl ExactSizeIterator<Item = &PinnedAbsolutePackageUrl> {
42        self.contents.iter()
43    }
44
45    /// Iterator over the contents of the mapping, consuming self.
46    pub fn into_contents(self) -> impl ExactSizeIterator<Item = PinnedAbsolutePackageUrl> {
47        self.contents.into_iter()
48    }
49
50    /// Get the hash for a package.
51    pub fn hash_for_package(&self, pkg: &AbsolutePackageUrl) -> Option<Hash> {
52        self.contents.iter().find_map(|candidate| {
53            if pkg.as_unpinned() == candidate.as_unpinned() {
54                match pkg.hash() {
55                    None => Some(candidate.hash()),
56                    Some(hash) if hash == candidate.hash() => Some(hash),
57                    _ => None,
58                }
59            } else {
60                None
61            }
62        })
63    }
64
65    pub fn serialize(&self, writer: impl std::io::Write) -> Result<(), serde_json::Error> {
66        if self.contents.is_empty() {
67            return Ok(());
68        }
69        let content = Packages { version: "1".to_string(), content: self.contents.clone() };
70        serde_json::to_writer(writer, &content)
71    }
72
73    pub fn find_unpinned_url(
74        &self,
75        url: &UnpinnedAbsolutePackageUrl,
76    ) -> Option<&PinnedAbsolutePackageUrl> {
77        self.contents().find(|pinned_url| pinned_url.as_unpinned() == url)
78    }
79
80    /// Returns a callback to be given to `finspect::Node::record_lazy_values`.
81    /// Creates an array named `array_name`.
82    pub fn record_lazy_inspect(
83        self: &Arc<Self>,
84        array_name: &'static str,
85    ) -> impl Fn() -> BoxFuture<'static, Result<finspect::Inspector, anyhow::Error>>
86    + Send
87    + Sync
88    + 'static {
89        let this = Arc::downgrade(self);
90        move || {
91            let this = this.clone();
92            async move {
93                let inspector = finspect::Inspector::default();
94                if let Some(this) = this.upgrade() {
95                    let root = inspector.root();
96                    let array = root.create_string_array(array_name, this.contents.len());
97                    let () = this
98                        .contents
99                        .iter()
100                        .enumerate()
101                        .for_each(|(i, url)| array.set(i, url.to_string()));
102                    root.record(array);
103                }
104                Ok(inspector)
105            }
106            .boxed()
107        }
108    }
109}
110
111#[derive(Serialize, Deserialize, Debug)]
112#[serde(deny_unknown_fields)]
113struct Packages {
114    version: String,
115    content: Vec<PinnedAbsolutePackageUrl>,
116}
117
118fn parse_json(contents: &[u8]) -> Result<Vec<PinnedAbsolutePackageUrl>, CachePackagesInitError> {
119    match serde_json::from_slice(contents).map_err(CachePackagesInitError::JsonError)? {
120        Packages { ref version, content } if version == "1" => Ok(content),
121        Packages { version, .. } => Err(CachePackagesInitError::VersionNotSupported(version)),
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use assert_matches::assert_matches;
129    use diagnostics_assertions::assert_data_tree;
130
131    #[test]
132    fn populate_from_valid_json() {
133        let file_contents = br#"
134        {
135            "version": "1",
136            "content": [
137                "fuchsia-pkg://foo.bar/qwe/0?hash=0000000000000000000000000000000000000000000000000000000000000000",
138                "fuchsia-pkg://foo.bar/rty/0?hash=1111111111111111111111111111111111111111111111111111111111111111"
139            ]
140        }"#;
141
142        let packages = CachePackages::from_json(file_contents).unwrap();
143        let expected = vec![
144            "fuchsia-pkg://foo.bar/qwe/0?hash=0000000000000000000000000000000000000000000000000000000000000000",
145            "fuchsia-pkg://foo.bar/rty/0?hash=1111111111111111111111111111111111111111111111111111111111111111",
146        ];
147        assert!(packages.into_contents().map(|u| u.to_string()).eq(expected));
148    }
149
150    #[test]
151    fn populate_from_empty_json() {
152        let packages = CachePackages::from_json(b"").unwrap();
153        assert_eq!(packages.into_contents().count(), 0);
154    }
155
156    #[test]
157    fn reject_non_empty_json_with_no_cache_packages() {
158        let file_contents = br#"
159        {
160            "version": "1",
161            "content": []
162        }"#;
163
164        assert_matches!(
165            CachePackages::from_json(file_contents),
166            Err(CachePackagesInitError::NoCachePackages)
167        );
168    }
169
170    #[test]
171    fn test_hash_for_package_returns_none() {
172        let correct_hash = fuchsia_hash::Hash::from([0; 32]);
173        let packages = CachePackages::from_entries(vec![
174            PinnedAbsolutePackageUrl::parse(&format!(
175                "fuchsia-pkg://fuchsia.com/name?hash={correct_hash}"
176            ))
177            .unwrap(),
178        ]);
179        let wrong_hash = fuchsia_hash::Hash::from([1; 32]);
180        assert_eq!(
181            None,
182            packages.hash_for_package(
183                &AbsolutePackageUrl::parse("fuchsia-pkg://fuchsia.com/wrong-name").unwrap()
184            )
185        );
186        assert_eq!(
187            None,
188            packages.hash_for_package(
189                &AbsolutePackageUrl::parse(&format!(
190                    "fuchsia-pkg://fuchsia.com/name?hash={wrong_hash}"
191                ))
192                .unwrap()
193            )
194        );
195    }
196
197    #[test]
198    fn test_hash_for_package_returns_hashes() {
199        let hash = fuchsia_hash::Hash::from([0; 32]);
200        let packages = CachePackages::from_entries(vec![
201            PinnedAbsolutePackageUrl::parse(&format!("fuchsia-pkg://fuchsia.com/name?hash={hash}"))
202                .unwrap(),
203        ]);
204        assert_eq!(
205            Some(hash),
206            packages.hash_for_package(
207                &AbsolutePackageUrl::parse(&format!("fuchsia-pkg://fuchsia.com/name?hash={hash}"))
208                    .unwrap()
209            )
210        );
211        assert_eq!(
212            Some(hash),
213            packages.hash_for_package(
214                &AbsolutePackageUrl::parse("fuchsia-pkg://fuchsia.com/name").unwrap()
215            )
216        );
217    }
218
219    #[test]
220    fn test_serialize() {
221        let hash = fuchsia_hash::Hash::from([0; 32]);
222        let packages = CachePackages::from_entries(vec![
223            PinnedAbsolutePackageUrl::parse(&format!("fuchsia-pkg://foo.bar/qwe/0?hash={hash}"))
224                .unwrap(),
225        ]);
226        let mut bytes = vec![];
227
228        let () = packages.serialize(&mut bytes).unwrap();
229
230        assert_eq!(
231            serde_json::from_slice::<serde_json::Value>(bytes.as_slice()).unwrap(),
232            serde_json::json!({
233                "version": "1",
234                "content": vec![
235                    "fuchsia-pkg://foo.bar/qwe/0?hash=0000000000000000000000000000000000000000000000000000000000000000"
236                    ],
237            })
238        );
239    }
240
241    #[test]
242    fn test_serialize_deserialize_round_trip() {
243        let hash = fuchsia_hash::Hash::from([0; 32]);
244        let packages = CachePackages::from_entries(vec![
245            PinnedAbsolutePackageUrl::parse(&format!("fuchsia-pkg://foo.bar/qwe/0?hash={hash}"))
246                .unwrap(),
247        ]);
248        let mut bytes = vec![];
249
250        packages.serialize(&mut bytes).unwrap();
251
252        assert_eq!(CachePackages::from_json(&bytes).unwrap(), packages);
253    }
254
255    #[fuchsia::test]
256    async fn test_inspect() {
257        let hash = fuchsia_hash::Hash::from([0; 32]);
258        let packages = Arc::new(CachePackages::from_entries(vec![
259            PinnedAbsolutePackageUrl::parse(&format!("fuchsia-pkg://foo.bar/qwe/0?hash={hash}"))
260                .unwrap(),
261            PinnedAbsolutePackageUrl::parse(&format!("fuchsia-pkg://foo.bar/other/0?hash={hash}"))
262                .unwrap(),
263        ]));
264        let inspector = finspect::Inspector::default();
265
266        inspector
267            .root()
268            .record_lazy_values("unused", packages.record_lazy_inspect("cache-packages"));
269
270        assert_data_tree!(inspector, root: {
271            "cache-packages": vec![
272                "fuchsia-pkg://foo.bar/qwe/0?hash=\
273                0000000000000000000000000000000000000000000000000000000000000000",
274                "fuchsia-pkg://foo.bar/other/0?hash=\
275                0000000000000000000000000000000000000000000000000000000000000000",
276            ],
277        });
278    }
279}