fidl_fuchsia_pkg_ext/
base_package_index.rs

1// Copyright 2020 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! Retrives and serves the base package index.
6
7use crate::BlobId;
8use anyhow::Error;
9use fidl_fuchsia_pkg::{PackageCacheProxy, PackageIndexIteratorMarker};
10use fuchsia_url::{AbsolutePackageUrl, UnpinnedAbsolutePackageUrl};
11use std::collections::HashMap;
12
13/// Represents the set of base packages.
14#[derive(Debug)]
15pub struct BasePackageIndex {
16    index: HashMap<UnpinnedAbsolutePackageUrl, BlobId>,
17}
18
19impl BasePackageIndex {
20    /// Creates a `BasePackageIndex` from a PackageCache proxy.
21    pub async fn from_proxy(cache: &PackageCacheProxy) -> Result<Self, Error> {
22        let (pkg_iterator, server_end) =
23            fidl::endpoints::create_proxy::<PackageIndexIteratorMarker>();
24        cache.base_package_index(server_end)?;
25
26        let mut index = HashMap::with_capacity(256);
27        let mut chunk = pkg_iterator.next().await?;
28        while !chunk.is_empty() {
29            for entry in chunk {
30                let pkg_url = UnpinnedAbsolutePackageUrl::parse(&entry.package_url.url)?;
31                let blob_id = BlobId::from(entry.meta_far_blob_id);
32                index.insert(pkg_url, blob_id);
33            }
34            chunk = pkg_iterator.next().await?;
35        }
36        index.shrink_to_fit();
37
38        Ok(Self { index })
39    }
40
41    /// Returns the package's hash if the url is unpinned and refers to a base package, otherwise
42    /// returns None.
43    pub fn is_unpinned_base_package(&self, pkg_url: &AbsolutePackageUrl) -> Option<BlobId> {
44        // Always send Merkle-pinned requests through the resolver.
45        // TODO(https://fxbug.dev/42140778) consider returning the pinned hash if it matches the base hash.
46        let pkg_url = match pkg_url {
47            AbsolutePackageUrl::Unpinned(unpinned) => unpinned,
48            AbsolutePackageUrl::Pinned(_) => return None,
49        };
50
51        // Make sure to strip off a "/0" variant before checking the base index.
52        let stripped_url;
53        let url_without_zero_variant = match pkg_url.variant() {
54            Some(variant) if variant.is_zero() => {
55                let mut url = pkg_url.clone();
56                url.clear_variant();
57                stripped_url = url;
58                &stripped_url
59            }
60            _ => pkg_url,
61        };
62        match self.index.get(&url_without_zero_variant) {
63            Some(base_merkle) => Some(base_merkle.clone()),
64            None => None,
65        }
66    }
67
68    /// Creates a BasePackageIndex backed only by the supplied `HashMap`. This
69    /// is useful for testing.
70    pub fn create_mock(index: HashMap<UnpinnedAbsolutePackageUrl, BlobId>) -> Self {
71        Self { index }
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use fidl::endpoints::create_proxy_and_stream;
79    use fidl_fuchsia_pkg::{
80        self as fpkg, PackageCacheMarker, PackageCacheRequest, PackageCacheRequestStream,
81        PackageIndexEntry, PackageIndexIteratorRequest, PackageIndexIteratorRequestStream,
82    };
83    use fuchsia_async as fasync;
84    use futures::prelude::*;
85    use log::error;
86    use std::sync::Arc;
87
88    // The actual pkg-cache will fit as many items per chunk as possible.  Intentionally choose a
89    // small, fixed value here to verify the BasePackageIndex behavior with multiple chunks without
90    // having to actually send hundreds of entries in these tests.
91    const PACKAGE_INDEX_CHUNK_SIZE: u32 = 30;
92
93    struct MockPackageCacheService {
94        base_packages: Arc<HashMap<UnpinnedAbsolutePackageUrl, BlobId>>,
95    }
96
97    impl MockPackageCacheService {
98        fn new_with_base_packages(
99            base_packages: Arc<HashMap<UnpinnedAbsolutePackageUrl, BlobId>>,
100        ) -> Self {
101            Self { base_packages: base_packages }
102        }
103
104        async fn run_service(self, mut stream: PackageCacheRequestStream) {
105            while let Some(req) = stream.try_next().await.unwrap() {
106                match req {
107                    PackageCacheRequest::BasePackageIndex { iterator, control_handle: _ } => {
108                        let iterator = iterator.into_stream();
109                        self.serve_package_iterator(iterator);
110                    }
111                    _ => panic!("unexpected PackageCache request: {:?}", req),
112                }
113            }
114        }
115
116        fn serve_package_iterator(&self, mut stream: PackageIndexIteratorRequestStream) {
117            let packages = self
118                .base_packages
119                .iter()
120                .map(|(path, hash)| PackageIndexEntry {
121                    package_url: fpkg::PackageUrl {
122                        url: format!("fuchsia-pkg://fuchsia.com/{}", path.name()),
123                    },
124                    meta_far_blob_id: BlobId::from(hash.clone()).into(),
125                })
126                .collect::<Vec<PackageIndexEntry>>();
127
128            fasync::Task::spawn(
129                async move {
130                    let mut iter = packages.chunks(PACKAGE_INDEX_CHUNK_SIZE as usize);
131                    while let Some(request) = stream.try_next().await? {
132                        let PackageIndexIteratorRequest::Next { responder } = request;
133                        responder.send(iter.next().unwrap_or(&[]))?;
134                    }
135                    Ok(())
136                }
137                .unwrap_or_else(|e: fidl::Error| {
138                    error!("while serving package index iterator: {:?}", e)
139                }),
140            )
141            .detach();
142        }
143    }
144
145    async fn spawn_pkg_cache(
146        base_package_index: HashMap<UnpinnedAbsolutePackageUrl, BlobId>,
147    ) -> PackageCacheProxy {
148        let (client, request_stream) = create_proxy_and_stream::<PackageCacheMarker>();
149        let cache = MockPackageCacheService::new_with_base_packages(Arc::new(base_package_index));
150        fasync::Task::spawn(cache.run_service(request_stream)).detach();
151        client
152    }
153
154    #[fasync::run_singlethreaded(test)]
155    async fn empty_base_packages() {
156        let expected_packages = HashMap::new();
157        let client = spawn_pkg_cache(expected_packages.clone()).await;
158        let base = BasePackageIndex::from_proxy(&client).await.unwrap();
159        assert_eq!(base.index, expected_packages);
160    }
161
162    // Generate an index with n unique entries.
163    fn index_with_n_entries(n: u32) -> HashMap<UnpinnedAbsolutePackageUrl, BlobId> {
164        let mut base = HashMap::new();
165        for i in 0..n {
166            let pkg_url = format!("fuchsia-pkg://fuchsia.com/{}", i)
167                .parse::<UnpinnedAbsolutePackageUrl>()
168                .unwrap();
169            let blob_id = format!("{:064}", i).parse::<BlobId>().unwrap();
170            base.insert(pkg_url, blob_id);
171        }
172        base
173    }
174
175    #[fasync::run_singlethreaded(test)]
176    async fn chunk_size_boundary() {
177        let package_counts = [
178            PACKAGE_INDEX_CHUNK_SIZE - 1,
179            PACKAGE_INDEX_CHUNK_SIZE,
180            PACKAGE_INDEX_CHUNK_SIZE + 1,
181            2 * PACKAGE_INDEX_CHUNK_SIZE + 1,
182        ];
183        for count in package_counts.iter() {
184            let expected_packages = index_with_n_entries(*count);
185            let client = spawn_pkg_cache(expected_packages.clone()).await;
186            let base = BasePackageIndex::from_proxy(&client).await.unwrap();
187            assert_eq!(base.index, expected_packages);
188        }
189    }
190
191    fn zeroes_hash() -> BlobId {
192        "0000000000000000000000000000000000000000000000000000000000000000".parse().unwrap()
193    }
194
195    #[test]
196    fn reject_pinned_urls() {
197        let url: AbsolutePackageUrl = "fuchsia-pkg://fuchsia.com/package-name?\
198                   hash=0000000000000000000000000000000000000000000000000000000000000000"
199            .parse()
200            .unwrap();
201        let index = BasePackageIndex { index: [(url.as_unpinned().clone(), zeroes_hash())].into() };
202
203        assert_eq!(index.is_unpinned_base_package(&url), None);
204    }
205
206    #[test]
207    fn strip_0_variant() {
208        let url_no_variant: UnpinnedAbsolutePackageUrl =
209            "fuchsia-pkg://fuchsia.com/package-name".parse().unwrap();
210        let index = BasePackageIndex { index: [(url_no_variant.clone(), zeroes_hash())].into() };
211
212        let url_with_variant: AbsolutePackageUrl =
213            "fuchsia-pkg://fuchsia.com/package-name/0".parse().unwrap();
214        assert_eq!(index.is_unpinned_base_package(&url_with_variant), Some(zeroes_hash()));
215    }
216
217    #[test]
218    fn leave_1_variant() {
219        let url_no_variant: UnpinnedAbsolutePackageUrl =
220            "fuchsia-pkg://fuchsia.com/package-name".parse().unwrap();
221        let index = BasePackageIndex { index: [(url_no_variant.clone(), zeroes_hash())].into() };
222
223        let url_with_variant: AbsolutePackageUrl =
224            "fuchsia-pkg://fuchsia.com/package-name/1".parse().unwrap();
225        assert_eq!(index.is_unpinned_base_package(&url_with_variant), None);
226    }
227}