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 resizable VMO buffer.
141    pub async fn open_image(&self, path: &str) -> Result<fmem::Buffer, OpenImageError> {
142        image::open_from_path(&self.proxy, path).await
143    }
144}
145
146/// An open handle to an "update" package.
147#[derive(Debug)]
148pub struct UpdatePackage {
149    proxy: fio::DirectoryProxy,
150}
151
152impl UpdatePackage {
153    /// Creates a new [`UpdatePackage`] with the given proxy.
154    pub fn new(proxy: fio::DirectoryProxy) -> Self {
155        Self { proxy }
156    }
157
158    /// Verifies that the package's name/variant is "update/0".
159    pub async fn verify_name(&self) -> Result<(), VerifyNameError> {
160        name::verify(&self.proxy).await
161    }
162
163    /// Loads the image packages manifest, or determines that it is not present.
164    pub async fn images_metadata(&self) -> Result<ImagesMetadata, ImagePackagesError> {
165        let file =
166            match fuchsia_fs::directory::open_file(&self.proxy, "images.json", fio::PERM_READABLE)
167                .await
168            {
169                Ok(file) => file,
170                Err(fuchsia_fs::node::OpenError::OpenError(Status::NOT_FOUND)) => {
171                    return Err(ImagePackagesError::NotFound);
172                }
173                Err(e) => return Err(ImagePackagesError::Open(e)),
174            };
175        let contents = fuchsia_fs::file::read(&file).await.map_err(ImagePackagesError::Read)?;
176        crate::images::parse_image_packages_json(&contents).map_err(Into::into).map(Into::into)
177    }
178
179    /// Verifies the board file has the given `contents`.
180    pub async fn verify_board(&self, contents: &str) -> Result<(), VerifyBoardError> {
181        board::verify_board(&self.proxy, contents).await
182    }
183
184    /// Parses the update-mode file to obtain update mode. Returns `Ok(None)` if the update-mode
185    /// file is not present in the update package.
186    pub async fn update_mode(&self) -> Result<Option<UpdateMode>, ParseUpdateModeError> {
187        let fopen_res =
188            fuchsia_fs::directory::open_file(&self.proxy, "update-mode", fio::PERM_READABLE).await;
189        if let Err(fuchsia_fs::node::OpenError::OpenError(Status::NOT_FOUND)) = fopen_res {
190            return Ok(None);
191        }
192        let file = fopen_res.map_err(ParseUpdateModeError::OpenFile)?;
193        let contents = fuchsia_fs::file::read_to_string(&file)
194            .await
195            .map_err(ParseUpdateModeError::ReadFile)?;
196        let UpdateModeFile::Version1 { update_mode: mode_str } = serde_json::from_str(&contents)
197            .map_err(|e| ParseUpdateModeError::Deserialize(contents, e))?;
198        match mode_str.as_str() {
199            "normal" => Ok(Some(UpdateMode::Normal)),
200            "force-recovery" => Ok(Some(UpdateMode::ForceRecovery)),
201            other => Err(ParseUpdateModeError::UpdateModeNotSupported(other.to_string())),
202        }
203    }
204
205    /// Returns the list of package urls that go in the universe of this update package.
206    pub async fn packages(&self) -> Result<Vec<PinnedAbsolutePackageUrl>, ParsePackageError> {
207        let file =
208            fuchsia_fs::directory::open_file(&self.proxy, "packages.json", fio::PERM_READABLE)
209                .await
210                .map_err(ParsePackageError::FailedToOpen)?;
211        let contents = fuchsia_fs::file::read(&file).await.map_err(ParsePackageError::ReadError)?;
212        crate::packages::parse_packages_json(&contents).map_err(Into::into)
213    }
214
215    /// Returns the package hash of this update package.
216    pub async fn hash(&self) -> Result<Hash, HashError> {
217        hash::hash(&self.proxy).await
218    }
219
220    /// Returns the version of this update package.
221    pub async fn version(&self) -> Result<SystemVersion, ReadVersionError> {
222        let file = fuchsia_fs::directory::open_file(&self.proxy, "version", fio::PERM_READABLE)
223            .await
224            .map_err(ReadVersionError::OpenFile)?;
225        let contents =
226            fuchsia_fs::file::read_to_string(&file).await.map_err(ReadVersionError::ReadFile)?;
227        SystemVersion::from_str(&contents).map_err(|e| match e {})
228    }
229
230    /// Parses the epoch.json file to obtain the epoch. Returns `Ok(None)` if the epoch.json file
231    /// is not present in the update package.
232    pub async fn epoch(&self) -> Result<Option<u64>, ParseEpochError> {
233        epoch::epoch(&self.proxy).await
234    }
235}
236
237#[cfg(test)]
238pub(crate) struct TestUpdatePackage {
239    update_pkg: UpdatePackage,
240    temp_dir: tempfile::TempDir,
241}
242
243#[cfg(test)]
244impl TestUpdatePackage {
245    pub(crate) fn new() -> Self {
246        let temp_dir = tempfile::tempdir().expect("/tmp to exist");
247        let update_pkg_proxy = fuchsia_fs::directory::open_in_namespace(
248            temp_dir.path().to_str().unwrap(),
249            fio::PERM_READABLE,
250        )
251        .expect("temp dir to open");
252        Self { temp_dir, update_pkg: UpdatePackage::new(update_pkg_proxy) }
253    }
254
255    pub(crate) async fn add_file(
256        self,
257        path: impl AsRef<std::path::Path>,
258        contents: impl AsRef<[u8]>,
259    ) -> Self {
260        let path = path.as_ref();
261        match path.parent() {
262            Some(empty) if empty == std::path::Path::new("") => {}
263            None => {}
264            Some(parent) => std::fs::create_dir_all(self.temp_dir.path().join(parent)).unwrap(),
265        }
266        fuchsia_fs::file::write_in_namespace(
267            self.temp_dir.path().join(path).to_str().unwrap(),
268            contents,
269        )
270        .await
271        .expect("create test update package file");
272        self
273    }
274}
275
276#[cfg(test)]
277impl std::ops::Deref for TestUpdatePackage {
278    type Target = UpdatePackage;
279
280    fn deref(&self) -> &Self::Target {
281        &self.update_pkg
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288    use assert_matches::assert_matches;
289    use omaha_client::version::Version as SemanticVersion;
290    use serde_json::json;
291
292    fn pkg_urls<'a>(v: impl IntoIterator<Item = &'a str>) -> Vec<PinnedAbsolutePackageUrl> {
293        v.into_iter().map(|s| s.parse().unwrap()).collect()
294    }
295
296    #[fuchsia::test]
297    async fn lifecycle() {
298        let (proxy, _server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
299        UpdatePackage::new(proxy);
300    }
301
302    #[fuchsia::test]
303    async fn test_packages_json_success() {
304        let pkg_list = [
305            "fuchsia-pkg://fuchsia.com/ls/0?hash=71bad1a35b87a073f72f582065f6b6efec7b6a4a129868f37f6131f02107f1ea",
306            "fuchsia-pkg://fuchsia.com/pkg-resolver/0?hash=26d43a3fc32eaa65e6981791874b6ab80fae31fbfca1ce8c31ab64275fd4e8c0",
307        ];
308        let packages = json!({
309            "version": "1",
310            "content": pkg_list,
311        })
312        .to_string();
313        let update_pkg = TestUpdatePackage::new().add_file("packages.json", packages).await;
314        assert_eq!(update_pkg.packages().await.unwrap(), pkg_urls(pkg_list));
315    }
316
317    #[fuchsia::test]
318    async fn test_packages_json_corrupt() {
319        let update_pkg = TestUpdatePackage::new().add_file("packages.json", "{}").await;
320        assert_matches!(update_pkg.packages().await, Err(ParsePackageError::JsonError(_)));
321    }
322
323    #[fuchsia::test]
324    async fn test_packages_json_version_not_supported() {
325        let pkg_list = vec![
326            "fuchsia-pkg://fuchsia.com/ls/0?hash=71bad1a35b87a073f72f582065f6b6efec7b6a4a129868f37f6131f02107f1ea",
327        ];
328        let packages = json!({
329            "version": "2",
330            "content": pkg_list,
331        })
332        .to_string();
333        let update_pkg = TestUpdatePackage::new().add_file("packages.json", packages).await;
334        assert_matches!(
335            update_pkg.packages().await,
336            Err(ParsePackageError::JsonError(e))
337                if e.to_string().contains("unknown variant `2`, expected `1`")
338        );
339    }
340
341    #[fuchsia::test]
342    async fn test_packages_json_failure() {
343        let update_pkg = TestUpdatePackage::new();
344        assert_matches!(update_pkg.packages().await, Err(ParsePackageError::FailedToOpen(_)));
345    }
346
347    proptest::proptest! {
348        #[test]
349        fn test_json_serialize_roundtrip(s in ".+") {
350            let starting_json_value = json!({
351                "version": "1",
352                "content": {
353                    "mode": &s,
354                },
355            });
356            let deserialized_object: UpdateModeFile =
357                serde_json::from_value(starting_json_value.clone())
358                    .expect("json to deserialize");
359            assert_eq!(deserialized_object, UpdateModeFile::Version1 { update_mode: s });
360
361            let final_json_value =
362                serde_json::to_value(&deserialized_object)
363                    .expect("serialize to value");
364            assert_eq!(final_json_value, starting_json_value);
365        }
366    }
367
368    #[fuchsia::test]
369    async fn test_update_mode_normal() {
370        let p = TestUpdatePackage::new()
371            .add_file(
372                "update-mode",
373                serde_json::to_vec(&UpdateModeFile::Version1 { update_mode: "normal".to_string() })
374                    .unwrap(),
375            )
376            .await;
377        assert_matches!(p.update_mode().await, Ok(Some(UpdateMode::Normal)));
378    }
379
380    #[fuchsia::test]
381    async fn test_update_mode_force_recovery() {
382        let p = TestUpdatePackage::new()
383            .add_file(
384                "update-mode",
385                serde_json::to_vec(&UpdateModeFile::Version1 {
386                    update_mode: "force-recovery".to_string(),
387                })
388                .unwrap(),
389            )
390            .await;
391        assert_matches!(p.update_mode().await, Ok(Some(UpdateMode::ForceRecovery)));
392    }
393
394    #[fuchsia::test]
395    async fn test_update_mode_fail_unsupported_mode() {
396        let p = TestUpdatePackage::new()
397            .add_file(
398                "update-mode",
399                serde_json::to_vec(&UpdateModeFile::Version1 { update_mode: "potato".to_string() })
400                    .unwrap(),
401            )
402            .await;
403        assert_matches!(
404            p.update_mode().await,
405            Err(ParseUpdateModeError::UpdateModeNotSupported(mode)) if mode == "potato"
406        );
407    }
408
409    #[fuchsia::test]
410    async fn test_update_mode_fail_deserialize() {
411        let p = TestUpdatePackage::new().add_file("update-mode", "oh no! this isn't json.").await;
412        assert_matches!(
413            p.update_mode().await,
414            Err(ParseUpdateModeError::Deserialize(s, _)) if s == "oh no! this isn't json."
415        );
416    }
417
418    #[fuchsia::test]
419    async fn test_update_mode_missing() {
420        let p = TestUpdatePackage::new();
421        assert_matches!(p.update_mode().await, Ok(None));
422    }
423
424    #[fuchsia::test]
425    async fn test_version_success_semantic() {
426        let p = TestUpdatePackage::new().add_file("version", "123").await;
427        assert_eq!(
428            p.version().await.unwrap(),
429            SystemVersion::Semantic(SemanticVersion::from([123]))
430        );
431    }
432
433    #[fuchsia::test]
434    async fn test_version_success_opaque() {
435        let p = TestUpdatePackage::new().add_file("version", "2020-09-08T10:17:00+10:00").await;
436        assert_eq!(
437            p.version().await.unwrap(),
438            SystemVersion::Opaque("2020-09-08T10:17:00+10:00".to_owned())
439        );
440    }
441
442    #[fuchsia::test]
443    async fn test_version_trims_trailing_whitespace() {
444        let p = TestUpdatePackage::new().add_file("version", "2020-09-08T10:17:00+10:00\n").await;
445        assert_eq!(
446            p.version().await.unwrap(),
447            SystemVersion::Opaque("2020-09-08T10:17:00+10:00".to_owned())
448        );
449    }
450
451    #[fuchsia::test]
452    async fn test_version_missing() {
453        let p = TestUpdatePackage::new();
454        assert_matches!(p.version().await, Err(ReadVersionError::OpenFile(_)));
455    }
456
457    #[fuchsia::test]
458    async fn test_images_metadata_missing() {
459        let p = TestUpdatePackage::new();
460        assert_matches!(p.images_metadata().await, Err(ImagePackagesError::NotFound));
461    }
462}