//! 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_err_reportable::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. #[kaptan_error(id = 1501)] MissingDescriptor(u16), /// A descriptor exists at the first index after `field_count`. #[kaptan_error(id = 1502)] ExtraDescriptor(u16), /// Two descriptor indexes expose the same stable field identity. #[kaptan_error(id = 1503)] DuplicateDescriptor(FieldId), /// A descriptor has no readable value in the supplied candidate. #[kaptan_error(id = 1504)] MissingValue(FieldId), /// A candidate returned a scalar type different from its descriptor. #[kaptan_error(id = 1505)] 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. #[kaptan_error(id = 1601)] 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. #[kaptan_error(id = 1602)] 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. #[kaptan_error(id = 1701)] 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. #[kaptan_error(id = 1702)] 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. #[kaptan_error(id = 1703)] InvalidMagic, /// The payload uses an unsupported scalar-snapshot format. #[kaptan_error(id = 1704)] UnsupportedVersion(u8), /// The payload belongs to another configuration schema. #[kaptan_error(id = 1705)] 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. #[kaptan_error(id = 1706)] 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. #[kaptan_error(id = 1707)] InvalidModel(SnapshotModelError), /// An entry used a reserved stable field identity. #[kaptan_error(id = 1708)] InvalidFieldId(u16), /// An entry names no field in the target schema. #[kaptan_error(id = 1709)] UnknownField(FieldId), /// The complete payload identifies one field more than once. #[kaptan_error(id = 1710)] DuplicateField(FieldId), /// The complete payload does not identify a required schema field. #[kaptan_error(id = 1711)] MissingField(FieldId), /// An entry contains no supported scalar-kind discriminator. #[kaptan_error(id = 1712)] InvalidValueKind(u8), /// An entry's scalar kind differs from the target descriptor. #[kaptan_error(id = 1713)] 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. #[kaptan_error(id = 1714)] InvalidValueBits { /// Entry identity. field: FieldId, /// Scalar kind whose representation was invalid. kind: ValueKind, }, /// The model rejected an otherwise structurally valid scalar write. #[kaptan_error(id = 1715)] 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::().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( input: &[u8], factory: &C, ) -> Result { validate_model::().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::(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() -> 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( 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::(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(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 { 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 { 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 { 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 { 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 { 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 { None } fn active_field_value(_: &Self::Active, _: FieldId) -> Option { None } fn set_field(&mut self, id: FieldId, _: ConfigValue) -> Result<(), FieldWriteError> { Err(FieldWriteError::UnknownField(id)) } fn validate(&self, _: &Self::Context) -> Result { 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) )) )); } }