fuchsia_pkg_testing/
update_package.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 serde_json::json;
7use sha2::Digest as _;
8use std::collections::BTreeMap;
9
10/// A fake `update_package::UpdatePackage` backed by a temp dir.
11pub struct FakeUpdatePackage {
12    update_pkg: update_package::UpdatePackage,
13    temp_dir: tempfile::TempDir,
14    packages: Vec<String>,
15}
16
17impl FakeUpdatePackage {
18    /// Creates a new TestUpdatePackage with nothing in it.
19    #[allow(clippy::new_without_default)]
20    pub fn new() -> Self {
21        let temp_dir = tempfile::tempdir().expect("/tmp to exist");
22        let update_pkg_proxy = fuchsia_fs::directory::open_in_namespace(
23            temp_dir.path().to_str().unwrap(),
24            fio::PERM_READABLE,
25        )
26        .expect("temp dir to open");
27        Self {
28            temp_dir,
29            update_pkg: update_package::UpdatePackage::new(update_pkg_proxy),
30            packages: vec![],
31        }
32    }
33
34    /// Adds a file to the update package, panics on error.
35    pub async fn add_file(
36        self,
37        path: impl AsRef<std::path::Path>,
38        contents: impl AsRef<[u8]>,
39    ) -> Self {
40        let path = path.as_ref();
41        match path.parent() {
42            Some(empty) if empty == std::path::Path::new("") => {}
43            None => {}
44            Some(parent) => std::fs::create_dir_all(self.temp_dir.path().join(parent)).unwrap(),
45        }
46        fuchsia_fs::file::write_in_namespace(
47            self.temp_dir.path().join(path).to_str().unwrap(),
48            contents,
49        )
50        .await
51        .expect("create test update package file");
52        self
53    }
54
55    /// Adds a package to the update package, panics on error.
56    pub async fn add_package(mut self, package_url: impl Into<String>) -> Self {
57        self.packages.push(package_url.into());
58        let packages_json = json!({
59            "version": "1",
60            "content": self.packages,
61        })
62        .to_string();
63        self.add_file("packages.json", packages_json).await
64    }
65
66    /// Set the hash of the update package, panics on error.
67    pub async fn hash(self, hash: impl AsRef<[u8]>) -> Self {
68        self.add_file("meta", hash).await
69    }
70}
71
72impl std::ops::Deref for FakeUpdatePackage {
73    type Target = update_package::UpdatePackage;
74
75    fn deref(&self) -> &Self::Target {
76        &self.update_pkg
77    }
78}
79
80/// Provided a list of strings representing fuchsia-pkg URLs, constructs
81/// a `packages.json` representing those packages and returns the JSON as a
82/// string.
83pub fn make_packages_json<'a>(urls: impl AsRef<[&'a str]>) -> String {
84    json!({
85      "version": "1",
86      "content": urls.as_ref(),
87    })
88    .to_string()
89}
90
91/// Integration test source epoch
92///
93/// We specifically make the integration tests dependent on this (rather than e.g. u64::MAX) so that
94/// when we bump the epoch, most of the integration tests will fail. To fix this, simply bump this
95/// constant to match the SOURCE epoch. This will encourage developers to think critically about
96/// bumping the epoch and follow the policy documented on fuchsia.dev.
97/// See //src/sys/pkg/bin/system-updater/epoch/playbook.md for information on bumping the epoch.
98pub const SOURCE_EPOCH: u64 = 1;
99
100/// Provided an epoch, constructs an `epoch.json` and returns the JSON as a string.
101pub fn make_epoch_json(epoch: u64) -> String {
102    serde_json::to_string(&epoch::EpochFile::Version1 { epoch }).unwrap()
103}
104
105/// Constructs an `epoch.json` with the current epoch and returns the JSON as a string.
106pub fn make_current_epoch_json() -> String {
107    make_epoch_json(SOURCE_EPOCH)
108}
109
110const IMAGES_PACKAGE_NAME: &str = "update-images";
111
112/// Like `crate::PackageBuilder` except it only makes update packages.
113pub struct UpdatePackageBuilder {
114    images_package_repo: fuchsia_url::RepositoryUrl,
115    epoch: Option<u64>,
116    packages: Vec<fuchsia_url::PinnedAbsolutePackageUrl>,
117    fuchsia_image: Option<(Vec<u8>, Option<Vec<u8>>)>,
118    recovery_image: Option<(Vec<u8>, Option<Vec<u8>>)>,
119    images_package_name: Option<fuchsia_url::PackageName>,
120    firmware_images: BTreeMap<String, Vec<u8>>,
121}
122
123impl UpdatePackageBuilder {
124    /// Images, like the ZBI or firmware, are not stored in the update package, but in a separate
125    /// package(s) referred to by the images manifest in the update package. For convenience, this
126    /// builder takes images by their contents and creates the manifest and separate package, but
127    /// to do so this builder must be given the URL of the repository that will serve the images
128    /// package so that the update package's manifest can reference it.
129    pub fn new(images_package_repo: fuchsia_url::RepositoryUrl) -> Self {
130        Self {
131            images_package_repo,
132            epoch: None,
133            packages: vec![],
134            fuchsia_image: None,
135            recovery_image: None,
136            images_package_name: None,
137            firmware_images: BTreeMap::new(),
138        }
139    }
140
141    /// The update package's epoch, used by the system-updater to prevent updating to an
142    /// incompatible build.
143    /// Defaults to `crate::SOURCE_EPOCH` if not set.
144    pub fn epoch(mut self, epoch: u64) -> Self {
145        assert_eq!(self.epoch, None);
146        self.epoch = Some(epoch);
147        self
148    }
149
150    /// The additional packages (e.g. base and cache) to be resolved during the OTA.
151    pub fn packages(mut self, packages: Vec<fuchsia_url::PinnedAbsolutePackageUrl>) -> Self {
152        assert_eq!(self.packages, vec![]);
153        self.packages = packages;
154        self
155    }
156
157    /// The contents of the zbi and, if provided, vbmeta.
158    pub fn fuchsia_image(mut self, zbi: Vec<u8>, vbmeta: Option<Vec<u8>>) -> Self {
159        assert_eq!(self.fuchsia_image, None);
160        self.fuchsia_image = Some((zbi, vbmeta));
161        self
162    }
163
164    /// The contents of the zbi and, if provided, vbmeta, to write to the recovery slot.
165    pub fn recovery_image(mut self, zbi: Vec<u8>, vbmeta: Option<Vec<u8>>) -> Self {
166        assert_eq!(self.recovery_image, None);
167        self.recovery_image = Some((zbi, vbmeta));
168        self
169    }
170
171    /// The name of the package of images pointed to by the update package's images manifest.
172    /// Defaults to `update-images` if not set.
173    pub fn images_package_name(mut self, name: fuchsia_url::PackageName) -> Self {
174        assert_eq!(self.images_package_name, None);
175        self.images_package_name = Some(name);
176        self
177    }
178
179    /// Any additional firmware to write.
180    pub fn firmware_images(mut self, images: BTreeMap<String, Vec<u8>>) -> Self {
181        assert_eq!(self.firmware_images, BTreeMap::new());
182        self.firmware_images = images;
183        self
184    }
185
186    /// Returns the update package and the package of images referenced by the contained images
187    /// manifest.
188    pub async fn build(self) -> (UpdatePackage, crate::Package) {
189        let Self {
190            images_package_repo,
191            epoch,
192            packages,
193            fuchsia_image,
194            recovery_image,
195            images_package_name,
196            firmware_images,
197        } = self;
198        let images_package_name =
199            images_package_name.unwrap_or_else(|| IMAGES_PACKAGE_NAME.parse().unwrap());
200
201        let mut update = crate::PackageBuilder::new("update")
202            .add_resource_at(
203                "packages.json",
204                update_package::serialize_packages_json(&packages).unwrap().as_slice(),
205            )
206            .add_resource_at(
207                "epoch.json",
208                make_epoch_json(epoch.unwrap_or(SOURCE_EPOCH)).as_bytes(),
209            );
210
211        let mut images = crate::PackageBuilder::new(images_package_name.clone());
212        if let Some((zbi, vbmeta)) = &fuchsia_image {
213            images = images.add_resource_at("zbi", zbi.as_slice());
214            if let Some(vbmeta) = vbmeta.as_ref() {
215                images = images.add_resource_at("vbmeta", vbmeta.as_slice());
216            }
217        }
218        if let Some((zbi, vbmeta)) = &recovery_image {
219            images = images.add_resource_at("recovery-zbi", zbi.as_slice());
220            if let Some(vbmeta) = vbmeta.as_ref() {
221                images = images.add_resource_at("recovery-vbmeta", vbmeta.as_slice());
222            }
223        }
224        for (type_, content) in &firmware_images {
225            images = images.add_resource_at(format!("firmware-{type_}"), content.as_slice());
226        }
227        let images = images.build().await.unwrap();
228
229        let mut images_manifest = update_package::ImagePackagesManifest::builder();
230        if let Some((zbi, vbmeta)) = &fuchsia_image {
231            images_manifest.fuchsia_package(
232                image_metadata(
233                    zbi,
234                    fuchsia_url::AbsoluteComponentUrl::new(
235                        images_package_repo.clone(),
236                        images_package_name.clone(),
237                        None,
238                        Some(*images.hash()),
239                        "zbi".to_owned(),
240                    )
241                    .unwrap(),
242                ),
243                vbmeta.as_ref().map(|v| {
244                    image_metadata(
245                        v,
246                        fuchsia_url::AbsoluteComponentUrl::new(
247                            images_package_repo.clone(),
248                            images_package_name.clone(),
249                            None,
250                            Some(*images.hash()),
251                            "vbmeta".to_owned(),
252                        )
253                        .unwrap(),
254                    )
255                }),
256            );
257        }
258        if let Some((zbi, vbmeta)) = &recovery_image {
259            images_manifest.recovery_package(
260                image_metadata(
261                    zbi,
262                    fuchsia_url::AbsoluteComponentUrl::new(
263                        images_package_repo.clone(),
264                        images_package_name.clone(),
265                        None,
266                        Some(*images.hash()),
267                        "recovery-zbi".to_owned(),
268                    )
269                    .unwrap(),
270                ),
271                vbmeta.as_ref().map(|v| {
272                    image_metadata(
273                        v,
274                        fuchsia_url::AbsoluteComponentUrl::new(
275                            images_package_repo.clone(),
276                            images_package_name.clone(),
277                            None,
278                            Some(*images.hash()),
279                            "recovery-vbmeta".to_owned(),
280                        )
281                        .unwrap(),
282                    )
283                }),
284            );
285        }
286        images_manifest.firmware_package(
287            firmware_images
288                .into_iter()
289                .map(|(type_, content)| {
290                    (
291                        type_.clone(),
292                        image_metadata(
293                            content.as_slice(),
294                            fuchsia_url::AbsoluteComponentUrl::new(
295                                images_package_repo.clone(),
296                                images_package_name.clone(),
297                                None,
298                                Some(*images.hash()),
299                                format!("firmware-{type_}"),
300                            )
301                            .unwrap(),
302                        ),
303                    )
304                })
305                .collect(),
306        );
307        update = update.add_resource_at(
308            "images.json",
309            serde_json::to_vec(&images_manifest.clone().build()).unwrap().as_slice(),
310        );
311        let update = update.build().await.unwrap();
312
313        (UpdatePackage { package: update }, images)
314    }
315}
316
317fn image_metadata(
318    image: &[u8],
319    url: fuchsia_url::AbsoluteComponentUrl,
320) -> update_package::ImageMetadata {
321    let mut hasher = sha2::Sha256::new();
322    let () = hasher.update(image);
323    update_package::ImageMetadata::new(
324        image.len().try_into().unwrap(),
325        fuchsia_hash::Sha256::from(*AsRef::<[u8; 32]>::as_ref(&hasher.finalize())),
326        url,
327    )
328}
329
330/// A `crate::Package` used to drive an OTA.
331pub struct UpdatePackage {
332    package: crate::Package,
333}
334
335impl UpdatePackage {
336    /// The underlying `crate::Package`.
337    pub fn as_package(&self) -> &crate::Package {
338        &self.package
339    }
340
341    /// The underlying `crate::Package`.
342    pub fn into_package(self) -> crate::Package {
343        self.package
344    }
345}