update_package/
packages.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
5use fidl_fuchsia_io as fio;
6use fuchsia_url::PinnedAbsolutePackageUrl;
7use serde::{Deserialize, Serialize};
8
9#[derive(Serialize, Deserialize, Debug)]
10#[serde(tag = "version", content = "content", deny_unknown_fields)]
11enum Packages {
12    #[serde(rename = "1")]
13    V1(Vec<PinnedAbsolutePackageUrl>),
14}
15
16/// ParsePackageError represents any error which might occur while reading
17/// `packages.json` from an update package.
18#[derive(Debug, thiserror::Error)]
19#[allow(missing_docs)]
20pub enum ParsePackageError {
21    #[error("could not open `packages.json`")]
22    FailedToOpen(#[source] fuchsia_fs::node::OpenError),
23
24    #[error("could not parse url from line: {0:?}")]
25    URLParseError(String, #[source] fuchsia_url::errors::ParseError),
26
27    #[error("error reading file `packages.json`")]
28    ReadError(#[source] fuchsia_fs::file::ReadError),
29
30    #[error("json parsing error while reading `packages.json`")]
31    JsonError(#[source] serde_json::error::Error),
32}
33
34/// SerializePackageError represents any error which might occur while writing
35/// `packages.json` for an update package.
36#[derive(Debug, thiserror::Error)]
37#[allow(missing_docs)]
38pub enum SerializePackageError {
39    #[error("serialization error while constructing `packages.json`")]
40    JsonError(#[source] serde_json::error::Error),
41}
42
43/// Returns structured `packages.json` data based on file contents string.
44pub fn parse_packages_json(
45    contents: &[u8],
46) -> Result<Vec<PinnedAbsolutePackageUrl>, ParsePackageError> {
47    match serde_json::from_slice(contents).map_err(ParsePackageError::JsonError)? {
48        Packages::V1(packages) => Ok(packages),
49    }
50}
51
52/// Returns serialized `packages.json` contents based package URLs.
53pub fn serialize_packages_json(
54    pkg_urls: &[PinnedAbsolutePackageUrl],
55) -> Result<Vec<u8>, SerializePackageError> {
56    serde_json::to_vec(&Packages::V1(pkg_urls.into())).map_err(SerializePackageError::JsonError)
57}
58
59/// Returns the list of package urls that go in the universe of this update package.
60pub(crate) async fn packages(
61    proxy: &fio::DirectoryProxy,
62) -> Result<Vec<PinnedAbsolutePackageUrl>, ParsePackageError> {
63    let file = fuchsia_fs::directory::open_file(proxy, "packages.json", fio::PERM_READABLE)
64        .await
65        .map_err(ParsePackageError::FailedToOpen)?;
66
67    let contents = fuchsia_fs::file::read(&file).await.map_err(ParsePackageError::ReadError)?;
68    parse_packages_json(&contents)
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use crate::TestUpdatePackage;
75    use assert_matches::assert_matches;
76    use serde_json::json;
77
78    fn pkg_urls<'a>(v: impl IntoIterator<Item = &'a str>) -> Vec<PinnedAbsolutePackageUrl> {
79        v.into_iter().map(|s| s.parse().unwrap()).collect()
80    }
81
82    #[test]
83    fn smoke_test_parse_packages_json() {
84        let pkg_urls = pkg_urls([
85            "fuchsia-pkg://fuchsia.com/ls/0?hash=71bad1a35b87a073f72f582065f6b6efec7b6a4a129868f37f6131f02107f1ea",
86            "fuchsia-pkg://fuchsia.com/pkg-resolver/0?hash=26d43a3fc32eaa65e6981791874b6ab80fae31fbfca1ce8c31ab64275fd4e8c0",
87        ]);
88        let packages = Packages::V1(pkg_urls.clone());
89        let packages_json = serde_json::to_vec(&packages).unwrap();
90        assert_eq!(parse_packages_json(&packages_json).unwrap(), pkg_urls);
91    }
92
93    #[test]
94    fn smoke_test_serialize_packages_json() {
95        let input = pkg_urls([
96            "fuchsia-pkg://fuchsia.com/ls/0?hash=71bad1a35b87a073f72f582065f6b6efec7b6a4a129868f37f6131f02107f1ea",
97            "fuchsia-pkg://fuchsia.com/pkg-resolver/0?hash=26d43a3fc32eaa65e6981791874b6ab80fae31fbfca1ce8c31ab64275fd4e8c0",
98        ]);
99        let output =
100            parse_packages_json(serialize_packages_json(input.as_slice()).unwrap().as_slice())
101                .unwrap();
102        assert_eq!(input, output);
103    }
104
105    #[test]
106    fn expect_failure_parse_packages_json() {
107        assert_matches!(parse_packages_json(&[]), Err(ParsePackageError::JsonError(_)));
108    }
109
110    #[fuchsia_async::run_singlethreaded(test)]
111    async fn smoke_test_packages_json_version_string() {
112        let pkg_list = [
113            "fuchsia-pkg://fuchsia.com/ls/0?hash=71bad1a35b87a073f72f582065f6b6efec7b6a4a129868f37f6131f02107f1ea",
114            "fuchsia-pkg://fuchsia.com/pkg-resolver/0?hash=26d43a3fc32eaa65e6981791874b6ab80fae31fbfca1ce8c31ab64275fd4e8c0",
115        ];
116        let packages = json!({
117            "version": "1",
118            "content": pkg_list,
119        })
120        .to_string();
121        let update_pkg = TestUpdatePackage::new().add_file("packages.json", packages).await;
122        assert_eq!(update_pkg.packages().await.unwrap(), pkg_urls(pkg_list));
123    }
124
125    #[fuchsia_async::run_singlethreaded(test)]
126    async fn expect_failure_json() {
127        let update_pkg = TestUpdatePackage::new();
128        let packages = "{}";
129        let update_pkg = update_pkg.add_file("packages.json", packages).await;
130        assert_matches!(update_pkg.packages().await, Err(ParsePackageError::JsonError(_)))
131    }
132
133    #[fuchsia_async::run_singlethreaded(test)]
134    async fn expect_failure_version_not_supported() {
135        let pkg_list = vec![
136            "fuchsia-pkg://fuchsia.com/ls/0?hash=71bad1a35b87a073f72f582065f6b6efec7b6a4a129868f37f6131f02107f1ea",
137        ];
138        let packages = json!({
139            "version": "2",
140            "content": pkg_list,
141        })
142        .to_string();
143        let update_pkg = TestUpdatePackage::new().add_file("packages.json", packages).await;
144        assert_matches!(
145            update_pkg.packages().await,
146            Err(ParsePackageError::JsonError(e))
147                if e.to_string().contains("unknown variant `2`, expected `1`")
148        );
149    }
150
151    #[fuchsia_async::run_singlethreaded(test)]
152    async fn expect_failure_no_files() {
153        let update_pkg = TestUpdatePackage::new();
154        assert_matches!(update_pkg.packages().await, Err(ParsePackageError::FailedToOpen(_)))
155    }
156
157    #[test]
158    fn reject_unpinned_urls() {
159        assert_matches!(
160            parse_packages_json(serde_json::json!({
161                "version": "1",
162                "content": ["fuchsia-pkg://fuchsia.example/unpinned"]
163            })
164            .to_string().as_bytes()),
165            Err(ParsePackageError::JsonError(e)) if e.to_string().contains("missing hash")
166        )
167    }
168}