blob: fdac55578448706485689a7c7bedd3e998dbe5a0 [file] [log] [blame]
// Copyright 2022 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 anyhow::Context as _;
use assembly_config_schema::ImageAssemblyConfig;
use assembly_util::FileEntry;
use assembly_validate_util::{BootfsContents, PkgNamespace};
use camino::Utf8PathBuf;
use fuchsia_pkg::PackageManifest;
use rayon::iter::{ParallelBridge, ParallelIterator};
use std::{collections::BTreeMap, fmt, fs::File, path::PathBuf};
use version_history::AbiRevision;
/// Validate a product config.
pub fn validate_product(
product: &ImageAssemblyConfig,
warn_only: bool,
) -> Result<(), ProductValidationError> {
// validate the packages in the system/base/cache package sets
let manifests = product.system.iter().chain(product.base.iter()).chain(product.cache.iter());
let packages: BTreeMap<_, _> = manifests
.par_bridge()
.filter_map(|package_manifest_path| {
match PackageManifest::try_load_from(&package_manifest_path) {
Ok(manifest) => {
// After loading the manifest, validate it
match validate_package(&manifest) {
Ok(()) => None,
Err(e) => {
// If validation issues have been downgraded to warnings, then print
// a warning instead of returning an error.
let print_warning = warn_only && match e {
PackageValidationError::MissingAbiRevisionFile(_) => true,
PackageValidationError::InvalidAbiRevisionFile(_) => true,
PackageValidationError::UnsupportedAbiRevision (_) => true,
_ => false};
if print_warning {
println!("WARNING: The package named '{}', with manifest at {} failed validation:\n{}", manifest.name(), package_manifest_path, e);
None
} else {
// return the error
Some((package_manifest_path.to_owned(), e))
}
}
}
}
// Convert any error loading the manifest into the appropriate
// error type.
Err(e) => Some((package_manifest_path.to_owned(), PackageValidationError::LoadPackageManifest(e)))
}
})
.collect();
// validate the contents of bootfs
match validate_bootfs(&product.bootfs_files) {
Ok(()) if packages.is_empty() => Ok(()),
Ok(()) => Err(ProductValidationError { bootfs: Default::default(), packages }),
Err(bootfs) => Err(ProductValidationError { bootfs: Some(bootfs), packages }),
}
}
/// Validate the contents of bootfs.
///
/// Assumes that all component manifests have a `.cm` extension within the destination namespace.
fn validate_bootfs(bootfs_files: &[FileEntry<String>]) -> Result<(), BootfsValidationError> {
let mut bootfs = BootfsContents::from_iter(
bootfs_files.iter().map(|entry| (entry.destination.to_string(), &entry.source)),
)
.map_err(BootfsValidationError::ReadContents)?;
// validate components
let mut errors = BTreeMap::new();
for path in bootfs.paths().into_iter().filter(|p| p.ends_with(".cm")) {
if let Err(e) = validate_component(&path, &mut bootfs) {
errors.insert(path, e);
}
}
if errors.is_empty() {
Ok(())
} else {
Err(BootfsValidationError::InvalidComponents(errors))
}
}
/// Validate a package's contents.
///
/// Assumes that all component manifests will be in the `meta/` directory and have a `.cm` extension
/// within the package namespace.
pub fn validate_package(manifest: &PackageManifest) -> Result<(), PackageValidationError> {
let blobs = manifest.blobs();
// read meta.far contents
let meta_far_info = blobs
.into_iter()
.find(|b| b.path == "meta/")
.ok_or(PackageValidationError::MissingMetaFar)?;
let meta_far =
File::open(&meta_far_info.source_path).map_err(|source| PackageValidationError::Open {
source,
path: PathBuf::from(meta_far_info.source_path.clone()),
})?;
let mut reader =
fuchsia_archive::Utf8Reader::new(meta_far).map_err(PackageValidationError::ReadArchive)?;
// validate components in the meta/ directory
let mut errors = BTreeMap::new();
for path in reader.paths().into_iter().filter(|p| p.ends_with(".cm")) {
if let Err(e) = validate_component(&path, &mut reader) {
errors.insert(path, e);
}
}
if !errors.is_empty() {
return Err(PackageValidationError::InvalidComponents(errors));
}
// validate the abi_revision of the package
let raw_abi_revision = reader
.read_file(fuchsia_pkg::ABI_REVISION_FILE_PATH)
.map_err(|e| PackageValidationError::MissingAbiRevisionFile(e))?;
let abi_revision = AbiRevision::try_from(raw_abi_revision.as_slice())
.map_err(|e| PackageValidationError::InvalidAbiRevisionFile(e))?;
if let Err(err) = version_history::HISTORY.check_abi_revision_for_runtime(abi_revision) {
return Err(PackageValidationError::UnsupportedAbiRevision(err));
}
Ok(())
}
/// Validate an individual component within the package.
fn validate_component(
manifest_path: &str,
pkg_namespace: &mut impl PkgNamespace,
) -> anyhow::Result<()> {
assembly_structured_config::validate_component(manifest_path, pkg_namespace)
.context("Validating structured configuration")?;
Ok(())
}
/// Collection of all package validation failures within a product.
#[derive(Debug)]
pub struct ProductValidationError {
/// Files in bootfs which failed validation.
bootfs: Option<BootfsValidationError>,
/// Packages which failed validation.
packages: BTreeMap<Utf8PathBuf, PackageValidationError>,
}
impl From<ProductValidationError> for anyhow::Error {
fn from(e: ProductValidationError) -> anyhow::Error {
anyhow::Error::msg(e)
}
}
impl fmt::Display for ProductValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Validating product assembly failed:")?;
if let Some(error) = &self.bootfs {
let error_msg = textwrap::indent(&error.to_string(), " ");
write!(f, " └── Failed to validate bootfs: {}", error_msg)?;
}
for (package, error) in &self.packages {
let error_msg = textwrap::indent(&error.to_string(), " ");
write!(f, " └── {}: {}", package, error_msg)?;
}
Ok(())
}
}
#[derive(Debug)]
pub enum BootfsValidationError {
ReadContents(assembly_validate_util::BootfsContentsError),
InvalidComponents(BTreeMap<String, anyhow::Error>),
}
impl fmt::Display for BootfsValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use BootfsValidationError::*;
match self {
ReadContents(source) => {
write!(f, "Unable to read bootfs contents: {}", source)
}
InvalidComponents(components) => {
for (name, error) in components {
write!(f, "\n└── {}: {}", name, error)?;
let mut source = error.source();
while let Some(s) = source {
write!(f, "\n └── {}", s)?;
source = s.source();
}
}
Ok(())
}
}
}
}
/// Failures that can occur when validating packages.
#[derive(Debug)]
pub enum PackageValidationError {
Open { path: PathBuf, source: std::io::Error },
LoadPackageManifest(anyhow::Error),
MissingMetaFar,
ReadArchive(fuchsia_archive::Error),
InvalidComponents(BTreeMap<String, anyhow::Error>),
MissingAbiRevisionFile(fuchsia_archive::Error),
InvalidAbiRevisionFile(std::array::TryFromSliceError),
UnsupportedAbiRevision(version_history::AbiRevisionError),
}
impl fmt::Display for PackageValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use PackageValidationError::*;
match self {
Open { path, source } => write!(f, "Unable to open `{}`: {}.", path.display(), source),
LoadPackageManifest(source) => {
write!(f, "Unable to decode JSON for package manifest: {}.", source)
}
MissingMetaFar => write!(f, "The package seems to be missing a meta/ directory."),
ReadArchive(source) => write!(f, "Unable to read the package's meta.far: {}.", source),
InvalidComponents(components) => {
for (name, error) in components {
write!(f, "\n└── {}: {}", name, error)?;
let mut source = error.source();
while let Some(s) = source {
write!(f, "\n └── {}", s)?;
source = s.source();
}
}
Ok(())
}
MissingAbiRevisionFile(cause) => {
write!(
f,
"The package seems to be missing an abi revision file: {}",
fuchsia_pkg::ABI_REVISION_FILE_PATH
)?;
write!(f, "\n└── {}", cause)?;
Ok(())
}
InvalidAbiRevisionFile(cause) => {
write!(f, "The package abi revision file was not valid")?;
write!(f, "\n└── {cause}")?;
Ok(())
}
UnsupportedAbiRevision(cause) => {
write!(f, "Error validating abi revision: {cause}")?;
Ok(())
}
}
}
}