1use crate::SystemVersion;
8use crate::images::AssetType;
9use crate::update_mode::UpdateMode;
10use serde::{Deserialize, Serialize};
11
12pub 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#[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#[derive(Serialize, Deserialize, Debug)]
33pub struct VersionedOtaManifest {
34 #[serde(skip_serializing_if = "Option::is_none")]
35 version1: Option<OtaManifestV1>,
36}
37
38#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
40pub struct OtaManifestV1 {
41 pub build_version: SystemVersion,
44 pub board: String,
46 pub epoch: u64,
48 #[serde(default, skip_serializing_if = "update_mode_is_normal")]
50 pub mode: UpdateMode,
51 pub blob_base_url: String,
55 #[serde(default, skip_serializing_if = "Vec::is_empty")]
57 pub images: Vec<Image>,
58 #[serde(default, skip_serializing_if = "Vec::is_empty")]
60 pub blobs: Vec<Blob>,
61}
62
63impl OtaManifestV1 {
64 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#[derive(Copy, Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Default)]
76pub enum Slot {
77 #[default]
79 AB,
80 R,
82}
83
84#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
86pub struct Image {
87 pub slot: Slot,
89 #[serde(flatten)]
91 pub image_type: ImageType,
92 pub sha256: fuchsia_hash::Sha256,
94 pub size: u64,
96 pub delivery_blob_type: u32,
98 pub fuchsia_merkle_root: fuchsia_hash::Hash,
100}
101
102impl Image {
103 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#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
139#[serde(rename_all = "snake_case")]
140pub enum ImageType {
141 Asset(AssetType),
143 Firmware(String),
145}
146
147#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
149pub struct Blob {
150 pub uncompressed_size: u64,
152 pub delivery_blob_type: u32,
154 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}