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