Skip to main content

update_package/target/
mod.rs

1// Copyright 2026 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
5mod board;
6mod epoch;
7mod hash;
8mod image;
9mod name;
10
11pub use board::VerifyBoardError;
12pub use epoch::ParseEpochError;
13pub use hash::HashError;
14pub use image::OpenImageError;
15pub use name::VerifyNameError;
16
17use fidl_fuchsia_io as fio;
18use fidl_fuchsia_mem as fmem;
19use fuchsia_hash::Hash;
20use fuchsia_url::fuchsia_pkg::PinnedAbsolutePackageUrl;
21use std::str::FromStr;
22use zx_status::Status;
23
24use crate::images::ImagesMetadata;
25use crate::update_mode::UpdateMode;
26use crate::version::SystemVersion;
27
28/// An error encountered while resolving images.
29#[derive(Debug, thiserror::Error)]
30#[allow(missing_docs)]
31pub enum ResolveImagesError {
32    #[error("while listing files in the update package")]
33    ListCandidates(#[source] fuchsia_fs::directory::EnumerateError),
34}
35
36/// An error encountered while reading the version.
37#[derive(Debug, thiserror::Error)]
38#[allow(missing_docs)]
39pub enum ReadVersionError {
40    #[error("while opening the file")]
41    OpenFile(#[source] fuchsia_fs::node::OpenError),
42
43    #[error("while reading the file")]
44    ReadFile(#[source] fuchsia_fs::file::ReadError),
45}
46
47/// An error encountered while parsing the update-mode file.
48#[derive(Debug, thiserror::Error)]
49#[allow(missing_docs)]
50pub enum ParseUpdateModeError {
51    #[error("while opening the file")]
52    OpenFile(#[source] fuchsia_fs::node::OpenError),
53
54    #[error("while reading the file")]
55    ReadFile(#[source] fuchsia_fs::file::ReadError),
56
57    #[error("while deserializing: '{0:?}'")]
58    Deserialize(String, #[source] serde_json::Error),
59
60    #[error("update mode not supported: '{0}'")]
61    UpdateModeNotSupported(String),
62}
63
64/// ParsePackageError represents any error which might occur while reading
65/// `packages.json` from an update package.
66#[derive(Debug, thiserror::Error)]
67#[allow(missing_docs)]
68pub enum ParsePackageError {
69    #[error("could not open `packages.json`")]
70    FailedToOpen(#[source] fuchsia_fs::node::OpenError),
71
72    #[error("could not parse url from line: {0:?}")]
73    URLParseError(String, #[source] fuchsia_url::errors::ParseError),
74
75    #[error("error reading file `packages.json`")]
76    ReadError(#[source] fuchsia_fs::file::ReadError),
77
78    #[error("json parsing error while reading `packages.json`")]
79    JsonError(#[source] serde_json::error::Error),
80}
81
82impl From<crate::packages::ParsePackageError> for ParsePackageError {
83    fn from(e: crate::packages::ParsePackageError) -> Self {
84        match e {
85            crate::packages::ParsePackageError::URLParseError(s, err) => {
86                Self::URLParseError(s, err)
87            }
88            crate::packages::ParsePackageError::JsonError(err) => Self::JsonError(err),
89        }
90    }
91}
92
93/// An error encountered while loading the images.json manifest.
94#[derive(Debug, thiserror::Error)]
95#[allow(missing_docs)]
96pub enum ImagePackagesError {
97    #[error("`images.json` not present in update package")]
98    NotFound,
99
100    #[error("while opening `images.json`")]
101    Open(#[source] fuchsia_fs::node::OpenError),
102
103    #[error("while reading `images.json`")]
104    Read(#[source] fuchsia_fs::file::ReadError),
105
106    #[error("while parsing `images.json`")]
107    Parse(#[source] serde_json::error::Error),
108}
109
110impl From<crate::images::ImagePackagesError> for ImagePackagesError {
111    fn from(e: crate::images::ImagePackagesError) -> Self {
112        match e {
113            crate::images::ImagePackagesError::NotFound => Self::NotFound,
114            crate::images::ImagePackagesError::Parse(err) => Self::Parse(err),
115        }
116    }
117}
118
119#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
120#[serde(tag = "version", content = "content", deny_unknown_fields)]
121enum UpdateModeFile {
122    #[serde(rename = "1")]
123    Version1 {
124        #[serde(rename = "mode")]
125        update_mode: String,
126    },
127}
128
129/// An open handle to an image package.
130pub struct UpdateImagePackage {
131    proxy: fio::DirectoryProxy,
132}
133
134impl UpdateImagePackage {
135    /// Creates a new [`UpdateImagePackage`] with a given proxy.
136    pub fn new(proxy: fio::DirectoryProxy) -> Self {
137        Self { proxy }
138    }
139
140    /// Opens the image at given `path` as a VMO buffer.
141    ///
142    /// The returned VMO is not resizable. If a resizable VMO is needed (e.g. it will be used with
143    /// some of the `fuchsia.paver/*` protocols), create a child VMO of the returned VMO with
144    /// options `SNAPSHOT_AT_LEAST_ON_WRITE | RESIZABLE`.
145    pub async fn open_image(&self, path: &str) -> Result<fmem::Buffer, OpenImageError> {
146        image::open_from_path(&self.proxy, path).await
147    }
148}
149
150/// An open handle to an "update" package.
151#[derive(Debug)]
152pub struct UpdatePackage {
153    proxy: fio::DirectoryProxy,
154}
155
156impl UpdatePackage {
157    /// Creates a new [`UpdatePackage`] with the given proxy.
158    pub fn new(proxy: fio::DirectoryProxy) -> Self {
159        Self { proxy }
160    }
161
162    /// Verifies that the package's name/variant is "update/0".
163    pub async fn verify_name(&self) -> Result<(), VerifyNameError> {
164        name::verify(&self.proxy).await
165    }
166
167    /// Loads the image packages manifest, or determines that it is not present.
168    pub async fn images_metadata(&self) -> Result<ImagesMetadata, ImagePackagesError> {
169        let file =
170            match fuchsia_fs::directory::open_file(&self.proxy, "images.json", fio::PERM_READABLE)
171                .await
172            {
173                Ok(file) => file,
174                Err(fuchsia_fs::node::OpenError::OpenError(Status::NOT_FOUND)) => {
175                    return Err(ImagePackagesError::NotFound);
176                }
177                Err(e) => return Err(ImagePackagesError::Open(e)),
178            };
179        let contents = fuchsia_fs::file::read(&file).await.map_err(ImagePackagesError::Read)?;
180        crate::images::parse_image_packages_json(&contents).map_err(Into::into).map(Into::into)
181    }
182
183    /// Verifies the board file has the given `contents`.
184    pub async fn verify_board(&self, contents: &str) -> Result<(), VerifyBoardError> {
185        board::verify_board(&self.proxy, contents).await
186    }
187
188    /// Parses the update-mode file to obtain update mode. Returns `Ok(None)` if the update-mode
189    /// file is not present in the update package.
190    pub async fn update_mode(&self) -> Result<Option<UpdateMode>, ParseUpdateModeError> {
191        let fopen_res =
192            fuchsia_fs::directory::open_file(&self.proxy, "update-mode", fio::PERM_READABLE).await;
193        if let Err(fuchsia_fs::node::OpenError::OpenError(Status::NOT_FOUND)) = fopen_res {
194            return Ok(None);
195        }
196        let file = fopen_res.map_err(ParseUpdateModeError::OpenFile)?;
197        let contents = fuchsia_fs::file::read_to_string(&file)
198            .await
199            .map_err(ParseUpdateModeError::ReadFile)?;
200        let UpdateModeFile::Version1 { update_mode: mode_str } = serde_json::from_str(&contents)
201            .map_err(|e| ParseUpdateModeError::Deserialize(contents, e))?;
202        match mode_str.as_str() {
203            "normal" => Ok(Some(UpdateMode::Normal)),
204            "force-recovery" => Ok(Some(UpdateMode::ForceRecovery)),
205            other => Err(ParseUpdateModeError::UpdateModeNotSupported(other.to_string())),
206        }
207    }
208
209    /// Returns the list of package urls that go in the universe of this update package.
210    pub async fn packages(&self) -> Result<Vec<PinnedAbsolutePackageUrl>, ParsePackageError> {
211        let file =
212            fuchsia_fs::directory::open_file(&self.proxy, "packages.json", fio::PERM_READABLE)
213                .await
214                .map_err(ParsePackageError::FailedToOpen)?;
215        let contents = fuchsia_fs::file::read(&file).await.map_err(ParsePackageError::ReadError)?;
216        crate::packages::parse_packages_json(&contents).map_err(Into::into)
217    }
218
219    /// Returns the package hash of this update package.
220    pub async fn hash(&self) -> Result<Hash, HashError> {
221        hash::hash(&self.proxy).await
222    }
223
224    /// Returns the version of this update package.
225    pub async fn version(&self) -> Result<SystemVersion, ReadVersionError> {
226        let file = fuchsia_fs::directory::open_file(&self.proxy, "version", fio::PERM_READABLE)
227            .await
228            .map_err(ReadVersionError::OpenFile)?;
229        let contents =
230            fuchsia_fs::file::read_to_string(&file).await.map_err(ReadVersionError::ReadFile)?;
231        SystemVersion::from_str(&contents).map_err(|e| match e {})
232    }
233
234    /// Parses the epoch.json file to obtain the epoch. Returns `Ok(None)` if the epoch.json file
235    /// is not present in the update package.
236    pub async fn epoch(&self) -> Result<Option<u64>, ParseEpochError> {
237        epoch::epoch(&self.proxy).await
238    }
239}
240
241#[cfg(test)]
242pub(crate) struct TestUpdatePackage {
243    update_pkg: UpdatePackage,
244    temp_dir: tempfile::TempDir,
245}
246
247#[cfg(test)]
248impl TestUpdatePackage {
249    pub(crate) fn new() -> Self {
250        let temp_dir = tempfile::tempdir().expect("/tmp to exist");
251        let update_pkg_proxy = fuchsia_fs::directory::open_in_namespace(
252            temp_dir.path().to_str().unwrap(),
253            fio::PERM_READABLE,
254        )
255        .expect("temp dir to open");
256        Self { temp_dir, update_pkg: UpdatePackage::new(update_pkg_proxy) }
257    }
258
259    pub(crate) async fn add_file(
260        self,
261        path: impl AsRef<std::path::Path>,
262        contents: impl AsRef<[u8]>,
263    ) -> Self {
264        let path = path.as_ref();
265        match path.parent() {
266            Some(empty) if empty == std::path::Path::new("") => {}
267            None => {}
268            Some(parent) => std::fs::create_dir_all(self.temp_dir.path().join(parent)).unwrap(),
269        }
270        fuchsia_fs::file::write_in_namespace(
271            self.temp_dir.path().join(path).to_str().unwrap(),
272            contents,
273        )
274        .await
275        .expect("create test update package file");
276        self
277    }
278}
279
280#[cfg(test)]
281impl std::ops::Deref for TestUpdatePackage {
282    type Target = UpdatePackage;
283
284    fn deref(&self) -> &Self::Target {
285        &self.update_pkg
286    }
287}
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292    use assert_matches::assert_matches;
293    use omaha_client::version::Version as SemanticVersion;
294    use serde_json::json;
295
296    fn pkg_urls<'a>(v: impl IntoIterator<Item = &'a str>) -> Vec<PinnedAbsolutePackageUrl> {
297        v.into_iter().map(|s| s.parse().unwrap()).collect()
298    }
299
300    #[fuchsia::test]
301    async fn lifecycle() {
302        let (proxy, _server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
303        UpdatePackage::new(proxy);
304    }
305
306    #[fuchsia::test]
307    async fn test_packages_json_success() {
308        let pkg_list = [
309            "fuchsia-pkg://fuchsia.com/ls/0?hash=71bad1a35b87a073f72f582065f6b6efec7b6a4a129868f37f6131f02107f1ea",
310            "fuchsia-pkg://fuchsia.com/pkg-resolver/0?hash=26d43a3fc32eaa65e6981791874b6ab80fae31fbfca1ce8c31ab64275fd4e8c0",
311        ];
312        let packages = json!({
313            "version": "1",
314            "content": pkg_list,
315        })
316        .to_string();
317        let update_pkg = TestUpdatePackage::new().add_file("packages.json", packages).await;
318        assert_eq!(update_pkg.packages().await.unwrap(), pkg_urls(pkg_list));
319    }
320
321    #[fuchsia::test]
322    async fn test_packages_json_corrupt() {
323        let update_pkg = TestUpdatePackage::new().add_file("packages.json", "{}").await;
324        assert_matches!(update_pkg.packages().await, Err(ParsePackageError::JsonError(_)));
325    }
326
327    #[fuchsia::test]
328    async fn test_packages_json_version_not_supported() {
329        let pkg_list = vec![
330            "fuchsia-pkg://fuchsia.com/ls/0?hash=71bad1a35b87a073f72f582065f6b6efec7b6a4a129868f37f6131f02107f1ea",
331        ];
332        let packages = json!({
333            "version": "2",
334            "content": pkg_list,
335        })
336        .to_string();
337        let update_pkg = TestUpdatePackage::new().add_file("packages.json", packages).await;
338        assert_matches!(
339            update_pkg.packages().await,
340            Err(ParsePackageError::JsonError(e))
341                if e.to_string().contains("unknown variant `2`, expected `1`")
342        );
343    }
344
345    #[fuchsia::test]
346    async fn test_packages_json_failure() {
347        let update_pkg = TestUpdatePackage::new();
348        assert_matches!(update_pkg.packages().await, Err(ParsePackageError::FailedToOpen(_)));
349    }
350
351    proptest::proptest! {
352        #[test]
353        fn test_json_serialize_roundtrip(s in ".+") {
354            let starting_json_value = json!({
355                "version": "1",
356                "content": {
357                    "mode": &s,
358                },
359            });
360            let deserialized_object: UpdateModeFile =
361                serde_json::from_value(starting_json_value.clone())
362                    .expect("json to deserialize");
363            assert_eq!(deserialized_object, UpdateModeFile::Version1 { update_mode: s });
364
365            let final_json_value =
366                serde_json::to_value(&deserialized_object)
367                    .expect("serialize to value");
368            assert_eq!(final_json_value, starting_json_value);
369        }
370    }
371
372    #[fuchsia::test]
373    async fn test_update_mode_normal() {
374        let p = TestUpdatePackage::new()
375            .add_file(
376                "update-mode",
377                serde_json::to_vec(&UpdateModeFile::Version1 { update_mode: "normal".to_string() })
378                    .unwrap(),
379            )
380            .await;
381        assert_matches!(p.update_mode().await, Ok(Some(UpdateMode::Normal)));
382    }
383
384    #[fuchsia::test]
385    async fn test_update_mode_force_recovery() {
386        let p = TestUpdatePackage::new()
387            .add_file(
388                "update-mode",
389                serde_json::to_vec(&UpdateModeFile::Version1 {
390                    update_mode: "force-recovery".to_string(),
391                })
392                .unwrap(),
393            )
394            .await;
395        assert_matches!(p.update_mode().await, Ok(Some(UpdateMode::ForceRecovery)));
396    }
397
398    #[fuchsia::test]
399    async fn test_update_mode_fail_unsupported_mode() {
400        let p = TestUpdatePackage::new()
401            .add_file(
402                "update-mode",
403                serde_json::to_vec(&UpdateModeFile::Version1 { update_mode: "potato".to_string() })
404                    .unwrap(),
405            )
406            .await;
407        assert_matches!(
408            p.update_mode().await,
409            Err(ParseUpdateModeError::UpdateModeNotSupported(mode)) if mode == "potato"
410        );
411    }
412
413    #[fuchsia::test]
414    async fn test_update_mode_fail_deserialize() {
415        let p = TestUpdatePackage::new().add_file("update-mode", "oh no! this isn't json.").await;
416        assert_matches!(
417            p.update_mode().await,
418            Err(ParseUpdateModeError::Deserialize(s, _)) if s == "oh no! this isn't json."
419        );
420    }
421
422    #[fuchsia::test]
423    async fn test_update_mode_missing() {
424        let p = TestUpdatePackage::new();
425        assert_matches!(p.update_mode().await, Ok(None));
426    }
427
428    #[fuchsia::test]
429    async fn test_version_success_semantic() {
430        let p = TestUpdatePackage::new().add_file("version", "123").await;
431        assert_eq!(
432            p.version().await.unwrap(),
433            SystemVersion::Semantic(SemanticVersion::from([123]))
434        );
435    }
436
437    #[fuchsia::test]
438    async fn test_version_success_opaque() {
439        let p = TestUpdatePackage::new().add_file("version", "2020-09-08T10:17:00+10:00").await;
440        assert_eq!(
441            p.version().await.unwrap(),
442            SystemVersion::Opaque("2020-09-08T10:17:00+10:00".to_owned())
443        );
444    }
445
446    #[fuchsia::test]
447    async fn test_version_trims_trailing_whitespace() {
448        let p = TestUpdatePackage::new().add_file("version", "2020-09-08T10:17:00+10:00\n").await;
449        assert_eq!(
450            p.version().await.unwrap(),
451            SystemVersion::Opaque("2020-09-08T10:17:00+10:00".to_owned())
452        );
453    }
454
455    #[fuchsia::test]
456    async fn test_version_missing() {
457        let p = TestUpdatePackage::new();
458        assert_matches!(p.version().await, Err(ReadVersionError::OpenFile(_)));
459    }
460
461    #[fuchsia::test]
462    async fn test_images_metadata_missing() {
463        let p = TestUpdatePackage::new();
464        assert_matches!(p.images_metadata().await, Err(ImagePackagesError::NotFound));
465    }
466}