blob: 700aecd154c2729c288b33083ce76b07f60b59ac [file] [log] [blame]
// Copyright 2021 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.
#![warn(missing_docs)]
//! Library for resolving and encoding the runtime configuration values of a component.
use cm_rust::{
ConfigChecksum, ConfigDecl, ConfigField as ConfigFieldDecl, ConfigNestedValueType,
ConfigValueType, NativeIntoFidl, SingleValue, Value, ValueSpec, ValuesData, VectorValue,
};
use dynfidl::{BasicField, Field, Structure, VectorField};
use fidl_fuchsia_component_config as fconfig;
use thiserror::Error;
/// The resolved configuration for a component.
#[derive(Clone, Debug, PartialEq)]
pub struct ConfigFields {
/// A list of all resolved fields, in the order of the compiled manifest.
pub fields: Vec<ConfigField>,
/// A checksum from the compiled manifest.
pub checksum: ConfigChecksum,
}
impl ConfigFields {
/// Resolve a component's configuration values according to its declared schema. Should not fail if
/// `decl` and `specs` are well-formed.
pub fn resolve(decl: &ConfigDecl, specs: ValuesData) -> Result<Self, ResolutionError> {
if decl.checksum != specs.checksum {
return Err(ResolutionError::ChecksumFailure {
expected: decl.checksum.clone(),
received: specs.checksum.clone(),
});
}
if decl.fields.len() != specs.values.len() {
return Err(ResolutionError::WrongNumberOfValues {
expected: decl.fields.len(),
received: specs.values.len(),
});
}
let fields = decl
.fields
.iter()
.zip(specs.values.into_iter())
.map(|(decl_field, spec_field)| {
ConfigField::resolve(spec_field, &decl_field).map_err(|source| {
ResolutionError::InvalidValue { key: decl_field.key.clone(), source }
})
})
.collect::<Result<Vec<ConfigField>, _>>()?;
Ok(Self { fields, checksum: decl.checksum.clone() })
}
/// Encode the resolved fields as a FIDL struct with every field non-nullable.
///
/// The first two bytes of the encoded buffer are a little-endian unsigned integer which denotes
/// the `checksum_length`. Bytes `2..2+checksum_length` are used to store the declaration
/// checksum. All remaining bytes are used to store the FIDL header and struct.
pub fn encode_as_fidl_struct(self) -> Vec<u8> {
let Self { fields, checksum: ConfigChecksum::Sha256(checksum) } = self;
let mut structure = Structure::default();
for ConfigField { value, .. } in fields {
structure = match value {
Value::Single(SingleValue::Bool(b)) => {
structure.field(Field::Basic(BasicField::Bool(b)))
}
Value::Single(SingleValue::Uint8(n)) => {
structure.field(Field::Basic(BasicField::UInt8(n)))
}
Value::Single(SingleValue::Uint16(n)) => {
structure.field(Field::Basic(BasicField::UInt16(n)))
}
Value::Single(SingleValue::Uint32(n)) => {
structure.field(Field::Basic(BasicField::UInt32(n)))
}
Value::Single(SingleValue::Uint64(n)) => {
structure.field(Field::Basic(BasicField::UInt64(n)))
}
Value::Single(SingleValue::Int8(n)) => {
structure.field(Field::Basic(BasicField::Int8(n)))
}
Value::Single(SingleValue::Int16(n)) => {
structure.field(Field::Basic(BasicField::Int16(n)))
}
Value::Single(SingleValue::Int32(n)) => {
structure.field(Field::Basic(BasicField::Int32(n)))
}
Value::Single(SingleValue::Int64(n)) => {
structure.field(Field::Basic(BasicField::Int64(n)))
}
Value::Single(SingleValue::String(s)) => {
// TODO(https://fxbug.dev/88174) improve string representation too
structure.field(Field::Vector(VectorField::UInt8Vector(s.into_bytes())))
}
Value::Vector(VectorValue::BoolVector(b)) => {
structure.field(Field::Vector(VectorField::BoolVector(b)))
}
Value::Vector(VectorValue::Uint8Vector(n)) => {
structure.field(Field::Vector(VectorField::UInt8Vector(n)))
}
Value::Vector(VectorValue::Uint16Vector(n)) => {
structure.field(Field::Vector(VectorField::UInt16Vector(n)))
}
Value::Vector(VectorValue::Uint32Vector(n)) => {
structure.field(Field::Vector(VectorField::UInt32Vector(n)))
}
Value::Vector(VectorValue::Uint64Vector(n)) => {
structure.field(Field::Vector(VectorField::UInt64Vector(n)))
}
Value::Vector(VectorValue::Int8Vector(n)) => {
structure.field(Field::Vector(VectorField::Int8Vector(n)))
}
Value::Vector(VectorValue::Int16Vector(n)) => {
structure.field(Field::Vector(VectorField::Int16Vector(n)))
}
Value::Vector(VectorValue::Int32Vector(n)) => {
structure.field(Field::Vector(VectorField::Int32Vector(n)))
}
Value::Vector(VectorValue::Int64Vector(n)) => {
structure.field(Field::Vector(VectorField::Int64Vector(n)))
}
Value::Vector(VectorValue::StringVector(s)) => structure.field(Field::Vector(
// TODO(https://fxbug.dev/88174) improve string representation too
VectorField::UInt8VectorVector(s.into_iter().map(|s| s.into_bytes()).collect()),
)),
};
}
let mut buf = Vec::new();
buf.extend((checksum.len() as u16).to_le_bytes());
buf.extend(checksum);
buf.extend(structure.encode_persistent());
buf
}
}
impl Into<fconfig::ResolvedConfig> for ConfigFields {
fn into(self) -> fconfig::ResolvedConfig {
let checksum = self.checksum.native_into_fidl();
let fields = self.fields.into_iter().map(|f| f.into()).collect();
fconfig::ResolvedConfig { checksum, fields }
}
}
/// A single resolved configuration field.
#[derive(Clone, Debug, PartialEq)]
pub struct ConfigField {
/// The configuration field's key.
pub key: String,
/// The configuration field's value.
pub value: Value,
}
impl ConfigField {
/// Reconciles a config field schema from the manifest with a value from the value file.
/// If the types and constraints don't match, an error is returned.
pub fn resolve(
spec_field: ValueSpec,
decl_field: &ConfigFieldDecl,
) -> Result<Self, ValueError> {
let key = decl_field.key.clone();
match (&spec_field.value, &decl_field.type_) {
(Value::Single(SingleValue::Bool(_)), ConfigValueType::Bool)
| (Value::Single(SingleValue::Uint8(_)), ConfigValueType::Uint8)
| (Value::Single(SingleValue::Uint16(_)), ConfigValueType::Uint16)
| (Value::Single(SingleValue::Uint32(_)), ConfigValueType::Uint32)
| (Value::Single(SingleValue::Uint64(_)), ConfigValueType::Uint64)
| (Value::Single(SingleValue::Int8(_)), ConfigValueType::Int8)
| (Value::Single(SingleValue::Int16(_)), ConfigValueType::Int16)
| (Value::Single(SingleValue::Int32(_)), ConfigValueType::Int32)
| (Value::Single(SingleValue::Int64(_)), ConfigValueType::Int64) => (),
(Value::Single(SingleValue::String(text)), ConfigValueType::String { max_size }) => {
let max_size = *max_size as usize;
if text.len() > max_size {
return Err(ValueError::StringTooLong { max: max_size, actual: text.len() });
}
}
(Value::Vector(list), ConfigValueType::Vector { nested_type, max_count }) => {
let max_count = *max_count as usize;
let actual_count = match (list, nested_type) {
(VectorValue::BoolVector(l), ConfigNestedValueType::Bool) => l.len(),
(VectorValue::Uint8Vector(l), ConfigNestedValueType::Uint8) => l.len(),
(VectorValue::Uint16Vector(l), ConfigNestedValueType::Uint16) => l.len(),
(VectorValue::Uint32Vector(l), ConfigNestedValueType::Uint32) => l.len(),
(VectorValue::Uint64Vector(l), ConfigNestedValueType::Uint64) => l.len(),
(VectorValue::Int8Vector(l), ConfigNestedValueType::Int8) => l.len(),
(VectorValue::Int16Vector(l), ConfigNestedValueType::Int16) => l.len(),
(VectorValue::Int32Vector(l), ConfigNestedValueType::Int32) => l.len(),
(VectorValue::Int64Vector(l), ConfigNestedValueType::Int64) => l.len(),
(VectorValue::StringVector(l), ConfigNestedValueType::String { max_size }) => {
let max_size = *max_size as usize;
for (i, s) in l.iter().enumerate() {
if s.len() > max_size {
return Err(ValueError::VectorElementInvalid {
offset: i,
source: Box::new(ValueError::StringTooLong {
max: max_size,
actual: s.len(),
}),
});
}
}
l.len()
}
(other_list, other_ty) => {
return Err(ValueError::TypeMismatch {
expected: format!("{:?}", other_ty),
received: format!("{:?}", other_list),
})
}
};
if actual_count > max_count {
return Err(ValueError::VectorTooLong { max: max_count, actual: actual_count });
}
}
(other_val, other_ty) => {
return Err(ValueError::TypeMismatch {
expected: format!("{:?}", other_ty),
received: format!("{:?}", other_val),
});
}
}
Ok(ConfigField { key, value: spec_field.value })
}
}
impl Into<fconfig::ResolvedConfigField> for ConfigField {
fn into(self) -> fconfig::ResolvedConfigField {
fconfig::ResolvedConfigField { key: self.key, value: self.value.native_into_fidl() }
}
}
#[derive(Clone, Debug, Error, PartialEq)]
#[allow(missing_docs)]
pub enum ResolutionError {
#[error("Checksums in declaration and value file do not match. Expected {expected:04x?}, received {received:04x?}")]
ChecksumFailure { expected: ConfigChecksum, received: ConfigChecksum },
#[error("Value file has a different number of values ({received}) than declaration has fields ({expected}).")]
WrongNumberOfValues { expected: usize, received: usize },
#[error("Value file has an invalid entry for `{key}`.")]
InvalidValue {
key: String,
#[source]
source: ValueError,
},
}
#[derive(Clone, Debug, Error, PartialEq)]
#[allow(missing_docs)]
pub enum ValueError {
#[error("Value of type `{received}` does not match declaration of type {expected}.")]
TypeMismatch { expected: String, received: String },
#[error("Received string of length {actual} for a field with a max of {max}.")]
StringTooLong { max: usize, actual: usize },
#[error("Received vector of length {actual} for a field with a max of {max}.")]
VectorTooLong { max: usize, actual: usize },
#[error("Vector element at {offset} index is invalid.")]
VectorElementInvalid {
offset: usize,
#[source]
source: Box<Self>,
},
}
#[cfg(test)]
mod tests {
use super::*;
use fidl_fuchsia_component_config_ext::{config_decl, values_data};
use SingleValue::*;
use Value::*;
use VectorValue::*;
#[test]
fn basic_success() {
let decl = config_decl! {
ck@ ConfigChecksum::Sha256([0; 32]),
my_flag: { bool },
my_uint8: { uint8 },
my_uint16: { uint16 },
my_uint32: { uint32 },
my_uint64: { uint64 },
my_int8: { int8 },
my_int16: { int16 },
my_int32: { int32 },
my_int64: { int64 },
my_string: { string, max_size: 100 },
my_vector_of_flag: { vector, element: bool, max_count: 100 },
my_vector_of_uint8: { vector, element: uint8, max_count: 100 },
my_vector_of_uint16: { vector, element: uint16, max_count: 100 },
my_vector_of_uint32: { vector, element: uint32, max_count: 100 },
my_vector_of_uint64: { vector, element: uint64, max_count: 100 },
my_vector_of_int8: { vector, element: int8, max_count: 100 },
my_vector_of_int16: { vector, element: int16, max_count: 100 },
my_vector_of_int32: { vector, element: int32, max_count: 100 },
my_vector_of_int64: { vector, element: int64, max_count: 100 },
my_vector_of_string: {
vector,
element: { string, max_size: 100 },
max_count: 100
},
};
let specs = values_data![
ck@ decl.checksum.clone(),
Single(Bool(false)),
Single(Uint8(255u8)),
Single(Uint16(65535u16)),
Single(Uint32(4000000000u32)),
Single(Uint64(8000000000u64)),
Single(Int8(-127i8)),
Single(Int16(-32766i16)),
Single(Int32(-2000000000i32)),
Single(Int64(-4000000000i64)),
Single(String("hello, world!".into())),
Vector(BoolVector(vec![true, false])),
Vector(Uint8Vector(vec![1, 2, 3])),
Vector(Uint16Vector(vec![2, 3, 4])),
Vector(Uint32Vector(vec![3, 4, 5])),
Vector(Uint64Vector(vec![4, 5, 6])),
Vector(Int8Vector(vec![-1, -2, 3])),
Vector(Int16Vector(vec![-2, -3, 4])),
Vector(Int32Vector(vec![-3, -4, 5])),
Vector(Int64Vector(vec![-4, -5, 6])),
Vector(StringVector(vec!["valid".into(), "valid".into()])),
];
let expected = ConfigFields {
fields: vec![
ConfigField { key: "my_flag".to_string(), value: Single(Bool(false)) },
ConfigField { key: "my_uint8".to_string(), value: Single(Uint8(255)) },
ConfigField { key: "my_uint16".to_string(), value: Single(Uint16(65535)) },
ConfigField { key: "my_uint32".to_string(), value: Single(Uint32(4000000000)) },
ConfigField { key: "my_uint64".to_string(), value: Single(Uint64(8000000000)) },
ConfigField { key: "my_int8".to_string(), value: Single(Int8(-127)) },
ConfigField { key: "my_int16".to_string(), value: Single(Int16(-32766)) },
ConfigField { key: "my_int32".to_string(), value: Single(Int32(-2000000000)) },
ConfigField { key: "my_int64".to_string(), value: Single(Int64(-4000000000)) },
ConfigField {
key: "my_string".to_string(),
value: Single(String("hello, world!".into())),
},
ConfigField {
key: "my_vector_of_flag".to_string(),
value: Vector(BoolVector(vec![true, false])),
},
ConfigField {
key: "my_vector_of_uint8".to_string(),
value: Vector(Uint8Vector(vec![1, 2, 3])),
},
ConfigField {
key: "my_vector_of_uint16".to_string(),
value: Vector(Uint16Vector(vec![2, 3, 4])),
},
ConfigField {
key: "my_vector_of_uint32".to_string(),
value: Vector(Uint32Vector(vec![3, 4, 5])),
},
ConfigField {
key: "my_vector_of_uint64".to_string(),
value: Vector(Uint64Vector(vec![4, 5, 6])),
},
ConfigField {
key: "my_vector_of_int8".to_string(),
value: Vector(Int8Vector(vec![-1, -2, 3])),
},
ConfigField {
key: "my_vector_of_int16".to_string(),
value: Vector(Int16Vector(vec![-2, -3, 4])),
},
ConfigField {
key: "my_vector_of_int32".to_string(),
value: Vector(Int32Vector(vec![-3, -4, 5])),
},
ConfigField {
key: "my_vector_of_int64".to_string(),
value: Vector(Int64Vector(vec![-4, -5, 6])),
},
ConfigField {
key: "my_vector_of_string".to_string(),
value: Vector(StringVector(vec!["valid".into(), "valid".into()])),
},
],
checksum: decl.checksum.clone(),
};
assert_eq!(ConfigFields::resolve(&decl, specs).unwrap(), expected);
}
#[test]
fn checksums_must_match() {
let expected = ConfigChecksum::Sha256([0; 32]);
let received = ConfigChecksum::Sha256([0xFF; 32]);
let decl = config_decl! {
ck@ expected.clone(),
foo: { bool },
};
let specs = values_data! [
ck@ received.clone(),
Value::Single(SingleValue::Bool(true)),
];
assert_eq!(
ConfigFields::resolve(&decl, specs).unwrap_err(),
ResolutionError::ChecksumFailure { expected, received }
);
}
#[test]
fn too_many_values_fails() {
let decl = config_decl! {
ck@ ConfigChecksum::Sha256([0; 32]),
foo: { bool },
};
let specs = values_data! [
ck@ decl.checksum.clone(),
Value::Single(SingleValue::Bool(true)),
Value::Single(SingleValue::Bool(false)),
];
assert_eq!(
ConfigFields::resolve(&decl, specs).unwrap_err(),
ResolutionError::WrongNumberOfValues { expected: 1, received: 2 }
);
}
#[test]
fn not_enough_values_fails() {
let decl = config_decl! {
ck@ ConfigChecksum::Sha256([0; 32]),
foo: { bool },
};
let specs = values_data! {
ck@ decl.checksum.clone(),
};
assert_eq!(
ConfigFields::resolve(&decl, specs).unwrap_err(),
ResolutionError::WrongNumberOfValues { expected: 1, received: 0 }
);
}
#[test]
fn string_length_is_validated() {
let decl = config_decl! {
ck@ ConfigChecksum::Sha256([0; 32]),
foo: { string, max_size: 10 },
};
let specs = values_data! [
ck@ decl.checksum.clone(),
Value::Single(SingleValue::String("hello, world!".into())),
];
assert_eq!(
ConfigFields::resolve(&decl, specs).unwrap_err(),
ResolutionError::InvalidValue {
key: "foo".to_string(),
source: ValueError::StringTooLong { max: 10, actual: 13 },
}
);
}
#[test]
fn vector_length_is_validated() {
let decl = config_decl! {
ck@ ConfigChecksum::Sha256([0; 32]),
foo: { vector, element: uint8, max_count: 2 },
};
let specs = values_data! [
ck@ decl.checksum.clone(),
Value::Vector(VectorValue::Uint8Vector(vec![1, 2, 3])),
];
assert_eq!(
ConfigFields::resolve(&decl, specs).unwrap_err(),
ResolutionError::InvalidValue {
key: "foo".to_string(),
source: ValueError::VectorTooLong { max: 2, actual: 3 },
}
);
}
#[test]
fn vector_elements_validated() {
let decl = config_decl! {
ck@ ConfigChecksum::Sha256([0; 32]),
foo: { vector, element: { string, max_size: 5 }, max_count: 2 },
};
let specs = values_data! [
ck@ decl.checksum.clone(),
Value::Vector(VectorValue::StringVector(vec![
"valid".into(),
"invalid".into(),
])),
];
assert_eq!(
ConfigFields::resolve(&decl, specs).unwrap_err(),
ResolutionError::InvalidValue {
key: "foo".to_string(),
source: ValueError::VectorElementInvalid {
offset: 1,
source: Box::new(ValueError::StringTooLong { max: 5, actual: 7 })
},
}
);
}
macro_rules! type_mismatch_test {
($test_name:ident: { $($ty_toks:tt)* }, $valid_spec:pat) => {
#[test]
fn $test_name() {
let type_ = fidl_fuchsia_component_config_ext::config_ty!($($ty_toks)*);
let decl = ConfigFieldDecl { key: "test_key".to_string(), type_ };
for value in [
// one value of each type
Single(Bool(true)),
Single(Uint8(1)),
Single(Uint16(1)),
Single(Uint32(1)),
Single(Uint64(1)),
Single(Int8(1)),
Single(Int16(1)),
Single(Int32(1)),
Single(Int64(1)),
Single(String("".to_string())),
Vector(BoolVector(vec![])),
Vector(Uint8Vector(vec![])),
Vector(Uint16Vector(vec![])),
Vector(Uint32Vector(vec![])),
Vector(Uint64Vector(vec![])),
Vector(Int8Vector(vec![])),
Vector(Int16Vector(vec![])),
Vector(Int32Vector(vec![])),
Vector(Int64Vector(vec![])),
Vector(StringVector(vec![])),
] {
let should_succeed = matches!(value, $valid_spec);
let spec = ValueSpec { value };
match ConfigField::resolve(spec, &decl) {
Ok(..) if should_succeed => (),
Err(ValueError::TypeMismatch { .. }) if !should_succeed => (),
other => panic!(
"test case {:?} received unexpected resolved value {:#?}",
decl, other
),
}
}
}
};
}
type_mismatch_test!(bool_type_mismatches: { bool }, Single(Bool(..)));
type_mismatch_test!(uint8_type_mismatches: { uint8 }, Single(Uint8(..)));
type_mismatch_test!(uint16_type_mismatches: { uint16 }, Single(Uint16(..)));
type_mismatch_test!(uint32_type_mismatches: { uint32 }, Single(Uint32(..)));
type_mismatch_test!(uint64_type_mismatches: { uint64 }, Single(Uint64(..)));
type_mismatch_test!(int8_type_mismatches: { int8 }, Single(Int8(..)));
type_mismatch_test!(int16_type_mismatches: { int16 }, Single(Int16(..)));
type_mismatch_test!(int32_type_mismatches: { int32 }, Single(Int32(..)));
type_mismatch_test!(int64_type_mismatches: { int64 }, Single(Int64(..)));
type_mismatch_test!(string_type_mismatches: { string, max_size: 10 }, Single(String(..)));
type_mismatch_test!(
bool_vector_type_mismatches: { vector, element: bool, max_count: 1 }, Vector(BoolVector(..))
);
type_mismatch_test!(
uint8_vector_type_mismatches:
{ vector, element: uint8, max_count: 1 },
Vector(Uint8Vector(..))
);
type_mismatch_test!(
uint16_vector_type_mismatches:
{ vector, element: uint16, max_count: 1 },
Vector(Uint16Vector(..))
);
type_mismatch_test!(
uint32_vector_type_mismatches:
{ vector, element: uint32, max_count: 1 },
Vector(Uint32Vector(..))
);
type_mismatch_test!(
uint64_vector_type_mismatches:
{ vector, element: uint64, max_count: 1 },
Vector(Uint64Vector(..))
);
type_mismatch_test!(
int8_vector_type_mismatches:
{ vector, element: int8, max_count: 1 },
Vector(Int8Vector(..))
);
type_mismatch_test!(
int16_vector_type_mismatches:
{ vector, element: int16, max_count: 1 },
Vector(Int16Vector(..))
);
type_mismatch_test!(
int32_vector_type_mismatches:
{ vector, element: int32, max_count: 1 },
Vector(Int32Vector(..))
);
type_mismatch_test!(
int64_vector_type_mismatches:
{ vector, element: int64, max_count: 1 },
Vector(Int64Vector(..))
);
type_mismatch_test!(
string_vector_type_mismatches:
{ vector, element: { string, max_size: 10 }, max_count: 1 },
Vector(StringVector(..))
);
}