blob: 8f3200a9ec5cdd454a2c9cc435e5758a9a67a803 [file] [log] [blame]
// 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);
}
}
}