fuchsia_pkg/
package_directory.rs

1// Copyright 2021 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//! Typesafe wrappers around an open package directory.
6
7use crate::{
8    MetaContents, MetaContentsError, MetaPackage, MetaPackageError, MetaSubpackages,
9    MetaSubpackagesError,
10};
11use fidl::endpoints::ServerEnd;
12use fuchsia_hash::{Hash, ParseHashError};
13use thiserror::Error;
14use version_history::AbiRevision;
15use {fidl_fuchsia_io as fio, zx_status};
16
17// re-export wrapped fuchsia_fs errors.
18pub use fuchsia_fs::file::ReadError;
19pub use fuchsia_fs::node::{CloneError, CloseError, OpenError};
20
21/// An error encountered while reading/parsing the package's hash
22#[derive(Debug, Error)]
23#[allow(missing_docs)]
24pub enum ReadHashError {
25    #[error("while reading 'meta/'")]
26    Read(#[source] ReadError),
27
28    #[error("while parsing 'meta/'")]
29    Parse(#[source] ParseHashError),
30}
31
32/// An error encountered while reading/parsing the package's meta/package file
33#[derive(Debug, Error)]
34#[allow(missing_docs)]
35pub enum LoadMetaPackageError {
36    #[error("while reading 'meta/package'")]
37    Read(#[source] ReadError),
38
39    #[error("while parsing 'meta/package'")]
40    Parse(#[source] MetaPackageError),
41}
42
43/// An error encountered while reading/parsing the package's
44/// meta/fuchsia.pkg/subpackages file
45#[derive(Debug, Error)]
46#[allow(missing_docs)]
47pub enum LoadMetaSubpackagesError {
48    #[error("while reading '{}'", MetaSubpackages::PATH)]
49    Read(#[source] ReadError),
50
51    #[error("while parsing '{}'", MetaSubpackages::PATH)]
52    Parse(#[source] MetaSubpackagesError),
53}
54
55/// An error encountered while reading/parsing the package's meta/contents file
56#[derive(Debug, Error)]
57#[allow(missing_docs)]
58pub enum LoadMetaContentsError {
59    #[error("while reading 'meta/contents'")]
60    Read(#[source] ReadError),
61
62    #[error("while parsing 'meta/contents'")]
63    Parse(#[source] MetaContentsError),
64}
65
66/// An error encountered while reading/parsing the package's `AbiRevision`.
67#[derive(Debug, Error)]
68#[allow(missing_docs)]
69pub enum LoadAbiRevisionError {
70    #[error("while opening '{}'", AbiRevision::PATH)]
71    Open(#[from] OpenError),
72
73    #[error("while reading '{}'", AbiRevision::PATH)]
74    Read(#[from] ReadError),
75
76    #[error("while parsing '{}'", AbiRevision::PATH)]
77    Parse(#[from] std::array::TryFromSliceError),
78}
79
80/// An open package directory
81#[derive(Debug, Clone)]
82pub struct PackageDirectory {
83    proxy: fio::DirectoryProxy,
84}
85
86impl PackageDirectory {
87    /// Interprets the provided directory proxy as a package dir.
88    pub fn from_proxy(proxy: fio::DirectoryProxy) -> Self {
89        Self { proxy }
90    }
91
92    /// Creates a new channel pair, returning the client end as Self and the
93    /// server end as a channel.
94    pub fn create_request() -> Result<(Self, ServerEnd<fio::DirectoryMarker>), fidl::Error> {
95        let (proxy, request) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
96        Ok((Self::from_proxy(proxy), request))
97    }
98
99    /// Returns the current component's package directory.
100    #[cfg(target_os = "fuchsia")]
101    pub fn open_from_namespace() -> Result<Self, OpenError> {
102        let dir = fuchsia_fs::directory::open_in_namespace("/pkg", fio::PERM_READABLE)?;
103        Ok(Self::from_proxy(dir))
104    }
105
106    /// Cleanly close the package directory, consuming self.
107    pub async fn close(self) -> Result<(), CloseError> {
108        fuchsia_fs::directory::close(self.proxy).await
109    }
110
111    /// Send request to also serve this package directory on the given directory request.
112    pub fn reopen(&self, dir_request: ServerEnd<fio::DirectoryMarker>) -> Result<(), CloneError> {
113        fuchsia_fs::directory::clone_onto(&self.proxy, dir_request)
114    }
115
116    /// Unwraps the inner DirectoryProxy, consuming self.
117    pub fn into_proxy(self) -> fio::DirectoryProxy {
118        self.proxy
119    }
120
121    /// Read the file in the package given by `path`, and return its contents as
122    /// a UTF-8 decoded string.
123    async fn read_file_to_string(&self, path: &str) -> Result<String, ReadError> {
124        fuchsia_fs::directory::read_file_to_string(&self.proxy, path).await
125    }
126
127    /// Read the file in the package given by `path`, and return its contents.
128    pub async fn read_file(&self, path: &str) -> Result<Vec<u8>, ReadError> {
129        fuchsia_fs::directory::read_file(&self.proxy, path).await
130    }
131
132    /// Reads the merkle root of the package.
133    pub async fn merkle_root(&self) -> Result<Hash, ReadHashError> {
134        let merkle = self.read_file_to_string("meta").await.map_err(ReadHashError::Read)?;
135        merkle.parse().map_err(ReadHashError::Parse)
136    }
137
138    /// Reads and parses the package's meta/contents file.
139    pub async fn meta_contents(&self) -> Result<MetaContents, LoadMetaContentsError> {
140        let meta_contents =
141            self.read_file("meta/contents").await.map_err(LoadMetaContentsError::Read)?;
142        let meta_contents = MetaContents::deserialize(meta_contents.as_slice())
143            .map_err(LoadMetaContentsError::Parse)?;
144        Ok(meta_contents)
145    }
146
147    /// Reads and parses the package's meta/package file.
148    pub async fn meta_package(&self) -> Result<MetaPackage, LoadMetaPackageError> {
149        let meta_package =
150            self.read_file("meta/package").await.map_err(LoadMetaPackageError::Read)?;
151        let meta_package = MetaPackage::deserialize(meta_package.as_slice())
152            .map_err(LoadMetaPackageError::Parse)?;
153        Ok(meta_package)
154    }
155
156    /// Reads and parses the package's meta/fuchsia.pkg/subpackages file. If the file
157    /// doesn't exist, an empty `MetaSubpackages` is returned.
158    pub async fn meta_subpackages(&self) -> Result<MetaSubpackages, LoadMetaSubpackagesError> {
159        match self.read_file(MetaSubpackages::PATH).await {
160            Ok(file) => Ok(MetaSubpackages::deserialize(file.as_slice())
161                .map_err(LoadMetaSubpackagesError::Parse)?),
162            Err(ReadError::Open(OpenError::OpenError(zx_status::Status::NOT_FOUND))) => {
163                Ok(MetaSubpackages::default())
164            }
165            Err(err) => Err(LoadMetaSubpackagesError::Read(err)),
166        }
167    }
168
169    /// Reads and parses the package's meta/fuchsia.abi/abi-revision file.
170    pub async fn abi_revision(&self) -> Result<AbiRevision, LoadAbiRevisionError> {
171        let abi_revision_bytes = self.read_file(AbiRevision::PATH).await?;
172        Ok(AbiRevision::try_from(abi_revision_bytes.as_slice())?)
173    }
174
175    /// Returns an iterator of blobs needed by this package, does not include meta.far blob itself.
176    /// Hashes may appear more than once.
177    pub async fn blobs(&self) -> Result<impl Iterator<Item = Hash>, LoadMetaContentsError> {
178        Ok(self.meta_contents().await?.into_hashes_undeduplicated())
179    }
180}
181
182#[cfg(test)]
183#[cfg(target_os = "fuchsia")]
184mod tests {
185    use super::*;
186    use assert_matches::assert_matches;
187    use fidl::endpoints::Proxy;
188
189    #[fuchsia_async::run_singlethreaded(test)]
190    async fn open_close() {
191        let pkg = PackageDirectory::open_from_namespace().unwrap();
192        let () = pkg.close().await.unwrap();
193    }
194
195    #[fuchsia_async::run_singlethreaded(test)]
196    async fn reopen_is_new_connection() {
197        let pkg = PackageDirectory::open_from_namespace().unwrap();
198
199        let (proxy, server_end) = fidl::endpoints::create_proxy();
200        assert_matches!(pkg.reopen(server_end), Ok(()));
201        assert_matches!(PackageDirectory::from_proxy(proxy).close().await, Ok(()));
202
203        pkg.into_proxy().into_channel().expect("no other users of the wrapped channel");
204    }
205
206    #[fuchsia_async::run_singlethreaded(test)]
207    async fn merkle_root_is_pkg_meta() {
208        let pkg = PackageDirectory::open_from_namespace().unwrap();
209
210        let merkle: Hash = std::fs::read_to_string("/pkg/meta").unwrap().parse().unwrap();
211
212        assert_eq!(pkg.merkle_root().await.unwrap(), merkle);
213    }
214
215    #[fuchsia_async::run_singlethreaded(test)]
216    async fn list_blobs() {
217        let pkg = PackageDirectory::open_from_namespace().unwrap();
218
219        // listing blobs succeeds, and this package has some blobs.
220        let blobs = pkg.blobs().await.unwrap().collect::<Vec<_>>();
221        assert!(!blobs.is_empty());
222
223        // the test duplicate blob appears twice
224        let duplicate_blob_merkle = fuchsia_merkle::from_slice("Hello World!".as_bytes()).root();
225        assert_eq!(blobs.iter().filter(|hash| *hash == &duplicate_blob_merkle).count(), 2);
226    }
227
228    #[fuchsia_async::run_singlethreaded(test)]
229    async fn package_name_is_test_package_name() {
230        let pkg = PackageDirectory::open_from_namespace().unwrap();
231
232        assert_eq!(
233            pkg.meta_package().await.unwrap().into_path().to_string(),
234            "fuchsia-pkg-tests/0"
235        );
236    }
237
238    #[fuchsia_async::run_singlethreaded(test)]
239    async fn missing_subpackages_file_is_empty_subpackages() {
240        let pkg = PackageDirectory::open_from_namespace().unwrap();
241
242        assert_eq!(pkg.meta_subpackages().await.unwrap(), MetaSubpackages::default(),);
243    }
244
245    #[fuchsia_async::run_singlethreaded(test)]
246    async fn abi_revision_succeeds() {
247        let pkg = PackageDirectory::open_from_namespace().unwrap();
248
249        let _: AbiRevision = pkg.abi_revision().await.unwrap();
250    }
251}