blob: b255e5ba64c88761e0e28a3c747b455aa81b4468 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! Retrives and serves the base package index.
use {
crate::BlobId,
anyhow::Error,
fidl_fuchsia_pkg::{PackageCacheProxy, PackageIndexIteratorMarker},
fuchsia_url::{AbsolutePackageUrl, UnpinnedAbsolutePackageUrl},
std::collections::HashMap,
};
/// Represents the set of base packages.
#[derive(Debug)]
pub struct BasePackageIndex {
index: HashMap<UnpinnedAbsolutePackageUrl, BlobId>,
}
impl BasePackageIndex {
/// Creates a `BasePackageIndex` from a PackageCache proxy.
pub async fn from_proxy(cache: &PackageCacheProxy) -> Result<Self, Error> {
let (pkg_iterator, server_end) =
fidl::endpoints::create_proxy::<PackageIndexIteratorMarker>()?;
cache.base_package_index(server_end)?;
let mut index = HashMap::with_capacity(256);
let mut chunk = pkg_iterator.next().await?;
while !chunk.is_empty() {
for entry in chunk {
let pkg_url = UnpinnedAbsolutePackageUrl::parse(&entry.package_url.url)?;
let blob_id = BlobId::from(entry.meta_far_blob_id);
index.insert(pkg_url, blob_id);
}
chunk = pkg_iterator.next().await?;
}
index.shrink_to_fit();
Ok(Self { index })
}
/// Returns the package's hash if the url is unpinned and refers to a base package, otherwise
/// returns None.
pub fn is_unpinned_base_package(&self, pkg_url: &AbsolutePackageUrl) -> Option<BlobId> {
// Always send Merkle-pinned requests through the resolver.
// TODO(https://fxbug.dev/42140778) consider returning the pinned hash if it matches the base hash.
let pkg_url = match pkg_url {
AbsolutePackageUrl::Unpinned(unpinned) => unpinned,
AbsolutePackageUrl::Pinned(_) => return None,
};
// Make sure to strip off a "/0" variant before checking the base index.
let stripped_url;
let url_without_zero_variant = match pkg_url.variant() {
Some(variant) if variant.is_zero() => {
let mut url = pkg_url.clone();
url.clear_variant();
stripped_url = url;
&stripped_url
}
_ => pkg_url,
};
match self.index.get(&url_without_zero_variant) {
Some(base_merkle) => Some(base_merkle.clone()),
None => None,
}
}
/// Creates a BasePackageIndex backed only by the supplied `HashMap`. This
/// is useful for testing.
pub fn create_mock(index: HashMap<UnpinnedAbsolutePackageUrl, BlobId>) -> Self {
Self { index }
}
}
#[cfg(test)]
mod tests {
use {
super::*,
fidl::endpoints::create_proxy_and_stream,
fidl_fuchsia_pkg::{
self as fpkg, PackageCacheMarker, PackageCacheRequest, PackageCacheRequestStream,
PackageIndexEntry, PackageIndexIteratorRequest, PackageIndexIteratorRequestStream,
},
fuchsia_async as fasync,
futures::prelude::*,
std::sync::Arc,
tracing::error,
};
// The actual pkg-cache will fit as many items per chunk as possible. Intentionally choose a
// small, fixed value here to verify the BasePackageIndex behavior with multiple chunks without
// having to actually send hundreds of entries in these tests.
const PACKAGE_INDEX_CHUNK_SIZE: u32 = 30;
struct MockPackageCacheService {
base_packages: Arc<HashMap<UnpinnedAbsolutePackageUrl, BlobId>>,
}
impl MockPackageCacheService {
fn new_with_base_packages(
base_packages: Arc<HashMap<UnpinnedAbsolutePackageUrl, BlobId>>,
) -> Self {
Self { base_packages: base_packages }
}
async fn run_service(self, mut stream: PackageCacheRequestStream) {
while let Some(req) = stream.try_next().await.unwrap() {
match req {
PackageCacheRequest::BasePackageIndex { iterator, control_handle: _ } => {
let iterator = iterator.into_stream().unwrap();
self.serve_package_iterator(iterator);
}
_ => panic!("unexpected PackageCache request: {:?}", req),
}
}
}
fn serve_package_iterator(&self, mut stream: PackageIndexIteratorRequestStream) {
let packages = self
.base_packages
.iter()
.map(|(path, hash)| PackageIndexEntry {
package_url: fpkg::PackageUrl {
url: format!("fuchsia-pkg://fuchsia.com/{}", path.name()),
},
meta_far_blob_id: BlobId::from(hash.clone()).into(),
})
.collect::<Vec<PackageIndexEntry>>();
fasync::Task::spawn(
async move {
let mut iter = packages.chunks(PACKAGE_INDEX_CHUNK_SIZE as usize);
while let Some(request) = stream.try_next().await? {
let PackageIndexIteratorRequest::Next { responder } = request;
responder.send(iter.next().unwrap_or(&[]))?;
}
Ok(())
}
.unwrap_or_else(|e: fidl::Error| {
error!("while serving package index iterator: {:?}", e)
}),
)
.detach();
}
}
async fn spawn_pkg_cache(
base_package_index: HashMap<UnpinnedAbsolutePackageUrl, BlobId>,
) -> PackageCacheProxy {
let (client, request_stream) = create_proxy_and_stream::<PackageCacheMarker>().unwrap();
let cache = MockPackageCacheService::new_with_base_packages(Arc::new(base_package_index));
fasync::Task::spawn(cache.run_service(request_stream)).detach();
client
}
#[fasync::run_singlethreaded(test)]
async fn empty_base_packages() {
let expected_packages = HashMap::new();
let client = spawn_pkg_cache(expected_packages.clone()).await;
let base = BasePackageIndex::from_proxy(&client).await.unwrap();
assert_eq!(base.index, expected_packages);
}
// Generate an index with n unique entries.
fn index_with_n_entries(n: u32) -> HashMap<UnpinnedAbsolutePackageUrl, BlobId> {
let mut base = HashMap::new();
for i in 0..n {
let pkg_url = format!("fuchsia-pkg://fuchsia.com/{}", i)
.parse::<UnpinnedAbsolutePackageUrl>()
.unwrap();
let blob_id = format!("{:064}", i).parse::<BlobId>().unwrap();
base.insert(pkg_url, blob_id);
}
base
}
#[fasync::run_singlethreaded(test)]
async fn chunk_size_boundary() {
let package_counts = [
PACKAGE_INDEX_CHUNK_SIZE - 1,
PACKAGE_INDEX_CHUNK_SIZE,
PACKAGE_INDEX_CHUNK_SIZE + 1,
2 * PACKAGE_INDEX_CHUNK_SIZE + 1,
];
for count in package_counts.iter() {
let expected_packages = index_with_n_entries(*count);
let client = spawn_pkg_cache(expected_packages.clone()).await;
let base = BasePackageIndex::from_proxy(&client).await.unwrap();
assert_eq!(base.index, expected_packages);
}
}
fn zeroes_hash() -> BlobId {
"0000000000000000000000000000000000000000000000000000000000000000".parse().unwrap()
}
#[test]
fn reject_pinned_urls() {
let url: AbsolutePackageUrl = "fuchsia-pkg://fuchsia.com/package-name?\
hash=0000000000000000000000000000000000000000000000000000000000000000"
.parse()
.unwrap();
let index = BasePackageIndex { index: [(url.as_unpinned().clone(), zeroes_hash())].into() };
assert_eq!(index.is_unpinned_base_package(&url), None);
}
#[test]
fn strip_0_variant() {
let url_no_variant: UnpinnedAbsolutePackageUrl =
"fuchsia-pkg://fuchsia.com/package-name".parse().unwrap();
let index = BasePackageIndex { index: [(url_no_variant.clone(), zeroes_hash())].into() };
let url_with_variant: AbsolutePackageUrl =
"fuchsia-pkg://fuchsia.com/package-name/0".parse().unwrap();
assert_eq!(index.is_unpinned_base_package(&url_with_variant), Some(zeroes_hash()));
}
#[test]
fn leave_1_variant() {
let url_no_variant: UnpinnedAbsolutePackageUrl =
"fuchsia-pkg://fuchsia.com/package-name".parse().unwrap();
let index = BasePackageIndex { index: [(url_no_variant.clone(), zeroes_hash())].into() };
let url_with_variant: AbsolutePackageUrl =
"fuchsia-pkg://fuchsia.com/package-name/1".parse().unwrap();
assert_eq!(index.is_unpinned_base_package(&url_with_variant), None);
}
}