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 blob: Blob,
87}
88
89impl Image {
90 pub fn from_path(
92 path: impl AsRef<std::path::Path>,
93 slot: Slot,
94 image_type: ImageType,
95 ) -> Result<Self, std::io::Error> {
96 let file = std::fs::File::open(path)?;
97 let size = file.metadata()?.len();
98 let fuchsia_merkle_root = fuchsia_merkle::root_from_reader(file)?;
99 Ok(Self { slot, image_type, blob: Blob { uncompressed_size: size, fuchsia_merkle_root } })
100 }
101}
102
103#[derive(Clone, Debug, PartialEq, Eq)]
105pub enum ImageType {
106 Asset(AssetType),
108 Firmware(String),
110}
111
112#[derive(Clone, Debug, PartialEq, Eq)]
114pub struct Blob {
115 pub uncompressed_size: u64,
117 pub fuchsia_merkle_root: fuchsia_hash::Hash,
119}
120
121impl TryFrom<proto::OtaManifest> for OtaManifest {
122 type Error = String;
123 fn try_from(proto: proto::OtaManifest) -> Result<Self, Self::Error> {
124 let mode = proto.mode().into();
125 let version: Result<_, Infallible> =
126 crate::SystemVersion::from_str(&proto.build_info_version);
127 Ok(Self {
128 build_info_version: version.unwrap(),
129 board: proto.board,
130 epoch: proto.epoch,
131 mode,
132 blob_base_url: proto.blob_base_url,
133 images: proto
134 .images
135 .into_iter()
136 .map(TryInto::try_into)
137 .collect::<Result<Vec<_>, _>>()?,
138 blobs: proto.blobs.into_iter().map(TryInto::try_into).collect::<Result<Vec<_>, _>>()?,
139 })
140 }
141}
142
143impl From<OtaManifest> for proto::OtaManifest {
144 fn from(manifest: OtaManifest) -> Self {
145 Self {
146 build_info_version: manifest.build_info_version.to_string(),
147 board: manifest.board,
148 epoch: manifest.epoch,
149 mode: proto::UpdateMode::from(manifest.mode).into(),
150 blob_base_url: manifest.blob_base_url,
151 images: manifest.images.into_iter().map(Into::into).collect(),
152 blobs: manifest.blobs.into_iter().map(Into::into).collect(),
153 }
154 }
155}
156
157impl From<proto::UpdateMode> for UpdateMode {
158 fn from(mode: proto::UpdateMode) -> Self {
159 match mode {
160 proto::UpdateMode::Normal => UpdateMode::Normal,
161 proto::UpdateMode::ForceRecovery => UpdateMode::ForceRecovery,
162 }
163 }
164}
165
166impl From<UpdateMode> for proto::UpdateMode {
167 fn from(mode: UpdateMode) -> Self {
168 match mode {
169 UpdateMode::Normal => proto::UpdateMode::Normal,
170 UpdateMode::ForceRecovery => proto::UpdateMode::ForceRecovery,
171 }
172 }
173}
174
175impl TryFrom<proto::Image> for Image {
176 type Error = String;
177 fn try_from(image: proto::Image) -> Result<Self, Self::Error> {
178 let slot = image.slot().into();
179 let image_type =
180 image.image_type.ok_or_else(|| "image_type missing".to_string())?.try_into()?;
181 let blob = image.blob.ok_or_else(|| "blob missing".to_string())?.try_into()?;
182 Ok(Self { slot, image_type, blob })
183 }
184}
185
186impl From<Image> for proto::Image {
187 fn from(image: Image) -> Self {
188 Self {
189 slot: proto::Slot::from(image.slot).into(),
190 image_type: Some(image.image_type.into()),
191 blob: Some(image.blob.into()),
192 }
193 }
194}
195
196impl From<proto::Slot> for Slot {
197 fn from(slot: proto::Slot) -> Self {
198 match slot {
199 proto::Slot::Ab => Slot::AB,
200 proto::Slot::R => Slot::R,
201 }
202 }
203}
204
205impl From<Slot> for proto::Slot {
206 fn from(slot: Slot) -> Self {
207 match slot {
208 Slot::AB => proto::Slot::Ab,
209 Slot::R => proto::Slot::R,
210 }
211 }
212}
213
214impl TryFrom<proto::image::ImageType> for ImageType {
215 type Error = String;
216 fn try_from(value: proto::image::ImageType) -> Result<Self, Self::Error> {
217 match value {
218 proto::image::ImageType::Asset(asset) => {
219 let asset_type = proto::AssetType::from_i32(asset)
220 .ok_or_else(|| format!("unknown asset type: {asset}"))?;
221 Ok(ImageType::Asset(asset_type))
222 }
223 proto::image::ImageType::Firmware(firmware) => Ok(ImageType::Firmware(firmware)),
224 }
225 }
226}
227
228impl From<ImageType> for proto::image::ImageType {
229 fn from(image_type: ImageType) -> Self {
230 match image_type {
231 ImageType::Asset(asset) => proto::image::ImageType::Asset(asset.into()),
232 ImageType::Firmware(firmware) => proto::image::ImageType::Firmware(firmware),
233 }
234 }
235}
236
237impl From<Blob> for proto::Blob {
238 fn from(blob: Blob) -> Self {
239 Self {
240 uncompressed_size: blob.uncompressed_size,
241 fuchsia_merkle_root: blob.fuchsia_merkle_root.as_ref().to_vec(),
242 }
243 }
244}
245
246impl TryFrom<proto::Blob> for Blob {
247 type Error = String;
248 fn try_from(blob: proto::Blob) -> Result<Self, Self::Error> {
249 Ok(Self {
250 uncompressed_size: blob.uncompressed_size,
251 fuchsia_merkle_root: fuchsia_hash::Hash::from(
252 <[u8; 32]>::try_from(blob.fuchsia_merkle_root)
253 .map_err(|e| format!("invalid merkle root length: {e:?}"))?,
254 ),
255 })
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262 use std::assert_matches;
263 use std::io::Write as _;
264 use std::str::FromStr;
265 use tempfile::NamedTempFile;
266
267 #[test]
268 fn test_parse_ota_manifest_success() {
269 let proto_manifest = proto::OtaManifest {
270 build_info_version: "1.2.3.4".to_string(),
271 board: "test-board".to_string(),
272 epoch: 1,
273 mode: proto::UpdateMode::Normal.into(),
274 blob_base_url: "http://example.com".to_string(),
275 images: vec![
276 proto::Image {
277 slot: proto::Slot::Ab.into(),
278 image_type: Some(proto::image::ImageType::Asset(proto::AssetType::Zbi.into())),
279 blob: Some(proto::Blob {
280 uncompressed_size: 1234,
281 fuchsia_merkle_root: vec![1; 32],
282 }),
283 },
284 proto::Image {
285 slot: proto::Slot::Ab.into(),
286 image_type: Some(proto::image::ImageType::Firmware("bootloader".to_string())),
287 blob: Some(proto::Blob {
288 uncompressed_size: 3456,
289 fuchsia_merkle_root: vec![3; 32],
290 }),
291 },
292 ],
293 blobs: vec![proto::Blob { uncompressed_size: 5678, fuchsia_merkle_root: vec![4; 32] }],
294 };
295 let buf = proto_manifest.encode_to_vec();
296
297 let manifest = parse_ota_manifest(&buf).unwrap();
298 assert_eq!(manifest.build_info_version, SystemVersion::from_str("1.2.3.4").unwrap());
299 assert_eq!(manifest.board, "test-board");
300 assert_eq!(manifest.epoch, 1);
301 assert_eq!(manifest.mode, UpdateMode::Normal);
302 assert_eq!(manifest.blob_base_url, "http://example.com");
303
304 assert_eq!(manifest.images.len(), 2);
305 assert_eq!(manifest.images[0].slot, Slot::AB);
306 assert_eq!(manifest.images[0].image_type, ImageType::Asset(AssetType::Zbi));
307 assert_eq!(manifest.images[0].blob.uncompressed_size, 1234);
308 assert_eq!(manifest.images[0].blob.fuchsia_merkle_root, [1; 32].into());
309
310 assert_eq!(manifest.images[1].slot, Slot::AB);
311 assert_eq!(manifest.images[1].image_type, ImageType::Firmware("bootloader".to_string()));
312 assert_eq!(manifest.images[1].blob.uncompressed_size, 3456);
313 assert_eq!(manifest.images[1].blob.fuchsia_merkle_root, [3; 32].into());
314
315 assert_eq!(manifest.blobs.len(), 1);
316 assert_eq!(manifest.blobs[0].uncompressed_size, 5678);
317 assert_eq!(manifest.blobs[0].fuchsia_merkle_root, [4; 32].into());
318 }
319
320 #[test]
321 fn test_parse_ota_manifest_invalid_proto() {
322 let err = parse_ota_manifest(b"invalid proto").unwrap_err();
323 assert_matches!(err, OtaManifestError::ParseProto(_));
324 }
325
326 #[test]
327 fn test_parse_ota_manifest_invalid_manifest() {
328 let proto_manifest = proto::OtaManifest {
329 build_info_version: "1.2.3.4".to_string(),
330 board: "test-board".to_string(),
331 epoch: 1,
332 mode: proto::UpdateMode::Normal.into(),
333 blob_base_url: "http://example.com".to_string(),
334 images: vec![proto::Image {
335 slot: proto::Slot::Ab.into(),
336 image_type: None,
337 blob: Some(proto::Blob {
338 uncompressed_size: 1234,
339 fuchsia_merkle_root: vec![1; 32],
340 }),
341 }],
342 blobs: vec![],
343 };
344 let buf = proto_manifest.encode_to_vec();
345
346 let err = parse_ota_manifest(&buf).unwrap_err();
347 assert_matches!(err, OtaManifestError::InvalidManifest(msg) if msg == "image_type missing");
348 }
349
350 #[test]
351 fn test_serialize_ota_manifest() {
352 let manifest = OtaManifest {
353 build_info_version: SystemVersion::from_str("1.2.3.4").unwrap(),
354 board: "test-board".to_string(),
355 epoch: 1,
356 mode: UpdateMode::Normal,
357 blob_base_url: "http://example.com".to_string(),
358 images: vec![
359 Image {
360 slot: Slot::AB,
361 image_type: ImageType::Asset(AssetType::Zbi),
362 blob: Blob { uncompressed_size: 1234, fuchsia_merkle_root: [1; 32].into() },
363 },
364 Image {
365 slot: Slot::AB,
366 image_type: ImageType::Firmware("bootloader".to_string()),
367 blob: Blob { uncompressed_size: 3456, fuchsia_merkle_root: [3; 32].into() },
368 },
369 ],
370 blobs: vec![Blob { uncompressed_size: 5678, fuchsia_merkle_root: [4; 32].into() }],
371 };
372
373 let buf = manifest.clone().serialize();
374 let parsed_manifest = parse_ota_manifest(&buf).unwrap();
375
376 assert_eq!(manifest, parsed_manifest);
377 }
378
379 #[test]
380 fn image_from_path() {
381 let file = NamedTempFile::new().unwrap();
382 std::fs::write(file.path(), b"hello world").unwrap();
383
384 let image =
385 Image::from_path(file.path(), Slot::AB, ImageType::Asset(AssetType::Zbi)).unwrap();
386
387 assert_eq!(image.blob.uncompressed_size, 11);
388 assert_eq!(
389 image.blob.fuchsia_merkle_root,
390 "8af85e2fe5da3385ea468ed1cb8412eaea6530a90b5dd8dee96529c8d9d39b97".parse().unwrap()
391 );
392 }
393
394 #[test]
395 fn image_from_path_large_file() {
396 let mut file = NamedTempFile::new().unwrap();
397 let chunk = [1; 1024];
398 let mut merkle_builder = fuchsia_merkle::BufferedMerkleRootBuilder::default();
399 for _ in 0..1000 {
400 file.write_all(&chunk).unwrap();
401 merkle_builder.write(&chunk);
402 }
403
404 let image =
405 Image::from_path(file.path(), Slot::AB, ImageType::Asset(AssetType::Zbi)).unwrap();
406
407 assert_eq!(image.blob.uncompressed_size, 1000 * 1024);
408 assert_eq!(image.blob.fuchsia_merkle_root, merkle_builder.complete());
409 }
410}