1use crate::SystemVersion;
8use crate::update_mode::UpdateMode;
9use ota_manifest_proto::fuchsia::update::manifest as proto;
10use prost::Message as _;
11use std::convert::Infallible;
12use std::str::FromStr as _;
13
14pub type AssetType = proto::AssetType;
16
17pub fn parse_ota_manifest(contents: &[u8]) -> Result<OtaManifest, OtaManifestError> {
19 let manifest = proto::OtaManifest::decode(contents).map_err(OtaManifestError::ParseProto)?;
20 manifest.try_into().map_err(OtaManifestError::InvalidManifest)
21}
22
23#[derive(Debug, thiserror::Error)]
25#[allow(missing_docs)]
26pub enum OtaManifestError {
27 #[error("while parsing proto")]
28 ParseProto(#[source] prost::DecodeError),
29
30 #[error("invalid proto manifest: {0}")]
31 InvalidManifest(String),
32}
33
34#[derive(Clone, Debug, PartialEq, Eq)]
36pub struct OtaManifest {
37 pub build_info_version: SystemVersion,
40 pub board: String,
44 pub epoch: u64,
46 pub mode: UpdateMode,
49 pub blob_base_url: String,
54 pub images: Vec<Image>,
56 pub blobs: Vec<Blob>,
58}
59
60impl OtaManifest {
61 pub fn serialize(self) -> Vec<u8> {
63 let proto: proto::OtaManifest = self.into();
64 proto.encode_to_vec()
65 }
66}
67
68#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
70pub enum Slot {
71 #[default]
73 AB,
74 R,
76}
77
78#[derive(Clone, Debug, PartialEq, Eq)]
80pub struct Image {
81 pub slot: Slot,
83 pub image_type: ImageType,
85 pub sha256: fuchsia_hash::Sha256,
87 pub blob: Blob,
89}
90
91impl Image {
92 pub fn from_path(
94 path: impl AsRef<std::path::Path>,
95 slot: Slot,
96 image_type: ImageType,
97 ) -> Result<Self, std::io::Error> {
98 use sha2::Digest as _;
99 use std::io::Read as _;
100
101 let mut file = std::fs::File::open(path)?;
102 let mut sha256_hasher = sha2::Sha256::new();
103 let mut merkle_builder = fuchsia_merkle::BufferedMerkleRootBuilder::default();
104 let mut buf = [0; 65536];
105 let mut size = 0;
106 loop {
107 let bytes_read = file.read(&mut buf)?;
108 if bytes_read == 0 {
109 break;
110 }
111 let buf = &buf[..bytes_read];
112 sha256_hasher.update(buf);
113 merkle_builder.write(buf);
114 size += bytes_read as u64;
115 }
116
117 let fuchsia_merkle_root = merkle_builder.complete();
118 let sha256 =
119 fuchsia_hash::Sha256::from(*AsRef::<[u8; 32]>::as_ref(&sha256_hasher.finalize()));
120
121 Ok(Self {
122 slot,
123 image_type,
124 sha256,
125 blob: Blob { uncompressed_size: size, fuchsia_merkle_root },
126 })
127 }
128}
129
130#[derive(Clone, Debug, PartialEq, Eq)]
132pub enum ImageType {
133 Asset(AssetType),
135 Firmware(String),
137}
138
139#[derive(Clone, Debug, PartialEq, Eq)]
141pub struct Blob {
142 pub uncompressed_size: u64,
144 pub fuchsia_merkle_root: fuchsia_hash::Hash,
146}
147
148impl TryFrom<proto::OtaManifest> for OtaManifest {
149 type Error = String;
150 fn try_from(proto: proto::OtaManifest) -> Result<Self, Self::Error> {
151 let mode = proto.mode().into();
152 let version: Result<_, Infallible> =
153 crate::SystemVersion::from_str(&proto.build_info_version);
154 Ok(Self {
155 build_info_version: version.unwrap(),
156 board: proto.board,
157 epoch: proto.epoch,
158 mode,
159 blob_base_url: proto.blob_base_url,
160 images: proto
161 .images
162 .into_iter()
163 .map(TryInto::try_into)
164 .collect::<Result<Vec<_>, _>>()?,
165 blobs: proto.blobs.into_iter().map(TryInto::try_into).collect::<Result<Vec<_>, _>>()?,
166 })
167 }
168}
169
170impl From<OtaManifest> for proto::OtaManifest {
171 fn from(manifest: OtaManifest) -> Self {
172 Self {
173 build_info_version: manifest.build_info_version.to_string(),
174 board: manifest.board,
175 epoch: manifest.epoch,
176 mode: proto::UpdateMode::from(manifest.mode).into(),
177 blob_base_url: manifest.blob_base_url,
178 images: manifest.images.into_iter().map(Into::into).collect(),
179 blobs: manifest.blobs.into_iter().map(Into::into).collect(),
180 }
181 }
182}
183
184impl From<proto::UpdateMode> for UpdateMode {
185 fn from(mode: proto::UpdateMode) -> Self {
186 match mode {
187 proto::UpdateMode::Normal => UpdateMode::Normal,
188 proto::UpdateMode::ForceRecovery => UpdateMode::ForceRecovery,
189 }
190 }
191}
192
193impl From<UpdateMode> for proto::UpdateMode {
194 fn from(mode: UpdateMode) -> Self {
195 match mode {
196 UpdateMode::Normal => proto::UpdateMode::Normal,
197 UpdateMode::ForceRecovery => proto::UpdateMode::ForceRecovery,
198 }
199 }
200}
201
202impl TryFrom<proto::Image> for Image {
203 type Error = String;
204 fn try_from(image: proto::Image) -> Result<Self, Self::Error> {
205 let slot = image.slot().into();
206 let image_type =
207 image.image_type.ok_or_else(|| "image_type missing".to_string())?.try_into()?;
208 let sha256 = fuchsia_hash::Sha256::from(
209 <[u8; 32]>::try_from(image.sha256)
210 .map_err(|e| format!("invalid sha256 length: {e:?}"))?,
211 );
212 let blob = image.blob.ok_or_else(|| "blob missing".to_string())?.try_into()?;
213 Ok(Self { slot, image_type, sha256, blob })
214 }
215}
216
217impl From<Image> for proto::Image {
218 fn from(image: Image) -> Self {
219 Self {
220 slot: proto::Slot::from(image.slot).into(),
221 image_type: Some(image.image_type.into()),
222 sha256: image.sha256.as_ref().to_vec(),
223 blob: Some(image.blob.into()),
224 }
225 }
226}
227
228impl From<proto::Slot> for Slot {
229 fn from(slot: proto::Slot) -> Self {
230 match slot {
231 proto::Slot::Ab => Slot::AB,
232 proto::Slot::R => Slot::R,
233 }
234 }
235}
236
237impl From<Slot> for proto::Slot {
238 fn from(slot: Slot) -> Self {
239 match slot {
240 Slot::AB => proto::Slot::Ab,
241 Slot::R => proto::Slot::R,
242 }
243 }
244}
245
246impl TryFrom<proto::image::ImageType> for ImageType {
247 type Error = String;
248 fn try_from(value: proto::image::ImageType) -> Result<Self, Self::Error> {
249 match value {
250 proto::image::ImageType::Asset(asset) => {
251 let asset_type = proto::AssetType::from_i32(asset)
252 .ok_or_else(|| format!("unknown asset type: {asset}"))?;
253 Ok(ImageType::Asset(asset_type))
254 }
255 proto::image::ImageType::Firmware(firmware) => Ok(ImageType::Firmware(firmware)),
256 }
257 }
258}
259
260impl From<ImageType> for proto::image::ImageType {
261 fn from(image_type: ImageType) -> Self {
262 match image_type {
263 ImageType::Asset(asset) => proto::image::ImageType::Asset(asset.into()),
264 ImageType::Firmware(firmware) => proto::image::ImageType::Firmware(firmware),
265 }
266 }
267}
268
269impl From<Blob> for proto::Blob {
270 fn from(blob: Blob) -> Self {
271 Self {
272 uncompressed_size: blob.uncompressed_size,
273 fuchsia_merkle_root: blob.fuchsia_merkle_root.as_ref().to_vec(),
274 }
275 }
276}
277
278impl TryFrom<proto::Blob> for Blob {
279 type Error = String;
280 fn try_from(blob: proto::Blob) -> Result<Self, Self::Error> {
281 Ok(Self {
282 uncompressed_size: blob.uncompressed_size,
283 fuchsia_merkle_root: fuchsia_hash::Hash::from(
284 <[u8; 32]>::try_from(blob.fuchsia_merkle_root)
285 .map_err(|e| format!("invalid merkle root length: {e:?}"))?,
286 ),
287 })
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294 use assert_matches::assert_matches;
295 use sha2::Digest as _;
296 use std::io::Write as _;
297 use std::str::FromStr;
298 use tempfile::NamedTempFile;
299
300 #[test]
301 fn test_parse_ota_manifest_success() {
302 let proto_manifest = proto::OtaManifest {
303 build_info_version: "1.2.3.4".to_string(),
304 board: "test-board".to_string(),
305 epoch: 1,
306 mode: proto::UpdateMode::Normal.into(),
307 blob_base_url: "http://example.com".to_string(),
308 images: vec![
309 proto::Image {
310 slot: proto::Slot::Ab.into(),
311 image_type: Some(proto::image::ImageType::Asset(proto::AssetType::Zbi.into())),
312 sha256: vec![0; 32],
313 blob: Some(proto::Blob {
314 uncompressed_size: 1234,
315 fuchsia_merkle_root: vec![1; 32],
316 }),
317 },
318 proto::Image {
319 slot: proto::Slot::Ab.into(),
320 image_type: Some(proto::image::ImageType::Firmware("bootloader".to_string())),
321 sha256: vec![2; 32],
322 blob: Some(proto::Blob {
323 uncompressed_size: 3456,
324 fuchsia_merkle_root: vec![3; 32],
325 }),
326 },
327 ],
328 blobs: vec![proto::Blob { uncompressed_size: 5678, fuchsia_merkle_root: vec![4; 32] }],
329 };
330 let buf = proto_manifest.encode_to_vec();
331
332 let manifest = parse_ota_manifest(&buf).unwrap();
333 assert_eq!(manifest.build_info_version, SystemVersion::from_str("1.2.3.4").unwrap());
334 assert_eq!(manifest.board, "test-board");
335 assert_eq!(manifest.epoch, 1);
336 assert_eq!(manifest.mode, UpdateMode::Normal);
337 assert_eq!(manifest.blob_base_url, "http://example.com");
338
339 assert_eq!(manifest.images.len(), 2);
340 assert_eq!(manifest.images[0].slot, Slot::AB);
341 assert_eq!(manifest.images[0].image_type, ImageType::Asset(AssetType::Zbi));
342 assert_eq!(manifest.images[0].sha256, [0; 32].into());
343 assert_eq!(manifest.images[0].blob.uncompressed_size, 1234);
344 assert_eq!(manifest.images[0].blob.fuchsia_merkle_root, [1; 32].into());
345
346 assert_eq!(manifest.images[1].slot, Slot::AB);
347 assert_eq!(manifest.images[1].image_type, ImageType::Firmware("bootloader".to_string()));
348 assert_eq!(manifest.images[1].sha256, [2; 32].into());
349 assert_eq!(manifest.images[1].blob.uncompressed_size, 3456);
350 assert_eq!(manifest.images[1].blob.fuchsia_merkle_root, [3; 32].into());
351
352 assert_eq!(manifest.blobs.len(), 1);
353 assert_eq!(manifest.blobs[0].uncompressed_size, 5678);
354 assert_eq!(manifest.blobs[0].fuchsia_merkle_root, [4; 32].into());
355 }
356
357 #[test]
358 fn test_parse_ota_manifest_invalid_proto() {
359 let err = parse_ota_manifest(b"invalid proto").unwrap_err();
360 assert_matches!(err, OtaManifestError::ParseProto(_));
361 }
362
363 #[test]
364 fn test_parse_ota_manifest_invalid_manifest() {
365 let proto_manifest = proto::OtaManifest {
366 build_info_version: "1.2.3.4".to_string(),
367 board: "test-board".to_string(),
368 epoch: 1,
369 mode: proto::UpdateMode::Normal.into(),
370 blob_base_url: "http://example.com".to_string(),
371 images: vec![proto::Image {
372 slot: proto::Slot::Ab.into(),
373 image_type: None,
374 sha256: vec![0; 32],
375 blob: Some(proto::Blob {
376 uncompressed_size: 1234,
377 fuchsia_merkle_root: vec![1; 32],
378 }),
379 }],
380 blobs: vec![],
381 };
382 let buf = proto_manifest.encode_to_vec();
383
384 let err = parse_ota_manifest(&buf).unwrap_err();
385 assert_matches!(err, OtaManifestError::InvalidManifest(msg) if msg == "image_type missing");
386 }
387
388 #[test]
389 fn test_serialize_ota_manifest() {
390 let manifest = OtaManifest {
391 build_info_version: SystemVersion::from_str("1.2.3.4").unwrap(),
392 board: "test-board".to_string(),
393 epoch: 1,
394 mode: UpdateMode::Normal,
395 blob_base_url: "http://example.com".to_string(),
396 images: vec![
397 Image {
398 slot: Slot::AB,
399 image_type: ImageType::Asset(AssetType::Zbi),
400 sha256: [0; 32].into(),
401 blob: Blob { uncompressed_size: 1234, fuchsia_merkle_root: [1; 32].into() },
402 },
403 Image {
404 slot: Slot::AB,
405 image_type: ImageType::Firmware("bootloader".to_string()),
406 sha256: [2; 32].into(),
407 blob: Blob { uncompressed_size: 3456, fuchsia_merkle_root: [3; 32].into() },
408 },
409 ],
410 blobs: vec![Blob { uncompressed_size: 5678, fuchsia_merkle_root: [4; 32].into() }],
411 };
412
413 let buf = manifest.clone().serialize();
414 let parsed_manifest = parse_ota_manifest(&buf).unwrap();
415
416 assert_eq!(manifest, parsed_manifest);
417 }
418
419 #[test]
420 fn image_from_path() {
421 let file = NamedTempFile::new().unwrap();
422 std::fs::write(file.path(), b"hello world").unwrap();
423
424 let image =
425 Image::from_path(file.path(), Slot::AB, ImageType::Asset(AssetType::Zbi)).unwrap();
426
427 assert_eq!(image.blob.uncompressed_size, 11);
428 assert_eq!(
429 image.sha256,
430 "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9".parse().unwrap()
431 );
432 assert_eq!(
433 image.blob.fuchsia_merkle_root,
434 "8af85e2fe5da3385ea468ed1cb8412eaea6530a90b5dd8dee96529c8d9d39b97".parse().unwrap()
435 );
436 }
437
438 #[test]
439 fn image_from_path_large_file() {
440 let mut file = NamedTempFile::new().unwrap();
441 let chunk = [1; 1024];
442 let mut sha256_hasher = sha2::Sha256::new();
443 let mut merkle_builder = fuchsia_merkle::BufferedMerkleRootBuilder::default();
444 for _ in 0..1000 {
445 file.write_all(&chunk).unwrap();
446 sha256_hasher.update(chunk);
447 merkle_builder.write(&chunk);
448 }
449
450 let image =
451 Image::from_path(file.path(), Slot::AB, ImageType::Asset(AssetType::Zbi)).unwrap();
452
453 assert_eq!(image.blob.uncompressed_size, 1000 * 1024);
454 assert_eq!(
455 image.sha256,
456 fuchsia_hash::Sha256::from(*AsRef::<[u8; 32]>::as_ref(&sha256_hasher.finalize()))
457 );
458 assert_eq!(image.blob.fuchsia_merkle_root, merkle_builder.complete());
459 }
460}