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