refactor(config): generate scalar validation boundaries

This commit is contained in:
Onur Özkan committed 2026-08-09 22:45:38 +03:00
1 parent afafd126d7
commit 8134235f48
29 files changed
+1008 -480

No files matched your search

+98 -9
View File
@@ -26,6 +26,19 @@ pub enum CborError {
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(
@@ -37,31 +50,50 @@ pub fn decode_candidate(
}
let candidate = decode_current(bytes)?;
candidate
.validate_against(capabilities)
.validate(capabilities)
.map_err(CborError::InvalidProfile)?;
Ok(candidate)
}
/// Encodes one complete current snapshot to a caller-owned CBOR writer.
/// 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<(), minicbor::encode::Error<W::Error>>
) -> 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)?;
encoder.u16(FORMAT_KEY)?.u16(FORMAT_VERSION)?;
encoder.u16(SCHEMA_KEY)?.bytes(&SCHEMA_ID)?;
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())?;
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(())
}
@@ -178,7 +210,7 @@ mod tests {
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(), &mut writer).unwrap();
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());
@@ -190,6 +222,23 @@ mod tests {
));
}
#[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();
@@ -259,6 +308,46 @@ mod tests {
));
}
#[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],