blob: 213174742feb2a5ded6dc50f28aaf40b556585b2 [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.
//! Utilities for working with structured configuration during the product assembly process.
use anyhow::{ensure, format_err, Context};
use assembly_validate_util::PkgNamespace;
use camino::{Utf8Path, Utf8PathBuf};
use cm_rust::{FidlIntoNative, NativeIntoFidl};
use fidl::encoding::{unpersist, Persistable};
use fuchsia_pkg::{PackageBuilder, PackageManifest};
use std::{collections::BTreeMap, fmt::Debug};
/// Add structured configuration values to an existing package.
pub struct Repackager<B> {
builder: B,
outdir: Utf8PathBuf,
}
impl Repackager<PackageBuilder> {
/// Read an existing package manifest for modification. The new manifest will be written to
/// `outdir` along with any needed temporary files.
pub fn new(
original_manifest: PackageManifest,
outdir: impl Into<Utf8PathBuf>,
) -> Result<Self, RepackageError> {
let outdir = outdir.into();
let builder = PackageBuilder::from_manifest(original_manifest, &outdir)
.map_err(RepackageError::CreatePackageBuilder)?;
Ok(Self { builder, outdir })
}
/// Build the modified package, returning a path to its new manifest.
pub fn build(self) -> Result<Utf8PathBuf, RepackageError> {
let Self { mut builder, outdir } = self;
let manifest_path = outdir.join("package_manifest.json");
builder.manifest_path(&manifest_path);
builder.build(&outdir, outdir.join("meta.far")).map_err(RepackageError::BuildPackage)?;
Ok(manifest_path)
}
}
impl<'f> Repackager<&'f mut BTreeMap<String, Utf8PathBuf>> {
pub fn for_bootfs(
files: &'f mut BTreeMap<String, Utf8PathBuf>,
outdir: impl Into<Utf8PathBuf>,
) -> Self {
Self { builder: files, outdir: outdir.into() }
}
}
impl<B: PkgNamespaceBuilder> Repackager<B> {
/// Check if the package we're building has a particular component manifest.
pub fn has_component(&self, manifest_path: &str) -> bool {
self.builder.read_contents(manifest_path).is_ok()
}
/// Apply structured configuration values to this package, failing if the package already has
/// a configuration value file for the component.
pub fn set_component_config(
&mut self,
manifest_path: &str,
values: BTreeMap<String, serde_json::Value>,
) -> Result<(), RepackageError> {
// read the manifest from the meta.far and parse it
let manifest_bytes =
self.builder.read_contents(manifest_path).map_err(RepackageError::ReadManifest)?;
let manifest: cm_rust::ComponentDecl =
read_and_validate_fidl(&manifest_bytes, cm_fidl_validator::validate)
.map_err(RepackageError::ParseManifest)?;
if let Some(config_decl) = manifest.config {
// create a value file
let mut config_values =
config_value_file::populate_value_file(&config_decl, values)?.native_into_fidl();
let config_bytes = fidl::encoding::persist(&mut config_values)
.map_err(RepackageError::EncodeConfig)?;
// write it to the meta.far at the path expected by the resolver
let cm_rust::ConfigValueSource::PackagePath(path) = &config_decl.value_source;
self.builder
.add_contents(path, &config_bytes, &self.outdir)
.map_err(RepackageError::WriteValueFile)?;
Ok(())
} else {
Err(RepackageError::MissingConfigDecl)
}
}
}
pub trait PkgNamespaceBuilder {
fn read_contents(&self, path: &str) -> anyhow::Result<Vec<u8>>;
fn add_contents(
&mut self,
path: &str,
contents: &[u8],
outdir: impl AsRef<Utf8Path>,
) -> anyhow::Result<()>;
}
impl PkgNamespaceBuilder for PackageBuilder {
fn read_contents(&self, path: &str) -> anyhow::Result<Vec<u8>> {
ensure!(path.starts_with("meta/"), "reading outside of meta/ is not supported");
self.read_contents_from_far(path)
}
fn add_contents(
&mut self,
path: &str,
contents: &[u8],
outdir: impl AsRef<Utf8Path>,
) -> anyhow::Result<()> {
ensure!(path.starts_with("meta/"), "writing outside of meta/ is not supported");
self.add_contents_to_far(path, contents, outdir.as_ref().as_std_path())
}
}
impl PkgNamespaceBuilder for &mut BTreeMap<String, Utf8PathBuf> {
fn read_contents(&self, path: &str) -> anyhow::Result<Vec<u8>> {
let src_path = self.get(path).ok_or_else(|| format_err!("missing {path}"))?;
Ok(std::fs::read(&src_path).with_context(|| format!("reading {}", src_path))?)
}
fn add_contents(
&mut self,
path: &str,
contents: &[u8],
outdir: impl AsRef<Utf8Path>,
) -> anyhow::Result<()> {
let out_path = outdir.as_ref().join("bootfs/repackaging/components").join(path);
std::fs::create_dir_all(out_path.parent().unwrap()).context("creating outdir")?;
std::fs::write(&out_path, contents).with_context(|| format!("writing {}", out_path))?;
self.insert(path.to_owned(), out_path);
Ok(())
}
}
#[derive(Debug, thiserror::Error)]
pub enum RepackageError {
#[error("Couldn't read package manifest for modification.")]
CreatePackageBuilder(#[source] anyhow::Error),
#[error("Couldn't read component manifest.")]
ReadManifest(#[source] anyhow::Error),
#[error("Couldn't parse manifest.")]
ParseManifest(#[source] PersistentFidlError),
#[error("No config decl found in manifest.")]
MissingConfigDecl,
#[error("Couldn't compile a configuration value file.")]
ValueFileCreation(
#[source]
#[from]
config_value_file::FileError,
),
#[error("Couldn't encode config values as persistent FIDL.")]
EncodeConfig(#[source] fidl::Error),
#[error("Couldn't write the config value file to the modified package.")]
WriteValueFile(#[source] anyhow::Error),
#[error("Couldn't build the modified package.")]
BuildPackage(#[source] anyhow::Error),
}
/// The list of runners currently supported by structured config.
static SUPPORTED_RUNNERS: &[&str] = &["driver", "elf"];
/// Validate a component manifest given access to the contents of its `/pkg` directory.
///
/// Ensures that if a component has a `config` stanza it can be paired with the specified
/// value file and that together they can produce a valid configuration for the component.
///
/// Also ensures that the component is using a runner which supports structured config.
pub fn validate_component(
manifest_path: &str,
reader: &mut impl PkgNamespace,
) -> Result<(), ValidationError> {
// get the manifest and validate it
let manifest_bytes =
reader.read_file(manifest_path).map_err(ValidationError::ManifestMissing)?;
let manifest: cm_rust::ComponentDecl =
read_and_validate_fidl(&manifest_bytes, cm_fidl_validator::validate)
.map_err(ValidationError::ParseManifest)?;
// check for config
if let Some(config_decl) = manifest.config {
// make sure the component has a runner that will deliver config before finding values
let runner = manifest
.program
.as_ref()
.ok_or(ValidationError::ProgramMissing)?
.runner
.as_ref()
.ok_or(ValidationError::RunnerMissing)?
.str();
if !SUPPORTED_RUNNERS.contains(&runner) {
return Err(ValidationError::UnsupportedRunner(runner.to_owned()));
}
// config is required, so find out where it's stored
let cm_rust::ConfigValueSource::PackagePath(path) = &config_decl.value_source;
let config_bytes = reader.read_file(&path).map_err(ValidationError::ConfigValuesMissing)?;
// read and validate the config values
let config_values: cm_rust::ValuesData =
read_and_validate_fidl(&config_bytes, cm_fidl_validator::validate_values_data)
.map_err(ValidationError::ParseConfig)?;
// we have config, make sure it's compatible with the manifest which references it
config_encoder::ConfigFields::resolve(&config_decl, config_values)
.map_err(ValidationError::ResolveConfig)?;
}
Ok(())
}
#[derive(Debug, thiserror::Error)]
pub enum ValidationError {
#[error("Couldn't read manifest.")]
ManifestMissing(#[source] assembly_validate_util::ReadError),
#[error("Couldn't parse manifest.")]
ParseManifest(#[source] PersistentFidlError),
#[error("Couldn't find component's config values in package.")]
ConfigValuesMissing(#[source] assembly_validate_util::ReadError),
#[error("Couldn't parse config values.")]
ParseConfig(#[source] PersistentFidlError),
#[error("Couldn't resolve config.")]
ResolveConfig(#[source] config_encoder::ResolutionError),
#[error("Component manifest does not specify `program`.")]
ProgramMissing,
#[error("Component manifest does not specify `program.runner`.")]
RunnerMissing,
#[error("{:?} is not a supported runner. (allowed: {:?})", _0, SUPPORTED_RUNNERS)]
UnsupportedRunner(String),
}
/// Parse bytes as a FIDL type, passing it to a `cm_fidl_validator` function before converting
/// it to the desired `cm_rust` type.
fn read_and_validate_fidl<Raw, Output, VF>(
bytes: &[u8],
validate: VF,
) -> Result<Output, PersistentFidlError>
where
Raw: FidlIntoNative<Output> + Persistable,
VF: Fn(&Raw) -> Result<(), cm_fidl_validator::error::ErrorList>,
{
let raw: Raw = unpersist(&bytes).map_err(PersistentFidlError::Decode)?;
validate(&raw).map_err(PersistentFidlError::Validate)?;
Ok(raw.fidl_into_native())
}
#[derive(Debug, thiserror::Error)]
pub enum PersistentFidlError {
#[error("Couldn't decode bytes.")]
Decode(#[source] fidl::Error),
#[error("Couldn't validate raw FIDL type.")]
Validate(#[source] cm_fidl_validator::error::ErrorList),
}