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::future::BoxFuture;
10use futures::FutureExt as _;
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![PinnedAbsolutePackageUrl::parse(
172 &format!("fuchsia-pkg://fuchsia.com/name?hash={correct_hash}"),
173 )
174 .unwrap()]);
175 let wrong_hash = fuchsia_hash::Hash::from([1; 32]);
176 assert_eq!(
177 None,
178 packages.hash_for_package(
179 &AbsolutePackageUrl::parse("fuchsia-pkg://fuchsia.com/wrong-name").unwrap()
180 )
181 );
182 assert_eq!(
183 None,
184 packages.hash_for_package(
185 &AbsolutePackageUrl::parse(&format!(
186 "fuchsia-pkg://fuchsia.com/name?hash={wrong_hash}"
187 ))
188 .unwrap()
189 )
190 );
191 }
192
193 #[test]
194 fn test_hash_for_package_returns_hashes() {
195 let hash = fuchsia_hash::Hash::from([0; 32]);
196 let packages = CachePackages::from_entries(vec![PinnedAbsolutePackageUrl::parse(
197 &format!("fuchsia-pkg://fuchsia.com/name?hash={hash}"),
198 )
199 .unwrap()]);
200 assert_eq!(
201 Some(hash),
202 packages.hash_for_package(
203 &AbsolutePackageUrl::parse(&format!("fuchsia-pkg://fuchsia.com/name?hash={hash}"))
204 .unwrap()
205 )
206 );
207 assert_eq!(
208 Some(hash),
209 packages.hash_for_package(
210 &AbsolutePackageUrl::parse("fuchsia-pkg://fuchsia.com/name").unwrap()
211 )
212 );
213 }
214
215 #[test]
216 fn test_serialize() {
217 let hash = fuchsia_hash::Hash::from([0; 32]);
218 let packages = CachePackages::from_entries(vec![PinnedAbsolutePackageUrl::parse(
219 &format!("fuchsia-pkg://foo.bar/qwe/0?hash={hash}"),
220 )
221 .unwrap()]);
222 let mut bytes = vec![];
223
224 let () = packages.serialize(&mut bytes).unwrap();
225
226 assert_eq!(
227 serde_json::from_slice::<serde_json::Value>(bytes.as_slice()).unwrap(),
228 serde_json::json!({
229 "version": "1",
230 "content": vec![
231 "fuchsia-pkg://foo.bar/qwe/0?hash=0000000000000000000000000000000000000000000000000000000000000000"
232 ],
233 })
234 );
235 }
236
237 #[test]
238 fn test_serialize_deserialize_round_trip() {
239 let hash = fuchsia_hash::Hash::from([0; 32]);
240 let packages = CachePackages::from_entries(vec![PinnedAbsolutePackageUrl::parse(
241 &format!("fuchsia-pkg://foo.bar/qwe/0?hash={hash}"),
242 )
243 .unwrap()]);
244 let mut bytes = vec![];
245
246 packages.serialize(&mut bytes).unwrap();
247
248 assert_eq!(CachePackages::from_json(&bytes).unwrap(), packages);
249 }
250
251 #[fuchsia::test]
252 async fn test_inspect() {
253 let hash = fuchsia_hash::Hash::from([0; 32]);
254 let packages = Arc::new(CachePackages::from_entries(vec![
255 PinnedAbsolutePackageUrl::parse(&format!("fuchsia-pkg://foo.bar/qwe/0?hash={hash}"))
256 .unwrap(),
257 PinnedAbsolutePackageUrl::parse(&format!("fuchsia-pkg://foo.bar/other/0?hash={hash}"))
258 .unwrap(),
259 ]));
260 let inspector = finspect::Inspector::default();
261
262 inspector
263 .root()
264 .record_lazy_values("unused", packages.record_lazy_inspect("cache-packages"));
265
266 assert_data_tree!(inspector, root: {
267 "cache-packages": vec![
268 "fuchsia-pkg://foo.bar/qwe/0?hash=\
269 0000000000000000000000000000000000000000000000000000000000000000",
270 "fuchsia-pkg://foo.bar/other/0?hash=\
271 0000000000000000000000000000000000000000000000000000000000000000",
272 ],
273 });
274 }
275}