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