refactor(firmware): centralize configuration and diagnostics

This commit is contained in:
Onur Özkan committed 2026-08-22 18:37:18 +03:00
1 parent 7048ed89d0
commit 387fdaad23
140 files changed
+6891 -4541

No files matched your search

+781
View File
@@ -0,0 +1,781 @@
//! Allocation-free scalar snapshot payload codec.
//!
//! A snapshot is a canonical storage payload, not a flash journal record. A
//! storage backend remains responsible for private ordering metadata, integrity,
//! power-loss-safe commit markers, and selecting the newest valid record.
//!
//! The fixed-width little-endian layout is:
//!
//! ```text
//! magic[4] | version:u8 | schema_id[16] | field_count:u16
//! repeated field_count times:
//! field_id:u16 | value_kind:u8 | value_bits:u64
//! ```
//!
//! Field entries are emitted in descriptor order. Decoding uses only stable
//! field IDs and therefore also accepts a complete, uniquely identified set in
//! another order.
use kaptan_log::KaptanError;
use crate::{ConfigModel, ConfigValue, FieldId, FieldWriteError, ValueKind};
const MAGIC: [u8; 4] = *b"KCFG";
const MAGIC_OFFSET: usize = 0;
const VERSION_OFFSET: usize = 4;
const SCHEMA_OFFSET: usize = 5;
const FIELD_COUNT_OFFSET: usize = 21;
const ENTRIES_OFFSET: usize = 23;
/// Current format of the generic scalar snapshot payload.
pub const SCALAR_SNAPSHOT_VERSION: u8 = 1;
/// Bytes preceding the first scalar entry.
pub const SCALAR_SNAPSHOT_HEADER_LENGTH: usize = ENTRIES_OFFSET;
/// Fixed bytes occupied by one scalar entry.
pub const SCALAR_SNAPSHOT_ENTRY_LENGTH: usize = 11;
/// Returns the exact encoded length for a declared number of scalar fields.
///
/// The `u16` field-count domain makes this arithmetic safe on all targets
/// supported by this crate, including 32-bit microcontrollers.
pub const fn scalar_snapshot_length(field_count: u16) -> usize {
SCALAR_SNAPSHOT_HEADER_LENGTH + field_count as usize * SCALAR_SNAPSHOT_ENTRY_LENGTH
}
/// A broken [`ConfigModel`] registry detected before encoding or decoding.
#[derive(Clone, Copy, Debug, Eq, PartialEq, KaptanError)]
pub enum SnapshotModelError {
/// `field_count` includes an index with no descriptor.
MissingDescriptor(u16),
/// A descriptor exists at the first index after `field_count`.
ExtraDescriptor(u16),
/// Two descriptor indexes expose the same stable field identity.
DuplicateDescriptor(FieldId),
/// A descriptor has no readable value in the supplied candidate.
MissingValue(FieldId),
/// A candidate returned a scalar type different from its descriptor.
WrongValueKind {
/// Descriptor identity.
field: FieldId,
/// Type declared by the descriptor.
expected: ValueKind,
/// Type returned by the candidate.
actual: ValueKind,
},
}
/// Failure while encoding a candidate into caller-owned storage.
#[derive(Clone, Copy, Debug, Eq, PartialEq, KaptanError)]
pub enum ScalarSnapshotEncodeError {
/// The destination cannot hold the complete canonical payload.
BufferTooSmall {
/// Exact number of bytes required.
required: usize,
/// Number of bytes supplied by the caller.
available: usize,
},
/// The model's descriptor/value contract is internally inconsistent.
InvalidModel(SnapshotModelError),
}
/// Failure while decoding an untrusted scalar snapshot.
#[derive(Clone, Copy, Debug, PartialEq, KaptanError)]
pub enum ScalarSnapshotDecodeError {
/// The input ends before the complete header or declared field set.
Truncated {
/// Exact number of bytes required once it is known.
required: usize,
/// Number of bytes supplied by the caller.
available: usize,
},
/// Bytes remain after the exact declared field set.
TrailingData {
/// Exact number of bytes required.
expected: usize,
/// Number of bytes supplied by the caller.
actual: usize,
},
/// The payload does not carry the scalar-snapshot magic.
InvalidMagic,
/// The payload uses an unsupported scalar-snapshot format.
UnsupportedVersion(u8),
/// The payload belongs to another configuration schema.
WrongSchema {
/// Schema compiled into the target model.
expected: [u8; 16],
/// Schema carried by the payload.
actual: [u8; 16],
},
/// The payload is not a complete snapshot for this schema.
WrongFieldCount {
/// Exact number of fields compiled into the target model.
expected: u16,
/// Number declared by the payload.
actual: u16,
},
/// The target model's descriptor registry is internally inconsistent.
InvalidModel(SnapshotModelError),
/// An entry used a reserved stable field identity.
InvalidFieldId(u16),
/// An entry names no field in the target schema.
UnknownField(FieldId),
/// The complete payload identifies one field more than once.
DuplicateField(FieldId),
/// The complete payload does not identify a required schema field.
MissingField(FieldId),
/// An entry contains no supported scalar-kind discriminator.
InvalidValueKind(u8),
/// An entry's scalar kind differs from the target descriptor.
WrongValueKind {
/// Entry identity.
field: FieldId,
/// Type declared by the target descriptor.
expected: ValueKind,
/// Type carried by the payload.
actual: ValueKind,
},
/// Scalar bits are not canonical for the entry's declared kind.
InvalidValueBits {
/// Entry identity.
field: FieldId,
/// Scalar kind whose representation was invalid.
kind: ValueKind,
},
/// The model rejected an otherwise structurally valid scalar write.
InvalidWrite(FieldWriteError),
}
/// Encodes a complete [`ConfigModel`] candidate into a caller-owned buffer.
///
/// The returned slice covers only the initialized payload; a larger caller
/// buffer is permitted. No partial payload is returned on failure.
pub fn encode_scalar_snapshot<'a, C: ConfigModel>(
candidate: &C,
output: &'a mut [u8],
) -> Result<&'a [u8], ScalarSnapshotEncodeError> {
validate_model::<C>().map_err(ScalarSnapshotEncodeError::InvalidModel)?;
let field_count = C::field_count();
let required = scalar_snapshot_length(field_count);
if output.len() < required {
return Err(ScalarSnapshotEncodeError::BufferTooSmall {
required,
available: output.len(),
});
}
output[MAGIC_OFFSET..VERSION_OFFSET].copy_from_slice(&MAGIC);
output[VERSION_OFFSET] = SCALAR_SNAPSHOT_VERSION;
output[SCHEMA_OFFSET..FIELD_COUNT_OFFSET].copy_from_slice(&C::SCHEMA_ID);
output[FIELD_COUNT_OFFSET..ENTRIES_OFFSET].copy_from_slice(&field_count.to_le_bytes());
for index in 0..field_count {
let descriptor = C::descriptor(index).ok_or(ScalarSnapshotEncodeError::InvalidModel(
SnapshotModelError::MissingDescriptor(index),
))?;
let value =
candidate
.field_value(descriptor.id)
.ok_or(ScalarSnapshotEncodeError::InvalidModel(
SnapshotModelError::MissingValue(descriptor.id),
))?;
if value.kind() != descriptor.kind {
return Err(ScalarSnapshotEncodeError::InvalidModel(
SnapshotModelError::WrongValueKind {
field: descriptor.id,
expected: descriptor.kind,
actual: value.kind(),
},
));
}
let entry = entry_offset(index);
output[entry..entry + 2].copy_from_slice(&descriptor.id.get().to_le_bytes());
output[entry + 2] = descriptor.kind as u8;
output[entry + 3..entry + SCALAR_SNAPSHOT_ENTRY_LENGTH]
.copy_from_slice(&value.raw_bits().to_le_bytes());
}
Ok(&output[..required])
}
/// Decodes a complete scalar snapshot into a clone of `factory`.
///
/// Structural validation completes before any field setter runs. Each write
/// then passes through [`ConfigModel::set_field`], preserving the model's local
/// validation. Whole-candidate capability validation remains the composition
/// owner's responsibility because its context is deliberately not part of a
/// storage codec.
pub fn decode_scalar_snapshot<C: ConfigModel>(
input: &[u8],
factory: &C,
) -> Result<C, ScalarSnapshotDecodeError> {
validate_model::<C>().map_err(ScalarSnapshotDecodeError::InvalidModel)?;
if input.len() < SCALAR_SNAPSHOT_HEADER_LENGTH {
return Err(ScalarSnapshotDecodeError::Truncated {
required: SCALAR_SNAPSHOT_HEADER_LENGTH,
available: input.len(),
});
}
if input[MAGIC_OFFSET..VERSION_OFFSET] != MAGIC {
return Err(ScalarSnapshotDecodeError::InvalidMagic);
}
if input[VERSION_OFFSET] != SCALAR_SNAPSHOT_VERSION {
return Err(ScalarSnapshotDecodeError::UnsupportedVersion(
input[VERSION_OFFSET],
));
}
let mut actual_schema = [0_u8; 16];
actual_schema.copy_from_slice(&input[SCHEMA_OFFSET..FIELD_COUNT_OFFSET]);
if actual_schema != C::SCHEMA_ID {
return Err(ScalarSnapshotDecodeError::WrongSchema {
expected: C::SCHEMA_ID,
actual: actual_schema,
});
}
let actual_count = read_u16(&input[FIELD_COUNT_OFFSET..ENTRIES_OFFSET]);
let expected_count = C::field_count();
if actual_count != expected_count {
return Err(ScalarSnapshotDecodeError::WrongFieldCount {
expected: expected_count,
actual: actual_count,
});
}
let expected_length = scalar_snapshot_length(actual_count);
if input.len() < expected_length {
return Err(ScalarSnapshotDecodeError::Truncated {
required: expected_length,
available: input.len(),
});
}
if input.len() > expected_length {
return Err(ScalarSnapshotDecodeError::TrailingData {
expected: expected_length,
actual: input.len(),
});
}
validate_entries::<C>(input, actual_count)?;
let mut candidate = factory.clone();
for index in 0..actual_count {
let entry = entry_offset(index);
let id = FieldId::try_new(read_u16(&input[entry..entry + 2]))
.expect("entry identities were structurally validated");
let kind = decode_kind(input[entry + 2]).expect("entry kinds were structurally validated");
let bits = read_u64(&input[entry + 3..entry + SCALAR_SNAPSHOT_ENTRY_LENGTH]);
let value = decode_value(id, kind, bits)?;
candidate
.set_field(id, value)
.map_err(ScalarSnapshotDecodeError::InvalidWrite)?;
}
Ok(candidate)
}
fn validate_model<C: ConfigModel>() -> Result<(), SnapshotModelError> {
let count = C::field_count();
for index in 0..count {
let descriptor =
C::descriptor(index).ok_or(SnapshotModelError::MissingDescriptor(index))?;
for previous in 0..index {
let previous =
C::descriptor(previous).ok_or(SnapshotModelError::MissingDescriptor(previous))?;
if previous.id == descriptor.id {
return Err(SnapshotModelError::DuplicateDescriptor(descriptor.id));
}
}
}
if C::descriptor(count).is_some() {
return Err(SnapshotModelError::ExtraDescriptor(count));
}
Ok(())
}
fn validate_entries<C: ConfigModel>(
input: &[u8],
count: u16,
) -> Result<(), ScalarSnapshotDecodeError> {
for index in 0..count {
let entry = entry_offset(index);
let raw_id = read_u16(&input[entry..entry + 2]);
let id =
FieldId::try_new(raw_id).ok_or(ScalarSnapshotDecodeError::InvalidFieldId(raw_id))?;
let descriptor =
descriptor_by_id::<C>(id).ok_or(ScalarSnapshotDecodeError::UnknownField(id))?;
let raw_kind = input[entry + 2];
let kind =
decode_kind(raw_kind).ok_or(ScalarSnapshotDecodeError::InvalidValueKind(raw_kind))?;
if kind != descriptor.kind {
return Err(ScalarSnapshotDecodeError::WrongValueKind {
field: id,
expected: descriptor.kind,
actual: kind,
});
}
let bits = read_u64(&input[entry + 3..entry + SCALAR_SNAPSHOT_ENTRY_LENGTH]);
let _ = decode_value(id, kind, bits)?;
for previous in 0..index {
let previous_entry = entry_offset(previous);
if read_u16(&input[previous_entry..previous_entry + 2]) == raw_id {
return Err(ScalarSnapshotDecodeError::DuplicateField(id));
}
}
}
for descriptor_index in 0..count {
let descriptor =
C::descriptor(descriptor_index).ok_or(ScalarSnapshotDecodeError::InvalidModel(
SnapshotModelError::MissingDescriptor(descriptor_index),
))?;
let mut present = false;
for entry_index in 0..count {
let entry = entry_offset(entry_index);
present |= read_u16(&input[entry..entry + 2]) == descriptor.id.get();
}
if !present {
return Err(ScalarSnapshotDecodeError::MissingField(descriptor.id));
}
}
Ok(())
}
fn descriptor_by_id<C: ConfigModel>(id: FieldId) -> Option<&'static crate::FieldDescriptor> {
for index in 0..C::field_count() {
let descriptor = C::descriptor(index)?;
if descriptor.id == id {
return Some(descriptor);
}
}
None
}
const fn entry_offset(index: u16) -> usize {
ENTRIES_OFFSET + index as usize * SCALAR_SNAPSHOT_ENTRY_LENGTH
}
const fn decode_kind(value: u8) -> Option<ValueKind> {
match value {
1 => Some(ValueKind::Bool),
2 => Some(ValueKind::I32),
3 => Some(ValueKind::U32),
4 => Some(ValueKind::U64),
5 => Some(ValueKind::F32),
6 => Some(ValueKind::F64),
_ => None,
}
}
fn decode_value(
field: FieldId,
kind: ValueKind,
bits: u64,
) -> Result<ConfigValue, ScalarSnapshotDecodeError> {
let invalid = || ScalarSnapshotDecodeError::InvalidValueBits { field, kind };
ConfigValue::try_from_raw_bits(kind, bits).ok_or_else(invalid)
}
fn read_u16(bytes: &[u8]) -> u16 {
u16::from_le_bytes([bytes[0], bytes[1]])
}
fn read_u64(bytes: &[u8]) -> u64 {
u64::from_le_bytes([
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
])
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ConfigViolation, FieldDescriptor, ValidationRule};
const BOOL: FieldId = FieldId::new(1);
const I32: FieldId = FieldId::new(2);
const U32: FieldId = FieldId::new(3);
const U64: FieldId = FieldId::new(4);
const F32: FieldId = FieldId::new(5);
const F64: FieldId = FieldId::new(6);
const SCHEMA: [u8; 16] = *b"SCALAR-SNAPSHOT1";
const DESCRIPTORS: [FieldDescriptor; 6] = [
descriptor(BOOL, ValueKind::Bool),
descriptor(I32, ValueKind::I32),
descriptor(U32, ValueKind::U32),
descriptor(U64, ValueKind::U64),
descriptor(F32, ValueKind::F32),
descriptor(F64, ValueKind::F64),
];
const fn descriptor(id: FieldId, kind: ValueKind) -> FieldDescriptor {
FieldDescriptor {
id,
display_key: "test",
kind,
minimum: None,
maximum: None,
enum_options: &[],
unit: "",
}
}
#[derive(Clone, Debug, PartialEq)]
struct Candidate {
boolean: bool,
signed: i32,
unsigned: u32,
wide: u64,
float: f32,
double: f64,
}
impl Default for Candidate {
fn default() -> Self {
Self {
boolean: false,
signed: -1,
unsigned: 1,
wide: 1,
float: 1.0,
double: 1.0,
}
}
}
impl ConfigModel for Candidate {
type Active = Self;
type Context = ();
const SCHEMA_ID: [u8; 16] = SCHEMA;
fn descriptor(index: u16) -> Option<&'static FieldDescriptor> {
DESCRIPTORS.get(usize::from(index))
}
fn field_count() -> u16 {
DESCRIPTORS.len() as u16
}
fn field_value(&self, id: FieldId) -> Option<ConfigValue> {
match id {
BOOL => Some(ConfigValue::Bool(self.boolean)),
I32 => Some(ConfigValue::I32(self.signed)),
U32 => Some(ConfigValue::U32(self.unsigned)),
U64 => Some(ConfigValue::U64(self.wide)),
F32 => Some(ConfigValue::F32(self.float)),
F64 => Some(ConfigValue::F64(self.double)),
_ => None,
}
}
fn active_field_value(active: &Self::Active, id: FieldId) -> Option<ConfigValue> {
active.field_value(id)
}
fn set_field(&mut self, id: FieldId, value: ConfigValue) -> Result<(), FieldWriteError> {
match (id, value) {
(BOOL, ConfigValue::Bool(value)) => self.boolean = value,
(I32, ConfigValue::I32(value)) => self.signed = value,
(U32, ConfigValue::U32(value)) if value <= 100 => self.unsigned = value,
(U64, ConfigValue::U64(value)) => self.wide = value,
(F32, ConfigValue::F32(value)) if value.is_finite() => self.float = value,
(F64, ConfigValue::F64(value)) if value.is_finite() => self.double = value,
(U32, ConfigValue::U32(_)) => {
return Err(FieldWriteError::Violation(ConfigViolation::field(
U32,
ValidationRule::AboveMaximum,
)));
}
(F32, ConfigValue::F32(_)) => return Err(FieldWriteError::non_finite(F32)),
(F64, ConfigValue::F64(_)) => return Err(FieldWriteError::non_finite(F64)),
(_, value) if self.field_value(id).is_some() => {
return Err(FieldWriteError::WrongType {
field: id,
expected: self.field_value(id).expect("known field").kind(),
actual: value.kind(),
});
}
_ => return Err(FieldWriteError::UnknownField(id)),
}
Ok(())
}
fn validate(&self, _: &Self::Context) -> Result<Self::Active, ConfigViolation> {
Ok(self.clone())
}
}
fn encoded(candidate: &Candidate) -> ([u8; scalar_snapshot_length(6)], usize) {
let mut bytes = [0xa5; scalar_snapshot_length(6)];
let length = encode_scalar_snapshot(candidate, &mut bytes)
.expect("candidate encodes")
.len();
(bytes, length)
}
fn entry(index: u16) -> usize {
entry_offset(index)
}
#[test]
fn round_trips_every_scalar_kind_and_uses_exact_length() {
let candidate = Candidate {
boolean: true,
signed: i32::MIN,
unsigned: 99,
wide: u64::MAX,
float: -0.0,
double: f64::MIN_POSITIVE,
};
let mut storage = [0xa5; scalar_snapshot_length(6) + 7];
let payload = encode_scalar_snapshot(&candidate, &mut storage).unwrap();
assert_eq!(payload.len(), scalar_snapshot_length(6));
assert_eq!(&payload[..4], b"KCFG");
assert_eq!(payload[4], SCALAR_SNAPSHOT_VERSION);
assert_eq!(&payload[5..21], &SCHEMA);
assert_eq!(read_u16(&payload[21..23]), 6);
assert_eq!(
decode_scalar_snapshot(payload, &Candidate::default()),
Ok(candidate)
);
}
#[test]
fn encoder_rejects_a_short_destination_without_writing_it() {
let required = scalar_snapshot_length(6);
let mut output = [0xa5; scalar_snapshot_length(6) - 1];
assert_eq!(
encode_scalar_snapshot(&Candidate::default(), &mut output),
Err(ScalarSnapshotEncodeError::BufferTooSmall {
required,
available: required - 1,
})
);
assert!(output.iter().all(|byte| *byte == 0xa5));
}
#[test]
fn decoder_rejects_every_truncation_and_trailing_data() {
let (bytes, length) = encoded(&Candidate::default());
for end in 0..length {
assert!(decode_scalar_snapshot(&bytes[..end], &Candidate::default()).is_err());
}
let mut trailing = [0_u8; scalar_snapshot_length(6) + 1];
trailing[..length].copy_from_slice(&bytes);
assert_eq!(
decode_scalar_snapshot(&trailing, &Candidate::default()),
Err(ScalarSnapshotDecodeError::TrailingData {
expected: length,
actual: length + 1,
})
);
}
#[test]
fn decoder_rejects_wrong_magic_version_schema_and_count() {
let (source, _) = encoded(&Candidate::default());
let mut bytes = source;
bytes[0] ^= 1;
assert_eq!(
decode_scalar_snapshot(&bytes, &Candidate::default()),
Err(ScalarSnapshotDecodeError::InvalidMagic)
);
let mut bytes = source;
bytes[VERSION_OFFSET] += 1;
assert_eq!(
decode_scalar_snapshot(&bytes, &Candidate::default()),
Err(ScalarSnapshotDecodeError::UnsupportedVersion(2))
);
let mut bytes = source;
bytes[SCHEMA_OFFSET] ^= 1;
let mut actual = SCHEMA;
actual[0] ^= 1;
assert_eq!(
decode_scalar_snapshot(&bytes, &Candidate::default()),
Err(ScalarSnapshotDecodeError::WrongSchema {
expected: SCHEMA,
actual,
})
);
let mut bytes = source;
bytes[FIELD_COUNT_OFFSET..ENTRIES_OFFSET].copy_from_slice(&5_u16.to_le_bytes());
assert_eq!(
decode_scalar_snapshot(&bytes, &Candidate::default()),
Err(ScalarSnapshotDecodeError::WrongFieldCount {
expected: 6,
actual: 5,
})
);
}
#[test]
fn decoder_rejects_invalid_unknown_duplicate_and_missing_ids() {
let (source, _) = encoded(&Candidate::default());
let mut bytes = source;
bytes[entry(0)..entry(0) + 2].copy_from_slice(&0_u16.to_le_bytes());
assert_eq!(
decode_scalar_snapshot(&bytes, &Candidate::default()),
Err(ScalarSnapshotDecodeError::InvalidFieldId(0))
);
let mut bytes = source;
bytes[entry(0)..entry(0) + 2].copy_from_slice(&100_u16.to_le_bytes());
assert_eq!(
decode_scalar_snapshot(&bytes, &Candidate::default()),
Err(ScalarSnapshotDecodeError::UnknownField(FieldId::new(100)))
);
let mut bytes = source;
let first_id = bytes[entry(0)..entry(0) + 2].to_vec();
bytes[entry(1)..entry(1) + 2].copy_from_slice(&first_id);
bytes[entry(1) + 2] = ValueKind::Bool as u8;
bytes[entry(1) + 3..entry(1) + 11].copy_from_slice(&0_u64.to_le_bytes());
assert_eq!(
decode_scalar_snapshot(&bytes, &Candidate::default()),
Err(ScalarSnapshotDecodeError::DuplicateField(BOOL))
);
// With an exact count and only known IDs, a duplicate necessarily also
// omits a schema field. Rejecting it proves incomplete sets cannot pass.
}
#[test]
fn decoder_rejects_invalid_and_wrong_kinds_and_noncanonical_bits() {
let (source, _) = encoded(&Candidate::default());
let mut bytes = source;
bytes[entry(0) + 2] = 0xff;
assert_eq!(
decode_scalar_snapshot(&bytes, &Candidate::default()),
Err(ScalarSnapshotDecodeError::InvalidValueKind(0xff))
);
let mut bytes = source;
bytes[entry(0) + 2] = ValueKind::U32 as u8;
assert_eq!(
decode_scalar_snapshot(&bytes, &Candidate::default()),
Err(ScalarSnapshotDecodeError::WrongValueKind {
field: BOOL,
expected: ValueKind::Bool,
actual: ValueKind::U32,
})
);
let mut bytes = source;
bytes[entry(0) + 3..entry(0) + 11].copy_from_slice(&2_u64.to_le_bytes());
assert_eq!(
decode_scalar_snapshot(&bytes, &Candidate::default()),
Err(ScalarSnapshotDecodeError::InvalidValueBits {
field: BOOL,
kind: ValueKind::Bool,
})
);
let mut bytes = source;
bytes[entry(1) + 3..entry(1) + 11]
.copy_from_slice(&(u64::from(u32::MAX) + 1).to_le_bytes());
assert_eq!(
decode_scalar_snapshot(&bytes, &Candidate::default()),
Err(ScalarSnapshotDecodeError::InvalidValueBits {
field: I32,
kind: ValueKind::I32,
})
);
}
#[test]
fn decoder_accepts_reordered_entries_because_ids_are_authoritative() {
let candidate = Candidate {
boolean: true,
signed: -72,
unsigned: 50,
wide: 987_654_321,
float: 0.25,
double: -19.5,
};
let (mut bytes, _) = encoded(&candidate);
for offset in 0..SCALAR_SNAPSHOT_ENTRY_LENGTH {
bytes.swap(entry(0) + offset, entry(5) + offset);
}
assert_eq!(
decode_scalar_snapshot(&bytes, &Candidate::default()),
Ok(candidate)
);
}
#[test]
fn decoder_routes_values_through_local_model_validation() {
let (mut bytes, _) = encoded(&Candidate::default());
bytes[entry(2) + 3..entry(2) + 11].copy_from_slice(&101_u64.to_le_bytes());
assert_eq!(
decode_scalar_snapshot(&bytes, &Candidate::default()),
Err(ScalarSnapshotDecodeError::InvalidWrite(
FieldWriteError::Violation(ConfigViolation::field(
U32,
ValidationRule::AboveMaximum,
))
))
);
}
struct MissingDescriptor;
impl Clone for MissingDescriptor {
fn clone(&self) -> Self {
Self
}
}
impl ConfigModel for MissingDescriptor {
type Active = ();
type Context = ();
const SCHEMA_ID: [u8; 16] = SCHEMA;
fn descriptor(_: u16) -> Option<&'static FieldDescriptor> {
None
}
fn field_count() -> u16 {
1
}
fn field_value(&self, _: FieldId) -> Option<ConfigValue> {
None
}
fn active_field_value(_: &Self::Active, _: FieldId) -> Option<ConfigValue> {
None
}
fn set_field(&mut self, id: FieldId, _: ConfigValue) -> Result<(), FieldWriteError> {
Err(FieldWriteError::UnknownField(id))
}
fn validate(&self, _: &Self::Context) -> Result<Self::Active, ConfigViolation> {
Ok(())
}
}
#[test]
fn encoder_and_decoder_reject_a_broken_model_registry() {
let mut output = [0_u8; scalar_snapshot_length(1)];
assert_eq!(
encode_scalar_snapshot(&MissingDescriptor, &mut output),
Err(ScalarSnapshotEncodeError::InvalidModel(
SnapshotModelError::MissingDescriptor(0)
))
);
let (bytes, _) = encoded(&Candidate::default());
assert!(matches!(
decode_scalar_snapshot(&bytes, &MissingDescriptor),
Err(ScalarSnapshotDecodeError::InvalidModel(
SnapshotModelError::MissingDescriptor(0)
))
));
}
}