system_image/
cache_packages.rsuse crate::CachePackagesInitError;
use fuchsia_hash::Hash;
use fuchsia_inspect::{self as finspect, ArrayProperty as _};
use fuchsia_url::{AbsolutePackageUrl, PinnedAbsolutePackageUrl, UnpinnedAbsolutePackageUrl};
use futures::future::BoxFuture;
use futures::FutureExt as _;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Debug, PartialEq, Eq)]
pub struct CachePackages {
contents: Vec<PinnedAbsolutePackageUrl>,
}
impl CachePackages {
pub fn from_entries(entries: Vec<PinnedAbsolutePackageUrl>) -> Self {
CachePackages { contents: entries }
}
pub(crate) fn from_json(file_contents: &[u8]) -> Result<Self, CachePackagesInitError> {
if file_contents.is_empty() {
return Ok(CachePackages { contents: vec![] });
}
let contents = parse_json(file_contents)?;
if contents.is_empty() {
return Err(CachePackagesInitError::NoCachePackages);
}
Ok(CachePackages { contents })
}
pub fn contents(&self) -> impl ExactSizeIterator<Item = &PinnedAbsolutePackageUrl> {
self.contents.iter()
}
pub fn into_contents(self) -> impl ExactSizeIterator<Item = PinnedAbsolutePackageUrl> {
self.contents.into_iter()
}
pub fn hash_for_package(&self, pkg: &AbsolutePackageUrl) -> Option<Hash> {
self.contents.iter().find_map(|candidate| {
if pkg.as_unpinned() == candidate.as_unpinned() {
match pkg.hash() {
None => Some(candidate.hash()),
Some(hash) if hash == candidate.hash() => Some(hash),
_ => None,
}
} else {
None
}
})
}
pub fn serialize(&self, writer: impl std::io::Write) -> Result<(), serde_json::Error> {
if self.contents.is_empty() {
return Ok(());
}
let content = Packages { version: "1".to_string(), content: self.contents.clone() };
serde_json::to_writer(writer, &content)
}
pub fn find_unpinned_url(
&self,
url: &UnpinnedAbsolutePackageUrl,
) -> Option<&PinnedAbsolutePackageUrl> {
self.contents().find(|pinned_url| pinned_url.as_unpinned() == url)
}
pub fn record_lazy_inspect(
self: &Arc<Self>,
array_name: &'static str,
) -> impl Fn() -> BoxFuture<'static, Result<finspect::Inspector, anyhow::Error>>
+ Send
+ Sync
+ 'static {
let this = Arc::downgrade(self);
move || {
let this = this.clone();
async move {
let inspector = finspect::Inspector::default();
if let Some(this) = this.upgrade() {
let root = inspector.root();
let array = root.create_string_array(array_name, this.contents.len());
let () = this
.contents
.iter()
.enumerate()
.for_each(|(i, url)| array.set(i, url.to_string()));
root.record(array);
}
Ok(inspector)
}
.boxed()
}
}
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
struct Packages {
version: String,
content: Vec<PinnedAbsolutePackageUrl>,
}
fn parse_json(contents: &[u8]) -> Result<Vec<PinnedAbsolutePackageUrl>, CachePackagesInitError> {
match serde_json::from_slice(contents).map_err(CachePackagesInitError::JsonError)? {
Packages { ref version, content } if version == "1" => Ok(content),
Packages { version, .. } => Err(CachePackagesInitError::VersionNotSupported(version)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use assert_matches::assert_matches;
use diagnostics_assertions::assert_data_tree;
#[test]
fn populate_from_valid_json() {
let file_contents = br#"
{
"version": "1",
"content": [
"fuchsia-pkg://foo.bar/qwe/0?hash=0000000000000000000000000000000000000000000000000000000000000000",
"fuchsia-pkg://foo.bar/rty/0?hash=1111111111111111111111111111111111111111111111111111111111111111"
]
}"#;
let packages = CachePackages::from_json(file_contents).unwrap();
let expected = vec![
"fuchsia-pkg://foo.bar/qwe/0?hash=0000000000000000000000000000000000000000000000000000000000000000",
"fuchsia-pkg://foo.bar/rty/0?hash=1111111111111111111111111111111111111111111111111111111111111111"
];
assert!(packages.into_contents().map(|u| u.to_string()).eq(expected));
}
#[test]
fn populate_from_empty_json() {
let packages = CachePackages::from_json(b"").unwrap();
assert_eq!(packages.into_contents().count(), 0);
}
#[test]
fn reject_non_empty_json_with_no_cache_packages() {
let file_contents = br#"
{
"version": "1",
"content": []
}"#;
assert_matches!(
CachePackages::from_json(file_contents),
Err(CachePackagesInitError::NoCachePackages)
);
}
#[test]
fn test_hash_for_package_returns_none() {
let correct_hash = fuchsia_hash::Hash::from([0; 32]);
let packages = CachePackages::from_entries(vec![PinnedAbsolutePackageUrl::parse(
&format!("fuchsia-pkg://fuchsia.com/name?hash={correct_hash}"),
)
.unwrap()]);
let wrong_hash = fuchsia_hash::Hash::from([1; 32]);
assert_eq!(
None,
packages.hash_for_package(
&AbsolutePackageUrl::parse("fuchsia-pkg://fuchsia.com/wrong-name").unwrap()
)
);
assert_eq!(
None,
packages.hash_for_package(
&AbsolutePackageUrl::parse(&format!(
"fuchsia-pkg://fuchsia.com/name?hash={wrong_hash}"
))
.unwrap()
)
);
}
#[test]
fn test_hash_for_package_returns_hashes() {
let hash = fuchsia_hash::Hash::from([0; 32]);
let packages = CachePackages::from_entries(vec![PinnedAbsolutePackageUrl::parse(
&format!("fuchsia-pkg://fuchsia.com/name?hash={hash}"),
)
.unwrap()]);
assert_eq!(
Some(hash),
packages.hash_for_package(
&AbsolutePackageUrl::parse(&format!("fuchsia-pkg://fuchsia.com/name?hash={hash}"))
.unwrap()
)
);
assert_eq!(
Some(hash),
packages.hash_for_package(
&AbsolutePackageUrl::parse("fuchsia-pkg://fuchsia.com/name").unwrap()
)
);
}
#[test]
fn test_serialize() {
let hash = fuchsia_hash::Hash::from([0; 32]);
let packages = CachePackages::from_entries(vec![PinnedAbsolutePackageUrl::parse(
&format!("fuchsia-pkg://foo.bar/qwe/0?hash={hash}"),
)
.unwrap()]);
let mut bytes = vec![];
let () = packages.serialize(&mut bytes).unwrap();
assert_eq!(
serde_json::from_slice::<serde_json::Value>(bytes.as_slice()).unwrap(),
serde_json::json!({
"version": "1",
"content": vec![
"fuchsia-pkg://foo.bar/qwe/0?hash=0000000000000000000000000000000000000000000000000000000000000000"
],
})
);
}
#[test]
fn test_serialize_deserialize_round_trip() {
let hash = fuchsia_hash::Hash::from([0; 32]);
let packages = CachePackages::from_entries(vec![PinnedAbsolutePackageUrl::parse(
&format!("fuchsia-pkg://foo.bar/qwe/0?hash={hash}"),
)
.unwrap()]);
let mut bytes = vec![];
packages.serialize(&mut bytes).unwrap();
assert_eq!(CachePackages::from_json(&bytes).unwrap(), packages);
}
#[fuchsia::test]
async fn test_inspect() {
let hash = fuchsia_hash::Hash::from([0; 32]);
let packages = Arc::new(CachePackages::from_entries(vec![
PinnedAbsolutePackageUrl::parse(&format!("fuchsia-pkg://foo.bar/qwe/0?hash={hash}"))
.unwrap(),
PinnedAbsolutePackageUrl::parse(&format!("fuchsia-pkg://foo.bar/other/0?hash={hash}"))
.unwrap(),
]));
let inspector = finspect::Inspector::default();
inspector
.root()
.record_lazy_values("unused", packages.record_lazy_inspect("cache-packages"));
assert_data_tree!(inspector, root: {
"cache-packages": vec![
"fuchsia-pkg://foo.bar/qwe/0?hash=\
0000000000000000000000000000000000000000000000000000000000000000",
"fuchsia-pkg://foo.bar/other/0?hash=\
0000000000000000000000000000000000000000000000000000000000000000",
],
});
}
}