Skip to main content

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