804 lines
28 KiB
Rust
804 lines
28 KiB
Rust
//! 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::<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)
|
|
))
|
|
));
|
|
}
|
|
}
|