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