feat(firmware): validate configuration and migrate sensor-driven flight APIs
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.
This commit is contained in:
1 parent
fb1b8af2d1
commit
f9a61a086f
211 files changed
+45657
-12740
No files matched your search
@@ -38,7 +38,7 @@ fn expand_scalars(
|
||||
checks: field_checks,
|
||||
} = expand_field(&crate_path, field)?;
|
||||
descriptors.push(descriptor);
|
||||
checks.extend(field_checks);
|
||||
checks.extend(field_checks.iter().cloned());
|
||||
let id = field.id.expect("scalar configuration fields have IDs");
|
||||
let ty = &field.ty;
|
||||
let field_ident = &field.ident;
|
||||
@@ -90,7 +90,11 @@ fn expand_scalars(
|
||||
};
|
||||
let previous = self.#field_ident;
|
||||
self.#field_ident = next;
|
||||
if let Err(violation) = self.__validate_local_configuration() {
|
||||
let validation = (|| -> Result<(), #crate_path::ConfigViolation> {
|
||||
#(#field_checks)*
|
||||
Ok(())
|
||||
})();
|
||||
if let Err(violation) = validation {
|
||||
self.#field_ident = previous;
|
||||
return Err(#crate_path::FieldWriteError::Violation(violation));
|
||||
}
|
||||
@@ -272,8 +276,35 @@ fn expand_field(crate_path: &syn::Path, field: &crate::parse::FieldModel) -> Res
|
||||
Check::Maximum(value) => Some(value),
|
||||
_ => None,
|
||||
});
|
||||
let finite = field
|
||||
.checks
|
||||
.iter()
|
||||
.any(|check| matches!(check, Check::Finite));
|
||||
let nonzero = field
|
||||
.checks
|
||||
.iter()
|
||||
.any(|check| matches!(check, Check::Nonzero));
|
||||
let float = matches!(&field.ty, syn::Type::Path(path)
|
||||
if path.path.is_ident("f32") || path.path.is_ident("f64"));
|
||||
if float && !finite && (minimum.is_some() || maximum.is_some()) {
|
||||
let field_ident = &field.ident;
|
||||
checks.push(quote! {
|
||||
if self.#field_ident.is_nan() {
|
||||
return Err(#crate_path::ConfigViolation::field(
|
||||
#crate_path::FieldId::new(#id),
|
||||
#crate_path::ValidationRule::Unordered,
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
for check in &field.checks {
|
||||
checks.push(expand_check(crate_path, &field.ident, id, check.clone()));
|
||||
checks.push(expand_check(
|
||||
crate_path,
|
||||
&field.ident,
|
||||
id,
|
||||
check.clone(),
|
||||
float,
|
||||
));
|
||||
}
|
||||
let key = field.display_key.as_ref().expect("id fields have a key");
|
||||
let unit = field.unit.as_ref().expect("id fields have a unit");
|
||||
@@ -302,6 +333,8 @@ fn expand_field(crate_path: &syn::Path, field: &crate::parse::FieldModel) -> Res
|
||||
maximum: #maximum,
|
||||
enum_options: #enum_options,
|
||||
unit: #unit,
|
||||
finite: #finite,
|
||||
nonzero: #nonzero,
|
||||
}
|
||||
},
|
||||
checks,
|
||||
@@ -344,7 +377,18 @@ fn config_value_constructor_tokens(crate_path: &syn::Path, ty: &syn::Type) -> Re
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn expand_check(crate_path: &syn::Path, field: &syn::Ident, id: u16, check: Check) -> TokenStream {
|
||||
fn expand_check(
|
||||
crate_path: &syn::Path,
|
||||
field: &syn::Ident,
|
||||
id: u16,
|
||||
check: Check,
|
||||
float: bool,
|
||||
) -> TokenStream {
|
||||
let zero = if float {
|
||||
quote! { 0.0 }
|
||||
} else {
|
||||
quote! { 0 }
|
||||
};
|
||||
match check {
|
||||
Check::Finite => quote! {
|
||||
if !self.#field.is_finite() {
|
||||
@@ -355,7 +399,7 @@ fn expand_check(crate_path: &syn::Path, field: &syn::Ident, id: u16, check: Chec
|
||||
}
|
||||
},
|
||||
Check::Nonzero => quote! {
|
||||
if self.#field == 0 {
|
||||
if self.#field == #zero {
|
||||
return Err(#crate_path::ConfigViolation::field(
|
||||
#crate_path::FieldId::new(#id),
|
||||
#crate_path::ValidationRule::Zero,
|
||||
|
||||
@@ -482,10 +482,12 @@ fn scalar_literal(expression: &Expr, ty: &str) -> Result<ScalarLiteral> {
|
||||
})?;
|
||||
Ok(ScalarLiteral::Unsigned(value))
|
||||
}
|
||||
"f32" => Ok(ScalarLiteral::Float(f64::from(
|
||||
float_literal(expression)? as f32
|
||||
))),
|
||||
"f64" => float_literal(expression).map(ScalarLiteral::Float),
|
||||
// Parse at the emitted literal's native precision. Parsing through f64
|
||||
// first can double-round an f32 bound across an ordering boundary.
|
||||
"f32" => Ok(ScalarLiteral::Float(f64::from(float_literal::<f32>(
|
||||
expression,
|
||||
)?))),
|
||||
"f64" => float_literal::<f64>(expression).map(ScalarLiteral::Float),
|
||||
_ => Err(syn::Error::new(
|
||||
expression.span(),
|
||||
"configuration metadata supports bool, i32, u8, u16, u32, u64, f32, and f64",
|
||||
@@ -542,7 +544,11 @@ fn unsigned_literal(expression: &Expr) -> Result<u128> {
|
||||
}
|
||||
}
|
||||
|
||||
fn float_literal(expression: &Expr) -> Result<f64> {
|
||||
fn float_literal<T>(expression: &Expr) -> Result<T>
|
||||
where
|
||||
T: std::str::FromStr + core::ops::Neg<Output = T>,
|
||||
T::Err: core::fmt::Display,
|
||||
{
|
||||
let (negative, literal) = match expression {
|
||||
Expr::Lit(expression) => (false, &expression.lit),
|
||||
Expr::Unary(expression) if matches!(expression.op, UnOp::Neg(_)) => {
|
||||
@@ -563,10 +569,10 @@ fn float_literal(expression: &Expr) -> Result<f64> {
|
||||
};
|
||||
let value = match literal {
|
||||
Lit::Int(value) => value
|
||||
.base10_parse::<f64>()
|
||||
.base10_parse::<T>()
|
||||
.map_err(|error| syn::Error::new(value.span(), error))?,
|
||||
Lit::Float(value) => value
|
||||
.base10_parse::<f64>()
|
||||
.base10_parse::<T>()
|
||||
.map_err(|error| syn::Error::new(value.span(), error))?,
|
||||
_ => {
|
||||
return Err(syn::Error::new(
|
||||
@@ -601,30 +607,14 @@ fn require_float(ty: &Type, span: &impl Spanned) -> Result<()> {
|
||||
}
|
||||
|
||||
fn require_numeric(ty: &Type, span: &impl Spanned) -> Result<()> {
|
||||
if scalar_type_name(ty).is_some_and(|name| {
|
||||
matches!(
|
||||
name,
|
||||
"f32"
|
||||
| "f64"
|
||||
| "i8"
|
||||
| "i16"
|
||||
| "i32"
|
||||
| "i64"
|
||||
| "i128"
|
||||
| "isize"
|
||||
| "u8"
|
||||
| "u16"
|
||||
| "u32"
|
||||
| "u64"
|
||||
| "u128"
|
||||
| "usize"
|
||||
)
|
||||
}) {
|
||||
if scalar_type_name(ty)
|
||||
.is_some_and(|name| matches!(name, "f32" | "f64" | "i32" | "u8" | "u16" | "u32" | "u64"))
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
Err(syn::Error::new(
|
||||
span.span(),
|
||||
"nonzero and range require a primitive numeric field",
|
||||
"nonzero and range require a supported primitive numeric field",
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -644,18 +634,11 @@ fn scalar_type_name(ty: &Type) -> Option<&str> {
|
||||
ident if ident == "bool" => "bool",
|
||||
ident if ident == "f32" => "f32",
|
||||
ident if ident == "f64" => "f64",
|
||||
ident if ident == "i8" => "i8",
|
||||
ident if ident == "i16" => "i16",
|
||||
ident if ident == "i32" => "i32",
|
||||
ident if ident == "i64" => "i64",
|
||||
ident if ident == "i128" => "i128",
|
||||
ident if ident == "isize" => "isize",
|
||||
ident if ident == "u8" => "u8",
|
||||
ident if ident == "u16" => "u16",
|
||||
ident if ident == "u32" => "u32",
|
||||
ident if ident == "u64" => "u64",
|
||||
ident if ident == "u128" => "u128",
|
||||
ident if ident == "usize" => "usize",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
@@ -290,3 +290,91 @@ fn recursive_tree_rejects_duplicate_display_keys() {
|
||||
Err(issue) if issue.rule() == ValidationRule::Inconsistent
|
||||
));
|
||||
}
|
||||
|
||||
#[derive(Config)]
|
||||
struct FloatRules {
|
||||
#[config(id = 1, key = "finite", unit = "", finite)]
|
||||
finite: f32,
|
||||
#[config(id = 2, key = "nonzero", unit = "", nonzero)]
|
||||
nonzero: f64,
|
||||
#[config(id = 3, key = "lower", unit = "", range(min = 0.0))]
|
||||
lower: f32,
|
||||
#[config(id = 4, key = "upper", unit = "", range(max = 1.0))]
|
||||
upper: f64,
|
||||
#[config(id = 5, key = "free", unit = "")]
|
||||
free: f64,
|
||||
#[config(id = 6, key = "nonzero32", unit = "", nonzero)]
|
||||
nonzero32: f32,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scalar_rules_preserve_local_repair_and_float_domains() {
|
||||
let mut values = FloatRules {
|
||||
finite: f32::NAN,
|
||||
nonzero: 0.0,
|
||||
lower: f32::NAN,
|
||||
upper: f64::NAN,
|
||||
free: f64::NAN,
|
||||
nonzero32: 0.0,
|
||||
};
|
||||
// Every other field is invalid. A local repair must still succeed.
|
||||
values
|
||||
.set_field(FieldId::new(1), ConfigValue::F32(1.0))
|
||||
.unwrap();
|
||||
for (id, value, rule) in [
|
||||
(
|
||||
1,
|
||||
ConfigValue::F32(f32::INFINITY),
|
||||
ValidationRule::NonFinite,
|
||||
),
|
||||
(2, ConfigValue::F64(0.0), ValidationRule::Zero),
|
||||
(2, ConfigValue::F64(-0.0), ValidationRule::Zero),
|
||||
(3, ConfigValue::F32(f32::NAN), ValidationRule::Unordered),
|
||||
(4, ConfigValue::F64(f64::NAN), ValidationRule::Unordered),
|
||||
(6, ConfigValue::F32(0.0), ValidationRule::Zero),
|
||||
(6, ConfigValue::F32(-0.0), ValidationRule::Zero),
|
||||
] {
|
||||
let id = FieldId::new(id);
|
||||
let before = values.field_value(id);
|
||||
assert_eq!(
|
||||
values.set_field(id, value),
|
||||
Err(FieldWriteError::Violation(ConfigViolation::field(id, rule)))
|
||||
);
|
||||
assert_eq!(values.field_value(id), before);
|
||||
}
|
||||
// Neither nonzero nor a one-sided range implies finite.
|
||||
for (id, value) in [
|
||||
(2, ConfigValue::F64(f64::from_bits(0xfff0_0000_0000_0042))),
|
||||
(3, ConfigValue::F32(f32::INFINITY)),
|
||||
(4, ConfigValue::F64(f64::NEG_INFINITY)),
|
||||
(5, ConfigValue::F64(f64::from_bits(0x7ff8_0000_0000_1234))),
|
||||
(6, ConfigValue::F32(f32::INFINITY)),
|
||||
] {
|
||||
values.set_field(FieldId::new(id), value).unwrap();
|
||||
assert_eq!(values.field_value(FieldId::new(id)), Some(value));
|
||||
}
|
||||
values.validate().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rule_metadata_changes_fingerprints() {
|
||||
let descriptor = FloatRules::DESCRIPTORS[0];
|
||||
assert!(descriptor.finite);
|
||||
assert!(!descriptor.nonzero);
|
||||
assert!(FloatRules::DESCRIPTORS[1].nonzero);
|
||||
let original = kaptan_config::validation::__schema_id(&[descriptor]);
|
||||
assert_ne!(
|
||||
original,
|
||||
kaptan_config::validation::__schema_id(&[kaptan_config::FieldDescriptor {
|
||||
finite: false,
|
||||
..descriptor
|
||||
}])
|
||||
);
|
||||
assert_ne!(
|
||||
original,
|
||||
kaptan_config::validation::__schema_id(&[kaptan_config::FieldDescriptor {
|
||||
nonzero: true,
|
||||
..descriptor
|
||||
}])
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
use kaptan_config_derive::Config;
|
||||
|
||||
#[derive(Config)]
|
||||
struct InvertedNativeBounds {
|
||||
#[config(id = 1, key = "value", unit = "", range(min = 1.0000000596046448, max = 1.0))]
|
||||
value: f32,
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
@@ -0,0 +1,5 @@
|
||||
error: configuration range minimum exceeds maximum
|
||||
--> tests/ui/f32_double_rounded_bounds.rs:6:5
|
||||
|
|
||||
6 | value: f32,
|
||||
| ^^^^^
|
||||
@@ -0,0 +1,24 @@
|
||||
use kaptan_config_derive::Config;
|
||||
|
||||
#[derive(Config)]
|
||||
struct NonzeroBool {
|
||||
#[config(id = 1, key = "value", unit = "")]
|
||||
#[config(nonzero)]
|
||||
value: bool,
|
||||
}
|
||||
|
||||
#[derive(Config)]
|
||||
struct RangedBool {
|
||||
#[config(id = 1, key = "value", unit = "")]
|
||||
#[config(range(min = 0, max = 1))]
|
||||
value: bool,
|
||||
}
|
||||
|
||||
#[derive(Config)]
|
||||
struct FiniteBool {
|
||||
#[config(id = 1, key = "value", unit = "")]
|
||||
#[config(finite)]
|
||||
value: bool,
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
@@ -0,0 +1,17 @@
|
||||
error: nonzero and range require a supported primitive numeric field
|
||||
--> tests/ui/numeric_rules_on_bool.rs:6:14
|
||||
|
|
||||
6 | #[config(nonzero)]
|
||||
| ^^^^^^^
|
||||
|
||||
error: nonzero and range require a supported primitive numeric field
|
||||
--> tests/ui/numeric_rules_on_bool.rs:13:14
|
||||
|
|
||||
13 | #[config(range(min = 0, max = 1))]
|
||||
| ^^^^^
|
||||
|
||||
error: finite requires an f32 or f64 field
|
||||
--> tests/ui/numeric_rules_on_bool.rs:20:14
|
||||
|
|
||||
20 | #[config(finite)]
|
||||
| ^^^^^^
|
||||
@@ -0,0 +1,31 @@
|
||||
use kaptan_config_derive::{Config, ConfigEnum};
|
||||
|
||||
#[derive(Clone, Copy, ConfigEnum)]
|
||||
#[repr(u8)]
|
||||
enum Mode {
|
||||
Off = 0,
|
||||
On = 1,
|
||||
}
|
||||
|
||||
#[derive(Config)]
|
||||
struct NonzeroEnum {
|
||||
#[config(enum, id = 1, key = "value", unit = "")]
|
||||
#[config(nonzero)]
|
||||
value: Mode,
|
||||
}
|
||||
|
||||
#[derive(Config)]
|
||||
struct RangedEnum {
|
||||
#[config(enum, id = 1, key = "value", unit = "")]
|
||||
#[config(range(min = 0, max = 1))]
|
||||
value: Mode,
|
||||
}
|
||||
|
||||
#[derive(Config)]
|
||||
struct FiniteEnum {
|
||||
#[config(enum, id = 1, key = "value", unit = "")]
|
||||
#[config(finite)]
|
||||
value: Mode,
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
@@ -0,0 +1,17 @@
|
||||
error: nonzero and range require a supported primitive numeric field
|
||||
--> tests/ui/numeric_rules_on_enum.rs:13:14
|
||||
|
|
||||
13 | #[config(nonzero)]
|
||||
| ^^^^^^^
|
||||
|
||||
error: nonzero and range require a supported primitive numeric field
|
||||
--> tests/ui/numeric_rules_on_enum.rs:20:14
|
||||
|
|
||||
20 | #[config(range(min = 0, max = 1))]
|
||||
| ^^^^^
|
||||
|
||||
error: finite requires an f32 or f64 field
|
||||
--> tests/ui/numeric_rules_on_enum.rs:27:14
|
||||
|
|
||||
27 | #[config(finite)]
|
||||
| ^^^^^^
|
||||
@@ -0,0 +1,15 @@
|
||||
use kaptan_config_derive::Config;
|
||||
|
||||
#[derive(Config)]
|
||||
struct UnsupportedScalar {
|
||||
#[config(id = 1, key = "wide", unit = "")]
|
||||
wide: u128,
|
||||
}
|
||||
|
||||
#[derive(Config)]
|
||||
struct UnsupportedCheckedScalar {
|
||||
#[config(id = 2, key = "signed", unit = "", nonzero)]
|
||||
signed: i64,
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
@@ -0,0 +1,11 @@
|
||||
error: non-primitive configuration fields require config(enum)
|
||||
--> tests/ui/unsupported_scalar.rs:6:11
|
||||
|
|
||||
6 | wide: u128,
|
||||
| ^^^^
|
||||
|
||||
error: nonzero and range require a supported primitive numeric field
|
||||
--> tests/ui/unsupported_scalar.rs:11:49
|
||||
|
|
||||
11 | #[config(id = 2, key = "signed", unit = "", nonzero)]
|
||||
| ^^^^^^^
|
||||
Reference in new issue
Block a user