update_package/
manifest.rs

1// Copyright 2025 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
5//! Structs for parsing an OTA manifest.
6
7use crate::SystemVersion;
8use crate::images::AssetType;
9use crate::update_mode::UpdateMode;
10use serde::{Deserialize, Serialize};
11
12/// Returns structured OTA manifest data based on raw file contents.
13pub fn parse_ota_manifest(contents: &[u8]) -> Result<OtaManifestV1, OtaManifestError> {
14    let manifest: VersionedOtaManifest =
15        serde_json::from_slice(contents).map_err(OtaManifestError::Parse)?;
16
17    manifest.version1.ok_or(OtaManifestError::NoSupportedVersion)
18}
19
20/// An error encountered while parsing the OTA manifest.
21#[derive(Debug, thiserror::Error)]
22#[allow(missing_docs)]
23pub enum OtaManifestError {
24    #[error("while parsing json")]
25    Parse(#[source] serde_json::error::Error),
26
27    #[error("no supported version found")]
28    NoSupportedVersion,
29}
30
31/// The versioned manifest, can support multiple versions in the same manifest.
32#[derive(Serialize, Deserialize, Debug)]
33pub struct VersionedOtaManifest {
34    #[serde(skip_serializing_if = "Option::is_none")]
35    version1: Option<OtaManifestV1>,
36}
37
38/// Information about a particular version of the OS.
39#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
40pub struct OtaManifestV1 {
41    /// The version of the target build. This is information only, not used to enforce
42    /// anti-rollback.
43    pub build_version: SystemVersion,
44    /// The board this OTA is for, must match build-info/board.
45    pub board: String,
46    /// The epoch of this OTA. See RFC-0071 for details.
47    pub epoch: u64,
48    /// The update mode, normal or forced-recovery.
49    #[serde(default, skip_serializing_if = "update_mode_is_normal")]
50    pub mode: UpdateMode,
51    /// The base URL prefix of the blobs, the final URL for each blob will be
52    /// "{blob_base_url}/{delivery_blob_type}/{fuchsia_merkle_root}".
53    /// The url can be absolute or relative to the URL of the manifest.
54    pub blob_base_url: String,
55    /// The images for this version. Each image will be written to their corresponding partition.
56    #[serde(default, skip_serializing_if = "Vec::is_empty")]
57    pub images: Vec<Image>,
58    /// The blobs for this version. Each blob will be written to blob storage.
59    #[serde(default, skip_serializing_if = "Vec::is_empty")]
60    pub blobs: Vec<Blob>,
61}
62
63impl OtaManifestV1 {
64    /// Wrap in a versioned manifest.
65    pub fn into_versioned(self) -> VersionedOtaManifest {
66        VersionedOtaManifest { version1: Some(self) }
67    }
68}
69
70fn update_mode_is_normal(mode: &UpdateMode) -> bool {
71    *mode == UpdateMode::Normal
72}
73
74/// The slot of an image.
75#[derive(Copy, Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Default)]
76pub enum Slot {
77    /// The A or B slot.
78    #[default]
79    AB,
80    /// The recovery slot in ABR.
81    R,
82}
83
84/// An image to be written to a partition.
85#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
86pub struct Image {
87    /// The slot of the image.
88    pub slot: Slot,
89    /// The type of the image.
90    #[serde(flatten)]
91    pub image_type: ImageType,
92    /// The sha256 hash of the image.
93    pub sha256: fuchsia_hash::Sha256,
94    /// The size of the image in bytes.
95    pub size: u64,
96    /// The delivery blob type.
97    pub delivery_blob_type: u32,
98    /// The fuchsia merkle root of the image.
99    pub fuchsia_merkle_root: fuchsia_hash::Hash,
100}
101
102impl Image {
103    /// Create a new `Image` from a file path.
104    pub fn from_path(
105        path: impl AsRef<std::path::Path>,
106        slot: Slot,
107        image_type: ImageType,
108        delivery_blob_type: u32,
109    ) -> Result<Self, std::io::Error> {
110        use sha2::Digest as _;
111        use std::io::Read as _;
112
113        let mut file = std::fs::File::open(path)?;
114        let mut sha256_hasher = sha2::Sha256::new();
115        let mut merkle_builder = fuchsia_merkle::BufferedMerkleRootBuilder::default();
116        let mut buf = [0; 65536];
117        let mut size = 0;
118        loop {
119            let bytes_read = file.read(&mut buf)?;
120            if bytes_read == 0 {
121                break;
122            }
123            let buf = &buf[..bytes_read];
124            sha256_hasher.update(buf);
125            merkle_builder.write(buf);
126            size += bytes_read as u64;
127        }
128
129        let fuchsia_merkle_root = merkle_builder.complete();
130        let sha256 =
131            fuchsia_hash::Sha256::from(*AsRef::<[u8; 32]>::as_ref(&sha256_hasher.finalize()));
132
133        Ok(Self { slot, image_type, sha256, size, delivery_blob_type, fuchsia_merkle_root })
134    }
135}
136
137/// The type of the image, asset or firmware.
138#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
139#[serde(rename_all = "snake_case")]
140pub enum ImageType {
141    /// ZBI or VbMeta.
142    Asset(AssetType),
143    /// Other A/B partitions.
144    Firmware(String),
145}
146
147/// A content blob.
148#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
149pub struct Blob {
150    /// The uncompressed size of the blob.
151    pub uncompressed_size: u64,
152    /// The delivery blob type.
153    pub delivery_blob_type: u32,
154    /// The fuchsia merkle root of the uncompressed blob.
155    pub fuchsia_merkle_root: fuchsia_hash::Hash,
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use assert_matches::assert_matches;
162    use sha2::Digest as _;
163    use std::io::Write as _;
164    use std::str::FromStr;
165    use tempfile::NamedTempFile;
166
167    #[test]
168    fn test_parse_ota_manifest_success() {
169        let json = serde_json::json!({
170            "version1": {
171                "build_version": "1.2.3.4",
172                "board": "test-board",
173                "epoch": 1,
174                "blob_base_url": "http://example.com",
175                "images": [
176                    {
177                        "slot": "AB",
178                        "asset": "zbi",
179                        "sha256": "00".repeat(32),
180                        "size": 1234,
181                        "delivery_blob_type": 1,
182                        "fuchsia_merkle_root": "01".repeat(32),
183                    },
184                    {
185                        "slot": "AB",
186                        "firmware": "bootloader",
187                        "sha256": "02".repeat(32),
188                        "size": 3456,
189                        "delivery_blob_type": 1,
190                        "fuchsia_merkle_root": "03".repeat(32),
191                    },
192                ],
193                "blobs": [
194                    {
195                        "uncompressed_size": 5678,
196                        "delivery_blob_type": 2,
197                        "fuchsia_merkle_root": "04".repeat(32),
198                    }
199                ]
200            }
201        });
202        let manifest = parse_ota_manifest(json.to_string().as_bytes()).unwrap();
203        assert_eq!(manifest.build_version, SystemVersion::from_str("1.2.3.4").unwrap());
204        assert_eq!(manifest.board, "test-board");
205        assert_eq!(manifest.epoch, 1);
206        assert_eq!(manifest.mode, UpdateMode::Normal);
207        assert_eq!(manifest.blob_base_url, "http://example.com");
208
209        assert_eq!(manifest.images.len(), 2);
210        assert_eq!(manifest.images[0].slot, Slot::AB);
211        assert_eq!(manifest.images[0].image_type, ImageType::Asset(AssetType::Zbi));
212        assert_eq!(manifest.images[0].sha256, [0; 32].into());
213        assert_eq!(manifest.images[0].size, 1234);
214        assert_eq!(manifest.images[0].delivery_blob_type, 1);
215        assert_eq!(manifest.images[0].fuchsia_merkle_root, [1; 32].into());
216
217        assert_eq!(manifest.images[1].slot, Slot::AB);
218        assert_eq!(manifest.images[1].image_type, ImageType::Firmware("bootloader".to_string()));
219        assert_eq!(manifest.images[1].sha256, [2; 32].into());
220        assert_eq!(manifest.images[1].size, 3456);
221        assert_eq!(manifest.images[1].delivery_blob_type, 1);
222        assert_eq!(manifest.images[1].fuchsia_merkle_root, [3; 32].into());
223
224        assert_eq!(manifest.blobs.len(), 1);
225        assert_eq!(manifest.blobs[0].uncompressed_size, 5678);
226        assert_eq!(manifest.blobs[0].delivery_blob_type, 2);
227        assert_eq!(manifest.blobs[0].fuchsia_merkle_root, [4; 32].into());
228    }
229
230    #[test]
231    fn test_serialize_ota_manifest() {
232        let manifest = OtaManifestV1 {
233            build_version: SystemVersion::from_str("1.2.3.4").unwrap(),
234            board: "test-board".to_string(),
235            epoch: 1,
236            mode: UpdateMode::Normal,
237            blob_base_url: "http://example.com".into(),
238            images: vec![
239                Image {
240                    slot: Slot::AB,
241                    image_type: ImageType::Asset(AssetType::Zbi),
242                    sha256: [0; 32].into(),
243                    size: 1234,
244                    delivery_blob_type: 1,
245                    fuchsia_merkle_root: [1; 32].into(),
246                },
247                Image {
248                    slot: Slot::AB,
249                    image_type: ImageType::Firmware("bootloader".to_string()),
250                    sha256: [2; 32].into(),
251                    size: 3456,
252                    delivery_blob_type: 1,
253                    fuchsia_merkle_root: [3; 32].into(),
254                },
255            ],
256            blobs: vec![Blob {
257                uncompressed_size: 5678,
258                delivery_blob_type: 2,
259                fuchsia_merkle_root: [4; 32].into(),
260            }],
261        };
262
263        let json = serde_json::to_value(manifest.into_versioned()).unwrap();
264        let expected = serde_json::json!({
265            "version1": {
266                "build_version": "1.2.3.4",
267                "board": "test-board",
268                "epoch": 1,
269                "blob_base_url": "http://example.com",
270                "images": [
271                    {
272                        "slot": "AB",
273                        "asset": "zbi",
274                        "sha256": "00".repeat(32),
275                        "size": 1234,
276                        "delivery_blob_type": 1,
277                        "fuchsia_merkle_root": "01".repeat(32),
278                    },
279                    {
280                        "slot": "AB",
281                        "firmware": "bootloader",
282                        "sha256": "02".repeat(32),
283                        "size": 3456,
284                        "delivery_blob_type": 1,
285                        "fuchsia_merkle_root": "03".repeat(32),
286                    },
287                ],
288                "blobs": [
289                    {
290                        "uncompressed_size": 5678,
291                        "delivery_blob_type": 2,
292                        "fuchsia_merkle_root": "04".repeat(32),
293                    }
294                ]
295            }
296        });
297        assert_eq!(json, expected);
298    }
299
300    #[test]
301    fn test_parse_ota_manifest_no_supported_version() {
302        let json = serde_json::json!({
303            "version2": {}
304        });
305        let err = parse_ota_manifest(json.to_string().as_bytes()).unwrap_err();
306        assert_matches!(err, OtaManifestError::NoSupportedVersion);
307    }
308
309    #[test]
310    fn test_parse_ota_manifest_invalid_json() {
311        let err = parse_ota_manifest(b"invalid json").unwrap_err();
312        assert_matches!(err, OtaManifestError::Parse(_));
313    }
314
315    #[test]
316    fn image_from_path() {
317        let file = NamedTempFile::new().unwrap();
318        std::fs::write(file.path(), b"hello world").unwrap();
319
320        let image =
321            Image::from_path(file.path(), Slot::AB, ImageType::Asset(AssetType::Zbi), 1).unwrap();
322
323        assert_eq!(image.size, 11);
324        assert_eq!(
325            image.sha256,
326            "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9".parse().unwrap()
327        );
328        assert_eq!(
329            image.fuchsia_merkle_root,
330            "8af85e2fe5da3385ea468ed1cb8412eaea6530a90b5dd8dee96529c8d9d39b97".parse().unwrap()
331        );
332    }
333
334    #[test]
335    fn image_from_path_large_file() {
336        let mut file = NamedTempFile::new().unwrap();
337        let chunk = [1; 1024];
338        let mut sha256_hasher = sha2::Sha256::new();
339        let mut merkle_builder = fuchsia_merkle::BufferedMerkleRootBuilder::default();
340        for _ in 0..1000 {
341            file.write_all(&chunk).unwrap();
342            sha256_hasher.update(chunk);
343            merkle_builder.write(&chunk);
344        }
345
346        let image =
347            Image::from_path(file.path(), Slot::AB, ImageType::Asset(AssetType::Zbi), 1).unwrap();
348
349        assert_eq!(image.size, 1000 * 1024);
350        assert_eq!(
351            image.sha256,
352            fuchsia_hash::Sha256::from(*AsRef::<[u8; 32]>::as_ref(&sha256_hasher.finalize()))
353        );
354        assert_eq!(image.fuchsia_merkle_root, merkle_builder.complete());
355    }
356}