Validate configuration through its owning schemas and keep board declarations separate from shared F722 resource ownership. Bound USB transitions, isolate transport failures and retain restart-only activation. Migrate navigation and flight consumers with the sensor and schema contracts. Require explicit installation data and synchronous output authority; board motor outputs remain inhibited. Validation: assembled-prefix host tests, strict Clippy, platform/USB tests and both ARM release builds. Final-source coverage and scoped Miri/Kani evidence are retained separately from hardware qualification.
319 lines
11 KiB
Rust
319 lines
11 KiB
Rust
use core::cell::Cell;
|
|
use kaptan_config::{
|
|
ConfigManager, ConfigModel, ConfigStore, ConfigValue, ConfigViolation, FieldDescriptor,
|
|
FieldId, FieldWriteError, KcfgDecodeError, KcfgExporter, KcfgImporter, KcfgUpdate,
|
|
ReplaceError, ValidationRule, decode_kcfg, encode_edited_kcfg,
|
|
};
|
|
|
|
#[derive(Clone, Debug, PartialEq, kaptan_config::Config)]
|
|
struct Scalars {
|
|
#[config(id = 1, key = "test.u8", unit = "")]
|
|
u8_value: u8,
|
|
#[config(id = 2, key = "test.u16", unit = "")]
|
|
u16_value: u16,
|
|
#[config(id = 3, key = "test.u32", unit = "")]
|
|
u32_value: u32,
|
|
#[config(id = 4, key = "test.u64", unit = "")]
|
|
u64_value: u64,
|
|
#[config(id = 7, key = "test.i32", unit = "")]
|
|
i32_value: i32,
|
|
#[config(id = 9, key = "test.f32", unit = "")]
|
|
f32_value: f32,
|
|
#[config(id = 10, key = "test.f64", unit = "")]
|
|
f64_value: f64,
|
|
#[config(id = 11, key = "test.bool", unit = "")]
|
|
bool_value: bool,
|
|
#[config(id = 12, key = "test.finite", unit = "", finite)]
|
|
finite: f32,
|
|
#[config(id = 13, key = "test.nonzero", unit = "", nonzero)]
|
|
nonzero: f64,
|
|
}
|
|
|
|
impl ConfigModel for Scalars {
|
|
type Active = Self;
|
|
type Context = ();
|
|
const SCHEMA_ID: [u8; 16] = <Self as kaptan_config::Config>::SCHEMA_ID;
|
|
fn descriptor(index: u16) -> Option<&'static FieldDescriptor> {
|
|
<Self as kaptan_config::Config>::descriptor(usize::from(index))
|
|
}
|
|
fn field_count() -> u16 {
|
|
<Self as kaptan_config::Config>::FIELD_COUNT as u16
|
|
}
|
|
fn field_value(&self, id: FieldId) -> Option<ConfigValue> {
|
|
<Self as kaptan_config::Config>::field_value(self, id)
|
|
}
|
|
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> {
|
|
<Self as kaptan_config::Config>::set_field(self, id, value)
|
|
}
|
|
fn validate(&self, _: &()) -> Result<Self::Active, ConfigViolation> {
|
|
<Self as kaptan_config::Config>::validate(self)?;
|
|
Ok(self.clone())
|
|
}
|
|
}
|
|
|
|
fn factory() -> Scalars {
|
|
Scalars {
|
|
u8_value: u8::MAX,
|
|
u16_value: u16::MAX,
|
|
u32_value: u32::MAX,
|
|
u64_value: u64::MAX,
|
|
i32_value: i32::MIN,
|
|
f32_value: 0.0,
|
|
f64_value: 0.0,
|
|
bool_value: true,
|
|
finite: 1.0,
|
|
nonzero: 1.0,
|
|
}
|
|
}
|
|
|
|
struct Store<'a>(&'a Cell<usize>);
|
|
impl ConfigStore<Scalars> for Store<'_> {
|
|
type Error = ();
|
|
fn load(&mut self, _: &Scalars) -> Result<Option<Scalars>, ()> {
|
|
Ok(None)
|
|
}
|
|
fn persist(&mut self, _: &Scalars) -> Result<(), ()> {
|
|
self.0.set(self.0.get() + 1);
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn export(current: &Scalars, factory: &Scalars) -> Vec<u8> {
|
|
let exporter = KcfgExporter::new(current, factory, false).unwrap();
|
|
let mut bytes = vec![0; exporter.length()];
|
|
exporter.read(0, &mut bytes).unwrap();
|
|
bytes
|
|
}
|
|
|
|
fn edit(bytes: &[u8], candidate: &Scalars) -> Vec<u8> {
|
|
let source = decode_kcfg(bytes, &mut [0; kaptan_config::KCFG_FIELD_ID_SCRATCH_BYTES]).unwrap();
|
|
let mut edited = vec![0; bytes.len()];
|
|
encode_edited_kcfg(&source, false, &mut edited, |field| {
|
|
candidate.field_value(field.id()).unwrap()
|
|
})
|
|
.unwrap();
|
|
edited
|
|
}
|
|
|
|
fn import(bytes: &[u8], factory: &Scalars) -> KcfgUpdate<Scalars> {
|
|
let mut importer = KcfgImporter::new(factory).unwrap();
|
|
for byte in bytes {
|
|
importer.push(factory, core::slice::from_ref(byte)).unwrap();
|
|
}
|
|
importer.finish().unwrap()
|
|
}
|
|
|
|
fn assert_exact(actual: ConfigValue, expected: ConfigValue) {
|
|
match (actual, expected) {
|
|
(ConfigValue::F32(a), ConfigValue::F32(b)) => assert_eq!(a.to_bits(), b.to_bits()),
|
|
(ConfigValue::F64(a), ConfigValue::F64(b)) => assert_eq!(a.to_bits(), b.to_bits()),
|
|
_ => assert_eq!(actual, expected),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn independent_editors_compare_the_downloaded_base_before_even_noop_or_validation() {
|
|
let a = factory();
|
|
let writes = Cell::new(0);
|
|
let mut manager = ConfigManager::recover(a.clone(), (), Store(&writes)).unwrap();
|
|
let editor_a = export(manager.committed().unwrap(), &a);
|
|
let editor_b = export(manager.committed().unwrap(), &a);
|
|
let mut b = a.clone();
|
|
b.u64_value -= 1; // Above the exact-integer range of floating-point transports.
|
|
b.f32_value = -0.0;
|
|
b.f64_value = -0.0;
|
|
let update = import(&edit(&editor_b, &b), &a);
|
|
assert!(
|
|
manager
|
|
.replace_if_current(update.candidate, &update.expected_base)
|
|
.unwrap()
|
|
.restart_required
|
|
);
|
|
assert_eq!(writes.get(), 1);
|
|
|
|
// Editing an already-edited draft must not rebase it. Even a requested
|
|
// value set exactly equal to current B is stale when its base was A.
|
|
let draft = edit(&edit(&editor_a, &b), &b);
|
|
let update = import(&draft, &a);
|
|
assert_eq!(
|
|
manager.replace_if_current(update.candidate, &update.expected_base),
|
|
Err(ReplaceError::Conflict)
|
|
);
|
|
let update = import(&editor_a, &a);
|
|
assert_eq!(
|
|
manager.replace_if_current(update.candidate, &update.expected_base),
|
|
Err(ReplaceError::Conflict)
|
|
);
|
|
let mut invalid = b.clone();
|
|
invalid.finite = f32::NAN;
|
|
let violation = ConfigViolation::field(FieldId::new(12), ValidationRule::NonFinite);
|
|
assert_eq!(invalid.validate(&()).unwrap_err(), violation);
|
|
assert_eq!(
|
|
manager.replace_if_current(invalid.clone(), &a),
|
|
Err(ReplaceError::Conflict)
|
|
);
|
|
assert_eq!(
|
|
manager.replace_if_current(invalid, &b),
|
|
Err(ReplaceError::InvalidCandidate(violation))
|
|
);
|
|
assert_eq!(writes.get(), 1);
|
|
|
|
let fresh = import(&export(manager.committed().unwrap(), &a), &a);
|
|
manager
|
|
.replace_if_current(fresh.candidate, &fresh.expected_base)
|
|
.unwrap();
|
|
assert_eq!(writes.get(), 1, "same-base no-op does not consume flash");
|
|
assert_eq!(manager.active().f32_value.to_bits(), 0.0_f32.to_bits());
|
|
assert_eq!(
|
|
manager.committed().unwrap().f32_value.to_bits(),
|
|
(-0.0_f32).to_bits()
|
|
);
|
|
assert!(!manager.is_factory(&b));
|
|
assert!(!manager.replace_if_current(a, &b).unwrap().restart_required);
|
|
assert_eq!(writes.get(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn v3_preserves_all_native_widths_and_signed_zero_in_both_value_sets() {
|
|
let factory = factory();
|
|
let mut current = factory.clone();
|
|
current.f32_value = -0.0;
|
|
current.f64_value = -0.0;
|
|
let bytes = export(¤t, &factory);
|
|
assert_eq!(bytes[4], 3);
|
|
let artifact =
|
|
decode_kcfg(&bytes, &mut [0; kaptan_config::KCFG_FIELD_ID_SCRATCH_BYTES]).unwrap();
|
|
assert_eq!(artifact.schema_id(), Scalars::SCHEMA_ID);
|
|
for field in artifact.fields() {
|
|
assert_eq!(field.finite(), field.id() == FieldId::new(12));
|
|
assert_eq!(field.nonzero(), field.id() == FieldId::new(13));
|
|
let expected = current.field_value(field.id()).unwrap();
|
|
assert_exact(field.value(), expected);
|
|
assert_exact(field.expected_base_value(), expected);
|
|
}
|
|
let update = import(&edit(&bytes, &factory), &factory);
|
|
for index in 0..Scalars::field_count() {
|
|
let id = Scalars::descriptor(index).unwrap().id;
|
|
assert_exact(
|
|
update.candidate.field_value(id).unwrap(),
|
|
factory.field_value(id).unwrap(),
|
|
);
|
|
assert_exact(
|
|
update.expected_base.field_value(id).unwrap(),
|
|
current.field_value(id).unwrap(),
|
|
);
|
|
}
|
|
// 24 header + 4 CRC + 70 prefix + 132 value + 87 key bytes.
|
|
assert_eq!(bytes.len(), 317);
|
|
|
|
let mut importer = KcfgImporter::new(&factory).unwrap();
|
|
assert_eq!(
|
|
importer.push(&factory, &export(¤t, ¤t)),
|
|
Err(KcfgDecodeError::FactoryMismatch(FieldId::new(9)))
|
|
);
|
|
|
|
// Base checks, no-op checks, restart checks, and factory checks must all
|
|
// distinguish signed zero even when every other scalar matches.
|
|
for id in [FieldId::new(9), FieldId::new(10)] {
|
|
let writes = Cell::new(0);
|
|
let mut manager = ConfigManager::recover(factory.clone(), (), Store(&writes)).unwrap();
|
|
let mut negative_zero = factory.clone();
|
|
negative_zero
|
|
.set_field(id, current.field_value(id).unwrap())
|
|
.unwrap();
|
|
assert!(!manager.is_factory(&negative_zero));
|
|
assert_eq!(
|
|
manager.replace_if_current(factory.clone(), &negative_zero),
|
|
Err(ReplaceError::Conflict)
|
|
);
|
|
assert!(
|
|
manager
|
|
.replace_if_current(negative_zero.clone(), &factory)
|
|
.unwrap()
|
|
.restart_required
|
|
);
|
|
assert_eq!(writes.get(), 1);
|
|
assert_eq!(
|
|
manager.replace_if_current(negative_zero, &factory),
|
|
Err(ReplaceError::Conflict)
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn nonfinite_payload_identity_survives_artifacts_cas_noops_and_factory_checks() {
|
|
for (single, double) in [
|
|
(0x7f80_0000, 0xfff0_0000_0000_0000), // infinities
|
|
(0x7fc0_0042, 0xfff8_0000_0000_0042), // quiet payloads
|
|
(0xff80_0042, 0x7ff0_0000_0000_0042), // signaling payloads
|
|
] {
|
|
let mut factory = factory();
|
|
factory.f32_value = f32::from_bits(single);
|
|
factory.f64_value = f64::from_bits(double);
|
|
let writes = Cell::new(0);
|
|
let mut manager = ConfigManager::recover(factory.clone(), (), Store(&writes)).unwrap();
|
|
let bytes = export(&factory, &factory);
|
|
let update = import(&bytes, &factory);
|
|
assert!(manager.is_factory(&update.candidate));
|
|
assert!(
|
|
!manager
|
|
.replace_if_current(update.candidate, &update.expected_base)
|
|
.unwrap()
|
|
.restart_required
|
|
);
|
|
assert_eq!(writes.get(), 0);
|
|
|
|
let mut other = factory.clone();
|
|
other.f64_value = f64::from_bits(double ^ 1);
|
|
assert!(!manager.is_factory(&other));
|
|
assert_eq!(
|
|
manager.replace_if_current(factory.clone(), &other),
|
|
Err(ReplaceError::Conflict)
|
|
);
|
|
assert!(
|
|
manager
|
|
.replace_if_current(other.clone(), &factory)
|
|
.unwrap()
|
|
.restart_required
|
|
);
|
|
assert_eq!(writes.get(), 1);
|
|
let update = import(&export(&other, &factory), &factory);
|
|
assert_eq!(
|
|
update.candidate.field_value(FieldId::new(10)),
|
|
Some(ConfigValue::F64(other.f64_value))
|
|
);
|
|
}
|
|
assert_ne!(ConfigValue::U32(0), ConfigValue::F32(0.0));
|
|
}
|
|
|
|
#[test]
|
|
fn streaming_import_checks_rule_flags_and_rejects_the_unshipped_old_format() {
|
|
let factory = factory();
|
|
let bytes = export(&factory, &factory);
|
|
let mut old = bytes.clone();
|
|
old[4] = 2;
|
|
let mut importer = KcfgImporter::new(&factory).unwrap();
|
|
assert_eq!(
|
|
importer.push(&factory, &old),
|
|
Err(KcfgDecodeError::UnsupportedVersion(2))
|
|
);
|
|
|
|
let mut offset = 24;
|
|
for index in 0..Scalars::field_count() {
|
|
let field = Scalars::descriptor(index).unwrap();
|
|
if field.finite || field.nonzero {
|
|
let mut edited = bytes.clone();
|
|
edited[offset + 3] ^= if field.finite { 4 } else { 8 };
|
|
let mut importer = KcfgImporter::new(&factory).unwrap();
|
|
assert_eq!(
|
|
importer.push(&factory, &edited),
|
|
Err(KcfgDecodeError::DescriptorMismatch(field.id))
|
|
);
|
|
}
|
|
offset += 7 + field.kind.width() * 3 + field.display_key.len() + field.unit.len();
|
|
}
|
|
}
|