blob: f74f85fd1ecdf0f1a63e058aae4a8a9cf22211ec [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::{FileEntry, ImageAssemblyConfig};
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};
/// Validate a product config.
pub fn validate_product(product: &ImageAssemblyConfig) -> 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| {
if let Err(e) = PackageManifest::try_load_from(&package)
.map_err(PackageValidationError::LoadPackageManifest)
.and_then(|m| validate_package(&m))
{
Some((package.to_owned(), e))
} else {
None
}
})
.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]) -> Result<(), BootfsValidationError> {
let mut bootfs = BootfsContents::from_iter(
bootfs_files.iter().map(|entry| (&entry.destination, &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));
}
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>),
}
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(())
}
}
}
}