diff --git a/README.md b/README.md index a5a0779..92b8ff1 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,7 @@ than one that stops, because the gap only shows up in flight. | Integers, 1–64 bits, all three signed codings | Yes | | IEEE-754 binary16, binary32, binary64 | Yes | | MIL-STD-1750A | Refused — reading it is many-to-one, so there is no inverse to write | +| A base container left concrete beside its own children | Refused by name — `abstract` defaults to false in XTCE, and a container with no restriction criteria matches every packet long enough to hold it, so the generated `decode` would answer `Ambiguous` for all of them | | Booleans, enumerations (as generated Rust enums) | Yes | | Fixed-size text: whole-buffer, terminated, length-prefixed | Yes | | Fixed-size binary | Yes | diff --git a/crates/xtce-flight/src/layout.rs b/crates/xtce-flight/src/layout.rs index 8350318..428f313 100644 --- a/crates/xtce-flight/src/layout.rs +++ b/crates/xtce-flight/src/layout.rs @@ -309,6 +309,27 @@ pub fn build(plan: &Plan) -> Result { }); } + // A container with no restriction criteria matches every packet long enough to hold it — + // its generated `matches` is `true` — so a decoder holding it beside any other container + // answers `Ambiguous` for every packet it is ever given. XTCE's `abstract` defaults to + // false, which makes an unmarked base container, the ordinary CCSDS primary-header shape, + // exactly that. Refused here by name rather than generated into a decoder that rejects + // every packet once it is on the spacecraft. + let unconditional = containers + .iter() + .find(|held| held.constants.is_empty()) + .filter(|_| containers.len() > 1); + if let Some(unconditional) = unconditional { + return Err(FlightError::Unsupported { + element: "SequenceContainer".to_owned(), + container: unconditional.xtce_name.clone(), + reason: "this container has no restriction criteria, so it matches every packet \ + long enough to hold it; beside the other containers generated here that \ + makes every packet ambiguous. Mark it abstract=\"true\" if other \ + containers inherit from it, or give it criteria that select it", + }); + } + Ok(Layout { containers, enums: builder.enums, diff --git a/crates/xtce-flight/tests/layout.rs b/crates/xtce-flight/tests/layout.rs index 9c83b9b..01b8f4e 100644 --- a/crates/xtce-flight/tests/layout.rs +++ b/crates/xtce-flight/tests/layout.rs @@ -234,6 +234,54 @@ fn a_data_dependent_width_is_refused() { ); } +/// A base left concrete matches everything, and `abstract` defaults to false. +/// +/// The generated `matches` for a container with no criteria is `true`, so a decoder holding it +/// beside its own children answers `Ambiguous` for every packet. Refused at generation time +/// rather than discovered on the spacecraft. +#[test] +fn a_base_container_that_is_not_abstract_is_refused_by_name() { + let body = r#" + + + + + + + + + + + + + + + + + + + + + + + + + + + + "#; + + let message = refusal(layout_of(body)); + assert!( + message.contains("Base"), + "the refusal has to name it: {message}" + ); + assert!( + message.contains("no restriction criteria"), + "unexpected refusal: {message}" + ); +} + #[test] fn restriction_criteria_become_constants_and_leave_the_struct() { let body = r#"