| // Copyright 2019 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use crate::errors::{Error, ErrorKind}; |
| use failure::ResultExt; |
| use fidl_fuchsia_io; |
| use fidl_fuchsia_pkg::{PackageResolverMarker, PackageResolverProxyInterface, UpdatePolicy}; |
| use fuchsia_component::client::connect_to_service; |
| use fuchsia_merkle::Hash; |
| use fuchsia_zircon as zx; |
| use std::io::{self, BufRead}; |
| |
| const UPDATE_PACKAGE_URL: &str = "fuchsia-pkg://fuchsia.com/update/0"; |
| |
| #[derive(PartialEq, Eq, Debug, Clone)] |
| pub enum SystemUpdateStatus { |
| UpToDate { system_image: Hash }, |
| UpdateAvailable { current_system_image: Hash, latest_system_image: Hash }, |
| } |
| |
| pub async fn check_for_system_update() -> Result<SystemUpdateStatus, Error> { |
| let mut file_system = RealFileSystem; |
| let package_resolver = |
| connect_to_service::<PackageResolverMarker>().context(ErrorKind::ConnectPackageResolver)?; |
| check_for_system_update_impl(&mut file_system, &package_resolver).await |
| } |
| |
| // For mocking |
| trait FileSystem { |
| fn read_to_string(&self, path: &str) -> io::Result<String>; |
| fn remove_file(&mut self, path: &str) -> io::Result<()>; |
| } |
| |
| struct RealFileSystem; |
| |
| impl FileSystem for RealFileSystem { |
| fn read_to_string(&self, path: &str) -> io::Result<String> { |
| std::fs::read_to_string(path) |
| } |
| fn remove_file(&mut self, path: &str) -> io::Result<()> { |
| std::fs::remove_file(path) |
| } |
| } |
| |
| async fn check_for_system_update_impl( |
| file_system: &mut impl FileSystem, |
| package_resolver: &impl PackageResolverProxyInterface, |
| ) -> Result<SystemUpdateStatus, Error> { |
| let current = current_system_image_merkle(file_system)?; |
| let latest = latest_system_image_merkle(package_resolver).await?; |
| if current == latest { |
| Ok(SystemUpdateStatus::UpToDate { system_image: current }) |
| } else { |
| Ok(SystemUpdateStatus::UpdateAvailable { |
| current_system_image: current, |
| latest_system_image: latest, |
| }) |
| } |
| } |
| |
| fn current_system_image_merkle(file_system: &impl FileSystem) -> Result<Hash, Error> { |
| Ok(file_system |
| .read_to_string("/system/meta") |
| .context(ErrorKind::ReadSystemMeta)? |
| .parse::<Hash>() |
| .context(ErrorKind::ParseSystemMeta)?) |
| } |
| |
| async fn latest_system_image_merkle( |
| package_resolver: &impl PackageResolverProxyInterface, |
| ) -> Result<Hash, Error> { |
| let (dir_proxy, dir_server_end) = |
| fidl::endpoints::create_proxy().context(ErrorKind::CreateUpdatePackageDirectoryProxy)?; |
| let status = package_resolver |
| .resolve( |
| &UPDATE_PACKAGE_URL, |
| &mut vec![].into_iter(), |
| &mut UpdatePolicy { fetch_if_absent: true, allow_old_versions: false }, |
| dir_server_end, |
| ) |
| .await |
| .context(ErrorKind::ResolveUpdatePackageFidl)?; |
| zx::Status::ok(status).context(ErrorKind::ResolveUpdatePackage)?; |
| |
| let (file_end, file_server_end) = fidl::endpoints::create_endpoints() |
| .context(ErrorKind::CreateUpdatePackagePackagesEndpoint)?; |
| dir_proxy |
| .open( |
| fidl_fuchsia_io::OPEN_FLAG_NOT_DIRECTORY | fidl_fuchsia_io::OPEN_RIGHT_READABLE, |
| 0, |
| "packages", |
| file_server_end, |
| ) |
| .context(ErrorKind::OpenUpdatePackagePackages)?; |
| |
| // danger: synchronous io in async |
| let packages_file = |
| fdio::create_fd(file_end.into_channel().into()).context(ErrorKind::CreatePackagesFd)?; |
| extract_system_image_merkle_from_update_packages(packages_file) |
| } |
| |
| fn extract_system_image_merkle_from_update_packages(reader: impl io::Read) -> Result<Hash, Error> { |
| for line in io::BufReader::new(reader).lines() { |
| let line = line.context(ErrorKind::ReadPackages)?; |
| if let Some(i) = line.rfind('=') { |
| let (key, value) = line.split_at(i + 1); |
| if key == "system_image/0=" { |
| return Ok(value |
| .parse::<Hash>() |
| .context(ErrorKind::ParseLatestSystemImageMerkle { packages_entry: line })?); |
| } |
| } |
| } |
| Err(ErrorKind::MissingLatestSystemImageMerkle)? |
| } |
| |
| #[cfg(test)] |
| mod test_check_for_system_update_impl { |
| use super::*; |
| use fuchsia_async::{self as fasync, futures::future}; |
| use maplit::hashmap; |
| use matches::assert_matches; |
| use std::collections::hash_map::HashMap; |
| use std::fs; |
| use std::io::Write; |
| |
| const ACTIVE_SYSTEM_IMAGE_MERKLE: &str = |
| "0000000000000000000000000000000000000000000000000000000000000000"; |
| const NEW_SYSTEM_IMAGE_MERKLE: &str = |
| "1111111111111111111111111111111111111111111111111111111111111111"; |
| |
| struct FakeFileSystem { |
| contents: HashMap<String, String>, |
| } |
| impl FakeFileSystem { |
| fn new_with_valid_system_meta() -> FakeFileSystem { |
| FakeFileSystem { |
| contents: hashmap![ |
| "/system/meta".to_string() => ACTIVE_SYSTEM_IMAGE_MERKLE.to_string() |
| ], |
| } |
| } |
| } |
| impl FileSystem for FakeFileSystem { |
| fn read_to_string(&self, path: &str) -> io::Result<String> { |
| self.contents |
| .get(path) |
| .ok_or(io::Error::new( |
| io::ErrorKind::NotFound, |
| format!("not present in fake file system: {}", path), |
| )) |
| .map(|s| s.to_string()) |
| } |
| fn remove_file(&mut self, path: &str) -> io::Result<()> { |
| self.contents.remove(path).and(Some(())).ok_or(io::Error::new( |
| io::ErrorKind::NotFound, |
| format!("fake file system cannot remove non-existent file: {}", path), |
| )) |
| } |
| } |
| |
| struct PackageResolverProxyTempDir { |
| temp_dir: tempfile::TempDir, |
| } |
| impl PackageResolverProxyTempDir { |
| fn new_with_empty_dir() -> PackageResolverProxyTempDir { |
| PackageResolverProxyTempDir { temp_dir: tempfile::tempdir().expect("create temp dir") } |
| } |
| fn new_with_empty_packages_file() -> PackageResolverProxyTempDir { |
| let temp_dir = tempfile::tempdir().expect("create temp dir"); |
| fs::File::create(format!( |
| "{}/packages", |
| temp_dir.path().to_str().expect("path is utf8") |
| )) |
| .expect("create empty packages file"); |
| PackageResolverProxyTempDir { temp_dir } |
| } |
| fn new_with_latest_system_image_merkle(merkle: &str) -> PackageResolverProxyTempDir { |
| let temp_dir = tempfile::tempdir().expect("create temp dir"); |
| let mut packages_file = fs::File::create(format!( |
| "{}/packages", |
| temp_dir.path().to_str().expect("path is utf8") |
| )) |
| .expect("create empty packages file"); |
| write!(&mut packages_file, "system_image/0={}\n", merkle) |
| .expect("write to package file"); |
| PackageResolverProxyTempDir { temp_dir } |
| } |
| } |
| impl PackageResolverProxyInterface for PackageResolverProxyTempDir { |
| type ResolveResponseFut = future::Ready<Result<i32, fidl::Error>>; |
| fn resolve( |
| &self, |
| package_url: &str, |
| selectors: &mut dyn ExactSizeIterator<Item = &str>, |
| update_policy: &mut UpdatePolicy, |
| dir: fidl::endpoints::ServerEnd<fidl_fuchsia_io::DirectoryMarker>, |
| ) -> Self::ResolveResponseFut { |
| assert_eq!(package_url, UPDATE_PACKAGE_URL); |
| assert_eq!(selectors.len(), 0); |
| assert_eq!( |
| update_policy, |
| &UpdatePolicy { fetch_if_absent: true, allow_old_versions: false } |
| ); |
| fdio::service_connect( |
| self.temp_dir.path().to_str().expect("path is utf8"), |
| dir.into_channel(), |
| ) |
| .unwrap(); |
| future::ok(zx::sys::ZX_OK) |
| } |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_missing_system_meta_file() { |
| let mut file_system = FakeFileSystem { contents: hashmap![] }; |
| let package_resolver = PackageResolverProxyTempDir::new_with_empty_dir(); |
| |
| let result = check_for_system_update_impl(&mut file_system, &package_resolver).await; |
| |
| assert_matches!(result.map_err(|e| e.kind()), Err(ErrorKind::ReadSystemMeta)); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_malformatted_system_meta_file() { |
| let mut file_system = FakeFileSystem { |
| contents: hashmap![ |
| "/system/meta".to_string() => "not-a-merkle".to_string() |
| ], |
| }; |
| let package_resolver = PackageResolverProxyTempDir::new_with_empty_dir(); |
| |
| let result = check_for_system_update_impl(&mut file_system, &package_resolver).await; |
| |
| assert_matches!(result.map_err(|e| e.kind()), Err(ErrorKind::ParseSystemMeta)); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_resolve_update_package_fidl_error() { |
| struct PackageResolverProxyFidlError; |
| impl PackageResolverProxyInterface for PackageResolverProxyFidlError { |
| type ResolveResponseFut = future::Ready<Result<i32, fidl::Error>>; |
| fn resolve( |
| &self, |
| _package_url: &str, |
| _selectors: &mut dyn ExactSizeIterator<Item = &str>, |
| _update_policy: &mut UpdatePolicy, |
| _dir: fidl::endpoints::ServerEnd<fidl_fuchsia_io::DirectoryMarker>, |
| ) -> Self::ResolveResponseFut { |
| future::err(fidl::Error::Invalid) |
| } |
| } |
| |
| let mut file_system = FakeFileSystem::new_with_valid_system_meta(); |
| let package_resolver = PackageResolverProxyFidlError; |
| |
| let result = check_for_system_update_impl(&mut file_system, &package_resolver).await; |
| |
| assert_matches!(result.map_err(|e| e.kind()), Err(ErrorKind::ResolveUpdatePackageFidl)); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_resolve_update_package_zx_error() { |
| struct PackageResolverProxyZxError; |
| impl PackageResolverProxyInterface for PackageResolverProxyZxError { |
| type ResolveResponseFut = future::Ready<Result<i32, fidl::Error>>; |
| fn resolve( |
| &self, |
| _package_url: &str, |
| _selectors: &mut dyn ExactSizeIterator<Item = &str>, |
| _update_policy: &mut UpdatePolicy, |
| _dir: fidl::endpoints::ServerEnd<fidl_fuchsia_io::DirectoryMarker>, |
| ) -> Self::ResolveResponseFut { |
| future::ok(zx::sys::ZX_ERR_INTERNAL) |
| } |
| } |
| |
| let mut file_system = FakeFileSystem::new_with_valid_system_meta(); |
| let package_resolver = PackageResolverProxyZxError; |
| |
| let result = check_for_system_update_impl(&mut file_system, &package_resolver).await; |
| |
| assert_matches!(result.map_err(|e| e.kind()), Err(ErrorKind::ResolveUpdatePackage)); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_resolve_update_package_directory_closed() { |
| struct PackageResolverProxyDirectoryCloser; |
| impl PackageResolverProxyInterface for PackageResolverProxyDirectoryCloser { |
| type ResolveResponseFut = future::Ready<Result<i32, fidl::Error>>; |
| fn resolve( |
| &self, |
| _package_url: &str, |
| _selectors: &mut dyn ExactSizeIterator<Item = &str>, |
| _update_policy: &mut UpdatePolicy, |
| _dir: fidl::endpoints::ServerEnd<fidl_fuchsia_io::DirectoryMarker>, |
| ) -> Self::ResolveResponseFut { |
| future::ok(zx::sys::ZX_OK) |
| } |
| } |
| |
| let mut file_system = FakeFileSystem::new_with_valid_system_meta(); |
| let package_resolver = PackageResolverProxyDirectoryCloser; |
| |
| let result = check_for_system_update_impl(&mut file_system, &package_resolver).await; |
| |
| assert_matches!(result.map_err(|e| e.kind()), Err(ErrorKind::OpenUpdatePackagePackages)); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_update_package_missing_packages_file() { |
| let mut file_system = FakeFileSystem::new_with_valid_system_meta(); |
| let package_resolver = PackageResolverProxyTempDir::new_with_empty_dir(); |
| |
| let result = check_for_system_update_impl(&mut file_system, &package_resolver).await; |
| |
| assert_matches!(result.map_err(|e| e.kind()), Err(ErrorKind::CreatePackagesFd)); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_update_package_empty_packages_file() { |
| let mut file_system = FakeFileSystem::new_with_valid_system_meta(); |
| let package_resolver = PackageResolverProxyTempDir::new_with_empty_packages_file(); |
| |
| let result = check_for_system_update_impl(&mut file_system, &package_resolver).await; |
| |
| assert_matches!( |
| result.map_err(|e| e.kind()), |
| Err(ErrorKind::MissingLatestSystemImageMerkle) |
| ); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_update_package_bad_system_image_merkle() { |
| let mut file_system = FakeFileSystem::new_with_valid_system_meta(); |
| let package_resolver = |
| PackageResolverProxyTempDir::new_with_latest_system_image_merkle("bad-merkle"); |
| |
| let result = check_for_system_update_impl(&mut file_system, &package_resolver).await; |
| |
| assert_matches!( |
| result.map_err(|e| e.kind()), |
| Err(ErrorKind::ParseLatestSystemImageMerkle { |
| ref packages_entry |
| }) |
| if packages_entry == "system_image/0=bad-merkle" |
| ); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_up_to_date() { |
| let mut file_system = FakeFileSystem::new_with_valid_system_meta(); |
| let package_resolver = PackageResolverProxyTempDir::new_with_latest_system_image_merkle( |
| ACTIVE_SYSTEM_IMAGE_MERKLE, |
| ); |
| |
| let result = check_for_system_update_impl(&mut file_system, &package_resolver).await; |
| |
| assert_matches!( |
| result, |
| Ok(SystemUpdateStatus::UpToDate { system_image }) |
| if system_image == ACTIVE_SYSTEM_IMAGE_MERKLE |
| .parse() |
| .expect("active system image string literal") |
| ); |
| } |
| |
| #[fasync::run_singlethreaded(test)] |
| async fn test_update_available() { |
| let mut file_system = FakeFileSystem::new_with_valid_system_meta(); |
| let package_resolver = PackageResolverProxyTempDir::new_with_latest_system_image_merkle( |
| NEW_SYSTEM_IMAGE_MERKLE, |
| ); |
| |
| let result = check_for_system_update_impl(&mut file_system, &package_resolver).await; |
| |
| assert_matches!( |
| result, |
| Ok(SystemUpdateStatus::UpdateAvailable { current_system_image, latest_system_image }) |
| if |
| current_system_image == ACTIVE_SYSTEM_IMAGE_MERKLE |
| .parse() |
| .expect("active system image string literal") && |
| latest_system_image == NEW_SYSTEM_IMAGE_MERKLE |
| .parse() |
| .expect("new system image string literal") |
| ); |
| } |
| } |
| |
| #[cfg(test)] |
| mod test_real_file_system { |
| use super::*; |
| use matches::assert_matches; |
| use proptest::prelude::*; |
| use std::fs; |
| use std::io::{self, Write}; |
| |
| #[test] |
| fn test_read_to_string_errors_on_missing_file() { |
| let dir = tempfile::tempdir().expect("create temp dir"); |
| let read_res = RealFileSystem.read_to_string( |
| dir.path().join("this-file-does-not-exist").to_str().expect("paths are utf8"), |
| ); |
| assert_matches!(read_res.map_err(|e| e.kind()), Err(io::ErrorKind::NotFound)); |
| } |
| |
| proptest! { |
| #[test] |
| fn test_read_to_string_preserves_contents( |
| contents in ".{0, 65}", |
| file_name in "[^\\.\0/]{1,10}", |
| ) { |
| let dir = tempfile::tempdir().expect("create temp dir"); |
| let file_path = dir.path().join(file_name); |
| let mut file = fs::File::create(&file_path).expect("create file"); |
| file.write_all(contents.as_bytes()).expect("write the contents"); |
| |
| let read_contents = RealFileSystem |
| .read_to_string(file_path.to_str().expect("paths are utf8")) |
| .expect("read the file"); |
| |
| prop_assert_eq!(read_contents, contents); |
| } |
| } |
| } |