system_image/
cache_packages.rs1use 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 pub fn from_entries(entries: Vec<PinnedAbsolutePackageUrl>) -> Self {
24 CachePackages { contents: entries }
25 }
26
27 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 pub fn contents(&self) -> impl ExactSizeIterator<Item = &PinnedAbsolutePackageUrl> {
42 self.contents.iter()
43 }
44
45 pub fn into_contents(self) -> impl ExactSizeIterator<Item = PinnedAbsolutePackageUrl> {
47 self.contents.into_iter()
48 }
49
50 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 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}