refactor: move firmware workspace under firmware
This commit is contained in:
1 parent
153b0bf262
commit
f03fc119a4
222 files changed
+56
-13
No files matched your search
@@ -0,0 +1,378 @@
|
||||
//! Bounded CBOR import/export for one explicit current record.
|
||||
|
||||
use kaptan_config::{
|
||||
ConfigModel, ConfigValue, ConfigViolation, FieldWriteError, ValidationRule, ValueKind,
|
||||
};
|
||||
use kaptan_log::KaptanError;
|
||||
use minicbor::{Decoder, Encoder, encode::Write};
|
||||
|
||||
use crate::{CandidateProfile, FORMAT_VERSION, SCHEMA_ID, VehicleCapabilities};
|
||||
|
||||
/// Maximum complete candidate snapshot accepted by the current vehicle schema.
|
||||
pub const MAXIMUM_CANDIDATE_BYTES: usize = 4096;
|
||||
const FORMAT_KEY: u16 = 0;
|
||||
const SCHEMA_KEY: u16 = u16::MAX;
|
||||
|
||||
/// CBOR import failure.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, KaptanError)]
|
||||
pub enum CborError {
|
||||
/// The supplied payload exceeded the bounded configuration storage budget.
|
||||
TooLarge,
|
||||
/// CBOR was malformed or did not match a supported versioned wire record.
|
||||
Malformed,
|
||||
/// A second CBOR item followed the candidate snapshot.
|
||||
TrailingData,
|
||||
/// The decoded candidate violated complete schema/capability validation.
|
||||
InvalidProfile(ConfigViolation),
|
||||
}
|
||||
|
||||
/// Failure while encoding one complete candidate snapshot.
|
||||
///
|
||||
/// The candidate is validated before the first CBOR item is written, so a
|
||||
/// caller can never export a locally valid but relationally invalid edit.
|
||||
#[derive(Debug)]
|
||||
pub enum CborEncodeError<E> {
|
||||
/// The complete candidate did not validate against immutable board
|
||||
/// capabilities.
|
||||
InvalidProfile(ConfigViolation),
|
||||
/// The caller-owned CBOR writer rejected the encoded snapshot.
|
||||
Encode(minicbor::encode::Error<E>),
|
||||
}
|
||||
|
||||
/// Decodes exactly one bounded current record and validates it against immutable
|
||||
/// board capabilities before returning a candidate to its caller.
|
||||
pub fn decode_candidate(
|
||||
bytes: &[u8],
|
||||
capabilities: &VehicleCapabilities,
|
||||
) -> Result<CandidateProfile, CborError> {
|
||||
if bytes.len() > MAXIMUM_CANDIDATE_BYTES {
|
||||
return Err(CborError::TooLarge);
|
||||
}
|
||||
let candidate = decode_current(bytes)?;
|
||||
candidate
|
||||
.validate(capabilities)
|
||||
.map_err(CborError::InvalidProfile)?;
|
||||
Ok(candidate)
|
||||
}
|
||||
|
||||
/// Validates and encodes one complete current snapshot to a caller-owned CBOR
|
||||
/// writer.
|
||||
///
|
||||
/// Validation happens before creating the encoder, ensuring a failed
|
||||
/// cross-field or capability check emits no partial CBOR record.
|
||||
pub fn encode_candidate<W>(
|
||||
candidate: CandidateProfile,
|
||||
capabilities: &VehicleCapabilities,
|
||||
writer: W,
|
||||
) -> Result<(), CborEncodeError<W::Error>>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
candidate
|
||||
.validate(capabilities)
|
||||
.map_err(CborEncodeError::InvalidProfile)?;
|
||||
let mut encoder = Encoder::new(writer);
|
||||
encoder
|
||||
.map(CandidateProfile::FIELD_COUNT as u64 + 2)
|
||||
.map_err(CborEncodeError::Encode)?;
|
||||
encoder
|
||||
.u16(FORMAT_KEY)
|
||||
.and_then(|encoder| encoder.u16(FORMAT_VERSION))
|
||||
.map_err(CborEncodeError::Encode)?;
|
||||
encoder
|
||||
.u16(SCHEMA_KEY)
|
||||
.and_then(|encoder| encoder.bytes(&SCHEMA_ID))
|
||||
.map_err(CborEncodeError::Encode)?;
|
||||
for descriptor in CandidateProfile::descriptors() {
|
||||
encoder
|
||||
.u16(descriptor.id.get())
|
||||
.map_err(CborEncodeError::Encode)?;
|
||||
encode_value(
|
||||
&mut encoder,
|
||||
candidate
|
||||
.field_value(descriptor.id)
|
||||
.expect("descriptor field"),
|
||||
)
|
||||
.map_err(CborEncodeError::Encode)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn decode_current(bytes: &[u8]) -> Result<CandidateProfile, CborError> {
|
||||
let mut decoder = Decoder::new(bytes);
|
||||
let entry_count = CandidateProfile::FIELD_COUNT as u64 + 2;
|
||||
if decoder.map().map_err(|_| CborError::Malformed)? != Some(entry_count) {
|
||||
return Err(CborError::Malformed);
|
||||
}
|
||||
let mut format_version = None;
|
||||
let mut schema_id = None;
|
||||
let mut values = [None; CandidateProfile::FIELD_COUNT];
|
||||
for _ in 0..entry_count {
|
||||
let key = decoder.u16().map_err(|_| CborError::Malformed)?;
|
||||
match key {
|
||||
FORMAT_KEY => {
|
||||
if format_version
|
||||
.replace(decoder.u16().map_err(|_| CborError::Malformed)?)
|
||||
.is_some()
|
||||
{
|
||||
return Err(CborError::Malformed);
|
||||
}
|
||||
}
|
||||
SCHEMA_KEY => {
|
||||
let bytes = decoder.bytes().map_err(|_| CborError::Malformed)?;
|
||||
let value: [u8; 16] = bytes.try_into().map_err(|_| CborError::Malformed)?;
|
||||
if schema_id.replace(value).is_some() {
|
||||
return Err(CborError::Malformed);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let index = CandidateProfile::descriptors()
|
||||
.iter()
|
||||
.position(|descriptor| descriptor.id.get() == key)
|
||||
.ok_or(CborError::Malformed)?;
|
||||
if values[index].is_some() {
|
||||
return Err(CborError::Malformed);
|
||||
}
|
||||
values[index] = Some(decode_value(
|
||||
&mut decoder,
|
||||
CandidateProfile::descriptors()[index].kind,
|
||||
)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
if decoder.position() != bytes.len() {
|
||||
return Err(CborError::TrailingData);
|
||||
}
|
||||
if format_version != Some(FORMAT_VERSION) || schema_id != Some(SCHEMA_ID) {
|
||||
return Err(CborError::InvalidProfile(ConfigViolation::global(
|
||||
ValidationRule::UnsupportedFormat,
|
||||
)));
|
||||
}
|
||||
if values.iter().any(Option::is_none) {
|
||||
return Err(CborError::Malformed);
|
||||
}
|
||||
let mut candidate = CandidateProfile::default();
|
||||
for (index, value) in values.into_iter().enumerate() {
|
||||
let descriptor = CandidateProfile::descriptors()
|
||||
.get(index)
|
||||
.ok_or(CborError::Malformed)?;
|
||||
candidate
|
||||
.set_field(descriptor.id, value.ok_or(CborError::Malformed)?)
|
||||
.map_err(field_write_error)?;
|
||||
}
|
||||
Ok(candidate)
|
||||
}
|
||||
|
||||
fn encode_value<W>(
|
||||
encoder: &mut Encoder<W>,
|
||||
value: ConfigValue,
|
||||
) -> Result<(), minicbor::encode::Error<W::Error>>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
match value {
|
||||
ConfigValue::Bool(value) => encoder.bool(value)?,
|
||||
ConfigValue::I32(value) => encoder.i32(value)?,
|
||||
ConfigValue::U32(value) => encoder.u32(value)?,
|
||||
ConfigValue::U64(value) => encoder.u64(value)?,
|
||||
ConfigValue::F32(value) => encoder.f32(value)?,
|
||||
ConfigValue::F64(value) => encoder.f64(value)?,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn decode_value(decoder: &mut Decoder<'_>, kind: ValueKind) -> Result<ConfigValue, CborError> {
|
||||
match kind {
|
||||
ValueKind::Bool => decoder.bool().map(ConfigValue::Bool),
|
||||
ValueKind::I32 => decoder.i32().map(ConfigValue::I32),
|
||||
ValueKind::U32 => decoder.u32().map(ConfigValue::U32),
|
||||
ValueKind::U64 => decoder.u64().map(ConfigValue::U64),
|
||||
ValueKind::F32 => decoder.f32().map(ConfigValue::F32),
|
||||
ValueKind::F64 => decoder.f64().map(ConfigValue::F64),
|
||||
}
|
||||
.map_err(|_| CborError::Malformed)
|
||||
}
|
||||
|
||||
fn field_write_error(error: FieldWriteError) -> CborError {
|
||||
match error {
|
||||
FieldWriteError::Violation(issue) => CborError::InvalidProfile(issue),
|
||||
FieldWriteError::UnknownField(_) | FieldWriteError::WrongType { .. } => {
|
||||
CborError::Malformed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn round_trip_and_trailing_rejection_are_exact() {
|
||||
let mut writer = minicbor::encode::write::Cursor::new([0_u8; MAXIMUM_CANDIDATE_BYTES]);
|
||||
let capabilities = VehicleCapabilities::quad_x();
|
||||
encode_candidate(CandidateProfile::default(), &capabilities, &mut writer).unwrap();
|
||||
let length = writer.position();
|
||||
let bytes = writer.into_inner();
|
||||
assert!(decode_candidate(&bytes[..length], &capabilities).is_ok());
|
||||
let mut trailing = bytes;
|
||||
trailing[length] = 0xf6;
|
||||
assert!(matches!(
|
||||
decode_candidate(&trailing[..=length], &capabilities),
|
||||
Err(CborError::TrailingData)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encoding_rejects_a_cross_field_invalid_transaction_before_writing() {
|
||||
let capabilities = VehicleCapabilities::quad_x();
|
||||
let mut candidate = CandidateProfile::default();
|
||||
candidate
|
||||
.set_field(kaptan_config::FieldId::new(2), ConfigValue::U64(50_001))
|
||||
.expect("the scalar is locally valid");
|
||||
let mut writer = minicbor::encode::write::Cursor::new([0_u8; MAXIMUM_CANDIDATE_BYTES]);
|
||||
|
||||
assert!(matches!(
|
||||
encode_candidate(candidate, &capabilities, &mut writer),
|
||||
Err(CborEncodeError::InvalidProfile(violation))
|
||||
if violation.rule() == ValidationRule::Inconsistent
|
||||
));
|
||||
assert_eq!(writer.position(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_wrong_format_and_schema_before_candidate_conversion() {
|
||||
let capabilities = VehicleCapabilities::quad_x();
|
||||
let (bytes, length) = encoded_test_record(FORMAT_VERSION + 1, SCHEMA_ID, |key| key);
|
||||
assert!(matches!(
|
||||
decode_candidate(&bytes[..length], &capabilities),
|
||||
Err(CborError::InvalidProfile(issue))
|
||||
if issue.rule() == ValidationRule::UnsupportedFormat
|
||||
));
|
||||
|
||||
let (bytes, length) = encoded_test_record(FORMAT_VERSION, [0; 16], |key| key);
|
||||
assert!(matches!(
|
||||
decode_candidate(&bytes[..length], &capabilities),
|
||||
Err(CborError::InvalidProfile(issue))
|
||||
if issue.rule() == kaptan_config::ValidationRule::UnsupportedFormat
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_map_rejects_unknown_duplicate_and_missing_fields() {
|
||||
let capabilities = VehicleCapabilities::quad_x();
|
||||
let (bytes, length) = encoded_test_record(FORMAT_VERSION, SCHEMA_ID, |key| key);
|
||||
assert!(decode_candidate(&bytes[..length], &capabilities).is_ok());
|
||||
|
||||
let (unknown, length) =
|
||||
encoded_test_record(
|
||||
FORMAT_VERSION,
|
||||
SCHEMA_ID,
|
||||
|key| {
|
||||
if key == 1 { 25 } else { key }
|
||||
},
|
||||
);
|
||||
assert!(matches!(
|
||||
decode_candidate(&unknown[..length], &capabilities),
|
||||
Err(CborError::Malformed)
|
||||
));
|
||||
|
||||
let (duplicate, length) = encoded_test_record(FORMAT_VERSION, SCHEMA_ID, |key| {
|
||||
if key == 1 { FORMAT_KEY } else { key }
|
||||
});
|
||||
assert!(matches!(
|
||||
decode_candidate(&duplicate[..length], &capabilities),
|
||||
Err(CborError::Malformed)
|
||||
));
|
||||
|
||||
let mut writer = minicbor::encode::write::Cursor::new([0_u8; MAXIMUM_CANDIDATE_BYTES]);
|
||||
let mut encoder = Encoder::new(&mut writer);
|
||||
encoder
|
||||
.map(CandidateProfile::FIELD_COUNT as u64 + 1)
|
||||
.unwrap();
|
||||
encoder
|
||||
.u16(FORMAT_KEY)
|
||||
.unwrap()
|
||||
.u16(FORMAT_VERSION)
|
||||
.unwrap();
|
||||
encoder.u16(SCHEMA_KEY).unwrap().bytes(&SCHEMA_ID).unwrap();
|
||||
let candidate = CandidateProfile::default();
|
||||
for descriptor in CandidateProfile::descriptors().iter().skip(1) {
|
||||
encoder.u16(descriptor.id.get()).unwrap();
|
||||
encode_value(&mut encoder, candidate.field_value(descriptor.id).unwrap()).unwrap();
|
||||
}
|
||||
let length = writer.position();
|
||||
let missing = writer.into_inner();
|
||||
assert!(matches!(
|
||||
decode_candidate(&missing[..length], &capabilities),
|
||||
Err(CborError::Malformed)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cbor_scalar_write_preserves_the_generated_field_and_rule() {
|
||||
let capabilities = VehicleCapabilities::quad_x();
|
||||
let mass = CandidateProfile::descriptors()
|
||||
.iter()
|
||||
.find(|descriptor| descriptor.display_key == "thrust.mass_kg")
|
||||
.expect("declared mass descriptor");
|
||||
let mut writer = minicbor::encode::write::Cursor::new([0_u8; MAXIMUM_CANDIDATE_BYTES]);
|
||||
let mut encoder = Encoder::new(&mut writer);
|
||||
encoder
|
||||
.map(CandidateProfile::FIELD_COUNT as u64 + 2)
|
||||
.unwrap();
|
||||
encoder
|
||||
.u16(FORMAT_KEY)
|
||||
.unwrap()
|
||||
.u16(FORMAT_VERSION)
|
||||
.unwrap();
|
||||
encoder.u16(SCHEMA_KEY).unwrap().bytes(&SCHEMA_ID).unwrap();
|
||||
let candidate = CandidateProfile::default();
|
||||
for descriptor in CandidateProfile::descriptors() {
|
||||
encoder.u16(descriptor.id.get()).unwrap();
|
||||
let value = if descriptor.id == mass.id {
|
||||
ConfigValue::F32(0.009)
|
||||
} else {
|
||||
candidate.field_value(descriptor.id).unwrap()
|
||||
};
|
||||
encode_value(&mut encoder, value).unwrap();
|
||||
}
|
||||
let length = writer.position();
|
||||
let bytes = writer.into_inner();
|
||||
|
||||
assert!(matches!(
|
||||
decode_candidate(&bytes[..length], &capabilities),
|
||||
Err(CborError::InvalidProfile(issue))
|
||||
if issue.primary() == Some(mass.id)
|
||||
&& issue.related_field().is_none()
|
||||
&& issue.rule() == ValidationRule::BelowMinimum
|
||||
));
|
||||
}
|
||||
|
||||
fn encoded_test_record<F>(
|
||||
format_version: u16,
|
||||
schema_id: [u8; 16],
|
||||
mut remap_key: F,
|
||||
) -> ([u8; MAXIMUM_CANDIDATE_BYTES], usize)
|
||||
where
|
||||
F: FnMut(u16) -> u16,
|
||||
{
|
||||
let mut writer = minicbor::encode::write::Cursor::new([0_u8; MAXIMUM_CANDIDATE_BYTES]);
|
||||
let mut encoder = Encoder::new(&mut writer);
|
||||
encoder
|
||||
.map(CandidateProfile::FIELD_COUNT as u64 + 2)
|
||||
.unwrap();
|
||||
encoder
|
||||
.u16(FORMAT_KEY)
|
||||
.unwrap()
|
||||
.u16(format_version)
|
||||
.unwrap();
|
||||
encoder.u16(SCHEMA_KEY).unwrap().bytes(&schema_id).unwrap();
|
||||
let candidate = CandidateProfile::default();
|
||||
for descriptor in CandidateProfile::descriptors() {
|
||||
encoder.u16(remap_key(descriptor.id.get())).unwrap();
|
||||
encode_value(&mut encoder, candidate.field_value(descriptor.id).unwrap()).unwrap();
|
||||
}
|
||||
let length = writer.position();
|
||||
(writer.into_inner(), length)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user