diff --git a/.fusa-reqs.json b/.fusa-reqs.json index 8186fb1..1040087 100644 --- a/.fusa-reqs.json +++ b/.fusa-reqs.json @@ -301,7 +301,7 @@ { "id": "REQ-PWR-002", "title": "Ordinary power-mode transition shape", - "text": "is_power_mode_transition_defined allows exactly the two ordinary (non-start-up) edges of TC18 v0.5.1_RC §12.4 Figure 17 'power and operation modes' (p.46): Normal->StandBy ('Go to StandBy') and Normal->Sleep ('Go to Sleep'). Every other ordered pair is undefined, including StandBy<->Sleep in both directions (Figure 17 draws no edge between the two low-power modes; each is reached from and returns to Normal only), the two wake-up edges back to Normal (start-up paths owned by try_hot_start/try_cold_start), any pair naming Unpowered, and staying in the same mode", + "text": "is_power_mode_transition_defined allows exactly the two ordinary (non-start-up) edges of TC18 v0.5.1_RC \u00a712.4 Figure 17 'power and operation modes' (p.46): Normal->StandBy ('Go to StandBy') and Normal->Sleep ('Go to Sleep'). Every other ordered pair is undefined, including StandBy<->Sleep in both directions (Figure 17 draws no edge between the two low-power modes; each is reached from and returns to Normal only), the two wake-up edges back to Normal (start-up paths owned by try_hot_start/try_cold_start), any pair naming Unpowered, and staying in the same mode", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -364,7 +364,7 @@ { "id": "REQ-PWRSTART-001", "title": "try_cold_start admits both TC18 cold-start origins (Unpowered and Sleep)", - "text": "TC18 v0.5.1_RC §12.4.1 'Power-On / Wake-Up / Start-Up behavior' (p.46) defines the cold start with two origins — 'a cold start (after power-on or wake-up from sleep)' — and §12.4 Figure 17 labels both the Unpowered->Normal and the Sleep->Normal arrow 'Cold start'. try_cold_start therefore returns Ok(PowerMode::Normal) when from is PowerMode::Unpowered or PowerMode::Sleep and gate satisfies is_power_mode_gate_satisfied, and Err(RcpError::RequestRejected) otherwise — including from PowerMode::Normal, from PowerMode::StandBy (whose resume is the hot start, REQ-PWRSTART-002), and from a valid origin whose gate is unsatisfied. No WakeUp handshake gates this path: §12.4.1 attaches the handshake to the 'Hot-start-up procedure' only", + "text": "TC18 v0.5.1_RC \u00a712.4.1 'Power-On / Wake-Up / Start-Up behavior' (p.46) defines the cold start with two origins \u2014 'a cold start (after power-on or wake-up from sleep)' \u2014 and \u00a712.4 Figure 17 labels both the Unpowered->Normal and the Sleep->Normal arrow 'Cold start'. try_cold_start therefore returns Ok(PowerMode::Normal) when from is PowerMode::Unpowered or PowerMode::Sleep and gate satisfies is_power_mode_gate_satisfied, and Err(RcpError::RequestRejected) otherwise \u2014 including from PowerMode::Normal, from PowerMode::StandBy (whose resume is the hot start, REQ-PWRSTART-002), and from a valid origin whose gate is unsatisfied. No WakeUp handshake gates this path: \u00a712.4.1 attaches the handshake to the 'Hot-start-up procedure' only", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -373,7 +373,7 @@ { "id": "REQ-PWRSTART-002", "title": "try_hot_start requires StandBy origin, acknowledged handshake, and gate", - "text": "TC18 v0.5.1_RC §12.4.1 (p.46) defines the hot start with exactly one origin — 'a hot start (=wake-up from StandBy)' — drawn in §12.4 Figure 17 as the single arrow labelled 'Hot start', and it is §12.4.1's 'Hot-start-up procedure' that specifies the repeated WakeUp message awaiting 'a valid AVTPDU from the sleep request Client', which WakeUpHandshakeState models. try_hot_start therefore returns Ok(PowerMode::Normal) only when from == PowerMode::StandBy, wakeup == WakeUpHandshakeState::Acknowledged, and gate satisfies is_power_mode_gate_satisfied; Err(RcpError::RequestRejected) otherwise, including from PowerMode::Sleep even with an acknowledged handshake (that origin is a cold start, REQ-PWRSTART-001)", + "text": "TC18 v0.5.1_RC \u00a712.4.1 (p.46) defines the hot start with exactly one origin \u2014 'a hot start (=wake-up from StandBy)' \u2014 drawn in \u00a712.4 Figure 17 as the single arrow labelled 'Hot start', and it is \u00a712.4.1's 'Hot-start-up procedure' that specifies the repeated WakeUp message awaiting 'a valid AVTPDU from the sleep request Client', which WakeUpHandshakeState models. try_hot_start therefore returns Ok(PowerMode::Normal) only when from == PowerMode::StandBy, wakeup == WakeUpHandshakeState::Acknowledged, and gate satisfies is_power_mode_gate_satisfied; Err(RcpError::RequestRejected) otherwise, including from PowerMode::Sleep even with an acknowledged handshake (that origin is a cold start, REQ-PWRSTART-001)", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -391,7 +391,7 @@ { "id": "REQ-WIRE-001", "title": "NTSCF frame identifies itself by subtype", - "text": "encode_ntscf_frame's output starts with avtp::NTSCF_SUBTYPE (0x82, TC18 v0.5.1_RC §11.1 Figure 6 (NTSCF-Header Version 0, p.22)) with the sv bit set, via its delegation to encode_ntscf_header; encode_ntscf_frame rejects an acf_payload exceeding the 11-bit ntscf_data_length field width (Figure 6, bits 13-23) with Err(InvalidSize)", + "text": "encode_ntscf_frame's output starts with avtp::NTSCF_SUBTYPE (0x82, TC18 v0.5.1_RC \u00a711.1 Figure 6 (NTSCF-Header Version 0, p.22)) with the sv bit set, via its delegation to encode_ntscf_header; encode_ntscf_frame rejects an acf_payload exceeding the 11-bit ntscf_data_length field width (Figure 6, bits 13-23) with Err(InvalidSize)", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -409,7 +409,7 @@ { "id": "REQ-WIRE-003", "title": "NTSCF frame payload is opaque to the composition step", - "text": "encode_ntscf_frame/decode_ntscf_frame carry the ACF payload bytes verbatim without parsing them as any specific ACF message type — that remains acf::decode_acf_abb/decode_acf_gbb's job on the split payload decode_ntscf_frame returns", + "text": "encode_ntscf_frame/decode_ntscf_frame carry the ACF payload bytes verbatim without parsing them as any specific ACF message type \u2014 that remains acf::decode_acf_abb/decode_acf_gbb's job on the split payload decode_ntscf_frame returns", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -418,7 +418,7 @@ { "id": "REQ-WIRE-004", "title": "NTSCF frame length is header length plus payload length", - "text": "encode_ntscf_frame's output is always exactly avtp::NTSCF_HEADER_LEN + acf_payload.len() bytes, with no padding or reserved gap inserted — TC18 v0.5.1_RC §11.1 Figure 6 (NTSCF-Header Version 0, p.22) places acf_payload_data immediately after stream_id, at octet 12", + "text": "encode_ntscf_frame's output is always exactly avtp::NTSCF_HEADER_LEN + acf_payload.len() bytes, with no padding or reserved gap inserted \u2014 TC18 v0.5.1_RC \u00a711.1 Figure 6 (NTSCF-Header Version 0, p.22) places acf_payload_data immediately after stream_id, at octet 12", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -445,7 +445,7 @@ { "id": "REQ-WIRE-007", "title": "NTSCF frame payload size limit", - "text": "encode_ntscf_frame rejects an acf_payload longer than avtp::NTSCF_DATA_LENGTH_MAX (0x07FF, the 11-bit ntscf_data_length field width at bits 13-23 of TC18 v0.5.1_RC §11.1 Figure 6 (NTSCF-Header Version 0, p.22)) with Err(InvalidSize)", + "text": "encode_ntscf_frame rejects an acf_payload longer than avtp::NTSCF_DATA_LENGTH_MAX (0x07FF, the 11-bit ntscf_data_length field width at bits 13-23 of TC18 v0.5.1_RC \u00a711.1 Figure 6 (NTSCF-Header Version 0, p.22)) with Err(InvalidSize)", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -463,7 +463,7 @@ { "id": "REQ-WIRE-009", "title": "NTSCF frame functions never panic on arbitrary/truncated input", - "text": "decode_ntscf_frame never panics for input shorter than avtp::NTSCF_HEADER_LEN (12 octets per TC18 v0.5.1_RC §11.1 Figure 6 (NTSCF-Header Version 0, p.22); returns Err(ShortFrame)) or for arbitrary byte content at any length, matching the fuzz-style discipline the deleted wire.rs originally established (carried forward by fuzz/fuzz_targets/fuzz_wire_decode.rs, repointed at this function)", + "text": "decode_ntscf_frame never panics for input shorter than avtp::NTSCF_HEADER_LEN (12 octets per TC18 v0.5.1_RC \u00a711.1 Figure 6 (NTSCF-Header Version 0, p.22); returns Err(ShortFrame)) or for arbitrary byte content at any length, matching the fuzz-style discipline the deleted wire.rs originally established (carried forward by fuzz/fuzz_targets/fuzz_wire_decode.rs, repointed at this function)", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1165,7 +1165,7 @@ { "id": "REQ-CFG-005", "title": "JSON and YAML parsing", - "text": "from_json() and from_yaml() parse RcServerConfig (GeneralRegisters, the five §3.7-§3.11 config-table row lists, and initial_state) from text", + "text": "from_json() and from_yaml() parse RcServerConfig (GeneralRegisters, the five \u00a73.7-\u00a73.11 config-table row lists, and initial_state) from text", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1444,7 +1444,7 @@ { "id": "REQ-UDP-004", "title": "Echo-back mismatch rejected", - "text": "UdpTransport::send_acf_abb/send_acf_gbb reject a response whose byte_bus_id does not echo the request's, via acf::verify_echo_back (Err(EpError)) — the StreamId-addressed replacement for the old Zone-mismatch check", + "text": "UdpTransport::send_acf_abb/send_acf_gbb reject a response whose byte_bus_id does not echo the request's, via acf::verify_echo_back (Err(EpError)) \u2014 the StreamId-addressed replacement for the old Zone-mismatch check", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1471,7 +1471,7 @@ { "id": "REQ-UDP-007", "title": "resolve_endpoint rejects an unregistered device endpoint", - "text": "resolve_endpoint returns Err(EpNotFound) when byte_bus_id is not the reserved EP0 address and no endpoint is registered under (stream_id, byte_bus_id) — including when the same byte_bus_id is registered only under a different stream_id", + "text": "resolve_endpoint returns Err(EpNotFound) when byte_bus_id is not the reserved EP0 address and no endpoint is registered under (stream_id, byte_bus_id) \u2014 including when the same byte_bus_id is registered only under a different stream_id", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1588,7 +1588,7 @@ { "id": "REQ-TLS-004", "title": "TlsBridge uses NTSCF/ACF encoding, addressed by StreamId", - "text": "TlsBridge.send_acf_abb()/send_acf_gbb() use avtp::encode_ntscf_frame + acf::encode_acf_abb/encode_acf_gbb, and decode responses via avtp::decode_ntscf_frame + acf::decode_acf_abb/decode_acf_gbb, verified via acf::verify_echo_back — the same framing crate::udp::UdpTransport uses, over a TlsStream instead of a UDP socket", + "text": "TlsBridge.send_acf_abb()/send_acf_gbb() use avtp::encode_ntscf_frame + acf::encode_acf_abb/encode_acf_gbb, and decode responses via avtp::decode_ntscf_frame + acf::decode_acf_abb/decode_acf_gbb, verified via acf::verify_echo_back \u2014 the same framing crate::udp::UdpTransport uses, over a TlsStream instead of a UDP socket", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1732,7 +1732,7 @@ { "id": "REQ-CLI-006", "title": "version --format json", - "text": "The CLI must emit a valid §12.1 JSON document when invoked with version --format json containing tool, version, spec_version, language, runtime fields", + "text": "The CLI must emit a valid \u00a712.1 JSON document when invoked with version --format json containing tool, version, spec_version, language, runtime fields", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1741,7 +1741,7 @@ { "id": "REQ-CLI-007", "title": "capabilities subcommand", - "text": "The CLI must emit a valid §12.2 capabilities JSON document when invoked with capabilities", + "text": "The CLI must emit a valid \u00a712.2 capabilities JSON document when invoked with capabilities", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1750,7 +1750,7 @@ { "id": "REQ-CLI-008", "title": "status --format json", - "text": "The CLI must emit a valid §12.3 status JSON document when invoked with status --format json containing protocol, tool, version, healthy, connected, endpoint, details fields", + "text": "The CLI must emit a valid \u00a712.3 status JSON document when invoked with status --format json containing protocol, tool, version, healthy, connected, endpoint, details fields", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1759,7 +1759,7 @@ { "id": "REQ-CLI-009", "title": "convert --protocol RCP", - "text": "The CLI must accept an rcp.Message JSON (§15.5) on stdin and emit the ToMessage() relay.Message JSON (§15.7.5) on stdout when invoked with convert --protocol RCP, addressing by decimal byte_bus_id; exit 1 on invalid input, exit 2 on wrong/missing --protocol", + "text": "The CLI must accept an rcp.Message JSON (\u00a715.5) on stdin and emit the ToMessage() relay.Message JSON (\u00a715.7.5) on stdout when invoked with convert --protocol RCP, addressing by decimal byte_bus_id; exit 1 on invalid input, exit 2 on wrong/missing --protocol", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1768,7 +1768,7 @@ { "id": "REQ-RELAY-001", "title": "Protocol enum", - "text": "The relay module shall define a Protocol enum matching RELAY spec §3 identifiers, serializing as its integer value", + "text": "The relay module shall define a Protocol enum matching RELAY spec \u00a73 identifiers, serializing as its integer value", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1777,7 +1777,7 @@ { "id": "REQ-RELAY-002", "title": "Message envelope", - "text": "The relay module shall define a Message type matching the RELAY spec §4 universal envelope, with base64-encoded payload", + "text": "The relay module shall define a Message type matching the RELAY spec \u00a74 universal envelope, with base64-encoded payload", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1786,7 +1786,7 @@ { "id": "REQ-RELAY-003", "title": "BackPressurePolicy", - "text": "The relay module shall define a BackPressurePolicy enum (DropNewest/DropOldest/Block) per RELAY spec §14, defaulting to DropNewest", + "text": "The relay module shall define a BackPressurePolicy enum (DropNewest/DropOldest/Block) per RELAY spec \u00a714, defaulting to DropNewest", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1795,7 +1795,7 @@ { "id": "REQ-RELAY-004", "title": "SubscriberOptions", - "text": "The relay module shall define a SubscriberOptions type per RELAY spec §18.3 with a channel_depth default of 64", + "text": "The relay module shall define a SubscriberOptions type per RELAY spec \u00a718.3 with a channel_depth default of 64", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1804,7 +1804,7 @@ { "id": "REQ-RELAY-005", "title": "relay::Error sentinels", - "text": "The relay module shall define the four mandatory RELAY error sentinels (Closed, NotConnected, Timeout, PayloadTooLarge) per §5.1, mutually distinct", + "text": "The relay module shall define the four mandatory RELAY error sentinels (Closed, NotConnected, Timeout, PayloadTooLarge) per \u00a75.1, mutually distinct", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1813,7 +1813,7 @@ { "id": "REQ-RELAY-006", "title": "Context", - "text": "The relay module shall define a Context type carrying an optional deadline per RELAY spec §18.3", + "text": "The relay module shall define a Context type carrying an optional deadline per RELAY spec \u00a718.3", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1822,7 +1822,7 @@ { "id": "REQ-RELAY-007", "title": "relay::Node trait", - "text": "The relay module shall define an async Node trait (protocol/send/subscribe/close) per RELAY spec §10.1", + "text": "The relay module shall define an async Node trait (protocol/send/subscribe/close) per RELAY spec \u00a710.1", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1831,7 +1831,7 @@ { "id": "REQ-RELAY-008", "title": "relay::Caller trait", - "text": "The relay module shall define an async Caller trait extending Node with call() per RELAY spec §10.2", + "text": "The relay module shall define an async Caller trait extending Node with call() per RELAY spec \u00a710.2", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1840,7 +1840,7 @@ { "id": "REQ-RELAY-009", "title": "Base64 payload serde", - "text": "Message and Command/Response/Status payload fields shall serialize as base64 strings per RELAY spec §15.1, matching Go's []byte JSON marshalling", + "text": "Message and Command/Response/Status payload fields shall serialize as base64 strings per RELAY spec \u00a715.1, matching Go's []byte JSON marshalling", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1849,7 +1849,7 @@ { "id": "REQ-ADAPT-006", "title": "AcfAbbMessage response ToMessage()", - "text": "to_message() shall convert an addressed ACF_ABB response to a relay::Message per RELAY spec §15.7.5 (stream_id+byte_bus_id encoded as ID, op surfaced via the rcp.op meta key, payload)", + "text": "to_message() shall convert an addressed ACF_ABB response to a relay::Message per RELAY spec \u00a715.7.5 (stream_id+byte_bus_id encoded as ID, op surfaced via the rcp.op meta key, payload)", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1858,7 +1858,7 @@ { "id": "REQ-ADAPT-007", "title": "Message.FromMessage() to AcfAbbMessage", - "text": "from_message() shall convert a relay::Message to an addressed ACF_ABB request per RELAY spec §15.7.5 (stream_id+byte_bus_id decoded from ID, op/read_size from meta keys), returning InvalidParameter for a malformed ID or an unrecognized rcp.op value", + "text": "from_message() shall convert a relay::Message to an addressed ACF_ABB request per RELAY spec \u00a715.7.5 (stream_id+byte_bus_id decoded from ID, op/read_size from meta keys), returning InvalidParameter for a malformed ID or an unrecognized rcp.op value", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1867,7 +1867,7 @@ { "id": "REQ-ADAPT-008", "title": "AcfAbbMessage response.ToMessage() reply half", - "text": "response_to_message() shall convert an addressed ACF_ABB response to a relay::Message per RELAY spec §15.7.5 (Caller.Call reply half), matching to_message()'s own conversion", + "text": "response_to_message() shall convert an addressed ACF_ABB response to a relay::Message per RELAY spec \u00a715.7.5 (Caller.Call reply half), matching to_message()'s own conversion", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1876,7 +1876,7 @@ { "id": "REQ-ADAPT-009", "title": "Adapt()", - "text": "adapt() shall wrap an RcServer as a relay::Node/Caller per RELAY spec §10.3, addressed by (StreamId, byte_bus_id), dispatching blocking RcServer calls via spawn_blocking, honoring an already-expired Context as Timeout, tracking its own closed state since RcServer has no open/closed connection concept of its own, and returning an immediately-closed channel from subscribe() since RcServer has no live-notification mechanism to forward", + "text": "adapt() shall wrap an RcServer as a relay::Node/Caller per RELAY spec \u00a710.3, addressed by (StreamId, byte_bus_id), dispatching blocking RcServer calls via spawn_blocking, honoring an already-expired Context as Timeout, tracking its own closed state since RcServer has no open/closed connection concept of its own, and returning an immediately-closed channel from subscribe() since RcServer has no live-notification mechanism to forward", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1885,7 +1885,7 @@ { "id": "REQ-ADAPT-010", "title": "Caller.Call()", - "text": "The Adapt() adapter's call() shall dispatch an addressed ACF_ABB request derived from the request Message against the wrapped RcServer and return its response as a relay::Message per RELAY spec §10.2/§15.7.5", + "text": "The Adapt() adapter's call() shall dispatch an addressed ACF_ABB request derived from the request Message against the wrapped RcServer and return its response as a relay::Message per RELAY spec \u00a710.2/\u00a715.7.5", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1903,7 +1903,7 @@ { "id": "REQ-NTSCF-001", "title": "NtscfHeader field model", - "text": "NtscfHeader models sequence_num, ntscf_data_length, and stream_id for the NTSCF AVTPDU header variant — the three named fields TC18 v0.5.1_RC §11.1 Figure 6 (NTSCF-Header Version 0, p.22) defines, with no others.", + "text": "NtscfHeader models sequence_num, ntscf_data_length, and stream_id for the NTSCF AVTPDU header variant \u2014 the three named fields TC18 v0.5.1_RC \u00a711.1 Figure 6 (NTSCF-Header Version 0, p.22) defines, with no others.", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1912,7 +1912,7 @@ { "id": "REQ-NTSCF-002", "title": "NTSCF header encode/decode round-trip", - "text": "encode_ntscf_header emits the exact 12-octet layout of TC18 v0.5.1_RC §11.1 Figure 6 (NTSCF-Header Version 0, p.22): octet 0 subtype, octet 1 sv(1)|version(3)|r(1)|ntscf_data_length[10:8](3), octet 2 ntscf_data_length[7:0], octet 3 sequence_num, octets 4-11 stream_id big-endian — with no reserved gap anywhere. decode_ntscf_header of that output recovers the original header, and an ntscf_data_length above the 11-bit field width (bits 13-23 of the first quadlet) is rejected with InvalidSize.", + "text": "encode_ntscf_header emits the exact 12-octet layout of TC18 v0.5.1_RC \u00a711.1 Figure 6 (NTSCF-Header Version 0, p.22): octet 0 subtype, octet 1 sv(1)|version(3)|r(1)|ntscf_data_length[10:8](3), octet 2 ntscf_data_length[7:0], octet 3 sequence_num, octets 4-11 stream_id big-endian \u2014 with no reserved gap anywhere. decode_ntscf_header of that output recovers the original header, and an ntscf_data_length above the 11-bit field width (bits 13-23 of the first quadlet) is rejected with InvalidSize.", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1921,7 +1921,7 @@ { "id": "REQ-NTSCF-003", "title": "NTSCF subtype and sv bit", - "text": "Encoded NTSCF headers carry subtype 0x82 and set the sv bit, identifying the AVTPDU as NTSCF with a valid stream_id. Both values are read from TC18 v0.5.1_RC §11.1 Figure 6 (NTSCF-Header Version 0, p.22) ('subtype(0x82)', bits 0-7; 'sv', bit 8) and confirmed by the worked example in TC18 Figure 20 (p.79).", + "text": "Encoded NTSCF headers carry subtype 0x82 and set the sv bit, identifying the AVTPDU as NTSCF with a valid stream_id. Both values are read from TC18 v0.5.1_RC \u00a711.1 Figure 6 (NTSCF-Header Version 0, p.22) ('subtype(0x82)', bits 0-7; 'sv', bit 8) and confirmed by the worked example in TC18 Figure 20 (p.79).", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1930,7 +1930,7 @@ { "id": "REQ-NTSCF-004", "title": "NTSCF decode rejects malformed input", - "text": "decode_ntscf_header rejects frames shorter than avtp::NTSCF_HEADER_LEN (12 octets, the header length TC18 v0.5.1_RC §11.1 Figure 6 (NTSCF-Header Version 0, p.22) specifies), a subtype other than 0x82, or an unset sv bit.", + "text": "decode_ntscf_header rejects frames shorter than avtp::NTSCF_HEADER_LEN (12 octets, the header length TC18 v0.5.1_RC \u00a711.1 Figure 6 (NTSCF-Header Version 0, p.22) specifies), a subtype other than 0x82, or an unset sv bit.", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1957,7 +1957,7 @@ { "id": "REQ-TSCF-001", "title": "TscfHeader field model", - "text": "TscfHeader models sequence_num, avtp_timestamp, stream_data_length, and stream_id for the TSCF AVTPDU header variant — the four value-carrying fields TC18 v0.5.1_RC §11.1 Figure 5 (TSCF-Header Version 0, p.22) defines. The figure's tv bit is derived from avtp_timestamp rather than stored, and its mr/tu/rsv/reserved bits are transmitted as zero.", + "text": "TscfHeader models sequence_num, avtp_timestamp, stream_data_length, and stream_id for the TSCF AVTPDU header variant \u2014 the four value-carrying fields TC18 v0.5.1_RC \u00a711.1 Figure 5 (TSCF-Header Version 0, p.22) defines. The figure's tv bit is derived from avtp_timestamp rather than stored, and its mr/tu/rsv/reserved bits are transmitted as zero.", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1966,7 +1966,7 @@ { "id": "REQ-TSCF-002", "title": "TSCF header encode/decode round-trip", - "text": "encode_tscf_header emits the exact 24-octet layout of TC18 v0.5.1_RC §11.1 Figure 5 (TSCF-Header Version 0, p.22): octet 0 subtype, octet 1 sv(1)|version(3)|mr(1)|rsv(2)|tv(1), octet 2 sequence_num, octet 3 reserved(7)|tu(1), octets 4-11 stream_id, octets 12-15 avtp_timestamp, octets 16-19 reserved, octets 20-21 stream_data_length as a full 16-bit big-endian field, octets 22-23 reserved. decode_tscf_header of that output recovers the original header. Because stream_data_length's wire field is a full 16 bits (avtp::TSCF_DATA_LENGTH_MAX == u16::MAX), every representable value is in range and the call never returns InvalidSize.", + "text": "encode_tscf_header emits the exact 24-octet layout of TC18 v0.5.1_RC \u00a711.1 Figure 5 (TSCF-Header Version 0, p.22): octet 0 subtype, octet 1 sv(1)|version(3)|mr(1)|rsv(2)|tv(1), octet 2 sequence_num, octet 3 reserved(7)|tu(1), octets 4-11 stream_id, octets 12-15 avtp_timestamp, octets 16-19 reserved, octets 20-21 stream_data_length as a full 16-bit big-endian field, octets 22-23 reserved. decode_tscf_header of that output recovers the original header. Because stream_data_length's wire field is a full 16 bits (avtp::TSCF_DATA_LENGTH_MAX == u16::MAX), every representable value is in range and the call never returns InvalidSize.", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1975,7 +1975,7 @@ { "id": "REQ-TSCF-003", "title": "TSCF subtype and sv bit", - "text": "Encoded TSCF headers carry subtype 0x05 and set the sv bit, identifying the AVTPDU as TSCF with a valid stream_id, distinct from the NTSCF subtype. The value is read from TC18 v0.5.1_RC §11.1 Figure 5 (TSCF-Header Version 0, p.22) ('subtype(0x05)', bits 0-7) and confirmed by the worked example in TC18 Figure 19 (p.79); it is unrelated to NTSCF's 0x82 and is the smaller of the two. The tv bit (Figure 5, bit 15) is set exactly when avtp_timestamp is non-zero.", + "text": "Encoded TSCF headers carry subtype 0x05 and set the sv bit, identifying the AVTPDU as TSCF with a valid stream_id, distinct from the NTSCF subtype. The value is read from TC18 v0.5.1_RC \u00a711.1 Figure 5 (TSCF-Header Version 0, p.22) ('subtype(0x05)', bits 0-7) and confirmed by the worked example in TC18 Figure 19 (p.79); it is unrelated to NTSCF's 0x82 and is the smaller of the two. The tv bit (Figure 5, bit 15) is set exactly when avtp_timestamp is non-zero.", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1984,7 +1984,7 @@ { "id": "REQ-TSCF-004", "title": "TSCF decode rejects malformed input", - "text": "decode_tscf_header rejects frames shorter than avtp::TSCF_HEADER_LEN (24 octets, the header length TC18 v0.5.1_RC §11.1 Figure 5 (TSCF-Header Version 0, p.22) specifies), a subtype other than 0x05, or an unset sv bit.", + "text": "decode_tscf_header rejects frames shorter than avtp::TSCF_HEADER_LEN (24 octets, the header length TC18 v0.5.1_RC \u00a711.1 Figure 5 (TSCF-Header Version 0, p.22) specifies), a subtype other than 0x05, or an unset sv bit.", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -2631,8 +2631,8 @@ }, { "id": "REQ-RMAP-008", - "title": "GeneralRegisters models the full §3.6 general register-map field set", - "text": "GeneralRegisters carries the six ROADMAP.md-quoted fields (svr_oa_tc18_magic_nr, svr_version, svr_vendor_id, svr_device_id, svr_ep_count, svr_implemented_options) plus the remaining rows of this crate's own §3.6 table extraction, each independently settable/readable, defaults to an all-zero value, and reports CATEGORY == lifecycle::RegisterCategory::General", + "title": "GeneralRegisters models the full \u00a73.6 general register-map field set", + "text": "GeneralRegisters carries the six ROADMAP.md-quoted fields (svr_oa_tc18_magic_nr, svr_version, svr_vendor_id, svr_device_id, svr_ep_count, svr_implemented_options) plus the remaining rows of this crate's own \u00a73.6 table extraction, each independently settable/readable, defaults to an all-zero value, and reports CATEGORY == lifecycle::RegisterCategory::General", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -2667,7 +2667,7 @@ }, { "id": "REQ-RMAP-012", - "title": "HwPinMappingEntry models the §3.7 per-pin mapping row", + "title": "HwPinMappingEntry models the \u00a73.7 per-pin mapping row", "text": "HwPinMappingEntry carries hw_ep_nr, hw_ep_pin_nr, and an undecomposed hw_pin_props byte, and reports CATEGORY == lifecycle::RegisterCategory::HwConfig", "standard": "iso26262", "level": "HLR", @@ -2695,7 +2695,7 @@ { "id": "REQ-RMAP-015", "title": "RequestStreamConfigEntry models TC18 Table 22's per-stream config row", - "text": "RequestStreamConfigEntry carries the sixteen fields TC18 v0.5.1_RC §12.7.7 Table 22 'Request stream configuration' (pp.57-58) tabulates — rx_stream_id (0x0000, 64 bit), rx_stream_max_request_size (0x0008, 16 bit), rx_wd_timeout_interval (0x000A, 16 bit), rx_secure_channel_index (0x000C, 8 bit), the eight 1-bit flags rx_enforce_e2e through rx_wd_info_enable sharing the bit-addressed byte 0x000D, rx_safestate_sequencer (0x000E), rx_safe_sequencer_state (0x000F), rx_ack_stream_index (0x0010), and rx_resp_stream_index (0x0011) — with the eight flags typed bool to match their 1-bit width, and reports CATEGORY == lifecycle::RegisterCategory::RcpConfig", + "text": "RequestStreamConfigEntry carries the sixteen fields TC18 v0.5.1_RC \u00a712.7.7 Table 22 'Request stream configuration' (pp.57-58) tabulates \u2014 rx_stream_id (0x0000, 64 bit), rx_stream_max_request_size (0x0008, 16 bit), rx_wd_timeout_interval (0x000A, 16 bit), rx_secure_channel_index (0x000C, 8 bit), the eight 1-bit flags rx_enforce_e2e through rx_wd_info_enable sharing the bit-addressed byte 0x000D, rx_safestate_sequencer (0x000E), rx_safe_sequencer_state (0x000F), rx_ack_stream_index (0x0010), and rx_resp_stream_index (0x0011) \u2014 with the eight flags typed bool to match their 1-bit width, and reports CATEGORY == lifecycle::RegisterCategory::RcpConfig", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -2722,7 +2722,7 @@ { "id": "REQ-RMAP-018", "title": "EpByteBusIdMapEntry models TC18 Table 23's EP_ID_config row and its end-of-table sentinel", - "text": "EpByteBusIdMapEntry carries the three fields TC18 v0.5.1_RC §12.7.8 Table 23 'EP_ID_config' (p.59) tabulates — map_stream_index (Request_Stream_Index, 0x0000, 8 bit), map_ep_nr (EP_Nr, 0x0001, 8 bit), and map_byte_bus_id (BBID, 0x0002, 16 bit carrying an 11-bit byte_bus_id) — reports CATEGORY == lifecycle::RegisterCategory::RcpConfig, and is_end_of_table returns true iff map_stream_index == END_OF_TABLE_STREAM_INDEX (0), per Table 23's note 'A Request_Stream_Index = 0 denotes the end of the table'. Per that same section, keeping rows in ascending order is the writing client's responsibility, so this module defines no ascending-order validation or enforcement", + "text": "EpByteBusIdMapEntry carries the three fields TC18 v0.5.1_RC \u00a712.7.8 Table 23 'EP_ID_config' (p.59) tabulates \u2014 map_stream_index (Request_Stream_Index, 0x0000, 8 bit), map_ep_nr (EP_Nr, 0x0001, 8 bit), and map_byte_bus_id (BBID, 0x0002, 16 bit carrying an 11-bit byte_bus_id) \u2014 reports CATEGORY == lifecycle::RegisterCategory::RcpConfig, and is_end_of_table returns true iff map_stream_index == END_OF_TABLE_STREAM_INDEX (0), per Table 23's note 'A Request_Stream_Index = 0 denotes the end of the table'. Per that same section, keeping rows in ascending order is the writing client's responsibility, so this module defines no ascending-order validation or enforcement", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -2731,7 +2731,7 @@ { "id": "REQ-RMAP-019", "title": "EpByteBusIdMapEntry encodes TC18 Table 23's 4-byte row layout", - "text": "EpByteBusIdMapEntry::ENCODED_LEN is 4, the row stride fixed by Table 23's own tabulation of the next row's 2_Request_Stream_Index at relative address 0x0004. encode emits [Request_Stream_Index, EP_Nr, BBID big-endian] in that order — EP_Nr at 0x0001 precedes BBID at 0x0002 — and encode/decode round-trip every field exactly; decode ignores any trailing bytes beyond ENCODED_LEN", + "text": "EpByteBusIdMapEntry::ENCODED_LEN is 4, the row stride fixed by Table 23's own tabulation of the next row's 2_Request_Stream_Index at relative address 0x0004. encode emits [Request_Stream_Index, EP_Nr, BBID big-endian] in that order \u2014 EP_Nr at 0x0001 precedes BBID at 0x0002 \u2014 and encode/decode round-trip every field exactly; decode ignores any trailing bytes beyond ENCODED_LEN", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -2748,7 +2748,7 @@ }, { "id": "REQ-RMAP-021", - "title": "ResponseStreamConfigEntry models the §3.10 response/ack queue config row", + "title": "ResponseStreamConfigEntry models the \u00a73.10 response/ack queue config row", "text": "ResponseStreamConfigEntry carries resp_stream_uid, resp_max_avtpdu_size, resp_queue_size, resp_flush_on_count, and resp_flush_time, and reports CATEGORY == lifecycle::RegisterCategory::RcpConfig", "standard": "iso26262", "level": "HLR", @@ -2776,7 +2776,7 @@ { "id": "REQ-RMAP-024", "title": "SequencerStateEntry models both TC18 Table 25 fields and the power-on default", - "text": "SequencerStateEntry carries both fields TC18 v0.5.1_RC §12.7.10 Table 25 'SEQUENCER_config' (p.61) tabulates per sequencer — seq_state (0x0000, 8 bit) and request_stream_index (0x0001, 8 bit, 'refers the Client Nr allowed to access this sequencer', the access-control binding §12.7.10 describes as 'Each sequencer is dedicated to a specific RC Client and its bound endpoints') — giving a 2-byte row stride, fixed by Table 25's own tabulation of Seq_2's Seq_state at relative address 0x0002. It reports CATEGORY == lifecycle::RegisterCategory::RcpConfig, and power_on_default() returns seq_state == 1 per §12.7.10 ('Upon power-on reset all sequencer state values are set to \"1\"') with request_stream_index == 0, Table 25 documenting no default for that field", + "text": "SequencerStateEntry carries both fields TC18 v0.5.1_RC \u00a712.7.10 Table 25 'SEQUENCER_config' (p.61) tabulates per sequencer \u2014 seq_state (0x0000, 8 bit) and request_stream_index (0x0001, 8 bit, 'refers the Client Nr allowed to access this sequencer', the access-control binding \u00a712.7.10 describes as 'Each sequencer is dedicated to a specific RC Client and its bound endpoints') \u2014 giving a 2-byte row stride, fixed by Table 25's own tabulation of Seq_2's Seq_state at relative address 0x0002. It reports CATEGORY == lifecycle::RegisterCategory::RcpConfig, and power_on_default() returns seq_state == 1 per \u00a712.7.10 ('Upon power-on reset all sequencer state values are set to \"1\"') with request_stream_index == 0, Table 25 documenting no default for that field", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -3927,7 +3927,7 @@ }, { "id": "REQ-BUNDLE-001", - "title": "check_compound_bundle_claim requires compound-wait support, ≥4 sequencers, and clear-non-safestate support together before accepting a \"compound request support\" claim", + "title": "check_compound_bundle_claim requires compound-wait support, \u22654 sequencers, and clear-non-safestate support together before accepting a \"compound request support\" claim", "text": "check_compound_bundle_claim(has_compound_wait, svr_sequencers_max, has_clear_non_safestate) returns Ok(()) only when has_compound_wait is true, svr_sequencers_max is greater than or equal to MIN_SEQUENCERS_FOR_COMPOUND_BUNDLE, and has_clear_non_safestate is true, all three simultaneously; returns Err(RcpError::InvalidParameter) whenever any one of the three does not hold, including when none of them hold; never panics for any sampled input", "standard": "iso26262", "level": "HLR", @@ -3936,7 +3936,7 @@ }, { "id": "REQ-BUNDLE-002", - "title": "MIN_SEQUENCERS_FOR_COMPOUND_BUNDLE fixes the \"≥4 sequencers\" threshold check_compound_bundle_claim enforces", + "title": "MIN_SEQUENCERS_FOR_COMPOUND_BUNDLE fixes the \"\u22654 sequencers\" threshold check_compound_bundle_claim enforces", "text": "MIN_SEQUENCERS_FOR_COMPOUND_BUNDLE equals 4; check_compound_bundle_claim rejects every svr_sequencers_max value strictly below MIN_SEQUENCERS_FOR_COMPOUND_BUNDLE (with the other two prerequisites otherwise satisfied) and accepts every svr_sequencers_max value at or above it, up to and including u8::MAX; never panics for any sampled svr_sequencers_max value", "standard": "iso26262", "level": "HLR", @@ -4864,7 +4864,7 @@ { "id": "REQ-CONF-001", "title": "NTSCF header golden vector", - "text": "conformance::golden::NTSCF_GOLDEN_BYTES is a frozen literal byte array whose 12 octets are derived by hand from TC18 v0.5.1_RC §11.1 Figure 6 (NTSCF-Header Version 0, p.22) and cross-checked against the worked example in TC18 Figure 20 (p.79), not captured from this crate's own encoder; avtp::encode_ntscf_header(conformance::golden::ntscf_header_fields()) must equal it, and avtp::decode_ntscf_header of it must recover ntscf_header_fields() unchanged", + "text": "conformance::golden::NTSCF_GOLDEN_BYTES is a frozen literal byte array whose 12 octets are derived by hand from TC18 v0.5.1_RC \u00a711.1 Figure 6 (NTSCF-Header Version 0, p.22) and cross-checked against the worked example in TC18 Figure 20 (p.79), not captured from this crate's own encoder; avtp::encode_ntscf_header(conformance::golden::ntscf_header_fields()) must equal it, and avtp::decode_ntscf_header of it must recover ntscf_header_fields() unchanged", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -4873,7 +4873,7 @@ { "id": "REQ-CONF-002", "title": "TSCF header golden vector with non-degenerate avtp_timestamp", - "text": "conformance::golden::TSCF_GOLDEN_BYTES is a frozen literal byte array for a TscfHeader whose avtp_timestamp is non-zero, whose 24 octets are derived by hand from TC18 v0.5.1_RC §11.1 Figure 5 (TSCF-Header Version 0, p.22) and cross-checked against the worked example in TC18 Figure 19 (p.79), not captured from this crate's own encoder; avtp::encode_tscf_header(conformance::golden::tscf_header_fields()) must equal it, and avtp::decode_tscf_header of it must recover tscf_header_fields() unchanged", + "text": "conformance::golden::TSCF_GOLDEN_BYTES is a frozen literal byte array for a TscfHeader whose avtp_timestamp is non-zero, whose 24 octets are derived by hand from TC18 v0.5.1_RC \u00a711.1 Figure 5 (TSCF-Header Version 0, p.22) and cross-checked against the worked example in TC18 Figure 19 (p.79), not captured from this crate's own encoder; avtp::encode_tscf_header(conformance::golden::tscf_header_fields()) must equal it, and avtp::decode_tscf_header of it must recover tscf_header_fields() unchanged", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -4900,7 +4900,7 @@ { "id": "REQ-CONF-005", "title": "Composed NTSCF+ACF_ABB frame golden vector", - "text": "conformance::golden::NTSCF_ACF_ABB_FRAME_GOLDEN_BYTES is a frozen literal byte array combining ntscf_header_fields() and the encoded acf_abb_fields() message via avtp::encode_ntscf_frame; its 24 octets are the concatenation of the spec-derived NTSCF_GOLDEN_BYTES (TC18 v0.5.1_RC §11.1 Figure 6, p.22) and ACF_ABB_GOLDEN_BYTES (TC18 §11.2.1 Figure 7 / Table 4, p.24), with nothing between them because Figure 6 places acf_payload_data immediately after stream_id; encoding must equal it, and avtp::decode_ntscf_frame of it must split back into the original header and ACF_ABB payload bytes", + "text": "conformance::golden::NTSCF_ACF_ABB_FRAME_GOLDEN_BYTES is a frozen literal byte array combining ntscf_header_fields() and the encoded acf_abb_fields() message via avtp::encode_ntscf_frame; its 24 octets are the concatenation of the spec-derived NTSCF_GOLDEN_BYTES (TC18 v0.5.1_RC \u00a711.1 Figure 6, p.22) and ACF_ABB_GOLDEN_BYTES (TC18 \u00a711.2.1 Figure 7 / Table 4, p.24), with nothing between them because Figure 6 places acf_payload_data immediately after stream_id; encoding must equal it, and avtp::decode_ntscf_frame of it must split back into the original header and ACF_ABB payload bytes", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -4918,7 +4918,7 @@ { "id": "REQ-CRC-012", "title": "finalize_crc_trailer places the CRC32 trailer after pad, matching TC18's real wire order", - "text": "finalize_crc_trailer(frame, crc), given an already-encoded CRC-free ACF_ABB/ACF_GBB frame built from the real payload alone, bumps that frame's own byte_message_info.acf_msg_length by CRC_TRAILER_QUADLETS (one quadlet) and appends crc's 4 big-endian octets after the encoder's own already-correctly-placed pad octets, producing the wire order header (+ message_timestamp), payload, pad, CRC — never payload, CRC, pad, which is what a caller gets by concatenating payload and CRC bytes into one blob before calling encode_acf_abb/encode_acf_gbb; returns Err(RcpError::ShortFrame) for a frame shorter than the byte_message_info header and Err(RcpError::InvalidSize) when bumping acf_msg_length would overflow its 9-bit field", + "text": "finalize_crc_trailer(frame, crc), given an already-encoded CRC-free ACF_ABB/ACF_GBB frame built from the real payload alone, bumps that frame's own byte_message_info.acf_msg_length by CRC_TRAILER_QUADLETS (one quadlet) and appends crc's 4 big-endian octets after the encoder's own already-correctly-placed pad octets, producing the wire order header (+ message_timestamp), payload, pad, CRC \u2014 never payload, CRC, pad, which is what a caller gets by concatenating payload and CRC bytes into one blob before calling encode_acf_abb/encode_acf_gbb; returns Err(RcpError::ShortFrame) for a frame shorter than the byte_message_info header and Err(RcpError::InvalidSize) when bumping acf_msg_length would overflow its 9-bit field", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -4963,7 +4963,7 @@ { "id": "REQ-L2-001", "title": "ETHERTYPE_AVTP names the real IEEE 1722 EtherType and is placed big-endian in the Ethernet header", - "text": "ETHERTYPE_AVTP is 0x22F0, matching TC18 §10.1 (“an AVTPDU is marked by an EtherType value of 0x22F0”); encode_ethernet_frame writes it as bytes 12-13 of the frame in big-endian order, and decode_ethernet_frame reads bytes 12-13 the same way when validating a frame's EtherType", + "text": "ETHERTYPE_AVTP is 0x22F0, matching TC18 \u00a710.1 (\u201can AVTPDU is marked by an EtherType value of 0x22F0\u201d); encode_ethernet_frame writes it as bytes 12-13 of the frame in big-endian order, and decode_ethernet_frame reads bytes 12-13 the same way when validating a frame's EtherType", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -5040,6 +5040,393 @@ "level": "HLR", "asil": "ASIL-B", "verificationMethod": "test" + }, + { + "id": "REQ-ABB-006", + "title": "ACF_ABB acf_msg_type discriminant is 0x0E", + "text": "ACF_ABB_MSG_TYPE is 0x0E, matching TC18 \u00a711.2.1 Table 4 (TC18.txt:1149, \"acf_msg_type 0x0E (ABB message)\") for requests and TC18 \u00a711.3 Table 15 (TC18.txt:1863, \"0x0E - ABB message without message_timestamp\") for responses.", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-GBB-007", + "title": "ACF_ABB and ACF_GBB msg_type constants are distinct, differing only by message_timestamp", + "text": "ACF_ABB_MSG_TYPE (0x0E) and ACF_GBB_MSG_TYPE (0x0D) are the two acf_msg_type discriminants TC18 \u00a711.3 Table 15 (TC18.txt:1863) defines; the sole structural difference between the two message shapes is the 8-octet message_timestamp field GBB carries and ABB omits.", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-ABB-007", + "title": "rsv bit-pairs are always encoded as zero and unrepresentable", + "text": "encode_byte_message_info never sets either rsv bit-pair to a nonzero value, per TC18 \u00a711.2.1 Table 4 (TC18.txt:1153, \"rsv 00b\") and TC18 \u00a711.3 Table 15 (TC18.txt:1867, \"rsv 00b\"); ByteMessageInfo has no field through which a caller could set them to anything else.", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-ABB-009", + "title": "ByteMessageInfo::read_size() is None when op indicates a write", + "text": "ByteMessageInfo::read_size() returns Some(value) only when op=0 (read, response-with-data expected) and None when op=1 (write), per TC18 \u00a711.2.1 Table 4's own polarity (TC18.txt:1163, \"if op = 0 this is read_size, else segment_num\", with op semantics at TC18.txt:1160-1161).", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-CAN-012", + "title": "FrameFormat enum names the six Classical/FD/XL CAN frame formats", + "text": "FrameFormat enumerates the six named CAN frame formats (Classical/FD standard and extended, XL classical and new PHY) this crate's CAN controller targets; wire-byte assignment is this crate's own working interpretation pending a confirmed spec encoding (see the module's own provenance note).", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-CAN-013", + "title": "FrameFormat::from_u8 rejects Table 54's two reserved codes without panicking", + "text": "FrameFormat::from_u8 returns Err(RcpError::InvalidParameter) for any byte outside 0..=5; values 6 and 7 are TC18 \u00a713.7.11.3 Table 54's own two Reserved rows (TC18.txt:5454), and no FrameFormat value wider than 3 bits exists. Never panics for any input.", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-CAN-014", + "title": "CanDataFrame::encode right-aligns an 11-bit CAN ID and never emits an RTR indication", + "text": "CanDataFrame::encode's big-endian id field right-aligns an 11-bit CAN ID within the CAN ID field, per TC18 \u00a713.7.11.3 (TC18.txt:5471, \"In case the CAN ID is 11bits, then it shall be right aligned in the CAN ID field\"); no remote-frame (RTR) indication is ever emitted, matching the same line's statement that remote frames are not supported.", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-CAN-015", + "title": "CanDataFrame has no field or variant capable of representing a remote frame", + "text": "CanDataFrame's fields (a FrameFormat restricted to the four non-XL variants, an arbitration ID, and data bytes) cannot represent a CAN remote frame under any value assignment, matching TC18 \u00a713.7.11.3's statement that remote frames are not supported (TC18.txt:5471).", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-CAN-016", + "title": "CAN_XL_MAX_PAYLOAD is 2048 bytes, CAN_XL_SUBHEADER_LEN is 6 bytes", + "text": "CAN_XL_MAX_PAYLOAD (2048) and CAN_XL_SUBHEADER_LEN (6) match TC18 \u00a713.7.11.3 (TC18.txt:5443, \"For CAN XL this can be up to 2054 bytes (2048 + 6)\") and TC18.txt:5472's 6-byte RRS/SDT/VCID/AF CAN XL sub-header content.", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-CMP-009", + "title": "RequestKind::Standard's discriminant carries materially lower confidence than the other eight", + "text": "RequestKind's doc comment explicitly flags that RequestKind::Standard's numeric wire value is this crate's own working assumption rather than a spec-confirmed encoding, unlike the other eight discriminants (compound/compound-wait/triggered/chained/timed/cancel variants) which trace to specific TC18 Table 5-13 citations.", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-CRC-014", + "title": "CRC32_TC18_POLY_REFLECTED is the bit-reversed form of TC18's CRC32P4 polynomial", + "text": "CRC32_TC18_POLY_REFLECTED (0xC8DF352F) is the standard reflected-CRC bit-reversal of TC18 \u00a713.6 Table 31's \"CRC32P4\" polynomial 0xF4ACFB13 (TC18.txt:3792), independently cross-checked by tests::crc32_tc18_reference against the unreversed form.", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-CRC-017", + "title": "build_crc32_coverage_buffer covers requests and responses via the identical scheme", + "text": "build_crc32_coverage_buffer and crc32_tc18 have no direction parameter; a response's covered bytes differ from a request's only via the rsp/err/evt bits already present inside the covered byte_message_info, per TC18 \u00a713.6 (TC18.txt:3808, \"The CRC calculation for request and response follows the identical scheme\").", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-CRC-019", + "title": "build_crc32_coverage_buffer is shared by both request and response encoding paths", + "text": "Both request-side and response-side CRC coverage go through the same build_crc32_coverage_buffer/crc32_tc18 pair with no direction-specific branch, per TC18 \u00a713.6's identical-scheme rule (TC18.txt:3808).", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-DISC-025", + "title": "build_discovery_request matches Table 16 except the acknowledged op-bit spec typo", + "text": "build_discovery_request matches TC18 \u00a712.6 Table 16's discovery-request field values, except that Table 16's own op row (\"1b (read request)\") contradicts both TC18.txt:3207's general op=0-means-read rule and this crate's implementation; the mismatch is treated as a specification typo, not implemented as written.", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-DISC-026", + "title": "build_discovery_response matches Table 17 except two disclosed, separately-tracked gaps", + "text": "build_discovery_response matches TC18 \u00a712.6 Table 17's discovery-response construction (RC Server's register map content starting from address 0x00000), except: response length is not clamped to the discovery request's read_size (TC18.txt:2422, tracked as REQ-DISC-029) and no stream_id is built by this function at all (Table 17's stream_id = localMAC + unique_id 0x0000 row, tracked as REQ-DISC-030) -- both gaps are recorded, not silently absorbed.", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-EPGEN-001", + "title": "EndpointType::to_u8 matches TC18 Table 29's ep_type values exactly", + "text": "EndpointType::to_u8 produces exactly the ep_type byte TC18 v0.5.1_RC \u00a713.2 Table 29 (\"ep_type values\", p.73) assigns to the same endpoint type, for every EndpointType variant.", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-ERR-021", + "title": "tc18_wire_code maps every mappable RcpError to its correct Table 27 numeric value", + "text": "RcpError::tc18_wire_code returns the exact TC18 \u00a712.9.6 Table 27 numeric value for every error variant that has one: UNSUPPORTED_CMD=1, SEQUENCER_NOT_KNOWN=2, UNAUTHORIZED_ACCESS=3, LOCKED_MEM_ACCESS=4, REQUEST_CANCELED=5, REQUEST_NOT_FOUND=6, EP_ERROR=7, EP_NOT_FOUND=8, PWM_IN_NO_SIGNAL=9, REQ_STORAGE_OVFL=10, REQUEST_REJECTED=11, POCI_FAILURE=12, PRESENTATION_TIME_TOO_FAR=13, GPTP_FAIL=14, INVALID_PARAMETER=15, CHAIN_ABORTED=16, CHAIN_ERROR=17.", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-ERR-022", + "title": "tc18_wire_code covers all seventeen Table 27 codes, not a subset", + "text": "RcpError::tc18_wire_code's match arms cover all seventeen real TC18 Table 27 error codes (\u00a712.9.6), including the four codes (PwmInNoSignal, PociFailure, PresentationTimeTooFar, GptpFail) a naive reading might expect to be missing.", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-ERRH-001", + "title": "RequestKind::from_u8 rejects any non-discriminant byte without panicking", + "text": "RequestKind::from_u8 returns Err(RcpError::InvalidParameter) for any byte value other than the type's named discriminants (compound/triggered/chained/timed/cancel/priority/safety/standard variants); never panics for any input.", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-EVT-001", + "title": "Evt::sub_opcode models evt[2:0] per TC18 \u00a713.5's request-direction reading", + "text": "Evt::sub_opcode models TC18 \u00a713.5's request-direction description of evt[2:0] (\"evt[2:0] are used to control the usage of the byte_msg_payload\"), distinct from the response-direction whole-nibble reading TC18 \u00a711.3 Table 15 assigns the same four bits (tracked separately as REQ-RESP-004).", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-EVT-002", + "title": "Evt::ack models evt[3] as the request-direction acknowledge-request bit", + "text": "Evt::ack models TC18 \u00a713.5's evt[3] (\"evt[3] is used to request an acknowledge. I.e. evt[3]=1 requests acknowledge\"), a request-direction reading distinct from the response-direction whole-nibble reading of the same four bits TC18 \u00a711.3 Table 15 defines (tracked separately as REQ-RESP-004).", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-GPIO-017", + "title": "GPIO_BITMASK_LEN is 4 bytes, matching a 32-IO endpoint", + "text": "GPIO_BITMASK_LEN is 4 bytes (32 bit positions), matching TC18 \u00a713.7.4.1's \"Each GPIO endpoint can handle up to 32 IOs\".", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-GPIO-018", + "title": "GpioBitmask::encode's big-endian byte order matches Figure 24's bit layout", + "text": "GpioBitmask::encode's big-endian encoding places IO0 in the last byte's least-significant bit and IO31 in the first byte's most-significant bit, matching TC18 \u00a713.7.4.1 Figure 24's payload layout (msb on the left) for its own 24-pin worked example.", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-GPIO-019", + "title": "GpioWriteSemantics::Or with an all-zero operand is a documented no-op", + "text": "apply_gpio_write with GpioWriteSemantics::Or and an all-zero operand leaves the current value unchanged for every current value (OR-with-zero is the identity), matching TC18 \u00a713.7.4.3's worked example (\"A request with data '0x0000 0000' and evt[2:0] = 0x001 (OR) results in 'no change'\").", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-GPIO-020", + "title": "GpioWriteSemantics' eight evt[2:0] codes are the endpoint's complete write surface", + "text": "GpioWriteSemantics models exactly the eight evt[2:0] codes TC18 \u00a713.7.4.1 defines for GPIO, with no ninth duration-carrying operation, matching the section's explicit statement that \"Generating a pulse for a defined time is NOT a function of the GPIO endpoint\" -- pulse generation is the Client's responsibility via two commands, a compound, or a trigger operation.", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-GPIO-021", + "title": "GpioWriteSemantics::And follows Table 30's AND definition over \u00a713.7.4.1's contradictory NAND wording", + "text": "GpioWriteSemantics::And (evt[2:0]=010b) implements bitwise AND of byte_msg_payload with the current interface status, per TC18 \u00a713.5 Table 30's normative per-endpoint evt table and its worked example (\"with a byte_msg_payload of 0xFFFF FFFE the first IO pin will be reset, while other IO pins remain unchanged\"); TC18 \u00a713.7.4.1's prose instead says \"NAND\", a TC18-internal inconsistency this crate resolves in Table 30's favor rather than implementing as literally written.", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-I2C-009", + "title": "I2cSpeedMode::to_u8 encodes each speed preset as its i2c_mode wire byte", + "text": "I2cSpeedMode::to_u8 encodes each of the five I2C speed presets (including the two adjacent high-speed rows this crate's spec-extraction pass could not distinguish) as its i2c_mode wire byte value 0..=4.", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-I2C-010", + "title": "I2cSpeedMode::from_u8 rejects any byte outside 0..=4 without panicking", + "text": "I2cSpeedMode::from_u8 returns Err(RcpError::InvalidParameter) for any byte outside 0..=4, matching SpiChannelSelect::from_sub_opcode's own range-check discipline; never panics for any input.", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-I2C-011", + "title": "I2cByteTransfer::encode emits bytes unmodified and unframed", + "text": "I2cByteTransfer::encode returns the transfer's raw bytes (address byte(s) included) unmodified and unframed, with no bytes inserted, removed, or reordered.", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-ISELED-011", + "title": "ISELED command payload is emitted verbatim with no inserted CRC", + "text": "ISELED command encoding emits byte_msg_payload verbatim, in supplied order, with nothing inserted -- in particular no CRC -- per TC18 \u00a713.7.12.3 (TC18.txt:5578, \"plain data in the byte_msg_payload that is to be presented ... on the ISELED bus\") and TC18.txt:5595's confirmation that no CRC is present on the ISELED network unless the endpoint is configured to generate one.", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-LIFE-015", + "title": "RcServerState::INITIAL defaults to HwUnconfigured absent stored configuration", + "text": "RcServerState::INITIAL is HwUnconfigured, the no-stored-configuration fallback TC18 \u00a712.3 (TC18.txt:2059) describes: \"After a power cycle or restart ... the RC Server shall start in the life-cycle state it is actually configured in\", with a device having no NVM permitted to \"incorporate default settings which allow it to be also starting in an advanced state\".", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-LIFE-019", + "title": "is_transition_defined excludes the direct HW_UNCONFIGURED-to-RCP_CONFIGURED jump", + "text": "is_transition_defined(HwUnconfigured, RcpConfigured) is false: TC18 \u00a712.3.1.1 (TC18.txt:2168) states a request to advance directly from HW_UNCONFIGURED to RCP_CONFIGURED \"will be rejected with an error response\" -- the two-hop route through HW_CONFIGURED is the only defined path.", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-LIN-007", + "title": "LinFrameTransfer::encode emits the PID byte plus data unmodified and unframed", + "text": "LinFrameTransfer::encode emits the PID byte followed by data, unmodified and unframed, matching TC18 \u00a713.7.10.3 (TC18.txt:5304)'s description of byte_msg_payload as \"the payload to be used on the Lin bus\", with nothing inserted, removed, or reordered.", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-NET-002", + "title": "select_header_variant discards any frame that is neither NTSCF nor TSCF", + "text": "select_header_variant accepts only the two AVTP subtypes RCP itself defines -- NTSCF (0x82) and TSCF (0x05) -- and discards every other leading subtype octet without attempting to interpret the following bytes, per the general AVTP discard rule (\"If neither a gPTP nor an IEEE1722 frame is found the received frame shall be discarded\").", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-NTSCF-007", + "title": "This crate assembles only NTSCF frames; no encode_tscf_frame counterpart exists", + "text": "encode_ntscf_frame has no encode_tscf_frame counterpart: encode_tscf_header exists only for a client's own use and for round-trip testing, but nothing in this crate assembles a TSCF-headed frame, per TC18 \u00a711.4.3 (TC18.txt:1988, \"The RC Server always uses NTSCF header\").", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-PWMI-003", + "title": "resolve_pwm_in_read returns NoSignal once the configured timeout elapses, regardless of stale data", + "text": "resolve_pwm_in_read returns Some(PwmInReadResolution::NoSignal) once elapsed_since_last_edge reaches config.no_signal_timeout, regardless of last_measured -- a stale measurement is never returned past the timeout; returns None only while still genuinely awaiting a first edge before the timeout, with the caller polling rather than this function blocking. Never panics for any input.", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-RESP-002", + "title": "build_error_response reuses the request's evt nibble verbatim, including evt[3]", + "text": "build_error_response copies the originating request's evt field verbatim (via build_response_info), including evt[3] (the acknowledge-request bit) -- a known gap, since TC18 \u00a711.3.4 (TC18.txt:1904) additionally requires an error response to carry evt[3:0] < 0x9, and this function does not enforce that bound (tracked further as REQ-RESP-007).", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-RESP-003", + "title": "build_error_response never sets message_timestamp on the error response", + "text": "build_error_response never populates a message_timestamp on the response it builds, since this crate has no capture-timestamp source to supply one from.", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-RMAP-032", + "title": "HwPinMappingEntry::ENCODED_LEN is 3 bytes per Table 19's row stride", + "text": "HwPinMappingEntry::ENCODED_LEN is 3, matching TC18 Table 19's row stride as fixed by IO_Pin 2's hw_ep_nr field at relative address 0x0003 -- hw_ep_nr, hw_ep_pin_nr, and hw_pin_type (undecomposed) each occupy one byte.", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-RMAP-036", + "title": "ResponseStreamConfigEntry::ENCODED_LEN is 10 bytes per Table 24's row stride", + "text": "ResponseStreamConfigEntry::ENCODED_LEN is 10, matching TC18 Table 24's row stride as fixed by Responder Queue 2's STREAM_UID field at relative address 0x000A.", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-SEQ-005", + "title": "SequencerState mirrors SequencerStateEntry::seq_state's single-byte shape", + "text": "SequencerState wraps a single u8, mirroring crate::regmap::SequencerStateEntry::seq_state's own single-byte shape -- this crate does not yet read this value from an actual register (see the module's own provenance note on the not-yet-built sequencer-state machine).", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-TS-007", + "title": "AvtpTimestamp::ROLLOVER_PERIOD is 2^32 nanosecond-ticks (~4.295s)", + "text": "AvtpTimestamp::ROLLOVER_PERIOD is 1u64<<32, matching TC18 \u00a711.4.1 (TC18.txt:1952-1953): \"avtp_timestamp = (AS_sec x 10^9 + AS_ns) mod 2^32 ... thus rolls over every 4 seconds\" -- ticks are nanoseconds and the modulus is the field's full 32-bit width (4.294967296s at 1ns/tick). The gPTP-derivation half of this same clause is a separate, unimplemented gap tracked as REQ-TIME-004.", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" + }, + { + "id": "REQ-WAKE-009", + "title": "request_sleep_via_sleep_cmd only reaches sleep once all EPs are idle and responder queues are empty", + "text": "request_sleep_via_sleep_cmd delegates to try_enter_power_mode, which admits the transition only when both PowerModeGateInput flags hold -- so a SleepCMD reaches sleep mode only once all EPs are idle and the responder queues are empty (all responses sent), per TC18 \u00a713.7.2.3's third sleep-sequence step.", + "standard": "iso26262", + "level": "HLR", + "asil": "ASIL-B", + "verificationMethod": "test" } ] } diff --git a/Cargo.lock b/Cargo.lock index 2423eb1..3d1a6b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -333,7 +333,7 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rcp" -version = "5.0.0" +version = "5.0.1" dependencies = [ "async-trait", "base64", diff --git a/Cargo.toml b/Cargo.toml index 9249aeb..2e9cb53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rcp" -version = "5.0.0" +version = "5.0.1" edition = "2021" rust-version = "1.75" license = "MPL-2.0" diff --git a/INCIDENT-RESPONSE.md b/INCIDENT-RESPONSE.md index d7ca3ca..a525aad 100644 --- a/INCIDENT-RESPONSE.md +++ b/INCIDENT-RESPONSE.md @@ -39,7 +39,7 @@ This plan covers security incidents affecting the rust-RCP library or any deploy ### 3. Analysis and Fix 1. Root cause analysis — trace to specific `REQ-*` requirement. -2. Write a failing test reproducing the vulnerability (`// fusa:test REQ-XXX`). +2. Write a failing test reproducing the vulnerability (`//fusa:test REQ-XXX`). 3. Implement the fix; add the requirement annotation. 4. Update `.fusa-problems.json` with problem record. 5. Re-run `rsfusa check` to verify coverage. diff --git a/SAFETY_PLAN.md b/SAFETY_PLAN.md index 324c7fb..2592d36 100644 --- a/SAFETY_PLAN.md +++ b/SAFETY_PLAN.md @@ -28,7 +28,7 @@ See `HARA.md` and `.fusa-hara.json` for the full HARA. Ten hazards (H-001 to H-0 ### 4.2 Requirements Tracing -All safety requirements are annotated with `// fusa:req REQ-XXX` in source files. Test cases are annotated with `// fusa:test REQ-XXX`. The `rsfusa` tool validates traceability in CI. +All safety requirements are annotated with `//fusa:req REQ-XXX` in source files. Test cases are annotated with `//fusa:test REQ-XXX`. The `rsfusa` tool validates traceability in CI. ### 4.3 Verification Strategy diff --git a/fuzz/fuzz_targets/fuzz_avtpdu_acf_decode.rs b/fuzz/fuzz_targets/fuzz_avtpdu_acf_decode.rs index a6ca492..973b860 100644 --- a/fuzz/fuzz_targets/fuzz_avtpdu_acf_decode.rs +++ b/fuzz/fuzz_targets/fuzz_avtpdu_acf_decode.rs @@ -9,27 +9,27 @@ use libfuzzer_sys::fuzz_target; // only failure mode under test is a panic inside the crate's own decode // logic, never an assertion in this harness. fuzz_target!(|data: &[u8]| { - // fusa:req REQ-NTSCF-005 - // fusa:req REQ-NTSCF-006 + //fusa:req REQ-NTSCF-005 + //fusa:req REQ-NTSCF-006 let _ = rcp::avtp::decode_ntscf_header(data); - // fusa:req REQ-TSCF-005 - // fusa:req REQ-TSCF-006 + //fusa:req REQ-TSCF-005 + //fusa:req REQ-TSCF-006 let _ = rcp::avtp::decode_tscf_header(data); - // fusa:req REQ-HVSEL-005 + //fusa:req REQ-HVSEL-005 // select_header_variant is exercised under both TimeSyncCapability // outcomes, since the rule branches on it before decoding the body. let _ = rcp::avtp::select_header_variant(data, rcp::avtp::TimeSyncCapability::Capable); let _ = rcp::avtp::select_header_variant(data, rcp::avtp::TimeSyncCapability::Incapable); - // fusa:req REQ-BMI-004 + //fusa:req REQ-BMI-004 let _ = rcp::acf::decode_byte_message_info(data); - // fusa:req REQ-ABB-005 + //fusa:req REQ-ABB-005 let _ = rcp::acf::decode_acf_abb(data); - // fusa:req REQ-GBB-005 + //fusa:req REQ-GBB-005 let _ = rcp::acf::decode_acf_gbb(data); // Belt-and-suspenders: parse_stream_id/StreamId::from_u64 take a plain diff --git a/fuzz/fuzz_targets/fuzz_config_parse.rs b/fuzz/fuzz_targets/fuzz_config_parse.rs index 435f8ea..09ad5a5 100644 --- a/fuzz/fuzz_targets/fuzz_config_parse.rs +++ b/fuzz/fuzz_targets/fuzz_config_parse.rs @@ -2,7 +2,7 @@ use libfuzzer_sys::fuzz_target; fuzz_target!(|data: &[u8]| { - // fusa:req REQ-CFG-005 + //fusa:req REQ-CFG-005 if let Ok(s) = std::str::from_utf8(data) { let _ = rcp::config::from_json(s); let _ = rcp::config::from_yaml(s); diff --git a/fuzz/fuzz_targets/fuzz_e2e_unwrap.rs b/fuzz/fuzz_targets/fuzz_e2e_unwrap.rs index 38dbbaa..af647f9 100644 --- a/fuzz/fuzz_targets/fuzz_e2e_unwrap.rs +++ b/fuzz/fuzz_targets/fuzz_e2e_unwrap.rs @@ -10,6 +10,6 @@ use libfuzzer_sys::fuzz_target; // an arbitrary caller-supplied byte slice, matching this harness's own // `data: &[u8]` shape. fuzz_target!(|data: &[u8]| { - // fusa:req REQ-CRC-002 + //fusa:req REQ-CRC-002 let _ = rcp::e2e::crc32_tc18(data); }); diff --git a/fuzz/fuzz_targets/fuzz_wire_decode.rs b/fuzz/fuzz_targets/fuzz_wire_decode.rs index e229974..62057ad 100644 --- a/fuzz/fuzz_targets/fuzz_wire_decode.rs +++ b/fuzz/fuzz_targets/fuzz_wire_decode.rs @@ -15,8 +15,8 @@ use libfuzzer_sys::fuzz_target; // into both ACF decoders. Each call is `let _ = ...;`: the only failure // mode under test is a panic inside the crate's own decode logic. fuzz_target!(|data: &[u8]| { - // fusa:req REQ-WIRE-008 - // fusa:req REQ-WIRE-009 + //fusa:req REQ-WIRE-008 + //fusa:req REQ-WIRE-009 if let Ok((_hdr, payload)) = rcp::avtp::decode_ntscf_frame(data) { let _ = rcp::acf::decode_acf_abb(payload); let _ = rcp::acf::decode_acf_gbb(payload); diff --git a/scripts/fusa-gap-check.sh b/scripts/fusa-gap-check.sh index 50edfb2..9576b36 100755 --- a/scripts/fusa-gap-check.sh +++ b/scripts/fusa-gap-check.sh @@ -1,5 +1,8 @@ #!/usr/bin/env bash # FuSa gap check: verify every requirement has source annotation and test annotation. +# Tag format is //fusa:req / //fusa:test with NO space after the slashes — this is +# exactly what rust-FuSa (rsfusa) trace.rs::annotation_kind matches; a space makes +# the annotation invisible to the real tool. # Exits 1 if any gap is found. Run from repo root. set -euo pipefail @@ -22,24 +25,38 @@ reqs_data = json.loads(reqs_file.read_text()) # .fusa-reqs.json schema 1.0: {"schemaVersion": "1.0", "requirements": [...]}. # Older files were a bare array; accept both for robustness. reqs_list = reqs_data["requirements"] if isinstance(reqs_data, dict) else reqs_data -declared = {r["id"] for r in reqs_list} + +# A requirement carrying "status": "not-implemented" is a deliberate, honest +# record that TC18 mandates something this crate does NOT do. It is part of +# the requirements corpus — so that the corpus is a complete map of TC18's +# normative surface rather than only the parts that happen to be built — but +# by definition it has no implementation and no test to trace to, so it is +# exempt from the annotation requirement below and reported separately. +NOT_IMPL = "not-implemented" +unimpl = {r["id"]: r for r in reqs_list if r.get("status") == NOT_IMPL} +declared = {r["id"] for r in reqs_list} - set(unimpl) src_text = "\n".join(p.read_text() for p in src_dir.rglob("*.rs")) -in_src = set(re.findall(r"//\s*fusa:req\s+(REQ-[\w-]+)", src_text)) -in_test = set(re.findall(r"//\s*fusa:test\s+(REQ-[\w-]+)", src_text)) +in_src = set(re.findall(r"//fusa:req\s+(REQ-[\w-]+)", src_text)) +in_test = set(re.findall(r"//fusa:test\s+(REQ-[\w-]+)", src_text)) gaps = [] for req_id in sorted(declared): missing = [] if req_id not in in_src: - missing.append("source annotation (// fusa:req)") + missing.append("source annotation (//fusa:req)") if req_id not in in_test: - missing.append("test annotation (// fusa:test)") + missing.append("test annotation (//fusa:test)") if missing: gaps.append((req_id, missing)) -undeclared_src = in_src - declared -undeclared_test = in_test - declared +undeclared_src = in_src - declared - set(unimpl) +undeclared_test = in_test - declared - set(unimpl) + +# An entry marked not-implemented must not also be traced to code/tests — +# that would mean the marker is stale and the corpus is lying in the other +# direction. Treat it as a gap so it gets fixed. +stale_unimpl = sorted((in_src | in_test) & set(unimpl)) if gaps: print(f"\nFuSa GAP REPORT — {len(gaps)} requirement(s) with missing coverage:\n") @@ -56,12 +73,23 @@ if undeclared_test: for r in sorted(undeclared_test): print(f" {r}") +if stale_unimpl: + print(f"\nERROR: {len(stale_unimpl)} requirement(s) marked " + f'"{NOT_IMPL}" but traced to code and/or tests:') + for r in stale_unimpl: + print(f" {r}") + total = len(declared) covered = len(declared - {g[0] for g in gaps}) pct = 100 * covered // total if total else 0 -print(f"\nCoverage: {covered}/{total} ({pct}%) requirements fully traced") +print(f"\nCoverage: {covered}/{total} ({pct}%) implemented requirements fully traced") +if unimpl: + print(f"Declared but NOT implemented (TC18 clauses this crate does not " + f"satisfy): {len(unimpl)}") + for req_id in sorted(unimpl): + print(f" {req_id}: {unimpl[req_id].get('title', '')}") -if gaps: +if gaps or stale_unimpl: sys.exit(1) print("OK — no FuSa gaps detected") EOF diff --git a/src/acf.rs b/src/acf.rs index ff86437..6b227c3 100644 --- a/src/acf.rs +++ b/src/acf.rs @@ -1,21 +1,21 @@ -// fusa:req REQ-BMI-001 -// fusa:req REQ-BMI-002 -// fusa:req REQ-BMI-003 -// fusa:req REQ-BMI-004 -// fusa:req REQ-ABB-001 -// fusa:req REQ-ABB-002 -// fusa:req REQ-ABB-003 -// fusa:req REQ-ABB-004 -// fusa:req REQ-ABB-005 -// fusa:req REQ-GBB-001 -// fusa:req REQ-GBB-002 -// fusa:req REQ-GBB-003 -// fusa:req REQ-GBB-004 -// fusa:req REQ-GBB-005 -// fusa:req REQ-ECHO-001 -// fusa:req REQ-ECHO-002 -// fusa:req REQ-ECHO-003 -// fusa:req REQ-ECHO-004 +//fusa:req REQ-BMI-001 +//fusa:req REQ-BMI-002 +//fusa:req REQ-BMI-003 +//fusa:req REQ-BMI-004 +//fusa:req REQ-ABB-001 +//fusa:req REQ-ABB-002 +//fusa:req REQ-ABB-003 +//fusa:req REQ-ABB-004 +//fusa:req REQ-ABB-005 +//fusa:req REQ-GBB-001 +//fusa:req REQ-GBB-002 +//fusa:req REQ-GBB-003 +//fusa:req REQ-GBB-004 +//fusa:req REQ-GBB-005 +//fusa:req REQ-ECHO-001 +//fusa:req REQ-ECHO-002 +//fusa:req REQ-ECHO-003 +//fusa:req REQ-ECHO-004 //! ACF (AVTP Control Format) messages — TC18 wire format core (`ROADMAP.md` //! Milestone 1, "ACF Messages" subsection). @@ -200,12 +200,28 @@ pub const BYTE_MESSAGE_INFO_LEN: usize = 8; /// The `evt` field: a 1-bit ack flag + 3-bit sub-opcode pair, packed into /// row 2's 4-bit `evt` nibble (octet 4 bits 7:4). +/// +/// The split into `ack` + `sub_opcode` is exactly TC18 §13.5's own split of +/// the `evt` nibble for **requests** (TC18.txt lines 3672-3673): "event bits +/// evt[2:0] are used to control the usage of the byte_msg_payload" +/// ([`Evt::sub_opcode`]) and "evt[3] is used to request an acknowledge. I.e. +/// evt[3]=1 requests acknowledge" ([`Evt::ack`]). +/// +/// Note that TC18 §11.3 Table 15 gives the *response* direction a different, +/// whole-nibble reading of the same four bits (0x0 simple/data/error, 0x1…0x8 +/// a further-responses counter, 0x9…0xE reserved, 0xF acknowledge) which this +/// two-field split does not model — see requirement `REQ-RESP-004`. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-BMI-001 +//fusa:req REQ-BMI-001 +//fusa:req REQ-EVT-001 +//fusa:req REQ-EVT-002 pub struct Evt { - /// The ack-flag bit of `evt`. + /// The ack-flag bit of `evt` — TC18 §13.5's `evt[3]`, which a request + /// sets to 1 to request an acknowledge (TC18.txt line 3673). pub ack: bool, - /// The 3-bit sub-opcode of `evt`. Valid range is `0..=EVT_SUB_OPCODE_MAX`. + /// The 3-bit sub-opcode of `evt` — TC18 §13.5's `evt[2:0]`, which + /// controls the endpoint-specific usage of the `byte_msg_payload` + /// (TC18.txt line 3672). Valid range is `0..=EVT_SUB_OPCODE_MAX`. pub sub_opcode: u8, } @@ -226,7 +242,7 @@ pub struct Evt { /// which apply the `op`-bit selection this module's provenance note /// describes rather than assuming one interpretation unconditionally. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-BMI-001 +//fusa:req REQ-BMI-001 pub struct ReadSizeOrSegment(pub u16); impl ReadSizeOrSegment { @@ -253,7 +269,7 @@ impl ReadSizeOrSegment { /// module does not implement `(stream_id, byte_bus_id)` addressing or the /// echo-back rule; those are the separate "Addressing" checklist item. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-BMI-001 +//fusa:req REQ-BMI-001 pub struct ByteMessageInfo { /// The ACF message-type discriminant (`ACF_ABB_MSG_TYPE`/ /// `ACF_GBB_MSG_TYPE`). 7 bits; valid range is @@ -313,7 +329,13 @@ impl ByteMessageInfo { /// describes: unlike [`ReadSizeOrSegment::as_read_size`]'s /// unconditional view of the raw field, this method refuses to hand /// back a value under the interpretation `op` says does not apply. - // fusa:req REQ-BMI-005 + /// + /// The polarity is TC18 §11.2.1 Table 4's own (TC18.txt line 1163): "if + /// op = 0 this is read_size, else segment_num", with `op = 0b` + /// meaning "the sender of this request expects a response with data" + /// and `op = 1b` meaning it does not (TC18.txt lines 1160-1161). + //fusa:req REQ-BMI-005 + //fusa:req REQ-ABB-009 pub fn read_size(&self) -> Option { if self.op { None @@ -330,7 +352,8 @@ impl ByteMessageInfo { /// See [`ByteMessageInfo::read_size`]'s doc comment for why this is the /// preferred accessor over [`ReadSizeOrSegment::as_segment_num`]'s /// unconditional view. - // fusa:req REQ-BMI-005 + //fusa:req REQ-BMI-005 + //fusa:req REQ-ABB-009 pub fn segment_num(&self) -> Option { if self.op { Some(self.read_size_segment.as_segment_num()) @@ -347,8 +370,15 @@ impl ByteMessageInfo { /// width (`acf_msg_type`: 7 bits, `acf_msg_length`: 9 bits, `pad`: 2 bits, /// `byte_bus_id`: 11 bits, `evt.sub_opcode`: 3 bits, `read_size_segment`: /// 12 bits). -// fusa:req REQ-BMI-002 -// fusa:req REQ-BMI-003 +/// +/// Both `rsv` bit-pairs are always transmitted as zero, per TC18 §11.2.1 +/// Table 4 (TC18.txt line 1153, "rsv 00b") and TC18 §11.3 Table 15 +/// (TC18.txt line 1867, "rsv 00b"). They are not modeled as +/// [`ByteMessageInfo`] fields at all, so there is no way for a caller to +/// set them to anything else. +//fusa:req REQ-BMI-002 +//fusa:req REQ-BMI-003 +//fusa:req REQ-ABB-007 pub fn encode_byte_message_info( info: &ByteMessageInfo, ) -> Result<[u8; BYTE_MESSAGE_INFO_LEN], RcpError> { @@ -403,8 +433,8 @@ pub fn encode_byte_message_info( /// Never panics on short, truncated, or arbitrary input — always returns /// `Err(RcpError::ShortFrame)` for input shorter than /// [`BYTE_MESSAGE_INFO_LEN`] instead. -// fusa:req REQ-BMI-002 -// fusa:req REQ-BMI-004 +//fusa:req REQ-BMI-002 +//fusa:req REQ-BMI-004 pub fn decode_byte_message_info(b: &[u8]) -> Result { if b.len() < BYTE_MESSAGE_INFO_LEN { return Err(RcpError::ShortFrame); @@ -454,9 +484,22 @@ pub fn decode_byte_message_info(b: &[u8]) -> Result { // ── Constants shared by both ACF message types ──────────────────────────────── /// `acf_msg_type` discriminant identifying an ACF_ABB message. +/// +/// TC18 §11.2.1 Table 4 (TC18.txt line 1149) fixes this at `0x0E` for a +/// standard request ("acf_msg_type 0x0E (ABB message)"), and TC18 §11.3 +/// Table 15 (TC18.txt line 1863) repeats it for responses, spelling out +/// what the value means: "0x0E - ABB message without message_timestamp". +//fusa:req REQ-ABB-006 +//fusa:req REQ-GBB-007 pub const ACF_ABB_MSG_TYPE: u8 = 0x0E; /// `acf_msg_type` discriminant identifying an ACF_GBB message. +/// +/// TC18 §11.3 Table 15 (TC18.txt line 1863): "0x0D - GBB message, with +/// message_timestamp". The one structural difference the two discriminants +/// name is exactly the 8-octet `message_timestamp`, which is why +/// [`ACF_GBB_HEADER_LEN`] - [`ACF_ABB_HEADER_LEN`] is 8. +//fusa:req REQ-GBB-007 pub const ACF_GBB_MSG_TYPE: u8 = 0x0D; /// Length, in bytes, of the ACF_ABB message header: just @@ -561,7 +604,7 @@ fn take_message_bytes<'a>( /// There is intentionally no `timestamp` field on this struct at all — see /// the module doc comment's opening summary of ACF_ABB. #[derive(Debug, Clone, PartialEq, Eq, Default)] -// fusa:req REQ-ABB-001 +//fusa:req REQ-ABB-001 pub struct AcfAbbMessage { /// The shared `byte_message_info` header. See [`ByteMessageInfo`]. pub info: ByteMessageInfo, @@ -587,8 +630,8 @@ pub struct AcfAbbMessage { /// the 9-bit quadlet-count range, or if `msg.info` (with those three /// fields so overwritten) otherwise fails /// [`encode_byte_message_info`]'s field-width validation. -// fusa:req REQ-ABB-002 -// fusa:req REQ-ABB-003 +//fusa:req REQ-ABB-002 +//fusa:req REQ-ABB-003 pub fn encode_acf_abb(msg: &AcfAbbMessage) -> Result, RcpError> { let (acf_msg_length, pad) = quadlets_and_pad_for_message(ACF_ABB_HEADER_LEN, msg.payload.len())?; @@ -615,9 +658,9 @@ pub fn encode_acf_abb(msg: &AcfAbbMessage) -> Result, RcpError> { /// §12.9.1.1) are left unread rather than folded into `payload` or /// rejected — see [`decode_acf_abb_messages`] for splitting a frame that /// carries more than one. -// fusa:req REQ-ABB-002 -// fusa:req REQ-ABB-004 -// fusa:req REQ-ABB-005 +//fusa:req REQ-ABB-002 +//fusa:req REQ-ABB-004 +//fusa:req REQ-ABB-005 pub fn decode_acf_abb(b: &[u8]) -> Result { if b.len() < ACF_ABB_HEADER_LEN { return Err(RcpError::ShortFrame); @@ -652,7 +695,7 @@ pub fn decode_acf_abb(b: &[u8]) -> Result { /// `b` is rejected the same way [`decode_acf_abb`] rejects it, rather than /// silently returning zero messages, so a caller cannot mistake "nothing /// to parse" for "one legitimately-empty-payload message". -// fusa:req REQ-ABB-004 +//fusa:req REQ-ABB-004 pub fn decode_acf_abb_messages(b: &[u8]) -> Result, RcpError> { if b.is_empty() { return Err(RcpError::ShortFrame); @@ -704,7 +747,7 @@ pub fn decode_acf_abb_messages(b: &[u8]) -> Result, RcpError> /// encoded/decoded here as one opaque `u64`, with no `RequestKind`-specific /// handling of its own. #[derive(Debug, Clone, PartialEq, Eq, Default)] -// fusa:req REQ-GBB-001 +//fusa:req REQ-GBB-001 pub struct AcfGbbMessage { /// The shared `byte_message_info` header. See [`ByteMessageInfo`]. pub info: ByteMessageInfo, @@ -733,8 +776,8 @@ pub struct AcfGbbMessage { /// 9-bit quadlet-count range, or if `msg.info` (with those three fields so /// overwritten) otherwise fails [`encode_byte_message_info`]'s /// field-width validation. -// fusa:req REQ-GBB-002 -// fusa:req REQ-GBB-003 +//fusa:req REQ-GBB-002 +//fusa:req REQ-GBB-003 pub fn encode_acf_gbb(msg: &AcfGbbMessage) -> Result, RcpError> { let (acf_msg_length, pad) = quadlets_and_pad_for_message(ACF_GBB_HEADER_LEN, msg.payload.len())?; @@ -760,9 +803,9 @@ pub fn encode_acf_gbb(msg: &AcfGbbMessage) -> Result, RcpError> { /// `acf_msg_length` describes — see [`decode_acf_abb`]'s doc comment for /// the same rule, applied here over the region following /// `message_timestamp`. -// fusa:req REQ-GBB-002 -// fusa:req REQ-GBB-004 -// fusa:req REQ-GBB-005 +//fusa:req REQ-GBB-002 +//fusa:req REQ-GBB-004 +//fusa:req REQ-GBB-005 pub fn decode_acf_gbb(b: &[u8]) -> Result { if b.len() < ACF_GBB_HEADER_LEN { return Err(RcpError::ShortFrame); @@ -828,8 +871,8 @@ fn wrong_discriminant_error( /// even if `request` is itself already a decoded response; rejecting that /// shape, if ever needed, is a separate concern for whichever later /// milestone builds the full request/response dispatch. -// fusa:req REQ-ECHO-001 -// fusa:req REQ-ECHO-002 +//fusa:req REQ-ECHO-001 +//fusa:req REQ-ECHO-002 pub fn build_response_info( request: &ByteMessageInfo, mut response: ByteMessageInfo, @@ -849,9 +892,9 @@ pub fn build_response_info( /// since that is a separate concern from the byte_bus_id-echoing rule this /// function checks. Never panics: both inputs are already-decoded values, /// not raw bytes, so there is no truncated-input shape to reject. -// fusa:req REQ-ECHO-001 -// fusa:req REQ-ECHO-003 -// fusa:req REQ-ECHO-004 +//fusa:req REQ-ECHO-001 +//fusa:req REQ-ECHO-003 +//fusa:req REQ-ECHO-004 pub fn verify_echo_back( request: &ByteMessageInfo, response: &ByteMessageInfo, @@ -890,6 +933,25 @@ pub fn verify_echo_back( /// crate's own working interpretation (Guiding Principle 5), matching the /// simplest reading of that text, and flagged here for reconciliation /// against real TC18 behavior before being relied on for interop. +/// +/// The returned message is always an [`AcfAbbMessage`] — i.e. the +/// timestamp-free ACF_ABB format. That matches TC18 §11.3 (TC18.txt line +/// 1859: "In case responses include a timestamp, they are in ACF_GBB +/// format, else in ACF_ABB format") and §11.4.2 (TC18.txt line 1963: "When +/// timestamping was not requested, then the ACF_ABB type format without +/// time stamp shall be default for any data transmitted by the RC Server"), +/// since this crate has no capture-timestamp source to put in a +/// `message_timestamp` in the first place. +/// +/// **Known gap.** TC18 §11.3.4 (TC18.txt line 1904) additionally requires an +/// error response to carry `evt[3:0] < 0x9`. This function copies +/// `request`'s `evt` verbatim (via [`build_response_info`]), including +/// `evt[3]` — TC18 §13.5's acknowledge-request bit — so an error response to +/// a request that asked for an acknowledge can carry an `evt` nibble in +/// TC18 Table 15's reserved `0x9…0xE` range, or even `0xF`, which Table 15 +/// defines as *acknowledge*. See requirement `REQ-RESP-007`. +//fusa:req REQ-RESP-002 +//fusa:req REQ-RESP-003 pub fn build_error_response(request: &ByteMessageInfo, error: &RcpError) -> Option { let code = error.tc18_wire_code()?; let mut info = build_response_info(request, *request); @@ -932,8 +994,8 @@ mod tests { // ── byte_message_info ────────────────────────────────────────────────── #[test] - // fusa:test REQ-BMI-001 - // fusa:test REQ-BMI-002 + //fusa:test REQ-BMI-001 + //fusa:test REQ-BMI-002 fn byte_message_info_round_trip() { let info = sample_info(); let frame = encode_byte_message_info(&info).unwrap(); @@ -943,7 +1005,7 @@ mod tests { } #[test] - // fusa:test REQ-BMI-002 + //fusa:test REQ-BMI-002 fn byte_message_info_round_trip_zero_values() { let info = ByteMessageInfo::default(); let frame = encode_byte_message_info(&info).unwrap(); @@ -952,7 +1014,7 @@ mod tests { } #[test] - // fusa:test REQ-BMI-002 + //fusa:test REQ-BMI-002 fn byte_message_info_round_trip_max_values() { let info = ByteMessageInfo { acf_msg_type: ACF_MSG_TYPE_7BIT_MAX, @@ -979,7 +1041,7 @@ mod tests { } #[test] - // fusa:test REQ-BMI-003 + //fusa:test REQ-BMI-003 fn byte_message_info_encode_rejects_oversized_acf_msg_type() { let info = ByteMessageInfo { acf_msg_type: ACF_MSG_TYPE_7BIT_MAX + 1, @@ -989,7 +1051,7 @@ mod tests { } #[test] - // fusa:test REQ-BMI-003 + //fusa:test REQ-BMI-003 fn byte_message_info_encode_rejects_oversized_acf_msg_length() { let info = ByteMessageInfo { acf_msg_length: ACF_MSG_LENGTH_9BIT_MAX + 1, @@ -999,7 +1061,7 @@ mod tests { } #[test] - // fusa:test REQ-BMI-003 + //fusa:test REQ-BMI-003 fn byte_message_info_encode_rejects_oversized_pad() { let info = ByteMessageInfo { pad: PAD_2BIT_MAX + 1, @@ -1009,7 +1071,7 @@ mod tests { } #[test] - // fusa:test REQ-BMI-003 + //fusa:test REQ-BMI-003 fn byte_message_info_encode_rejects_oversized_byte_bus_id() { let info = ByteMessageInfo { byte_bus_id: BYTE_MESSAGE_INFO_11BIT_MAX + 1, @@ -1019,7 +1081,7 @@ mod tests { } #[test] - // fusa:test REQ-BMI-003 + //fusa:test REQ-BMI-003 fn byte_message_info_encode_rejects_oversized_sub_opcode() { let info = ByteMessageInfo { evt: Evt { @@ -1032,7 +1094,7 @@ mod tests { } #[test] - // fusa:test REQ-BMI-003 + //fusa:test REQ-BMI-003 fn byte_message_info_encode_rejects_oversized_read_size_segment() { let info = ByteMessageInfo { read_size_segment: ReadSizeOrSegment(READ_SIZE_SEGMENT_12BIT_MAX + 1), @@ -1042,7 +1104,7 @@ mod tests { } #[test] - // fusa:test REQ-BMI-004 + //fusa:test REQ-BMI-004 fn byte_message_info_decode_rejects_short_input() { assert_eq!( decode_byte_message_info(&[0u8; BYTE_MESSAGE_INFO_LEN - 1]), @@ -1051,7 +1113,7 @@ mod tests { } #[test] - // fusa:test REQ-BMI-004 + //fusa:test REQ-BMI-004 fn byte_message_info_decode_never_panics_across_lengths() { let mut state: u32 = 0xB111_5713; let mut next = || { @@ -1069,7 +1131,7 @@ mod tests { // ── op-gated read_size/segment_num selection ──────────────────────────── #[test] - // fusa:test REQ-BMI-005 + //fusa:test REQ-BMI-005 fn byte_message_info_read_size_is_some_only_when_op_is_read() { let info = ByteMessageInfo { op: false, @@ -1081,7 +1143,7 @@ mod tests { } #[test] - // fusa:test REQ-BMI-005 + //fusa:test REQ-BMI-005 fn byte_message_info_segment_num_is_some_only_when_op_is_write() { let info = ByteMessageInfo { op: true, @@ -1093,7 +1155,7 @@ mod tests { } #[test] - // fusa:test REQ-BMI-005 + //fusa:test REQ-BMI-005 fn byte_message_info_read_size_segment_num_are_mutually_exclusive_across_op() { for op in [false, true] { let info = ByteMessageInfo { @@ -1165,8 +1227,8 @@ mod tests { // ── ACF_ABB round-trip ───────────────────────────────────────────────── #[test] - // fusa:test REQ-ABB-001 - // fusa:test REQ-ABB-002 + //fusa:test REQ-ABB-001 + //fusa:test REQ-ABB-002 fn acf_abb_round_trip() { // acf_msg_type/acf_msg_length/pad are all derived/overwritten at // encode time (see encode_acf_abb's doc comment) — set them here to @@ -1188,7 +1250,7 @@ mod tests { } #[test] - // fusa:test REQ-ABB-002 + //fusa:test REQ-ABB-002 fn acf_abb_round_trip_empty_payload() { // header(8) + 0 payload = 8 -> already quadlet-aligned -> pad 0 -> // 2 quadlets. @@ -1208,7 +1270,7 @@ mod tests { } #[test] - // fusa:test REQ-ABB-002 + //fusa:test REQ-ABB-002 fn acf_abb_round_trip_large_payload() { // header(8) + 256 payload = 264 -> already quadlet-aligned -> pad 0 // -> 66 quadlets. @@ -1227,8 +1289,8 @@ mod tests { } #[test] - // fusa:test REQ-ABB-002 - // fusa:test REQ-ABB-003 + //fusa:test REQ-ABB-002 + //fusa:test REQ-ABB-003 fn acf_abb_encoded_message_has_no_timestamp_region() { // The defining Milestone 1 constraint for ACF_ABB: unlike ACF_GBB's // 64-bit message_timestamp, there must be no reserved slot for a @@ -1246,7 +1308,7 @@ mod tests { } #[test] - // fusa:test REQ-ABB-003 + //fusa:test REQ-ABB-003 fn acf_abb_encoded_message_has_expected_discriminant() { let msg = AcfAbbMessage { info: ByteMessageInfo::default(), @@ -1258,7 +1320,7 @@ mod tests { } #[test] - // fusa:test REQ-ABB-002 + //fusa:test REQ-ABB-002 fn acf_abb_encode_propagates_byte_message_info_validation_error() { // acf_msg_type/acf_msg_length/pad are always overwritten by the // derived values before this validation runs, so this uses an @@ -1276,7 +1338,7 @@ mod tests { } #[test] - // fusa:test REQ-ABB-002 + //fusa:test REQ-ABB-002 fn acf_abb_encode_rejects_payload_too_large_for_the_quadlet_field() { // (ACF_MSG_LENGTH_9BIT_MAX + 1) quadlets' worth of bytes is one // quadlet past what the 9-bit acf_msg_length field can encode. @@ -1291,13 +1353,13 @@ mod tests { // ── ACF_ABB decode rejection ──────────────────────────────────────────── #[test] - // fusa:test REQ-ABB-004 + //fusa:test REQ-ABB-004 fn acf_abb_decode_rejects_empty_input() { assert_eq!(decode_acf_abb(&[]), Err(RcpError::ShortFrame)); } #[test] - // fusa:test REQ-ABB-004 + //fusa:test REQ-ABB-004 fn acf_abb_decode_rejects_wrong_discriminant() { assert!(matches!( decode_acf_abb(&[0xFFu8; ACF_ABB_HEADER_LEN]), @@ -1306,7 +1368,7 @@ mod tests { } #[test] - // fusa:test REQ-ABB-004 + //fusa:test REQ-ABB-004 fn acf_abb_decode_rejects_gbb_discriminant_with_specific_hint() { let msg = AcfGbbMessage { info: ByteMessageInfo::default(), @@ -1322,7 +1384,7 @@ mod tests { } #[test] - // fusa:test REQ-ABB-004 + //fusa:test REQ-ABB-004 fn acf_abb_decode_rejects_truncated_byte_message_info() { let short = vec![0u8; BYTE_MESSAGE_INFO_LEN - 1]; assert_eq!(decode_acf_abb(&short), Err(RcpError::ShortFrame)); @@ -1331,7 +1393,7 @@ mod tests { // ── ACF_ABB fuzz-style: arbitrary bytes never panic ──────────────────── #[test] - // fusa:test REQ-ABB-005 + //fusa:test REQ-ABB-005 fn acf_abb_decode_never_panics_on_arbitrary_input() { let inputs: &[&[u8]] = &[&[], &[0x0E], &[0x0D], &[0xFF; 32], &[0x00; 32], &[0x0E; 64]]; for input in inputs { @@ -1340,7 +1402,7 @@ mod tests { } #[test] - // fusa:test REQ-ABB-005 + //fusa:test REQ-ABB-005 fn acf_abb_decode_never_panics_on_random_lengths() { let mut state: u32 = 0x0E0D_0E0D; let mut next = || { @@ -1414,8 +1476,8 @@ mod tests { // ── ACF_GBB round-trip ────────────────────────────────────────────────── #[test] - // fusa:test REQ-GBB-001 - // fusa:test REQ-GBB-002 + //fusa:test REQ-GBB-001 + //fusa:test REQ-GBB-002 fn acf_gbb_round_trip() { // header(16) + 5 payload = 21 -> pad 3 -> 24 total -> 6 quadlets. let msg = AcfGbbMessage { @@ -1434,7 +1496,7 @@ mod tests { } #[test] - // fusa:test REQ-GBB-002 + //fusa:test REQ-GBB-002 fn acf_gbb_round_trip_zero_and_max_timestamp() { for message_timestamp in [0u64, u64::MAX] { let msg = AcfGbbMessage { @@ -1455,7 +1517,7 @@ mod tests { } #[test] - // fusa:test REQ-GBB-002 + //fusa:test REQ-GBB-002 fn acf_gbb_round_trip_large_payload() { // header(16) + 256 payload = 272 -> already aligned -> pad 0 -> 68 // quadlets. @@ -1475,13 +1537,13 @@ mod tests { } #[test] - // fusa:test REQ-GBB-002 + //fusa:test REQ-GBB-002 fn acf_gbb_encoded_header_is_exactly_8_bytes_wider_than_acf_abb() { assert_eq!(ACF_GBB_HEADER_LEN, ACF_ABB_HEADER_LEN + 8); } #[test] - // fusa:test REQ-GBB-003 + //fusa:test REQ-GBB-003 fn acf_gbb_encoded_message_has_expected_discriminant() { let msg = AcfGbbMessage { info: ByteMessageInfo::default(), @@ -1494,7 +1556,7 @@ mod tests { } #[test] - // fusa:test REQ-GBB-002 + //fusa:test REQ-GBB-002 fn acf_gbb_encode_propagates_byte_message_info_validation_error() { let msg = AcfGbbMessage { info: ByteMessageInfo { @@ -1510,13 +1572,13 @@ mod tests { // ── ACF_GBB decode rejection ──────────────────────────────────────────── #[test] - // fusa:test REQ-GBB-004 + //fusa:test REQ-GBB-004 fn acf_gbb_decode_rejects_empty_input() { assert_eq!(decode_acf_gbb(&[]), Err(RcpError::ShortFrame)); } #[test] - // fusa:test REQ-GBB-004 + //fusa:test REQ-GBB-004 fn acf_gbb_decode_rejects_wrong_discriminant() { assert!(matches!( decode_acf_gbb(&[0xFFu8; ACF_GBB_HEADER_LEN]), @@ -1525,7 +1587,7 @@ mod tests { } #[test] - // fusa:test REQ-GBB-004 + //fusa:test REQ-GBB-004 fn acf_gbb_decode_rejects_abb_discriminant_with_specific_hint() { let msg = AcfAbbMessage { info: ByteMessageInfo::default(), @@ -1542,7 +1604,7 @@ mod tests { } #[test] - // fusa:test REQ-GBB-004 + //fusa:test REQ-GBB-004 fn acf_gbb_decode_rejects_truncated_timestamp() { // Correct discriminant and full byte_message_info, but truncated // before the 8-byte message_timestamp is complete. @@ -1555,7 +1617,7 @@ mod tests { // ── ACF_GBB fuzz-style: arbitrary bytes never panic ──────────────────── #[test] - // fusa:test REQ-GBB-005 + //fusa:test REQ-GBB-005 fn acf_gbb_decode_never_panics_on_arbitrary_input() { let inputs: &[&[u8]] = &[&[], &[0x0E], &[0x0D], &[0xFF; 32], &[0x00; 32], &[0x0D; 64]]; for input in inputs { @@ -1564,7 +1626,7 @@ mod tests { } #[test] - // fusa:test REQ-GBB-005 + //fusa:test REQ-GBB-005 fn acf_gbb_decode_never_panics_on_random_lengths() { let mut state: u32 = 0x0D0E_0D0E; let mut next = || { @@ -1582,8 +1644,8 @@ mod tests { // ── Echo-back rule ────────────────────────────────────────────────────── #[test] - // fusa:test REQ-ECHO-001 - // fusa:test REQ-ECHO-002 + //fusa:test REQ-ECHO-001 + //fusa:test REQ-ECHO-002 fn build_response_info_echoes_request_byte_bus_id_and_sets_rsp() { let request = ByteMessageInfo { byte_bus_id: 0x0123, @@ -1605,8 +1667,8 @@ mod tests { } #[test] - // fusa:test REQ-ECHO-002 - // fusa:test REQ-ECHO-003 + //fusa:test REQ-ECHO-002 + //fusa:test REQ-ECHO-003 fn build_response_info_output_passes_verify_echo_back() { let request = sample_info(); let response = build_response_info(&request, ByteMessageInfo::default()); @@ -1614,7 +1676,7 @@ mod tests { } #[test] - // fusa:test REQ-ECHO-003 + //fusa:test REQ-ECHO-003 fn verify_echo_back_accepts_matching_byte_bus_id() { let request = ByteMessageInfo { byte_bus_id: 0x0456, @@ -1629,7 +1691,7 @@ mod tests { } #[test] - // fusa:test REQ-ECHO-003 + //fusa:test REQ-ECHO-003 fn verify_echo_back_rejects_mismatched_byte_bus_id() { let request = ByteMessageInfo { byte_bus_id: 0x0001, @@ -1647,7 +1709,7 @@ mod tests { } #[test] - // fusa:test REQ-ECHO-003 + //fusa:test REQ-ECHO-003 fn verify_echo_back_ignores_rsp_flag() { // The echo-back rule this function checks is scoped to byte_bus_id // only; it deliberately does not require response.rsp to be set. @@ -1664,7 +1726,7 @@ mod tests { } #[test] - // fusa:test REQ-ECHO-004 + //fusa:test REQ-ECHO-004 fn echo_back_never_panics_across_arbitrary_field_combinations() { let mut state: u32 = 0xECC0_BACC; let mut next_u16 = || { @@ -1697,6 +1759,7 @@ mod tests { // ── Wire-level error responses ────────────────────────────────────────── #[test] + //fusa:test REQ-RESP-003 fn build_error_response_echoes_request_and_sets_err_and_rsp() { let request = ByteMessageInfo { byte_bus_id: 0x0042, @@ -1712,6 +1775,7 @@ mod tests { } #[test] + //fusa:test REQ-RESP-003 fn build_error_response_covers_all_seventeen_table_27_codes() { let request = sample_info(); let errors_and_codes: &[(RcpError, u8)] = &[ @@ -1758,6 +1822,7 @@ mod tests { } #[test] + //fusa:test REQ-RESP-003 fn build_error_response_is_a_valid_encodable_acf_abb_frame() { let request = sample_info(); let response = build_error_response(&request, &RcpError::EpNotFound).unwrap(); @@ -1767,6 +1832,238 @@ mod tests { assert_eq!(decoded.payload, vec![8]); } + #[test] + //fusa:test REQ-RESP-002 + fn build_error_response_is_always_the_timestamp_free_abb_format() { + // TC18 §11.3 (TC18.txt line 1859): "In case responses include a + // timestamp, they are in ACF_GBB format, else in ACF_ABB format." + // TC18 §11.4.2 (line 1963): "When timestamping was not requested, + // then the ACF_ABB type format without time stamp shall be default + // for any data transmitted by the RC Server." + // + // The encoded response's `acf_msg_type` must therefore be Table 15's + // literal 0x0E ("ABB message without message_timestamp"), never + // 0x0D ("GBB message, with message_timestamp"), and the encoded + // message must be exactly 8 octets shorter than the same payload + // would occupy in GBB form (Table 15's only structural difference). + let request = sample_info(); + for error in [ + RcpError::UnsupportedCmd, + RcpError::EpNotFound, + RcpError::InvalidParameter, + ] { + let response = build_error_response(&request, &error).unwrap(); + let frame = encode_acf_abb(&response).unwrap(); + let info = decode_byte_message_info(&frame[..BYTE_MESSAGE_INFO_LEN]).unwrap(); + assert_eq!(info.acf_msg_type, 0x0E, "Table 15 ABB discriminant"); + assert_ne!(info.acf_msg_type, 0x0D, "must not be the GBB form"); + + let gbb_equivalent = encode_acf_gbb(&AcfGbbMessage { + info: response.info, + message_timestamp: 0, + payload: response.payload.clone(), + }) + .unwrap(); + assert_eq!( + gbb_equivalent.len() - frame.len(), + 8, + "the ABB form omits exactly the 8-octet message_timestamp" + ); + } + } + + // ── TC18 §11.2.1 Table 4 / §11.3 Table 15 literal field values ───────── + + #[test] + //fusa:test REQ-ABB-006 + fn abb_standard_request_msg_type_matches_table_4_literal_and_bit_position() { + // TC18 §11.2.1 Table 4 (TC18.txt line 1149): "acf_msg_type + // 0x0E (ABB message)". Per this module's canonical wire layout, + // acf_msg_type occupies octet 0 bits 7:1, with acf_msg_length's MSB + // in bit 0 — so for any message whose acf_msg_length fits in 8 bits, + // octet 0 must read exactly 0x0E << 1 == 0x1C. + assert_eq!(ACF_ABB_MSG_TYPE, 0x0E); + + for payload_len in [0usize, 1, 4, 6, 7, 32] { + let msg = AcfAbbMessage { + info: ByteMessageInfo::default(), + payload: vec![0x5Au8; payload_len], + }; + let frame = encode_acf_abb(&msg).unwrap(); + assert_eq!( + frame[0], 0x1C, + "octet 0 for a {payload_len}-byte payload: 0x0E << 1" + ); + assert_eq!(frame[0] >> 1, 0x0E, "acf_msg_type recovered from bits 7:1"); + } + } + + #[test] + //fusa:test REQ-GBB-007 + fn table_15_discriminants_differ_by_exactly_the_message_timestamp() { + // TC18 §11.3 Table 15 (TC18.txt line 1863): + // acf_msg_type 0x0E - ABB message without message_timestamp + // 0x0D - GBB message, with message_timestamp + // The message_timestamp is a 64-bit field (TC18 §11.4.1, line 1955: + // "message_timestamp = ... mod 2^64"), i.e. 8 octets. + assert_eq!(ACF_ABB_MSG_TYPE, 0x0E); + assert_eq!(ACF_GBB_MSG_TYPE, 0x0D); + assert_eq!(ACF_GBB_HEADER_LEN - ACF_ABB_HEADER_LEN, 8); + + let payload = vec![0xAAu8; 4]; + let abb = encode_acf_abb(&AcfAbbMessage { + info: ByteMessageInfo::default(), + payload: payload.clone(), + }) + .unwrap(); + let gbb = encode_acf_gbb(&AcfGbbMessage { + info: ByteMessageInfo::default(), + message_timestamp: 0x0102_0304_0506_0708, + payload, + }) + .unwrap(); + assert_eq!(abb[0] >> 1, 0x0E); + assert_eq!(gbb[0] >> 1, 0x0D); + assert_eq!(gbb.len() - abb.len(), 8); + assert_eq!( + &gbb[BYTE_MESSAGE_INFO_LEN..BYTE_MESSAGE_INFO_LEN + 8], + &0x0102_0304_0506_0708u64.to_be_bytes(), + "message_timestamp sits immediately after byte_message_info" + ); + } + + #[test] + //fusa:test REQ-ABB-007 + fn both_reserved_bit_pairs_are_always_transmitted_as_zero() { + // TC18 §11.2.1 Table 4 (TC18.txt line 1153) and §11.3 Table 15 + // (line 1867) both fix `rsv` at 00b. Per this module's canonical + // wire layout the two rsv pairs are octet 2 bits 4:3 (mask 0x18) + // and octet 4 bits 3:2 (mask 0x0C). Encoding every other field at + // its own maximum must still leave both masks clear. + let info = ByteMessageInfo { + acf_msg_type: ACF_MSG_TYPE_7BIT_MAX, + acf_msg_length: ACF_MSG_LENGTH_9BIT_MAX, + pad: PAD_2BIT_MAX, + mtv: true, + byte_bus_id: BYTE_MESSAGE_INFO_11BIT_MAX, + evt: Evt { + ack: true, + sub_opcode: EVT_SUB_OPCODE_MAX, + }, + hs: true, + cs: true, + transaction_num: 0xFF, + op: true, + rsp: true, + err: true, + ms: true, + read_size_segment: ReadSizeOrSegment(READ_SIZE_SEGMENT_12BIT_MAX), + }; + let frame = encode_byte_message_info(&info).unwrap(); + assert_eq!(frame[2] & 0x18, 0x00, "row-1 rsv (octet 2 bits 4:3) = 00b"); + assert_eq!(frame[4] & 0x0C, 0x00, "row-2 rsv (octet 4 bits 3:2) = 00b"); + } + + #[test] + //fusa:test REQ-ABB-009 + fn table_4_op_polarity_selects_read_size_at_zero_and_segment_num_at_one() { + // TC18 §11.2.1 Table 4 (TC18.txt line 1163): "read_size/segment_num + // — if op = 0 this is read_size, else segment_num", with op = 0b + // meaning the sender expects a response with data (line 1160). + let read_request = ByteMessageInfo { + op: false, + read_size_segment: ReadSizeOrSegment(0x123), + ..ByteMessageInfo::default() + }; + assert_eq!(read_request.read_size(), Some(0x123)); + assert_eq!(read_request.segment_num(), None); + + let write_request = ByteMessageInfo { + op: true, + read_size_segment: ReadSizeOrSegment(0x123), + ..ByteMessageInfo::default() + }; + assert_eq!(write_request.segment_num(), Some(0x123)); + assert_eq!(write_request.read_size(), None); + } + + // ── TC18 §13.5 general evt-bit semantics ─────────────────────────────── + + #[test] + //fusa:test REQ-EVT-001 + fn evt_bit_3_is_the_acknowledge_request_bit() { + // TC18 §13.5 (TC18.txt line 3673): "evt[3] is used to request an + // acknowledge. I.e. evt[3]=1 requests acknowledge." + // The evt nibble occupies octet 4 bits 7:4, so evt[3] is octet 4 + // bit 7 — mask 0x80. + let with_ack = encode_byte_message_info(&ByteMessageInfo { + evt: Evt { + ack: true, + sub_opcode: 0, + }, + ..ByteMessageInfo::default() + }) + .unwrap(); + assert_eq!(with_ack[4], 0x80, "evt[3]=1, evt[2:0]=000b"); + + let without_ack = encode_byte_message_info(&ByteMessageInfo { + evt: Evt { + ack: false, + sub_opcode: 0, + }, + ..ByteMessageInfo::default() + }) + .unwrap(); + assert_eq!(without_ack[4], 0x00, "evt[3]=0, evt[2:0]=000b"); + + // And it survives the round trip in both directions. + assert!(decode_byte_message_info(&with_ack).unwrap().evt.ack); + assert!(!decode_byte_message_info(&without_ack).unwrap().evt.ack); + } + + #[test] + //fusa:test REQ-EVT-002 + fn evt_bits_2_to_0_are_the_three_bit_payload_usage_sub_opcode() { + // TC18 §13.5 (TC18.txt line 3672): "event bits evt[2:0] are used to + // control the usage of the byte_msg_payload." Table 30's own rows + // range over 000b..=111b, i.e. exactly three bits. + assert_eq!(EVT_SUB_OPCODE_MAX, 0x07); + + // evt[2:0] occupies octet 4 bits 6:4, so a sub_opcode of `n` with + // evt[3] clear must encode to octet 4 == n << 4. + for sub_opcode in 0u8..=EVT_SUB_OPCODE_MAX { + let frame = encode_byte_message_info(&ByteMessageInfo { + evt: Evt { + ack: false, + sub_opcode, + }, + ..ByteMessageInfo::default() + }) + .unwrap(); + assert_eq!( + frame[4], + sub_opcode << 4, + "evt[2:0] = {sub_opcode:03b} at octet 4 bits 6:4" + ); + let decoded = decode_byte_message_info(&frame).unwrap(); + assert_eq!(decoded.evt.sub_opcode, sub_opcode); + assert!(!decoded.evt.ack, "evt[3] must not bleed into evt[2:0]"); + } + + // A 4-bit value must be rejected, not silently truncated into + // evt[3]'s acknowledge bit. + assert_eq!( + encode_byte_message_info(&ByteMessageInfo { + evt: Evt { + ack: false, + sub_opcode: EVT_SUB_OPCODE_MAX + 1, + }, + ..ByteMessageInfo::default() + }), + Err(RcpError::InvalidSize) + ); + } + // ── Golden vectors: TC18 Figure 19 / Figure 20 worked examples ────────── // // Moved to `crate::e2e` (`finalize_crc_trailer_matches_figure_19_worked_example`/ diff --git a/src/adapt.rs b/src/adapt.rs index c165b26..84b3fab 100644 --- a/src/adapt.rs +++ b/src/adapt.rs @@ -1,14 +1,14 @@ -// fusa:req REQ-ADAPT-001 -// fusa:req REQ-ADAPT-002 -// fusa:req REQ-ADAPT-003 -// fusa:req REQ-ADAPT-004 -// fusa:req REQ-ADAPT-005 -// fusa:req REQ-ADAPT-006 -// fusa:req REQ-ADAPT-007 -// fusa:req REQ-ADAPT-008 -// fusa:req REQ-ADAPT-009 -// fusa:req REQ-ADAPT-010 -// fusa:req REQ-ADAPT-011 +//fusa:req REQ-ADAPT-001 +//fusa:req REQ-ADAPT-002 +//fusa:req REQ-ADAPT-003 +//fusa:req REQ-ADAPT-004 +//fusa:req REQ-ADAPT-005 +//fusa:req REQ-ADAPT-006 +//fusa:req REQ-ADAPT-007 +//fusa:req REQ-ADAPT-008 +//fusa:req REQ-ADAPT-009 +//fusa:req REQ-ADAPT-010 +//fusa:req REQ-ADAPT-011 //! Adapter layer — converts between RCP and external protocol representations. //! @@ -118,7 +118,7 @@ use crate::RcpError; /// Converts between an external message format `M` and endpoint payload /// bytes. -// fusa:req REQ-ADAPT-001 +//fusa:req REQ-ADAPT-001 pub trait Adapter: Send + Sync { /// Convert an external message to endpoint write-payload bytes. fn to_write_payload(&self, msg: M) -> Result, RcpError>; @@ -130,7 +130,7 @@ pub trait Adapter: Send + Sync { /// Endpoint wrapper that adapts an external message type `M` to/from /// endpoint payload bytes. -// fusa:req REQ-ADAPT-002 +//fusa:req REQ-ADAPT-002 pub struct AdaptEndpoint { inner: Arc, adapter: Arc>, @@ -145,7 +145,7 @@ impl AdaptEndpoint { /// to `read_size` bytes and adapt the response — see this module's doc /// comment for why this is a write-then-read round trip rather than a /// single `send`-shaped call. - // fusa:req REQ-ADAPT-003 + //fusa:req REQ-ADAPT-003 pub fn send_msg(&self, msg: M, read_size: u16) -> Result { let payload = self.adapter.to_write_payload(msg)?; self.inner.write(&payload)?; @@ -157,7 +157,7 @@ impl AdaptEndpoint { // ── Passthrough adapter ─────────────────────────────────────────────────────── /// Identity adapter for `Vec` → `Vec` testing. -// fusa:req REQ-ADAPT-004 +//fusa:req REQ-ADAPT-004 pub struct PassthroughAdapter; impl Adapter> for PassthroughAdapter { @@ -185,7 +185,7 @@ const ENDPOINT_ID_SEP: char = '.'; /// 16 lowercase hex digits, zero-padded so [`parse_endpoint_id`] never has /// to guess where it ends; `byte_bus_id` follows in plain decimal, needing /// no padding since [`ENDPOINT_ID_SEP`] already marks where it starts. -// fusa:req REQ-ADAPT-011 +//fusa:req REQ-ADAPT-011 pub fn format_endpoint_id(stream_id: StreamId, byte_bus_id: u16) -> String { format!( "{:016x}{}{}", @@ -202,7 +202,7 @@ pub fn format_endpoint_id(stream_id: StreamId, byte_bus_id: u16) -> String { /// [`ENDPOINT_ID_SEP`]-separated pair of a 16-hex-digit `stream_id` and a /// decimal `byte_bus_id` in `0..=u16::MAX`. Never panics on malformed /// input. -// fusa:req REQ-ADAPT-011 +//fusa:req REQ-ADAPT-011 pub fn parse_endpoint_id(id: &str) -> Result<(StreamId, u16), RcpError> { let (sid_hex, bus_dec) = id .split_once(ENDPOINT_ID_SEP) @@ -239,7 +239,7 @@ pub fn parse_endpoint_id(id: &str) -> Result<(StreamId, u16), RcpError> { /// analog for (`evt`, `hs`, `cs`, `transaction_num`, `ms`, `pad`, `mtv`) is /// left at its zero default; [`crate::mock::RcServer::handle_abb`]'s /// dispatch logic does not consult any of them. -// fusa:req REQ-ADAPT-007 +//fusa:req REQ-ADAPT-007 pub fn from_message(msg: &Message) -> Result<(StreamId, AcfAbbMessage), RcpError> { let (stream_id, byte_bus_id) = parse_endpoint_id(&msg.id)?; let op = match msg.meta.get("rcp.op").map(String::as_str) { @@ -279,7 +279,7 @@ pub fn from_message(msg: &Message) -> Result<(StreamId, AcfAbbMessage), RcpError /// (`"write"`/`"read"`), mirroring [`from_message`]'s own request-side key, /// so a caller can confirm which operation the RC Server actually /// performed. -// fusa:req REQ-ADAPT-006 +//fusa:req REQ-ADAPT-006 pub fn to_message(stream_id: StreamId, resp: &AcfAbbMessage) -> Message { let mut meta = std::collections::BTreeMap::new(); meta.insert( @@ -305,7 +305,7 @@ pub fn to_message(stream_id: StreamId, resp: &AcfAbbMessage) -> Message { /// share one addressed-response conversion (see this module's provenance /// note on why the retired `Status`/`Response` split collapsed to one /// shape). -// fusa:req REQ-ADAPT-008 +//fusa:req REQ-ADAPT-008 pub fn response_to_message(stream_id: StreamId, resp: &AcfAbbMessage) -> Message { to_message(stream_id, resp) } @@ -446,7 +446,7 @@ impl crate::relay::Caller for RcpAdapter { /// path — flagged per Guiding Principle 5 rather than asserted as a /// closed question, since that absence has not been exhaustively /// confirmed against the rest of the spec. - // fusa:req REQ-ADAPT-010 + //fusa:req REQ-ADAPT-010 async fn call(&self, ctx: Context, req: Message) -> Result { if self.closed.load(Ordering::SeqCst) { return Err(crate::relay::Error::Closed); @@ -500,8 +500,8 @@ mod tests { } #[test] - // fusa:test REQ-ADAPT-001 - // fusa:test REQ-ADAPT-004 + //fusa:test REQ-ADAPT-001 + //fusa:test REQ-ADAPT-004 fn passthrough_adapter_identity() { let ep = AdaptEndpoint::new(ok_endpoint(), Arc::new(PassthroughAdapter)); let out = ep.send_msg(b"hi".to_vec(), 8).unwrap(); @@ -509,7 +509,7 @@ mod tests { } #[test] - // fusa:test REQ-ADAPT-002 + //fusa:test REQ-ADAPT-002 fn ep_type_forwarded() { let inner = ok_endpoint(); let ep = AdaptEndpoint::new(Arc::clone(&inner), Arc::new(PassthroughAdapter)); @@ -517,7 +517,7 @@ mod tests { } #[test] - // fusa:test REQ-ADAPT-003 + //fusa:test REQ-ADAPT-003 fn adapter_error_propagated() { struct FailAdapter; impl Adapter> for FailAdapter { @@ -534,7 +534,7 @@ mod tests { } #[test] - // fusa:test REQ-ADAPT-005 + //fusa:test REQ-ADAPT-005 fn passthrough_preserves_payload() { let ep = AdaptEndpoint::new(ok_endpoint(), Arc::new(PassthroughAdapter)); let out = ep.send_msg(b"data".to_vec(), 8).unwrap(); @@ -544,7 +544,7 @@ mod tests { // ── endpoint-id encoding ─────────────────────────────────────────────── #[test] - // fusa:test REQ-ADAPT-011 + //fusa:test REQ-ADAPT-011 fn endpoint_id_roundtrips() { let sid = stream(0x1234); let encoded = format_endpoint_id(sid, 0x07FF); @@ -554,7 +554,7 @@ mod tests { } #[test] - // fusa:test REQ-ADAPT-011 + //fusa:test REQ-ADAPT-011 fn parse_endpoint_id_rejects_malformed_input() { assert_eq!( parse_endpoint_id("not-an-address").unwrap_err(), @@ -573,7 +573,7 @@ mod tests { // ── to_message / from_message / response_to_message (§15.7.5) ──────────── #[test] - // fusa:test REQ-ADAPT-006 + //fusa:test REQ-ADAPT-006 fn to_message_maps_address_op_and_payload() { let sid = stream(7); let resp = AcfAbbMessage { @@ -592,7 +592,7 @@ mod tests { } #[test] - // fusa:test REQ-ADAPT-007 + //fusa:test REQ-ADAPT-007 fn from_message_defaults_op_from_payload_emptiness() { let write_msg = Message::new(Protocol::Rcp, format_endpoint_id(stream(1), 3), vec![0xAA]); let (_, write_req) = from_message(&write_msg).unwrap(); @@ -604,7 +604,7 @@ mod tests { } #[test] - // fusa:test REQ-ADAPT-007 + //fusa:test REQ-ADAPT-007 fn from_message_honors_explicit_op_and_read_size_meta() { let mut msg = Message::new(Protocol::Rcp, format_endpoint_id(stream(1), 3), vec![]); msg.meta.insert("rcp.op".to_string(), "read".to_string()); @@ -618,7 +618,7 @@ mod tests { } #[test] - // fusa:test REQ-ADAPT-007 + //fusa:test REQ-ADAPT-007 fn from_message_rejects_unknown_op_value() { let mut msg = Message::new(Protocol::Rcp, format_endpoint_id(stream(1), 3), vec![]); msg.meta @@ -627,14 +627,14 @@ mod tests { } #[test] - // fusa:test REQ-ADAPT-007 + //fusa:test REQ-ADAPT-007 fn from_message_rejects_malformed_id() { let msg = Message::new(Protocol::Rcp, "not-an-address", vec![]); assert_eq!(from_message(&msg).unwrap_err(), RcpError::InvalidParameter); } #[test] - // fusa:test REQ-ADAPT-008 + //fusa:test REQ-ADAPT-008 fn response_to_message_matches_to_message() { let sid = stream(2); let resp = AcfAbbMessage { @@ -655,9 +655,9 @@ mod tests { // ── adapt() (§10.3) ──────────────────────────────────────────────────── #[tokio::test] - // fusa:test REQ-ADAPT-009 - // fusa:test REQ-ADAPT-010 - // fusa:test REQ-RELAY-008 + //fusa:test REQ-ADAPT-009 + //fusa:test REQ-ADAPT-010 + //fusa:test REQ-RELAY-008 async fn adapt_call_write_then_read_round_trips_payload() { let sid = stream(11); let server = server_with_endpoint(sid, 4, vec![0u8; 8]); @@ -679,7 +679,7 @@ mod tests { } #[tokio::test] - // fusa:test REQ-ADAPT-009 + //fusa:test REQ-ADAPT-009 async fn adapt_send_discards_response() { let sid = stream(12); let server = server_with_endpoint(sid, 4, vec![0u8; 8]); @@ -691,7 +691,7 @@ mod tests { } #[tokio::test] - // fusa:test REQ-ADAPT-009 + //fusa:test REQ-ADAPT-009 async fn adapt_send_invalid_address_is_not_connected() { let sid = stream(13); let server = server_with_endpoint(sid, 4, vec![0u8; 8]); @@ -704,7 +704,7 @@ mod tests { } #[tokio::test] - // fusa:test REQ-ADAPT-009 + //fusa:test REQ-ADAPT-009 async fn adapt_call_unknown_endpoint_is_not_connected() { let sid = stream(14); let server = server_with_endpoint(sid, 4, vec![0u8; 8]); @@ -716,7 +716,7 @@ mod tests { } #[tokio::test] - // fusa:test REQ-ADAPT-009 + //fusa:test REQ-ADAPT-009 async fn adapt_call_already_expired_context_is_timeout() { let sid = stream(15); let server = server_with_endpoint(sid, 4, vec![0u8; 8]); @@ -729,7 +729,7 @@ mod tests { } #[tokio::test] - // fusa:test REQ-ADAPT-009 + //fusa:test REQ-ADAPT-009 async fn adapt_subscribe_returns_immediately_closed_channel() { let sid = stream(16); let server = server_with_endpoint(sid, 4, vec![0u8; 8]); @@ -741,7 +741,7 @@ mod tests { } #[tokio::test] - // fusa:test REQ-ADAPT-009 + //fusa:test REQ-ADAPT-009 async fn adapt_close_then_call_is_closed() { let sid = stream(17); let server = server_with_endpoint(sid, 4, vec![0u8; 8]); @@ -754,8 +754,8 @@ mod tests { } #[test] - // fusa:test REQ-ADAPT-009 - // fusa:test REQ-RELAY-007 + //fusa:test REQ-ADAPT-009 + //fusa:test REQ-RELAY-007 fn adapt_protocol_is_rcp() { let sid = stream(18); let server = server_with_endpoint(sid, 4, vec![0u8; 8]); diff --git a/src/adc.rs b/src/adc.rs index daeddcc..fe62701 100644 --- a/src/adc.rs +++ b/src/adc.rs @@ -1,13 +1,13 @@ -// fusa:req REQ-ADC-001 -// fusa:req REQ-ADC-002 -// fusa:req REQ-ADC-003 -// fusa:req REQ-ADC-004 -// fusa:req REQ-ADC-005 -// fusa:req REQ-ADC-006 -// fusa:req REQ-ADC-007 -// fusa:req REQ-ADC-008 -// fusa:req REQ-ADC-009 -// fusa:req REQ-ADC-010 +//fusa:req REQ-ADC-001 +//fusa:req REQ-ADC-002 +//fusa:req REQ-ADC-003 +//fusa:req REQ-ADC-004 +//fusa:req REQ-ADC-005 +//fusa:req REQ-ADC-006 +//fusa:req REQ-ADC-007 +//fusa:req REQ-ADC-008 +//fusa:req REQ-ADC-009 +//fusa:req REQ-ADC-010 //! The ADC endpoint type (`ep_type 0x09`) — `ROADMAP.md` Milestone 4 //! ("Basic Endpoint Types"), fifth checklist bullet: "≤16-bit resolution; @@ -174,7 +174,7 @@ use crate::RcpError; /// type rather than this module assuming every sample is a full 16-bit /// value. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// fusa:req REQ-ADC-001 +//fusa:req REQ-ADC-001 pub struct AdcResolutionBits(u8); impl AdcResolutionBits { @@ -184,8 +184,8 @@ impl AdcResolutionBits { /// meaningful resolution) or any value above `16` (wider than /// `ROADMAP.md`'s "≤16-bit resolution" upper bound). Never panics for /// any input. - // fusa:req REQ-ADC-001 - // fusa:req REQ-ADC-002 + //fusa:req REQ-ADC-001 + //fusa:req REQ-ADC-002 pub fn new(bits: u8) -> Result { if (1..=16).contains(&bits) { Ok(Self(bits)) @@ -195,7 +195,7 @@ impl AdcResolutionBits { } /// This resolution's bit width, `1..=16`. - // fusa:req REQ-ADC-001 + //fusa:req REQ-ADC-001 pub fn to_u8(self) -> u8 { self.0 } @@ -205,7 +205,7 @@ impl AdcResolutionBits { /// /// Never panics for any valid [`AdcResolutionBits`] — the widest /// modeled resolution, 16 bits, yields `u16::MAX` exactly. - // fusa:req REQ-ADC-003 + //fusa:req REQ-ADC-003 pub fn max_raw_value(self) -> u16 { let bits = u32::from(self.0); ((1u32 << bits) - 1) as u16 @@ -232,7 +232,7 @@ impl Default for AdcResolutionBits { /// regardless of `resolution`, matching [`crate::gpio::GpioBitmask`]'s own /// fixed-width, big-endian discipline. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// fusa:req REQ-ADC-004 +//fusa:req REQ-ADC-004 pub struct AdcSampleValue { /// The raw sample reading. pub raw: u16, @@ -246,7 +246,7 @@ impl AdcSampleValue { /// /// Returns `Err(RcpError::InvalidParameter)` if `raw` exceeds that /// maximum. Never panics for any input. - // fusa:req REQ-ADC-004 + //fusa:req REQ-ADC-004 pub fn new(raw: u16, resolution: AdcResolutionBits) -> Result { if raw > resolution.max_raw_value() { Err(RcpError::InvalidParameter) @@ -256,7 +256,7 @@ impl AdcSampleValue { } /// Encode this sample to its 2-byte big-endian wire representation. - // fusa:req REQ-ADC-004 + //fusa:req REQ-ADC-004 pub fn encode(&self) -> [u8; 2] { self.raw.to_be_bytes() } @@ -267,7 +267,7 @@ impl AdcSampleValue { /// Returns `Err(RcpError::InvalidParameter)` if the decoded raw value /// exceeds `resolution`'s [`AdcResolutionBits::max_raw_value`]. Never /// panics for any input. - // fusa:req REQ-ADC-004 + //fusa:req REQ-ADC-004 pub fn decode(bytes: [u8; 2], resolution: AdcResolutionBits) -> Result { Self::new(u16::from_be_bytes(bytes), resolution) } @@ -283,7 +283,7 @@ impl AdcSampleValue { /// averaging chain" for each field's unconfirmed width/units and for how /// the two resolving functions connect them. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-ADC-005 +//fusa:req REQ-ADC-005 pub struct AdcAveragingConfig { /// How often one raw sample is taken, in this crate's own unconfirmed /// tick-count units. @@ -303,7 +303,7 @@ pub struct AdcAveragingConfig { /// this is a dedicated type rather than content added directly to /// [`crate::regmap::PerEpTypeFunctionalConfig`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-ADC-005 +//fusa:req REQ-ADC-005 pub struct AdcFunctionalConfig { /// This endpoint's configured sample resolution. pub resolution: AdcResolutionBits, @@ -319,7 +319,7 @@ impl AdcFunctionalConfig { /// This module does not itself call that function — it only shows how a /// caller would obtain the matching tag, per this module's doc comment /// "Relationship to `crate::regmap`". - // fusa:req REQ-ADC-006 + //fusa:req REQ-ADC-006 pub fn layer_tag(&self) -> crate::regmap::PerEpTypeFunctionalConfig { crate::regmap::PerEpTypeFunctionalConfig::new(crate::regmap::EndpointType::Adc) } @@ -341,7 +341,7 @@ impl AdcFunctionalConfig { /// protects a future field-width revision from silently wrapping instead. /// See this module's doc comment "Provenance note: the three-level /// averaging chain". -// fusa:req REQ-ADC-007 +//fusa:req REQ-ADC-007 pub fn resolve_adc_sample_window_ticks(averaging: &AdcAveragingConfig) -> Option { let raw_samples_needed = u64::from(averaging.adc_avg_intervals_per_request) .checked_mul(u64::from(averaging.adc_combine_avg_values))?; @@ -363,8 +363,8 @@ pub fn resolve_adc_sample_window_ticks(averaging: &AdcAveragingConfig) -> Option /// arithmetic even though the `u16 * u16` product this function multiplies /// cannot overflow `usize` on any platform this crate targets — if that /// product would overflow. Never panics for any input. -// fusa:req REQ-ADC-008 -// fusa:req REQ-ADC-009 +//fusa:req REQ-ADC-008 +//fusa:req REQ-ADC-009 pub fn resolve_adc_averaged_value( raw_samples: &[u16], averaging: &AdcAveragingConfig, @@ -403,7 +403,7 @@ pub fn resolve_adc_averaged_value( /// only" for why [`AdcSamplingMode::Continuous`] exists here at all despite /// this endpoint type never actually running one. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// fusa:req REQ-ADC-010 +//fusa:req REQ-ADC-010 pub enum AdcSamplingMode { /// One sample (or one fully-combined result) per request — the only /// mode `ROADMAP.md`'s ADC checklist bullet allows. @@ -429,7 +429,7 @@ impl Default for AdcSamplingMode { /// "Provenance note: request-driven sampling only" for why this crate reads /// the checklist's unstated violation code onto this already-defined /// variant. Never panics for any input. -// fusa:req REQ-ADC-010 +//fusa:req REQ-ADC-010 pub fn validate_adc_sample_request(mode: AdcSamplingMode) -> Result<(), RcpError> { match mode { AdcSamplingMode::RequestDriven => Ok(()), @@ -444,7 +444,7 @@ mod tests { // ── AdcResolutionBits: construction / round trip ───────────────────────── #[test] - // fusa:test REQ-ADC-001 + //fusa:test REQ-ADC-001 fn adc_resolution_bits_round_trips_through_new_to_u8_for_the_full_1_to_16_range() { for bits in 1u8..=16 { let resolution = AdcResolutionBits::new(bits).unwrap(); @@ -453,7 +453,7 @@ mod tests { } #[test] - // fusa:test REQ-ADC-002 + //fusa:test REQ-ADC-002 fn adc_resolution_bits_new_rejects_zero_and_above_sixteen() { for bits in [0u8, 17, 32, 255] { assert_eq!( @@ -464,7 +464,7 @@ mod tests { } #[test] - // fusa:test REQ-ADC-002 + //fusa:test REQ-ADC-002 fn adc_resolution_bits_new_never_panics_for_any_sampled_input() { for bits in [0u8, 1, 8, 16, 17, 128, 255] { let _ = AdcResolutionBits::new(bits); @@ -472,7 +472,7 @@ mod tests { } #[test] - // fusa:test REQ-ADC-003 + //fusa:test REQ-ADC-003 fn adc_resolution_bits_max_raw_value_is_two_pow_bits_minus_one() { for bits in 1u8..=16 { let resolution = AdcResolutionBits::new(bits).unwrap(); @@ -487,7 +487,7 @@ mod tests { } #[test] - // fusa:test REQ-ADC-003 + //fusa:test REQ-ADC-003 fn adc_resolution_bits_default_is_sixteen_bits() { assert_eq!(AdcResolutionBits::default().to_u8(), 16); assert_eq!(AdcResolutionBits::default().max_raw_value(), u16::MAX); @@ -496,7 +496,7 @@ mod tests { // ── AdcSampleValue: construction / round trip ──────────────────────────── #[test] - // fusa:test REQ-ADC-004 + //fusa:test REQ-ADC-004 fn adc_sample_value_round_trips_through_encode_decode_within_resolution() { let resolution = AdcResolutionBits::new(12).unwrap(); for raw in [0u16, 1, 2048, resolution.max_raw_value()] { @@ -507,7 +507,7 @@ mod tests { } #[test] - // fusa:test REQ-ADC-004 + //fusa:test REQ-ADC-004 fn adc_sample_value_new_rejects_raw_wider_than_resolution() { let resolution = AdcResolutionBits::new(8).unwrap(); assert_eq!( @@ -522,7 +522,7 @@ mod tests { } #[test] - // fusa:test REQ-ADC-004 + //fusa:test REQ-ADC-004 fn adc_sample_value_decode_never_panics_for_any_sampled_input() { for resolution_bits in [1u8, 8, 16] { let resolution = AdcResolutionBits::new(resolution_bits).unwrap(); @@ -535,7 +535,7 @@ mod tests { // ── AdcFunctionalConfig / layer_tag ────────────────────────────────────── #[test] - // fusa:test REQ-ADC-005 + //fusa:test REQ-ADC-005 fn adc_functional_config_default_uses_default_resolution_and_zeroed_averaging() { let config = AdcFunctionalConfig::default(); assert_eq!(config.resolution, AdcResolutionBits::default()); @@ -546,7 +546,7 @@ mod tests { } #[test] - // fusa:test REQ-ADC-006 + //fusa:test REQ-ADC-006 fn adc_functional_config_layer_tag_matches_ep_type_adc() { let functional = AdcFunctionalConfig::default(); let generic = crate::regmap::PerEpConfigBlock::new(crate::regmap::EndpointType::Adc); @@ -562,7 +562,7 @@ mod tests { } #[test] - // fusa:test REQ-ADC-006 + //fusa:test REQ-ADC-006 fn adc_functional_config_layer_tag_rejects_mismatched_ep_type() { let functional = AdcFunctionalConfig::default(); let generic = crate::regmap::PerEpConfigBlock::new(crate::regmap::EndpointType::Uart); @@ -575,7 +575,7 @@ mod tests { // ── resolve_adc_sample_window_ticks: the timing chain ──────────────────── #[test] - // fusa:test REQ-ADC-007 + //fusa:test REQ-ADC-007 fn resolve_adc_sample_window_ticks_multiplies_all_three_fields() { let averaging = AdcAveragingConfig { adc_sample_interval: 10, @@ -587,7 +587,7 @@ mod tests { } #[test] - // fusa:test REQ-ADC-007 + //fusa:test REQ-ADC-007 fn resolve_adc_sample_window_ticks_does_not_overflow_at_max_field_widths() { // With AdcAveragingConfig's current u16/u16/u32 field widths, the // full product never exceeds u64::MAX (see this function's doc @@ -603,7 +603,7 @@ mod tests { } #[test] - // fusa:test REQ-ADC-007 + //fusa:test REQ-ADC-007 fn resolve_adc_sample_window_ticks_never_panics_for_any_sampled_input() { let intervals = [0u32, 1, 1000, u32::MAX]; let counts = [0u16, 1, 255, u16::MAX]; @@ -624,7 +624,7 @@ mod tests { // ── resolve_adc_averaged_value: the value chain ────────────────────────── #[test] - // fusa:test REQ-ADC-008 + //fusa:test REQ-ADC-008 fn resolve_adc_averaged_value_computes_the_two_stage_average() { let averaging = AdcAveragingConfig { adc_sample_interval: 1, @@ -638,7 +638,7 @@ mod tests { } #[test] - // fusa:test REQ-ADC-008 + //fusa:test REQ-ADC-008 fn resolve_adc_averaged_value_ignores_trailing_samples_beyond_what_the_pipeline_needs() { let averaging = AdcAveragingConfig { adc_sample_interval: 1, @@ -654,7 +654,7 @@ mod tests { } #[test] - // fusa:test REQ-ADC-008 + //fusa:test REQ-ADC-008 fn resolve_adc_averaged_value_single_sample_groups_return_the_samples_own_value() { let averaging = AdcAveragingConfig { adc_sample_interval: 1, @@ -665,7 +665,7 @@ mod tests { } #[test] - // fusa:test REQ-ADC-009 + //fusa:test REQ-ADC-009 fn resolve_adc_averaged_value_rejects_zero_avg_intervals_per_request() { let averaging = AdcAveragingConfig { adc_sample_interval: 1, @@ -679,7 +679,7 @@ mod tests { } #[test] - // fusa:test REQ-ADC-009 + //fusa:test REQ-ADC-009 fn resolve_adc_averaged_value_rejects_zero_combine_avg_values() { let averaging = AdcAveragingConfig { adc_sample_interval: 1, @@ -693,7 +693,7 @@ mod tests { } #[test] - // fusa:test REQ-ADC-009 + //fusa:test REQ-ADC-009 fn resolve_adc_averaged_value_rejects_insufficient_raw_samples() { let averaging = AdcAveragingConfig { adc_sample_interval: 1, @@ -708,7 +708,7 @@ mod tests { } #[test] - // fusa:test REQ-ADC-009 + //fusa:test REQ-ADC-009 fn resolve_adc_averaged_value_rejects_insufficient_samples_at_max_field_widths() { // group_size * group_count at u16::MAX/u16::MAX does not actually // overflow usize (see this function's doc comment) — this exercises @@ -726,7 +726,7 @@ mod tests { } #[test] - // fusa:test REQ-ADC-009 + //fusa:test REQ-ADC-009 fn resolve_adc_averaged_value_never_panics_for_any_sampled_input() { let averagings = [ AdcAveragingConfig { @@ -756,13 +756,13 @@ mod tests { // ── AdcSamplingMode / validate_adc_sample_request ──────────────────────── #[test] - // fusa:test REQ-ADC-010 + //fusa:test REQ-ADC-010 fn adc_sampling_mode_defaults_to_request_driven() { assert_eq!(AdcSamplingMode::default(), AdcSamplingMode::RequestDriven); } #[test] - // fusa:test REQ-ADC-010 + //fusa:test REQ-ADC-010 fn validate_adc_sample_request_accepts_request_driven() { assert_eq!( validate_adc_sample_request(AdcSamplingMode::RequestDriven), @@ -771,7 +771,7 @@ mod tests { } #[test] - // fusa:test REQ-ADC-010 + //fusa:test REQ-ADC-010 fn validate_adc_sample_request_rejects_continuous() { assert_eq!( validate_adc_sample_request(AdcSamplingMode::Continuous), diff --git a/src/addressing.rs b/src/addressing.rs index a1c39b2..dab08cb 100644 --- a/src/addressing.rs +++ b/src/addressing.rs @@ -1,7 +1,7 @@ -// fusa:req REQ-EPLK-001 -// fusa:req REQ-EPLK-002 -// fusa:req REQ-EPLK-003 -// fusa:req REQ-EPLK-004 +//fusa:req REQ-EPLK-001 +//fusa:req REQ-EPLK-002 +//fusa:req REQ-EPLK-003 +//fusa:req REQ-EPLK-004 //! `(stream_id, byte_bus_id)` → endpoint lookup — TC18 wire format core //! (`ROADMAP.md` Milestone 1, "Addressing" subsection). @@ -73,7 +73,7 @@ use std::collections::HashMap; /// something hashable/comparable to resolve a `(stream_id, byte_bus_id)` /// pair to — it does not need to know what an endpoint actually is. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -// fusa:req REQ-EPLK-001 +//fusa:req REQ-EPLK-001 pub struct EndpointId(pub u32); /// A stream-scoped `(stream_id, byte_bus_id)` → [`EndpointId`] lookup table. @@ -81,7 +81,7 @@ pub struct EndpointId(pub u32); /// See the module doc comment for why `byte_bus_id`'s uniqueness is /// enforced per-[`StreamId`] rather than across the whole table. #[derive(Debug, Clone, Default)] -// fusa:req REQ-EPLK-001 +//fusa:req REQ-EPLK-001 pub struct EndpointTable { streams: HashMap>, } @@ -102,7 +102,7 @@ impl EndpointTable { /// silently overwritten. The same `byte_bus_id` may be registered /// independently under any number of *different* `stream_id`s, since /// uniqueness here is stream-relative, not global. - // fusa:req REQ-EPLK-002 + //fusa:req REQ-EPLK-002 pub fn register( &mut self, stream_id: StreamId, @@ -125,8 +125,8 @@ impl EndpointTable { /// /// A `byte_bus_id` registered under one `stream_id` is never visible /// under any other `stream_id` — both must match exactly. - // fusa:req REQ-EPLK-003 - // fusa:req REQ-EPLK-004 + //fusa:req REQ-EPLK-003 + //fusa:req REQ-EPLK-004 pub fn lookup(&self, stream_id: StreamId, byte_bus_id: u16) -> Option { self.streams.get(&stream_id)?.get(&byte_bus_id).copied() } @@ -146,9 +146,9 @@ mod tests { // ── Round-trip ───────────────────────────────────────────────────────── #[test] - // fusa:test REQ-EPLK-001 - // fusa:test REQ-EPLK-002 - // fusa:test REQ-EPLK-003 + //fusa:test REQ-EPLK-001 + //fusa:test REQ-EPLK-002 + //fusa:test REQ-EPLK-003 fn register_then_lookup_resolves_the_same_endpoint() { let mut table = EndpointTable::new(); let sid = stream(1); @@ -157,7 +157,7 @@ mod tests { } #[test] - // fusa:test REQ-EPLK-002 + //fusa:test REQ-EPLK-002 fn register_accepts_byte_bus_id_at_11bit_max() { let mut table = EndpointTable::new(); let sid = stream(1); @@ -173,8 +173,8 @@ mod tests { // ── Stream-relative (not global) uniqueness ───────────────────────────── #[test] - // fusa:test REQ-EPLK-002 - // fusa:test REQ-EPLK-003 + //fusa:test REQ-EPLK-002 + //fusa:test REQ-EPLK-003 fn same_byte_bus_id_under_two_streams_resolves_independently() { let mut table = EndpointTable::new(); let sid_a = stream(1); @@ -190,7 +190,7 @@ mod tests { } #[test] - // fusa:test REQ-EPLK-003 + //fusa:test REQ-EPLK-003 fn lookup_does_not_leak_across_streams() { let mut table = EndpointTable::new(); let sid_a = stream(1); @@ -202,7 +202,7 @@ mod tests { // ── Explicit ambiguity flagging ────────────────────────────────────────── #[test] - // fusa:test REQ-EPLK-002 + //fusa:test REQ-EPLK-002 fn register_rejects_duplicate_pair_without_overwriting() { let mut table = EndpointTable::new(); let sid = stream(1); @@ -214,7 +214,7 @@ mod tests { } #[test] - // fusa:test REQ-EPLK-002 + //fusa:test REQ-EPLK-002 fn register_rejects_oversized_byte_bus_id() { let mut table = EndpointTable::new(); let sid = stream(1); @@ -226,7 +226,7 @@ mod tests { // ── Fuzz-style: arbitrary lookups never panic ─────────────────────────── #[test] - // fusa:test REQ-EPLK-004 + //fusa:test REQ-EPLK-004 fn lookup_never_panics_on_empty_or_populated_table() { let empty = EndpointTable::new(); let sweep: &[(StreamId, u16)] = &[ diff --git a/src/admin.rs b/src/admin.rs index 9bc9eec..4b22d35 100644 --- a/src/admin.rs +++ b/src/admin.rs @@ -1,8 +1,8 @@ -// fusa:req REQ-ADMIN-001 -// fusa:req REQ-ADMIN-002 -// fusa:req REQ-ADMIN-003 -// fusa:req REQ-ADMIN-004 -// fusa:req REQ-ADMIN-005 +//fusa:req REQ-ADMIN-001 +//fusa:req REQ-ADMIN-002 +//fusa:req REQ-ADMIN-003 +//fusa:req REQ-ADMIN-004 +//fusa:req REQ-ADMIN-005 //! Administrative interface: discovered-peer health/staleness reporting //! and diagnostic info. @@ -42,7 +42,7 @@ use crate::RcpError; // ── AdminServer ─────────────────────────────────────────────────────────────── /// Provides administrative diagnostics over a discovery cache of peers. -// fusa:req REQ-ADMIN-001 +//fusa:req REQ-ADMIN-001 pub struct AdminServer { cache: Arc>, started: SystemTime, @@ -61,7 +61,7 @@ impl AdminServer { } /// Increment the admin request counter (call once per admin endpoint hit). - // fusa:req REQ-ADMIN-002 + //fusa:req REQ-ADMIN-002 pub fn record_request(&self) { self.req_count.fetch_add(1, Ordering::Relaxed); } @@ -72,7 +72,7 @@ impl AdminServer { } /// Uptime since the admin server was created. - // fusa:req REQ-ADMIN-003 + //fusa:req REQ-ADMIN-003 pub fn uptime(&self) -> Duration { self.started.elapsed().unwrap_or(Duration::ZERO) } @@ -85,14 +85,14 @@ impl AdminServer { /// "non-empty" for this coarse check. A caller that already knows /// which `StreamId`s to probe should use [`Self::is_peer_healthy`] /// instead, which does apply a real staleness check. - // fusa:req REQ-ADMIN-004 + //fusa:req REQ-ADMIN-004 pub fn is_healthy(&self) -> bool { !self.cache.lock().unwrap().is_empty() } /// True if `stream_id`'s cached entry is known and not stale as of /// `now` under `max_age`. - // fusa:req REQ-ADMIN-004 + //fusa:req REQ-ADMIN-004 pub fn is_peer_healthy( &self, stream_id: crate::avtp::StreamId, @@ -111,7 +111,7 @@ impl AdminServer { /// /// See this module's doc comment for why this narrows from "closes the /// registry" to "invalidate what's cached." - // fusa:req REQ-ADMIN-005 + //fusa:req REQ-ADMIN-005 pub fn shutdown_peer(&self, stream_id: crate::avtp::StreamId) -> Result<(), RcpError> { self.shutdown.store(true, Ordering::SeqCst); self.cache.lock().unwrap().invalidate(stream_id); @@ -147,22 +147,22 @@ mod tests { } #[test] - // fusa:test REQ-ADMIN-001 - // fusa:test REQ-ADMIN-004 + //fusa:test REQ-ADMIN-001 + //fusa:test REQ-ADMIN-004 fn healthy_with_populated_cache() { let (a, _) = admin_with_peer(); assert!(a.is_healthy()); } #[test] - // fusa:test REQ-ADMIN-004 + //fusa:test REQ-ADMIN-004 fn unhealthy_with_empty_cache() { let a = AdminServer::new(Arc::new(Mutex::new(DiscoveryCache::new()))); assert!(!a.is_healthy()); } #[test] - // fusa:test REQ-ADMIN-002 + //fusa:test REQ-ADMIN-002 fn request_count_increments() { let (a, _) = admin_with_peer(); for _ in 0..5 { @@ -172,14 +172,14 @@ mod tests { } #[test] - // fusa:test REQ-ADMIN-003 + //fusa:test REQ-ADMIN-003 fn uptime_is_non_negative() { let (a, _) = admin_with_peer(); assert!(a.uptime() >= Duration::ZERO); } #[test] - // fusa:test REQ-ADMIN-005 + //fusa:test REQ-ADMIN-005 fn shutdown_invalidates_peer() { let (a, sid) = admin_with_peer(); a.shutdown_peer(sid).unwrap(); @@ -188,7 +188,7 @@ mod tests { } #[test] - // fusa:test REQ-ADMIN-004 + //fusa:test REQ-ADMIN-004 fn peer_count_matches_cache() { let (a, _) = admin_with_peer(); assert_eq!(a.peer_count(), 1); diff --git a/src/authz.rs b/src/authz.rs index 5f93747..f8cecd6 100644 --- a/src/authz.rs +++ b/src/authz.rs @@ -1,10 +1,10 @@ -// fusa:req REQ-AUTHZ-001 -// fusa:req REQ-AUTHZ-002 -// fusa:req REQ-AUTHZ-003 -// fusa:req REQ-AUTHZ-004 -// fusa:req REQ-AUTHZ-005 -// fusa:req REQ-AUTHZ-006 -// fusa:req REQ-AUTHZ-007 +//fusa:req REQ-AUTHZ-001 +//fusa:req REQ-AUTHZ-002 +//fusa:req REQ-AUTHZ-003 +//fusa:req REQ-AUTHZ-004 +//fusa:req REQ-AUTHZ-005 +//fusa:req REQ-AUTHZ-006 +//fusa:req REQ-AUTHZ-007 //! Authorization policy enforcement over an [`Endpoint`]. //! @@ -38,7 +38,7 @@ use crate::RcpError; /// Authorization policy for an endpoint: an allowlist of /// `(ep_type, is_write)` pairs. Empty = deny all. -// fusa:req REQ-AUTHZ-001 +//fusa:req REQ-AUTHZ-001 #[derive(Clone, Debug, Default)] pub struct Policy { pub allowed: HashSet<(u8, bool)>, @@ -50,7 +50,7 @@ const ALL_EP_TYPE_BYTES: std::ops::RangeInclusive = 0x01..=0x0D; impl Policy { /// Allow every recognized endpoint type, for both reads and writes. - // fusa:req REQ-AUTHZ-002 + //fusa:req REQ-AUTHZ-002 pub fn allow_all() -> Self { let mut set = HashSet::new(); for b in ALL_EP_TYPE_BYTES { @@ -61,7 +61,7 @@ impl Policy { } /// Deny everything (closed policy). - // fusa:req REQ-AUTHZ-003 + //fusa:req REQ-AUTHZ-003 pub fn deny_all() -> Self { Policy::default() } @@ -74,7 +74,7 @@ impl Policy { // ── AuthzEndpoint ────────────────────────────────────────────────────────────── /// Policy-enforcing endpoint wrapper. -// fusa:req REQ-AUTHZ-004 +//fusa:req REQ-AUTHZ-004 pub struct AuthzEndpoint { inner: Arc, policy: RwLock, @@ -89,7 +89,7 @@ impl AuthzEndpoint { } /// Replace the active policy atomically. - // fusa:req REQ-AUTHZ-006 + //fusa:req REQ-AUTHZ-006 pub fn set_policy(&self, policy: Policy) { *self.policy.write().unwrap() = policy; } @@ -105,7 +105,7 @@ impl Endpoint for AuthzEndpoint { self.inner.ep_type() } - // fusa:req REQ-AUTHZ-005 + //fusa:req REQ-AUTHZ-005 fn read(&self, read_size: u16) -> Result, RcpError> { let ep_type = self.inner.ep_type(); if !self.policy.read().unwrap().is_allowed(ep_type, false) { @@ -114,8 +114,8 @@ impl Endpoint for AuthzEndpoint { self.inner.read(read_size) } - // fusa:req REQ-AUTHZ-005 - // fusa:req REQ-AUTHZ-007 + //fusa:req REQ-AUTHZ-005 + //fusa:req REQ-AUTHZ-007 fn write(&self, payload: &[u8]) -> Result<(), RcpError> { let ep_type = self.inner.ep_type(); if !self.policy.read().unwrap().is_allowed(ep_type, true) { @@ -138,9 +138,9 @@ mod tests { } #[test] - // fusa:test REQ-AUTHZ-002 - // fusa:test REQ-AUTHZ-004 - // fusa:test REQ-AUTHZ-005 + //fusa:test REQ-AUTHZ-002 + //fusa:test REQ-AUTHZ-004 + //fusa:test REQ-AUTHZ-005 fn allow_all_permits_any_call() { let a = AuthzEndpoint::new(inner(EndpointType::Gpio), Policy::allow_all()); a.write(b"x").unwrap(); @@ -148,8 +148,8 @@ mod tests { } #[test] - // fusa:test REQ-AUTHZ-003 - // fusa:test REQ-AUTHZ-005 + //fusa:test REQ-AUTHZ-003 + //fusa:test REQ-AUTHZ-005 fn deny_all_blocks_every_call() { let a = AuthzEndpoint::new(inner(EndpointType::Gpio), Policy::deny_all()); let err = a.read(4).unwrap_err(); @@ -158,8 +158,8 @@ mod tests { } #[test] - // fusa:test REQ-AUTHZ-001 - // fusa:test REQ-AUTHZ-005 + //fusa:test REQ-AUTHZ-001 + //fusa:test REQ-AUTHZ-005 fn partial_allowlist_enforced_by_request_type() { let mut set = HashSet::new(); set.insert((EndpointType::Gpio.to_u8(), false)); // reads only @@ -172,7 +172,7 @@ mod tests { } #[test] - // fusa:test REQ-AUTHZ-001 + //fusa:test REQ-AUTHZ-001 fn partial_allowlist_enforced_by_ep_type() { let mut set = HashSet::new(); set.insert((EndpointType::Adc.to_u8(), false)); @@ -184,7 +184,7 @@ mod tests { } #[test] - // fusa:test REQ-AUTHZ-006 + //fusa:test REQ-AUTHZ-006 fn set_policy_takes_effect_immediately() { let a = AuthzEndpoint::new(inner(EndpointType::Gpio), Policy::deny_all()); a.write(b"x").unwrap_err(); @@ -193,7 +193,7 @@ mod tests { } #[test] - // fusa:test REQ-AUTHZ-007 + //fusa:test REQ-AUTHZ-007 fn ep_type_forwarded() { let a = AuthzEndpoint::new(inner(EndpointType::Gpio), Policy::allow_all()); assert_eq!(a.ep_type(), EndpointType::Gpio); diff --git a/src/avtp.rs b/src/avtp.rs index 41edc02..2ddbef5 100644 --- a/src/avtp.rs +++ b/src/avtp.rs @@ -1,25 +1,25 @@ -// fusa:req REQ-NTSCF-001 -// fusa:req REQ-NTSCF-002 -// fusa:req REQ-NTSCF-003 -// fusa:req REQ-NTSCF-004 -// fusa:req REQ-NTSCF-005 -// fusa:req REQ-NTSCF-006 -// fusa:req REQ-TSCF-001 -// fusa:req REQ-TSCF-002 -// fusa:req REQ-TSCF-003 -// fusa:req REQ-TSCF-004 -// fusa:req REQ-TSCF-005 -// fusa:req REQ-TSCF-006 -// fusa:req REQ-HVSEL-001 -// fusa:req REQ-HVSEL-002 -// fusa:req REQ-HVSEL-003 -// fusa:req REQ-HVSEL-004 -// fusa:req REQ-HVSEL-005 -// fusa:req REQ-SID-001 -// fusa:req REQ-SID-002 -// fusa:req REQ-SID-003 -// fusa:req REQ-SID-004 -// fusa:req REQ-SID-005 +//fusa:req REQ-NTSCF-001 +//fusa:req REQ-NTSCF-002 +//fusa:req REQ-NTSCF-003 +//fusa:req REQ-NTSCF-004 +//fusa:req REQ-NTSCF-005 +//fusa:req REQ-NTSCF-006 +//fusa:req REQ-TSCF-001 +//fusa:req REQ-TSCF-002 +//fusa:req REQ-TSCF-003 +//fusa:req REQ-TSCF-004 +//fusa:req REQ-TSCF-005 +//fusa:req REQ-TSCF-006 +//fusa:req REQ-HVSEL-001 +//fusa:req REQ-HVSEL-002 +//fusa:req REQ-HVSEL-003 +//fusa:req REQ-HVSEL-004 +//fusa:req REQ-HVSEL-005 +//fusa:req REQ-SID-001 +//fusa:req REQ-SID-002 +//fusa:req REQ-SID-003 +//fusa:req REQ-SID-004 +//fusa:req REQ-SID-005 //! IEEE 1722 AVTPDU framing — TC18 wire format core (`ROADMAP.md` Milestone 1). //! @@ -172,7 +172,7 @@ pub const NTSCF_DATA_LENGTH_MAX: u16 = 0x07FF; /// interpret or construct its sender-MAC/unique-id-suffix structure; see /// this module's doc comment for why the field remains untyped here. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-NTSCF-001 +//fusa:req REQ-NTSCF-001 pub struct NtscfHeader { /// Per-stream sequence number, incremented once per NTSCF AVTPDU sent. pub sequence_num: u8, @@ -212,7 +212,7 @@ fn get_u64_be(b: &[u8]) -> u64 { /// /// Returns `Err(RcpError::InvalidSize)` if `ntscf_data_length` exceeds the /// 11-bit field width. -// fusa:req REQ-NTSCF-002 +//fusa:req REQ-NTSCF-002 pub fn encode_ntscf_header(hdr: &NtscfHeader) -> Result<[u8; NTSCF_HEADER_LEN], RcpError> { if hdr.ntscf_data_length > NTSCF_DATA_LENGTH_MAX { return Err(RcpError::InvalidSize); @@ -233,9 +233,9 @@ pub fn encode_ntscf_header(hdr: &NtscfHeader) -> Result<[u8; NTSCF_HEADER_LEN], /// /// Never panics on short, truncated, or arbitrary input — always returns /// `Err` instead. -// fusa:req REQ-NTSCF-003 -// fusa:req REQ-NTSCF-004 -// fusa:req REQ-NTSCF-006 +//fusa:req REQ-NTSCF-003 +//fusa:req REQ-NTSCF-004 +//fusa:req REQ-NTSCF-006 pub fn decode_ntscf_header(b: &[u8]) -> Result { if b.len() < NTSCF_HEADER_LEN { return Err(RcpError::ShortFrame); @@ -302,7 +302,7 @@ pub const TSCF_DATA_LENGTH_MAX: u16 = 0xFFFF; /// 64-bit value, same as [`NtscfHeader::stream_id`] — see that field's doc /// comment for how to decompose/compose it via [`StreamId`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-TSCF-001 +//fusa:req REQ-TSCF-001 pub struct TscfHeader { /// Per-stream sequence number, incremented once per TSCF AVTPDU sent. pub sequence_num: u8, @@ -355,7 +355,7 @@ pub struct TscfHeader { /// the struct can hold is representable. The `Result` return type is kept /// for signature symmetry with [`encode_ntscf_header`] and so that adding /// a future validation rule is not itself a breaking change. -// fusa:req REQ-TSCF-002 +//fusa:req REQ-TSCF-002 pub fn encode_tscf_header(hdr: &TscfHeader) -> Result<[u8; TSCF_HEADER_LEN], RcpError> { let mut buf = [0u8; TSCF_HEADER_LEN]; buf[0] = TSCF_SUBTYPE; @@ -375,9 +375,9 @@ pub fn encode_tscf_header(hdr: &TscfHeader) -> Result<[u8; TSCF_HEADER_LEN], Rcp /// /// Never panics on short, truncated, or arbitrary input — always returns /// `Err` instead. -// fusa:req REQ-TSCF-003 -// fusa:req REQ-TSCF-004 -// fusa:req REQ-TSCF-006 +//fusa:req REQ-TSCF-003 +//fusa:req REQ-TSCF-004 +//fusa:req REQ-TSCF-006 pub fn decode_tscf_header(b: &[u8]) -> Result { if b.len() < TSCF_HEADER_LEN { return Err(RcpError::ShortFrame); @@ -423,7 +423,7 @@ pub fn decode_tscf_header(b: &[u8]) -> Result { /// this type exists purely to make the header-variant selection rule below /// testable against both outcomes. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// fusa:req REQ-HVSEL-001 +//fusa:req REQ-HVSEL-001 pub enum TimeSyncCapability { /// The server participates in time synchronization; TSCF-headed /// AVTPDUs may be decoded. @@ -443,7 +443,7 @@ impl TimeSyncCapability { /// A decoded AVTPDU header, tagged by which Milestone 1 header variant /// produced it. Returned by [`select_header_variant`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// fusa:req REQ-HVSEL-001 +//fusa:req REQ-HVSEL-001 pub enum HeaderVariant { /// Decoded via [`decode_ntscf_header`]. Ntscf(NtscfHeader), @@ -468,10 +468,18 @@ pub enum HeaderVariant { /// a subtype that is neither [`NTSCF_SUBTYPE`] nor [`TSCF_SUBTYPE`]. All /// other decode-rejection paths (short frame past the subtype byte, sv bit, /// ...) are delegated to [`decode_ntscf_header`]/[`decode_tscf_header`]. -// fusa:req REQ-HVSEL-002 -// fusa:req REQ-HVSEL-003 -// fusa:req REQ-HVSEL-004 -// fusa:req REQ-HVSEL-005 +/// TC18 §12.8.2 "Reception of frames" (TC18.txt line 3170) states the +/// discard rule this function's `other =>` arm implements: "If neither a +/// gPTP nor an IEEE1722 frame is found the received frame shall be +/// discarded." Only the two subtypes RCP itself defines — NTSCF +/// ([`NTSCF_SUBTYPE`], 0x82) and TSCF ([`TSCF_SUBTYPE`], 0x05) — are +/// accepted; every other leading subtype octet is rejected without any +/// attempt to interpret the bytes that follow it. +//fusa:req REQ-HVSEL-002 +//fusa:req REQ-HVSEL-003 +//fusa:req REQ-HVSEL-004 +//fusa:req REQ-HVSEL-005 +//fusa:req REQ-NET-002 pub fn select_header_variant( b: &[u8], time_sync: TimeSyncCapability, @@ -503,7 +511,7 @@ pub fn select_header_variant( /// [`build_stream_id`]. See the module's provenance note for the bit-layout /// caveat that applies to both directions. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)] -// fusa:req REQ-SID-001 +//fusa:req REQ-SID-001 pub struct StreamId { /// The stream's sender's 48-bit MAC address, occupying the upper 48 /// bits of the wire `stream_id` (see the module's provenance note). @@ -516,7 +524,7 @@ pub struct StreamId { impl StreamId { /// Compose a [`StreamId`] from its sender-MAC and unique-id parts. - // fusa:req REQ-SID-001 + //fusa:req REQ-SID-001 pub fn new(sender_mac: [u8; 6], unique_id: u16) -> Self { Self { sender_mac, @@ -526,7 +534,7 @@ impl StreamId { /// Compose the opaque 64-bit wire `stream_id` value from this /// [`StreamId`]'s parts. Equivalent to [`build_stream_id`]. - // fusa:req REQ-SID-001 + //fusa:req REQ-SID-001 pub fn to_u64(self) -> u64 { build_stream_id(self.sender_mac, self.unique_id) } @@ -535,7 +543,7 @@ impl StreamId { /// [`StreamId`]. Equivalent to [`parse_stream_id`]. Infallible: every /// `u64` value — including all-zero and all-`0xFF` — maps to exactly /// one [`StreamId`]. - // fusa:req REQ-SID-001 + //fusa:req REQ-SID-001 pub fn from_u64(raw: u64) -> Self { let (sender_mac, unique_id) = parse_stream_id(raw); Self::new(sender_mac, unique_id) @@ -560,7 +568,7 @@ impl From for StreamId { /// upper 48 bits, `unique_id` the lower 16. /// /// Infallible — every `([u8; 6], u16)` pair maps to exactly one `u64`. -// fusa:req REQ-SID-001 +//fusa:req REQ-SID-001 pub fn build_stream_id(sender_mac: [u8; 6], unique_id: u16) -> u64 { let mac_bits = (u64::from(sender_mac[0]) << 56) | (u64::from(sender_mac[1]) << 48) @@ -577,7 +585,7 @@ pub fn build_stream_id(sender_mac: [u8; 6], unique_id: u16) -> u64 { /// Never panics: a `u64` always carries exactly the 64 bits this function /// reads, so there is no truncated-input case to reject the way the header /// decoders above do. -// fusa:req REQ-SID-001 +//fusa:req REQ-SID-001 pub fn parse_stream_id(raw: u64) -> ([u8; 6], u16) { let sender_mac = [ (raw >> 56) as u8, @@ -604,10 +612,20 @@ pub fn parse_stream_id(raw: u64) -> ([u8; 6], u16) { /// `Err(RcpError::InvalidSize)` if that length exceeds /// [`NTSCF_DATA_LENGTH_MAX`] (the same 11-bit field width /// [`encode_ntscf_header`] itself enforces). -// fusa:req REQ-WIRE-001 -// fusa:req REQ-WIRE-002 -// fusa:req REQ-WIRE-004 -// fusa:req REQ-WIRE-007 +/// This is also the only whole-frame composition entry point this crate +/// has, and it always emits an NTSCF header — matching TC18 §11.1 +/// (TC18.txt line 1062: the NTSCF header "shall be used for RCP requests +/// which shall be executed as soon as possible or under request specific +/// conditions as well as for responses and acknowledge") and §11.4.3 +/// (TC18.txt line 1988: "The RC Server always uses NTSCF header"). There is +/// deliberately no `encode_tscf_frame` counterpart: [`encode_tscf_header`] +/// exists for a client's own use and for round-trip testing, but nothing in +/// this crate assembles a TSCF-headed frame. +//fusa:req REQ-WIRE-001 +//fusa:req REQ-WIRE-002 +//fusa:req REQ-WIRE-004 +//fusa:req REQ-WIRE-007 +//fusa:req REQ-NTSCF-007 pub fn encode_ntscf_frame( stream_id: StreamId, sequence_num: u8, @@ -635,10 +653,10 @@ pub fn encode_ntscf_frame( /// input: delegates directly to [`decode_ntscf_header`] for that case, then /// returns whatever trailing bytes remain (including zero of them) /// verbatim. -// fusa:req REQ-WIRE-003 -// fusa:req REQ-WIRE-005 -// fusa:req REQ-WIRE-008 -// fusa:req REQ-WIRE-009 +//fusa:req REQ-WIRE-003 +//fusa:req REQ-WIRE-005 +//fusa:req REQ-WIRE-008 +//fusa:req REQ-WIRE-009 pub fn decode_ntscf_frame(b: &[u8]) -> Result<(NtscfHeader, &[u8]), RcpError> { let hdr = decode_ntscf_header(b)?; Ok((hdr, &b[NTSCF_HEADER_LEN..])) @@ -654,9 +672,9 @@ mod tests { // ── Round-trip ───────────────────────────────────────────────────────── #[test] - // fusa:test REQ-NTSCF-001 - // fusa:test REQ-NTSCF-002 - // fusa:test REQ-NTSCF-003 + //fusa:test REQ-NTSCF-001 + //fusa:test REQ-NTSCF-002 + //fusa:test REQ-NTSCF-003 fn ntscf_header_round_trip() { let hdr = NtscfHeader { sequence_num: 0x42, @@ -670,7 +688,7 @@ mod tests { } #[test] - // fusa:test REQ-NTSCF-002 + //fusa:test REQ-NTSCF-002 fn ntscf_header_round_trip_zero_values() { let hdr = NtscfHeader { sequence_num: 0, @@ -683,7 +701,7 @@ mod tests { } #[test] - // fusa:test REQ-NTSCF-002 + //fusa:test REQ-NTSCF-002 fn ntscf_header_round_trip_max_values() { let hdr = NtscfHeader { sequence_num: 0xFF, @@ -696,7 +714,7 @@ mod tests { } #[test] - // fusa:test REQ-NTSCF-002 + //fusa:test REQ-NTSCF-002 fn encode_rejects_oversized_data_length() { let hdr = NtscfHeader { ntscf_data_length: NTSCF_DATA_LENGTH_MAX + 1, @@ -706,7 +724,7 @@ mod tests { } #[test] - // fusa:test REQ-NTSCF-002 + //fusa:test REQ-NTSCF-002 fn encoded_header_has_expected_subtype_and_sv_bit() { let frame = encode_ntscf_header(&NtscfHeader::default()).unwrap(); assert_eq!(frame[0], NTSCF_SUBTYPE); @@ -716,7 +734,7 @@ mod tests { // ── Decode rejection ────────────────────────────────────────────────── #[test] - // fusa:test REQ-NTSCF-004 + //fusa:test REQ-NTSCF-004 fn decode_rejects_short_input() { for len in 0..NTSCF_HEADER_LEN { let buf = vec![0u8; len]; @@ -725,7 +743,7 @@ mod tests { } #[test] - // fusa:test REQ-NTSCF-004 + //fusa:test REQ-NTSCF-004 fn decode_rejects_wrong_subtype() { let mut frame = encode_ntscf_header(&NtscfHeader::default()).unwrap(); frame[0] = TSCF_SUBTYPE; // the other RCP subtype, not NTSCF @@ -736,7 +754,7 @@ mod tests { } #[test] - // fusa:test REQ-NTSCF-004 + //fusa:test REQ-NTSCF-004 fn decode_rejects_sv_bit_unset() { let mut frame = encode_ntscf_header(&NtscfHeader::default()).unwrap(); frame[1] &= 0x7F; // clear sv @@ -747,7 +765,7 @@ mod tests { } #[test] - // fusa:test REQ-NTSCF-004 + //fusa:test REQ-NTSCF-004 fn decode_ignores_reserved_bits() { let hdr = NtscfHeader { sequence_num: 7, @@ -769,8 +787,8 @@ mod tests { // ── Fuzz-style: arbitrary bytes never panic ─────────────────────────── #[test] - // fusa:test REQ-NTSCF-005 - // fusa:test REQ-NTSCF-006 + //fusa:test REQ-NTSCF-005 + //fusa:test REQ-NTSCF-006 fn decode_never_panics_on_arbitrary_input() { let inputs: &[&[u8]] = &[ &[], @@ -790,7 +808,7 @@ mod tests { } #[test] - // fusa:test REQ-NTSCF-006 + //fusa:test REQ-NTSCF-006 fn decode_never_panics_on_random_lengths() { // Deterministic pseudo-random coverage across many lengths/contents, // matching wire.rs's fuzz-style discipline without adding a @@ -815,9 +833,9 @@ mod tests { // ── Round-trip ───────────────────────────────────────────────────────── #[test] - // fusa:test REQ-TSCF-001 - // fusa:test REQ-TSCF-002 - // fusa:test REQ-TSCF-003 + //fusa:test REQ-TSCF-001 + //fusa:test REQ-TSCF-002 + //fusa:test REQ-TSCF-003 fn tscf_header_round_trip() { let hdr = TscfHeader { sequence_num: 0x42, @@ -832,7 +850,7 @@ mod tests { } #[test] - // fusa:test REQ-TSCF-002 + //fusa:test REQ-TSCF-002 fn tscf_header_round_trip_zero_values() { let hdr = TscfHeader { sequence_num: 0, @@ -846,7 +864,7 @@ mod tests { } #[test] - // fusa:test REQ-TSCF-002 + //fusa:test REQ-TSCF-002 fn tscf_header_round_trip_max_values() { let hdr = TscfHeader { sequence_num: 0xFF, @@ -860,7 +878,7 @@ mod tests { } #[test] - // fusa:test REQ-TSCF-002 + //fusa:test REQ-TSCF-002 fn tscf_accepts_the_full_16_bit_data_length_range() { // TC18 v0.5.1_RC §11.1 Figure 5 (p.22) gives `stream_data_length` // its own 16-bit half-quadlet, so — unlike NTSCF's 11-bit @@ -880,7 +898,7 @@ mod tests { } #[test] - // fusa:test REQ-TSCF-002 + //fusa:test REQ-TSCF-002 fn tscf_encoded_header_has_expected_subtype_and_sv_bit() { let frame = encode_tscf_header(&TscfHeader::default()).unwrap(); assert_eq!(frame[0], TSCF_SUBTYPE); @@ -890,7 +908,7 @@ mod tests { // ── Wire layout, pinned to the specification's worked examples ──────── #[test] - // fusa:test REQ-NTSCF-003 + //fusa:test REQ-NTSCF-003 fn ntscf_header_matches_figure_20_worked_example() { // TC18 v0.5.1_RC page 79, Figure 20 ("Single ACF_GBB CRC32 // mandatory fields"). Its AVTPDU header row reads, left to right: @@ -925,7 +943,7 @@ mod tests { } #[test] - // fusa:test REQ-TSCF-003 + //fusa:test REQ-TSCF-003 fn tscf_header_matches_figure_19_worked_example() { // TC18 v0.5.1_RC page 79, Figure 19 ("single ACF_ABB CRC32 // mandatory fields"). Its AVTPDU header rows read: @@ -960,7 +978,7 @@ mod tests { } #[test] - // fusa:test REQ-TSCF-003 + //fusa:test REQ-TSCF-003 fn tscf_tv_bit_tracks_avtp_timestamp_presence() { // TC18 v0.5.1_RC §11.1 Figure 5 (p.22) bit 15, `tv`. See // `encode_tscf_header` for why this crate derives the bit from @@ -985,7 +1003,7 @@ mod tests { } #[test] - // fusa:test REQ-TSCF-002 + //fusa:test REQ-TSCF-002 fn tscf_and_ntscf_headers_use_distinct_subtypes() { assert_ne!(TSCF_SUBTYPE, NTSCF_SUBTYPE); } @@ -993,7 +1011,7 @@ mod tests { // ── Decode rejection ────────────────────────────────────────────────── #[test] - // fusa:test REQ-TSCF-004 + //fusa:test REQ-TSCF-004 fn tscf_decode_rejects_short_input() { for len in 0..TSCF_HEADER_LEN { let buf = vec![0u8; len]; @@ -1002,7 +1020,7 @@ mod tests { } #[test] - // fusa:test REQ-TSCF-004 + //fusa:test REQ-TSCF-004 fn tscf_decode_rejects_wrong_subtype() { let mut frame = encode_tscf_header(&TscfHeader::default()).unwrap(); frame[0] = NTSCF_SUBTYPE; // NTSCF subtype, not TSCF @@ -1013,7 +1031,7 @@ mod tests { } #[test] - // fusa:test REQ-TSCF-004 + //fusa:test REQ-TSCF-004 fn tscf_decode_rejects_sv_bit_unset() { let mut frame = encode_tscf_header(&TscfHeader::default()).unwrap(); frame[1] &= 0x7F; // clear sv @@ -1024,7 +1042,7 @@ mod tests { } #[test] - // fusa:test REQ-TSCF-004 + //fusa:test REQ-TSCF-004 fn tscf_decode_ignores_reserved_bits() { let hdr = TscfHeader { sequence_num: 7, @@ -1052,8 +1070,8 @@ mod tests { // ── Fuzz-style: arbitrary bytes never panic ─────────────────────────── #[test] - // fusa:test REQ-TSCF-005 - // fusa:test REQ-TSCF-006 + //fusa:test REQ-TSCF-005 + //fusa:test REQ-TSCF-006 fn tscf_decode_never_panics_on_arbitrary_input() { let inputs: &[&[u8]] = &[ &[], @@ -1076,7 +1094,7 @@ mod tests { } #[test] - // fusa:test REQ-TSCF-006 + //fusa:test REQ-TSCF-006 fn tscf_decode_never_panics_on_random_lengths() { // Deterministic pseudo-random coverage across many lengths/contents, // matching wire.rs's fuzz-style discipline without adding a @@ -1099,14 +1117,14 @@ mod tests { // ═══════════════════════════════════════════════════════════════════ #[test] - // fusa:test REQ-HVSEL-001 + //fusa:test REQ-HVSEL-001 fn time_sync_capability_accepts_tscf_only_when_capable() { assert!(TimeSyncCapability::Capable.accepts_tscf()); assert!(!TimeSyncCapability::Incapable.accepts_tscf()); } #[test] - // fusa:test REQ-HVSEL-004 + //fusa:test REQ-HVSEL-004 fn select_header_variant_accepts_ntscf_when_time_sync_capable() { let hdr = NtscfHeader { sequence_num: 3, @@ -1119,7 +1137,7 @@ mod tests { } #[test] - // fusa:test REQ-HVSEL-004 + //fusa:test REQ-HVSEL-004 fn select_header_variant_accepts_ntscf_when_time_sync_incapable() { // NTSCF carries no timing assumption: it is accepted regardless of // the receiving server's time-sync capability. @@ -1134,7 +1152,7 @@ mod tests { } #[test] - // fusa:test REQ-HVSEL-003 + //fusa:test REQ-HVSEL-003 fn select_header_variant_accepts_tscf_when_time_sync_capable() { let hdr = TscfHeader { sequence_num: 9, @@ -1148,7 +1166,7 @@ mod tests { } #[test] - // fusa:test REQ-HVSEL-002 + //fusa:test REQ-HVSEL-002 fn select_header_variant_drops_tscf_when_time_sync_incapable() { let hdr = TscfHeader { sequence_num: 9, @@ -1164,7 +1182,7 @@ mod tests { } #[test] - // fusa:test REQ-HVSEL-002 + //fusa:test REQ-HVSEL-002 fn select_header_variant_drops_tscf_before_decoding_body() { // Even a TSCF frame that would otherwise fail to decode (too short // past the subtype byte) must still be reported as the time-sync @@ -1178,7 +1196,7 @@ mod tests { } #[test] - // fusa:test REQ-HVSEL-005 + //fusa:test REQ-HVSEL-005 fn select_header_variant_rejects_empty_input() { assert_eq!( select_header_variant(&[], TimeSyncCapability::Capable), @@ -1191,7 +1209,7 @@ mod tests { } #[test] - // fusa:test REQ-HVSEL-005 + //fusa:test REQ-HVSEL-005 fn select_header_variant_rejects_unrecognized_subtype() { let frame = [0x01u8; TSCF_HEADER_LEN]; assert!(matches!( @@ -1201,8 +1219,8 @@ mod tests { } #[test] - // fusa:test REQ-HVSEL-002 - // fusa:test REQ-HVSEL-003 + //fusa:test REQ-HVSEL-002 + //fusa:test REQ-HVSEL-003 fn select_header_variant_propagates_tscf_decode_errors_when_capable() { // Wrong subtype byte inside an otherwise TSCF-length frame should // still surface as a genuine decode error (not the time-sync @@ -1220,7 +1238,7 @@ mod tests { } #[test] - // fusa:test REQ-HVSEL-005 + //fusa:test REQ-HVSEL-005 fn select_header_variant_never_panics_on_arbitrary_input() { let mut state: u32 = 0x2468_ACE0; let mut next = || { @@ -1241,7 +1259,7 @@ mod tests { // ═══════════════════════════════════════════════════════════════════ #[test] - // fusa:test REQ-SID-002 + //fusa:test REQ-SID-002 fn stream_id_round_trip() { let mac = [0x00, 0x11, 0x22, 0x33, 0x44, 0x55]; let unique_id = 0xBEEF; @@ -1252,7 +1270,7 @@ mod tests { } #[test] - // fusa:test REQ-SID-002 + //fusa:test REQ-SID-002 fn stream_id_round_trip_zero_values() { let raw = build_stream_id([0; 6], 0); assert_eq!(raw, 0); @@ -1260,7 +1278,7 @@ mod tests { } #[test] - // fusa:test REQ-SID-002 + //fusa:test REQ-SID-002 fn stream_id_round_trip_max_values() { let raw = build_stream_id([0xFF; 6], u16::MAX); assert_eq!(raw, u64::MAX); @@ -1268,8 +1286,8 @@ mod tests { } #[test] - // fusa:test REQ-SID-001 - // fusa:test REQ-SID-002 + //fusa:test REQ-SID-001 + //fusa:test REQ-SID-002 fn stream_id_type_round_trips_through_new_to_u64_from_u64() { let id = StreamId::new([0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF], 0x1234); let raw = id.to_u64(); @@ -1280,7 +1298,7 @@ mod tests { } #[test] - // fusa:test REQ-SID-003 + //fusa:test REQ-SID-003 fn stream_id_places_sender_mac_in_upper_48_bits() { // Each MAC octet must land at a distinct, predictable byte position // (upper 48 bits, most-significant octet first), independent of @@ -1298,14 +1316,14 @@ mod tests { } #[test] - // fusa:test REQ-SID-003 + //fusa:test REQ-SID-003 fn stream_id_places_unique_id_in_lower_16_bits() { let raw = build_stream_id([0; 6], 0x1234); assert_eq!(raw, 0x0000_0000_0000_1234); } #[test] - // fusa:test REQ-SID-004 + //fusa:test REQ-SID-004 fn parse_stream_id_never_panics_across_arbitrary_u64_values() { // parse_stream_id/build_stream_id operate on fixed-width integers // and fixed-size arrays only, so there is no truncated/malformed @@ -1329,7 +1347,7 @@ mod tests { } #[test] - // fusa:test REQ-SID-005 + //fusa:test REQ-SID-005 fn stream_id_interoperates_with_ntscf_header_opaque_field() { let id = StreamId::new([0x02, 0x42, 0xAC, 0x11, 0x00, 0x02], 0x0007); let hdr = NtscfHeader { @@ -1343,7 +1361,7 @@ mod tests { } #[test] - // fusa:test REQ-SID-005 + //fusa:test REQ-SID-005 fn stream_id_interoperates_with_tscf_header_opaque_field() { let id = StreamId::new([0x02, 0x42, 0xAC, 0x11, 0x00, 0x03], 0x0008); let hdr = TscfHeader { @@ -1362,11 +1380,11 @@ mod tests { // ═══════════════════════════════════════════════════════════════════ #[test] - // fusa:test REQ-WIRE-001 - // fusa:test REQ-WIRE-002 - // fusa:test REQ-WIRE-003 - // fusa:test REQ-WIRE-004 - // fusa:test REQ-WIRE-005 + //fusa:test REQ-WIRE-001 + //fusa:test REQ-WIRE-002 + //fusa:test REQ-WIRE-003 + //fusa:test REQ-WIRE-004 + //fusa:test REQ-WIRE-005 fn ntscf_frame_round_trips_arbitrary_payload() { let sid = StreamId::new([0x02, 0x11, 0x22, 0x33, 0x44, 0x55], 0x0042); let payload = vec![0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03]; @@ -1380,7 +1398,7 @@ mod tests { } #[test] - // fusa:test REQ-WIRE-005 + //fusa:test REQ-WIRE-005 fn ntscf_frame_round_trips_empty_payload() { let sid = StreamId::default(); let frame = encode_ntscf_frame(sid, 0, &[]).unwrap(); @@ -1391,8 +1409,8 @@ mod tests { } #[test] - // fusa:test REQ-WIRE-001 - // fusa:test REQ-WIRE-007 + //fusa:test REQ-WIRE-001 + //fusa:test REQ-WIRE-007 fn ntscf_frame_rejects_oversized_payload() { let oversized = vec![0u8; NTSCF_DATA_LENGTH_MAX as usize + 1]; assert_eq!( @@ -1402,7 +1420,7 @@ mod tests { } #[test] - // fusa:test REQ-WIRE-008 + //fusa:test REQ-WIRE-008 fn decode_ntscf_frame_propagates_wrong_subtype() { let mut frame = encode_ntscf_frame(StreamId::default(), 0, &[1, 2, 3]).unwrap(); frame[0] = TSCF_SUBTYPE; @@ -1413,7 +1431,7 @@ mod tests { } #[test] - // fusa:test REQ-WIRE-009 + //fusa:test REQ-WIRE-009 fn decode_ntscf_frame_rejects_short_input() { for len in 0..NTSCF_HEADER_LEN { let buf = vec![0u8; len]; @@ -1422,7 +1440,59 @@ mod tests { } #[test] - // fusa:test REQ-WIRE-009 + //fusa:test REQ-NET-002 + fn select_header_variant_discards_every_subtype_but_the_two_rcp_ones() { + // TC18 §12.8.2 (TC18.txt line 3170): "If neither a gPTP nor an + // IEEE1722 frame is found the received frame shall be discarded." + // The only two AVTPDU subtypes RCP defines are TC18 §11.1 + // Figure 5's TSCF `subtype(0x05)` and Figure 6's NTSCF + // `subtype(0x82)` — every other value of that octet must be + // discarded rather than parsed. The literals below are read from + // those two figures, not from this crate's own constants. + const TC18_TSCF_SUBTYPE: u8 = 0x05; + const TC18_NTSCF_SUBTYPE: u8 = 0x82; + + for subtype in 0u8..=0xFF { + // sv=1 in octet 1 and a full TSCF-length buffer, so the only + // thing that can decide accept-vs-discard is the subtype octet. + let mut buf = [0u8; TSCF_HEADER_LEN]; + buf[0] = subtype; + buf[1] = 0x80; + let accepted = select_header_variant(&buf, TimeSyncCapability::Capable).is_ok(); + let expected = subtype == TC18_TSCF_SUBTYPE || subtype == TC18_NTSCF_SUBTYPE; + assert_eq!( + accepted, + expected, + "subtype 0x{subtype:02X} must {} be accepted", + if expected { "" } else { "not" } + ); + } + } + + #[test] + //fusa:test REQ-NTSCF-007 + fn frame_composition_always_emits_the_server_side_ntscf_header() { + // TC18 §11.1 (TC18.txt line 1062) and §11.4.3 (line 1988): the RC + // Server always uses the NTSCF header, and responses/acknowledges + // are always carried under it. `encode_ntscf_frame` is this + // crate's only whole-frame composition entry point, and its first + // octet is always Figure 6's literal `subtype(0x82)`. + for payload_len in [0usize, 1, 4, 7, 64] { + let payload = vec![0xA5u8; payload_len]; + let frame = encode_ntscf_frame(StreamId::default(), 0x11, &payload).unwrap(); + assert_eq!( + frame[0], 0x82, + "TC18 Figure 6 subtype(0x82) for a {payload_len}-byte payload" + ); + assert_ne!( + frame[0], 0x05, + "must never be TC18 Figure 5's client-only TSCF subtype" + ); + } + } + + #[test] + //fusa:test REQ-WIRE-009 fn ntscf_frame_functions_never_panic_on_arbitrary_input() { let mut state: u32 = 0x1357_9BDF; let mut next = || { diff --git a/src/base64_serde.rs b/src/base64_serde.rs index e687d0a..c32e68b 100644 --- a/src/base64_serde.rs +++ b/src/base64_serde.rs @@ -1,4 +1,4 @@ -// fusa:req REQ-RELAY-009 +//fusa:req REQ-RELAY-009 //! Serde helper: serialize `Vec` as a base64 string per RELAY spec §15.1. //! @@ -47,7 +47,7 @@ mod tests { } #[test] - // fusa:test REQ-RELAY-009 + //fusa:test REQ-RELAY-009 fn base64_round_trip() { let w = Wrapper { data: vec![0xDE, 0xAD, 0xBE, 0xEF], diff --git a/src/bin/rcp.rs b/src/bin/rcp.rs index f6d6c20..444dc08 100644 --- a/src/bin/rcp.rs +++ b/src/bin/rcp.rs @@ -1,12 +1,12 @@ -// fusa:req REQ-CLI-001 -// fusa:req REQ-CLI-002 -// fusa:req REQ-CLI-003 -// fusa:req REQ-CLI-004 -// fusa:req REQ-CLI-005 -// fusa:req REQ-CLI-006 -// fusa:req REQ-CLI-007 -// fusa:req REQ-CLI-008 -// fusa:req REQ-CLI-009 +//fusa:req REQ-CLI-001 +//fusa:req REQ-CLI-002 +//fusa:req REQ-CLI-003 +//fusa:req REQ-CLI-004 +//fusa:req REQ-CLI-005 +//fusa:req REQ-CLI-006 +//fusa:req REQ-CLI-007 +//fusa:req REQ-CLI-008 +//fusa:req REQ-CLI-009 //! RCP command-line interface — RELAY spec §12 conformant. //! @@ -107,8 +107,8 @@ fn main() { match args[1].as_str() { // ── §12.1 version ───────────────────────────────────────────────────── - // fusa:req REQ-CLI-003 - // fusa:req REQ-CLI-006 + //fusa:req REQ-CLI-003 + //fusa:req REQ-CLI-006 "version" => { let format = flag_value(&args, "--format").unwrap_or("text"); if format == "json" { @@ -144,7 +144,7 @@ fn main() { } // ── §12.2 capabilities ──────────────────────────────────────────────── - // fusa:req REQ-CLI-007 + //fusa:req REQ-CLI-007 // "fragmentation" reflects ROADMAP.md Milestone 8's "go" decision: // crate::fragment::FragmentReassemblyBuffer implements ms/segment_num // multi-AVTPDU reassembly bounded by rx_stream_max_request_size. @@ -197,7 +197,7 @@ fn main() { } // ── §12.3 status ────────────────────────────────────────────────────── - // fusa:req REQ-CLI-008 + //fusa:req REQ-CLI-008 // The old --zone-addressed subscription branch is gone (there is no // Zone/Controller left to subscribe against — see rcp::mock's own // doc comment on why RcServer models no live-notification mechanism @@ -236,7 +236,7 @@ fn main() { } // ── §11.2 convert ───────────────────────────────────────────────────── - // fusa:req REQ-CLI-009 + //fusa:req REQ-CLI-009 "convert" => { let protocol = flag_value(&args, "--protocol").unwrap_or(""); if protocol != PROTOCOL { @@ -258,21 +258,21 @@ fn main() { } // ── discover ────────────────────────────────────────────────────────── - // fusa:req REQ-CLI-001 + //fusa:req REQ-CLI-001 "discover" => cmd_discover(&args), // ── register read / register write ────────────────────────────────── - // fusa:req REQ-CLI-002 - // fusa:req REQ-CLI-005 + //fusa:req REQ-CLI-002 + //fusa:req REQ-CLI-005 "register" => cmd_register(&args), // ── endpoint read / endpoint write ─────────────────────────────────── - // fusa:req REQ-CLI-004 - // fusa:req REQ-CLI-005 + //fusa:req REQ-CLI-004 + //fusa:req REQ-CLI-005 "endpoint" => cmd_endpoint(&args), // ── serve ───────────────────────────────────────────────────────────── - // fusa:req REQ-CLI-010 + //fusa:req REQ-CLI-010 "serve" => cmd_serve(&args), cmd => { @@ -290,7 +290,7 @@ fn main() { /// then answers it — against a fresh in-process [`RcServer`] (see this /// file's own doc comment) — via [`discovery::build_discovery_response`], /// and decodes/prints the resulting [`GeneralRegisters`] snapshot. -// fusa:req REQ-CLI-001 +//fusa:req REQ-CLI-001 fn cmd_discover(args: &[String]) { let transaction_num = parse_u8_arg(args, "--transaction").unwrap_or(0); let format = flag_value(args, "--format").unwrap_or("text"); @@ -350,7 +350,7 @@ fn cmd_discover(args: &[String]) { /// /// Dispatches to [`cmd_register_read`]/[`cmd_register_write`], both /// addressed via [`EP0_BYTE_BUS_ID`] through [`RcServer::handle_abb`]. -// fusa:req REQ-CLI-002 +//fusa:req REQ-CLI-002 fn cmd_register(args: &[String]) { match args.get(2).map(String::as_str) { Some("read") => cmd_register_read(args), @@ -368,7 +368,7 @@ fn cmd_register(args: &[String]) { /// [`AcfAbbMessage`] read request, dispatched through /// [`RcServer::handle_abb`] — never root-client-gated, per /// [`rcp::ep0::check_ep0_access_for_stream`]'s own doc comment. -// fusa:req REQ-CLI-005 +//fusa:req REQ-CLI-005 fn cmd_register_read(args: &[String]) { let stream = parse_stream_arg(args, "--stream").unwrap_or_else(|| StreamId::from_u64(0)); let format = flag_value(args, "--format").unwrap_or("text"); @@ -429,7 +429,7 @@ fn cmd_register_read(args: &[String]) { /// `RcpError::LockedMemAccess` for the root client itself: see /// [`RcServer::handle_abb`]'s own doc comment for why a general-register /// write is currently never actually accepted by this in-process server. -// fusa:req REQ-CLI-005 +//fusa:req REQ-CLI-005 fn cmd_register_write(args: &[String]) { let stream = parse_stream_arg(args, "--stream").unwrap_or_else(|| StreamId::from_u64(0)); let payload = match parse_hex_arg(args, "--payload") { @@ -471,7 +471,7 @@ fn cmd_register_write(args: &[String]) { /// Dispatches to [`cmd_endpoint_read`]/[`cmd_endpoint_write`], both /// addressed via `(--stream, --bus-id)` through [`RcServer::handle_abb`]'s /// `DeviceEndpoint` route. -// fusa:req REQ-CLI-004 +//fusa:req REQ-CLI-004 fn cmd_endpoint(args: &[String]) { match args.get(2).map(String::as_str) { Some("read") => cmd_endpoint_read(args), @@ -491,7 +491,7 @@ fn cmd_endpoint(args: &[String]) { /// `(--stream, --bus-id)`, then issues a read request for `--read-size` /// bytes (default `u8::MAX`, matching [`rcp::adapt::from_message`]'s own /// default) via [`RcServer::handle_abb`]. -// fusa:req REQ-CLI-005 +//fusa:req REQ-CLI-005 fn cmd_endpoint_read(args: &[String]) { let stream = parse_stream_arg(args, "--stream").unwrap_or_else(|| StreamId::from_u64(0)); let bus_id = match parse_u16_arg(args, "--bus-id") { @@ -558,7 +558,7 @@ fn cmd_endpoint_read(args: &[String]) { /// [`EndpointType::Gpio`]) holding `--initial` (default empty) under /// `(--stream, --bus-id)`, then issues a write request carrying /// `--payload` via [`RcServer::handle_abb`]. -// fusa:req REQ-CLI-005 +//fusa:req REQ-CLI-005 fn cmd_endpoint_write(args: &[String]) { let stream = parse_stream_arg(args, "--stream").unwrap_or_else(|| StreamId::from_u64(0)); let bus_id = match parse_u16_arg(args, "--bus-id") { @@ -627,7 +627,7 @@ fn cmd_endpoint_write(args: &[String]) { /// This is the first `rust-rcp` command to talk to a real OS socket rather /// than dispatching directly against an in-process `RcServer` — see this /// file's own module doc comment. -// fusa:req REQ-CLI-010 +//fusa:req REQ-CLI-010 fn cmd_serve(args: &[String]) { let bind_ip_str = match flag_value(args, "--udp") { Some(ip) => ip, @@ -897,7 +897,7 @@ mod tests { } #[test] - // fusa:test REQ-CLI-002 + //fusa:test REQ-CLI-002 fn flag_value_finds_option() { let args: Vec = vec![ "rcp".into(), @@ -911,7 +911,7 @@ mod tests { } #[test] - // fusa:test REQ-CLI-002 + //fusa:test REQ-CLI-002 fn parse_hex_arg_decodes_bytes() { let args: Vec = vec!["rcp".into(), "--payload".into(), "deadbeef".into()]; let bytes = parse_hex_arg(&args, "--payload").unwrap(); @@ -919,42 +919,42 @@ mod tests { } #[test] - // fusa:test REQ-CLI-002 + //fusa:test REQ-CLI-002 fn parse_hex_arg_absent_returns_none() { let args: Vec = vec!["rcp".into(), "register".into(), "write".into()]; assert!(parse_hex_arg(&args, "--payload").is_none()); } #[test] - // fusa:test REQ-CLI-002 + //fusa:test REQ-CLI-002 fn parse_u16_arg_parses_decimal() { let args: Vec = vec!["rcp".into(), "--bus-id".into(), "42".into()]; assert_eq!(parse_u16_arg(&args, "--bus-id"), Some(42u16)); } #[test] - // fusa:test REQ-CLI-010 + //fusa:test REQ-CLI-010 fn parse_u32_arg_parses_decimal() { let args: Vec = vec!["rcp".into(), "--max-requests".into(), "5".into()]; assert_eq!(parse_u32_arg(&args, "--max-requests"), Some(5u32)); } #[test] - // fusa:test REQ-CLI-010 + //fusa:test REQ-CLI-010 fn parse_u32_arg_absent_returns_none() { let args: Vec = vec!["rcp".into(), "serve".into()]; assert!(parse_u32_arg(&args, "--max-requests").is_none()); } #[test] - // fusa:test REQ-CLI-002 + //fusa:test REQ-CLI-002 fn parse_u8_arg_parses_transaction() { let args: Vec = vec!["rcp".into(), "--transaction".into(), "2".into()]; assert_eq!(parse_u8_arg(&args, "--transaction"), Some(2u8)); } #[test] - // fusa:test REQ-CLI-002 + //fusa:test REQ-CLI-002 fn has_flag_detects_bare_flag() { let args: Vec = vec![ "rcp".into(), @@ -967,7 +967,7 @@ mod tests { } #[test] - // fusa:test REQ-CLI-002 + //fusa:test REQ-CLI-002 fn parse_stream_arg_roundtrips_hex() { let sid = stream(0x1234); let hex = format_stream_hex(sid); @@ -976,54 +976,54 @@ mod tests { } #[test] - // fusa:test REQ-CLI-002 + //fusa:test REQ-CLI-002 fn parse_stream_arg_absent_returns_none() { let args: Vec = vec!["rcp".into()]; assert!(parse_stream_arg(&args, "--stream").is_none()); } #[test] - // fusa:test REQ-CLI-002 + //fusa:test REQ-CLI-002 fn parse_ep_type_arg_defaults_to_gpio() { let args: Vec = vec!["rcp".into()]; assert_eq!(parse_ep_type_arg(&args), Some(EndpointType::Gpio)); } #[test] - // fusa:test REQ-CLI-002 + //fusa:test REQ-CLI-002 fn parse_ep_type_arg_rejects_unrecognized_byte() { let args: Vec = vec!["rcp".into(), "--ep-type".into(), "255".into()]; assert_eq!(parse_ep_type_arg(&args), None); } #[test] - // fusa:test REQ-CLI-003 - // fusa:test REQ-CLI-006 + //fusa:test REQ-CLI-003 + //fusa:test REQ-CLI-006 fn spec_version_is_non_empty() { assert!(!rcp::SPEC_VERSION.is_empty()); } #[test] - // fusa:test REQ-CLI-006 + //fusa:test REQ-CLI-006 fn spec_version_is_relay_2_0() { assert_eq!(rcp::SPEC_VERSION, "2.0", "must track RELAY spec v2.0"); } #[test] - // fusa:test REQ-SPEC-001 + //fusa:test REQ-SPEC-001 fn relay_spec_version_alias_matches_spec_version() { assert_eq!(rcp::RELAY_SPEC_VERSION, rcp::SPEC_VERSION); } #[test] - // fusa:test REQ-CLI-007 + //fusa:test REQ-CLI-007 fn capabilities_json_is_valid() { assert!(!rcp::SPEC_VERSION.is_empty()); assert!(!env!("CARGO_PKG_VERSION").is_empty()); } #[test] - // fusa:test REQ-CLI-008 + //fusa:test REQ-CLI-008 fn status_json_fields_present() { assert!(!env!("CARGO_PKG_VERSION").is_empty()); } @@ -1031,7 +1031,7 @@ mod tests { // ── discover ────────────────────────────────────────────────────────────── #[test] - // fusa:test REQ-CLI-001 + //fusa:test REQ-CLI-001 fn discover_request_is_recognized_and_answered() { let server = RcServer::new(GeneralRegisters { svr_vendor_id: 0x1234, @@ -1053,7 +1053,7 @@ mod tests { // ── register read / register write ─────────────────────────────────────── #[test] - // fusa:test REQ-CLI-005 + //fusa:test REQ-CLI-005 fn register_read_returns_general_registers_snapshot() { let server = RcServer::new(GeneralRegisters { svr_device_id: 0xBEEF, @@ -1075,7 +1075,7 @@ mod tests { } #[test] - // fusa:test REQ-CLI-005 + //fusa:test REQ-CLI-005 fn register_write_succeeds_for_the_root_client() { // Mirrors rcp::mock::rc_server_tests::ep0_write_succeeds_for_the_root_client // — RegisterCategory::General now has LockPolicy::W, writable @@ -1107,7 +1107,7 @@ mod tests { // ── endpoint read / endpoint write ─────────────────────────────────────── #[test] - // fusa:test REQ-CLI-004 + //fusa:test REQ-CLI-004 fn endpoint_read_returns_registered_endpoint_payload() { let server = RcServer::new(GeneralRegisters::default()); let sid = stream(1); @@ -1128,7 +1128,7 @@ mod tests { } #[test] - // fusa:test REQ-CLI-004 + //fusa:test REQ-CLI-004 fn endpoint_write_replaces_endpoint_buffer() { let server = RcServer::new(GeneralRegisters::default()); let sid = stream(1); @@ -1160,7 +1160,7 @@ mod tests { } #[test] - // fusa:test REQ-CLI-004 + //fusa:test REQ-CLI-004 fn endpoint_read_unregistered_bus_id_is_ep_not_found() { let server = RcServer::new(GeneralRegisters::default()); let sid = stream(1); @@ -1185,7 +1185,7 @@ mod tests { // comment. #[test] - // fusa:test REQ-CLI-009 + //fusa:test REQ-CLI-009 fn convert_official_golden_vector() { // RELAY spec/vectors/rcp-message.json — the exact vector // `relay interop` feeds this binary's `convert` in CI. @@ -1203,7 +1203,7 @@ mod tests { } #[test] - // fusa:test REQ-CLI-009 + //fusa:test REQ-CLI-009 fn convert_write_with_body_uses_decimal_byte_bus_id_as_id() { let input = r#"{"byte_bus_id":7,"transaction_num":17,"control":32,"body":"3q2+7w=="}"#; let output = convert_rcp_message(input).unwrap(); @@ -1214,7 +1214,7 @@ mod tests { } #[test] - // fusa:test REQ-CLI-009 + //fusa:test REQ-CLI-009 fn convert_read_with_read_size_omits_no_payload_field() { let input = r#"{"byte_bus_id":1,"transaction_num":17,"control":64,"read_size_or_segment":4}"#; @@ -1227,7 +1227,7 @@ mod tests { } #[test] - // fusa:test REQ-CLI-009 + //fusa:test REQ-CLI-009 fn convert_minimal_request_defaults_absent_fields_to_zero() { let input = r#"{"byte_bus_id":0,"control":32}"#; let output = convert_rcp_message(input).unwrap(); @@ -1239,7 +1239,7 @@ mod tests { } #[test] - // fusa:test REQ-CLI-009 + //fusa:test REQ-CLI-009 fn convert_error_control_bit_sets_rcp_error_meta() { let input = r#"{"byte_bus_id":99,"control":72}"#; // Read | Error let output = convert_rcp_message(input).unwrap(); @@ -1250,7 +1250,7 @@ mod tests { } #[test] - // fusa:test REQ-CLI-009 + //fusa:test REQ-CLI-009 fn convert_rejects_byte_bus_id_out_of_range() { // byte_bus_id is 0-255 (u8) per spec/schemas/rcp-message.json; // the real reference implementation rejects an out-of-range value @@ -1260,21 +1260,21 @@ mod tests { } #[test] - // fusa:test REQ-CLI-009 + //fusa:test REQ-CLI-009 fn convert_rejects_unknown_field() { let input = r#"{"byte_bus_id":1,"control":32,"extra":"bad"}"#; assert!(convert_rcp_message(input).is_err()); } #[test] - // fusa:test REQ-CLI-009 + //fusa:test REQ-CLI-009 fn convert_rejects_malformed_json() { assert!(convert_rcp_message("not json").is_err()); assert!(convert_rcp_message("[]").is_err()); } #[test] - // fusa:test REQ-CLI-009 + //fusa:test REQ-CLI-009 fn convert_rejects_invalid_body_base64() { let input = r#"{"byte_bus_id":1,"control":32,"body":"not-base64!!"}"#; assert!(convert_rcp_message(input).is_err()); diff --git a/src/can.rs b/src/can.rs index dcf3ae6..15c6eb1 100644 --- a/src/can.rs +++ b/src/can.rs @@ -1,14 +1,14 @@ -// fusa:req REQ-CAN-001 -// fusa:req REQ-CAN-002 -// fusa:req REQ-CAN-003 -// fusa:req REQ-CAN-004 -// fusa:req REQ-CAN-005 -// fusa:req REQ-CAN-006 -// fusa:req REQ-CAN-007 -// fusa:req REQ-CAN-008 -// fusa:req REQ-CAN-009 -// fusa:req REQ-CAN-010 -// fusa:req REQ-CAN-011 +//fusa:req REQ-CAN-001 +//fusa:req REQ-CAN-002 +//fusa:req REQ-CAN-003 +//fusa:req REQ-CAN-004 +//fusa:req REQ-CAN-005 +//fusa:req REQ-CAN-006 +//fusa:req REQ-CAN-007 +//fusa:req REQ-CAN-008 +//fusa:req REQ-CAN-009 +//fusa:req REQ-CAN-010 +//fusa:req REQ-CAN-011 //! The CAN controller endpoint type (`ep_type 0x0B`) — `ROADMAP.md` //! Milestone 7 ("Remaining Endpoint Types"), second checklist bullet: @@ -114,19 +114,21 @@ //! [`CanFunctionalConfig`] carries one [`FrameFormat`] field rather than //! being left empty. //! -//! ## Provenance note: `FrameFormat` wire encoding +//! ## `FrameFormat` wire encoding (confirmed against TC18 Table 54) //! //! `ROADMAP.md`'s checklist bullet names the six [`FrameFormat`] variants by -//! their standard CAN abbreviations but states no numeric byte value for -//! any of them. Per Guiding Principle 5, [`FrameFormat::to_u8`]/ -//! [`FrameFormat::from_u8`] assign each variant a stable, sequential byte -//! value (`0`..=`5`, declaration order) rather than this crate guessing at -//! a specific spec-defined encoding — the same discipline -//! [`crate::i2c::I2cSpeedMode::to_u8`] already applies to its own -//! spec-unconfirmed `i2c_mode` byte values. A later item that recovers the -//! real wire encoding (against this crate's own spec-extraction pass, never -//! against restated spec prose) is expected to update this mapping then, -//! not now. +//! their standard CAN abbreviations but states no numeric byte value for any +//! of them, so [`FrameFormat::to_u8`]/[`FrameFormat::from_u8`] originally +//! assigned each variant a stable, sequential byte value (`0`..=`5`, +//! declaration order) as this crate's own working choice. TC18 §13.7.11.3 +//! Table 54 (TC18.txt line 5447) has since been reconciled against that +//! mapping and confirms it exactly: `CBFF = 0`, `CEFF = 1`, `FBFF = 2`, +//! `FEFF = 3`, `XL (classic physical layer) = 4`, `XL (new physical +//! layer) = 5`, with `6` and `7` both Reserved. The two XL rows also settle +//! what `ROADMAP.md`'s "XL-classical"/"XL-new" naming meant: which physical +//! layer the XL frame uses, not two different XL frame shapes. +//! [`FrameFormat::from_u8`]'s rejection of `6`/`7` is therefore Table 54's +//! own Reserved rows, not merely an out-of-enum range check. //! //! [`FrameFormat::is_extended_id`] additionally distinguishes the base //! (11-bit arbitration ID) vs. extended (29-bit arbitration ID) rows among @@ -138,6 +140,43 @@ //! FBFF/FEFF themselves already name, so this one piece is treated as a //! genuine CAN physical fact rather than a further guessed encoding. //! +//! ## TC18 reconciliation note (§13.7.11) +//! +//! Reconciling this module against TC18 §13.7.11 confirms three further +//! behaviors and records four gaps. +//! +//! Confirmed: TC18 §13.7.11.3 (TC18.txt line 5471) states "Sending remote +//! frames is not supported", matching this module's own "Data frames only" +//! section below; the same line states "In case the CAN ID is 11bits, then +//! it shall be right aligned in the CAN ID field", which +//! [`CanDataFrame::encode`]'s big-endian [`CanDataFrame::id`] field +//! satisfies; and line 5443 plus line 5472 together give CAN XL's total +//! "CAN data" field size as 2054 bytes — up to 2048 payload bytes plus the +//! 6 additional ISO 11898-1 bytes (RRS, SDT, VCID, AF) — matching +//! [`CAN_XL_MAX_PAYLOAD`] + [`CAN_XL_SUB_HEADER_LEN`]. +//! +//! Not implemented, and recorded as explicit not-implemented requirement +//! entries rather than silently omitted: +//! +//! - The exact request-payload bit layout of TC18 Figure 39 (line 5428), +//! which carries `FrameFormat` and the CAN ID together in the payload's +//! first 32-bit word. This module instead emits a full format-tag byte +//! followed by a 4-byte big-endian CAN ID (5 bytes), so a +//! [`CanDataFrame::encode`] buffer is **not** byte-compatible with Figure +//! 39's own word layout; the figure's column rendering does not survive +//! text extraction, so the exact bit positions are not transcribed here. +//! - Naming the 6 XL bytes as RRS/SDT/VCID/AF — [`CanXlSubHeader`] keeps +//! them opaque. +//! - Segmentation of an over-long CAN XL payload via the `ms` and +//! `segment_num` fields (line 5444); [`CanXlCombinedPayload::assemble`] +//! takes caller-ordered segments and reads neither field. +//! - TC18 Table 53's functional-config register layout (§13.7.11.2, lines +//! 5363-5419: bit-time registers 1-3, TDCC, EP/FIFO status, acceptance +//! filters 1-4, receive filters 1-4) and the six configuration +//! capabilities §13.7.11.2 enumerates (lines 5351-5356) — see +//! [`CanFunctionalConfig`], which carries a [`FrameFormat`] and nothing +//! else. +//! //! ## Provenance note: the CAN XL sub-header is carried opaque //! //! `ROADMAP.md`'s checklist bullet states CAN XL's sub-header is 6 bytes but @@ -217,7 +256,7 @@ use crate::RcpError; /// interpreted value. See this module's doc comment "Validation against /// `canbr.rs`" for why this is stated fresh here rather than imported from /// anywhere else. -// fusa:req REQ-CAN-003 +//fusa:req REQ-CAN-003 pub const CLASSICAL_CAN_MAX_DATA: usize = 8; /// Maximum CAN FD payload in bytes — a genuine physical ceiling of the CAN @@ -226,29 +265,35 @@ pub const CLASSICAL_CAN_MAX_DATA: usize = 8; /// comment "Validation against `canbr.rs`"); stated directly as this /// module's own constant since Milestone 9's canbr REPLACE cutover deleted /// that module. -// fusa:req REQ-CAN-003 +//fusa:req REQ-CAN-003 pub const CAN_FD_MAX_PAYLOAD: usize = 64; /// Maximum CAN XL payload in bytes, per `ROADMAP.md`'s own stated ceiling -/// for this checklist bullet. -// fusa:req REQ-CAN-006 +/// for this checklist bullet and confirmed by TC18 §13.7.11.3 (TC18.txt +/// line 5443): "For CAN XL this can be up to 2054 bytes (2048 + 6)". +//fusa:req REQ-CAN-006 +//fusa:req REQ-CAN-016 pub const CAN_XL_MAX_PAYLOAD: usize = 2048; /// CAN XL's sub-header length in bytes, per `ROADMAP.md`'s own stated -/// ceiling for this checklist bullet. See [`CanXlSubHeader`]. -// fusa:req REQ-CAN-006 +/// ceiling for this checklist bullet and confirmed by TC18 §13.7.11.3 +/// (TC18.txt line 5472): the "CAN data" field includes 6 additional bytes +/// (RRS, SDT, VCID, AF — see ISO 11898-1) for either XL frame format. See +/// [`CanXlSubHeader`]. +//fusa:req REQ-CAN-006 +//fusa:req REQ-CAN-016 pub const CAN_XL_SUB_HEADER_LEN: usize = 6; /// Maximum standard (base-format, 11-bit) CAN arbitration ID — a genuine /// physical fact about classical/FD CAN's base frame formats, not a /// spec-defined or otherwise interpreted value. -// fusa:req REQ-CAN-004 +//fusa:req REQ-CAN-004 pub const CAN_STANDARD_ID_MAX: u32 = 0x7FF; /// Maximum extended (extended-format, 29-bit) CAN arbitration ID — a /// genuine physical fact about classical/FD CAN's extended frame formats, /// not a spec-defined or otherwise interpreted value. -// fusa:req REQ-CAN-004 +//fusa:req REQ-CAN-004 pub const CAN_EXTENDED_ID_MAX: u32 = 0x1FFF_FFFF; // ── FrameFormat ────────────────────────────────────────────────────────────── @@ -261,7 +306,8 @@ pub const CAN_EXTENDED_ID_MAX: u32 = 0x1FFF_FFFF; /// own working interpretation rather than a confirmed spec encoding. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] -// fusa:req REQ-CAN-001 +//fusa:req REQ-CAN-001 +//fusa:req REQ-CAN-012 pub enum FrameFormat { /// Classic Base Frame Format: classical CAN 2.0, 11-bit (standard) /// arbitration ID. @@ -284,9 +330,11 @@ pub enum FrameFormat { } impl FrameFormat { - /// Encode this frame format as its wire byte value. See this module's - /// doc comment "Provenance note: `FrameFormat` wire encoding". - // fusa:req REQ-CAN-001 + /// Encode this frame format as its wire byte value, per TC18 §13.7.11.3 + /// Table 54 (TC18.txt line 5447). See this module's doc comment + /// "`FrameFormat` wire encoding (confirmed against TC18 Table 54)". + //fusa:req REQ-CAN-001 + //fusa:req REQ-CAN-012 pub fn to_u8(self) -> u8 { self as u8 } @@ -294,9 +342,11 @@ impl FrameFormat { /// Decode a wire byte value into a [`FrameFormat`]. /// /// Returns `Err(RcpError::InvalidParameter)` for any byte outside - /// `0..=5`, matching [`crate::i2c::I2cSpeedMode::from_u8`]'s own - /// range-check discipline. Never panics for any input. - // fusa:req REQ-CAN-002 + /// `0..=5` — `6` and `7` are Table 54's own two Reserved rows (TC18 + /// §13.7.11.3, TC18.txt line 5454), and no `FrameFormat` value wider + /// than 3 bits exists. Never panics for any input. + //fusa:req REQ-CAN-002 + //fusa:req REQ-CAN-013 pub fn from_u8(raw: u8) -> Result { match raw { 0 => Ok(Self::Cbff), @@ -310,19 +360,19 @@ impl FrameFormat { } /// Is this one of the two classical (non-FD, non-XL) formats? - // fusa:req REQ-CAN-001 + //fusa:req REQ-CAN-001 pub fn is_classical(self) -> bool { matches!(self, Self::Cbff | Self::Ceff) } /// Is this one of the two CAN FD formats? - // fusa:req REQ-CAN-001 + //fusa:req REQ-CAN-001 pub fn is_fd(self) -> bool { matches!(self, Self::Fbff | Self::Feff) } /// Is this one of the two CAN XL formats? - // fusa:req REQ-CAN-001 + //fusa:req REQ-CAN-001 pub fn is_xl(self) -> bool { matches!(self, Self::XlClassical | Self::XlNew) } @@ -331,7 +381,7 @@ impl FrameFormat { /// an 11-bit standard one)? Meaningful only for the four non-XL /// variants — see this module's doc comment "Provenance note: /// `FrameFormat` wire encoding". - // fusa:req REQ-CAN-004 + //fusa:req REQ-CAN-004 pub fn is_extended_id(self) -> bool { matches!(self, Self::Ceff | Self::Feff) } @@ -342,7 +392,7 @@ impl FrameFormat { /// [`FrameFormat::Fbff`]/[`FrameFormat::Feff`], and `None` for either /// XL variant (which use [`CAN_XL_MAX_PAYLOAD`] via [`CanXlFrame`] /// instead — see [`CanDataFrame`]). - // fusa:req REQ-CAN-004 + //fusa:req REQ-CAN-004 pub fn max_data_len(self) -> Option { if self.is_classical() { Some(CLASSICAL_CAN_MAX_DATA) @@ -363,7 +413,8 @@ impl FrameFormat { /// representation" — this type has no field or variant that could represent /// a remote frame. #[derive(Debug, Clone, PartialEq, Eq)] -// fusa:req REQ-CAN-004 +//fusa:req REQ-CAN-004 +//fusa:req REQ-CAN-015 pub struct CanDataFrame { /// This frame's format. Always one of [`FrameFormat::Cbff`], /// [`FrameFormat::Ceff`], [`FrameFormat::Fbff`], [`FrameFormat::Feff`] @@ -387,7 +438,17 @@ impl CanDataFrame { /// [`crate::lin::LinFrameTransfer::encode`]'s own trust-the-caller /// discipline; [`CanDataFrame::decode`] is where this module's /// validation lives. Never panics. - // fusa:req REQ-CAN-004 + /// + /// The big-endian `id` field right-aligns an 11-bit CAN ID within the + /// CAN ID field, as TC18 §13.7.11.3 (TC18.txt line 5471) requires: "In + /// case the CAN ID is 11bits, then it shall be right aligned in the CAN + /// ID field." No remote-frame (RTR) indication is emitted anywhere in + /// this encoding — the same line states remote frames are not supported. + /// See this module's doc comment "TC18 reconciliation note (§13.7.11)" + /// for how this 5-byte form relates to Figure 39's own 32-bit word. + //fusa:req REQ-CAN-004 + //fusa:req REQ-CAN-014 + //fusa:req REQ-CAN-015 pub fn encode(&self) -> Vec { let mut buf = Vec::with_capacity(1 + 4 + self.data.len()); buf.push(self.format.to_u8()); @@ -407,7 +468,7 @@ impl CanDataFrame { /// [`FrameFormat::is_extended_id`]). Returns /// `Err(RcpError::PayloadTooLarge)` when `data` exceeds `format`'s own /// [`FrameFormat::max_data_len`] ceiling. Never panics for any input. - // fusa:req REQ-CAN-005 + //fusa:req REQ-CAN-005 pub fn decode(b: &[u8]) -> Result { if b.len() < 5 { return Err(RcpError::ShortFrame); @@ -445,12 +506,12 @@ impl CanDataFrame { /// CAN XL's 6-byte sub-header, carried opaque — see this module's doc /// comment "Provenance note: the CAN XL sub-header is carried opaque". #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-CAN-006 +//fusa:req REQ-CAN-006 pub struct CanXlSubHeader(pub [u8; CAN_XL_SUB_HEADER_LEN]); impl CanXlSubHeader { /// Encode this sub-header to its raw 6-byte wire representation. - // fusa:req REQ-CAN-006 + //fusa:req REQ-CAN-006 pub fn encode(&self) -> [u8; CAN_XL_SUB_HEADER_LEN] { self.0 } @@ -460,7 +521,7 @@ impl CanXlSubHeader { /// /// Returns `Err(RcpError::ShortFrame)` for input shorter than /// [`CAN_XL_SUB_HEADER_LEN`] bytes. Never panics for any input. - // fusa:req REQ-CAN-006 + //fusa:req REQ-CAN-006 pub fn decode(b: &[u8]) -> Result { if b.len() < CAN_XL_SUB_HEADER_LEN { return Err(RcpError::ShortFrame); @@ -482,7 +543,7 @@ impl CanXlSubHeader { /// remote-frame representation" — this type has no field or variant that /// could represent a remote frame. #[derive(Debug, Clone, PartialEq, Eq)] -// fusa:req REQ-CAN-007 +//fusa:req REQ-CAN-007 pub struct CanXlFrame { /// This frame's format. Always one of [`FrameFormat::XlClassical`] or /// [`FrameFormat::XlNew`] — [`CanXlFrame::decode`] rejects any other @@ -502,7 +563,7 @@ impl CanXlFrame { /// Performs no validation of its own, mirroring /// [`CanDataFrame::encode`]'s trust-the-caller discipline. Never /// panics. - // fusa:req REQ-CAN-007 + //fusa:req REQ-CAN-007 pub fn encode(&self) -> Vec { let mut buf = Vec::with_capacity(1 + CAN_XL_SUB_HEADER_LEN + self.payload.len()); buf.push(self.format.to_u8()); @@ -520,8 +581,8 @@ impl CanXlFrame { /// of the two XL variants (use [`CanDataFrame::decode`] instead). /// Returns `Err(RcpError::PayloadTooLarge)` when the remaining payload /// exceeds [`CAN_XL_MAX_PAYLOAD`] bytes. Never panics for any input. - // fusa:req REQ-CAN-008 - // fusa:req REQ-CAN-009 + //fusa:req REQ-CAN-008 + //fusa:req REQ-CAN-009 pub fn decode(b: &[u8]) -> Result { if b.len() < 1 + CAN_XL_SUB_HEADER_LEN { return Err(RcpError::ShortFrame); @@ -554,7 +615,7 @@ impl CanXlFrame { /// not-yet-built multi-AVTPDU reassembly buffer — see this module's doc /// comment "CAN XL fragmentation interaction". #[derive(Debug, Clone, PartialEq, Eq, Default)] -// fusa:req REQ-CAN-010 +//fusa:req REQ-CAN-010 pub struct CanXlCombinedPayload(pub Vec); impl CanXlCombinedPayload { @@ -562,7 +623,7 @@ impl CanXlCombinedPayload { /// concatenating `segments` verbatim, in the order given. An empty /// `segments` slice yields an empty combined payload; this function /// never panics for any input, including empty per-segment payloads. - // fusa:req REQ-CAN-010 + //fusa:req REQ-CAN-010 pub fn assemble(segments: &[&[u8]]) -> Self { let mut combined = Vec::new(); for segment in segments { @@ -581,7 +642,7 @@ impl CanXlCombinedPayload { /// this carries a field (unlike [`crate::lin::LinFunctionalConfig`]'s empty /// placeholder). #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// fusa:req REQ-CAN-011 +//fusa:req REQ-CAN-011 pub struct CanFunctionalConfig { /// The [`FrameFormat`] this CAN controller endpoint is configured to /// use. @@ -596,7 +657,7 @@ impl CanFunctionalConfig { /// This module does not itself call that function — it only shows how a /// caller would obtain the matching tag, per this module's doc comment /// "Relationship to `crate::regmap`". - // fusa:req REQ-CAN-011 + //fusa:req REQ-CAN-011 pub fn layer_tag(&self) -> crate::regmap::PerEpTypeFunctionalConfig { crate::regmap::PerEpTypeFunctionalConfig::new(crate::regmap::EndpointType::Can) } @@ -609,7 +670,7 @@ mod tests { // ── Physical-fact constants ────────────────────────────────────────────── #[test] - // fusa:test REQ-CAN-003 + //fusa:test REQ-CAN-003 fn physical_fact_constants_match_real_can_ceilings() { assert_eq!(CLASSICAL_CAN_MAX_DATA, 8); assert_eq!(CAN_FD_MAX_PAYLOAD, 64); @@ -622,8 +683,8 @@ mod tests { // ── FrameFormat: encoding / classification round-trip ─────────────────── #[test] - // fusa:test REQ-CAN-001 - // fusa:test REQ-CAN-002 + //fusa:test REQ-CAN-001 + //fusa:test REQ-CAN-002 fn frame_format_round_trips_through_to_u8_from_u8() { for format in [ FrameFormat::Cbff, @@ -638,15 +699,63 @@ mod tests { } #[test] - // fusa:test REQ-CAN-002 + //fusa:test REQ-CAN-002 fn frame_format_from_u8_rejects_out_of_range_byte() { for raw in [6u8, 7, 255] { assert_eq!(FrameFormat::from_u8(raw), Err(RcpError::InvalidParameter)); } } + // ── TC18 Table 54: confirmed FrameFormat wire values ──────────────────── + + #[test] + //fusa:test REQ-CAN-012 + fn frame_format_wire_values_match_tc18_table_54() { + // TC18 §13.7.11.3 Table 54 "can frame formats" (TC18.txt line 5447), + // transcribed row by row: + // CBFF -> 0 + // CEFF -> 1 + // FBFF -> 2 + // FEFF -> 3 + // XL (classic physical layer) -> 4 + // XL (new physical layer) -> 5 + assert_eq!(FrameFormat::Cbff.to_u8(), 0); + assert_eq!(FrameFormat::Ceff.to_u8(), 1); + assert_eq!(FrameFormat::Fbff.to_u8(), 2); + assert_eq!(FrameFormat::Feff.to_u8(), 3); + assert_eq!(FrameFormat::XlClassical.to_u8(), 4); + assert_eq!(FrameFormat::XlNew.to_u8(), 5); + + assert_eq!(FrameFormat::from_u8(0), Ok(FrameFormat::Cbff)); + assert_eq!(FrameFormat::from_u8(1), Ok(FrameFormat::Ceff)); + assert_eq!(FrameFormat::from_u8(2), Ok(FrameFormat::Fbff)); + assert_eq!(FrameFormat::from_u8(3), Ok(FrameFormat::Feff)); + assert_eq!(FrameFormat::from_u8(4), Ok(FrameFormat::XlClassical)); + assert_eq!(FrameFormat::from_u8(5), Ok(FrameFormat::XlNew)); + + // Table 54's rows 4 and 5 are the two physical-layer variants of the + // same XL frame format, so both classify as XL and neither as FD. + assert!(FrameFormat::from_u8(4).unwrap().is_xl()); + assert!(FrameFormat::from_u8(5).unwrap().is_xl()); + } + + #[test] + //fusa:test REQ-CAN-013 + fn frame_format_from_u8_rejects_table_54_reserved_rows_6_and_7() { + // TC18 §13.7.11.3 Table 54 (TC18.txt lines 5454-5455): FrameFormat 6 + // and 7 are both "Reserved" — the only two of the 3-bit field's eight + // code points without an assigned frame format. + for reserved in [6u8, 7] { + assert_eq!( + FrameFormat::from_u8(reserved), + Err(RcpError::InvalidParameter), + "Table 54 row {reserved} is Reserved" + ); + } + } + #[test] - // fusa:test REQ-CAN-001 + //fusa:test REQ-CAN-001 fn frame_format_classification_helpers_partition_all_six_variants() { assert!(FrameFormat::Cbff.is_classical()); assert!(FrameFormat::Ceff.is_classical()); @@ -672,7 +781,7 @@ mod tests { } #[test] - // fusa:test REQ-CAN-004 + //fusa:test REQ-CAN-004 fn frame_format_is_extended_id_matches_ceff_feff_only() { assert!(!FrameFormat::Cbff.is_extended_id()); assert!(FrameFormat::Ceff.is_extended_id()); @@ -681,7 +790,7 @@ mod tests { } #[test] - // fusa:test REQ-CAN-004 + //fusa:test REQ-CAN-004 fn frame_format_max_data_len_matches_classical_fd_ceilings_and_none_for_xl() { assert_eq!( FrameFormat::Cbff.max_data_len(), @@ -700,8 +809,8 @@ mod tests { // ── CanDataFrame: round-trip / never-panic ─────────────────────────────── #[test] - // fusa:test REQ-CAN-004 - // fusa:test REQ-CAN-005 + //fusa:test REQ-CAN-004 + //fusa:test REQ-CAN-005 fn can_data_frame_round_trips_through_encode_decode() { for (format, id, data) in [ (FrameFormat::Cbff, 0x000u32, vec![]), @@ -723,7 +832,62 @@ mod tests { } #[test] - // fusa:test REQ-CAN-005 + //fusa:test REQ-CAN-014 + fn can_data_frame_encode_right_aligns_an_11_bit_can_id() { + // TC18 §13.7.11.3 (TC18.txt line 5471): "In case the CAN ID is + // 11bits, then it shall be right aligned in the CAN ID field." + // + // 11-bit ID 0x123 in a 4-byte big-endian CAN ID field is + // 0x00 0x00 0x01 0x23 — the ID's least-significant bit sits in the + // field's least-significant bit, and the 21 leading bits are zero. + let frame = CanDataFrame { + format: FrameFormat::Cbff, + id: 0x123, + data: vec![0xDE, 0xAD], + }; + assert_eq!( + frame.encode(), + vec![0x00, 0x00, 0x00, 0x01, 0x23, 0xDE, 0xAD] + ); + + // The widest 11-bit ID, 0x7FF, likewise occupies the field's low + // bits — not the left-aligned 0xFFE0_0000 a 29-bit-field + // left-justification would produce. + let widest = CanDataFrame { + format: FrameFormat::Fbff, + id: CAN_STANDARD_ID_MAX, + data: vec![], + }; + assert_eq!(widest.encode(), vec![0x02, 0x00, 0x00, 0x07, 0xFF]); + assert_ne!(&widest.encode()[1..5], &0xFFE0_0000u32.to_be_bytes()[..]); + } + + #[test] + //fusa:test REQ-CAN-015 + fn can_data_frame_encoding_carries_no_remote_frame_indication() { + // TC18 §13.7.11.3 (TC18.txt line 5471): "Sending remote frames is not + // supported." The encoded form is exactly one Table 54 format byte, + // four CAN ID bytes, and the data bytes — there is no RTR bit, byte, + // or trailing flag anywhere in it, for any of the four data-frame + // formats. + for (tag, format) in [ + (0u8, FrameFormat::Cbff), + (1, FrameFormat::Ceff), + (2, FrameFormat::Fbff), + (3, FrameFormat::Feff), + ] { + let frame = CanDataFrame { + format, + id: 0x001, + data: vec![0x11], + }; + assert_eq!(frame.encode(), vec![tag, 0x00, 0x00, 0x00, 0x01, 0x11]); + assert_eq!(frame.encode().len(), 5 + frame.data.len()); + } + } + + #[test] + //fusa:test REQ-CAN-005 fn can_data_frame_decode_rejects_short_input() { for len in [0usize, 1, 2, 3, 4] { assert_eq!( @@ -734,7 +898,7 @@ mod tests { } #[test] - // fusa:test REQ-CAN-005 + //fusa:test REQ-CAN-005 fn can_data_frame_decode_rejects_xl_format_tags() { for format in [FrameFormat::XlClassical, FrameFormat::XlNew] { let mut buf = vec![format.to_u8()]; @@ -744,7 +908,7 @@ mod tests { } #[test] - // fusa:test REQ-CAN-005 + //fusa:test REQ-CAN-005 fn can_data_frame_decode_rejects_id_wider_than_format_allows() { let mut buf = vec![FrameFormat::Cbff.to_u8()]; buf.extend_from_slice(&(CAN_STANDARD_ID_MAX + 1).to_be_bytes()); @@ -756,7 +920,7 @@ mod tests { } #[test] - // fusa:test REQ-CAN-005 + //fusa:test REQ-CAN-005 fn can_data_frame_decode_rejects_data_exceeding_format_ceiling() { let mut buf = vec![FrameFormat::Cbff.to_u8()]; buf.extend_from_slice(&0u32.to_be_bytes()); @@ -770,7 +934,7 @@ mod tests { } #[test] - // fusa:test REQ-CAN-005 + //fusa:test REQ-CAN-005 fn can_data_frame_decode_never_panics_for_any_sampled_input() { for len in [0usize, 1, 4, 5, 6, 13, 200] { let buf = vec![0x5Au8; len]; @@ -781,14 +945,14 @@ mod tests { // ── CanXlSubHeader / CanXlFrame: round-trip / never-panic ─────────────── #[test] - // fusa:test REQ-CAN-006 + //fusa:test REQ-CAN-006 fn can_xl_sub_header_round_trips_through_encode_decode() { let header = CanXlSubHeader([0x01, 0x02, 0x03, 0x04, 0x05, 0x06]); assert_eq!(CanXlSubHeader::decode(&header.encode()).unwrap(), header); } #[test] - // fusa:test REQ-CAN-006 + //fusa:test REQ-CAN-006 fn can_xl_sub_header_decode_rejects_short_input() { for len in [0usize, 1, 5] { assert_eq!( @@ -799,8 +963,8 @@ mod tests { } #[test] - // fusa:test REQ-CAN-007 - // fusa:test REQ-CAN-008 + //fusa:test REQ-CAN-007 + //fusa:test REQ-CAN-008 fn can_xl_frame_round_trips_through_encode_decode() { for (format, payload) in [ (FrameFormat::XlClassical, vec![]), @@ -820,7 +984,7 @@ mod tests { } #[test] - // fusa:test REQ-CAN-008 + //fusa:test REQ-CAN-008 fn can_xl_frame_decode_rejects_non_xl_format_tags() { for format in [ FrameFormat::Cbff, @@ -835,7 +999,7 @@ mod tests { } #[test] - // fusa:test REQ-CAN-008 + //fusa:test REQ-CAN-008 fn can_xl_frame_decode_rejects_short_input() { for len in [0usize, 1, 3, 6] { assert_eq!( @@ -846,7 +1010,7 @@ mod tests { } #[test] - // fusa:test REQ-CAN-009 + //fusa:test REQ-CAN-009 fn can_xl_frame_decode_rejects_payload_exceeding_2048_bytes() { let mut buf = vec![FrameFormat::XlClassical.to_u8()]; buf.extend_from_slice(&[0u8; CAN_XL_SUB_HEADER_LEN]); @@ -855,7 +1019,7 @@ mod tests { } #[test] - // fusa:test REQ-CAN-009 + //fusa:test REQ-CAN-009 fn can_xl_frame_decode_accepts_payload_at_exactly_2048_bytes() { let mut buf = vec![FrameFormat::XlNew.to_u8()]; buf.extend_from_slice(&[0u8; CAN_XL_SUB_HEADER_LEN]); @@ -865,7 +1029,30 @@ mod tests { } #[test] - // fusa:test REQ-CAN-008 + //fusa:test REQ-CAN-016 + fn can_xl_can_data_field_totals_2054_bytes() { + // TC18 §13.7.11.3 (TC18.txt line 5443): "For CAN XL this can be up to + // 2054 bytes (2048 + 6, see below)", and line 5472: the "CAN data" + // field includes 6 additional bytes (RRS, SDT, VCID, AF — see + // ISO 11898-1) for either XL frame format. + assert_eq!(CAN_XL_SUB_HEADER_LEN + CAN_XL_MAX_PAYLOAD, 2054); + + let frame = CanXlFrame { + format: FrameFormat::XlNew, + sub_header: CanXlSubHeader([0u8; CAN_XL_SUB_HEADER_LEN]), + payload: vec![0xA5; CAN_XL_MAX_PAYLOAD], + }; + // 2054 CAN-data bytes, plus this module's own leading format tag. + assert_eq!(frame.encode().len(), 1 + 2054); + let decoded = CanXlFrame::decode(&frame.encode()).unwrap(); + assert_eq!( + decoded.sub_header.encode().len() + decoded.payload.len(), + 2054 + ); + } + + #[test] + //fusa:test REQ-CAN-008 fn can_xl_frame_decode_never_panics_for_any_sampled_input() { for len in [0usize, 1, 6, 7, 20, 300] { let buf = vec![0x5Au8; len]; @@ -876,7 +1063,7 @@ mod tests { // ── CanXlCombinedPayload ────────────────────────────────────────────────── #[test] - // fusa:test REQ-CAN-010 + //fusa:test REQ-CAN-010 fn can_xl_combined_payload_concatenates_segments_in_caller_supplied_order() { let segments: Vec<&[u8]> = vec![&[1, 2, 3], &[], &[4, 5]]; let combined = CanXlCombinedPayload::assemble(&segments); @@ -884,7 +1071,7 @@ mod tests { } #[test] - // fusa:test REQ-CAN-010 + //fusa:test REQ-CAN-010 fn can_xl_combined_payload_empty_segments_yields_empty_payload() { assert_eq!( CanXlCombinedPayload::assemble(&[]), @@ -895,7 +1082,7 @@ mod tests { // ── CanFunctionalConfig / layer_tag ────────────────────────────────────── #[test] - // fusa:test REQ-CAN-011 + //fusa:test REQ-CAN-011 fn can_functional_config_layer_tag_matches_ep_type_can() { let functional = CanFunctionalConfig { format: FrameFormat::Fbff, @@ -913,7 +1100,7 @@ mod tests { } #[test] - // fusa:test REQ-CAN-011 + //fusa:test REQ-CAN-011 fn can_functional_config_layer_tag_rejects_mismatched_ep_type() { let functional = CanFunctionalConfig { format: FrameFormat::Cbff, diff --git a/src/capi.rs b/src/capi.rs index 48af13b..738ccf5 100644 --- a/src/capi.rs +++ b/src/capi.rs @@ -1,7 +1,7 @@ -// fusa:req REQ-CAPI-001 -// fusa:req REQ-CAPI-002 -// fusa:req REQ-CAPI-003 -// fusa:req REQ-CAPI-004 +//fusa:req REQ-CAPI-001 +//fusa:req REQ-CAPI-002 +//fusa:req REQ-CAPI-003 +//fusa:req REQ-CAPI-004 //! C API bridge — exposes a C-compatible FFI surface for embedding this //! crate's OPEN Alliance TC18 Remote Control Protocol Specification @@ -85,7 +85,7 @@ use crate::RcpError; /// C-compatible mirror of [`StreamId`]: a sender MAC address plus a /// locally-assigned unique-id suffix. -// fusa:req REQ-CAPI-001 +//fusa:req REQ-CAPI-001 #[repr(C)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct CStreamId { @@ -94,7 +94,7 @@ pub struct CStreamId { } impl From for CStreamId { - // fusa:req REQ-CAPI-003 + //fusa:req REQ-CAPI-003 fn from(id: StreamId) -> Self { CStreamId { sender_mac: id.sender_mac, @@ -104,7 +104,7 @@ impl From for CStreamId { } impl From for StreamId { - // fusa:req REQ-CAPI-003 + //fusa:req REQ-CAPI-003 fn from(id: CStreamId) -> Self { StreamId::new(id.sender_mac, id.unique_id) } @@ -116,7 +116,7 @@ impl From for StreamId { /// module's doc comment for why `evt`/`read_size_segment` are /// flattened rather than nested, and why field-width validation is not /// repeated here. -// fusa:req REQ-CAPI-002 +//fusa:req REQ-CAPI-002 #[repr(C)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct CByteMessageInfo { @@ -138,7 +138,7 @@ pub struct CByteMessageInfo { } impl From for CByteMessageInfo { - // fusa:req REQ-CAPI-003 + //fusa:req REQ-CAPI-003 fn from(info: ByteMessageInfo) -> Self { CByteMessageInfo { acf_msg_type: info.acf_msg_type, @@ -161,7 +161,7 @@ impl From for CByteMessageInfo { } impl From for ByteMessageInfo { - // fusa:req REQ-CAPI-003 + //fusa:req REQ-CAPI-003 fn from(c: CByteMessageInfo) -> Self { ByteMessageInfo { acf_msg_type: c.acf_msg_type, @@ -190,8 +190,8 @@ impl From for ByteMessageInfo { /// C-compatible ACF_ABB request header: a [`CStreamId`] plus the request's /// [`CByteMessageInfo`]. See this module's doc comment for why `payload` /// bytes are not part of this type. -// fusa:req REQ-CAPI-001 -// fusa:req REQ-CAPI-002 +//fusa:req REQ-CAPI-001 +//fusa:req REQ-CAPI-002 #[repr(C)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct CAbbRequest { @@ -223,8 +223,8 @@ impl CAbbRequest { /// [`CAbbRequest`] (see this module's doc comment for why they are still /// kept as two distinct types), for a response's [`CStreamId`]/ /// [`CByteMessageInfo`]. -// fusa:req REQ-CAPI-001 -// fusa:req REQ-CAPI-002 +//fusa:req REQ-CAPI-001 +//fusa:req REQ-CAPI-002 #[repr(C)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct CAbbResponse { @@ -257,7 +257,7 @@ impl CAbbResponse { /// C-compatible error code, rebuilt against every current /// [`crate::RcpError`] variant. See this module's doc comment for the /// full provenance/mapping note. -// fusa:req REQ-CAPI-004 +//fusa:req REQ-CAPI-004 #[repr(C)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CError { @@ -310,7 +310,7 @@ pub enum CError { } impl From<&RcpError> for CError { - // fusa:req REQ-CAPI-004 + //fusa:req REQ-CAPI-004 fn from(e: &RcpError) -> Self { match e { RcpError::Closed => CError::Closed, @@ -381,14 +381,14 @@ mod tests { // ── repr(C) sanity ─────────────────────────────────────────────────────── #[test] - // fusa:test REQ-CAPI-001 + //fusa:test REQ-CAPI-001 fn c_stream_id_is_repr_c() { // 6-byte MAC + u16 unique_id: at least 8 bytes on every platform. assert!(std::mem::size_of::() >= 8); } #[test] - // fusa:test REQ-CAPI-002 + //fusa:test REQ-CAPI-002 fn c_byte_message_info_is_repr_c() { // 2x u16 + 9 bool/u8-sized fields: at least 13 bytes on every // platform (padding may add more, never less). @@ -398,7 +398,7 @@ mod tests { // ── CStreamId <-> StreamId ─────────────────────────────────────────────── #[test] - // fusa:test REQ-CAPI-003 + //fusa:test REQ-CAPI-003 fn stream_id_round_trip() { let sid = stream_id(); let c: CStreamId = sid.into(); @@ -409,7 +409,7 @@ mod tests { // ── CByteMessageInfo <-> ByteMessageInfo ───────────────────────────────── #[test] - // fusa:test REQ-CAPI-003 + //fusa:test REQ-CAPI-003 fn byte_message_info_round_trip() { let bmi = info(7, true); let c: CByteMessageInfo = bmi.into(); @@ -426,9 +426,9 @@ mod tests { // ── CAbbRequest / CAbbResponse ─────────────────────────────────────────── #[test] - // fusa:test REQ-CAPI-001 - // fusa:test REQ-CAPI-002 - // fusa:test REQ-CAPI-003 + //fusa:test REQ-CAPI-001 + //fusa:test REQ-CAPI-002 + //fusa:test REQ-CAPI-003 fn abb_request_round_trip() { let sid = stream_id(); let bmi = info(7, true); @@ -438,9 +438,9 @@ mod tests { } #[test] - // fusa:test REQ-CAPI-001 - // fusa:test REQ-CAPI-002 - // fusa:test REQ-CAPI-003 + //fusa:test REQ-CAPI-001 + //fusa:test REQ-CAPI-002 + //fusa:test REQ-CAPI-003 fn abb_response_round_trip() { let sid = stream_id(); let bmi = info(7, false); @@ -450,7 +450,7 @@ mod tests { } #[test] - // fusa:test REQ-CAPI-003 + //fusa:test REQ-CAPI-003 fn request_and_response_headers_are_distinct_types_same_layout() { // Same field layout by construction (see this module's doc // comment) — this test exists so a future divergence in either @@ -465,7 +465,7 @@ mod tests { // ── CError mapping ─────────────────────────────────────────────────────── #[test] - // fusa:test REQ-CAPI-004 + //fusa:test REQ-CAPI-004 fn error_code_mapping_covers_tc18_codes() { assert_eq!( CError::from(&RcpError::UnsupportedCmd), @@ -520,7 +520,7 @@ mod tests { } #[test] - // fusa:test REQ-CAPI-004 + //fusa:test REQ-CAPI-004 fn error_code_mapping_covers_relay_and_general_sentinels() { assert_eq!(CError::from(&RcpError::Closed), CError::Closed); assert_eq!(CError::from(&RcpError::NotConnected), CError::NotConnected); @@ -537,7 +537,7 @@ mod tests { } #[test] - // fusa:test REQ-CAPI-004 + //fusa:test REQ-CAPI-004 fn error_code_mapping_collapses_legacy_and_other_to_other() { assert_eq!(CError::from(&RcpError::NotFound), CError::Other); assert_eq!(CError::from(&RcpError::AlreadyExists), CError::Other); diff --git a/src/certgap.rs b/src/certgap.rs index 8e4e087..d3fc964 100644 --- a/src/certgap.rs +++ b/src/certgap.rs @@ -1,8 +1,8 @@ -// fusa:req REQ-GAP-001 -// fusa:req REQ-GAP-002 -// fusa:req REQ-GAP-003 -// fusa:req REQ-GAP-004 -// fusa:req REQ-GAP-005 +//fusa:req REQ-GAP-001 +//fusa:req REQ-GAP-002 +//fusa:req REQ-GAP-003 +//fusa:req REQ-GAP-004 +//fusa:req REQ-GAP-005 //! Certification gap analysis — identifies untraced requirements and missing //! test coverage for FuSa and cybersecurity standards. @@ -12,7 +12,7 @@ use std::collections::{HashMap, HashSet}; // ── Gap analysis ────────────────────────────────────────────────────────────── /// Result of a gap analysis run. -// fusa:req REQ-GAP-001 +//fusa:req REQ-GAP-001 #[derive(Debug, Clone, Default)] pub struct GapReport { /// Requirements that have no implementation annotation. @@ -32,9 +32,9 @@ impl GapReport { /// Run a gap analysis. /// /// - `declared_reqs`: requirements listed in the spec/`.fusa-reqs.json` -/// - `implemented_reqs`: requirements traced in source (`// fusa:req REQ-*`) -/// - `tested_reqs`: requirements traced in tests (`// fusa:test REQ-*`) -// fusa:req REQ-GAP-002 +/// - `implemented_reqs`: requirements traced in source (`//fusa:req REQ-*`) +/// - `tested_reqs`: requirements traced in tests (`//fusa:test REQ-*`) +//fusa:req REQ-GAP-002 pub fn analyse( declared_reqs: &HashSet, implemented_reqs: &HashSet, @@ -64,7 +64,7 @@ pub fn analyse( } /// Coverage ratio: 0.0 (none) to 1.0 (all). -// fusa:req REQ-GAP-003 +//fusa:req REQ-GAP-003 pub fn coverage(declared: &HashSet, covered: &HashSet) -> f64 { if declared.is_empty() { return 1.0; @@ -74,7 +74,7 @@ pub fn coverage(declared: &HashSet, covered: &HashSet) -> f64 { } /// Summarise coverage by requirement prefix (e.g. "REQ-CTRL"). -// fusa:req REQ-GAP-004 +//fusa:req REQ-GAP-004 pub fn coverage_by_prefix( declared: &HashSet, covered: &HashSet, @@ -111,7 +111,7 @@ mod tests { } #[test] - // fusa:test REQ-GAP-002 + //fusa:test REQ-GAP-002 fn no_gaps_when_fully_covered() { let reqs = set(&["REQ-A-001", "REQ-A-002"]); let r = analyse(&reqs, &reqs, &reqs); @@ -119,7 +119,7 @@ mod tests { } #[test] - // fusa:test REQ-GAP-002 + //fusa:test REQ-GAP-002 fn unimplemented_detected() { let reqs = set(&["REQ-A-001", "REQ-A-002"]); let imp = set(&["REQ-A-001"]); @@ -128,7 +128,7 @@ mod tests { } #[test] - // fusa:test REQ-GAP-002 + //fusa:test REQ-GAP-002 fn untested_detected() { let reqs = set(&["REQ-A-001", "REQ-A-002"]); let tested = set(&["REQ-A-001"]); @@ -137,7 +137,7 @@ mod tests { } #[test] - // fusa:test REQ-GAP-002 + //fusa:test REQ-GAP-002 fn undeclared_detected() { let declared = set(&["REQ-A-001"]); let all = set(&["REQ-A-001", "REQ-PHANTOM-001"]); @@ -146,7 +146,7 @@ mod tests { } #[test] - // fusa:test REQ-GAP-003 + //fusa:test REQ-GAP-003 fn coverage_ratio() { let d = set(&["A", "B", "C", "D"]); let c = set(&["A", "B"]); @@ -155,13 +155,13 @@ mod tests { } #[test] - // fusa:test REQ-GAP-003 + //fusa:test REQ-GAP-003 fn coverage_empty_declared_is_one() { assert_eq!(coverage(&set(&[]), &set(&[])), 1.0); } #[test] - // fusa:test REQ-GAP-004 + //fusa:test REQ-GAP-004 fn coverage_by_prefix_groups_correctly() { let declared = set(&["REQ-CTRL-001", "REQ-CTRL-002", "REQ-WIRE-001"]); let covered = set(&["REQ-CTRL-001", "REQ-WIRE-001"]); @@ -171,7 +171,7 @@ mod tests { } #[test] - // fusa:test REQ-GAP-001 + //fusa:test REQ-GAP-001 fn gap_report_has_gaps_flag() { let mut r = GapReport::default(); assert!(!r.has_gaps()); @@ -180,7 +180,7 @@ mod tests { } #[test] - // fusa:test REQ-GAP-005 + //fusa:test REQ-GAP-005 fn zero_coverage_when_nothing_covered() { let declared = set(&["REQ-A-001", "REQ-A-002", "REQ-B-001"]); let covered: HashSet = HashSet::new(); diff --git a/src/codegen.rs b/src/codegen.rs index 9f5a36d..261eadf 100644 --- a/src/codegen.rs +++ b/src/codegen.rs @@ -1,7 +1,7 @@ -// fusa:req REQ-CG-001 -// fusa:req REQ-CG-002 -// fusa:req REQ-CG-003 -// fusa:req REQ-CG-004 +//fusa:req REQ-CG-001 +//fusa:req REQ-CG-002 +//fusa:req REQ-CG-003 +//fusa:req REQ-CG-004 //! Stub-code generator — emits Rust type definitions from a JSON schema. //! @@ -12,7 +12,7 @@ use std::collections::HashMap; // ── Schema types ────────────────────────────────────────────────────────────── /// Simple field types supported by the code generator. -// fusa:req REQ-CG-001 +//fusa:req REQ-CG-001 #[derive(Debug, Clone, PartialEq)] pub enum FieldType { U8, @@ -39,7 +39,7 @@ impl FieldType { } /// A named field in a generated struct. -// fusa:req REQ-CG-002 +//fusa:req REQ-CG-002 #[derive(Debug, Clone)] pub struct Field { pub name: String, @@ -48,7 +48,7 @@ pub struct Field { } /// A schema for a generated struct. -// fusa:req REQ-CG-002 +//fusa:req REQ-CG-002 #[derive(Debug, Clone)] pub struct StructSchema { pub name: String, @@ -58,7 +58,7 @@ pub struct StructSchema { // ── Code generator ──────────────────────────────────────────────────────────── /// Generates Rust struct definitions from a list of schemas. -// fusa:req REQ-CG-003 +//fusa:req REQ-CG-003 pub fn generate_structs(schemas: &[StructSchema]) -> String { let mut out = String::new(); for schema in schemas { @@ -82,7 +82,7 @@ pub fn generate_structs(schemas: &[StructSchema]) -> String { /// Parse a simple JSON-like schema definition map into [`StructSchema`] list. /// /// Accepts: `{"StructName": {"field": "type", "opt_field?": "type"}}` -// fusa:req REQ-CG-004 +//fusa:req REQ-CG-004 pub fn parse_schema(map: &HashMap>) -> Vec { let mut schemas = Vec::new(); let mut names: Vec<&String> = map.keys().collect(); @@ -133,7 +133,7 @@ mod tests { use std::collections::HashMap; #[test] - // fusa:test REQ-CG-001 + //fusa:test REQ-CG-001 fn field_type_rust_names() { assert_eq!(FieldType::U8.rust_type(), "u8"); assert_eq!(FieldType::Bool.rust_type(), "bool"); @@ -141,7 +141,7 @@ mod tests { } #[test] - // fusa:test REQ-CG-003 + //fusa:test REQ-CG-003 fn generate_empty_struct() { let schema = StructSchema { name: "Empty".into(), @@ -152,8 +152,8 @@ mod tests { } #[test] - // fusa:test REQ-CG-002 - // fusa:test REQ-CG-003 + //fusa:test REQ-CG-002 + //fusa:test REQ-CG-003 fn generate_struct_with_optional_field() { let schema = StructSchema { name: "Cmd".into(), @@ -176,7 +176,7 @@ mod tests { } #[test] - // fusa:test REQ-CG-004 + //fusa:test REQ-CG-004 fn parse_schema_roundtrip() { let mut map = HashMap::new(); let mut fields = HashMap::new(); diff --git a/src/config.rs b/src/config.rs index 9af7983..84ec6cc 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,8 +1,8 @@ -// fusa:req REQ-CFG-001 -// fusa:req REQ-CFG-005 -// fusa:req REQ-CFG-006 -// fusa:req REQ-CFG-007 -// fusa:req REQ-CFG-008 +//fusa:req REQ-CFG-001 +//fusa:req REQ-CFG-005 +//fusa:req REQ-CFG-006 +//fusa:req REQ-CFG-007 +//fusa:req REQ-CFG-008 //! Configuration loader and validator for the RC Server register-map/ //! lifecycle model (`ROADMAP.md` Milestone 9, Satellite Package Migration, @@ -95,7 +95,7 @@ use crate::RcpError; /// [`crate::regmap::GeneralRegisters`], the five `§3.7`-`§3.11` child /// config-table row types, and [`crate::lifecycle::RcServerState`] rather /// than reinventing them. -// fusa:req REQ-CFG-001 +//fusa:req REQ-CFG-001 #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] pub struct RcServerConfig { /// Lifecycle state to bring a freshly loaded RC Server up in. @@ -136,7 +136,7 @@ pub struct RcServerConfig { /// /// Returns `Err(RcpError::Other(_))` wrapping the underlying `serde_json` /// message on malformed input. Never panics. -// fusa:req REQ-CFG-005 +//fusa:req REQ-CFG-005 pub fn from_json(s: &str) -> Result { serde_json::from_str(s).map_err(|e| RcpError::Other(format!("config: invalid JSON: {e}"))) } @@ -145,7 +145,7 @@ pub fn from_json(s: &str) -> Result { /// /// Returns `Err(RcpError::Other(_))` wrapping the underlying `serde_yaml` /// message on malformed input. Never panics. -// fusa:req REQ-CFG-005 +//fusa:req REQ-CFG-005 pub fn from_yaml(s: &str) -> Result { serde_yaml::from_str(s).map_err(|e| RcpError::Other(format!("config: invalid YAML: {e}"))) } @@ -161,9 +161,9 @@ pub fn from_yaml(s: &str) -> Result { /// [`crate::lifecycle::check_register_reachable`]'s own error choice for /// exactly this "not reachable in this state" shape), `Ok(())` otherwise. /// Never panics for any input. -// fusa:req REQ-CFG-006 -// fusa:req REQ-CFG-007 -// fusa:req REQ-CFG-008 +//fusa:req REQ-CFG-006 +//fusa:req REQ-CFG-007 +//fusa:req REQ-CFG-008 pub fn validate(cfg: &RcServerConfig) -> Result<(), RcpError> { // ── Table row counts vs. declared capacity ────────────────────────────── check_capacity(cfg.hw_pin_mapping.len(), cfg.general.svr_hw_cfg.capacity)?; @@ -220,7 +220,7 @@ mod tests { use crate::regmap::TableDescriptor; #[test] - // fusa:test REQ-CFG-001 + //fusa:test REQ-CFG-001 fn default_config_is_valid() { let cfg = RcServerConfig::default(); validate(&cfg).unwrap(); @@ -228,7 +228,7 @@ mod tests { } #[test] - // fusa:test REQ-CFG-005 + //fusa:test REQ-CFG-005 fn parse_json_minimal() { let json = r#"{"initial_state": "HwConfigured"}"#; let cfg = from_json(json).unwrap(); @@ -237,7 +237,7 @@ mod tests { } #[test] - // fusa:test REQ-CFG-005 + //fusa:test REQ-CFG-005 fn parse_yaml_minimal() { let yaml = "initial_state: RcpConfigured\n"; let cfg = from_yaml(yaml).unwrap(); @@ -245,19 +245,19 @@ mod tests { } #[test] - // fusa:test REQ-CFG-005 + //fusa:test REQ-CFG-005 fn invalid_json_returns_error() { assert!(from_json("{invalid}").is_err()); } #[test] - // fusa:test REQ-CFG-005 + //fusa:test REQ-CFG-005 fn invalid_yaml_returns_error() { assert!(from_yaml(":\n - not: [valid").is_err()); } #[test] - // fusa:test REQ-CFG-005 + //fusa:test REQ-CFG-005 fn round_trips_through_json_with_table_rows() { let mut cfg = RcServerConfig::default(); cfg.general.svr_hw_cfg = TableDescriptor { @@ -276,7 +276,7 @@ mod tests { } #[test] - // fusa:test REQ-CFG-006 + //fusa:test REQ-CFG-006 fn validate_rejects_hw_pin_mapping_exceeding_capacity() { let mut cfg = RcServerConfig::default(); cfg.general.svr_hw_cfg = TableDescriptor { @@ -288,7 +288,7 @@ mod tests { } #[test] - // fusa:test REQ-CFG-006 + //fusa:test REQ-CFG-006 fn validate_rejects_request_streams_exceeding_capacity() { let mut cfg = RcServerConfig::default(); cfg.general.svr_request_stream_cfg = TableDescriptor { @@ -301,7 +301,7 @@ mod tests { } #[test] - // fusa:test REQ-CFG-006 + //fusa:test REQ-CFG-006 fn validate_rejects_ep_bytebus_id_map_exceeding_capacity() { let mut cfg = RcServerConfig::default(); cfg.general.svr_ep_bytebus_id_map = TableDescriptor { @@ -314,7 +314,7 @@ mod tests { } #[test] - // fusa:test REQ-CFG-006 + //fusa:test REQ-CFG-006 fn validate_rejects_response_streams_exceeding_capacity() { let mut cfg = RcServerConfig::default(); cfg.general.svr_response_stream_cfg = TableDescriptor { @@ -327,7 +327,7 @@ mod tests { } #[test] - // fusa:test REQ-CFG-007 + //fusa:test REQ-CFG-007 fn validate_rejects_sequencer_state_exceeding_svr_sequencers_max() { let mut cfg = RcServerConfig { initial_state: RcServerState::HwConfigured, @@ -342,7 +342,7 @@ mod tests { } #[test] - // fusa:test REQ-CFG-007 + //fusa:test REQ-CFG-007 fn validate_accepts_sequencer_state_within_svr_sequencers_max() { let mut cfg = RcServerConfig { initial_state: RcServerState::HwConfigured, @@ -354,7 +354,7 @@ mod tests { } #[test] - // fusa:test REQ-CFG-008 + //fusa:test REQ-CFG-008 fn validate_rejects_rcp_config_tables_while_hw_unconfigured() { let mut cfg = RcServerConfig::default(); // initial_state defaults to HwUnconfigured, where RcpConfig-category @@ -365,7 +365,7 @@ mod tests { } #[test] - // fusa:test REQ-CFG-008 + //fusa:test REQ-CFG-008 fn validate_accepts_rcp_config_tables_once_hw_configured() { let mut cfg = RcServerConfig { initial_state: RcServerState::HwConfigured, @@ -377,7 +377,7 @@ mod tests { } #[test] - // fusa:test REQ-CFG-008 + //fusa:test REQ-CFG-008 fn validate_accepts_hw_config_tables_while_hw_unconfigured() { // HwConfig-category tables (hw_pin_mapping) are reachable from the // very first state onward, unlike RcpConfig-category tables. diff --git a/src/conformance.rs b/src/conformance.rs index 786109c..1eaa6a0 100644 --- a/src/conformance.rs +++ b/src/conformance.rs @@ -1,9 +1,9 @@ -// fusa:req REQ-CONF-001 -// fusa:req REQ-CONF-002 -// fusa:req REQ-CONF-003 -// fusa:req REQ-CONF-004 -// fusa:req REQ-CONF-005 -// fusa:req REQ-CONF-006 +//fusa:req REQ-CONF-001 +//fusa:req REQ-CONF-002 +//fusa:req REQ-CONF-003 +//fusa:req REQ-CONF-004 +//fusa:req REQ-CONF-005 +//fusa:req REQ-CONF-006 //! Conformance test vectors — `ROADMAP.md` Milestone 10's last checklist //! item, "Conformance test vectors / interop verification against at least @@ -529,7 +529,7 @@ mod tests { // ── Self-referential golden vectors ─────────────────────────────────── #[test] - // fusa:test REQ-CONF-001 + //fusa:test REQ-CONF-001 fn ntscf_header_matches_golden_bytes_both_directions() { let hdr = golden::ntscf_header_fields(); let encoded = avtp::encode_ntscf_header(&hdr).unwrap(); @@ -546,7 +546,7 @@ mod tests { } #[test] - // fusa:test REQ-CONF-002 + //fusa:test REQ-CONF-002 fn tscf_header_matches_golden_bytes_both_directions() { let hdr = golden::tscf_header_fields(); // Sanity: this vector's avtp_timestamp must stay non-degenerate — @@ -568,7 +568,7 @@ mod tests { } #[test] - // fusa:test REQ-CONF-003 + //fusa:test REQ-CONF-003 fn acf_abb_matches_golden_bytes_both_directions() { let msg = golden::acf_abb_fields(); let encoded = acf::encode_acf_abb(&msg).unwrap(); @@ -585,7 +585,7 @@ mod tests { } #[test] - // fusa:test REQ-CONF-004 + //fusa:test REQ-CONF-004 fn acf_gbb_matches_golden_bytes_both_directions() { let msg = golden::acf_gbb_fields(); // Sanity: this vector's message_timestamp must stay non-zero — see @@ -605,7 +605,7 @@ mod tests { } #[test] - // fusa:test REQ-CONF-005 + //fusa:test REQ-CONF-005 fn ntscf_acf_abb_frame_matches_golden_bytes_both_directions() { let sid = StreamId::new(golden::SENDER_MAC_1, golden::UNIQUE_ID_1); let abb_bytes = acf::encode_acf_abb(&golden::acf_abb_fields()).unwrap(); @@ -627,7 +627,7 @@ mod tests { // ── go-RCP interop cross-check ────────────────────────────────────── #[test] - // fusa:test REQ-CONF-006 + //fusa:test REQ-CONF-006 fn go_rcp_bytes_diverge_from_this_crates_own_encoding() { // Per this module's doc comment: rust-RCP and go-RCP, both // independently interpreting the same (confidential) TC18 spec @@ -658,7 +658,7 @@ mod tests { } #[test] - // fusa:test REQ-CONF-006 + //fusa:test REQ-CONF-006 fn stream_id_bytes_agree_with_go_rcp_crosscheck() { // The one part of the cross-check that *does* agree: both // implementations place the 6-byte sender MAC in the high-order @@ -688,7 +688,7 @@ mod tests { } #[test] - // fusa:test REQ-CONF-006 + //fusa:test REQ-CONF-006 fn go_rcp_crosscheck_message_kind_discriminants_differ_from_this_crates_own() { // A narrower, explicit check on the "Message-kind discriminant" // divergence documented above, independent of the byte-array-wide diff --git a/src/deadline.rs b/src/deadline.rs index 5bb7afc..9a3a517 100644 --- a/src/deadline.rs +++ b/src/deadline.rs @@ -1,9 +1,9 @@ -// fusa:req REQ-DL-001 -// fusa:req REQ-DL-002 -// fusa:req REQ-DL-003 -// fusa:req REQ-DL-004 -// fusa:req REQ-DL-005 -// fusa:req REQ-DL-006 +//fusa:req REQ-DL-001 +//fusa:req REQ-DL-002 +//fusa:req REQ-DL-003 +//fusa:req REQ-DL-004 +//fusa:req REQ-DL-005 +//fusa:req REQ-DL-006 //! Deadline monitor — enforces a maximum call latency on an [`Endpoint`]. //! @@ -43,7 +43,7 @@ use crate::RcpError; /// Wraps an inner [`Endpoint`] with a configured deadline, honored by /// [`Self::read_with_deadline`]/[`Self::write_with_deadline`]. -// fusa:req REQ-DL-001 +//fusa:req REQ-DL-001 pub struct DeadlineEndpoint { inner: Arc, deadline: Duration, @@ -54,7 +54,7 @@ impl DeadlineEndpoint { /// /// # Panics /// Panics if `deadline` is zero (use the `Timeout` sentinel instead). - // fusa:req REQ-DL-002 + //fusa:req REQ-DL-002 pub fn new(inner: Arc, deadline: Duration) -> Self { assert!(!deadline.is_zero(), "deadline must be non-zero"); DeadlineEndpoint { inner, deadline } @@ -80,9 +80,9 @@ impl DeadlineEndpoint { /// Read from the inner endpoint, enforcing this deadline against /// `timeout` — see this module's doc comment for what "enforcing" /// means on this crate's synchronous `Endpoint` model. - // fusa:req REQ-DL-003 - // fusa:req REQ-DL-004 - // fusa:req REQ-DL-005 + //fusa:req REQ-DL-003 + //fusa:req REQ-DL-004 + //fusa:req REQ-DL-005 pub fn read_with_deadline( &self, read_size: u16, @@ -93,9 +93,9 @@ impl DeadlineEndpoint { } /// Same as [`Self::read_with_deadline`], for a write. - // fusa:req REQ-DL-003 - // fusa:req REQ-DL-004 - // fusa:req REQ-DL-005 + //fusa:req REQ-DL-003 + //fusa:req REQ-DL-004 + //fusa:req REQ-DL-005 pub fn write_with_deadline( &self, payload: &[u8], @@ -111,12 +111,12 @@ impl Endpoint for DeadlineEndpoint { self.inner.ep_type() } - // fusa:req REQ-DL-006 + //fusa:req REQ-DL-006 fn read(&self, read_size: u16) -> Result, RcpError> { self.inner.read(read_size) } - // fusa:req REQ-DL-006 + //fusa:req REQ-DL-006 fn write(&self, payload: &[u8]) -> Result<(), RcpError> { self.inner.write(payload) } @@ -135,8 +135,8 @@ mod tests { } #[test] - // fusa:test REQ-DL-001 - // fusa:test REQ-DL-003 + //fusa:test REQ-DL-001 + //fusa:test REQ-DL-003 fn passes_calls_to_inner() { let dl = DeadlineEndpoint::new(quick_endpoint(), Duration::from_secs(1)); dl.write_with_deadline(b"hi", None).unwrap(); @@ -144,7 +144,7 @@ mod tests { } #[test] - // fusa:test REQ-DL-002 + //fusa:test REQ-DL-002 fn deadline_getter() { let d = Duration::from_millis(500); let dl = DeadlineEndpoint::new(quick_endpoint(), d); @@ -152,7 +152,7 @@ mod tests { } #[test] - // fusa:test REQ-DL-004 + //fusa:test REQ-DL-004 fn zero_timeout_returns_timeout_error() { let dl = DeadlineEndpoint::new(quick_endpoint(), Duration::from_secs(1)); let err = dl @@ -165,7 +165,7 @@ mod tests { } #[test] - // fusa:test REQ-DL-005 + //fusa:test REQ-DL-005 fn shorter_caller_timeout_wins() { let dl = DeadlineEndpoint::new(quick_endpoint(), Duration::from_secs(10)); // If caller timeout is shorter, that is the effective timeout. @@ -174,7 +174,7 @@ mod tests { } #[test] - // fusa:test REQ-DL-006 + //fusa:test REQ-DL-006 fn plain_endpoint_impl_ignores_deadline() { let dl = DeadlineEndpoint::new(quick_endpoint(), Duration::from_secs(1)); let as_endpoint: &dyn Endpoint = &dl; diff --git a/src/discovery.rs b/src/discovery.rs index 08b03a6..7547346 100644 --- a/src/discovery.rs +++ b/src/discovery.rs @@ -1,24 +1,24 @@ -// fusa:req REQ-DISC-001 -// fusa:req REQ-DISC-002 -// fusa:req REQ-DISC-003 -// fusa:req REQ-DISC-004 -// fusa:req REQ-DISC-005 -// fusa:req REQ-DISC-006 -// fusa:req REQ-DISC-007 -// fusa:req REQ-DISC-008 -// fusa:req REQ-DISC-009 -// fusa:req REQ-DISC-010 -// fusa:req REQ-DISC-011 -// fusa:req REQ-DISC-012 -// fusa:req REQ-DISC-013 -// fusa:req REQ-DISC-014 -// fusa:req REQ-DISC-015 -// fusa:req REQ-DISC-016 -// fusa:req REQ-DISC-017 -// fusa:req REQ-DISC-018 -// fusa:req REQ-DISC-019 -// fusa:req REQ-DISC-020 -// fusa:req REQ-DISC-021 +//fusa:req REQ-DISC-001 +//fusa:req REQ-DISC-002 +//fusa:req REQ-DISC-003 +//fusa:req REQ-DISC-004 +//fusa:req REQ-DISC-005 +//fusa:req REQ-DISC-006 +//fusa:req REQ-DISC-007 +//fusa:req REQ-DISC-008 +//fusa:req REQ-DISC-009 +//fusa:req REQ-DISC-010 +//fusa:req REQ-DISC-011 +//fusa:req REQ-DISC-012 +//fusa:req REQ-DISC-013 +//fusa:req REQ-DISC-014 +//fusa:req REQ-DISC-015 +//fusa:req REQ-DISC-016 +//fusa:req REQ-DISC-017 +//fusa:req REQ-DISC-018 +//fusa:req REQ-DISC-019 +//fusa:req REQ-DISC-020 +//fusa:req REQ-DISC-021 //! Discovery request/response, discovery-stream claiming, multi-client //! coexistence, and the client-side discovery cache — TC18 register-map @@ -338,7 +338,7 @@ pub const DISCOVERY_BROADCAST_STREAM_ID: StreamId = StreamId { /// Is `stream_id` this module's sentinel broadcast discovery address? /// /// Never panics for any input. -// fusa:req REQ-DISC-001 +//fusa:req REQ-DISC-001 pub fn is_discovery_broadcast_stream_id(stream_id: StreamId) -> bool { stream_id == DISCOVERY_BROADCAST_STREAM_ID } @@ -377,7 +377,25 @@ fn decode_register_address(payload: &[u8]) -> Option { /// transaction-id allocation, which remains a caller/later-milestone /// concern. The result always satisfies [`is_discovery_request`]. Never /// panics for any input. -// fusa:req REQ-DISC-002 +/// +/// The header fields this builds match TC18 §12.6.1 Table 16 "Discovery +/// request" (TC18.txt lines 2370-2381): `acf_msg_type` = `ACF_ABB`, +/// `byte_bus_id` = `00000000000b`, `evt` = `0000b`, and a read-direction +/// `op` — §12.6.1's own opening sentence (TC18.txt line 2365) is "A +/// discovery request is a read request", and TC18 encodes a read as +/// `op = 0` (the request-header tables' "if op = 0 this is read_size, else +/// segment_num", TC18.txt line 1169, and §12.9's "op=0 (read request)", +/// TC18.txt line 3207). Table 16's own `op` row reads "1b (read request)", +/// which contradicts both and is treated here as a specification typo. +/// +/// One Table 16 row this deliberately does **not** match: +/// `byte_msg_payload` is specified as "none", whereas this builder emits +/// the [`DISCOVERY_REGISTER_ADDRESS_LEN`]-byte register-address prefix this +/// module's Provenance note describes. See requirement `REQ-DISC-027` in +/// `.fusa-reqs.json`, which records that divergence rather than papering +/// over it. +//fusa:req REQ-DISC-002 +//fusa:req REQ-DISC-025 pub fn build_discovery_request(transaction_num: u8) -> AcfAbbMessage { AcfAbbMessage { info: ByteMessageInfo { @@ -400,8 +418,8 @@ pub fn build_discovery_request(transaction_num: u8) -> AcfAbbMessage { /// bytes beyond the register-address prefix, if any, are ignored — this /// function does not itself decide whether extra payload content is /// otherwise meaningful or malformed. Never panics for any input. -// fusa:req REQ-DISC-002 -// fusa:req REQ-DISC-003 +//fusa:req REQ-DISC-002 +//fusa:req REQ-DISC-003 pub fn is_discovery_request(msg: &AcfAbbMessage) -> bool { route_byte_bus_id(msg.info.byte_bus_id) == RequestRoute::Ep0 && access_kind(&msg.info) == Ep0AccessKind::Read @@ -440,7 +458,7 @@ pub fn is_discovery_request(msg: &AcfAbbMessage) -> bool { /// /// Trailing payload bytes beyond the register-address prefix, if any, are /// ignored, mirroring [`is_discovery_request`]. Never panics for any input. -// fusa:req REQ-DISC-021 +//fusa:req REQ-DISC-021 pub fn is_discovery_configure_request(msg: &AcfAbbMessage) -> bool { route_byte_bus_id(msg.info.byte_bus_id) == RequestRoute::Ep0 && access_kind(&msg.info) == Ep0AccessKind::Write @@ -466,7 +484,24 @@ pub fn is_discovery_configure_request(msg: &AcfAbbMessage) -> bool { /// caller-supplied value, exactly as [`crate::ep0::check_ep0_access`] /// performs no register I/O either — see that module's own doc comment for /// why. Never panics for any input. -// fusa:req REQ-DISC-004 +/// +/// The header fields this builds match TC18 §12.6.2 Table 17 "Discovery +/// response" (TC18.txt lines 2414-2428): `acf_msg_type` = `ACF_ABB`, +/// `byte_bus_id` = `00000000000b` (here by echo-back of the request's own +/// EP0 `byte_bus_id`), `evt` = `0000b`, a read-direction `op` (see +/// [`build_discovery_request`] for why a read is `op = 0` despite Table +/// 17's "1b (read request)" row), and a `byte_msg_payload` that "contains +/// RC Server's register map content starting from address 0x00000". +/// +/// Two Table 17 rows this deliberately does **not** implement: the +/// response length is *not* clamped to the discovery request's `read_size` +/// (`acf_msg_length ≤ read_size of discovery request`, TC18.txt line +/// 2422), and this function builds no `stream_id` at all, so Table 17's +/// `stream_id = localMAC + unique_id 0x0000` row is a transport-level +/// concern here. See requirements `REQ-DISC-029`/`REQ-DISC-030` in +/// `.fusa-reqs.json`, which record both gaps. +//fusa:req REQ-DISC-004 +//fusa:req REQ-DISC-026 pub fn build_discovery_response( request: &ByteMessageInfo, state: RcServerState, @@ -520,7 +555,7 @@ impl DiscoveryClaim { /// an `now` that is (incorrectly) earlier than [`Self::claimed_at`] /// reads as zero elapsed time rather than panicking or wrapping. Never /// panics for any input. - // fusa:req REQ-DISC-006 + //fusa:req REQ-DISC-006 pub fn has_lapsed(&self, now: Instant, timeout: Duration) -> bool { now.saturating_duration_since(self.claimed_at) >= timeout } @@ -547,28 +582,28 @@ impl DiscoveryClaim { /// an exclusive claim. /// /// Never panics for any input. -// fusa:req REQ-DISC-007 -// fusa:req REQ-DISC-008 -// fusa:req REQ-DISC-009 +//fusa:req REQ-DISC-007 +//fusa:req REQ-DISC-008 +//fusa:req REQ-DISC-009 pub fn try_claim_discovery_stream( current: Option, claimant: StreamId, now: Instant, timeout: Duration, ) -> Result { - // fusa:req REQ-DISC-009 + //fusa:req REQ-DISC-009 if is_discovery_broadcast_stream_id(claimant) { return Err(RcpError::InvalidParameter); } - // fusa:req REQ-DISC-007 + //fusa:req REQ-DISC-007 if claim_permits(current, claimant, now, timeout) { Ok(DiscoveryClaim { claimant, claimed_at: now, }) } else { - // fusa:req REQ-DISC-008 + //fusa:req REQ-DISC-008 Err(RcpError::RequestRejected) } } @@ -605,7 +640,7 @@ fn claim_permits( /// rationale, including why this is a caller-supplied value rather than one /// [`check_discovery_access`] derives from a decoded message itself. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// fusa:req REQ-DISC-011 +//fusa:req REQ-DISC-011 pub enum DiscoveryAccessKind { /// Reading discovery info — the broadcast discovery request/response /// mechanism ([`build_discovery_request`]/[`build_discovery_response`]). @@ -637,9 +672,9 @@ pub enum DiscoveryAccessKind { /// Performs no register I/O and does not mutate `current` — like /// [`try_claim_discovery_stream`], this function only ever answers a /// question about caller-supplied state. Never panics for any input. -// fusa:req REQ-DISC-012 -// fusa:req REQ-DISC-013 -// fusa:req REQ-DISC-014 +//fusa:req REQ-DISC-012 +//fusa:req REQ-DISC-013 +//fusa:req REQ-DISC-014 pub fn check_discovery_access( current: Option, requester: StreamId, @@ -650,16 +685,16 @@ pub fn check_discovery_access( match kind { DiscoveryAccessKind::Read => Ok(()), DiscoveryAccessKind::Configure => { - // fusa:req REQ-DISC-014 + //fusa:req REQ-DISC-014 if is_discovery_broadcast_stream_id(requester) { return Err(RcpError::InvalidParameter); } - // fusa:req REQ-DISC-012 + //fusa:req REQ-DISC-012 if claim_permits(current, requester, now, timeout) { Ok(()) } else { - // fusa:req REQ-DISC-013 + //fusa:req REQ-DISC-013 Err(RcpError::UnauthorizedAccess) } } @@ -740,7 +775,7 @@ impl DiscoveryCacheEntry { /// as zero elapsed time rather than panicking or wrapping. `max_age` is /// entirely caller-supplied — see this module's Provenance note for why /// no default is provided here. Never panics for any input. - // fusa:req REQ-DISC-018 + //fusa:req REQ-DISC-018 pub fn is_stale(&self, now: Instant, max_age: Duration) -> bool { now.saturating_duration_since(self.cached_at) >= max_age } @@ -754,7 +789,7 @@ impl DiscoveryCacheEntry { /// [`StreamId`] — a real re-discovery is warranted, not merely a cache /// refresh — see this module's "Client-side discovery cache" section. /// Never panics for any input. - // fusa:req REQ-DISC-019 + //fusa:req REQ-DISC-019 pub fn matches(&self, general: &GeneralRegisters) -> bool { self.svr_oa_tc18_magic_nr == general.svr_oa_tc18_magic_nr && self.svr_version == general.svr_version @@ -794,8 +829,8 @@ impl DiscoveryCache { /// [`try_claim_discovery_stream`]'s own rejection of it as a claimant. /// An existing entry for `stream_id`, if any, is overwritten rather than /// preserved. Never panics for any input. - // fusa:req REQ-DISC-016 - // fusa:req REQ-DISC-017 + //fusa:req REQ-DISC-016 + //fusa:req REQ-DISC-017 pub fn remember( &mut self, stream_id: StreamId, @@ -830,8 +865,8 @@ impl DiscoveryCache { /// [`build_discovery_request`]'s broadcast exchange for `stream_id` when /// this returns `true`, and falls back to real discovery otherwise /// (unknown, or known but stale). Never panics for any input. - // fusa:req REQ-DISC-016 - // fusa:req REQ-DISC-018 + //fusa:req REQ-DISC-016 + //fusa:req REQ-DISC-018 pub fn is_known(&self, stream_id: StreamId, now: Instant, max_age: Duration) -> bool { self.entries .get(&stream_id) @@ -846,7 +881,7 @@ impl DiscoveryCache { /// Provenance note for why claim state and cache state are deliberately /// kept independent; a caller that wants that coupling calls this /// explicitly. Never panics for any input. - // fusa:req REQ-DISC-019 + //fusa:req REQ-DISC-019 pub fn invalidate(&mut self, stream_id: StreamId) -> bool { self.entries.remove(&stream_id).is_some() } @@ -889,7 +924,7 @@ mod tests { // ── Broadcast addressing ───────────────────────────────────────────── #[test] - // fusa:test REQ-DISC-001 + //fusa:test REQ-DISC-001 fn is_discovery_broadcast_stream_id_true_only_for_the_sentinel() { assert!(is_discovery_broadcast_stream_id( DISCOVERY_BROADCAST_STREAM_ID @@ -914,7 +949,7 @@ mod tests { // ── Discovery request ───────────────────────────────────────────────── #[test] - // fusa:test REQ-DISC-002 + //fusa:test REQ-DISC-002 fn build_discovery_request_is_recognized_by_is_discovery_request() { for transaction_num in [0u8, 1, 0x42, 0xFF] { let request = build_discovery_request(transaction_num); @@ -926,7 +961,7 @@ mod tests { } #[test] - // fusa:test REQ-DISC-002 + //fusa:test REQ-DISC-002 fn build_discovery_request_round_trips_through_acf_abb_encode_decode() { let request = build_discovery_request(0x11); let frame = crate::acf::encode_acf_abb(&request).unwrap(); @@ -951,7 +986,7 @@ mod tests { } #[test] - // fusa:test REQ-DISC-003 + //fusa:test REQ-DISC-003 fn is_discovery_request_rejects_non_ep0_byte_bus_id() { let mut request = build_discovery_request(0); request.info.byte_bus_id = 1; @@ -959,7 +994,7 @@ mod tests { } #[test] - // fusa:test REQ-DISC-003 + //fusa:test REQ-DISC-003 fn is_discovery_request_rejects_write_direction() { let mut request = build_discovery_request(0); request.info.op = true; @@ -967,7 +1002,7 @@ mod tests { } #[test] - // fusa:test REQ-DISC-003 + //fusa:test REQ-DISC-003 fn is_discovery_request_rejects_mismatched_register_address() { let mut request = build_discovery_request(0); request.payload = 7u16.to_be_bytes().to_vec(); @@ -975,7 +1010,7 @@ mod tests { } #[test] - // fusa:test REQ-DISC-003 + //fusa:test REQ-DISC-003 fn is_discovery_request_rejects_short_payload() { let mut request = build_discovery_request(0); request.payload = vec![0x00]; @@ -986,17 +1021,83 @@ mod tests { } #[test] - // fusa:test REQ-DISC-003 + //fusa:test REQ-DISC-003 fn is_discovery_request_ignores_trailing_payload_bytes() { let mut request = build_discovery_request(0); request.payload.extend_from_slice(&[0xAA, 0xBB, 0xCC]); assert!(is_discovery_request(&request)); } + // ── TC18 §12.6.1 Table 16 / §12.6.2 Table 17 field conformance ────────── + + /// TC18 §12.6.1 Table 16 "Discovery request" (TC18.txt lines + /// 2370-2381), asserted against the *encoded* frame rather than the + /// in-memory struct, so the bit positions are exercised too. Expected + /// values are written out as literals taken from Table 16: + /// + /// | Table 16 field | Required value | + /// |----------------|----------------| + /// | `acf_msg_type` | `ACF_ABB` (`0x0E`) | + /// | `Byte_bus_id` | `00000000000b` | + /// | `evt` | `0000b` | + /// | `op` | read request | + /// + /// `op` is asserted as `0`: §12.6.1's opening sentence (TC18.txt line + /// 2365) says "A discovery request is a read request", and TC18 encodes + /// a read as `op = 0` ("if op = 0 this is read_size, else segment_num", + /// TC18.txt line 1169; "op=0 (read request)", TC18.txt line 3207). + /// Table 16's own "1b (read request)" row contradicts both. + #[test] + //fusa:test REQ-DISC-025 + fn tc18_table_16_discovery_request_header_fields_match_the_required_values() { + let frame = crate::acf::encode_acf_abb(&build_discovery_request(0x42)).unwrap(); + + // octet 0 bits 7:1 — acf_msg_type; ACF_ABB is 0x0E. + assert_eq!(frame[0] >> 1, 0x0E); + // byte_bus_id, 11 bits: octet 2 bits 2:0 (high 3) || octet 3 (low 8). + assert_eq!(frame[2] & 0x07, 0x00); + assert_eq!(frame[3], 0x00); + // octet 4 bits 7:4 — evt (ack || sub_opcode) = 0000b. + assert_eq!(frame[4] & 0xF0, 0x00); + // octet 6 bit 7 — op = 0 (read request). + assert_eq!(frame[6] & 0x80, 0x00); + // octet 5 — transaction_num, passed through unchanged. + assert_eq!(frame[5], 0x42); + } + + /// TC18 §12.6.2 Table 17 "Discovery response" (TC18.txt lines + /// 2414-2428), asserted against the encoded frame with literal expected + /// values taken from Table 17: `acf_msg_type` = `ACF_ABB` (`0x0E`), + /// `Byte_bus_id` = `00000000000b`, `evt` = `0000b`, `op` = read (see + /// the Table 16 test above for the op-bit polarity), and + /// `byte_msg_payload` = "RC Server's register map content starting from + /// address 0x00000". + #[test] + //fusa:test REQ-DISC-026 + fn tc18_table_17_discovery_response_header_fields_match_the_required_values() { + let general = sample_general_registers(); + let request = build_discovery_request(0x42); + let response = + build_discovery_response(&request.info, RcServerState::HwUnconfigured, &general) + .unwrap(); + let frame = crate::acf::encode_acf_abb(&response).unwrap(); + + assert_eq!(frame[0] >> 1, 0x0E); // acf_msg_type = ACF_ABB + assert_eq!(frame[2] & 0x07, 0x00); // byte_bus_id[10:8] = 000b + assert_eq!(frame[3], 0x00); // byte_bus_id[7:0] = 0x00 + assert_eq!(frame[4] & 0xF0, 0x00); // evt = 0000b + assert_eq!(frame[6] & 0x80, 0x00); // op = 0 (read) + + // byte_msg_payload: the register map starting at address 0x0000, + // immediately after the 8-octet byte_message_info header. + let encoded_general = general.encode(); + assert_eq!(&frame[8..8 + encoded_general.len()], &encoded_general[..]); + } + // ── is_discovery_configure_request ────────────────────────────────────── #[test] - // fusa:test REQ-DISC-021 + //fusa:test REQ-DISC-021 fn is_discovery_configure_request_recognizes_the_write_direction_shape() { let mut request = build_discovery_request(0); request.info.op = true; @@ -1007,7 +1108,7 @@ mod tests { } #[test] - // fusa:test REQ-DISC-021 + //fusa:test REQ-DISC-021 fn is_discovery_configure_request_and_is_discovery_request_are_mutually_exclusive() { for op in [false, true] { let mut request = build_discovery_request(0); @@ -1020,7 +1121,7 @@ mod tests { } #[test] - // fusa:test REQ-DISC-021 + //fusa:test REQ-DISC-021 fn is_discovery_configure_request_rejects_non_matching_shapes() { let mut request = build_discovery_request(0); request.info.op = true; @@ -1043,7 +1144,7 @@ mod tests { } #[test] - // fusa:test REQ-DISC-021 + //fusa:test REQ-DISC-021 fn is_discovery_configure_request_ignores_trailing_payload_bytes() { let mut request = build_discovery_request(0); request.info.op = true; @@ -1054,7 +1155,7 @@ mod tests { // ── Discovery response: answerable in any lifecycle state ───────────── #[test] - // fusa:test REQ-DISC-004 + //fusa:test REQ-DISC-004 fn discovery_response_succeeds_in_every_lifecycle_state() { let general = sample_general_registers(); let request = build_discovery_request(0x55).info; @@ -1068,7 +1169,7 @@ mod tests { } #[test] - // fusa:test REQ-DISC-004 + //fusa:test REQ-DISC-004 fn discovery_response_echoes_request_byte_bus_id_per_echo_back_rule() { let general = sample_general_registers(); let request = ByteMessageInfo { @@ -1086,7 +1187,7 @@ mod tests { } #[test] - // fusa:test REQ-DISC-004 + //fusa:test REQ-DISC-004 fn discovery_response_round_trips_through_acf_abb_encode_decode() { let general = sample_general_registers(); let request = build_discovery_request(0x22).info; @@ -1122,7 +1223,7 @@ mod tests { } #[test] - // fusa:test REQ-DISC-007 + //fusa:test REQ-DISC-007 fn first_claimant_wins_an_unclaimed_stream() { let now = Instant::now(); let claim = try_claim_discovery_stream(None, client_a(), now, DISCOVERY_TIME_OUT) @@ -1132,7 +1233,7 @@ mod tests { } #[test] - // fusa:test REQ-DISC-008 + //fusa:test REQ-DISC-008 fn second_different_claimant_is_rejected_while_claim_is_live() { let claimed_at = Instant::now(); let claim = @@ -1145,7 +1246,7 @@ mod tests { } #[test] - // fusa:test REQ-DISC-007 + //fusa:test REQ-DISC-007 fn same_claimant_may_refresh_its_own_live_claim() { let claimed_at = Instant::now(); let claim = @@ -1159,8 +1260,8 @@ mod tests { } #[test] - // fusa:test REQ-DISC-006 - // fusa:test REQ-DISC-007 + //fusa:test REQ-DISC-006 + //fusa:test REQ-DISC-007 fn lapsed_claim_reopens_to_a_new_claimant() { let claimed_at = Instant::now(); let claim = @@ -1178,7 +1279,7 @@ mod tests { } #[test] - // fusa:test REQ-DISC-006 + //fusa:test REQ-DISC-006 fn has_lapsed_boundary_is_inclusive_of_the_exact_timeout() { let claimed_at = Instant::now(); let claim = DiscoveryClaim { @@ -1192,7 +1293,7 @@ mod tests { } #[test] - // fusa:test REQ-DISC-006 + //fusa:test REQ-DISC-006 fn has_lapsed_never_panics_when_now_precedes_claimed_at() { let claimed_at = Instant::now(); let claim = DiscoveryClaim { @@ -1206,7 +1307,7 @@ mod tests { } #[test] - // fusa:test REQ-DISC-009 + //fusa:test REQ-DISC-009 fn broadcast_sentinel_is_never_an_eligible_claimant() { let now = Instant::now(); let err = try_claim_discovery_stream( @@ -1236,8 +1337,8 @@ mod tests { // ── Multi-client coexistence: read/configure access-kind distinction ─── #[test] - // fusa:test REQ-DISC-011 - // fusa:test REQ-DISC-012 + //fusa:test REQ-DISC-011 + //fusa:test REQ-DISC-012 fn read_access_always_succeeds_and_never_consults_claim_state() { let now = Instant::now(); // Unclaimed. @@ -1278,7 +1379,7 @@ mod tests { } #[test] - // fusa:test REQ-DISC-012 + //fusa:test REQ-DISC-012 fn configure_succeeds_on_an_unclaimed_stream() { let now = Instant::now(); assert_eq!( @@ -1294,7 +1395,7 @@ mod tests { } #[test] - // fusa:test REQ-DISC-012 + //fusa:test REQ-DISC-012 fn configure_succeeds_for_the_live_claimant() { let claimed_at = Instant::now(); let claim = @@ -1313,7 +1414,7 @@ mod tests { } #[test] - // fusa:test REQ-DISC-013 + //fusa:test REQ-DISC-013 fn configure_rejects_a_different_live_claimant() { let claimed_at = Instant::now(); let claim = @@ -1332,7 +1433,7 @@ mod tests { } #[test] - // fusa:test REQ-DISC-012 + //fusa:test REQ-DISC-012 fn configure_succeeds_for_any_requester_once_the_claim_has_lapsed() { let claimed_at = Instant::now(); let claim = @@ -1351,7 +1452,7 @@ mod tests { } #[test] - // fusa:test REQ-DISC-014 + //fusa:test REQ-DISC-014 fn configure_rejects_the_broadcast_sentinel_regardless_of_claim_state() { let now = Instant::now(); // Unclaimed. @@ -1392,8 +1493,8 @@ mod tests { } #[test] - // fusa:test REQ-DISC-012 - // fusa:test REQ-DISC-013 + //fusa:test REQ-DISC-012 + //fusa:test REQ-DISC-013 fn check_discovery_access_configure_agrees_with_try_claim_discovery_stream_grant_decision() { // check_discovery_access's Configure gate and // try_claim_discovery_stream's grant decision share the same @@ -1445,7 +1546,7 @@ mod tests { } #[test] - // fusa:test REQ-DISC-016 + //fusa:test REQ-DISC-016 fn discovery_cache_remember_then_lookup_round_trips_the_cache_worthy_subset() { let mut cache = DiscoveryCache::new(); assert!(cache.is_empty()); @@ -1469,7 +1570,7 @@ mod tests { } #[test] - // fusa:test REQ-DISC-016 + //fusa:test REQ-DISC-016 fn discovery_cache_remember_overwrites_an_existing_entry_for_the_same_stream_id() { let mut cache = DiscoveryCache::new(); let first = Instant::now(); @@ -1492,7 +1593,7 @@ mod tests { } #[test] - // fusa:test REQ-DISC-017 + //fusa:test REQ-DISC-017 fn discovery_cache_remember_rejects_the_broadcast_sentinel_as_stream_id() { let mut cache = DiscoveryCache::new(); let err = cache @@ -1507,7 +1608,7 @@ mod tests { } #[test] - // fusa:test REQ-DISC-018 + //fusa:test REQ-DISC-018 fn discovery_cache_is_known_reflects_staleness_under_max_age() { let mut cache = DiscoveryCache::new(); let cached_at = Instant::now(); @@ -1528,7 +1629,7 @@ mod tests { } #[test] - // fusa:test REQ-DISC-018 + //fusa:test REQ-DISC-018 fn discovery_cache_entry_is_stale_boundary_is_inclusive_and_never_panics_on_out_of_order_now() { let cached_at = Instant::now(); let general = sample_general_registers(); @@ -1546,7 +1647,7 @@ mod tests { } #[test] - // fusa:test REQ-DISC-019 + //fusa:test REQ-DISC-019 fn discovery_cache_entry_matches_detects_an_identity_change() { let general = sample_general_registers(); let entry = DiscoveryCacheEntry::from_general_registers(&general, Instant::now()); @@ -1558,7 +1659,7 @@ mod tests { } #[test] - // fusa:test REQ-DISC-019 + //fusa:test REQ-DISC-019 fn discovery_cache_invalidate_removes_the_entry_and_is_idempotent() { let mut cache = DiscoveryCache::new(); cache @@ -1573,7 +1674,7 @@ mod tests { } #[test] - // fusa:test REQ-DISC-016 + //fusa:test REQ-DISC-016 fn discovery_cache_holds_independent_entries_across_multiple_stream_ids() { let mut cache = DiscoveryCache::new(); let now = Instant::now(); @@ -1602,7 +1703,7 @@ mod tests { // ── Never panics ──────────────────────────────────────────────────────── #[test] - // fusa:test REQ-DISC-010 + //fusa:test REQ-DISC-010 fn try_claim_discovery_stream_never_panics_across_sampled_inputs() { let mut state: u32 = 0xC1A1_0DEC; let mut next = || { @@ -1633,7 +1734,7 @@ mod tests { } #[test] - // fusa:test REQ-DISC-015 + //fusa:test REQ-DISC-015 fn check_discovery_access_never_panics_across_sampled_inputs() { let mut state: u32 = 0xACCE_5501; let mut next = || { @@ -1668,7 +1769,7 @@ mod tests { } #[test] - // fusa:test REQ-DISC-005 + //fusa:test REQ-DISC-005 fn is_discovery_request_never_panics_on_arbitrary_payloads() { let mut state: u32 = 0xD15C_0BE1; let mut next = || { @@ -1697,7 +1798,7 @@ mod tests { } #[test] - // fusa:test REQ-DISC-005 + //fusa:test REQ-DISC-005 fn build_discovery_response_never_panics_for_any_state() { let general = sample_general_registers(); for state in ALL_STATES { @@ -1707,7 +1808,7 @@ mod tests { } #[test] - // fusa:test REQ-DISC-005 + //fusa:test REQ-DISC-005 fn is_discovery_broadcast_stream_id_never_panics_across_sampled_values() { let mut state: u32 = 0xB0AD_CA57; let mut next = || { @@ -1731,7 +1832,7 @@ mod tests { } #[test] - // fusa:test REQ-DISC-020 + //fusa:test REQ-DISC-020 fn discovery_cache_operations_never_panic_across_sampled_inputs() { let mut state: u32 = 0xCAC4_E5EE; let mut next = || { diff --git a/src/dyndata.rs b/src/dyndata.rs index f84249e..d11d217 100644 --- a/src/dyndata.rs +++ b/src/dyndata.rs @@ -1,8 +1,8 @@ -// fusa:req REQ-DYN-001 -// fusa:req REQ-DYN-002 -// fusa:req REQ-DYN-003 -// fusa:req REQ-DYN-004 -// fusa:req REQ-DYN-005 +//fusa:req REQ-DYN-001 +//fusa:req REQ-DYN-002 +//fusa:req REQ-DYN-003 +//fusa:req REQ-DYN-004 +//fusa:req REQ-DYN-005 //! Dynamic data store — runtime key/value parameters accessible to controllers. //! @@ -15,7 +15,7 @@ use std::sync::RwLock; // ── DynStore ────────────────────────────────────────────────────────────────── /// Thread-safe dynamic parameter store. -// fusa:req REQ-DYN-001 +//fusa:req REQ-DYN-001 pub struct DynStore { data: RwLock>>, } @@ -28,25 +28,25 @@ impl DynStore { } /// Insert or replace a parameter. - // fusa:req REQ-DYN-002 + //fusa:req REQ-DYN-002 pub fn set(&self, key: impl Into, value: Vec) { self.data.write().unwrap().insert(key.into(), value); } /// Retrieve a parameter value, or `None` if not present. - // fusa:req REQ-DYN-003 + //fusa:req REQ-DYN-003 pub fn get(&self, key: &str) -> Option> { self.data.read().unwrap().get(key).cloned() } /// Delete a parameter. Returns `true` if it existed. - // fusa:req REQ-DYN-004 + //fusa:req REQ-DYN-004 pub fn delete(&self, key: &str) -> bool { self.data.write().unwrap().remove(key).is_some() } /// All parameter keys currently present. - // fusa:req REQ-DYN-005 + //fusa:req REQ-DYN-005 pub fn keys(&self) -> Vec { self.data.read().unwrap().keys().cloned().collect() } @@ -75,14 +75,14 @@ mod tests { use super::*; #[test] - // fusa:test REQ-DYN-001 + //fusa:test REQ-DYN-001 fn new_store_is_empty() { assert!(DynStore::new().is_empty()); } #[test] - // fusa:test REQ-DYN-002 - // fusa:test REQ-DYN-003 + //fusa:test REQ-DYN-002 + //fusa:test REQ-DYN-003 fn set_and_get() { let s = DynStore::new(); s.set("key", b"value".to_vec()); @@ -90,13 +90,13 @@ mod tests { } #[test] - // fusa:test REQ-DYN-003 + //fusa:test REQ-DYN-003 fn get_absent_returns_none() { assert!(DynStore::new().get("missing").is_none()); } #[test] - // fusa:test REQ-DYN-004 + //fusa:test REQ-DYN-004 fn delete_removes_key() { let s = DynStore::new(); s.set("k", vec![1]); @@ -106,7 +106,7 @@ mod tests { } #[test] - // fusa:test REQ-DYN-005 + //fusa:test REQ-DYN-005 fn keys_lists_all() { let s = DynStore::new(); s.set("a", vec![]); @@ -117,7 +117,7 @@ mod tests { } #[test] - // fusa:test REQ-DYN-002 + //fusa:test REQ-DYN-002 fn overwrite_replaces_value() { let s = DynStore::new(); s.set("k", b"v1".to_vec()); diff --git a/src/e2e.rs b/src/e2e.rs index 135a170..2ccfdc8 100644 --- a/src/e2e.rs +++ b/src/e2e.rs @@ -1,13 +1,13 @@ -// fusa:req REQ-CRC-001 -// fusa:req REQ-CRC-002 -// fusa:req REQ-CRC-003 -// fusa:req REQ-CRC-004 -// fusa:req REQ-CRC-005 -// fusa:req REQ-CRC-006 -// fusa:req REQ-CRC-007 -// fusa:req REQ-CRC-008 -// fusa:req REQ-CRC-009 -// fusa:req REQ-CRC-010 +//fusa:req REQ-CRC-001 +//fusa:req REQ-CRC-002 +//fusa:req REQ-CRC-003 +//fusa:req REQ-CRC-004 +//fusa:req REQ-CRC-005 +//fusa:req REQ-CRC-006 +//fusa:req REQ-CRC-007 +//fusa:req REQ-CRC-008 +//fusa:req REQ-CRC-009 +//fusa:req REQ-CRC-010 //! End-to-end protection: the OPEN Alliance TC18 safe-point CRC-32 //! mechanism (polynomial `0xF4ACFB13`). @@ -46,12 +46,25 @@ //! `avtp_timestamp` apply, and [`AcfCoverageMessage`] to decide which ACF //! header shape applies, rather than re-deriving either decision. //! -//! ### Working interpretation: which length field gets the one-quadlet -//! pre-adjustment (Guiding Principle 5) +//! ### Which length field gets the one-quadlet pre-adjustment +//! (working interpretation, since reconciled against TC18 §13.6) //! -//! The Milestone 6 checklist states a length field is pre-adjusted by one -//! quadlet (4 octets) before the CRC is computed over it, but does not say -//! which length field. Of this crate's currently-decoded length fields, +//! **Reconciled.** TC18 §13.6 (TC18.txt line 3798) names the field +//! outright: "Before CRC calculation it is essential to adapt the +//! acf_message_length by plus 1 quadlet for the CRC32 addition to the +//! payload and in the AVTPD to increase the ntscf_data_length or in an TSCF +//! header the stream_data_length by 4 octets per ACF type in the ACF +//! payload being E-2-E protected." The `acf_msg_length + 1 quadlet` half of +//! that clause is what [`build_crc32_coverage_buffer`] implements, and the +//! working interpretation recorded below turned out to be correct. The +//! **second** half — increasing the AVTPDU-level `ntscf_data_length` / +//! `stream_data_length` by 4 octets per E-2-E-protected ACF type — is *not* +//! implemented anywhere in this crate; see requirement `REQ-CRC-016`. +//! +//! The original reasoning, kept for the record: the Milestone 6 checklist +//! stated a length field is pre-adjusted by one quadlet (4 octets) before +//! the CRC is computed over it, but did not say which length field. Of +//! this crate's currently-decoded length fields, //! `byte_message_info`'s `acf_msg_length` is the only one that lives inside //! the region this coverage rule actually covers (`stream_id`/ //! `avtp_timestamp`/ACF-header/payload) — the AVTP-level @@ -62,10 +75,7 @@ //! encodes into the coverage buffer's ACF header is the caller-supplied //! `ByteMessageInfo::acf_msg_length` plus one quadlet (since //! rust-RCP-W01/W02, `acf_msg_length` is itself counted in quadlets, so -//! this is a raw `+1`, not `+4`). This is this crate's own working -//! interpretation, not a spec-confirmed fact, and is flagged here for -//! reconciliation against real TC18 behavior (never against spec prose) -//! before being relied on for interop. +//! this is a raw `+1`, not `+4`). //! //! ## Provenance note: `crc32_tc18` verified by cross-implementation, not //! by a published check value @@ -172,10 +182,23 @@ //! [`AcfCoverageMessage`] — mirroring [`build_crc32_coverage_buffer`]'s //! existing single-message behavior with the payload field alone replaced //! by the train's combined payload — rather than inventing a multi- -//! fragment header-combination rule the roadmap text does not state. This -//! is this crate's own working interpretation, flagged here for -//! reconciliation against real TC18 behavior (never against spec prose) -//! before being relied on for interop. +//! fragment header-combination rule the roadmap text does not state. +//! +//! **Reconciled against TC18 §13.6 — and this working interpretation turned +//! out to be WRONG.** TC18 §13.6 (TC18.txt line 3801) states: "For +//! fragmented requests or responses going through CRC calculation only the +//! *first* AVTPDU and ACF header data will be used and the payload of all +//! segments." rust-RCP uses the *final* fragment's AVTPDU/ACF header +//! instead. This is a real, known divergence from TC18. It is recorded +//! honestly as requirement `REQ-CRC-015` rather than papered over, and is +//! deliberately left unfixed here: correcting it changes what +//! [`build_crc32_coverage_buffer_for_fragment_train`]/ +//! [`crc32_tc18_for_fragment_train`]/ +//! [`crate::fragment::verify_reassembled_train_crc`] take as their header +//! argument, so it belongs in its own change rather than in a +//! requirements-coverage pass. Interop with a conformant TC18 peer will +//! fail for any multi-fragment E-2-E-protected message whose first and last +//! fragments' AVTPDU/ACF header fields differ. use crate::acf::{self, AcfAbbMessage, AcfGbbMessage}; use crate::avtp::HeaderVariant; @@ -191,6 +214,12 @@ use crate::RcpError; /// this module's provenance note above; the unreversed polynomial itself /// reappears in `tests::crc32_tc18_reference`, the independent /// cross-check. +/// +/// TC18 §13.6 Table 31 "CRC32 Polynomial" (TC18.txt line 3792) names this +/// CRC "CRC32P4" and fixes its six parameters: Polynomial `0xF4ACFB13`, +/// Width 32 bit, Initial Value `0xFFFFFFFF`, Final XOR `0xFFFFFFFF`, Input +/// reflection TRUE, Output reflection TRUE. +//fusa:req REQ-CRC-014 const CRC32_TC18_POLY_REFLECTED: u32 = 0xC8DF_352F; /// Initial and final-XOR value, `0xFFFFFFFF`, shared by both ends of the @@ -205,8 +234,8 @@ const CRC32_TC18_INIT_XOROUT: u32 = 0xFFFF_FFFF; /// This function computes the CRC over exactly the bytes it is given; it /// takes no position on which bytes of a safe-point frame belong in that /// slice (see this module's provenance note above). -// fusa:req REQ-CRC-001 -// fusa:req REQ-CRC-002 +//fusa:req REQ-CRC-001 +//fusa:req REQ-CRC-002 pub fn crc32_tc18(data: &[u8]) -> u32 { let mut crc = CRC32_TC18_INIT_XOROUT; for &byte in data { @@ -290,10 +319,27 @@ pub enum AcfCoverageMessage<'a> { /// discipline: not called from [`crc32_tc18`] or a decoder/dispatch loop — /// this function only assembles the buffer a caller would pass to /// `crc32_tc18` and `crate::request`'s `CRC_ERROR` dispatch path. -// fusa:req REQ-CRC-004 -// fusa:req REQ-CRC-005 -// fusa:req REQ-CRC-006 -// fusa:req REQ-CRC-007 +/// Two further TC18 §13.6 properties fall out of this function's shape +/// rather than needing code of their own: +/// +/// - **The CRC is ACF-specific** (TC18.txt line 3789: "the CRC32 is ACF +/// specific, which means it is calculated for multiple ACF types in one +/// AVTPDU for each ACF type individually"). This function takes exactly +/// one [`AcfCoverageMessage`], so an AVTPDU carrying N E-2-E-protected +/// ACF messages needs N independent calls and produces N independent +/// CRCs; there is no way to fold two ACF messages into one coverage +/// buffer. +/// - **Requests and responses use the identical scheme** (TC18.txt line +/// 3808: "The CRC calculation for request and response follows the +/// identical scheme"). Neither this function nor [`crc32_tc18`] has a +/// direction parameter; a response differs from a request only by the +/// `rsp`/`err`/`evt` bits already inside the covered `byte_message_info`. +//fusa:req REQ-CRC-004 +//fusa:req REQ-CRC-005 +//fusa:req REQ-CRC-006 +//fusa:req REQ-CRC-007 +//fusa:req REQ-CRC-017 +//fusa:req REQ-CRC-019 pub fn build_crc32_coverage_buffer( header: &HeaderVariant, acf: &AcfCoverageMessage, @@ -398,7 +444,7 @@ pub const CRC_TRAILER_LEN: usize = acf::QUADLET_LEN; /// Returns `Err(RcpError::ShortFrame)` if `frame` is shorter than /// [`acf::BYTE_MESSAGE_INFO_LEN`], and `Err(RcpError::InvalidSize)` if /// bumping `acf_msg_length` would overflow its 9-bit field width. -// fusa:req REQ-CRC-012 +//fusa:req REQ-CRC-012 pub fn finalize_crc_trailer(frame: &mut Vec, crc: u32) -> Result<(), RcpError> { if frame.len() < acf::BYTE_MESSAGE_INFO_LEN { return Err(RcpError::ShortFrame); @@ -441,7 +487,7 @@ pub fn finalize_crc_trailer(frame: &mut Vec, crc: u32) -> Result<(), RcpErro /// [`CRC_TRAILER_LEN`]), and `Err(RcpError::InvalidSize)` if the header's /// `acf_msg_length` is smaller than [`CRC_TRAILER_QUADLETS`] (i.e. does not /// actually describe a message with room for a CRC trailer at all). -// fusa:req REQ-CRC-013 +//fusa:req REQ-CRC-013 pub fn split_crc_trailer(frame: &[u8]) -> Result<(Vec, u32), RcpError> { if frame.len() < acf::BYTE_MESSAGE_INFO_LEN + CRC_TRAILER_LEN { return Err(RcpError::ShortFrame); @@ -482,7 +528,7 @@ impl CombinedFragmentPayload { /// `segments` verbatim, in the order given. An empty `segments` slice /// yields an empty combined payload; this function never panics for /// any input, including empty per-segment payloads. - // fusa:req REQ-CRC-009 + //fusa:req REQ-CRC-009 pub fn assemble(segments: &[&[u8]]) -> Self { let mut combined = Vec::new(); for segment in segments { @@ -512,7 +558,7 @@ impl CombinedFragmentPayload { /// discipline: composes [`build_crc32_coverage_buffer`] rather than /// re-deriving its buffer-assembly logic, and is not wired into /// `crc32_tc18` or a decoder/dispatch loop. -// fusa:req REQ-CRC-010 +//fusa:req REQ-CRC-010 pub fn build_crc32_coverage_buffer_for_fragment_train( header: &HeaderVariant, final_fragment: &AcfCoverageMessage, @@ -544,7 +590,7 @@ pub fn build_crc32_coverage_buffer_for_fragment_train( /// fragment carries on the wire per the "only the last fragment carries /// the CRC" rule — see [`fragment_crc_expectation`]/ /// [`check_fragment_crc_placement`] for that placement rule itself. -// fusa:req REQ-CRC-010 +//fusa:req REQ-CRC-010 pub fn crc32_tc18_for_fragment_train( header: &HeaderVariant, final_fragment: &AcfCoverageMessage, @@ -570,7 +616,7 @@ pub enum FragmentCrcExpectation { /// Derives [`FragmentCrcExpectation`] from a fragment's `ms` flag alone, /// per the "only the last fragment carries the CRC" rule. -// fusa:req REQ-CRC-008 +//fusa:req REQ-CRC-008 pub fn fragment_crc_expectation(ms: bool) -> FragmentCrcExpectation { if ms { FragmentCrcExpectation::NotExpected @@ -595,7 +641,7 @@ pub fn fragment_crc_expectation(ms: bool) -> FragmentCrcExpectation { /// rule ahead of the later "`CRC_ERROR` error path" checklist item, which /// is scoped to the wire-level error code a received `CRC_ERROR` produces, /// not to this placement rule. -// fusa:req REQ-CRC-008 +//fusa:req REQ-CRC-008 pub fn check_fragment_crc_placement(ms: bool, crc_present: bool) -> Result<(), RcpError> { match (fragment_crc_expectation(ms), crc_present) { (FragmentCrcExpectation::NotExpected, false) => Ok(()), @@ -645,14 +691,14 @@ mod tests { } #[test] - // fusa:test REQ-CRC-001 + //fusa:test REQ-CRC-001 fn crc32_tc18_empty_input_matches_reference() { assert_eq!(crc32_tc18(&[]), crc32_tc18_reference(&[])); assert_eq!(crc32_tc18(&[]), 0x0000_0000); } #[test] - // fusa:test REQ-CRC-001 + //fusa:test REQ-CRC-001 fn crc32_tc18_ascii_check_string_matches_reference() { // "123456789" is the conventional CRC-32 check corpus; the expected // constant below is this polynomial's own derived value (see this @@ -663,7 +709,7 @@ mod tests { } #[test] - // fusa:test REQ-CRC-001 + //fusa:test REQ-CRC-001 fn crc32_tc18_all_zero_boundary_matches_reference() { let data = [0u8; 16]; assert_eq!(crc32_tc18(&data), crc32_tc18_reference(&data)); @@ -671,7 +717,7 @@ mod tests { } #[test] - // fusa:test REQ-CRC-001 + //fusa:test REQ-CRC-001 fn crc32_tc18_all_0xff_boundary_matches_reference() { let data = [0xFFu8; 16]; assert_eq!(crc32_tc18(&data), crc32_tc18_reference(&data)); @@ -679,7 +725,7 @@ mod tests { } #[test] - // fusa:test REQ-CRC-001 + //fusa:test REQ-CRC-001 fn crc32_tc18_matches_reference_across_varied_inputs() { let vectors: [&[u8]; 4] = [ b"OPEN Alliance TC18", @@ -695,7 +741,7 @@ mod tests { } #[test] - // fusa:test REQ-CRC-002 + //fusa:test REQ-CRC-002 fn crc32_tc18_never_panics_across_arbitrary_lengths() { for len in [0usize, 1, 2, 3, 6, 17, 64, 257, 1000] { let data: Vec = (0..len).map(|i| (i % 256) as u8).collect(); @@ -704,7 +750,7 @@ mod tests { } #[test] - // fusa:test REQ-CRC-003 + //fusa:test REQ-CRC-003 fn crc32_tc18_different_payload_produces_different_crc() { let a = crc32_tc18(b"payload-a"); let b = crc32_tc18(b"payload-b"); @@ -712,7 +758,7 @@ mod tests { } #[test] - // fusa:test REQ-CRC-003 + //fusa:test REQ-CRC-003 fn crc32_tc18_single_bit_flip_changes_crc() { let mut data = b"integrity check data".to_vec(); let baseline = crc32_tc18(&data); @@ -747,8 +793,132 @@ mod tests { } } + // ── TC18 §13.6 Table 31 / ACF-specific / request-response symmetry ───── + + #[test] + //fusa:test REQ-CRC-014 + fn poly_constant_is_the_bit_reversal_of_table_31_polynomial() { + // TC18 §13.6 Table 31 "CRC32 Polynomial" (TC18.txt line 3792), read + // row by row: + // Polynomial 0xF4ACFB13 + // Width 32 bit + // Initial Value 0xFFFFFFFF + // Final XOR 0xFFFFFFFF + // Input reflection TRUE + // Output reflection TRUE + // + // `crc32_tc18` runs a reflected (LSB-first) shift register, which + // is only equivalent to that definition if the constant it shifts + // against is the exact bit reversal of Table 31's polynomial. + const TABLE_31_POLYNOMIAL: u32 = 0xF4AC_FB13; + const TABLE_31_INITIAL_VALUE: u32 = 0xFFFF_FFFF; + const TABLE_31_FINAL_XOR: u32 = 0xFFFF_FFFF; + const TABLE_31_WIDTH_BITS: u32 = 32; + + assert_eq!( + CRC32_TC18_POLY_REFLECTED, + TABLE_31_POLYNOMIAL.reverse_bits(), + "the reflected engine must shift against bitrev32(0xF4ACFB13)" + ); + assert_eq!(CRC32_TC18_INIT_XOROUT, TABLE_31_INITIAL_VALUE); + assert_eq!(CRC32_TC18_INIT_XOROUT, TABLE_31_FINAL_XOR); + assert_eq!(u32::BITS, TABLE_31_WIDTH_BITS); + } + + #[test] + //fusa:test REQ-CRC-017 + fn crc32_is_computed_per_acf_type_individually() { + // TC18 §13.6 (TC18.txt line 3789): "the CRC32 is ACF specific, + // which means it is calculated for multiple ACF types in one AVTPDU + // for each ACF type individually." + // + // Two ACF messages riding under one and the same AVTPDU header must + // therefore yield two independent coverage buffers and two + // independent CRCs — neither buffer may contain the other message's + // payload bytes. + let header = HeaderVariant::Ntscf(avtp::NtscfHeader { + stream_id: 0x0011_2233_4455_6677, + ..Default::default() + }); + let first = sample_abb_message(3, b"first-acf"); + let second = sample_gbb_message(5, 0x1234, b"second-acf"); + + let buf_first = build_crc32_coverage_buffer(&header, &AcfCoverageMessage::Abb(&first)) + .expect("first ACF message covers on its own"); + let buf_second = build_crc32_coverage_buffer(&header, &AcfCoverageMessage::Gbb(&second)) + .expect("second ACF message covers on its own"); + + assert!( + !buf_first.windows(10).any(|w| w == b"second-acf"), + "the first ACF type's coverage must not include the second's payload" + ); + assert!( + !buf_second.windows(9).any(|w| w == b"first-acf"), + "the second ACF type's coverage must not include the first's payload" + ); + assert_ne!( + crc32_tc18(&buf_first), + crc32_tc18(&buf_second), + "two ACF types in one AVTPDU get two distinct CRC32 values" + ); + } + + #[test] + //fusa:test REQ-CRC-019 + fn crc_scheme_is_identical_for_request_and_response() { + // TC18 §13.6 (TC18.txt line 3808): "The CRC calculation for request + // and response follows the identical scheme." + // + // TC18 §11.2.1 Table 4 gives a request rsp = 0b and §11.3 Table 15 + // gives a response rsp = 1b; that single covered header bit is the + // *only* thing that may differ between the two directions' coverage + // buffers. In particular, neither build_crc32_coverage_buffer nor + // crc32_tc18 takes a direction argument. + let header = HeaderVariant::Ntscf(avtp::NtscfHeader { + stream_id: 0x0102_0304_0506_0708, + ..Default::default() + }); + let payload = b"same-bytes-both-ways"; + + let mut request = sample_abb_message(4, payload); + request.info.rsp = false; + let mut response = request.clone(); + response.info.rsp = true; + + let buf_request = + build_crc32_coverage_buffer(&header, &AcfCoverageMessage::Abb(&request)).unwrap(); + let buf_response = + build_crc32_coverage_buffer(&header, &AcfCoverageMessage::Abb(&response)).unwrap(); + + assert_eq!( + buf_request.len(), + buf_response.len(), + "identical scheme: same coverage extent in both directions" + ); + // rsp is octet 6 bit 6 of byte_message_info, which starts 12 bytes + // into the coverage buffer (8-byte stream_id + 4-byte + // avtp_timestamp position) — so index 12 + 6 == 18. + let differing: Vec = (0..buf_request.len()) + .filter(|&i| buf_request[i] != buf_response[i]) + .collect(); + assert_eq!( + differing, + vec![18], + "only the rsp bit's own octet may differ between the two directions" + ); + assert_eq!(buf_request[18] ^ buf_response[18], 0x40, "rsp is bit 6"); + + // And with rsp forced equal, the two directions produce byte-for-byte + // identical coverage and therefore an identical CRC. + response.info.rsp = false; + let buf_response_same = + build_crc32_coverage_buffer(&header, &AcfCoverageMessage::Abb(&response)).unwrap(); + assert_eq!(buf_request, buf_response_same); + assert_eq!(crc32_tc18(&buf_request), crc32_tc18(&buf_response_same)); + } + #[test] - // fusa:test REQ-CRC-004 + //fusa:test REQ-CRC-004 fn coverage_buffer_leads_with_stream_id_bytes() { let header = HeaderVariant::Ntscf(avtp::NtscfHeader { stream_id: 0x0102_0304_0506_0708, @@ -760,7 +930,7 @@ mod tests { } #[test] - // fusa:test REQ-CRC-005 + //fusa:test REQ-CRC-005 fn coverage_buffer_zeroes_avtp_timestamp_under_ntscf() { let header = HeaderVariant::Ntscf(avtp::NtscfHeader { stream_id: 0xAABB_CCDD_EEFF_0011, @@ -775,7 +945,7 @@ mod tests { } #[test] - // fusa:test REQ-CRC-005 + //fusa:test REQ-CRC-005 fn coverage_buffer_uses_real_avtp_timestamp_under_tscf() { let header = HeaderVariant::Tscf(avtp::TscfHeader { stream_id: 0x1, @@ -788,7 +958,7 @@ mod tests { } #[test] - // fusa:test REQ-CRC-007 + //fusa:test REQ-CRC-007 fn coverage_buffer_abb_header_has_no_message_timestamp_region() { let header = HeaderVariant::Ntscf(avtp::NtscfHeader::default()); let payload = b"abb-payload"; @@ -805,7 +975,7 @@ mod tests { } #[test] - // fusa:test REQ-CRC-007 + //fusa:test REQ-CRC-007 fn coverage_buffer_gbb_header_carries_message_timestamp() { let header = HeaderVariant::Ntscf(avtp::NtscfHeader::default()); let payload = b"gbb-payload"; @@ -832,7 +1002,7 @@ mod tests { } #[test] - // fusa:test REQ-CRC-006 + //fusa:test REQ-CRC-006 fn coverage_buffer_preadjusts_acf_msg_length_by_one_quadlet() { let header = HeaderVariant::Ntscf(avtp::NtscfHeader::default()); let acf_msg_length = 0x0100u16; @@ -854,7 +1024,7 @@ mod tests { } #[test] - // fusa:test REQ-CRC-006 + //fusa:test REQ-CRC-006 fn coverage_buffer_rejects_length_that_overflows_9_bits_after_preadjustment() { let header = HeaderVariant::Ntscf(avtp::NtscfHeader::default()); // Max legal 9-bit acf_msg_length; +1 pushes it past the field @@ -868,7 +1038,7 @@ mod tests { } #[test] - // fusa:test REQ-CRC-004 + //fusa:test REQ-CRC-004 fn coverage_buffer_feeds_crc32_tc18_without_panicking() { let header = HeaderVariant::Tscf(avtp::TscfHeader { stream_id: 0x0203_0405_0607_0809, @@ -882,7 +1052,7 @@ mod tests { } #[test] - // fusa:test REQ-CRC-004 + //fusa:test REQ-CRC-004 fn coverage_buffer_changes_when_stream_id_differs() { let payload = b"same-payload"; let acf = AcfCoverageMessage::Abb(&sample_abb_message(0, payload)); @@ -909,8 +1079,8 @@ mod tests { // order and so never caught that bug. #[test] - // fusa:test REQ-CRC-012 - // fusa:test REQ-CRC-013 + //fusa:test REQ-CRC-012 + //fusa:test REQ-CRC-013 fn finalize_crc_trailer_matches_figure_19_worked_example() { // Figure 19: ACF_ABB, 8-byte header + 6 real payload bytes + 2 pad // bytes + 4-byte CRC32 trailer = 20 bytes total = 5 quadlets, wire @@ -1004,8 +1174,8 @@ mod tests { } #[test] - // fusa:test REQ-CRC-012 - // fusa:test REQ-CRC-013 + //fusa:test REQ-CRC-012 + //fusa:test REQ-CRC-013 fn finalize_crc_trailer_matches_figure_20_worked_example() { // Figure 20: ACF_GBB, 8-byte header + 8-byte timestamp + 7 real // payload bytes + 1 pad byte + 4-byte CRC32 trailer = 28 bytes @@ -1104,7 +1274,7 @@ mod tests { } #[test] - // fusa:test REQ-CRC-012 + //fusa:test REQ-CRC-012 fn finalize_crc_trailer_never_places_pad_after_crc() { // Regression guard for the padding-order bug this module's own // "CRC trailer wire placement" doc section describes: naively @@ -1196,7 +1366,7 @@ mod tests { // ── Fragmentation interaction ──────────────────────────────────────────── #[test] - // fusa:test REQ-CRC-009 + //fusa:test REQ-CRC-009 fn combined_fragment_payload_concatenates_in_given_order() { let segments: [&[u8]; 3] = [b"ab", b"cd", b"ef"]; let combined = CombinedFragmentPayload::assemble(&segments); @@ -1204,7 +1374,7 @@ mod tests { } #[test] - // fusa:test REQ-CRC-009 + //fusa:test REQ-CRC-009 fn combined_fragment_payload_empty_segments_yields_empty() { let segments: [&[u8]; 0] = []; let combined = CombinedFragmentPayload::assemble(&segments); @@ -1212,7 +1382,7 @@ mod tests { } #[test] - // fusa:test REQ-CRC-009 + //fusa:test REQ-CRC-009 fn combined_fragment_payload_single_segment_matches_it_verbatim() { let segments: [&[u8]; 1] = [b"solo"]; let combined = CombinedFragmentPayload::assemble(&segments); @@ -1220,7 +1390,7 @@ mod tests { } #[test] - // fusa:test REQ-CRC-008 + //fusa:test REQ-CRC-008 fn fragment_crc_expectation_not_expected_when_more_segments_follow() { assert_eq!( fragment_crc_expectation(true), @@ -1229,7 +1399,7 @@ mod tests { } #[test] - // fusa:test REQ-CRC-008 + //fusa:test REQ-CRC-008 fn fragment_crc_expectation_expected_on_final_fragment() { assert_eq!( fragment_crc_expectation(false), @@ -1238,19 +1408,19 @@ mod tests { } #[test] - // fusa:test REQ-CRC-008 + //fusa:test REQ-CRC-008 fn check_fragment_crc_placement_accepts_no_crc_on_intermediate_fragment() { assert_eq!(check_fragment_crc_placement(true, false), Ok(())); } #[test] - // fusa:test REQ-CRC-008 + //fusa:test REQ-CRC-008 fn check_fragment_crc_placement_accepts_crc_on_final_fragment() { assert_eq!(check_fragment_crc_placement(false, true), Ok(())); } #[test] - // fusa:test REQ-CRC-008 + //fusa:test REQ-CRC-008 fn check_fragment_crc_placement_rejects_crc_on_intermediate_fragment() { assert_eq!( check_fragment_crc_placement(true, true), @@ -1259,7 +1429,7 @@ mod tests { } #[test] - // fusa:test REQ-CRC-008 + //fusa:test REQ-CRC-008 fn check_fragment_crc_placement_rejects_missing_crc_on_final_fragment() { assert_eq!( check_fragment_crc_placement(false, false), @@ -1268,7 +1438,7 @@ mod tests { } #[test] - // fusa:test REQ-CRC-010 + //fusa:test REQ-CRC-010 fn coverage_buffer_for_fragment_train_matches_manual_concatenation() { let header = HeaderVariant::Ntscf(avtp::NtscfHeader { stream_id: 0x0102_0304_0506_0708, @@ -1305,7 +1475,7 @@ mod tests { } #[test] - // fusa:test REQ-CRC-010 + //fusa:test REQ-CRC-010 fn coverage_buffer_for_fragment_train_changes_when_any_segment_differs() { let header = HeaderVariant::Ntscf(avtp::NtscfHeader::default()); let final_fragment_msg = sample_abb_message(0, b"unused"); @@ -1324,7 +1494,7 @@ mod tests { } #[test] - // fusa:test REQ-CRC-010 + //fusa:test REQ-CRC-010 fn coverage_buffer_for_fragment_train_gbb_carries_final_fragment_timestamp() { let header = HeaderVariant::Ntscf(avtp::NtscfHeader::default()); let final_fragment_msg = sample_gbb_message(0, 0x1122_3344_5566_7788, b"unused"); @@ -1349,7 +1519,7 @@ mod tests { } #[test] - // fusa:test REQ-CRC-010 + //fusa:test REQ-CRC-010 fn coverage_buffer_for_fragment_train_propagates_length_overflow_error() { let header = HeaderVariant::Ntscf(avtp::NtscfHeader::default()); let final_fragment_msg = sample_abb_message(acf::ACF_MSG_LENGTH_9BIT_MAX, b"unused"); @@ -1362,7 +1532,7 @@ mod tests { } #[test] - // fusa:test REQ-CRC-010 + //fusa:test REQ-CRC-010 fn crc32_tc18_for_fragment_train_matches_manual_computation() { let header = HeaderVariant::Tscf(avtp::TscfHeader { stream_id: 0x0203_0405_0607_0809, diff --git a/src/ep0.rs b/src/ep0.rs index 06bba25..2b11545 100644 --- a/src/ep0.rs +++ b/src/ep0.rs @@ -1,14 +1,14 @@ -// fusa:req REQ-EP0-001 -// fusa:req REQ-EP0-002 -// fusa:req REQ-EP0-003 -// fusa:req REQ-EP0-004 -// fusa:req REQ-EP0-005 -// fusa:req REQ-EP0-006 -// fusa:req REQ-EP0-007 -// fusa:req REQ-EP0-008 -// fusa:req REQ-EP0-009 -// fusa:req REQ-EP0-010 -// fusa:req REQ-EP0-011 +//fusa:req REQ-EP0-001 +//fusa:req REQ-EP0-002 +//fusa:req REQ-EP0-003 +//fusa:req REQ-EP0-004 +//fusa:req REQ-EP0-005 +//fusa:req REQ-EP0-006 +//fusa:req REQ-EP0-007 +//fusa:req REQ-EP0-008 +//fusa:req REQ-EP0-009 +//fusa:req REQ-EP0-010 +//fusa:req REQ-EP0-011 //! EP0 (RC-Server-as-endpoint) whole-register-map read/write addressing, //! plus the root-client access-control axis layered on top of it — @@ -237,7 +237,7 @@ pub const EP0_BYTE_BUS_ID: u16 = 0; /// Is `byte_bus_id` the reserved EP0 address? /// /// Never panics for any input. -// fusa:req REQ-EP0-001 +//fusa:req REQ-EP0-001 pub fn is_ep0_address(byte_bus_id: u16) -> bool { byte_bus_id == EP0_BYTE_BUS_ID } @@ -250,7 +250,7 @@ pub fn is_ep0_address(byte_bus_id: u16) -> bool { /// module's doc comment for why it must be made *before* /// [`crate::addressing::EndpointTable`] is ever consulted. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// fusa:req REQ-EP0-002 +//fusa:req REQ-EP0-002 pub enum RequestRoute { /// `byte_bus_id` is the reserved EP0 address. Route to this module's /// whole-register-map read/write path ([`check_ep0_access`]), not to @@ -270,7 +270,7 @@ pub enum RequestRoute { /// which is itself the point: the EP0/device-endpoint routing decision must /// not depend on, or be overridable by, whatever a table happens to have /// registered. Never panics for any input. -// fusa:req REQ-EP0-002 +//fusa:req REQ-EP0-002 pub fn route_byte_bus_id(byte_bus_id: u16) -> RequestRoute { if is_ep0_address(byte_bus_id) { RequestRoute::Ep0 @@ -287,7 +287,7 @@ pub fn route_byte_bus_id(byte_bus_id: u16) -> RequestRoute { /// See this module's provenance note for why `op`'s direction convention is /// this crate's own working interpretation. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// fusa:req REQ-EP0-003 +//fusa:req REQ-EP0-003 pub enum Ep0AccessKind { /// `op = false`. Read, @@ -299,7 +299,7 @@ pub enum Ep0AccessKind { /// /// Never panics: `info` is an already-decoded value, not raw bytes, so /// there is no truncated-input shape to reject. -// fusa:req REQ-EP0-003 +//fusa:req REQ-EP0-003 pub fn access_kind(info: &ByteMessageInfo) -> Ep0AccessKind { if info.op { Ep0AccessKind::Write @@ -328,9 +328,9 @@ pub fn access_kind(info: &ByteMessageInfo) -> Ep0AccessKind { /// [`EP0_BYTE_BUS_ID`] (that is [`route_byte_bus_id`]'s job, expected to /// have already run before this function is called). Never panics for any /// input. -// fusa:req REQ-EP0-004 -// fusa:req REQ-EP0-005 -// fusa:req REQ-EP0-006 +//fusa:req REQ-EP0-004 +//fusa:req REQ-EP0-005 +//fusa:req REQ-EP0-006 pub fn check_ep0_access( state: RcServerState, category: RegisterCategory, @@ -351,7 +351,7 @@ pub fn check_ep0_access( /// module's Provenance note. `None` means no stream currently holds /// root-client status, in which case this always answers `false`. Never /// panics for any input. -// fusa:req REQ-EP0-007 +//fusa:req REQ-EP0-007 pub fn is_root_client(root_client: Option, stream: StreamId) -> bool { root_client == Some(stream) } @@ -376,10 +376,10 @@ pub fn is_root_client(root_client: Option, stream: StreamId) -> bool { /// Like [`check_ep0_access`], this function performs no register I/O and /// does not itself verify that `info`'s `byte_bus_id` is actually /// [`EP0_BYTE_BUS_ID`]. Never panics for any input. -// fusa:req REQ-EP0-008 -// fusa:req REQ-EP0-009 -// fusa:req REQ-EP0-010 -// fusa:req REQ-EP0-011 +//fusa:req REQ-EP0-008 +//fusa:req REQ-EP0-009 +//fusa:req REQ-EP0-010 +//fusa:req REQ-EP0-011 pub fn check_ep0_access_for_stream( state: RcServerState, category: RegisterCategory, @@ -430,7 +430,7 @@ mod tests { // ── Reserved address ────────────────────────────────────────────────── #[test] - // fusa:test REQ-EP0-001 + //fusa:test REQ-EP0-001 fn is_ep0_address_true_only_for_zero() { assert!(is_ep0_address(0)); for byte_bus_id in [1u16, 2, 7, 0x0123, crate::acf::BYTE_MESSAGE_INFO_11BIT_MAX] { @@ -439,7 +439,7 @@ mod tests { } #[test] - // fusa:test REQ-EP0-001 + //fusa:test REQ-EP0-001 fn is_ep0_address_never_panics_across_the_full_range() { for byte_bus_id in 0u16..=crate::acf::BYTE_MESSAGE_INFO_11BIT_MAX { let _ = is_ep0_address(byte_bus_id); @@ -449,7 +449,7 @@ mod tests { // ── Routing ─────────────────────────────────────────────────────────── #[test] - // fusa:test REQ-EP0-002 + //fusa:test REQ-EP0-002 fn route_byte_bus_id_is_ep0_only_for_zero() { assert_eq!(route_byte_bus_id(0), RequestRoute::Ep0); for byte_bus_id in [1u16, 2, 7, 0x0123, crate::acf::BYTE_MESSAGE_INFO_11BIT_MAX] { @@ -458,7 +458,7 @@ mod tests { } #[test] - // fusa:test REQ-EP0-002 + //fusa:test REQ-EP0-002 fn ep0_route_is_decided_before_and_independent_of_endpoint_table_contents() { use crate::addressing::{EndpointId, EndpointTable}; use crate::avtp::StreamId; @@ -484,7 +484,7 @@ mod tests { } #[test] - // fusa:test REQ-EP0-002 + //fusa:test REQ-EP0-002 fn route_byte_bus_id_never_panics_across_the_full_range() { for byte_bus_id in 0u16..=crate::acf::BYTE_MESSAGE_INFO_11BIT_MAX { let _ = route_byte_bus_id(byte_bus_id); @@ -494,7 +494,7 @@ mod tests { // ── Access direction ───────────────────────────────────────────────── #[test] - // fusa:test REQ-EP0-003 + //fusa:test REQ-EP0-003 fn access_kind_matches_op_flag() { assert_eq!(access_kind(&info_with_op(false)), Ep0AccessKind::Read); assert_eq!(access_kind(&info_with_op(true)), Ep0AccessKind::Write); @@ -503,7 +503,7 @@ mod tests { // ── Whole-register-map access check ───────────────────────────────── #[test] - // fusa:test REQ-EP0-004 + //fusa:test REQ-EP0-004 fn ep0_read_agrees_with_check_register_reachable() { for state in ALL_STATES { for category in ALL_CATEGORIES { @@ -522,7 +522,7 @@ mod tests { } #[test] - // fusa:test REQ-EP0-005 + //fusa:test REQ-EP0-005 fn ep0_write_agrees_with_check_register_writable() { for state in ALL_STATES { for category in ALL_CATEGORIES { @@ -541,7 +541,7 @@ mod tests { } #[test] - // fusa:test REQ-EP0-005 + //fusa:test REQ-EP0-005 fn ep0_write_to_unreachable_category_reports_unreachable_not_locked() { // RcpConfig is unreachable (not merely locked) while // HwUnconfigured -- check_ep0_access must surface that specific @@ -559,7 +559,7 @@ mod tests { } #[test] - // fusa:test REQ-EP0-005 + //fusa:test REQ-EP0-005 fn ep0_write_to_permanently_locked_category_reports_locked() { // HwConfig is reachable but W*-locked once RcpConfigured. let info = info_with_op(true); @@ -574,7 +574,7 @@ mod tests { } #[test] - // fusa:test REQ-EP0-006 + //fusa:test REQ-EP0-006 fn check_ep0_access_never_panics_for_any_state_category_op_combination() { for state in ALL_STATES { for category in ALL_CATEGORIES { @@ -589,7 +589,7 @@ mod tests { // ── Interop with the existing echo-back rule ───────────────────────── #[test] - // fusa:test REQ-EP0-002 + //fusa:test REQ-EP0-002 fn ep0_addressed_request_round_trips_through_the_existing_echo_back_rule() { // No EP0-specific echo-back logic is needed: build_response_info/ // verify_echo_back already operate purely on byte_bus_id, and 0 @@ -611,7 +611,7 @@ mod tests { } #[test] - // fusa:test REQ-EP0-007 + //fusa:test REQ-EP0-007 fn is_root_client_true_only_for_the_designated_stream() { let root = stream(1); let other = stream(2); @@ -622,7 +622,7 @@ mod tests { } #[test] - // fusa:test REQ-EP0-008 + //fusa:test REQ-EP0-008 fn root_client_read_agrees_with_check_ep0_access_regardless_of_root_status() { let root = stream(1); let non_root = stream(2); @@ -650,7 +650,7 @@ mod tests { } #[test] - // fusa:test REQ-EP0-009 + //fusa:test REQ-EP0-009 fn root_client_write_agrees_with_check_ep0_access_for_the_root_stream() { let root = stream(1); for state in ALL_STATES { @@ -666,7 +666,7 @@ mod tests { } #[test] - // fusa:test REQ-EP0-010 + //fusa:test REQ-EP0-010 fn non_root_write_is_always_rejected_with_root_client_required() { let root = stream(1); let non_root = stream(2); @@ -688,7 +688,7 @@ mod tests { } #[test] - // fusa:test REQ-EP0-010 + //fusa:test REQ-EP0-010 fn root_client_write_can_still_be_rejected_by_lifecycle_state() { // Root-client status only clears the root-client gate -- it does // not bypass the lifecycle-state gating check_ep0_access already @@ -709,7 +709,7 @@ mod tests { } #[test] - // fusa:test REQ-EP0-011 + //fusa:test REQ-EP0-011 fn check_ep0_access_for_stream_never_panics_for_any_combination() { let root = stream(1); let non_root = stream(2); diff --git a/src/evtgroup.rs b/src/evtgroup.rs index b52778d..9047fe2 100644 --- a/src/evtgroup.rs +++ b/src/evtgroup.rs @@ -1,7 +1,7 @@ -// fusa:req REQ-EVTGRP-001 -// fusa:req REQ-EVTGRP-002 -// fusa:req REQ-EVTGRP-003 -// fusa:req REQ-EVTGRP-004 +//fusa:req REQ-EVTGRP-001 +//fusa:req REQ-EVTGRP-002 +//fusa:req REQ-EVTGRP-003 +//fusa:req REQ-EVTGRP-004 //! The "Groups A/B/C" `evt[2:0]` sub-opcode convention — `ROADMAP.md` //! Milestone 4 ("Basic Endpoint Types"), final checklist bullet: "Generic @@ -92,7 +92,7 @@ use crate::RcpError; /// assignment logic of its own. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] -// fusa:req REQ-EVTGRP-001 +//fusa:req REQ-EVTGRP-001 pub enum EvtGroup { /// Group A. A = 0, @@ -110,7 +110,7 @@ impl EvtGroup { /// internal identity for the label itself, not a `sub_opcode` value — /// see this module's doc comment for why no `sub_opcode`-keyed /// encoding is provided. - // fusa:req REQ-EVTGRP-001 + //fusa:req REQ-EVTGRP-001 pub fn to_u8(self) -> u8 { self as u8 } @@ -119,7 +119,7 @@ impl EvtGroup { /// /// Returns `Err(RcpError::InvalidParameter)` for any value outside /// `0..=2`. Never panics for any input. - // fusa:req REQ-EVTGRP-002 + //fusa:req REQ-EVTGRP-002 pub fn from_u8(raw: u8) -> Result { match raw { 0 => Ok(Self::A), @@ -142,8 +142,8 @@ impl EvtGroup { /// "Provenance note: the Groups A/B/C classification" — this is not a /// bug, it is this function's honest total answer given the ambiguity. /// Never panics for any input. -// fusa:req REQ-EVTGRP-003 -// fusa:req REQ-EVTGRP-004 +//fusa:req REQ-EVTGRP-003 +//fusa:req REQ-EVTGRP-004 pub fn classify_evt_sub_opcode(sub_opcode: u8) -> Result, RcpError> { if sub_opcode > EVT_SUB_OPCODE_MAX { return Err(RcpError::InvalidParameter); @@ -158,7 +158,7 @@ mod tests { // ── EvtGroup: round-trip / never-panic ────────────────────────────────── #[test] - // fusa:test REQ-EVTGRP-001 + //fusa:test REQ-EVTGRP-001 fn evt_group_to_u8_round_trips_through_from_u8() { for group in EvtGroup::ALL { assert_eq!(EvtGroup::from_u8(group.to_u8()).unwrap(), group); @@ -166,7 +166,7 @@ mod tests { } #[test] - // fusa:test REQ-EVTGRP-001 + //fusa:test REQ-EVTGRP-001 fn evt_group_ordinal_values_match_roadmap_listed_order() { assert_eq!(EvtGroup::A.to_u8(), 0); assert_eq!(EvtGroup::B.to_u8(), 1); @@ -174,7 +174,7 @@ mod tests { } #[test] - // fusa:test REQ-EVTGRP-002 + //fusa:test REQ-EVTGRP-002 fn evt_group_from_u8_rejects_out_of_range_values() { for raw in 3..=u8::MAX { assert_eq!(EvtGroup::from_u8(raw), Err(RcpError::InvalidParameter)); @@ -184,7 +184,7 @@ mod tests { // ── classify_evt_sub_opcode ────────────────────────────────────────────── #[test] - // fusa:test REQ-EVTGRP-003 + //fusa:test REQ-EVTGRP-003 fn classify_evt_sub_opcode_returns_none_for_every_in_range_value() { for sub_opcode in 0..=EVT_SUB_OPCODE_MAX { assert_eq!(classify_evt_sub_opcode(sub_opcode), Ok(None)); @@ -192,7 +192,7 @@ mod tests { } #[test] - // fusa:test REQ-EVTGRP-004 + //fusa:test REQ-EVTGRP-004 fn classify_evt_sub_opcode_rejects_out_of_range_values() { for sub_opcode in (EVT_SUB_OPCODE_MAX + 1)..=u8::MAX { assert_eq!( @@ -203,7 +203,7 @@ mod tests { } #[test] - // fusa:test REQ-EVTGRP-004 + //fusa:test REQ-EVTGRP-004 fn classify_evt_sub_opcode_never_panics_across_full_u8_range() { for sub_opcode in 0..=u8::MAX { let _ = classify_evt_sub_opcode(sub_opcode); diff --git a/src/faultinject.rs b/src/faultinject.rs index 468328a..50e349c 100644 --- a/src/faultinject.rs +++ b/src/faultinject.rs @@ -1,10 +1,10 @@ -// fusa:req REQ-FI-001 -// fusa:req REQ-FI-002 -// fusa:req REQ-FI-003 -// fusa:req REQ-FI-004 -// fusa:req REQ-FI-005 -// fusa:req REQ-FI-006 -// fusa:req REQ-FI-007 +//fusa:req REQ-FI-001 +//fusa:req REQ-FI-002 +//fusa:req REQ-FI-003 +//fusa:req REQ-FI-004 +//fusa:req REQ-FI-005 +//fusa:req REQ-FI-006 +//fusa:req REQ-FI-007 //! Fault injection — deterministic error injection for safety test campaigns. //! @@ -31,7 +31,7 @@ use crate::RcpError; // ── FaultRule ───────────────────────────────────────────────────────────────── /// Rule controlling when a fault is injected. -// fusa:req REQ-FI-002 +//fusa:req REQ-FI-002 #[derive(Clone, Debug)] pub enum FaultRule { /// Inject on every call. @@ -43,7 +43,7 @@ pub enum FaultRule { } /// A configured fault to inject. -// fusa:req REQ-FI-001 +//fusa:req REQ-FI-001 #[derive(Clone, Debug)] pub struct FaultSpec { pub rule: FaultRule, @@ -58,7 +58,7 @@ struct Inner { } /// Fault-injecting endpoint wrapper. -// fusa:req REQ-FI-003 +//fusa:req REQ-FI-003 pub struct FaultInjectEndpoint { inner: Arc, state: Mutex, @@ -78,13 +78,13 @@ impl FaultInjectEndpoint { } /// Install a fault rule. - // fusa:req REQ-FI-004 + //fusa:req REQ-FI-004 pub fn inject(&self, spec: FaultSpec) { self.state.lock().unwrap().faults.push(spec); } /// Remove all fault rules. - // fusa:req REQ-FI-005 + //fusa:req REQ-FI-005 pub fn clear(&self) { self.state.lock().unwrap().faults.clear(); } @@ -125,7 +125,7 @@ impl Endpoint for FaultInjectEndpoint { self.inner.ep_type() } - // fusa:req REQ-FI-006 + //fusa:req REQ-FI-006 fn read(&self, read_size: u16) -> Result, RcpError> { if let Some(err) = self.next_fault() { return Err(err); @@ -133,7 +133,7 @@ impl Endpoint for FaultInjectEndpoint { self.inner.read(read_size) } - // fusa:req REQ-FI-006 + //fusa:req REQ-FI-006 fn write(&self, payload: &[u8]) -> Result<(), RcpError> { if let Some(err) = self.next_fault() { return Err(err); @@ -156,16 +156,16 @@ mod tests { } #[test] - // fusa:test REQ-FI-001 - // fusa:test REQ-FI-003 + //fusa:test REQ-FI-001 + //fusa:test REQ-FI-003 fn no_fault_passes_through() { let fi = fi(); fi.write(b"x").unwrap(); } #[test] - // fusa:test REQ-FI-002 - // fusa:test REQ-FI-006 + //fusa:test REQ-FI-002 + //fusa:test REQ-FI-006 fn always_fault_injects_every_call() { let fi = fi(); fi.inject(FaultSpec { @@ -179,8 +179,8 @@ mod tests { } #[test] - // fusa:test REQ-FI-002 - // fusa:test REQ-FI-006 + //fusa:test REQ-FI-002 + //fusa:test REQ-FI-006 fn nth_call_fault_triggers_only_on_n() { let fi = fi(); fi.inject(FaultSpec { @@ -194,8 +194,8 @@ mod tests { } #[test] - // fusa:test REQ-FI-002 - // fusa:test REQ-FI-006 + //fusa:test REQ-FI-002 + //fusa:test REQ-FI-006 fn after_nth_call_triggers_from_n_onwards() { let fi = fi(); fi.inject(FaultSpec { @@ -211,7 +211,7 @@ mod tests { } #[test] - // fusa:test REQ-FI-004 + //fusa:test REQ-FI-004 fn inject_multiple_rules_first_match_wins() { let fi = fi(); fi.inject(FaultSpec { @@ -228,7 +228,7 @@ mod tests { } #[test] - // fusa:test REQ-FI-005 + //fusa:test REQ-FI-005 fn clear_removes_all_faults() { let fi = fi(); fi.inject(FaultSpec { @@ -240,7 +240,7 @@ mod tests { } #[test] - // fusa:test REQ-FI-007 + //fusa:test REQ-FI-007 fn call_count_tracks_both_ops() { let fi = fi(); fi.write(b"x").unwrap(); diff --git a/src/federation.rs b/src/federation.rs index 2440fe6..6a9c6eb 100644 --- a/src/federation.rs +++ b/src/federation.rs @@ -1,8 +1,8 @@ -// fusa:req REQ-FED-001 -// fusa:req REQ-FED-002 -// fusa:req REQ-FED-003 -// fusa:req REQ-FED-004 -// fusa:req REQ-FED-005 +//fusa:req REQ-FED-001 +//fusa:req REQ-FED-002 +//fusa:req REQ-FED-003 +//fusa:req REQ-FED-004 +//fusa:req REQ-FED-005 //! Multi-vehicle federation — routes lookups to remote vehicles' own //! discovery caches. @@ -38,7 +38,7 @@ use crate::RcpError; // ── FederationRouter ────────────────────────────────────────────────────────── /// Routes lookups to one of several remote vehicles' own discovery caches. -// fusa:req REQ-FED-001 +//fusa:req REQ-FED-001 pub struct FederationRouter { peers: RwLock>>>, } @@ -51,19 +51,19 @@ impl FederationRouter { } /// Register a remote vehicle's discovery cache under `vehicle_id`. - // fusa:req REQ-FED-002 + //fusa:req REQ-FED-002 pub fn add_peer(&self, vehicle_id: impl Into, cache: Arc>) { self.peers.write().unwrap().insert(vehicle_id.into(), cache); } /// Remove a peer. - // fusa:req REQ-FED-003 + //fusa:req REQ-FED-003 pub fn remove_peer(&self, vehicle_id: &str) -> Option>> { self.peers.write().unwrap().remove(vehicle_id) } /// List all registered vehicle IDs. - // fusa:req REQ-FED-004 + //fusa:req REQ-FED-004 pub fn peer_ids(&self) -> Vec { self.peers.read().unwrap().keys().cloned().collect() } @@ -74,7 +74,7 @@ impl FederationRouter { /// Returns `Err(RcpError::NotFound)` if `vehicle_id` names no /// registered peer, or if `stream_id` has no cached entry in that /// peer's cache. - // fusa:req REQ-FED-005 + //fusa:req REQ-FED-005 pub fn lookup_peer( &self, vehicle_id: &str, @@ -118,8 +118,8 @@ mod tests { } #[test] - // fusa:test REQ-FED-001 - // fusa:test REQ-FED-002 + //fusa:test REQ-FED-001 + //fusa:test REQ-FED-002 fn add_and_list_peers() { let r = FederationRouter::new(); r.add_peer("VIN-001", Arc::new(Mutex::new(DiscoveryCache::new()))); @@ -130,7 +130,7 @@ mod tests { } #[test] - // fusa:test REQ-FED-003 + //fusa:test REQ-FED-003 fn remove_peer() { let r = FederationRouter::new(); r.add_peer("VIN-001", Arc::new(Mutex::new(DiscoveryCache::new()))); @@ -139,7 +139,7 @@ mod tests { } #[test] - // fusa:test REQ-FED-005 + //fusa:test REQ-FED-005 fn lookup_unknown_peer_returns_not_found() { let r = FederationRouter::new(); let err = r.lookup_peer("VIN-999", stream(1)).err().unwrap(); @@ -147,7 +147,7 @@ mod tests { } #[test] - // fusa:test REQ-FED-005 + //fusa:test REQ-FED-005 fn lookup_unknown_stream_in_known_peer_returns_not_found() { let r = FederationRouter::new(); r.add_peer("VIN-001", Arc::new(Mutex::new(DiscoveryCache::new()))); @@ -156,7 +156,7 @@ mod tests { } #[test] - // fusa:test REQ-FED-005 + //fusa:test REQ-FED-005 fn lookup_peer_stream_returns_cached_entry() { let r = FederationRouter::new(); let sid = stream(7); @@ -169,7 +169,7 @@ mod tests { } #[test] - // fusa:test REQ-FED-004 + //fusa:test REQ-FED-004 fn peer_ids_empty_initially() { let r = FederationRouter::new(); assert!(r.peer_ids().is_empty()); diff --git a/src/formal.rs b/src/formal.rs index 172ee13..dfc43b6 100644 --- a/src/formal.rs +++ b/src/formal.rs @@ -1,7 +1,7 @@ -// fusa:req REQ-FORMAL-001 -// fusa:req REQ-FORMAL-002 -// fusa:req REQ-FORMAL-003 -// fusa:req REQ-FORMAL-004 +//fusa:req REQ-FORMAL-001 +//fusa:req REQ-FORMAL-002 +//fusa:req REQ-FORMAL-003 +//fusa:req REQ-FORMAL-004 //! Formal model helpers — state-machine invariant checking and property witnesses. //! @@ -12,7 +12,7 @@ // ── Invariant types ─────────────────────────────────────────────────────────── /// A named invariant with a checkable predicate. -// fusa:req REQ-FORMAL-001 +//fusa:req REQ-FORMAL-001 pub struct Invariant { pub name: &'static str, pub predicate: Box bool + Send + Sync>, @@ -32,7 +32,7 @@ impl Invariant { } /// Result of checking a set of invariants against a state. -// fusa:req REQ-FORMAL-002 +//fusa:req REQ-FORMAL-002 #[derive(Debug, Default)] pub struct CheckResult { pub passed: Vec<&'static str>, @@ -46,7 +46,7 @@ impl CheckResult { } /// Check all invariants against `state`. -// fusa:req REQ-FORMAL-003 +//fusa:req REQ-FORMAL-003 pub fn check_all(state: &S, invs: &[Invariant]) -> CheckResult { let mut result = CheckResult::default(); for inv in invs { @@ -60,7 +60,7 @@ pub fn check_all(state: &S, invs: &[Invariant]) -> CheckResult { } /// Witness: records the first state that violates an invariant. -// fusa:req REQ-FORMAL-004 +//fusa:req REQ-FORMAL-004 pub fn witness(states: &[S], inv: &Invariant) -> Option { states.iter().find(|s| !inv.check(s)).cloned() } @@ -73,8 +73,8 @@ mod tests { use super::*; #[test] - // fusa:test REQ-FORMAL-001 - // fusa:test REQ-FORMAL-003 + //fusa:test REQ-FORMAL-001 + //fusa:test REQ-FORMAL-003 fn invariant_passes_when_predicate_true() { let inv = Invariant::new("non-negative", |x: &i32| *x >= 0); let result = check_all(&42i32, std::slice::from_ref(&inv)); @@ -82,7 +82,7 @@ mod tests { } #[test] - // fusa:test REQ-FORMAL-003 + //fusa:test REQ-FORMAL-003 fn invariant_fails_when_predicate_false() { let inv = Invariant::new("always-false", |_: &i32| false); let result = check_all(&0i32, std::slice::from_ref(&inv)); @@ -91,7 +91,7 @@ mod tests { } #[test] - // fusa:test REQ-FORMAL-002 + //fusa:test REQ-FORMAL-002 fn check_result_tracks_pass_fail() { let invs = vec![ Invariant::new("pass", |x: &i32| *x >= 0), @@ -103,7 +103,7 @@ mod tests { } #[test] - // fusa:test REQ-FORMAL-004 + //fusa:test REQ-FORMAL-004 fn witness_finds_first_violation() { let inv = Invariant::new("positive", |x: &i32| *x > 0); let states = vec![1, 2, -1, 3, -2]; @@ -112,7 +112,7 @@ mod tests { } #[test] - // fusa:test REQ-FORMAL-004 + //fusa:test REQ-FORMAL-004 fn witness_returns_none_when_no_violation() { let inv = Invariant::new("positive", |x: &i32| *x > 0); let states = vec![1, 2, 3]; diff --git a/src/fragment.rs b/src/fragment.rs index bd555a5..146a53b 100644 --- a/src/fragment.rs +++ b/src/fragment.rs @@ -1,11 +1,11 @@ -// fusa:req REQ-FRAG-001 -// fusa:req REQ-FRAG-002 -// fusa:req REQ-FRAG-003 -// fusa:req REQ-FRAG-004 -// fusa:req REQ-FRAG-005 -// fusa:req REQ-FRAG-006 -// fusa:req REQ-FRAG-007 -// fusa:req REQ-FRAG-008 +//fusa:req REQ-FRAG-001 +//fusa:req REQ-FRAG-002 +//fusa:req REQ-FRAG-003 +//fusa:req REQ-FRAG-004 +//fusa:req REQ-FRAG-005 +//fusa:req REQ-FRAG-006 +//fusa:req REQ-FRAG-007 +//fusa:req REQ-FRAG-008 //! Multi-AVTPDU fragmentation reassembly (`ROADMAP.md` Milestone 8, //! "Fragmentation Go/No-Go"). @@ -134,7 +134,7 @@ pub enum FragmentAcceptOutcome { /// see this module's doc comment for the full provenance notes behind this /// type's ordering and bounding rules. #[derive(Debug, Clone, PartialEq, Eq)] -// fusa:req REQ-FRAG-001 +//fusa:req REQ-FRAG-001 pub struct FragmentReassemblyBuffer { max_request_size: u16, segments: Vec>, @@ -147,7 +147,7 @@ impl FragmentReassemblyBuffer { /// bytes of *combined* payload. `0` is accepted here (it is the /// stream-config field's own "fragmentation unsupported" sentinel); see /// [`Self::fragmentation_supported`] and [`Self::accept_fragment`]. - // fusa:req REQ-FRAG-001 + //fusa:req REQ-FRAG-001 pub fn new(rx_stream_max_request_size: u16) -> Self { FragmentReassemblyBuffer { max_request_size: rx_stream_max_request_size, @@ -161,7 +161,7 @@ impl FragmentReassemblyBuffer { /// `false` exactly when this buffer was constructed with /// `rx_stream_max_request_size == 0`, per that field's own documented /// sentinel meaning. - // fusa:req REQ-FRAG-001 + //fusa:req REQ-FRAG-001 pub fn fragmentation_supported(&self) -> bool { self.max_request_size != 0 } @@ -195,10 +195,10 @@ impl FragmentReassemblyBuffer { /// `info.ms` is `true`, or [`FragmentAcceptOutcome::Complete`] when it /// is `false` — the final-fragment signal this crate's Milestone 1 "ACF /// Messages" item already decodes. - // fusa:req REQ-FRAG-002 - // fusa:req REQ-FRAG-003 - // fusa:req REQ-FRAG-004 - // fusa:req REQ-FRAG-005 + //fusa:req REQ-FRAG-002 + //fusa:req REQ-FRAG-003 + //fusa:req REQ-FRAG-004 + //fusa:req REQ-FRAG-005 pub fn accept_fragment( &mut self, info: &ByteMessageInfo, @@ -241,14 +241,14 @@ impl FragmentReassemblyBuffer { /// The combined payload of every segment accepted so far, concatenated /// in arrival order by composing (not re-deriving) /// [`crate::e2e::CombinedFragmentPayload::assemble`]. - // fusa:req REQ-FRAG-005 + //fusa:req REQ-FRAG-005 pub fn combined_payload(&self) -> CombinedFragmentPayload { CombinedFragmentPayload::assemble(&self.segment_refs()) } /// Clears all accumulated state, readying this buffer for a new train. /// Does not change the configured `rx_stream_max_request_size` bound. - // fusa:req REQ-FRAG-006 + //fusa:req REQ-FRAG-006 pub fn reset(&mut self) { self.segments.clear(); self.total_len = 0; @@ -272,7 +272,7 @@ impl FragmentReassemblyBuffer { /// with the "final fragment carries a CRC" rule, or /// `Err(RcpError::InvalidSize)` if `final_fragment`'s own header fields /// fail [`crate::acf::encode_byte_message_info`]'s field-width validation. -// fusa:req REQ-FRAG-007 +//fusa:req REQ-FRAG-007 pub fn verify_reassembled_train_crc( buffer: &FragmentReassemblyBuffer, header: &HeaderVariant, @@ -303,21 +303,21 @@ mod tests { // ── fragmentation_supported / new ──────────────────────────────────────── #[test] - // fusa:test REQ-FRAG-001 + //fusa:test REQ-FRAG-001 fn new_with_zero_bound_reports_fragmentation_unsupported() { let buf = FragmentReassemblyBuffer::new(0); assert!(!buf.fragmentation_supported()); } #[test] - // fusa:test REQ-FRAG-001 + //fusa:test REQ-FRAG-001 fn new_with_nonzero_bound_reports_fragmentation_supported() { let buf = FragmentReassemblyBuffer::new(128); assert!(buf.fragmentation_supported()); } #[test] - // fusa:test REQ-FRAG-001 + //fusa:test REQ-FRAG-001 fn new_buffer_is_not_in_progress() { let buf = FragmentReassemblyBuffer::new(128); assert!(!buf.is_in_progress()); @@ -327,7 +327,7 @@ mod tests { // ── unsupported-stream rejection ───────────────────────────────────────── #[test] - // fusa:test REQ-FRAG-002 + //fusa:test REQ-FRAG-002 fn accept_fragment_rejects_on_zero_bound_stream() { let mut buf = FragmentReassemblyBuffer::new(0); let info = fragment_info(true, 0); @@ -341,7 +341,7 @@ mod tests { // ── segment_num ordering ───────────────────────────────────────────────── #[test] - // fusa:test REQ-FRAG-003 + //fusa:test REQ-FRAG-003 fn accept_fragment_accepts_strictly_incrementing_zero_based_segment_nums() { let mut buf = FragmentReassemblyBuffer::new(64); assert_eq!( @@ -363,7 +363,7 @@ mod tests { } #[test] - // fusa:test REQ-FRAG-003 + //fusa:test REQ-FRAG-003 fn accept_fragment_rejects_gap_in_segment_num() { let mut buf = FragmentReassemblyBuffer::new(64); buf.accept_fragment(&fragment_info(true, 0), b"a").unwrap(); @@ -374,7 +374,7 @@ mod tests { } #[test] - // fusa:test REQ-FRAG-003 + //fusa:test REQ-FRAG-003 fn accept_fragment_rejects_duplicate_segment_num() { let mut buf = FragmentReassemblyBuffer::new(64); buf.accept_fragment(&fragment_info(true, 0), b"a").unwrap(); @@ -385,7 +385,7 @@ mod tests { } #[test] - // fusa:test REQ-FRAG-003 + //fusa:test REQ-FRAG-003 fn accept_fragment_rejects_out_of_order_segment_num() { let mut buf = FragmentReassemblyBuffer::new(64); buf.accept_fragment(&fragment_info(true, 0), b"a").unwrap(); @@ -397,7 +397,7 @@ mod tests { } #[test] - // fusa:test REQ-FRAG-003 + //fusa:test REQ-FRAG-003 fn accept_fragment_first_call_must_start_at_segment_num_zero() { let mut buf = FragmentReassemblyBuffer::new(64); assert_eq!( @@ -407,7 +407,7 @@ mod tests { } #[test] - // fusa:test REQ-FRAG-003 + //fusa:test REQ-FRAG-003 fn rejected_fragment_does_not_mutate_buffer_state() { let mut buf = FragmentReassemblyBuffer::new(64); buf.accept_fragment(&fragment_info(true, 0), b"a").unwrap(); @@ -419,7 +419,7 @@ mod tests { // ── rx_stream_max_request_size bound ───────────────────────────────────── #[test] - // fusa:test REQ-FRAG-004 + //fusa:test REQ-FRAG-004 fn accept_fragment_rejects_when_combined_length_exceeds_bound() { let mut buf = FragmentReassemblyBuffer::new(3); buf.accept_fragment(&fragment_info(true, 0), b"ab").unwrap(); @@ -430,7 +430,7 @@ mod tests { } #[test] - // fusa:test REQ-FRAG-004 + //fusa:test REQ-FRAG-004 fn accept_fragment_accepts_combined_length_exactly_at_bound() { let mut buf = FragmentReassemblyBuffer::new(4); buf.accept_fragment(&fragment_info(true, 0), b"ab").unwrap(); @@ -441,7 +441,7 @@ mod tests { } #[test] - // fusa:test REQ-FRAG-004 + //fusa:test REQ-FRAG-004 fn overflowing_fragment_does_not_mutate_buffer_state() { let mut buf = FragmentReassemblyBuffer::new(2); buf.accept_fragment(&fragment_info(true, 0), b"ab").unwrap(); @@ -453,7 +453,7 @@ mod tests { // ── Continuing / Complete / combined payload ───────────────────────────── #[test] - // fusa:test REQ-FRAG-005 + //fusa:test REQ-FRAG-005 fn single_fragment_train_completes_immediately() { let mut buf = FragmentReassemblyBuffer::new(64); assert_eq!( @@ -467,7 +467,7 @@ mod tests { } #[test] - // fusa:test REQ-FRAG-005 + //fusa:test REQ-FRAG-005 fn combined_payload_matches_e2e_combined_fragment_payload_assemble() { let mut buf = FragmentReassemblyBuffer::new(64); let segments: [&[u8]; 3] = [b"seg-one-", b"seg-two-", b"seg-three"]; @@ -485,7 +485,7 @@ mod tests { // ── reset ───────────────────────────────────────────────────────────────── #[test] - // fusa:test REQ-FRAG-006 + //fusa:test REQ-FRAG-006 fn reset_clears_state_for_a_new_train() { let mut buf = FragmentReassemblyBuffer::new(64); buf.accept_fragment(&fragment_info(false, 0), b"first-train") @@ -503,7 +503,7 @@ mod tests { } #[test] - // fusa:test REQ-FRAG-006 + //fusa:test REQ-FRAG-006 fn accepting_a_fragment_after_complete_without_reset_is_rejected() { let mut buf = FragmentReassemblyBuffer::new(64); buf.accept_fragment(&fragment_info(false, 0), b"done") @@ -520,7 +520,7 @@ mod tests { // ── never panics ────────────────────────────────────────────────────────── #[test] - // fusa:test REQ-FRAG-008 + //fusa:test REQ-FRAG-008 fn accept_fragment_never_panics_across_arbitrary_payload_lengths() { for len in [0usize, 1, 2, 3, 17, 64, 257] { let mut buf = FragmentReassemblyBuffer::new(u16::MAX); @@ -530,7 +530,7 @@ mod tests { } #[test] - // fusa:test REQ-FRAG-008 + //fusa:test REQ-FRAG-008 fn accept_fragment_never_panics_across_segment_num_wraparound() { let mut buf = FragmentReassemblyBuffer::new(u16::MAX); // 65540 fragments is well past the 16-bit segment_num field's own @@ -569,7 +569,7 @@ mod tests { } #[test] - // fusa:test REQ-FRAG-007 + //fusa:test REQ-FRAG-007 fn verify_reassembled_train_crc_matches_manual_e2e_computation() { let mut buf = FragmentReassemblyBuffer::new(64); buf.accept_fragment(&fragment_info(true, 0), b"hello-") @@ -592,7 +592,7 @@ mod tests { } #[test] - // fusa:test REQ-FRAG-007 + //fusa:test REQ-FRAG-007 fn verify_reassembled_train_crc_rejects_missing_crc_on_completed_train() { let mut buf = FragmentReassemblyBuffer::new(64); buf.accept_fragment(&fragment_info(false, 0), b"solo") @@ -607,7 +607,7 @@ mod tests { } #[test] - // fusa:test REQ-FRAG-007 + //fusa:test REQ-FRAG-007 fn verify_reassembled_train_crc_propagates_length_overflow_error() { let mut buf = FragmentReassemblyBuffer::new(64); buf.accept_fragment(&fragment_info(false, 0), b"x").unwrap(); @@ -621,7 +621,7 @@ mod tests { } #[test] - // fusa:test REQ-FRAG-007 + //fusa:test REQ-FRAG-007 fn verify_reassembled_train_crc_on_empty_buffer_matches_empty_segments() { let buf = FragmentReassemblyBuffer::new(64); let header = HeaderVariant::Ntscf(avtp::NtscfHeader::default()); diff --git a/src/gpio.rs b/src/gpio.rs index 9387eb2..ba16ade 100644 --- a/src/gpio.rs +++ b/src/gpio.rs @@ -1,19 +1,19 @@ -// fusa:req REQ-GPIO-001 -// fusa:req REQ-GPIO-002 -// fusa:req REQ-GPIO-003 -// fusa:req REQ-GPIO-004 -// fusa:req REQ-GPIO-005 -// fusa:req REQ-GPIO-006 -// fusa:req REQ-GPIO-007 -// fusa:req REQ-GPIO-008 -// fusa:req REQ-GPIO-009 -// fusa:req REQ-GPIO-010 -// fusa:req REQ-GPIO-011 -// fusa:req REQ-GPIO-012 -// fusa:req REQ-GPIO-013 -// fusa:req REQ-GPIO-014 -// fusa:req REQ-GPIO-015 -// fusa:req REQ-GPIO-016 +//fusa:req REQ-GPIO-001 +//fusa:req REQ-GPIO-002 +//fusa:req REQ-GPIO-003 +//fusa:req REQ-GPIO-004 +//fusa:req REQ-GPIO-005 +//fusa:req REQ-GPIO-006 +//fusa:req REQ-GPIO-007 +//fusa:req REQ-GPIO-008 +//fusa:req REQ-GPIO-009 +//fusa:req REQ-GPIO-010 +//fusa:req REQ-GPIO-011 +//fusa:req REQ-GPIO-012 +//fusa:req REQ-GPIO-013 +//fusa:req REQ-GPIO-014 +//fusa:req REQ-GPIO-015 +//fusa:req REQ-GPIO-016 //! The GPIO endpoint type (`ep_type 0x02`) — `ROADMAP.md` Milestone 4 //! ("Basic Endpoint Types"), first checklist bullet: "4-byte bitmask @@ -177,18 +177,29 @@ use crate::RcpError; // ── GpioBitmask ──────────────────────────────────────────────────────────── /// Length, in bytes, of the GPIO 4-byte read/write bitmask. +/// +/// Four bytes is exactly 32 bit positions, matching TC18 §13.7.4.1's "Each +/// GPIO endpoint can handle up to 32 IOs". +//fusa:req REQ-GPIO-017 pub const GPIO_BITMASK_LEN: usize = 4; /// The GPIO endpoint's 4-byte read/write bitmask: one bit per pin. /// /// See this module's doc comment for the checklist wording this models. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-GPIO-001 +//fusa:req REQ-GPIO-001 pub struct GpioBitmask(pub u32); impl GpioBitmask { /// Encode this bitmask to its 4-byte big-endian wire representation. - // fusa:req REQ-GPIO-001 + /// + /// Big-endian places IO0 in the last byte's least-significant bit and + /// IO31 in the first byte's most-significant bit, so IO `n` is bit `n` + /// of the `byte_msg_payload` exactly as TC18 §13.7.4.1's Figure 24 lays + /// the payload out (msb on the left, IO23..IO0 in the low positions for + /// that figure's own 24-pin example endpoint). + //fusa:req REQ-GPIO-001 + //fusa:req REQ-GPIO-018 pub fn encode(self) -> [u8; GPIO_BITMASK_LEN] { self.0.to_be_bytes() } @@ -200,7 +211,7 @@ impl GpioBitmask { /// [`GPIO_BITMASK_LEN`] instead. Trailing bytes beyond the first four /// are ignored, matching [`crate::acf::decode_byte_message_info`]'s own /// handling of a longer-than-required slice. - // fusa:req REQ-GPIO-002 + //fusa:req REQ-GPIO-002 pub fn decode(b: &[u8]) -> Result { if b.len() < GPIO_BITMASK_LEN { return Err(RcpError::ShortFrame); @@ -217,15 +228,32 @@ impl GpioBitmask { /// selection via `evt.sub_opcode`", "Provenance note: the spec-reserved /// write-semantics code", and "Provenance note: `AddSaturating` and /// `Reconfigure`" for the working interpretations this type embodies. +/// These eight `evt[2:0]` codes are the endpoint's *complete* write +/// surface: TC18 §13.7.4.1 states plainly that "Generating a pulse for a +/// defined time is NOT a function of the GPIO endpoint" — it must instead be +/// managed by the Client via two commands, a compound, or a trigger +/// operation — so no duration-carrying ninth operation exists here. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] -// fusa:req REQ-GPIO-003 +//fusa:req REQ-GPIO-003 +//fusa:req REQ-GPIO-020 pub enum GpioWriteSemantics { /// Overwrite the current value with the operand. Replace = 0, /// Bitwise OR the operand into the current value. Or = 1, /// Bitwise AND the operand into the current value. + /// + /// TC18 §13.7.4.1's prose words the value-changing operations as + /// "(NAND, OR, XOR)", but TC18 §13.5's Table 30 — the normative + /// per-endpoint `evt[2:0]` table — defines code `010b` as the + /// *byte_msg_payload bitwise AND current interface status*, with the + /// worked example "with a byte_msg_payload of 0xFFFF FFFE the first IO + /// pin will be reset, while other IO pins remain unchanged". This crate + /// follows Table 30's AND; §13.7.4.1's "NAND" wording contradicts it and + /// is recorded as a TC18-internal inconsistency, not implemented as + /// written. + //fusa:req REQ-GPIO-021 And = 2, /// Bitwise XOR the operand into the current value. Xor = 3, @@ -251,7 +279,7 @@ pub enum GpioWriteSemantics { impl GpioWriteSemantics { /// Encode this write semantics as its `evt.sub_opcode` value (`0..=7`). - // fusa:req REQ-GPIO-003 + //fusa:req REQ-GPIO-003 pub fn to_sub_opcode(self) -> u8 { self as u8 } @@ -262,7 +290,7 @@ impl GpioWriteSemantics { /// 3-bit `sub_opcode` field's range /// (`> `[`crate::acf::EVT_SUB_OPCODE_MAX`]``). Never panics for any /// input. - // fusa:req REQ-GPIO-004 + //fusa:req REQ-GPIO-004 pub fn from_sub_opcode(raw: u8) -> Result { match raw { 0 => Ok(Self::Replace), @@ -281,7 +309,7 @@ impl GpioWriteSemantics { /// the seven write-semantics `ROADMAP.md`'s checklist text actually /// names. See this module's doc comment "Provenance note: the /// spec-reserved write-semantics code". - // fusa:req REQ-GPIO-009 + //fusa:req REQ-GPIO-009 pub fn is_named(self) -> bool { !matches!(self, Self::Reserved4) } @@ -297,12 +325,17 @@ impl GpioWriteSemantics { /// including at the /// [`GpioWriteSemantics::AddSaturating`]/[`GpioWriteSemantics::SubtractSaturating`] /// `u32` saturation boundaries. -// fusa:req REQ-GPIO-005 -// fusa:req REQ-GPIO-006 -// fusa:req REQ-GPIO-007 -// fusa:req REQ-GPIO-008 -// fusa:req REQ-GPIO-009 -// fusa:req REQ-GPIO-010 +/// +/// [`GpioWriteSemantics::Or`] with an all-zero operand is the identity, so +/// TC18 §13.7.4.3's "A request with data '0x0000 0000' and evt[2:0] = 0x001 +/// (OR) results in 'no change'" holds for every current value. +//fusa:req REQ-GPIO-005 +//fusa:req REQ-GPIO-006 +//fusa:req REQ-GPIO-007 +//fusa:req REQ-GPIO-008 +//fusa:req REQ-GPIO-009 +//fusa:req REQ-GPIO-010 +//fusa:req REQ-GPIO-019 pub fn apply_gpio_write( semantics: GpioWriteSemantics, current: GpioBitmask, @@ -334,7 +367,7 @@ pub const GPIO_TRIGGER_CONFIG_LEN: usize = 3 * GPIO_BITMASK_LEN; /// See this module's doc comment "Provenance note: per-pin trigger /// modeling". #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-GPIO-011 +//fusa:req REQ-GPIO-011 pub struct GpioTriggerConfig { /// Per-pin bitmask: change (either-edge) trigger enabled. pub change_enable: GpioBitmask, @@ -348,7 +381,7 @@ impl GpioTriggerConfig { /// Encode this config to its 12-byte big-endian wire representation: /// `change_enable` then `rising_enable` then `falling_enable`, each /// 4 bytes. - // fusa:req REQ-GPIO-011 + //fusa:req REQ-GPIO-011 pub fn encode(self) -> [u8; GPIO_TRIGGER_CONFIG_LEN] { let mut buf = [0u8; GPIO_TRIGGER_CONFIG_LEN]; buf[0..4].copy_from_slice(&self.change_enable.encode()); @@ -362,7 +395,7 @@ impl GpioTriggerConfig { /// Never panics on short, truncated, or arbitrary input — always /// returns `Err(RcpError::ShortFrame)` for input shorter than /// [`GPIO_TRIGGER_CONFIG_LEN`] instead. - // fusa:req REQ-GPIO-012 + //fusa:req REQ-GPIO-012 pub fn decode(b: &[u8]) -> Result { if b.len() < GPIO_TRIGGER_CONFIG_LEN { return Err(RcpError::ShortFrame); @@ -378,7 +411,7 @@ impl GpioTriggerConfig { /// Which pins actually fired a trigger between one [`GpioBitmask`] sample /// and the next, as reported by [`evaluate_gpio_triggers`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-GPIO-013 +//fusa:req REQ-GPIO-013 pub struct GpioTriggerSignals { /// Per-pin bitmask: this pin changed and had `change_enable` armed. pub changed: GpioBitmask, @@ -397,9 +430,9 @@ pub struct GpioTriggerSignals { /// differs between `previous`/`current` *and* has `change_enable` armed for /// that pin in `config` — likewise for `rising`/`falling`, each additionally /// gated on the transition direction. Never panics for any input. -// fusa:req REQ-GPIO-013 -// fusa:req REQ-GPIO-014 -// fusa:req REQ-GPIO-015 +//fusa:req REQ-GPIO-013 +//fusa:req REQ-GPIO-014 +//fusa:req REQ-GPIO-015 pub fn evaluate_gpio_triggers( config: &GpioTriggerConfig, previous: GpioBitmask, @@ -427,7 +460,7 @@ pub fn evaluate_gpio_triggers( /// this is a dedicated type rather than content added directly to /// [`crate::regmap::PerEpTypeFunctionalConfig`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-GPIO-016 +//fusa:req REQ-GPIO-016 pub struct GpioFunctionalConfig { /// This endpoint's per-pin trigger arming. pub trigger: GpioTriggerConfig, @@ -441,7 +474,7 @@ impl GpioFunctionalConfig { /// This module does not itself call that function — it only shows how a /// caller would obtain the matching tag, per this module's doc comment /// "Relationship to `crate::regmap`". - // fusa:req REQ-GPIO-016 + //fusa:req REQ-GPIO-016 pub fn layer_tag(&self) -> crate::regmap::PerEpTypeFunctionalConfig { crate::regmap::PerEpTypeFunctionalConfig::new(crate::regmap::EndpointType::Gpio) } @@ -454,7 +487,7 @@ mod tests { // ── GpioBitmask: round-trip / never-panic ─────────────────────────────── #[test] - // fusa:test REQ-GPIO-001 + //fusa:test REQ-GPIO-001 fn gpio_bitmask_round_trips_through_encode_decode() { for raw in [0u32, 1, 0xFFFF_FFFF, 0x8000_0001, 0x0102_0304] { let mask = GpioBitmask(raw); @@ -464,14 +497,14 @@ mod tests { } #[test] - // fusa:test REQ-GPIO-001 + //fusa:test REQ-GPIO-001 fn gpio_bitmask_encode_is_big_endian() { let mask = GpioBitmask(0x0102_0304); assert_eq!(mask.encode(), [0x01, 0x02, 0x03, 0x04]); } #[test] - // fusa:test REQ-GPIO-002 + //fusa:test REQ-GPIO-002 fn gpio_bitmask_decode_rejects_short_input() { for len in 0..GPIO_BITMASK_LEN { let short = vec![0xAAu8; len]; @@ -480,7 +513,7 @@ mod tests { } #[test] - // fusa:test REQ-GPIO-002 + //fusa:test REQ-GPIO-002 fn gpio_bitmask_decode_ignores_trailing_bytes() { let b = [0x00, 0x00, 0x00, 0x2A, 0xFF, 0xFF]; assert_eq!(GpioBitmask::decode(&b).unwrap(), GpioBitmask(42)); @@ -500,7 +533,7 @@ mod tests { ]; #[test] - // fusa:test REQ-GPIO-003 + //fusa:test REQ-GPIO-003 fn gpio_write_semantics_sub_opcode_round_trips_for_all_eight_values() { for semantics in ALL_WRITE_SEMANTICS { let raw = semantics.to_sub_opcode(); @@ -510,7 +543,7 @@ mod tests { } #[test] - // fusa:test REQ-GPIO-003 + //fusa:test REQ-GPIO-003 fn gpio_write_semantics_sub_opcode_values_are_the_full_0_to_7_range() { let mut raws: Vec = ALL_WRITE_SEMANTICS .iter() @@ -521,7 +554,7 @@ mod tests { } #[test] - // fusa:test REQ-GPIO-004 + //fusa:test REQ-GPIO-004 fn gpio_write_semantics_from_sub_opcode_rejects_out_of_range() { for raw in [8u8, 9, 0x7F, 0xFF] { assert_eq!( @@ -532,7 +565,7 @@ mod tests { } #[test] - // fusa:test REQ-GPIO-009 + //fusa:test REQ-GPIO-009 fn gpio_write_semantics_is_named_true_only_for_the_seven_named_variants() { for semantics in ALL_WRITE_SEMANTICS { assert_eq!( @@ -545,7 +578,7 @@ mod tests { // ── apply_gpio_write: per-semantics correctness ───────────────────────── #[test] - // fusa:test REQ-GPIO-005 + //fusa:test REQ-GPIO-005 fn apply_gpio_write_replace_overwrites_current() { let result = apply_gpio_write( GpioWriteSemantics::Replace, @@ -556,7 +589,7 @@ mod tests { } #[test] - // fusa:test REQ-GPIO-005 + //fusa:test REQ-GPIO-005 fn apply_gpio_write_or_and_xor_match_bitwise_ops() { let current = GpioBitmask(0b1010); let operand = GpioBitmask(0b0110); @@ -575,7 +608,7 @@ mod tests { } #[test] - // fusa:test REQ-GPIO-006 + //fusa:test REQ-GPIO-006 fn apply_gpio_write_add_saturating_clamps_at_u32_max() { let result = apply_gpio_write( GpioWriteSemantics::AddSaturating, @@ -586,7 +619,7 @@ mod tests { } #[test] - // fusa:test REQ-GPIO-006 + //fusa:test REQ-GPIO-006 fn apply_gpio_write_add_saturating_exact_u32_max_boundary() { let result = apply_gpio_write( GpioWriteSemantics::AddSaturating, @@ -597,7 +630,7 @@ mod tests { } #[test] - // fusa:test REQ-GPIO-006 + //fusa:test REQ-GPIO-006 fn apply_gpio_write_add_saturating_ordinary_case() { let result = apply_gpio_write( GpioWriteSemantics::AddSaturating, @@ -608,7 +641,7 @@ mod tests { } #[test] - // fusa:test REQ-GPIO-007 + //fusa:test REQ-GPIO-007 fn apply_gpio_write_subtract_saturating_clamps_at_zero() { let result = apply_gpio_write( GpioWriteSemantics::SubtractSaturating, @@ -619,7 +652,7 @@ mod tests { } #[test] - // fusa:test REQ-GPIO-007 + //fusa:test REQ-GPIO-007 fn apply_gpio_write_subtract_saturating_exact_zero_boundary() { let result = apply_gpio_write( GpioWriteSemantics::SubtractSaturating, @@ -630,7 +663,7 @@ mod tests { } #[test] - // fusa:test REQ-GPIO-007 + //fusa:test REQ-GPIO-007 fn apply_gpio_write_subtract_saturating_ordinary_case() { let result = apply_gpio_write( GpioWriteSemantics::SubtractSaturating, @@ -641,7 +674,7 @@ mod tests { } #[test] - // fusa:test REQ-GPIO-008 + //fusa:test REQ-GPIO-008 fn apply_gpio_write_reconfigure_matches_replace_at_the_bitmask_level() { let current = GpioBitmask(0xFFFF_FFFF); let operand = GpioBitmask(0x1234); @@ -652,7 +685,7 @@ mod tests { } #[test] - // fusa:test REQ-GPIO-009 + //fusa:test REQ-GPIO-009 fn apply_gpio_write_refuses_the_spec_reserved_semantics() { let result = apply_gpio_write( GpioWriteSemantics::Reserved4, @@ -663,7 +696,7 @@ mod tests { } #[test] - // fusa:test REQ-GPIO-010 + //fusa:test REQ-GPIO-010 fn apply_gpio_write_never_panics_for_any_sampled_input() { let samples = [0u32, 1, 2, 0x7FFF_FFFF, 0x8000_0000, 0xFFFF_FFFE, u32::MAX]; for semantics in ALL_WRITE_SEMANTICS { @@ -678,7 +711,7 @@ mod tests { // ── GpioTriggerConfig: round-trip / never-panic ───────────────────────── #[test] - // fusa:test REQ-GPIO-011 + //fusa:test REQ-GPIO-011 fn gpio_trigger_config_round_trips_through_encode_decode() { let config = GpioTriggerConfig { change_enable: GpioBitmask(0x1111_1111), @@ -690,7 +723,7 @@ mod tests { } #[test] - // fusa:test REQ-GPIO-011 + //fusa:test REQ-GPIO-011 fn gpio_trigger_config_encode_field_order_is_change_rising_falling() { let config = GpioTriggerConfig { change_enable: GpioBitmask(0x0000_0001), @@ -704,7 +737,7 @@ mod tests { } #[test] - // fusa:test REQ-GPIO-012 + //fusa:test REQ-GPIO-012 fn gpio_trigger_config_decode_rejects_short_input() { for len in 0..GPIO_TRIGGER_CONFIG_LEN { let short = vec![0xAAu8; len]; @@ -715,7 +748,7 @@ mod tests { // ── evaluate_gpio_triggers: edge detection + arming ───────────────────── #[test] - // fusa:test REQ-GPIO-013 + //fusa:test REQ-GPIO-013 fn evaluate_gpio_triggers_detects_rising_edge_on_armed_pin() { let config = GpioTriggerConfig { change_enable: GpioBitmask(0xFFFF_FFFF), @@ -729,7 +762,7 @@ mod tests { } #[test] - // fusa:test REQ-GPIO-013 + //fusa:test REQ-GPIO-013 fn evaluate_gpio_triggers_detects_falling_edge_on_armed_pin() { let config = GpioTriggerConfig { change_enable: GpioBitmask(0xFFFF_FFFF), @@ -743,7 +776,7 @@ mod tests { } #[test] - // fusa:test REQ-GPIO-013 + //fusa:test REQ-GPIO-013 fn evaluate_gpio_triggers_no_signal_when_value_unchanged() { let config = GpioTriggerConfig { change_enable: GpioBitmask(0xFFFF_FFFF), @@ -755,7 +788,7 @@ mod tests { } #[test] - // fusa:test REQ-GPIO-014 + //fusa:test REQ-GPIO-014 fn evaluate_gpio_triggers_masks_out_disarmed_pins() { // Pin 0 rises but only pin 1's rising trigger is armed. let config = GpioTriggerConfig { @@ -768,7 +801,7 @@ mod tests { } #[test] - // fusa:test REQ-GPIO-014 + //fusa:test REQ-GPIO-014 fn evaluate_gpio_triggers_per_pin_independence() { // Pin 0 rises (armed), pin 1 falls (not armed for falling). let config = GpioTriggerConfig { @@ -783,7 +816,7 @@ mod tests { } #[test] - // fusa:test REQ-GPIO-015 + //fusa:test REQ-GPIO-015 fn evaluate_gpio_triggers_never_panics_for_any_sampled_input() { let samples = [0u32, 1, 0x5555_5555, 0xAAAA_AAAA, 0x8000_0000, u32::MAX]; let config = GpioTriggerConfig { @@ -802,7 +835,7 @@ mod tests { // ── GpioFunctionalConfig / crate::regmap composition ──────────────────── #[test] - // fusa:test REQ-GPIO-016 + //fusa:test REQ-GPIO-016 fn gpio_functional_config_layer_tag_matches_ep_type_gpio() { let functional = GpioFunctionalConfig::default(); let generic = crate::regmap::PerEpConfigBlock::new(crate::regmap::EndpointType::Gpio); @@ -817,8 +850,145 @@ mod tests { ); } + // ── TC18 §13.7.4 spec-literal conformance checks ──────────────────────── + + /// TC18 §13.7.4.1 (TC18.txt line 4381): "Each GPIO endpoint can handle + /// up to 32 IOs." The 4-byte bitmask is exactly 32 bit positions wide, + /// so IO0..IO31 are all representable and there is no IO32. + #[test] + //fusa:test REQ-GPIO-017 + fn gpio_bitmask_spans_exactly_the_32_ios_tc18_admits() { + // 4 bytes x 8 bits = the 32 IO positions TC18 §13.7.4.1 names. + assert_eq!(GPIO_BITMASK_LEN, 4); + assert_eq!(GPIO_BITMASK_LEN * 8, 32); + // IO31 — the highest IO a 32-IO endpoint has — is representable and + // round-trips. + let io31 = GpioBitmask(1u32 << 31); + assert_eq!(io31.encode(), [0x80, 0x00, 0x00, 0x00]); + assert_eq!(GpioBitmask::decode(&io31.encode()), Ok(io31)); + // All 32 IOs asserted at once is the all-ones 4-byte payload. + assert_eq!(GpioBitmask(u32::MAX).encode(), [0xFF, 0xFF, 0xFF, 0xFF]); + // A fifth payload byte would be outside the endpoint's 32-IO reach: + // decode reads exactly four. + assert_eq!( + GpioBitmask::decode(&[0x80, 0x00, 0x00, 0x00, 0xFF]), + Ok(io31) + ); + } + + /// TC18 §13.7.4.1 / Figure 24 (TC18.txt lines 4389-4398): "Each GPIO pin + /// can be assigned to a bit position of the byte_msg_payload", laid out + /// msb-first with IO23..IO0 in the low bit positions for that figure's + /// own 24-pin example endpoint. IO `n` is therefore bit `n`, and the + /// 4-byte payload is big-endian. + #[test] + //fusa:test REQ-GPIO-018 + fn gpio_bitmask_maps_io_n_to_bit_n_per_tc18_figure_24() { + // IO0 is the least-significant bit: last payload byte, bit 0. + assert_eq!(GpioBitmask(1u32).encode(), [0x00, 0x00, 0x00, 0x01]); + // IO7 completes the last payload byte. + assert_eq!(GpioBitmask(1u32 << 7).encode(), [0x00, 0x00, 0x00, 0x80]); + // IO8 opens the next byte up. + assert_eq!(GpioBitmask(1u32 << 8).encode(), [0x00, 0x00, 0x01, 0x00]); + // IO23 — the highest IO drawn in Figure 24's 24-pin example. + assert_eq!(GpioBitmask(1u32 << 23).encode(), [0x00, 0x80, 0x00, 0x00]); + // Figure 24's whole 24-pin example EP: IO0..IO23 asserted, the + // "don't care" positions above IO23 clear. + assert_eq!(GpioBitmask(0x00FF_FFFF).encode(), [0x00, 0xFF, 0xFF, 0xFF]); + // IO31 tops out the 32-IO range. + assert_eq!(GpioBitmask(1u32 << 31).encode(), [0x80, 0x00, 0x00, 0x00]); + } + + /// TC18 §13.7.4.3 (TC18.txt line 4480): "A request with data + /// '0x0000 0000' and evt[2:0] = 0x001 (OR) results in 'no change'." + #[test] + //fusa:test REQ-GPIO-019 + fn gpio_or_with_all_zero_payload_is_no_change_per_tc18_13_7_4_3() { + // evt[2:0] = 0x001 is the OR semantics. + assert_eq!( + GpioWriteSemantics::from_sub_opcode(0x001), + Ok(GpioWriteSemantics::Or) + ); + // The literal 0x0000_0000 payload leaves every current interface + // state untouched, bit for bit. + for current in [0x0000_0000u32, 0x0000_0002, 0x5555_5555, 0xFFFF_FFFF] { + assert_eq!( + apply_gpio_write( + GpioWriteSemantics::Or, + GpioBitmask(current), + GpioBitmask(0x0000_0000) + ), + Ok(GpioBitmask(current)) + ); + } + } + + /// TC18 §13.7.4.1 (TC18.txt lines 4403-4404): "Generating a pulse for a + /// defined time is NOT a function of the GPIO endpoint. This either + /// needs to be managed by the Client ... by a compound or a trigger + /// operation." The endpoint's whole write surface is therefore exactly + /// TC18 §13.5 Table 30's eight `evt[2:0]` value-level codes, with no + /// ninth, duration-carrying operation. + #[test] + //fusa:test REQ-GPIO-020 + fn gpio_write_surface_is_tc18_eight_codes_with_no_pulse_operation() { + // TC18 §13.5 Table 30's GPIO/PWM_OUT rows, code by code. + let table_30: [(u8, GpioWriteSemantics); 8] = [ + (0b000, GpioWriteSemantics::Replace), + (0b001, GpioWriteSemantics::Or), + (0b010, GpioWriteSemantics::And), + (0b011, GpioWriteSemantics::Xor), + (0b100, GpioWriteSemantics::Reserved4), + (0b101, GpioWriteSemantics::AddSaturating), + (0b110, GpioWriteSemantics::SubtractSaturating), + (0b111, GpioWriteSemantics::Reconfigure), + ]; + for (code, semantics) in table_30 { + assert_eq!(GpioWriteSemantics::from_sub_opcode(code), Ok(semantics)); + } + // Those eight are the complete set — nothing outside `evt[2:0]`'s + // 3-bit range decodes, so there is no pulse-shaped ninth operation. + assert_eq!(table_30.len(), ALL_WRITE_SEMANTICS.len()); + for raw in 8u8..=0x1F { + assert_eq!( + GpioWriteSemantics::from_sub_opcode(raw), + Err(RcpError::InvalidParameter) + ); + } + } + + /// TC18 §13.7.4.1 (TC18.txt line 4402) words the value-changing + /// operations as "(NAND, OR, XOR)", but TC18 §13.5 Table 30 (TC18.txt + /// lines 3699-3702) defines `evt[2:0] = 010b` as the byte_msg_payload + /// *bitwise AND* the current interface status, with the worked example + /// "with a byte_msg_payload of 0xFFFF FFFE the first IO pin will be + /// reset, while other IO pins remain unchanged". This crate follows + /// Table 30's AND. + #[test] + //fusa:test REQ-GPIO-021 + fn gpio_evt_010b_is_bitwise_and_per_tc18_table_30_worked_example() { + assert_eq!( + GpioWriteSemantics::from_sub_opcode(0b010), + Ok(GpioWriteSemantics::And) + ); + // Table 30's own worked example: every IO currently set, payload + // 0xFFFF_FFFE -> IO0 reset, IO1..IO31 unchanged. + let current = GpioBitmask(0xFFFF_FFFF); + let payload = GpioBitmask(0xFFFF_FFFE); + assert_eq!( + apply_gpio_write(GpioWriteSemantics::And, current, payload), + Ok(GpioBitmask(0xFFFF_FFFE)) + ); + // A NAND, as §13.7.4.1's prose would have it, would instead have + // produced the complement — 0x0000_0001. + assert_ne!( + apply_gpio_write(GpioWriteSemantics::And, current, payload), + Ok(GpioBitmask(0x0000_0001)) + ); + } + #[test] - // fusa:test REQ-GPIO-016 + //fusa:test REQ-GPIO-016 fn gpio_functional_config_layer_tag_rejects_mismatched_ep_type() { let functional = GpioFunctionalConfig::default(); let generic = crate::regmap::PerEpConfigBlock::new(crate::regmap::EndpointType::Spi); diff --git a/src/i2c.rs b/src/i2c.rs index f03c910..8ca323d 100644 --- a/src/i2c.rs +++ b/src/i2c.rs @@ -1,9 +1,9 @@ -// fusa:req REQ-I2C-001 -// fusa:req REQ-I2C-002 -// fusa:req REQ-I2C-003 -// fusa:req REQ-I2C-004 -// fusa:req REQ-I2C-005 -// fusa:req REQ-I2C-006 +//fusa:req REQ-I2C-001 +//fusa:req REQ-I2C-002 +//fusa:req REQ-I2C-003 +//fusa:req REQ-I2C-004 +//fusa:req REQ-I2C-005 +//fusa:req REQ-I2C-006 //! The I²C endpoint type (`ep_type 0x04`) — `ROADMAP.md` Milestone 4 //! ("Basic Endpoint Types"), third checklist bullet: "controller-only, raw @@ -124,7 +124,7 @@ use crate::RcpError; /// speed. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] -// fusa:req REQ-I2C-001 +//fusa:req REQ-I2C-001 pub enum I2cSpeedMode { /// Standard-mode speed preset (this crate's slowest, most broadly /// compatible preset). @@ -145,7 +145,8 @@ pub enum I2cSpeedMode { impl I2cSpeedMode { /// Encode this speed preset as its `i2c_mode` wire byte value. - // fusa:req REQ-I2C-001 + //fusa:req REQ-I2C-001 + //fusa:req REQ-I2C-009 pub fn to_u8(self) -> u8 { self as u8 } @@ -155,7 +156,9 @@ impl I2cSpeedMode { /// Returns `Err(RcpError::InvalidParameter)` for any byte outside /// `0..=4`, matching [`crate::spi::SpiChannelSelect::from_sub_opcode`]'s /// own range-check discipline. Never panics for any input. - // fusa:req REQ-I2C-002 + //fusa:req REQ-I2C-002 + //fusa:req REQ-I2C-009 + //fusa:req REQ-I2C-010 pub fn from_u8(raw: u8) -> Result { match raw { 0 => Ok(Self::Standard), @@ -171,7 +174,8 @@ impl I2cSpeedMode { /// — the two adjacent high-speed `i2c_mode` rows this module's doc /// comment flags as unresolved pending errata. False for /// [`I2cSpeedMode::Standard`]/[`I2cSpeedMode::Fast`]/[`I2cSpeedMode::FastPlus`]. - // fusa:req REQ-I2C-003 + //fusa:req REQ-I2C-003 + //fusa:req REQ-I2C-010 pub fn is_ambiguous_high_speed_row(self) -> bool { matches!(self, Self::HighSpeedRowA | Self::HighSpeedRowB) } @@ -195,7 +199,7 @@ impl Default for I2cSpeedMode { /// this is a dedicated type rather than content added directly to /// [`crate::regmap::PerEpTypeFunctionalConfig`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-I2C-004 +//fusa:req REQ-I2C-004 pub struct I2cFunctionalConfig { /// This endpoint's configured `i2c_mode` speed preset. pub speed_mode: I2cSpeedMode, @@ -209,7 +213,7 @@ impl I2cFunctionalConfig { /// This module does not itself call that function — it only shows how a /// caller would obtain the matching tag, per this module's doc comment /// "Relationship to `crate::regmap`". - // fusa:req REQ-I2C-004 + //fusa:req REQ-I2C-004 pub fn layer_tag(&self) -> crate::regmap::PerEpTypeFunctionalConfig { crate::regmap::PerEpTypeFunctionalConfig::new(crate::regmap::EndpointType::I2c) } @@ -228,7 +232,7 @@ impl I2cFunctionalConfig { /// an empty one, has a valid encoding, so [`I2cByteTransfer::decode`] is /// infallible. #[derive(Debug, Clone, PartialEq, Eq, Default)] -// fusa:req REQ-I2C-005 +//fusa:req REQ-I2C-005 pub struct I2cByteTransfer { /// The raw bytes sent from controller to bus, address byte(s) included. pub bytes: Vec, @@ -237,7 +241,8 @@ pub struct I2cByteTransfer { impl I2cByteTransfer { /// Encode this transfer to its raw wire representation: `bytes`, /// unmodified and unframed. - // fusa:req REQ-I2C-005 + //fusa:req REQ-I2C-005 + //fusa:req REQ-I2C-011 pub fn encode(&self) -> Vec { self.bytes.clone() } @@ -246,7 +251,8 @@ impl I2cByteTransfer { /// /// Every possible byte slice, including an empty one, is a valid I²C /// transfer, so this never fails and never panics for any input. - // fusa:req REQ-I2C-005 + //fusa:req REQ-I2C-005 + //fusa:req REQ-I2C-011 pub fn decode(b: &[u8]) -> Self { Self { bytes: b.to_vec() } } @@ -259,7 +265,7 @@ impl I2cByteTransfer { /// variable-length byte-stream modeling for the opposite transfer /// direction. #[derive(Debug, Clone, PartialEq, Eq, Default)] -// fusa:req REQ-I2C-006 +//fusa:req REQ-I2C-006 pub struct I2cByteTransferResult { /// The raw bytes returned from bus to controller. pub bytes: Vec, @@ -268,7 +274,7 @@ pub struct I2cByteTransferResult { impl I2cByteTransferResult { /// Encode this transfer result to its raw wire representation: `bytes`, /// unmodified and unframed. - // fusa:req REQ-I2C-006 + //fusa:req REQ-I2C-006 pub fn encode(&self) -> Vec { self.bytes.clone() } @@ -277,7 +283,8 @@ impl I2cByteTransferResult { /// /// Every possible byte slice, including an empty one, is a valid I²C /// transfer result, so this never fails and never panics for any input. - // fusa:req REQ-I2C-006 + //fusa:req REQ-I2C-006 + //fusa:req REQ-I2C-011 pub fn decode(b: &[u8]) -> Self { Self { bytes: b.to_vec() } } @@ -298,7 +305,7 @@ mod tests { ]; #[test] - // fusa:test REQ-I2C-001 + //fusa:test REQ-I2C-001 fn i2c_speed_mode_round_trips_through_to_u8_from_u8_for_all_five_values() { for mode in ALL_SPEED_MODES { let raw = mode.to_u8(); @@ -307,7 +314,7 @@ mod tests { } #[test] - // fusa:test REQ-I2C-001 + //fusa:test REQ-I2C-001 fn i2c_speed_mode_to_u8_values_are_the_full_0_to_4_range() { let mut raws: Vec = ALL_SPEED_MODES.iter().map(|m| m.to_u8()).collect(); raws.sort_unstable(); @@ -315,7 +322,7 @@ mod tests { } #[test] - // fusa:test REQ-I2C-002 + //fusa:test REQ-I2C-002 fn i2c_speed_mode_from_u8_rejects_out_of_range() { for raw in [5u8, 6, 0x7F, 0xFF] { assert_eq!(I2cSpeedMode::from_u8(raw), Err(RcpError::InvalidParameter)); @@ -323,7 +330,7 @@ mod tests { } #[test] - // fusa:test REQ-I2C-003 + //fusa:test REQ-I2C-003 fn i2c_speed_mode_is_ambiguous_high_speed_row_true_only_for_the_two_flagged_rows() { for mode in ALL_SPEED_MODES { let expected = matches!( @@ -335,24 +342,107 @@ mod tests { } #[test] - // fusa:test REQ-I2C-003 + //fusa:test REQ-I2C-003 fn i2c_speed_mode_default_is_standard_and_not_an_ambiguous_row() { let mode = I2cSpeedMode::default(); assert_eq!(mode, I2cSpeedMode::Standard); assert!(!mode.is_ambiguous_high_speed_row()); } + // ── TC18 Table 46: i2c_mode preset wire values ────────────────────────── + + #[test] + //fusa:test REQ-I2C-009 + fn i2c_mode_wire_values_match_tc18_table_46_unambiguous_rows() { + // TC18 §13.7.7.2 Table 46 (TC18.txt lines 4815-4817), i2c_mode + // (relative address 0x0007, 8 bit R/W): + // 0: Standard Mode 100kbit/s + // 1: Fast Mode 400kbit/s + // 2: Fast Mode plus 1Mbit/s + assert_eq!(I2cSpeedMode::from_u8(0), Ok(I2cSpeedMode::Standard)); + assert_eq!(I2cSpeedMode::from_u8(1), Ok(I2cSpeedMode::Fast)); + assert_eq!(I2cSpeedMode::from_u8(2), Ok(I2cSpeedMode::FastPlus)); + assert_eq!(I2cSpeedMode::Standard.to_u8(), 0); + assert_eq!(I2cSpeedMode::Fast.to_u8(), 1); + assert_eq!(I2cSpeedMode::FastPlus.to_u8(), 2); + // None of these three rows is one of Table 46's duplicated + // high-speed rows, so none may be reported as unresolved. + for mode in [ + I2cSpeedMode::Standard, + I2cSpeedMode::Fast, + I2cSpeedMode::FastPlus, + ] { + assert!(!mode.is_ambiguous_high_speed_row()); + } + } + + #[test] + //fusa:test REQ-I2C-010 + fn i2c_mode_value_three_is_not_resolved_to_a_single_high_speed_rate() { + // TC18 §13.7.7.2 Table 46 (TC18.txt lines 4818-4819) lists two + // adjacent High-speed rows that both carry the same i2c_mode wire + // value 3: + // 3: High-speed mode 1.7Mbit/s + // 3: High-speed mode 3.4Mbit/s + // Decoding value 3 must therefore be flagged as unresolved rather + // than silently picking either bit rate. + let decoded = I2cSpeedMode::from_u8(3).expect("3 is an enumerated Table 46 i2c_mode value"); + assert!(decoded.is_ambiguous_high_speed_row()); + assert_eq!(decoded.to_u8(), 3); + } + + // ── TC18 §13.7.7.3: address-format transparency ───────────────────────── + + #[test] + //fusa:test REQ-I2C-011 + fn i2c_byte_transfer_is_transparent_to_seven_and_ten_bit_addressing() { + // TC18 §13.7.7.3 (TC18.txt line 4830): "The byte msg payload is the + // I2C payload including the address. The I2C endpoint does not know + // whether there is a 7- or 10-bit address, since the endpoint is just + // transparent." The worked example there (Figure 29) is an I²C + // transfer with a 10-bit address and 5 bytes of data — 2 address + // bytes + 5 data bytes = a 7-byte byte_msg_payload. + let ten_bit_addressed = vec![0xF2, 0x34, 0x11, 0x22, 0x33, 0x44, 0x55]; + assert_eq!(ten_bit_addressed.len(), 7); + let transfer = I2cByteTransfer { + bytes: ten_bit_addressed.clone(), + }; + // Emitted verbatim: no length prefix, no address framing, no + // reordering, nothing stripped. + assert_eq!(transfer.encode(), ten_bit_addressed); + assert_eq!( + I2cByteTransfer::decode(&ten_bit_addressed).bytes, + ten_bit_addressed + ); + + // The same 5 data bytes behind a single 7-bit address byte are + // carried identically — exactly one byte shorter, nothing else + // differs, and no addressing scheme is inferred either way. + let seven_bit_addressed = vec![0xA0, 0x11, 0x22, 0x33, 0x44, 0x55]; + assert_eq!(seven_bit_addressed.len(), ten_bit_addressed.len() - 1); + assert_eq!( + I2cByteTransfer::decode(&seven_bit_addressed).bytes, + seven_bit_addressed + ); + + // The bus-to-controller direction is equally transparent. + assert_eq!( + I2cByteTransferResult::decode(&ten_bit_addressed).encode(), + ten_bit_addressed + ); + } + // ── I2cFunctionalConfig / layer_tag ───────────────────────────────────── #[test] - // fusa:test REQ-I2C-004 + //fusa:test REQ-I2C-004 fn i2c_functional_config_default_uses_default_speed_mode() { let config = I2cFunctionalConfig::default(); assert_eq!(config.speed_mode, I2cSpeedMode::default()); } #[test] - // fusa:test REQ-I2C-004 + //fusa:test REQ-I2C-004 fn i2c_functional_config_layer_tag_matches_ep_type_i2c() { let functional = I2cFunctionalConfig::default(); let generic = crate::regmap::PerEpConfigBlock::new(crate::regmap::EndpointType::I2c); @@ -368,7 +458,7 @@ mod tests { } #[test] - // fusa:test REQ-I2C-004 + //fusa:test REQ-I2C-004 fn i2c_functional_config_layer_tag_rejects_mismatched_ep_type() { let functional = I2cFunctionalConfig::default(); let generic = crate::regmap::PerEpConfigBlock::new(crate::regmap::EndpointType::Spi); @@ -381,7 +471,7 @@ mod tests { // ── I2cByteTransfer: round-trip / never-panic ─────────────────────────── #[test] - // fusa:test REQ-I2C-005 + //fusa:test REQ-I2C-005 fn i2c_byte_transfer_round_trips_through_encode_decode() { for bytes in [ vec![], @@ -400,7 +490,7 @@ mod tests { } #[test] - // fusa:test REQ-I2C-005 + //fusa:test REQ-I2C-005 fn i2c_byte_transfer_decode_never_panics_for_any_sampled_input() { for len in [0usize, 1, 2, 7, 64] { let buf = vec![0x5Au8; len]; @@ -411,7 +501,7 @@ mod tests { // ── I2cByteTransferResult: round-trip / never-panic ───────────────────── #[test] - // fusa:test REQ-I2C-006 + //fusa:test REQ-I2C-006 fn i2c_byte_transfer_result_round_trips_through_encode_decode() { for bytes in [vec![], vec![0xFF], vec![0x01, 0x02, 0x03]] { let result = I2cByteTransferResult { @@ -422,7 +512,7 @@ mod tests { } #[test] - // fusa:test REQ-I2C-006 + //fusa:test REQ-I2C-006 fn i2c_byte_transfer_result_decode_never_panics_for_any_sampled_input() { for len in [0usize, 1, 5, 32] { let buf = vec![0xA5u8; len]; diff --git a/src/iseled.rs b/src/iseled.rs index 38e8e2f..96da282 100644 --- a/src/iseled.rs +++ b/src/iseled.rs @@ -1,13 +1,13 @@ -// fusa:req REQ-ISELED-001 -// fusa:req REQ-ISELED-002 -// fusa:req REQ-ISELED-003 -// fusa:req REQ-ISELED-004 -// fusa:req REQ-ISELED-005 -// fusa:req REQ-ISELED-006 -// fusa:req REQ-ISELED-007 -// fusa:req REQ-ISELED-008 -// fusa:req REQ-ISELED-009 -// fusa:req REQ-ISELED-010 +//fusa:req REQ-ISELED-001 +//fusa:req REQ-ISELED-002 +//fusa:req REQ-ISELED-003 +//fusa:req REQ-ISELED-004 +//fusa:req REQ-ISELED-005 +//fusa:req REQ-ISELED-006 +//fusa:req REQ-ISELED-007 +//fusa:req REQ-ISELED-008 +//fusa:req REQ-ISELED-009 +//fusa:req REQ-ISELED-010 //! The ISELED endpoint type (`ep_type 0x0C`) — `ROADMAP.md` Milestone 7 //! ("Remaining Endpoint Types"), third checklist bullet: native @@ -144,6 +144,52 @@ //! spec prose) is expected to update `iseled_frame_crc8`'s algorithm and //! reconsider this gate then, not now. //! +//! ## TC18 reconciliation note (§13.7.12) +//! +//! Reconciling this module against TC18 §13.7.12 confirms one behavior and +//! records a substantial set of gaps, all recorded as explicit +//! not-implemented requirement entries rather than silently omitted. +//! +//! Confirmed: TC18 §13.7.12.3 (TC18.txt line 5578) states "The ISELED +//! request and response contains plain data in the `byte_msg_payload` that +//! is to be presented or has been received on the ISELED bus", and +//! [`IseledFrame::encode`] emits its fields verbatim, in order, inserting +//! nothing — in particular no CRC, which line 5595 confirms "is not present +//! on the ISELED network" unless the endpoint is configured to generate one. +//! +//! Not implemented: +//! +//! - The request payload's own field layout. TC18 Figure 40 (line 5588) and +//! its accompanying example (line 5597) describe "4 bit instruction, 12 +//! bit address and 3 bytes of data"; this module's [`IseledFrame`] instead +//! carries a full-byte `chain_address` and a full-byte `command`, so an +//! [`IseledFrame::encode`] buffer is **not** field-compatible with Figure +//! 40 even though it is byte-order-preserving. +//! - The response payload's own field layout. TC18 §13.7.12.3 (line 5600) +//! states "A response always contains the 12 bit address and 12 bit data +//! plus the optional 4 bit CRC"; [`IseledDeviceResponse`] carries a +//! full-byte `chain_address` plus opaque data bytes instead. +//! - The 4/5-bit encoding's own code-group table. TC18 (line 5492) requires +//! data to be "4/5bit encoded according to the ISLED standard"; this crate +//! has no access to that standard's table and uses the public +//! FDDI/100BASE-TX one ([`NIBBLE_TO_5B`]) as an explicitly unconfirmed +//! stand-in, so conformance of the *values* is not claimed. +//! - Aggregation of 5/4-bit-decoded responses into one or multiple ACF +//! messages bounded by the request's `read_size` (lines 5493-5494); +//! [`iseled_collect_resp`] performs no `read_size` accounting and emits no +//! ACF message. +//! - Generating and attaching the optional native CRC to write messages, and +//! recomputing and checking it on read data (lines 5494-5496). +//! - The single trigger event on completion of a data packet's transmission +//! (line 5497). +//! - TC18 Table 55's functional-config register layout (§13.7.12.2, lines +//! 5504-5545), including `iseled_collect_resp` (0x0007.3, 1 bit), +//! `iseled_use_rcv_clk` (0x0007.4, 1 bit), `iseled_nr_leds` (0x0008, +//! 16 bit) and `iseled_rcv_timeout` (0x000A, 16 bit) — see +//! [`IseledFunctionalConfig`], which carries one `native_crc_enabled` +//! flag and nothing else — and the Freq_Sync-vs-ISP_N clock-recovery +//! choice that `iseled_use_rcv_clk` selects (lines 5549-5551). +//! //! ## Multi-device response aggregation //! //! A daisy chain's devices each contribute their own response; per @@ -226,7 +272,7 @@ fn symbol_to_nibble(symbol: u8) -> Option { /// module's doc comment for why this crate represents 4b/5b output at /// symbol-per-byte granularity rather than as a packed bitstream. Never /// panics for any input, including empty input. -// fusa:req REQ-ISELED-001 +//fusa:req REQ-ISELED-001 pub fn encode_4b5b(data: &[u8]) -> Vec { let mut out = Vec::with_capacity(data.len() * 2); for &byte in data { @@ -245,7 +291,7 @@ pub fn encode_4b5b(data: &[u8]) -> Vec { /// `symbols` has an odd length — every encoded byte contributes exactly two /// symbols, so a trailing lone symbol cannot complete a byte. Never panics /// for any input. -// fusa:req REQ-ISELED-002 +//fusa:req REQ-ISELED-002 pub fn decode_4b5b(symbols: &[u8]) -> Result, RcpError> { if symbols.len() % 2 != 0 { return Err(RcpError::ShortFrame); @@ -268,7 +314,7 @@ pub fn decode_4b5b(symbols: &[u8]) -> Result, RcpError> { /// layout" for why this shape is this crate's own working interpretation, /// and for why `data` carries no length ceiling. #[derive(Debug, Clone, PartialEq, Eq, Default)] -// fusa:req REQ-ISELED-003 +//fusa:req REQ-ISELED-003 pub struct IseledFrame { /// The client-supplied device-selector byte within the daisy chain, /// carried unparsed — this module takes no position on any @@ -285,7 +331,17 @@ pub struct IseledFrame { impl IseledFrame { /// Encode this frame to its raw (pre-line-coding) wire representation: /// `chain_address`, then `command`, then `data`, unmodified. - // fusa:req REQ-ISELED-003 + /// + /// This is the "plain data in the `byte_msg_payload` that is to be + /// presented ... on the ISELED bus" of TC18 §13.7.12.3 (TC18.txt line + /// 5578): the bytes are emitted verbatim, in supplied order, with + /// nothing inserted — in particular no CRC, which TC18.txt line 5595 + /// confirms is not present on the ISELED network unless the endpoint is + /// configured to generate one. See this module's doc comment "TC18 + /// reconciliation note (§13.7.12)" for the field-layout gaps this does + /// **not** close. + //fusa:req REQ-ISELED-003 + //fusa:req REQ-ISELED-011 pub fn encode(&self) -> Vec { let mut buf = Vec::with_capacity(2 + self.data.len()); buf.push(self.chain_address); @@ -300,7 +356,7 @@ impl IseledFrame { /// Returns `Err(RcpError::ShortFrame)` for input shorter than 2 bytes /// (no room for both `chain_address` and `command`). Never panics for /// any input. - // fusa:req REQ-ISELED-004 + //fusa:req REQ-ISELED-004 pub fn decode(b: &[u8]) -> Result { if b.len() < 2 { return Err(RcpError::ShortFrame); @@ -315,7 +371,7 @@ impl IseledFrame { /// Encode this frame to its native 4b/5b line-coded form: composes /// [`IseledFrame::encode`] and [`encode_4b5b`] rather than re-deriving /// either. - // fusa:req REQ-ISELED-005 + //fusa:req REQ-ISELED-005 pub fn encode_line(&self) -> Vec { encode_4b5b(&self.encode()) } @@ -324,7 +380,7 @@ impl IseledFrame { /// composes [`decode_4b5b`] and [`IseledFrame::decode`] rather than /// re-deriving either. Propagates either function's own error variants /// unchanged, and never panics for any input. - // fusa:req REQ-ISELED-005 + //fusa:req REQ-ISELED-005 pub fn decode_line(symbols: &[u8]) -> Result { let raw = decode_4b5b(symbols)?; Self::decode(&raw) @@ -343,7 +399,7 @@ impl IseledFrame { /// default build rather than shipped as an ordinary, always-available item. #[cfg(feature = "iseled-unconfirmed-crc")] #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-ISELED-006 +//fusa:req REQ-ISELED-006 pub struct IseledFrameCrc(pub u8); /// Computes `iseled_frame_crc8`'s underlying CRC-8/AUTOSAR value over @@ -378,7 +434,7 @@ fn crc8_autosar(data: &[u8]) -> u8 { /// this module's doc comment "Provenance note: the native ISELED CRC is a /// distinct, additive layer" for why. #[cfg(feature = "iseled-unconfirmed-crc")] -// fusa:req REQ-ISELED-006 +//fusa:req REQ-ISELED-006 pub fn iseled_frame_crc8(frame: &IseledFrame) -> IseledFrameCrc { IseledFrameCrc(crc8_autosar(&frame.encode())) } @@ -387,7 +443,7 @@ pub fn iseled_frame_crc8(frame: &IseledFrame) -> IseledFrameCrc { /// One daisy-chain device's own contribution to a multi-device response. #[derive(Debug, Clone, PartialEq, Eq, Default)] -// fusa:req REQ-ISELED-007 +//fusa:req REQ-ISELED-007 pub struct IseledDeviceResponse { /// The responding device's chain-address selector byte, matching /// [`IseledFrame::chain_address`]'s own field. @@ -406,7 +462,7 @@ pub struct IseledDeviceResponse { /// [`crate::can::CanXlCombinedPayload`]'s single-payload fragment /// concatenation. #[derive(Debug, Clone, PartialEq, Eq, Default)] -// fusa:req REQ-ISELED-008 +//fusa:req REQ-ISELED-008 pub struct IseledCollectedResponse(pub Vec); /// Collects a daisy chain's per-device responses into one @@ -420,7 +476,7 @@ pub struct IseledCollectedResponse(pub Vec); /// device order from any protocol-level position field this module does /// not model. An empty `per_device` slice yields an empty collected /// response; this function never panics for any input. -// fusa:req REQ-ISELED-008 +//fusa:req REQ-ISELED-008 pub fn iseled_collect_resp(per_device: &[IseledDeviceResponse]) -> IseledCollectedResponse { IseledCollectedResponse(per_device.to_vec()) } @@ -438,7 +494,7 @@ pub fn iseled_collect_resp(per_device: &[IseledDeviceResponse]) -> IseledCollect /// comment "Provenance note: the native ISELED CRC is a distinct, additive /// layer". #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-ISELED-009 +//fusa:req REQ-ISELED-009 pub struct IseledFunctionalConfig { /// Whether this ISELED endpoint is configured to append a native /// per-frame CRC (`iseled_frame_crc8`, when the `iseled-unconfirmed-crc` @@ -458,7 +514,7 @@ impl IseledFunctionalConfig { /// This module does not itself call that function — it only shows how a /// caller would obtain the matching tag, per this module's doc comment /// "Relationship to `crate::regmap`". - // fusa:req REQ-ISELED-010 + //fusa:req REQ-ISELED-010 pub fn layer_tag(&self) -> crate::regmap::PerEpTypeFunctionalConfig { crate::regmap::PerEpTypeFunctionalConfig::new(crate::regmap::EndpointType::Iseled) } @@ -471,7 +527,7 @@ mod tests { // ── 4b/5b line coding ──────────────────────────────────────────────────── #[test] - // fusa:test REQ-ISELED-001 + //fusa:test REQ-ISELED-001 fn nibble_to_5b_table_has_no_duplicate_code_groups() { let mut seen = std::collections::HashSet::new(); for &code in NIBBLE_TO_5B.iter() { @@ -481,8 +537,8 @@ mod tests { } #[test] - // fusa:test REQ-ISELED-001 - // fusa:test REQ-ISELED-002 + //fusa:test REQ-ISELED-001 + //fusa:test REQ-ISELED-002 fn encode_4b5b_round_trips_through_decode_4b5b() { for data in [ vec![], @@ -498,7 +554,7 @@ mod tests { } #[test] - // fusa:test REQ-ISELED-002 + //fusa:test REQ-ISELED-002 fn decode_4b5b_rejects_odd_length_input() { for len in [1usize, 3, 5] { let buf = vec![NIBBLE_TO_5B[0]; len]; @@ -507,7 +563,7 @@ mod tests { } #[test] - // fusa:test REQ-ISELED-002 + //fusa:test REQ-ISELED-002 fn decode_4b5b_rejects_invalid_code_groups() { // 0b00000 and 0b11111 are not among NIBBLE_TO_5B's 16 data code // groups (they're FDDI/100BASE-TX's own reserved/control symbols @@ -521,7 +577,7 @@ mod tests { } #[test] - // fusa:test REQ-ISELED-002 + //fusa:test REQ-ISELED-002 fn decode_4b5b_never_panics_for_any_sampled_input() { for len in [0usize, 1, 2, 3, 7, 64] { let buf = vec![0x5Au8; len]; @@ -532,8 +588,8 @@ mod tests { // ── IseledFrame: round-trip / never-panic ──────────────────────────────── #[test] - // fusa:test REQ-ISELED-003 - // fusa:test REQ-ISELED-004 + //fusa:test REQ-ISELED-003 + //fusa:test REQ-ISELED-004 fn iseled_frame_round_trips_through_encode_decode() { for (chain_address, command, data) in [ (0x00u8, 0x00u8, vec![]), @@ -553,7 +609,32 @@ mod tests { } #[test] - // fusa:test REQ-ISELED-004 + //fusa:test REQ-ISELED-011 + fn iseled_byte_msg_payload_is_carried_as_plain_data_with_no_crc_inserted() { + // TC18 §13.7.12.3 (TC18.txt line 5578): "The ISELED request and + // response contains plain data in the byte_msg_payload that is to be + // presented or has been received on the ISELED bus." Figure 40's own + // on-wire example (line 5594) shows three data bytes; line 5595 adds + // that a safe-operation-mode CRC "is not present on the ISELED + // network", so encode() must never synthesise one. + const PAYLOAD: [u8; 5] = [0x01, 0x02, 0xAA, 0xBB, 0xCC]; + + let frame = IseledFrame { + chain_address: PAYLOAD[0], + command: PAYLOAD[1], + data: PAYLOAD[2..].to_vec(), + }; + assert_eq!(frame.encode(), PAYLOAD.to_vec()); + // Exactly the supplied bytes: no CRC byte/nibble appended, no length + // or framing prefix prepended. + assert_eq!(frame.encode().len(), 2 + frame.data.len()); + + let decoded = IseledFrame::decode(&PAYLOAD).unwrap(); + assert_eq!(decoded, frame); + } + + #[test] + //fusa:test REQ-ISELED-004 fn iseled_frame_decode_rejects_short_input() { for len in [0usize, 1] { assert_eq!( @@ -564,7 +645,7 @@ mod tests { } #[test] - // fusa:test REQ-ISELED-004 + //fusa:test REQ-ISELED-004 fn iseled_frame_decode_never_panics_for_any_sampled_input() { for len in [0usize, 1, 2, 3, 9, 64] { let buf = vec![0x5Au8; len]; @@ -573,7 +654,7 @@ mod tests { } #[test] - // fusa:test REQ-ISELED-005 + //fusa:test REQ-ISELED-005 fn iseled_frame_round_trips_through_encode_line_decode_line() { let frame = IseledFrame { chain_address: 0x03, @@ -585,7 +666,7 @@ mod tests { } #[test] - // fusa:test REQ-ISELED-005 + //fusa:test REQ-ISELED-005 fn iseled_frame_decode_line_propagates_4b5b_errors() { assert_eq!(IseledFrame::decode_line(&[0x5A]), Err(RcpError::ShortFrame)); assert_eq!( @@ -605,7 +686,7 @@ mod tests { use super::*; #[test] - // fusa:test REQ-ISELED-006 + //fusa:test REQ-ISELED-006 fn iseled_frame_crc8_is_deterministic_and_sensitive_to_frame_content() { let frame_a = IseledFrame { chain_address: 0x01, @@ -622,7 +703,7 @@ mod tests { } #[test] - // fusa:test REQ-ISELED-006 + //fusa:test REQ-ISELED-006 fn iseled_frame_crc8_is_independent_of_e2e_crc32_tc18() { // Both CRC layers can be computed over related content without // either function calling the other or the two outputs colliding @@ -646,8 +727,8 @@ mod tests { // ── Multi-device response aggregation ──────────────────────────────────── #[test] - // fusa:test REQ-ISELED-007 - // fusa:test REQ-ISELED-008 + //fusa:test REQ-ISELED-007 + //fusa:test REQ-ISELED-008 fn iseled_collect_resp_preserves_per_device_structure_and_order() { let per_device = vec![ IseledDeviceResponse { @@ -668,7 +749,7 @@ mod tests { } #[test] - // fusa:test REQ-ISELED-008 + //fusa:test REQ-ISELED-008 fn iseled_collect_resp_empty_input_yields_empty_collected_response() { assert_eq!(iseled_collect_resp(&[]), IseledCollectedResponse(vec![])); } @@ -676,8 +757,8 @@ mod tests { // ── IseledFunctionalConfig / layer_tag ─────────────────────────────────── #[test] - // fusa:test REQ-ISELED-009 - // fusa:test REQ-ISELED-010 + //fusa:test REQ-ISELED-009 + //fusa:test REQ-ISELED-010 fn iseled_functional_config_layer_tag_matches_ep_type_iseled() { let functional = IseledFunctionalConfig { native_crc_enabled: true, @@ -695,7 +776,7 @@ mod tests { } #[test] - // fusa:test REQ-ISELED-010 + //fusa:test REQ-ISELED-010 fn iseled_functional_config_layer_tag_rejects_mismatched_ep_type() { let functional = IseledFunctionalConfig::default(); let generic = crate::regmap::PerEpConfigBlock::new(crate::regmap::EndpointType::Can); diff --git a/src/iso21434.rs b/src/iso21434.rs index 6867573..c10bbb6 100644 --- a/src/iso21434.rs +++ b/src/iso21434.rs @@ -1,9 +1,9 @@ -// fusa:req REQ-CYB-001 -// fusa:req REQ-CYB-002 -// fusa:req REQ-CYB-003 -// fusa:req REQ-CYB-004 -// fusa:req REQ-CYB-005 -// fusa:req REQ-CYB-006 +//fusa:req REQ-CYB-001 +//fusa:req REQ-CYB-002 +//fusa:req REQ-CYB-003 +//fusa:req REQ-CYB-004 +//fusa:req REQ-CYB-005 +//fusa:req REQ-CYB-006 //! ISO 21434 cybersecurity artifacts: TARA (Threat Analysis and Risk Assessment) //! data types and validation helpers. @@ -15,7 +15,7 @@ use std::fmt; // ── Risk classification ─────────────────────────────────────────────────────── /// SFOP attack feasibility rating dimensions. -// fusa:req REQ-CYB-001 +//fusa:req REQ-CYB-001 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum Feasibility { Low, @@ -36,7 +36,7 @@ impl fmt::Display for Feasibility { } /// Impact levels per ISO 21434 §15. -// fusa:req REQ-CYB-002 +//fusa:req REQ-CYB-002 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum Impact { Negligible, @@ -59,7 +59,7 @@ impl fmt::Display for Impact { // ── Risk level ──────────────────────────────────────────────────────────────── /// Combined risk level = Feasibility × Impact. -// fusa:req REQ-CYB-003 +//fusa:req REQ-CYB-003 #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum RiskLevel { Low, @@ -69,7 +69,7 @@ pub enum RiskLevel { } /// Compute risk level from feasibility and impact per ISO 21434 Table 14. -// fusa:req REQ-CYB-004 +//fusa:req REQ-CYB-004 pub fn risk_level(f: Feasibility, i: Impact) -> RiskLevel { match (f, i) { (_, Impact::Negligible) => RiskLevel::Low, @@ -85,7 +85,7 @@ pub fn risk_level(f: Feasibility, i: Impact) -> RiskLevel { // ── Threat entry ────────────────────────────────────────────────────────────── /// A single threat in the TARA. -// fusa:req REQ-CYB-005 +//fusa:req REQ-CYB-005 #[derive(Debug, Clone)] pub struct Threat { pub id: String, @@ -101,7 +101,7 @@ impl Threat { } /// Filter threats that meet or exceed the minimum risk level. -// fusa:req REQ-CYB-006 +//fusa:req REQ-CYB-006 pub fn filter_by_risk(threats: &[Threat], min: RiskLevel) -> Vec<&Threat> { let rank = |r: RiskLevel| r as u8; threats @@ -118,8 +118,8 @@ mod tests { use super::*; #[test] - // fusa:test REQ-CYB-003 - // fusa:test REQ-CYB-004 + //fusa:test REQ-CYB-003 + //fusa:test REQ-CYB-004 fn negligible_impact_always_low_risk() { for f in [ Feasibility::Low, @@ -132,7 +132,7 @@ mod tests { } #[test] - // fusa:test REQ-CYB-004 + //fusa:test REQ-CYB-004 fn critical_feasibility_severe_impact_is_critical() { assert_eq!( risk_level(Feasibility::Critical, Impact::Severe), @@ -141,7 +141,7 @@ mod tests { } #[test] - // fusa:test REQ-CYB-004 + //fusa:test REQ-CYB-004 fn low_feasibility_is_always_low_risk() { for i in [ Impact::Negligible, @@ -154,7 +154,7 @@ mod tests { } #[test] - // fusa:test REQ-CYB-005 + //fusa:test REQ-CYB-005 fn threat_risk_level() { let t = Threat { id: "T-001".into(), @@ -166,7 +166,7 @@ mod tests { } #[test] - // fusa:test REQ-CYB-006 + //fusa:test REQ-CYB-006 fn filter_by_risk_high() { let threats = vec![ Threat { @@ -188,8 +188,8 @@ mod tests { } #[test] - // fusa:test REQ-CYB-001 - // fusa:test REQ-CYB-002 + //fusa:test REQ-CYB-001 + //fusa:test REQ-CYB-002 fn display_variants() { assert_eq!(format!("{}", Feasibility::Critical), "critical"); assert_eq!(format!("{}", Impact::Severe), "severe"); diff --git a/src/l2.rs b/src/l2.rs index 204b750..1009d53 100644 --- a/src/l2.rs +++ b/src/l2.rs @@ -1,11 +1,11 @@ -// fusa:req REQ-L2-001 -// fusa:req REQ-L2-002 -// fusa:req REQ-L2-003 -// fusa:req REQ-L2-004 -// fusa:req REQ-L2-005 -// fusa:req REQ-L2-006 -// fusa:req REQ-L2-007 -// fusa:req REQ-L2-008 +//fusa:req REQ-L2-001 +//fusa:req REQ-L2-002 +//fusa:req REQ-L2-003 +//fusa:req REQ-L2-004 +//fusa:req REQ-L2-005 +//fusa:req REQ-L2-006 +//fusa:req REQ-L2-007 +//fusa:req REQ-L2-008 //! Layer-2 (raw Ethernet) transport for the TC18 AVTPDU/ACF wire format. //! @@ -101,7 +101,7 @@ use crate::RcpError; /// AVTPDU is marked by an EtherType value of 0x22F0." Sent in place of, /// never alongside, [`crate::udp::encode_annex_j_udp_payload`]'s 4-byte /// encapsulation sequence number — see this module's own doc comment. -// fusa:req REQ-L2-001 +//fusa:req REQ-L2-001 pub const ETHERTYPE_AVTP: u16 = 0x22F0; /// Ethernet II header length: 6-byte destination MAC + 6-byte source MAC + @@ -111,8 +111,8 @@ const ETHERNET_HEADER_LEN: usize = 14; /// Encode a raw Ethernet II frame carrying `avtpdu`: `dest_mac` + /// `src_mac` + [`ETHERTYPE_AVTP`] (big-endian) + `avtpdu` directly, with /// no encapsulation sequence number — see this module's own doc comment. -// fusa:req REQ-L2-001 -// fusa:req REQ-L2-002 +//fusa:req REQ-L2-001 +//fusa:req REQ-L2-002 pub fn encode_ethernet_frame(dest_mac: [u8; 6], src_mac: [u8; 6], avtpdu: &[u8]) -> Vec { let mut frame = Vec::with_capacity(ETHERNET_HEADER_LEN + avtpdu.len()); frame.extend_from_slice(&dest_mac); @@ -133,8 +133,8 @@ pub type DecodedEthernetFrame<'a> = ([u8; 6], [u8; 6], &'a [u8]); /// Other(_))` if the EtherType field is not [`ETHERTYPE_AVTP`] — such a /// frame is real Ethernet traffic this transport is simply not addressed /// to decode (e.g. ARP, IPv4/IPv6), not a malformed AVTPDU. -// fusa:req REQ-L2-001 -// fusa:req REQ-L2-002 +//fusa:req REQ-L2-001 +//fusa:req REQ-L2-002 pub fn decode_ethernet_frame(frame: &[u8]) -> Result, RcpError> { if frame.len() < ETHERNET_HEADER_LEN { return Err(RcpError::ShortFrame); @@ -170,7 +170,7 @@ pub fn decode_ethernet_frame(frame: &[u8]) -> Result, R /// ambiguity entirely by special-casing `Duration::ZERO` themselves before /// ever reaching this method, the same discipline /// [`crate::udp::UdpTransport::send_acf_abb`] uses. -// fusa:req REQ-L2-003 +//fusa:req REQ-L2-003 pub trait L2Socket: Send + Sync { /// Send `frame` (a full Ethernet frame — see [`encode_ethernet_frame`]) /// out this socket's bound interface. @@ -193,7 +193,7 @@ pub trait L2Socket: Send + Sync { /// address, used to build every outgoing frame's Ethernet header — see /// [`RawEthernetSocket::bind`] for how a real caller obtains its /// interface's own MAC without supplying one itself). -// fusa:req REQ-L2-004 +//fusa:req REQ-L2-004 pub struct L2Transport { local_stream: StreamId, socket: Arc, @@ -244,8 +244,8 @@ impl L2Transport { /// Returns `Err(RcpError::Timeout)` immediately for a zero `timeout`, /// matching [`crate::udp::UdpTransport::send_acf_abb`]'s own /// discipline. - // fusa:req REQ-L2-005 - // fusa:req REQ-L2-006 + //fusa:req REQ-L2-005 + //fusa:req REQ-L2-006 pub fn send_acf_abb( &self, msg: &AcfAbbMessage, @@ -269,8 +269,8 @@ impl L2Transport { /// Same as [`Self::send_acf_abb`], for an ACF_GBB request/response /// pair. - // fusa:req REQ-L2-005 - // fusa:req REQ-L2-006 + //fusa:req REQ-L2-005 + //fusa:req REQ-L2-006 pub fn send_acf_gbb( &self, msg: &AcfGbbMessage, @@ -321,8 +321,8 @@ mod raw_socket { /// doc comment ("Why `nix`, not raw `libc` `unsafe` syscalls" and /// "Runtime requirement") for the design rationale and privilege /// requirement. - // fusa:req REQ-L2-007 - // fusa:req REQ-L2-008 + //fusa:req REQ-L2-007 + //fusa:req REQ-L2-008 #[derive(Debug)] pub struct RawEthernetSocket { fd: OwnedFd, @@ -341,7 +341,7 @@ mod raw_socket { /// the interface itself via `getifaddrs`, never supplied by the /// caller — mirroring how [`crate::udp::StdUdpSocket::bind`] never /// asks a caller for its own local IP address. - // fusa:req REQ-L2-007 + //fusa:req REQ-L2-007 pub fn bind(interface_name: &str) -> Result { let addrs = getifaddrs().map_err(|e| RcpError::Other(format!("l2: getifaddrs: {e}")))?; @@ -419,7 +419,7 @@ mod raw_socket { /// that argument's interface index matters for a raw send, so /// this reuses [`Self::bind_addr`], which already carries the /// correct one. - // fusa:req REQ-L2-007 + //fusa:req REQ-L2-007 fn send(&self, frame: &[u8]) -> Result { sendto( self.fd.as_raw_fd(), @@ -430,7 +430,7 @@ mod raw_socket { .map_err(|e| RcpError::Other(format!("l2: sendto: {e}"))) } - // fusa:req REQ-L2-007 + //fusa:req REQ-L2-007 fn recv(&self, timeout: Option) -> Result, RcpError> { self.set_recv_timeout(timeout)?; let mut buf = [0u8; 65535]; @@ -457,7 +457,7 @@ mod raw_socket { /// platforms". [`Self::bind`] always fails explicitly; this type /// exists at all only so `crate::l2::RawEthernetSocket` resolves on /// every target. - // fusa:req REQ-L2-008 + //fusa:req REQ-L2-008 #[derive(Debug)] pub struct RawEthernetSocket { _unconstructible: (), @@ -467,7 +467,7 @@ mod raw_socket { /// Always returns `Err(RcpError::Other(_))` — `AF_PACKET` raw /// sockets are a Linux-specific facility this crate has no /// implementation of on this target. - // fusa:req REQ-L2-008 + //fusa:req REQ-L2-008 pub fn bind(_interface_name: &str) -> Result { Err(RcpError::Other( "l2::RawEthernetSocket is only implemented on target_os = \"linux\" \ @@ -513,8 +513,8 @@ mod tests { // ── Ethernet II framing (pure byte manipulation, no socket) ─────────── #[test] - // fusa:test REQ-L2-001 - // fusa:test REQ-L2-002 + //fusa:test REQ-L2-001 + //fusa:test REQ-L2-002 fn ethernet_frame_encode_decode_round_trips() { let dest = [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]; let src = [0x02, 0x11, 0x22, 0x33, 0x44, 0x55]; @@ -534,7 +534,7 @@ mod tests { } #[test] - // fusa:test REQ-L2-002 + //fusa:test REQ-L2-002 fn ethernet_frame_encode_handles_empty_avtpdu() { let frame = encode_ethernet_frame([0; 6], [0; 6], &[]); assert_eq!(frame.len(), 14); @@ -543,7 +543,7 @@ mod tests { } #[test] - // fusa:test REQ-L2-002 + //fusa:test REQ-L2-002 fn ethernet_frame_decode_rejects_short_frames() { for len in 0..14 { let buf = vec![0u8; len]; @@ -553,7 +553,7 @@ mod tests { } #[test] - // fusa:test REQ-L2-002 + //fusa:test REQ-L2-002 fn ethernet_frame_decode_rejects_wrong_ethertype() { let mut frame = encode_ethernet_frame([0; 6], [0; 6], &[0xAA]); // Corrupt the EtherType field to something real but not AVTP @@ -646,9 +646,9 @@ mod tests { const SRC_MAC: [u8; 6] = [0x02, 0x11, 0x22, 0x33, 0x44, 0x55]; #[test] - // fusa:test REQ-L2-004 - // fusa:test REQ-L2-005 - // fusa:test REQ-L2-006 + //fusa:test REQ-L2-004 + //fusa:test REQ-L2-005 + //fusa:test REQ-L2-006 fn l2_send_acf_abb_round_trips_over_socket() { let socket = Arc::new(EchoL2 { mismatch: false }); let transport = L2Transport::new(local_stream(), socket, DEST_MAC, SRC_MAC); @@ -658,7 +658,7 @@ mod tests { } #[test] - // fusa:test REQ-L2-006 + //fusa:test REQ-L2-006 fn l2_send_acf_abb_rejects_echo_back_mismatch() { let socket = Arc::new(EchoL2 { mismatch: true }); let transport = L2Transport::new(local_stream(), socket, DEST_MAC, SRC_MAC); @@ -667,7 +667,7 @@ mod tests { } #[test] - // fusa:test REQ-L2-005 + //fusa:test REQ-L2-005 fn l2_send_acf_abb_rejects_zero_timeout() { let socket = Arc::new(EchoL2 { mismatch: false }); let transport = L2Transport::new(local_stream(), socket, DEST_MAC, SRC_MAC); @@ -678,7 +678,7 @@ mod tests { } #[test] - // fusa:test REQ-L2-004 + //fusa:test REQ-L2-004 fn l2_transport_getters_match_constructor() { let socket = Arc::new(EchoL2 { mismatch: false }); let sid = local_stream(); @@ -690,8 +690,8 @@ mod tests { } #[test] - // fusa:test REQ-L2-004 - // fusa:test REQ-L2-005 + //fusa:test REQ-L2-004 + //fusa:test REQ-L2-005 fn l2_send_acf_gbb_round_trips_over_socket() { struct EchoGbb; impl L2Socket for EchoGbb { @@ -730,8 +730,8 @@ mod tests { } #[test] - // fusa:test REQ-L2-003 - // fusa:test REQ-L2-004 + //fusa:test REQ-L2-003 + //fusa:test REQ-L2-004 fn l2_transport_send_records_the_real_ethernet_frame() { let socket = QueuedL2::with_inbound(Vec::new()); let transport = L2Transport::new(local_stream(), socket.clone(), DEST_MAC, SRC_MAC); @@ -750,7 +750,7 @@ mod tests { #[cfg(not(target_os = "linux"))] #[test] - // fusa:test REQ-L2-008 + //fusa:test REQ-L2-008 fn raw_ethernet_socket_bind_fails_explicitly_off_linux() { let err = RawEthernetSocket::bind("eth0").unwrap_err(); assert!(matches!(err, RcpError::Other(_))); @@ -764,7 +764,7 @@ mod tests { #[cfg(target_os = "linux")] #[test] #[ignore = "requires root/CAP_NET_RAW and a pre-existing veth0/veth1 pair; see ci.yml's l2-veth job"] - // fusa:test REQ-L2-007 + //fusa:test REQ-L2-007 fn real_raw_ethernet_socket_round_trips_a_frame_over_a_veth_pair() { let tx = RawEthernetSocket::bind("veth0").expect("bind veth0 (needs sudo/CAP_NET_RAW)"); let rx = RawEthernetSocket::bind("veth1").expect("bind veth1 (needs sudo/CAP_NET_RAW)"); diff --git a/src/lib.rs b/src/lib.rs index 38dd80b..acd5825 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,35 +1,35 @@ -// fusa:req REQ-ERR-001 -// fusa:req REQ-ERR-002 -// fusa:req REQ-ERR-003 -// fusa:req REQ-ERR-004 -// fusa:req REQ-ERR-005 -// fusa:req REQ-ERR-006 -// fusa:req REQ-ERR-007 -// fusa:req REQ-ERR-008 -// fusa:req REQ-ERR-009 -// fusa:req REQ-ERR-010 -// fusa:req REQ-ERR-012 -// fusa:req REQ-ERR-013 -// fusa:req REQ-ERR-014 -// fusa:req REQ-ERR-015 -// fusa:req REQ-ERR-016 -// fusa:req REQ-ERR-017 -// fusa:req REQ-ERR-019 -// fusa:req REQ-ERR-020 -// fusa:req REQ-ERRM-001 -// fusa:req REQ-ERRM-002 -// fusa:req REQ-ERRM-003 -// fusa:req REQ-ERRM-004 -// fusa:req REQ-ERRM-005 -// fusa:req REQ-ERRM-006 -// fusa:req REQ-ERRM-007 -// fusa:req REQ-ERRM-008 -// fusa:req REQ-ERRM-009 -// fusa:req REQ-ERRM-010 -// fusa:req REQ-ERRM-011 -// fusa:req REQ-ERRM-012 -// fusa:req REQ-ERRM-013 -// fusa:req REQ-SPEC-001 +//fusa:req REQ-ERR-001 +//fusa:req REQ-ERR-002 +//fusa:req REQ-ERR-003 +//fusa:req REQ-ERR-004 +//fusa:req REQ-ERR-005 +//fusa:req REQ-ERR-006 +//fusa:req REQ-ERR-007 +//fusa:req REQ-ERR-008 +//fusa:req REQ-ERR-009 +//fusa:req REQ-ERR-010 +//fusa:req REQ-ERR-012 +//fusa:req REQ-ERR-013 +//fusa:req REQ-ERR-014 +//fusa:req REQ-ERR-015 +//fusa:req REQ-ERR-016 +//fusa:req REQ-ERR-017 +//fusa:req REQ-ERR-019 +//fusa:req REQ-ERR-020 +//fusa:req REQ-ERRM-001 +//fusa:req REQ-ERRM-002 +//fusa:req REQ-ERRM-003 +//fusa:req REQ-ERRM-004 +//fusa:req REQ-ERRM-005 +//fusa:req REQ-ERRM-006 +//fusa:req REQ-ERRM-007 +//fusa:req REQ-ERRM-008 +//fusa:req REQ-ERRM-009 +//fusa:req REQ-ERRM-010 +//fusa:req REQ-ERRM-011 +//fusa:req REQ-ERRM-012 +//fusa:req REQ-ERRM-013 +//fusa:req REQ-SPEC-001 //! Remote Control Protocol (RCP) — a Rust implementation of the OPEN //! Alliance TC18 Remote Control Protocol Specification v0.5.1_RC for @@ -130,12 +130,12 @@ use std::fmt; // ── Spec version ──────────────────────────────────────────────────────────── /// RELAY specification version this crate implements. -// fusa:req REQ-SPEC-001 +//fusa:req REQ-SPEC-001 pub const SPEC_VERSION: &str = "2.0"; /// Alias for [`SPEC_VERSION`], exported from the crate root per RELAY spec /// §19.4 ("`RELAY_SPEC_VERSION` MUST be exported from the crate root"). -// fusa:req REQ-SPEC-001 +//fusa:req REQ-SPEC-001 pub const RELAY_SPEC_VERSION: &str = SPEC_VERSION; // ── Error types ─────────────────────────────────────────────────────────────── @@ -409,7 +409,7 @@ pub enum RcpError { // continuing check_rx_enforce_e2e's earlier reuse of CrcMismatch, and // for why this stays separate from the eleven-member "TC18 RCP spec // error codes" group. - // fusa:req REQ-CRC-011 + //fusa:req REQ-CRC-011 #[error("rcp/error: CRC_ERROR — end-to-end CRC-32 safe-point verification failed")] CrcError, @@ -451,38 +451,38 @@ impl RcpError { // ── RELAY sentinel membership queries ───────────────────────────────── /// True for the `Closed` sentinel (wraps `relay::ErrClosed`). - // fusa:req REQ-ERR-007 - // fusa:req REQ-ERR-014 + //fusa:req REQ-ERR-007 + //fusa:req REQ-ERR-014 pub fn is_relay_closed(&self) -> bool { matches!(self, Self::Closed) } /// True for `NotConnected` and `NotFound` (both wrap /// `relay::ErrNotConnected`). - // fusa:req REQ-ERR-008 - // fusa:req REQ-ERR-015 + //fusa:req REQ-ERR-008 + //fusa:req REQ-ERR-015 pub fn is_relay_not_connected(&self) -> bool { matches!(self, Self::NotConnected | Self::NotFound) } /// True for `Timeout` and `Busy` (both wrap `relay::ErrTimeout`). - // fusa:req REQ-ERR-010 - // fusa:req REQ-ERR-016 - // fusa:req REQ-ERR-020 + //fusa:req REQ-ERR-010 + //fusa:req REQ-ERR-016 + //fusa:req REQ-ERR-020 pub fn is_relay_timeout(&self) -> bool { matches!(self, Self::Timeout | Self::Busy) } /// True for the `PayloadTooLarge` sentinel. - // fusa:req REQ-ERR-013 - // fusa:req REQ-ERR-017 + //fusa:req REQ-ERR-013 + //fusa:req REQ-ERR-017 pub fn is_relay_payload_too_large(&self) -> bool { matches!(self, Self::PayloadTooLarge) } /// True for `AlreadyExists` (standalone per RELAY spec §5.4). - // fusa:req REQ-ERR-009 - // fusa:req REQ-ERR-019 + //fusa:req REQ-ERR-009 + //fusa:req REQ-ERR-019 pub fn is_already_exists(&self) -> bool { matches!(self, Self::AlreadyExists) } @@ -493,7 +493,7 @@ impl RcpError { /// RELAY-sentinel, general-purpose, or wire/E2E variants above. See /// this enum's own doc comment for the full list and provenance/ /// mapping note. - // fusa:req REQ-ERRM-012 + //fusa:req REQ-ERRM-012 pub fn is_tc18_error_code(&self) -> bool { matches!( self, @@ -533,6 +533,8 @@ impl RcpError { /// `REQUEST_REJECTED` = 11, `POCI_FAILURE` = 12, /// `PRESENTATION_TIME_TOO_FAR` = 13, `GPTP_FAIL` = 14, /// `INVALID_PARAMETER` = 15, `CHAIN_ABORTED` = 16, `CHAIN_ERROR` = 17. + //fusa:req REQ-ERR-021 + //fusa:req REQ-ERR-022 pub fn tc18_wire_code(&self) -> Option { match self { Self::UnsupportedCmd => Some(1), @@ -612,7 +614,7 @@ mod tests { // ── Error sentinels ─────────────────────────────────────────────────────── #[test] - // fusa:test REQ-ERR-001 + //fusa:test REQ-ERR-001 fn err_closed_is_distinct() { // Non-nil equivalent: it's a valid discriminant value let e = RcpError::Closed; @@ -620,35 +622,35 @@ mod tests { } #[test] - // fusa:test REQ-ERR-002 + //fusa:test REQ-ERR-002 fn err_not_found_is_distinct() { let e = RcpError::NotFound; assert!(e.is_relay_not_connected()); } #[test] - // fusa:test REQ-ERR-003 + //fusa:test REQ-ERR-003 fn err_already_exists_is_distinct() { let e = RcpError::AlreadyExists; assert!(e.is_already_exists()); } #[test] - // fusa:test REQ-ERR-004 + //fusa:test REQ-ERR-004 fn err_timeout_is_distinct() { let e = RcpError::Timeout; assert!(e.is_relay_timeout()); } #[test] - // fusa:test REQ-ERR-005 + //fusa:test REQ-ERR-005 fn err_busy_is_distinct() { let e = RcpError::Busy; assert!(e.is_relay_timeout()); } #[test] - // fusa:test REQ-ERR-006 + //fusa:test REQ-ERR-006 fn all_sentinels_are_mutually_distinct() { let sentinels = [ RcpError::Closed, @@ -665,8 +667,8 @@ mod tests { } #[test] - // fusa:test REQ-ERR-007 - // fusa:test REQ-ERR-014 + //fusa:test REQ-ERR-007 + //fusa:test REQ-ERR-014 fn err_closed_is_relay_closed() { assert!(RcpError::Closed.is_relay_closed()); assert!(!RcpError::Timeout.is_relay_closed()); @@ -674,7 +676,7 @@ mod tests { } #[test] - // fusa:test REQ-ERR-008 + //fusa:test REQ-ERR-008 fn err_not_found_is_relay_not_connected() { assert!(RcpError::NotConnected.is_relay_not_connected()); assert!(RcpError::NotFound.is_relay_not_connected()); @@ -683,8 +685,8 @@ mod tests { } #[test] - // fusa:test REQ-ERR-009 - // fusa:test REQ-ERR-019 + //fusa:test REQ-ERR-009 + //fusa:test REQ-ERR-019 fn err_already_exists_is_standalone() { assert!(RcpError::AlreadyExists.is_already_exists()); assert!(!RcpError::AlreadyExists.is_relay_closed()); @@ -693,8 +695,8 @@ mod tests { } #[test] - // fusa:test REQ-ERR-010 - // fusa:test REQ-ERR-020 + //fusa:test REQ-ERR-010 + //fusa:test REQ-ERR-020 fn err_busy_wraps_timeout() { assert!(RcpError::Busy.is_relay_timeout()); assert!(RcpError::Timeout.is_relay_timeout()); @@ -702,22 +704,22 @@ mod tests { } #[test] - // fusa:test REQ-ERR-012 - // fusa:test REQ-ERR-015 + //fusa:test REQ-ERR-012 + //fusa:test REQ-ERR-015 fn err_not_connected_is_relay_not_connected() { assert!(RcpError::NotConnected.is_relay_not_connected()); } #[test] - // fusa:test REQ-ERR-013 - // fusa:test REQ-ERR-017 + //fusa:test REQ-ERR-013 + //fusa:test REQ-ERR-017 fn err_payload_too_large_is_relay_payload_too_large() { assert!(RcpError::PayloadTooLarge.is_relay_payload_too_large()); assert!(!RcpError::Closed.is_relay_payload_too_large()); } #[test] - // fusa:test REQ-ERR-016 + //fusa:test REQ-ERR-016 fn err_timeout_is_relay_timeout() { assert!(RcpError::Timeout.is_relay_timeout()); } @@ -725,73 +727,73 @@ mod tests { // ── TC18 RCP spec error codes (Milestone 2 "Error Model") ──────────────── #[test] - // fusa:test REQ-ERRM-001 + //fusa:test REQ-ERRM-001 fn err_unsupported_cmd_is_tc18_error_code() { assert!(RcpError::UnsupportedCmd.is_tc18_error_code()); } #[test] - // fusa:test REQ-ERRM-002 + //fusa:test REQ-ERRM-002 fn err_sequencer_not_known_is_tc18_error_code() { assert!(RcpError::SequencerNotKnown.is_tc18_error_code()); } #[test] - // fusa:test REQ-ERRM-003 + //fusa:test REQ-ERRM-003 fn err_unauthorized_access_is_tc18_error_code() { assert!(RcpError::UnauthorizedAccess.is_tc18_error_code()); } #[test] - // fusa:test REQ-ERRM-004 + //fusa:test REQ-ERRM-004 fn err_locked_mem_access_is_tc18_error_code() { assert!(RcpError::LockedMemAccess.is_tc18_error_code()); } #[test] - // fusa:test REQ-ERRM-005 + //fusa:test REQ-ERRM-005 fn err_request_canceled_is_tc18_error_code() { assert!(RcpError::RequestCanceled.is_tc18_error_code()); } #[test] - // fusa:test REQ-ERRM-006 + //fusa:test REQ-ERRM-006 fn err_request_not_found_is_tc18_error_code() { assert!(RcpError::RequestNotFound.is_tc18_error_code()); } #[test] - // fusa:test REQ-ERRM-007 + //fusa:test REQ-ERRM-007 fn err_ep_error_is_tc18_error_code() { assert!(RcpError::EpError.is_tc18_error_code()); } #[test] - // fusa:test REQ-ERRM-008 + //fusa:test REQ-ERRM-008 fn err_ep_not_found_is_tc18_error_code() { assert!(RcpError::EpNotFound.is_tc18_error_code()); } #[test] - // fusa:test REQ-ERRM-009 + //fusa:test REQ-ERRM-009 fn err_req_storage_ovfl_is_tc18_error_code() { assert!(RcpError::ReqStorageOvfl.is_tc18_error_code()); } #[test] - // fusa:test REQ-ERRM-010 + //fusa:test REQ-ERRM-010 fn err_request_rejected_is_tc18_error_code() { assert!(RcpError::RequestRejected.is_tc18_error_code()); } #[test] - // fusa:test REQ-ERRM-011 + //fusa:test REQ-ERRM-011 fn err_invalid_parameter_is_tc18_error_code() { assert!(RcpError::InvalidParameter.is_tc18_error_code()); } #[test] - // fusa:test REQ-ERRM-012 + //fusa:test REQ-ERRM-012 fn tc18_error_codes_are_mutually_distinct_and_exclusive() { let codes = [ RcpError::UnsupportedCmd, @@ -821,6 +823,7 @@ mod tests { } #[test] + //fusa:test REQ-ERR-021 fn tc18_wire_code_covers_all_seventeen_table_27_codes_with_distinct_values() { // rust-RCP-W05. let codes = [ @@ -853,6 +856,7 @@ mod tests { } #[test] + //fusa:test REQ-ERR-022 fn tc18_wire_code_is_none_for_non_table_27_variants() { // rust-RCP-W05: RELAY sentinels, general-purpose, and wire/E2E // variants have no Table 27 counterpart. @@ -874,7 +878,7 @@ mod tests { } #[test] - // fusa:test REQ-ERRM-013 + //fusa:test REQ-ERRM-013 fn tc18_error_code_messages_carry_spec_name() { assert!(RcpError::UnsupportedCmd .to_string() @@ -910,13 +914,13 @@ mod tests { // ── Spec version ────────────────────────────────────────────────────────── #[test] - // fusa:test REQ-SPEC-001 + //fusa:test REQ-SPEC-001 fn spec_version_nonempty() { assert!(!SPEC_VERSION.is_empty()); } #[test] - // fusa:test REQ-SPEC-001 + //fusa:test REQ-SPEC-001 fn relay_spec_version_is_exported_and_matches_spec_version() { assert!(!RELAY_SPEC_VERSION.is_empty()); assert_eq!(RELAY_SPEC_VERSION, SPEC_VERSION); diff --git a/src/lifecycle.rs b/src/lifecycle.rs index 277e6a3..b865722 100644 --- a/src/lifecycle.rs +++ b/src/lifecycle.rs @@ -1,15 +1,15 @@ -// fusa:req REQ-LIFE-001 -// fusa:req REQ-LIFE-002 -// fusa:req REQ-LIFE-003 -// fusa:req REQ-LIFE-004 -// fusa:req REQ-LIFE-005 -// fusa:req REQ-LIFE-006 -// fusa:req REQ-LIFE-007 -// fusa:req REQ-LIFE-008 -// fusa:req REQ-LIFE-009 -// fusa:req REQ-LIFE-010 -// fusa:req REQ-LIFE-011 -// fusa:req REQ-LIFE-012 +//fusa:req REQ-LIFE-001 +//fusa:req REQ-LIFE-002 +//fusa:req REQ-LIFE-003 +//fusa:req REQ-LIFE-004 +//fusa:req REQ-LIFE-005 +//fusa:req REQ-LIFE-006 +//fusa:req REQ-LIFE-007 +//fusa:req REQ-LIFE-008 +//fusa:req REQ-LIFE-009 +//fusa:req REQ-LIFE-010 +//fusa:req REQ-LIFE-011 +//fusa:req REQ-LIFE-012 //! RC Server lifecycle state machine — TC18 register-map model //! (`ROADMAP.md` Milestone 2, "Lifecycle State Machine" subsection, now in @@ -268,7 +268,7 @@ use crate::RcpError; /// including the `HW_CONFIGURED` -> `HW_UNCONFIGURED` demotion path. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[repr(u8)] -// fusa:req REQ-LIFE-001 +//fusa:req REQ-LIFE-001 pub enum RcServerState { /// No hardware configuration has been applied yet. Only [`RegisterCategory::General`] /// registers are reachable (see [`is_register_reachable`]). @@ -291,6 +291,17 @@ impl RcServerState { /// default (per Guiding Principle 5, flagged rather than asserted as /// spec fact) given `HW_UNCONFIGURED`'s name and `0x00` encoding both /// suggest a power-on/reset default. + /// + /// TC18 §12.3 (TC18.txt line 2059) settles the surrounding rule this + /// default serves: "After a power cycle or restart ... the RC Server + /// shall start in the life-cycle state it is actually configured in", + /// where a device with no NVM "may incorporate default settings which + /// allow it to be also starting in an advanced state". A configured + /// start state reaches this type through its + /// `Serialize`/`Deserialize` derive (see this module's doc comment's + /// `config` REPLACE section); this constant is the no-stored- + /// configuration default that rule falls back to. + //fusa:req REQ-LIFE-015 pub const INITIAL: Self = Self::HwUnconfigured; /// Encode this state as its wire-level byte value. @@ -304,7 +315,7 @@ impl RcServerState { /// defined encodings, mirroring /// [`crate::avtp::select_header_variant`]'s handling of an /// unrecognized subtype byte. Never panics for any input. - // fusa:req REQ-LIFE-002 + //fusa:req REQ-LIFE-002 pub fn from_u8(raw: u8) -> Result { match raw { 0x00 => Ok(Self::HwUnconfigured), @@ -333,7 +344,7 @@ impl Default for RcServerState { /// See this module's doc comment for how this split — and the reachability /// rule [`is_register_reachable`] derives from it — was inferred. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// fusa:req REQ-LIFE-003 +//fusa:req REQ-LIFE-003 pub enum RegisterCategory { /// Server-identity/status registers (e.g. the eventual /// `svr_oa_tc18_magic_nr`, `svr_version`, `svr_vendor_id`, @@ -354,8 +365,8 @@ pub enum RegisterCategory { /// about whether a *write* to a reachable register is further locked (see /// this module's doc comment for why that is separate, later work). /// Never panics for any input. -// fusa:req REQ-LIFE-003 -// fusa:req REQ-LIFE-004 +//fusa:req REQ-LIFE-003 +//fusa:req REQ-LIFE-004 pub fn is_register_reachable(state: RcServerState, category: RegisterCategory) -> bool { match (state, category) { // General registers are reachable in every state (see this @@ -386,7 +397,7 @@ pub fn is_register_reachable(state: RcServerState, category: RegisterCategory) - /// crate-invented `RegisterUnreachable` sentinel, since remapped; see /// [`crate::RcpError`]'s own doc comment for the full provenance/mapping /// note. -// fusa:req REQ-LIFE-004 +//fusa:req REQ-LIFE-004 pub fn check_register_reachable( state: RcServerState, category: RegisterCategory, @@ -409,7 +420,7 @@ pub fn check_register_reachable( /// policy combines with reachability to produce a final writable/not /// answer. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// fusa:req REQ-LIFE-009 +//fusa:req REQ-LIFE-009 pub enum LockPolicy { /// `W` — write access may still vary with lifecycle state (through /// [`is_register_reachable`]'s reachability gate) but this policy never @@ -441,7 +452,7 @@ pub enum LockPolicy { /// /// See this module's doc comment for the per-category reasoning behind /// each assignment. Never panics for any input. -// fusa:req REQ-LIFE-009 +//fusa:req REQ-LIFE-009 pub fn lock_policy(category: RegisterCategory) -> Option { match category { RegisterCategory::General => Some(LockPolicy::W), @@ -459,8 +470,8 @@ pub fn lock_policy(category: RegisterCategory) -> Option { /// whenever reachable; `W*` categories are writable whenever reachable /// except while `RcServerState::RcpConfigured`, where they are permanently /// locked. Never panics for any input. -// fusa:req REQ-LIFE-009 -// fusa:req REQ-LIFE-010 +//fusa:req REQ-LIFE-009 +//fusa:req REQ-LIFE-010 pub fn is_register_writable(state: RcServerState, category: RegisterCategory) -> bool { if !is_register_reachable(state, category) { return false; @@ -486,7 +497,7 @@ pub fn is_register_writable(state: RcServerState, category: RegisterCategory) -> /// this function originally called `RegisterLocked`; see /// [`crate::RcpError`]'s own doc comment for the full provenance/mapping /// note. -// fusa:req REQ-LIFE-010 +//fusa:req REQ-LIFE-010 pub fn check_register_writable( state: RcServerState, category: RegisterCategory, @@ -515,8 +526,16 @@ pub fn check_register_writable( /// the same state, any move originating at `RCP_CONFIGURED` (backward or /// otherwise), and skipping `HW_CONFIGURED` entirely on the way up. Never /// panics for any input. -// fusa:req REQ-LIFE-006 -// fusa:req REQ-LIFE-012 +/// +/// One of the excluded pairs is excluded by TC18 itself, not merely by +/// this crate's reading of `ROADMAP.md`: TC18 §12.3.1.1 (TC18.txt line +/// 2168) states that, from `HW_UNCONFIGURED`, "a request to advance the +/// state directly to RCP_CONFIGURED will be rejected with an error +/// response" — the two-hop route through `HW_CONFIGURED` is the only way +/// up. +//fusa:req REQ-LIFE-006 +//fusa:req REQ-LIFE-012 +//fusa:req REQ-LIFE-019 pub fn is_transition_defined(from: RcServerState, to: RcServerState) -> bool { matches!( (from, to), @@ -561,10 +580,11 @@ impl RcServerState { /// `is_consistent` is deliberately a caller-supplied placeholder, /// mirroring [`crate::formal::Invariant`]'s predicate shape. Never /// panics for any input, including a `target` equal to `self`. - // fusa:req REQ-LIFE-006 - // fusa:req REQ-LIFE-007 - // fusa:req REQ-LIFE-008 - // fusa:req REQ-LIFE-012 + //fusa:req REQ-LIFE-006 + //fusa:req REQ-LIFE-007 + //fusa:req REQ-LIFE-008 + //fusa:req REQ-LIFE-012 + //fusa:req REQ-LIFE-019 pub fn try_transition( self, target: Self, @@ -620,7 +640,7 @@ mod tests { // ── Numeric encoding / round-trip ──────────────────────────────────── #[test] - // fusa:test REQ-LIFE-001 + //fusa:test REQ-LIFE-001 fn state_encodings_match_roadmap_values() { assert_eq!(RcServerState::HwUnconfigured.to_u8(), 0x00); assert_eq!(RcServerState::HwConfigured.to_u8(), 0x55); @@ -628,7 +648,7 @@ mod tests { } #[test] - // fusa:test REQ-LIFE-001 + //fusa:test REQ-LIFE-001 fn from_u8_round_trips_every_valid_encoding() { for state in ALL_STATES { let raw = state.to_u8(); @@ -637,16 +657,90 @@ mod tests { } #[test] - // fusa:test REQ-LIFE-001 + //fusa:test REQ-LIFE-001 fn initial_state_is_hw_unconfigured() { assert_eq!(RcServerState::INITIAL, RcServerState::HwUnconfigured); assert_eq!(RcServerState::default(), RcServerState::HwUnconfigured); } + // ── TC18 §12.3: start state after a power cycle or restart ─────────── + + /// TC18 §12.3 (TC18.txt line 2059): "After a power cycle or restart, + /// depending on the RC Server's implementation, the RC Server shall + /// start in the life-cycle state it is actually configured in. + /// Depending on the devices physical implementation it may have either + /// NVM memory in which configurations and the life-cycle state were + /// stored and recovered after reset or power-on or a device has no NVM, + /// but may incorporate default settings which allow it to be also + /// starting in an advanced state." + /// + /// Both halves are asserted against literal values: the no-stored- + /// configuration default is `HW_UNCONFIGURED`/`0x00`, and each of the + /// three state names TC18 §12.3 lists (TC18.txt lines 2065-2067) can be + /// restored by name as a configured start state. + #[test] + //fusa:test REQ-LIFE-015 + fn tc18_12_3_start_state_is_the_configured_one_or_the_no_nvm_default() { + // No stored configuration: the built-in default settings. + assert_eq!(RcServerState::INITIAL, RcServerState::HwUnconfigured); + assert_eq!(RcServerState::default(), RcServerState::HwUnconfigured); + assert_eq!(RcServerState::default().to_u8(), 0x00); + + // A stored/configured start state, restored by name — including the + // "advanced state" case TC18 explicitly permits a device to start + // in. + for (stored, expected) in [ + ("\"HwUnconfigured\"", RcServerState::HwUnconfigured), + ("\"HwConfigured\"", RcServerState::HwConfigured), + ("\"RcpConfigured\"", RcServerState::RcpConfigured), + ] { + let restored: RcServerState = + serde_json::from_str(stored).expect("a configured start state must be restorable"); + assert_eq!(restored, expected, "{stored}"); + } + } + + // ── TC18 §12.3.1.1: no direct advance to RCP_CONFIGURED ────────────── + + /// TC18 §12.3.1.1 (TC18.txt line 2168): from `HW_UNCONFIGURED`, "per + /// write request to the server functional configuration entry + /// svr_lifecycle_state the life-cycle state can be advanced to + /// HW_CONFIGURED. A request to advance the state directly to + /// RCP_CONFIGURED will be rejected with an error response." + #[test] + //fusa:test REQ-LIFE-019 + fn tc18_12_3_1_1_direct_advance_to_rcp_configured_is_rejected() { + assert!(!is_transition_defined( + RcServerState::HwUnconfigured, + RcServerState::RcpConfigured + )); + + // Rejected outright, without even consulting the plausibility + // guard: the transition shape itself is not admissible. + let mut guard_called = false; + let result = + RcServerState::HwUnconfigured.try_transition(RcServerState::RcpConfigured, || { + guard_called = true; + true + }); + assert_eq!(result, Err(RcpError::RequestRejected)); + assert!(!guard_called); + + // The two-hop route TC18 does define is the only way up. + let hw_configured = RcServerState::HwUnconfigured + .try_transition(RcServerState::HwConfigured, || true) + .unwrap(); + assert_eq!(hw_configured, RcServerState::HwConfigured); + assert_eq!( + hw_configured.try_transition(RcServerState::RcpConfigured, || true), + Ok(RcServerState::RcpConfigured) + ); + } + // ── Rejection of unrecognized encodings ────────────────────────────── #[test] - // fusa:test REQ-LIFE-002 + //fusa:test REQ-LIFE-002 fn from_u8_rejects_every_byte_other_than_the_three_valid_ones() { for raw in 0u8..=255 { let result = RcServerState::from_u8(raw); @@ -660,7 +754,7 @@ mod tests { // ── Per-state register reachability ────────────────────────────────── #[test] - // fusa:test REQ-LIFE-003 + //fusa:test REQ-LIFE-003 fn general_registers_are_reachable_in_every_state() { for state in ALL_STATES { assert!(is_register_reachable(state, RegisterCategory::General)); @@ -668,7 +762,7 @@ mod tests { } #[test] - // fusa:test REQ-LIFE-003 + //fusa:test REQ-LIFE-003 fn hw_config_registers_are_reachable_in_every_state() { for state in ALL_STATES { assert!(is_register_reachable(state, RegisterCategory::HwConfig)); @@ -676,7 +770,7 @@ mod tests { } #[test] - // fusa:test REQ-LIFE-003 + //fusa:test REQ-LIFE-003 fn rcp_config_registers_are_unreachable_only_while_hw_unconfigured() { assert!(!is_register_reachable( RcServerState::HwUnconfigured, @@ -693,7 +787,7 @@ mod tests { } #[test] - // fusa:test REQ-LIFE-004 + //fusa:test REQ-LIFE-004 fn check_register_reachable_agrees_with_is_register_reachable() { for state in ALL_STATES { for category in ALL_CATEGORIES { @@ -710,7 +804,7 @@ mod tests { // ── Fuzz-style: arbitrary inputs never panic ───────────────────────── #[test] - // fusa:test REQ-LIFE-005 + //fusa:test REQ-LIFE-005 fn from_u8_never_panics_across_the_full_byte_range() { for raw in 0u8..=255 { let _ = RcServerState::from_u8(raw); @@ -718,7 +812,7 @@ mod tests { } #[test] - // fusa:test REQ-LIFE-005 + //fusa:test REQ-LIFE-005 fn reachability_checks_never_panic_for_any_state_category_pair() { for state in ALL_STATES { for category in ALL_CATEGORIES { @@ -731,7 +825,7 @@ mod tests { // ── Register write-locking (W vs W*) ────────────────────────────────── #[test] - // fusa:test REQ-LIFE-009 + //fusa:test REQ-LIFE-009 fn lock_policy_matches_the_documented_per_category_assignment() { assert_eq!(lock_policy(RegisterCategory::General), Some(LockPolicy::W)); assert_eq!( @@ -745,8 +839,8 @@ mod tests { } #[test] - // fusa:test REQ-LIFE-009 - // fusa:test REQ-LIFE-010 + //fusa:test REQ-LIFE-009 + //fusa:test REQ-LIFE-010 fn general_registers_are_writable_in_every_state() { for state in ALL_STATES { assert!(is_register_writable(state, RegisterCategory::General)); @@ -754,8 +848,8 @@ mod tests { } #[test] - // fusa:test REQ-LIFE-009 - // fusa:test REQ-LIFE-010 + //fusa:test REQ-LIFE-009 + //fusa:test REQ-LIFE-010 fn hw_config_registers_are_writable_until_permanently_locked_at_rcp_configured() { assert!(is_register_writable( RcServerState::HwUnconfigured, @@ -772,8 +866,8 @@ mod tests { } #[test] - // fusa:test REQ-LIFE-009 - // fusa:test REQ-LIFE-010 + //fusa:test REQ-LIFE-009 + //fusa:test REQ-LIFE-010 fn rcp_config_registers_are_writable_whenever_reachable_including_at_rcp_configured() { // Unreachable in HW_UNCONFIGURED, so not writable either -- but for // reachability's reason, not a write-lock. @@ -794,7 +888,7 @@ mod tests { } #[test] - // fusa:test REQ-LIFE-010 + //fusa:test REQ-LIFE-010 fn is_register_writable_never_true_for_an_unreachable_category() { for state in ALL_STATES { for category in ALL_CATEGORIES { @@ -806,7 +900,7 @@ mod tests { } #[test] - // fusa:test REQ-LIFE-010 + //fusa:test REQ-LIFE-010 fn check_register_writable_agrees_with_is_register_writable_and_distinguishes_the_reason() { for state in ALL_STATES { for category in ALL_CATEGORIES { @@ -826,7 +920,7 @@ mod tests { } #[test] - // fusa:test REQ-LIFE-011 + //fusa:test REQ-LIFE-011 fn write_lock_checks_never_panic_for_any_state_category_pair() { for state in ALL_STATES { for category in ALL_CATEGORIES { @@ -840,8 +934,8 @@ mod tests { // ── Transition guard: which shapes are defined ─────────────────────── #[test] - // fusa:test REQ-LIFE-006 - // fusa:test REQ-LIFE-012 + //fusa:test REQ-LIFE-006 + //fusa:test REQ-LIFE-012 fn is_transition_defined_true_only_for_the_three_implemented_transitions() { for from in ALL_STATES { for to in ALL_STATES { @@ -858,7 +952,7 @@ mod tests { } #[test] - // fusa:test REQ-LIFE-006 + //fusa:test REQ-LIFE-006 fn try_transition_success_agrees_with_is_transition_defined_when_guard_passes() { for from in ALL_STATES { for to in ALL_STATES { @@ -872,7 +966,7 @@ mod tests { // ── Transition guard: round-trip on the two guarded transitions ────── #[test] - // fusa:test REQ-LIFE-007 + //fusa:test REQ-LIFE-007 fn hw_unconfigured_to_hw_configured_succeeds_when_guard_passes() { let result = RcServerState::HwUnconfigured.try_transition(RcServerState::HwConfigured, || true); @@ -880,7 +974,7 @@ mod tests { } #[test] - // fusa:test REQ-LIFE-007 + //fusa:test REQ-LIFE-007 fn hw_unconfigured_to_hw_configured_rejected_when_guard_fails() { let result = RcServerState::HwUnconfigured.try_transition(RcServerState::HwConfigured, || false); @@ -888,7 +982,7 @@ mod tests { } #[test] - // fusa:test REQ-LIFE-007 + //fusa:test REQ-LIFE-007 fn hw_configured_to_rcp_configured_succeeds_when_guard_passes() { let result = RcServerState::HwConfigured.try_transition(RcServerState::RcpConfigured, || true); @@ -896,7 +990,7 @@ mod tests { } #[test] - // fusa:test REQ-LIFE-007 + //fusa:test REQ-LIFE-007 fn hw_configured_to_rcp_configured_rejected_when_guard_fails() { let result = RcServerState::HwConfigured.try_transition(RcServerState::RcpConfigured, || false); @@ -906,7 +1000,7 @@ mod tests { // ── Transition guard: round-trip on the demotion path ──────────────── #[test] - // fusa:test REQ-LIFE-012 + //fusa:test REQ-LIFE-012 fn hw_configured_to_hw_unconfigured_succeeds_regardless_of_guard_result() { // Unconditional: the demotion path is not gated by any consistency // guard (see this module's doc comment Provenance note), so it @@ -922,7 +1016,7 @@ mod tests { } #[test] - // fusa:test REQ-LIFE-012 + //fusa:test REQ-LIFE-012 fn hw_configured_to_hw_unconfigured_never_consults_the_guard() { let mut guard_called = false; let result = @@ -940,7 +1034,7 @@ mod tests { // ── Transition guard: rejection of every undefined transition ──────── #[test] - // fusa:test REQ-LIFE-008 + //fusa:test REQ-LIFE-008 fn undefined_transitions_are_rejected_without_consulting_the_guard() { let undefined_pairs = [ // Full rollback in one step -- out of scope for this item, which @@ -975,8 +1069,8 @@ mod tests { // ── Fuzz-style: arbitrary (state, state, guard-result) never panics ── #[test] - // fusa:test REQ-LIFE-008 - // fusa:test REQ-LIFE-012 + //fusa:test REQ-LIFE-008 + //fusa:test REQ-LIFE-012 fn try_transition_never_panics_for_any_state_pair_or_guard_result() { for from in ALL_STATES { for to in ALL_STATES { diff --git a/src/lin.rs b/src/lin.rs index a4e2867..7ae832d 100644 --- a/src/lin.rs +++ b/src/lin.rs @@ -1,9 +1,9 @@ -// fusa:req REQ-LIN-001 -// fusa:req REQ-LIN-002 -// fusa:req REQ-LIN-003 -// fusa:req REQ-LIN-004 -// fusa:req REQ-LIN-005 -// fusa:req REQ-LIN-006 +//fusa:req REQ-LIN-001 +//fusa:req REQ-LIN-002 +//fusa:req REQ-LIN-003 +//fusa:req REQ-LIN-004 +//fusa:req REQ-LIN-005 +//fusa:req REQ-LIN-006 //! The LIN commander endpoint type (`ep_type 0x06`) — `ROADMAP.md` //! Milestone 7 ("Remaining Endpoint Types"), opening checklist bullet: raw @@ -82,6 +82,32 @@ //! stub module behind purely to hold one constant — the same resolution //! `can.rs`'s own `CAN_FD_MAX_PAYLOAD` used when `canbr.rs` was deleted. //! +//! ## TC18 reconciliation note (§13.7.10) +//! +//! TC18 §13.7.10.3 (TC18.txt line 5304) states only that "the Byte Msg +//! Payload is the payload to be used on the Lin bus", and Figure 38 shows +//! that payload as one undifferentiated "Lin payload" field followed by +//! padding — it defines no PID sub-field, no checksum sub-field, and no +//! per-frame length ceiling of its own. This module's split of the leading +//! byte into [`LinFrameTransfer::pid`] is therefore a crate-local modeling +//! convenience that changes no wire byte: `pid` followed by `data` is the +//! same byte sequence, in the same order, that TC18 calls the Lin payload. +//! The [`LIN_MAX_DATA`] ceiling is likewise this crate's own (real LIN 2.x) +//! physical fact, not a TC18 clause. +//! +//! Three normative §13.7.10.1 behaviors are **not** implemented here and are +//! recorded as explicit not-implemented requirement entries rather than +//! silently omitted: matching each received LIN message against the pending +//! read request's `byte_msg_payload` under the conditions given by +//! `evt[2:0]` and replying when `op = 0` (TC18.txt lines 5276-5277); issuing +//! a trigger once a transmission has been finalized and the configured +//! trailing time has expired (line 5278); and the cyclic-transmission +//! pattern built from a repeated trigger request on the endpoint's own +//! trigger (line 5279). All three are RC-Server run-time endpoint behaviors, +//! outside this module's codec-only scope. TC18 Table 52's own +//! functional-config register layout (§13.7.10.2, lines 5287-5298) is +//! likewise unimplemented — see [`LinFunctionalConfig`]. +//! //! ## Relationship to [`crate::regmap`] //! //! As with every Milestone 4 endpoint-type module, LIN's real @@ -138,7 +164,7 @@ use crate::RcpError; /// comment "Validation against `linbr.rs` (historical — see below for its /// outcome)"); stated directly as this module's own constant since /// Milestone 9's `linbr` REPLACE cutover deleted that module. -// fusa:req REQ-LIN-001 +//fusa:req REQ-LIN-001 pub const LIN_MAX_DATA: usize = 8; // ── LinFrameTransfer ───────────────────────────────────────────────────────── @@ -151,7 +177,7 @@ pub const LIN_MAX_DATA: usize = 8; /// and for why neither field is parsed or validated beyond the /// [`LIN_MAX_DATA`] data-length ceiling. #[derive(Debug, Clone, PartialEq, Eq, Default)] -// fusa:req REQ-LIN-002 +//fusa:req REQ-LIN-002 pub struct LinFrameTransfer { /// The client-computed PID byte, carried unparsed and unvalidated — /// this module performs no parity check or PID derivation of its own. @@ -165,7 +191,13 @@ pub struct LinFrameTransfer { impl LinFrameTransfer { /// Encode this transfer to its raw wire representation: the PID byte /// followed by `data`, unmodified and unframed. - // fusa:req REQ-LIN-002 + /// + /// This is the `byte_msg_payload` TC18 §13.7.10.3 (TC18.txt line 5304) + /// calls "the payload to be used on the Lin bus": the bytes are emitted + /// verbatim, in supplied order, with nothing inserted, removed, or + /// reordered — see this module's doc comment "TC18 reconciliation note". + //fusa:req REQ-LIN-002 + //fusa:req REQ-LIN-007 pub fn encode(&self) -> Vec { let mut buf = Vec::with_capacity(1 + self.data.len()); buf.push(self.pid); @@ -182,8 +214,8 @@ impl LinFrameTransfer { /// when the remaining data would exceed [`LIN_MAX_DATA`] bytes, the same /// error variant the legacy `linbr::LinBridge::send` already used for /// the same physical ceiling. Never panics for any input. - // fusa:req REQ-LIN-003 - // fusa:req REQ-LIN-004 + //fusa:req REQ-LIN-003 + //fusa:req REQ-LIN-004 pub fn decode(b: &[u8]) -> Result { let (pid, data) = b.split_first().ok_or(RcpError::ShortFrame)?; if data.len() > LIN_MAX_DATA { @@ -203,7 +235,7 @@ impl LinFrameTransfer { /// a LIN response frame is identified by the request's own PID rather than /// carrying a second one of its own. #[derive(Debug, Clone, PartialEq, Eq, Default)] -// fusa:req REQ-LIN-005 +//fusa:req REQ-LIN-005 pub struct LinFrameTransferResult { /// The data bytes read back off the bus, carried unparsed. Never longer /// than [`LIN_MAX_DATA`] bytes. @@ -213,7 +245,7 @@ pub struct LinFrameTransferResult { impl LinFrameTransferResult { /// Encode this transfer result to its raw wire representation: `data`, /// unmodified and unframed. - // fusa:req REQ-LIN-005 + //fusa:req REQ-LIN-005 pub fn encode(&self) -> Vec { self.data.clone() } @@ -223,7 +255,7 @@ impl LinFrameTransferResult { /// Returns `Err(RcpError::PayloadTooLarge)` for input longer than /// [`LIN_MAX_DATA`] bytes; an empty slice is a valid (zero-length) LIN /// response and decodes successfully. Never panics for any input. - // fusa:req REQ-LIN-005 + //fusa:req REQ-LIN-005 pub fn decode(b: &[u8]) -> Result { if b.len() > LIN_MAX_DATA { return Err(RcpError::PayloadTooLarge); @@ -240,7 +272,7 @@ impl LinFrameTransferResult { /// "Relationship to `crate::regmap`" for why the checklist text names no /// LIN-specific configuration content to model here. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-LIN-006 +//fusa:req REQ-LIN-006 pub struct LinFunctionalConfig; impl LinFunctionalConfig { @@ -251,7 +283,7 @@ impl LinFunctionalConfig { /// This module does not itself call that function — it only shows how a /// caller would obtain the matching tag, per this module's doc comment /// "Relationship to `crate::regmap`". - // fusa:req REQ-LIN-006 + //fusa:req REQ-LIN-006 pub fn layer_tag(&self) -> crate::regmap::PerEpTypeFunctionalConfig { crate::regmap::PerEpTypeFunctionalConfig::new(crate::regmap::EndpointType::Lin) } @@ -264,7 +296,7 @@ mod tests { // ── LIN_MAX_DATA ───────────────────────────────────────────────────────── #[test] - // fusa:test REQ-LIN-001 + //fusa:test REQ-LIN-001 fn lin_max_data_is_eight() { assert_eq!(LIN_MAX_DATA, 8); } @@ -272,7 +304,7 @@ mod tests { // ── LinFrameTransfer: round-trip / never-panic ────────────────────────── #[test] - // fusa:test REQ-LIN-002 + //fusa:test REQ-LIN-002 fn lin_frame_transfer_round_trips_through_encode_decode() { for (pid, data) in [ (0x00u8, vec![]), @@ -291,13 +323,46 @@ mod tests { } #[test] - // fusa:test REQ-LIN-003 + //fusa:test REQ-LIN-007 + fn lin_byte_msg_payload_is_carried_verbatim_onto_the_bus() { + // TC18 §13.7.10.3 (TC18.txt line 5304): "The Byte Msg Payload is the + // payload to be used on the Lin bus." Figure 38's own on-wire example + // (line 5305) carries three payload bytes with no PID/checksum + // sub-structure and no length/format byte of its own, so the encoded + // form must be byte-for-byte identical to the supplied payload. + // + // Literal payload: LIN 2.x diagnostic master-request frame identifier + // 0x3C followed by three data bytes. + const PAYLOAD: [u8; 4] = [0x3C, 0x01, 0x02, 0x03]; + + let transfer = LinFrameTransfer { + pid: PAYLOAD[0], + data: PAYLOAD[1..].to_vec(), + }; + assert_eq!(transfer.encode(), PAYLOAD.to_vec()); + + let decoded = LinFrameTransfer::decode(&PAYLOAD).unwrap(); + assert_eq!(decoded.pid, 0x3C); + assert_eq!(decoded.data, vec![0x01, 0x02, 0x03]); + + // Nothing is appended (no checksum byte is synthesised) even at the + // full LIN 2.x data ceiling: 1 payload-leading byte + 8 data bytes. + let full = LinFrameTransfer { + pid: 0x3C, + data: vec![0xA5; LIN_MAX_DATA], + }; + assert_eq!(full.encode().len(), 9); + assert_eq!(&full.encode()[1..], &[0xA5u8; LIN_MAX_DATA][..]); + } + + #[test] + //fusa:test REQ-LIN-003 fn lin_frame_transfer_decode_rejects_empty_input() { assert_eq!(LinFrameTransfer::decode(&[]), Err(RcpError::ShortFrame)); } #[test] - // fusa:test REQ-LIN-004 + //fusa:test REQ-LIN-004 fn lin_frame_transfer_decode_rejects_data_longer_than_lin_max_data() { let mut buf = vec![0x00u8]; // pid buf.extend(vec![0xAAu8; LIN_MAX_DATA + 1]); @@ -308,7 +373,7 @@ mod tests { } #[test] - // fusa:test REQ-LIN-004 + //fusa:test REQ-LIN-004 fn lin_frame_transfer_decode_accepts_data_at_exactly_lin_max_data() { let mut buf = vec![0x00u8]; // pid buf.extend(vec![0xAAu8; LIN_MAX_DATA]); @@ -317,7 +382,7 @@ mod tests { } #[test] - // fusa:test REQ-LIN-002 + //fusa:test REQ-LIN-002 fn lin_frame_transfer_decode_never_panics_for_any_sampled_input() { for len in [0usize, 1, 2, 9, 64] { let buf = vec![0x5Au8; len]; @@ -328,7 +393,7 @@ mod tests { // ── LinFrameTransferResult: round-trip / never-panic ──────────────────── #[test] - // fusa:test REQ-LIN-005 + //fusa:test REQ-LIN-005 fn lin_frame_transfer_result_round_trips_through_encode_decode() { for data in [vec![], vec![0xFF], vec![0x01, 0x02, 0x03]] { let result = LinFrameTransferResult { data: data.clone() }; @@ -342,7 +407,7 @@ mod tests { } #[test] - // fusa:test REQ-LIN-005 + //fusa:test REQ-LIN-005 fn lin_frame_transfer_result_decode_rejects_longer_than_lin_max_data() { let buf = vec![0xAAu8; LIN_MAX_DATA + 1]; assert_eq!( @@ -352,7 +417,7 @@ mod tests { } #[test] - // fusa:test REQ-LIN-005 + //fusa:test REQ-LIN-005 fn lin_frame_transfer_result_decode_accepts_exactly_lin_max_data() { let buf = vec![0xAAu8; LIN_MAX_DATA]; assert_eq!( @@ -362,7 +427,7 @@ mod tests { } #[test] - // fusa:test REQ-LIN-005 + //fusa:test REQ-LIN-005 fn lin_frame_transfer_result_decode_never_panics_for_any_sampled_input() { for len in [0usize, 1, 5, 32] { let buf = vec![0xA5u8; len]; @@ -373,7 +438,7 @@ mod tests { // ── LinFunctionalConfig / layer_tag ───────────────────────────────────── #[test] - // fusa:test REQ-LIN-006 + //fusa:test REQ-LIN-006 fn lin_functional_config_layer_tag_matches_ep_type_lin() { let functional = LinFunctionalConfig; let generic = crate::regmap::PerEpConfigBlock::new(crate::regmap::EndpointType::Lin); @@ -389,7 +454,7 @@ mod tests { } #[test] - // fusa:test REQ-LIN-006 + //fusa:test REQ-LIN-006 fn lin_functional_config_layer_tag_rejects_mismatched_ep_type() { let functional = LinFunctionalConfig; let generic = crate::regmap::PerEpConfigBlock::new(crate::regmap::EndpointType::Spi); diff --git a/src/loan.rs b/src/loan.rs index 2c1c767..e08ea45 100644 --- a/src/loan.rs +++ b/src/loan.rs @@ -1,10 +1,10 @@ -// fusa:req REQ-LOAN-001 -// fusa:req REQ-LOAN-002 -// fusa:req REQ-LOAN-003 -// fusa:req REQ-LOAN-004 -// fusa:req REQ-LOAN-005 -// fusa:req REQ-LOAN-006 -// fusa:req REQ-LOAN-007 +//fusa:req REQ-LOAN-001 +//fusa:req REQ-LOAN-002 +//fusa:req REQ-LOAN-003 +//fusa:req REQ-LOAN-004 +//fusa:req REQ-LOAN-005 +//fusa:req REQ-LOAN-006 +//fusa:req REQ-LOAN-007 //! Pool-based zero-copy payload loaning. //! @@ -39,7 +39,7 @@ use crate::{Loan, RcpError}; // ── LoanPool ────────────────────────────────────────────────────────────────── /// Pre-allocated buffer pool. -// fusa:req REQ-LOAN-001 +//fusa:req REQ-LOAN-001 pub struct LoanPool { state: Arc<(Mutex>>, Condvar)>, size: usize, @@ -47,7 +47,7 @@ pub struct LoanPool { impl LoanPool { /// Create a pool with `count` buffers each of `size` bytes. - // fusa:req REQ-LOAN-002 + //fusa:req REQ-LOAN-002 pub fn new(count: usize, size: usize) -> Self { let pool: Vec> = (0..count).map(|_| vec![0u8; size]).collect(); LoanPool { @@ -57,7 +57,7 @@ impl LoanPool { } /// Obtain a buffer from the pool, blocking until one is available. - // fusa:req REQ-LOAN-003 + //fusa:req REQ-LOAN-003 pub fn acquire(&self) -> Loan { let state = Arc::clone(&self.state); let buf = { @@ -78,7 +78,7 @@ impl LoanPool { } /// Try to obtain a buffer without blocking. Returns `None` if pool is empty. - // fusa:req REQ-LOAN-004 + //fusa:req REQ-LOAN-004 pub fn try_acquire(&self) -> Option { let state = Arc::clone(&self.state); let (lock, _) = &*self.state; @@ -104,7 +104,7 @@ impl LoanPool { // ── LoanPoolEndpoint ───────────────────────────────────────────────────────── /// An endpoint decorator backed by a `LoanPool` for zero-copy writes. -// fusa:req REQ-LOAN-005 +//fusa:req REQ-LOAN-005 pub struct LoanPoolEndpoint { inner: Arc, pool: Arc, @@ -119,7 +119,7 @@ impl LoanPoolEndpoint { /// /// Returns `Err(RcpError::PayloadTooLarge)` if `size` exceeds the /// pool's buffer size. - // fusa:req REQ-LOAN-006 + //fusa:req REQ-LOAN-006 pub fn loan(&self, size: usize) -> Result { if size > self.pool.buffer_size() { return Err(RcpError::PayloadTooLarge); @@ -131,7 +131,7 @@ impl LoanPoolEndpoint { /// /// The buffer is returned to the pool once this call completes (the /// `loan` is dropped either way). - // fusa:req REQ-LOAN-007 + //fusa:req REQ-LOAN-007 pub fn write_loaned(&self, loan: Loan) -> Result<(), RcpError> { let payload = loan.payload.clone(); // Buffer returned to pool on drop (loan's release fn fires). @@ -167,8 +167,8 @@ mod tests { } #[test] - // fusa:test REQ-LOAN-001 - // fusa:test REQ-LOAN-002 + //fusa:test REQ-LOAN-001 + //fusa:test REQ-LOAN-002 fn pool_created_with_correct_count() { let pool = LoanPool::new(3, 64); assert_eq!(pool.available(), 3); @@ -176,7 +176,7 @@ mod tests { } #[test] - // fusa:test REQ-LOAN-003 + //fusa:test REQ-LOAN-003 fn acquire_reduces_available() { let pool = LoanPool::new(2, 64); let _loan = pool.acquire(); @@ -184,7 +184,7 @@ mod tests { } #[test] - // fusa:test REQ-LOAN-003 + //fusa:test REQ-LOAN-003 fn buffer_returned_on_drop() { let pool = LoanPool::new(1, 64); { @@ -195,7 +195,7 @@ mod tests { } #[test] - // fusa:test REQ-LOAN-004 + //fusa:test REQ-LOAN-004 fn try_acquire_returns_none_when_empty() { let pool = LoanPool::new(1, 64); let _l1 = pool.acquire(); @@ -203,7 +203,7 @@ mod tests { } #[test] - // fusa:test REQ-LOAN-006 + //fusa:test REQ-LOAN-006 fn loan_rejects_oversized_request() { let pool = Arc::new(LoanPool::new(2, 64)); let ep = LoanPoolEndpoint::new(inner(), Arc::clone(&pool)); @@ -212,7 +212,7 @@ mod tests { } #[test] - // fusa:test REQ-LOAN-007 + //fusa:test REQ-LOAN-007 fn write_loaned_forwards_payload() { let received = Arc::new(std::sync::Mutex::new(vec![])); let recv2 = Arc::clone(&received); @@ -257,7 +257,7 @@ mod tests { } #[test] - // fusa:test REQ-LOAN-005 + //fusa:test REQ-LOAN-005 fn loan_endpoint_ep_type_matches_inner() { let pool = Arc::new(LoanPool::new(1, 64)); let ep = LoanPoolEndpoint::new(inner(), pool); diff --git a/src/mdio.rs b/src/mdio.rs index 900ee1c..493a65a 100644 --- a/src/mdio.rs +++ b/src/mdio.rs @@ -1,9 +1,9 @@ -// fusa:req REQ-MDIO-001 -// fusa:req REQ-MDIO-002 -// fusa:req REQ-MDIO-003 -// fusa:req REQ-MDIO-004 -// fusa:req REQ-MDIO-005 -// fusa:req REQ-MDIO-006 +//fusa:req REQ-MDIO-001 +//fusa:req REQ-MDIO-002 +//fusa:req REQ-MDIO-003 +//fusa:req REQ-MDIO-004 +//fusa:req REQ-MDIO-005 +//fusa:req REQ-MDIO-006 //! The MDIO endpoint type (`ep_type 0x0D`) — `ROADMAP.md` Milestone 7 //! ("Remaining Endpoint Types"), fourth checklist bullet: IEEE 802.3 @@ -72,6 +72,47 @@ //! than silently guessing which of the two conflicting spec passages is //! authoritative. //! +//! ## Divergence note: `mdio_mode` does **not** select Clause 22 vs Clause 45 +//! +//! **This module's [`MdioAddressingMode`] contradicts TC18 and must not be +//! relied on for wire conformance.** TC18 §13.7.13.3 Table 57 "Usage of ABB +//! message for mdio requests" (TC18.txt line 5676) defines `mdio_mode` as an +//! MMD-vs-MMS access-kind and access-width selector, not an IEEE 802.3 +//! clause selector: +//! +//! | `mdio_mode` | meaning (TC18 Table 57) | +//! |-------------|-------------------------| +//! | `01b` | MMD, single word access | +//! | `01b` *(as printed — see below)* | MMD, multiple byte access | +//! | `10b` | MMS, single word access | +//! | `11b` | MMS, multiple (double) word access | +//! +//! Table 57 as printed lists `01b` twice and never lists `00b`, so one of +//! the two MMD rows is a spec typo whose intended code point (`00b` for one +//! of them) this crate cannot resolve from the text alone. Either way, the +//! `Clause22 = 0` / `Clause45 = 1` / `Spare2` / `Spare3` mapping below is +//! **wrong** against Table 57: TC18 assigns no `mdio_mode` value to a +//! Clause-22-vs-Clause-45 choice at all, and it leaves at most one code +//! point unallocated rather than two. Correcting this is a behavior change +//! deliberately not made in the requirements-completeness pass that +//! discovered it; the accompanying requirement entry records the divergence +//! as not-implemented, and the surrounding provenance note is retained below +//! only as the historical record of how the wrong mapping arose (it was +//! derived from `ROADMAP.md`'s restatement, never from TC18 itself). +//! Table 57 also fixes the payload widths this module does not model: +//! `mdio_address` "as per IEEE & OA SPI spec", and `mdio_payload` data +//! fields of 16 bits for MMD, 32 bits for MMS0 and MMS1, and 16 bits for +//! every other MMS. +//! +//! Two further §13.7.13 gaps are recorded as not-implemented entries: TC18 +//! Figure 42's request-payload layout (line 5664: `reserved`, `mdio_mode`, +//! `mdio_address`, `mdio_payload`), which [`MdioTransfer`] carries opaque; +//! and TC18 Table 56's functional-config register layout (§13.7.13.2, line +//! 5639), whose §13.7.13.2 prose additionally states "The MDIO EP does not +//! have any configurable parameters" — which [`MdioFunctionalConfig`]'s own +//! `addressing_mode` field contradicts by carrying a per-EP-type +//! configurable parameter TC18 does not define. +//! //! ## Provenance note: the two unallocated `mdio_mode` slots //! //! `ROADMAP.md`'s checklist bullet states `mdio_mode` selects between IEEE @@ -149,7 +190,7 @@ use crate::RcpError; /// than each given a specific addressing-mode meaning. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] -// fusa:req REQ-MDIO-001 +//fusa:req REQ-MDIO-001 pub enum MdioAddressingMode { /// IEEE 802.3 Clause 22 addressing: the simple, 5-bit PHY-address / /// 5-bit register-address scheme. @@ -169,7 +210,7 @@ pub enum MdioAddressingMode { impl MdioAddressingMode { /// Encode this addressing mode as its `mdio_mode` 2-bit wire value. - // fusa:req REQ-MDIO-001 + //fusa:req REQ-MDIO-001 pub fn to_u8(self) -> u8 { self as u8 } @@ -180,7 +221,7 @@ impl MdioAddressingMode { /// `0..=3` — `mdio_mode`'s full 2-bit range — matching /// [`crate::i2c::I2cSpeedMode::from_u8`]'s own range-check discipline. /// Never panics for any input. - // fusa:req REQ-MDIO-002 + //fusa:req REQ-MDIO-002 pub fn from_u8(raw: u8) -> Result { match raw { 0 => Ok(Self::Clause22), @@ -195,7 +236,7 @@ impl MdioAddressingMode { /// — the two `mdio_mode` 2-bit values this module's doc comment flags as /// unallocated pending errata. False for /// [`MdioAddressingMode::Clause22`]/[`MdioAddressingMode::Clause45`]. - // fusa:req REQ-MDIO-003 + //fusa:req REQ-MDIO-003 pub fn is_unallocated_slot(self) -> bool { matches!(self, Self::Spare2 | Self::Spare3) } @@ -220,7 +261,7 @@ impl Default for MdioAddressingMode { /// this stays a single-field type rather than growing a clock-divider or /// further mode-select fields. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-MDIO-004 +//fusa:req REQ-MDIO-004 pub struct MdioFunctionalConfig { /// This endpoint's configured `mdio_mode` addressing-mode selector. pub addressing_mode: MdioAddressingMode, @@ -234,7 +275,7 @@ impl MdioFunctionalConfig { /// This module does not itself call that function — it only shows how a /// caller would obtain the matching tag, per this module's doc comment /// "Relationship to `crate::regmap`". - // fusa:req REQ-MDIO-004 + //fusa:req REQ-MDIO-004 pub fn layer_tag(&self) -> crate::regmap::PerEpTypeFunctionalConfig { crate::regmap::PerEpTypeFunctionalConfig::new(crate::regmap::EndpointType::Mdio) } @@ -254,7 +295,7 @@ impl MdioFunctionalConfig { /// empty one, has a valid encoding, so [`MdioTransfer::decode`] is /// infallible. #[derive(Debug, Clone, PartialEq, Eq, Default)] -// fusa:req REQ-MDIO-005 +//fusa:req REQ-MDIO-005 pub struct MdioTransfer { /// The raw bytes sent for this MDIO register-access request, unparsed. pub bytes: Vec, @@ -263,7 +304,7 @@ pub struct MdioTransfer { impl MdioTransfer { /// Encode this transfer to its raw wire representation: `bytes`, /// unmodified and unframed. - // fusa:req REQ-MDIO-005 + //fusa:req REQ-MDIO-005 pub fn encode(&self) -> Vec { self.bytes.clone() } @@ -272,7 +313,7 @@ impl MdioTransfer { /// /// Every possible byte slice, including an empty one, is a valid MDIO /// transfer, so this never fails and never panics for any input. - // fusa:req REQ-MDIO-005 + //fusa:req REQ-MDIO-005 pub fn decode(b: &[u8]) -> Self { Self { bytes: b.to_vec() } } @@ -286,7 +327,7 @@ impl MdioTransfer { /// variable-length byte-stream modeling for the opposite transfer /// direction. #[derive(Debug, Clone, PartialEq, Eq, Default)] -// fusa:req REQ-MDIO-006 +//fusa:req REQ-MDIO-006 pub struct MdioTransferResult { /// The raw bytes returned by this MDIO register-access response, /// unparsed. @@ -296,7 +337,7 @@ pub struct MdioTransferResult { impl MdioTransferResult { /// Encode this transfer result to its raw wire representation: `bytes`, /// unmodified and unframed. - // fusa:req REQ-MDIO-006 + //fusa:req REQ-MDIO-006 pub fn encode(&self) -> Vec { self.bytes.clone() } @@ -305,7 +346,7 @@ impl MdioTransferResult { /// /// Every possible byte slice, including an empty one, is a valid MDIO /// transfer result, so this never fails and never panics for any input. - // fusa:req REQ-MDIO-006 + //fusa:req REQ-MDIO-006 pub fn decode(b: &[u8]) -> Self { Self { bytes: b.to_vec() } } @@ -325,7 +366,7 @@ mod tests { ]; #[test] - // fusa:test REQ-MDIO-001 + //fusa:test REQ-MDIO-001 fn mdio_addressing_mode_round_trips_through_to_u8_from_u8_for_all_four_values() { for mode in ALL_ADDRESSING_MODES { let raw = mode.to_u8(); @@ -334,7 +375,7 @@ mod tests { } #[test] - // fusa:test REQ-MDIO-001 + //fusa:test REQ-MDIO-001 fn mdio_addressing_mode_to_u8_values_are_the_full_2_bit_0_to_3_range() { let mut raws: Vec = ALL_ADDRESSING_MODES.iter().map(|m| m.to_u8()).collect(); raws.sort_unstable(); @@ -342,7 +383,7 @@ mod tests { } #[test] - // fusa:test REQ-MDIO-002 + //fusa:test REQ-MDIO-002 fn mdio_addressing_mode_from_u8_rejects_out_of_range() { for raw in [4u8, 5, 0x7F, 0xFF] { assert_eq!( @@ -353,7 +394,7 @@ mod tests { } #[test] - // fusa:test REQ-MDIO-003 + //fusa:test REQ-MDIO-003 fn mdio_addressing_mode_is_unallocated_slot_true_only_for_the_two_spare_values() { for mode in ALL_ADDRESSING_MODES { let expected = matches!( @@ -365,7 +406,7 @@ mod tests { } #[test] - // fusa:test REQ-MDIO-003 + //fusa:test REQ-MDIO-003 fn mdio_addressing_mode_default_is_clause22_and_not_unallocated() { let mode = MdioAddressingMode::default(); assert_eq!(mode, MdioAddressingMode::Clause22); @@ -375,7 +416,7 @@ mod tests { // ── MdioFunctionalConfig / layer_tag ──────────────────────────────────── #[test] - // fusa:test REQ-MDIO-004 + //fusa:test REQ-MDIO-004 fn mdio_functional_config_default_is_clause22_and_layer_tag_matches_ep_type_mdio() { let functional = MdioFunctionalConfig::default(); assert_eq!(functional.addressing_mode, MdioAddressingMode::Clause22); @@ -393,7 +434,7 @@ mod tests { } #[test] - // fusa:test REQ-MDIO-004 + //fusa:test REQ-MDIO-004 fn mdio_functional_config_layer_tag_rejects_mismatched_ep_type() { let functional = MdioFunctionalConfig { addressing_mode: MdioAddressingMode::Clause45, @@ -408,7 +449,7 @@ mod tests { // ── MdioTransfer / MdioTransferResult: round-trip, never panic ───────── #[test] - // fusa:test REQ-MDIO-005 + //fusa:test REQ-MDIO-005 fn mdio_transfer_round_trips_through_encode_decode_for_any_byte_slice() { for bytes in [ vec![], @@ -425,7 +466,7 @@ mod tests { } #[test] - // fusa:test REQ-MDIO-005 + //fusa:test REQ-MDIO-005 fn mdio_transfer_decode_never_panics_for_any_sampled_input() { for len in [0usize, 1, 2, 3, 9, 64] { let buf = vec![0x5Au8; len]; @@ -434,7 +475,7 @@ mod tests { } #[test] - // fusa:test REQ-MDIO-006 + //fusa:test REQ-MDIO-006 fn mdio_transfer_result_round_trips_through_encode_decode_for_any_byte_slice() { for bytes in [ vec![], @@ -451,7 +492,7 @@ mod tests { } #[test] - // fusa:test REQ-MDIO-006 + //fusa:test REQ-MDIO-006 fn mdio_transfer_result_decode_never_panics_for_any_sampled_input() { for len in [0usize, 1, 2, 3, 9, 64] { let buf = vec![0x5Au8; len]; diff --git a/src/mdns.rs b/src/mdns.rs index 76738eb..4c942c6 100644 --- a/src/mdns.rs +++ b/src/mdns.rs @@ -1,7 +1,7 @@ -// fusa:req REQ-MDNS-001 -// fusa:req REQ-MDNS-002 -// fusa:req REQ-MDNS-003 -// fusa:req REQ-MDNS-004 +//fusa:req REQ-MDNS-001 +//fusa:req REQ-MDNS-002 +//fusa:req REQ-MDNS-003 +//fusa:req REQ-MDNS-004 //! mDNS/DNS-SD service discovery — an optional pre-discovery rendezvous //! helper. @@ -37,7 +37,7 @@ use crate::avtp::StreamId; // ── ServiceRecord ───────────────────────────────────────────────────────────── /// A discovered mDNS service record. -// fusa:req REQ-MDNS-001 +//fusa:req REQ-MDNS-001 #[derive(Debug, Clone)] pub struct ServiceRecord { pub host: String, @@ -50,7 +50,7 @@ pub struct ServiceRecord { /// In-process mDNS registry for testing. Production implementations /// integrate with OS mDNS APIs via the same interface. -// fusa:req REQ-MDNS-002 +//fusa:req REQ-MDNS-002 pub struct MdnsRegistry { records: RwLock>, } @@ -63,7 +63,7 @@ impl MdnsRegistry { } /// Announce a service (called by a server on startup). - // fusa:req REQ-MDNS-003 + //fusa:req REQ-MDNS-003 pub fn announce(&self, name: impl Into, record: ServiceRecord) { self.records.write().unwrap().insert(name.into(), record); } @@ -74,7 +74,7 @@ impl MdnsRegistry { } /// Resolve a service name to its record. - // fusa:req REQ-MDNS-004 + //fusa:req REQ-MDNS-004 pub fn resolve(&self, name: &str) -> Option { self.records.read().unwrap().get(name).cloned() } @@ -112,8 +112,8 @@ mod tests { } #[test] - // fusa:test REQ-MDNS-003 - // fusa:test REQ-MDNS-004 + //fusa:test REQ-MDNS-003 + //fusa:test REQ-MDNS-004 fn announce_and_resolve() { let r = MdnsRegistry::new(); r.announce("fl-svr._rcp._tcp.local.", record(1)); @@ -122,13 +122,13 @@ mod tests { } #[test] - // fusa:test REQ-MDNS-004 + //fusa:test REQ-MDNS-004 fn resolve_unknown_returns_none() { assert!(MdnsRegistry::new().resolve("unknown").is_none()); } #[test] - // fusa:test REQ-MDNS-002 + //fusa:test REQ-MDNS-002 fn withdraw_removes_record() { let r = MdnsRegistry::new(); r.announce("svc", record(2)); @@ -137,7 +137,7 @@ mod tests { } #[test] - // fusa:test REQ-MDNS-001 + //fusa:test REQ-MDNS-001 fn service_record_fields() { let rec = ServiceRecord { host: "h".into(), diff --git a/src/mock.rs b/src/mock.rs index 7d77004..4f8dcb4 100644 --- a/src/mock.rs +++ b/src/mock.rs @@ -1,13 +1,13 @@ -// fusa:req REQ-MOCKSRV-001 -// fusa:req REQ-MOCKSRV-002 -// fusa:req REQ-MOCKSRV-003 -// fusa:req REQ-MOCKSRV-004 -// fusa:req REQ-MOCKSRV-005 -// fusa:req REQ-MOCKSRV-006 -// fusa:req REQ-MOCKSRV-007 -// fusa:req REQ-MOCKSRV-008 -// fusa:req REQ-MOCKSRV-009 -// fusa:req REQ-MOCKSRV-010 +//fusa:req REQ-MOCKSRV-001 +//fusa:req REQ-MOCKSRV-002 +//fusa:req REQ-MOCKSRV-003 +//fusa:req REQ-MOCKSRV-004 +//fusa:req REQ-MOCKSRV-005 +//fusa:req REQ-MOCKSRV-006 +//fusa:req REQ-MOCKSRV-007 +//fusa:req REQ-MOCKSRV-008 +//fusa:req REQ-MOCKSRV-009 +//fusa:req REQ-MOCKSRV-010 //! In-process test double for this crate's OPEN Alliance TC18 Remote //! Control Protocol Specification v0.5.1_RC RC Server model. @@ -153,14 +153,14 @@ impl Endpoint for MockEndpoint { self.ep_type } - // fusa:req REQ-MOCKSRV-010 + //fusa:req REQ-MOCKSRV-010 fn read(&self, read_size: u16) -> Result, RcpError> { let buf = self.buf.lock().unwrap(); let n = (read_size as usize).min(buf.len()); Ok(buf[..n].to_vec()) } - // fusa:req REQ-MOCKSRV-009 + //fusa:req REQ-MOCKSRV-009 fn write(&self, payload: &[u8]) -> Result<(), RcpError> { *self.buf.lock().unwrap() = payload.to_vec(); Ok(()) @@ -207,7 +207,7 @@ impl RcServer { /// Construct a fresh RC Server, starting at [`RcServerState::INITIAL`] /// with no root client and no registered endpoints, holding `general` /// as its initial [`GeneralRegisters`] snapshot. - // fusa:req REQ-MOCKSRV-001 + //fusa:req REQ-MOCKSRV-001 pub fn new(general: GeneralRegisters) -> Arc { Arc::new(Self { state: Mutex::new(RcServerState::INITIAL), @@ -221,7 +221,7 @@ impl RcServer { } /// This server's current lifecycle state. - // fusa:req REQ-MOCKSRV-002 + //fusa:req REQ-MOCKSRV-002 pub fn state(&self) -> RcServerState { *self.state.lock().unwrap() } @@ -235,7 +235,7 @@ impl RcServer { /// delegating to [`RcServerState::try_transition`]. On success, this /// server's stored state is updated to `target`; on failure, it is left /// unchanged. - // fusa:req REQ-MOCKSRV-002 + //fusa:req REQ-MOCKSRV-002 pub fn try_transition( &self, target: RcServerState, @@ -261,7 +261,7 @@ impl RcServer { /// `Err(RcpError::InvalidSize)` for an oversized `byte_bus_id`, or /// `Err(RcpError::EpError)` for an already-registered pair — without /// allocating an endpoint id or storing `endpoint` in either case. - // fusa:req REQ-MOCKSRV-003 + //fusa:req REQ-MOCKSRV-003 pub fn register_endpoint( &self, stream_id: StreamId, @@ -316,10 +316,10 @@ impl RcServer { /// since `build_response_info` always sets the field it echoes — this /// call exists so a future change to either function is caught by this /// module's own tests rather than by a caller). - // fusa:req REQ-MOCKSRV-004 - // fusa:req REQ-MOCKSRV-005 - // fusa:req REQ-MOCKSRV-006 - // fusa:req REQ-MOCKSRV-007 + //fusa:req REQ-MOCKSRV-004 + //fusa:req REQ-MOCKSRV-005 + //fusa:req REQ-MOCKSRV-006 + //fusa:req REQ-MOCKSRV-007 pub fn handle_abb( &self, stream_id: StreamId, @@ -414,7 +414,7 @@ impl RcServer { /// The response frame's `sequence_num` is this server's own /// free-running counter (see [`Self`]'s doc comment), unrelated to the /// request frame's. - // fusa:req REQ-MOCKSRV-008 + //fusa:req REQ-MOCKSRV-008 pub fn handle_ntscf_frame( &self, stream_id: StreamId, @@ -467,14 +467,14 @@ mod rc_server_tests { // ── Lifecycle ───────────────────────────────────────────────────────────── #[test] - // fusa:test REQ-MOCKSRV-001 + //fusa:test REQ-MOCKSRV-001 fn new_server_starts_hw_unconfigured_with_no_root_client() { let srv = RcServer::new(GeneralRegisters::default()); assert_eq!(srv.state(), RcServerState::HwUnconfigured); } #[test] - // fusa:test REQ-MOCKSRV-002 + //fusa:test REQ-MOCKSRV-002 fn try_transition_updates_state_on_success_and_leaves_it_on_failure() { let srv = RcServer::new(GeneralRegisters::default()); srv.try_transition(RcServerState::HwConfigured, || true) @@ -491,7 +491,7 @@ mod rc_server_tests { // ── Endpoint registration ───────────────────────────────────────────────── #[test] - // fusa:test REQ-MOCKSRV-003 + //fusa:test REQ-MOCKSRV-003 fn register_endpoint_assigns_unique_ids_and_rejects_duplicate_pair() { let srv = RcServer::new(GeneralRegisters::default()); let sid = stream(1); @@ -509,8 +509,8 @@ mod rc_server_tests { // ── EP0 dispatch ────────────────────────────────────────────────────────── #[test] - // fusa:test REQ-MOCKSRV-004 - // fusa:test REQ-MOCKSRV-005 + //fusa:test REQ-MOCKSRV-004 + //fusa:test REQ-MOCKSRV-005 fn ep0_read_returns_general_registers_snapshot() { let regs = GeneralRegisters { svr_vendor_id: 0x1234, @@ -525,8 +525,8 @@ mod rc_server_tests { } #[test] - // fusa:test REQ-MOCKSRV-004 - // fusa:test REQ-MOCKSRV-005 + //fusa:test REQ-MOCKSRV-004 + //fusa:test REQ-MOCKSRV-005 fn ep0_write_succeeds_for_the_root_client() { // RegisterCategory::General now has LockPolicy::W // (crate::lifecycle::lock_policy), writable whenever reachable, and @@ -551,7 +551,7 @@ mod rc_server_tests { } #[test] - // fusa:test REQ-MOCKSRV-004 + //fusa:test REQ-MOCKSRV-004 fn ep0_write_from_non_root_client_is_rejected() { let srv = RcServer::new(GeneralRegisters::default()); let root = stream(1); @@ -573,7 +573,7 @@ mod rc_server_tests { } #[test] - // fusa:test REQ-MOCKSRV-004 + //fusa:test REQ-MOCKSRV-004 fn ep0_read_is_reachable_in_every_lifecycle_state() { let srv = RcServer::new(GeneralRegisters::default()); let sid = stream(1); @@ -594,7 +594,7 @@ mod rc_server_tests { // ── Device endpoint dispatch ────────────────────────────────────────────── #[test] - // fusa:test REQ-MOCKSRV-006 + //fusa:test REQ-MOCKSRV-006 fn device_endpoint_write_then_read_round_trips_through_dispatch() { let srv = RcServer::new(GeneralRegisters::default()); let sid = stream(1); @@ -611,7 +611,7 @@ mod rc_server_tests { } #[test] - // fusa:test REQ-MOCKSRV-006 + //fusa:test REQ-MOCKSRV-006 fn unregistered_device_endpoint_returns_ep_not_found() { let srv = RcServer::new(GeneralRegisters::default()); let sid = stream(1); @@ -621,7 +621,7 @@ mod rc_server_tests { } #[test] - // fusa:test REQ-MOCKSRV-006 + //fusa:test REQ-MOCKSRV-006 fn endpoint_registered_under_one_stream_is_not_visible_from_another() { let srv = RcServer::new(GeneralRegisters::default()); let sid_a = stream(1); @@ -638,7 +638,7 @@ mod rc_server_tests { // ── Echo-back ───────────────────────────────────────────────────────────── #[test] - // fusa:test REQ-MOCKSRV-007 + //fusa:test REQ-MOCKSRV-007 fn response_echoes_request_byte_bus_id() { let srv = RcServer::new(GeneralRegisters::default()); let sid = stream(1); @@ -654,7 +654,7 @@ mod rc_server_tests { // ── Whole on-wire round trip ────────────────────────────────────────────── #[test] - // fusa:test REQ-MOCKSRV-008 + //fusa:test REQ-MOCKSRV-008 fn handle_ntscf_frame_round_trips_a_whole_on_wire_request() { let srv = RcServer::new(GeneralRegisters::default()); let sid = stream(1); @@ -674,7 +674,7 @@ mod rc_server_tests { } #[test] - // fusa:test REQ-MOCKSRV-008 + //fusa:test REQ-MOCKSRV-008 fn handle_ntscf_frame_dispatches_multiple_requests_concatenated_in_one_frame() { // TC18 §12.9.1.1: an RC Server must support multiple requests // concatenated in a single frame (rust-RCP-W03). @@ -708,7 +708,7 @@ mod rc_server_tests { } #[test] - // fusa:test REQ-MOCKSRV-008 + //fusa:test REQ-MOCKSRV-008 fn handle_ntscf_frame_answers_a_dispatch_failure_with_a_wire_error_response_not_a_local_err() { // TC18 §12.9.6 "Handling errors" / rust-RCP-W04: a dispatch failure // that has a Table 27 wire code must be answered on the wire with @@ -729,7 +729,7 @@ mod rc_server_tests { } #[test] - // fusa:test REQ-MOCKSRV-008 + //fusa:test REQ-MOCKSRV-008 fn handle_ntscf_frame_never_panics_on_garbage_input() { let srv = RcServer::new(GeneralRegisters::default()); let sid = stream(1); @@ -741,7 +741,7 @@ mod rc_server_tests { // ── MockEndpoint ────────────────────────────────────────────────────────── #[test] - // fusa:test REQ-MOCKSRV-009 + //fusa:test REQ-MOCKSRV-009 fn mock_endpoint_read_returns_last_written_bytes() { let ep = MockEndpoint::new(EndpointType::Gpio, Vec::new()); ep.write(&[1, 2, 3]).unwrap(); @@ -749,7 +749,7 @@ mod rc_server_tests { } #[test] - // fusa:test REQ-MOCKSRV-010 + //fusa:test REQ-MOCKSRV-010 fn mock_endpoint_read_size_exceeding_buffer_does_not_panic() { let ep = MockEndpoint::new(EndpointType::Gpio, vec![1, 2]); let out = ep.read(255).unwrap(); @@ -757,7 +757,7 @@ mod rc_server_tests { } #[test] - // fusa:test REQ-MOCKSRV-010 + //fusa:test REQ-MOCKSRV-010 fn mock_endpoint_read_on_empty_buffer_does_not_panic() { let ep = MockEndpoint::new(EndpointType::Gpio, Vec::new()); assert_eq!(ep.read(10).unwrap(), Vec::::new()); diff --git a/src/observe.rs b/src/observe.rs index 0f22b96..45fb1b4 100644 --- a/src/observe.rs +++ b/src/observe.rs @@ -1,9 +1,9 @@ -// fusa:req REQ-OBS-001 -// fusa:req REQ-OBS-002 -// fusa:req REQ-OBS-003 -// fusa:req REQ-OBS-004 -// fusa:req REQ-OBS-005 -// fusa:req REQ-OBS-006 +//fusa:req REQ-OBS-001 +//fusa:req REQ-OBS-002 +//fusa:req REQ-OBS-003 +//fusa:req REQ-OBS-004 +//fusa:req REQ-OBS-005 +//fusa:req REQ-OBS-006 //! Observability hooks — latency histogram, error counters, and event //! callbacks over an [`Endpoint`]. @@ -31,7 +31,7 @@ use crate::RcpError; // ── Metrics ─────────────────────────────────────────────────────────────────── /// Aggregated call metrics for an endpoint. -// fusa:req REQ-OBS-001 +//fusa:req REQ-OBS-001 #[derive(Debug, Default)] pub struct Metrics { pub total_calls: AtomicU64, @@ -71,7 +71,7 @@ type ReadHookFn = Box, RcpError>, Duration) + Send + type WriteHookFn = Box, Duration) + Send + Sync>; /// Observing wrapper that records metrics and fires post-call hooks. -// fusa:req REQ-OBS-002 +//fusa:req REQ-OBS-002 pub struct ObserveEndpoint { inner: Arc, metrics: Arc, @@ -90,13 +90,13 @@ impl ObserveEndpoint { } /// Snapshot of aggregated metrics (covers both reads and writes). - // fusa:req REQ-OBS-003 + //fusa:req REQ-OBS-003 pub fn metrics(&self) -> Arc { Arc::clone(&self.metrics) } /// Register a post-read hook. - // fusa:req REQ-OBS-004 + //fusa:req REQ-OBS-004 pub fn add_read_hook( &self, f: impl Fn(u16, &Result, RcpError>, Duration) + Send + Sync + 'static, @@ -105,7 +105,7 @@ impl ObserveEndpoint { } /// Register a post-write hook. - // fusa:req REQ-OBS-004 + //fusa:req REQ-OBS-004 pub fn add_write_hook( &self, f: impl Fn(&[u8], &Result<(), RcpError>, Duration) + Send + Sync + 'static, @@ -119,7 +119,7 @@ impl Endpoint for ObserveEndpoint { self.inner.ep_type() } - // fusa:req REQ-OBS-005 + //fusa:req REQ-OBS-005 fn read(&self, read_size: u16) -> Result, RcpError> { let start = Instant::now(); let result = self.inner.read(read_size); @@ -131,7 +131,7 @@ impl Endpoint for ObserveEndpoint { result } - // fusa:req REQ-OBS-006 + //fusa:req REQ-OBS-006 fn write(&self, payload: &[u8]) -> Result<(), RcpError> { let start = Instant::now(); let result = self.inner.write(payload); @@ -170,8 +170,8 @@ mod tests { } #[test] - // fusa:test REQ-OBS-002 - // fusa:test REQ-OBS-003 + //fusa:test REQ-OBS-002 + //fusa:test REQ-OBS-003 fn call_count_increments() { let o = ObserveEndpoint::new(ok_ep()); for _ in 0..5 { @@ -181,7 +181,7 @@ mod tests { } #[test] - // fusa:test REQ-OBS-003 + //fusa:test REQ-OBS-003 fn error_count_increments_on_failure() { let o = ObserveEndpoint::new(Arc::new(AlwaysFail) as Arc); let _ = o.write(b"x"); @@ -189,8 +189,8 @@ mod tests { } #[test] - // fusa:test REQ-OBS-004 - // fusa:test REQ-OBS-005 + //fusa:test REQ-OBS-004 + //fusa:test REQ-OBS-005 fn read_hook_is_called_after_read() { let fired = Arc::new(AtomicU64::new(0)); let f2 = Arc::clone(&fired); @@ -203,7 +203,7 @@ mod tests { } #[test] - // fusa:test REQ-OBS-006 + //fusa:test REQ-OBS-006 fn write_hook_is_called_after_write() { let fired = Arc::new(AtomicU64::new(0)); let f2 = Arc::clone(&fired); @@ -216,7 +216,7 @@ mod tests { } #[test] - // fusa:test REQ-OBS-001 + //fusa:test REQ-OBS-001 fn mean_latency_is_non_negative() { let o = ObserveEndpoint::new(ok_ep()); o.write(b"x").unwrap(); diff --git a/src/powerstate.rs b/src/powerstate.rs index 6801cb0..8d83a42 100644 --- a/src/powerstate.rs +++ b/src/powerstate.rs @@ -1,14 +1,14 @@ -// fusa:req REQ-PWR-001 -// fusa:req REQ-PWR-002 -// fusa:req REQ-PWR-003 -// fusa:req REQ-PWR-004 -// fusa:req REQ-PWR-005 -// fusa:req REQ-PWR-006 -// fusa:req REQ-PWR-007 -// fusa:req REQ-PWR-008 -// fusa:req REQ-PWRSTART-001 -// fusa:req REQ-PWRSTART-002 -// fusa:req REQ-PWRSTART-003 +//fusa:req REQ-PWR-001 +//fusa:req REQ-PWR-002 +//fusa:req REQ-PWR-003 +//fusa:req REQ-PWR-004 +//fusa:req REQ-PWR-005 +//fusa:req REQ-PWR-006 +//fusa:req REQ-PWR-007 +//fusa:req REQ-PWR-008 +//fusa:req REQ-PWRSTART-001 +//fusa:req REQ-PWRSTART-002 +//fusa:req REQ-PWRSTART-003 //! Power-mode model (`ROADMAP.md` Milestone 6, "Real power-mode model //! backing the safe-state work" bullet). @@ -248,7 +248,7 @@ use crate::RcpError; /// "Provenance note: `Unpowered`'s software-model semantics" for what /// [`PowerMode::Unpowered`] can and cannot mean for a running process. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// fusa:req REQ-PWR-001 +//fusa:req REQ-PWR-001 pub enum PowerMode { /// Fully operational — every endpoint may be driven normally. TC18 /// §12.4's "Powered" mode, in the lifecycle state the RC Server is @@ -309,7 +309,7 @@ impl PowerMode { /// the same mode. /// /// Never panics for any input. -// fusa:req REQ-PWR-002 +//fusa:req REQ-PWR-002 pub fn is_power_mode_transition_defined(from: PowerMode, to: PowerMode) -> bool { matches!( (from, to), @@ -329,7 +329,7 @@ pub fn is_power_mode_transition_defined(from: PowerMode, to: PowerMode) -> bool /// [`power_mode_gate_from_request_states`] for how a caller can derive both /// from `crate::request::RequestLifecycleState` directly. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-PWR-003 +//fusa:req REQ-PWR-003 pub struct PowerModeGateInput { /// No endpoint reachable from this power domain is currently busy. pub all_endpoints_idle: bool, @@ -339,7 +339,7 @@ pub struct PowerModeGateInput { /// The full gate check: both [`PowerModeGateInput`] fields must be `true`. /// Never panics for any input. -// fusa:req REQ-PWR-003 +//fusa:req REQ-PWR-003 pub fn is_power_mode_gate_satisfied(input: PowerModeGateInput) -> bool { input.all_endpoints_idle && input.no_pending_response } @@ -354,7 +354,7 @@ pub fn is_power_mode_gate_satisfied(input: PowerModeGateInput) -> bool { /// module's own working interpretation of "idle" and "no pending response" /// as the same underlying fact about request progress. Never panics for /// any input. -// fusa:req REQ-PWRSTART-003 +//fusa:req REQ-PWRSTART-003 pub fn power_mode_gate_from_request_states(states: &[RequestLifecycleState]) -> PowerModeGateInput { let idle = states .iter() @@ -377,7 +377,7 @@ pub fn power_mode_gate_from_request_states(states: &[RequestLifecycleState]) -> /// [`crate::request::check_compound_gate`]'s own use of /// `RequestRejected` for "known but not currently satisfied." Never panics /// for any input. -// fusa:req REQ-PWR-004 +//fusa:req REQ-PWR-004 pub fn try_enter_power_mode( from: PowerMode, to: PowerMode, @@ -399,7 +399,7 @@ pub fn try_enter_power_mode( /// as an unconditional demotion" for why no [`PowerModeGateInput`] gate /// applies here. Always returns [`PowerMode::Unpowered`]. Never panics for /// any input. -// fusa:req REQ-PWR-005 +//fusa:req REQ-PWR-005 pub fn shutdown_to_unpowered(_from: PowerMode) -> PowerMode { PowerMode::Unpowered } @@ -439,7 +439,7 @@ pub enum StartupPath { /// (already started) or [`PowerMode::StandBy`] (whose resume is the hot /// start — see [`try_hot_start`]), or when the origin is valid but `gate` /// is not yet satisfied. Never panics for any input. -// fusa:req REQ-PWRSTART-001 +//fusa:req REQ-PWRSTART-001 pub fn try_cold_start(from: PowerMode, gate: PowerModeGateInput) -> Result { if !matches!(from, PowerMode::Unpowered | PowerMode::Sleep) { return Err(RcpError::RequestRejected); @@ -468,7 +468,7 @@ pub fn try_cold_start(from: PowerMode, gate: PowerModeGateInput) -> Result Result { match state { WakeUpHandshakeState::Idle => Ok(WakeUpHandshakeState::RequestSent), @@ -530,7 +530,7 @@ pub fn send_wakeup_request(state: WakeUpHandshakeState) -> Result Result { @@ -543,7 +543,7 @@ pub fn acknowledge_wakeup_request( /// Whether the WakeUp handshake has reached completion: /// [`WakeUpHandshakeState::Acknowledged`], and only that variant. Never /// panics for any input. -// fusa:req REQ-PWR-007 +//fusa:req REQ-PWR-007 pub fn is_wakeup_handshake_complete(state: WakeUpHandshakeState) -> bool { matches!(state, WakeUpHandshakeState::Acknowledged) } @@ -590,7 +590,7 @@ mod tests { // ── PowerMode ───────────────────────────────────────────────────────── #[test] - // fusa:test REQ-PWR-001 + //fusa:test REQ-PWR-001 fn power_mode_variants_are_pairwise_distinct() { for (i, a) in ALL_MODES.iter().enumerate() { for (j, b) in ALL_MODES.iter().enumerate() { @@ -600,7 +600,7 @@ mod tests { } #[test] - // fusa:test REQ-PWR-001 + //fusa:test REQ-PWR-001 fn as_str_gives_a_distinct_name_per_mode() { let names: Vec<&str> = ALL_MODES.iter().map(|m| m.as_str()).collect(); let mut sorted = names.clone(); @@ -614,7 +614,7 @@ mod tests { /// TC18 §12.4 Figure 17's two "Go to ..." edges, the only ordinary /// (non-start-up) transitions the diagram draws. Both leave `Normal`. #[test] - // fusa:test REQ-PWR-002 + //fusa:test REQ-PWR-002 fn figure_17_go_to_edges_are_the_ordinary_transitions() { // "Go to StandBy" assert!(is_power_mode_transition_defined( @@ -633,7 +633,7 @@ mod tests { /// powered") and are reached only via `Normal`. Releases before /// v5.0.0 wrongly accepted this pair in both directions. #[test] - // fusa:test REQ-PWR-002 + //fusa:test REQ-PWR-002 fn standby_sleep_pair_is_not_a_transition_in_either_direction() { assert!(!is_power_mode_transition_defined( PowerMode::StandBy, @@ -649,7 +649,7 @@ mod tests { /// their own extra preconditions, so they are not members of the /// ordinary set — [`try_hot_start`] and [`try_cold_start`] own them. #[test] - // fusa:test REQ-PWR-002 + //fusa:test REQ-PWR-002 fn wakeup_edges_back_to_normal_are_not_ordinary_transitions() { assert!(!is_power_mode_transition_defined( PowerMode::StandBy, @@ -662,7 +662,7 @@ mod tests { } #[test] - // fusa:test REQ-PWR-002 + //fusa:test REQ-PWR-002 fn unpowered_is_never_an_ordinary_transition_member() { for &m in &ALL_MODES { assert!(!is_power_mode_transition_defined(m, PowerMode::Unpowered)); @@ -671,7 +671,7 @@ mod tests { } #[test] - // fusa:test REQ-PWR-002 + //fusa:test REQ-PWR-002 fn staying_in_the_same_mode_is_not_defined() { for &m in &ALL_MODES { assert!(!is_power_mode_transition_defined(m, m)); @@ -681,7 +681,7 @@ mod tests { /// Exhaustive cross-product: exactly the two Figure 17 "Go to" edges, /// and nothing else, out of all 16 ordered pairs. #[test] - // fusa:test REQ-PWR-002 + //fusa:test REQ-PWR-002 fn exactly_two_ordered_pairs_are_defined() { let defined: Vec<(PowerMode, PowerMode)> = ALL_MODES .iter() @@ -700,7 +700,7 @@ mod tests { // ── PowerModeGateInput / is_power_mode_gate_satisfied ──────────────── #[test] - // fusa:test REQ-PWR-003 + //fusa:test REQ-PWR-003 fn gate_requires_both_flags_true() { assert!(is_power_mode_gate_satisfied(PowerModeGateInput { all_endpoints_idle: true, @@ -720,14 +720,14 @@ mod tests { // ── power_mode_gate_from_request_states ────────────────────────────── #[test] - // fusa:test REQ-PWRSTART-003 + //fusa:test REQ-PWRSTART-003 fn empty_request_state_slice_is_vacuously_idle() { let gate = power_mode_gate_from_request_states(&[]); assert!(is_power_mode_gate_satisfied(gate)); } #[test] - // fusa:test REQ-PWRSTART-003 + //fusa:test REQ-PWRSTART-003 fn all_finalized_states_are_idle() { let states = [ RequestLifecycleState::Finalized, @@ -738,7 +738,7 @@ mod tests { } #[test] - // fusa:test REQ-PWRSTART-003 + //fusa:test REQ-PWRSTART-003 fn any_non_finalized_state_is_not_idle() { for state in [ RequestLifecycleState::Pending, @@ -756,7 +756,7 @@ mod tests { // ── try_enter_power_mode ────────────────────────────────────────────── #[test] - // fusa:test REQ-PWR-004 + //fusa:test REQ-PWR-004 fn succeeds_when_defined_and_gated() { let gate = PowerModeGateInput { all_endpoints_idle: true, @@ -773,7 +773,7 @@ mod tests { } #[test] - // fusa:test REQ-PWR-004 + //fusa:test REQ-PWR-004 fn rejected_when_transition_undefined_even_if_gated() { let gate = PowerModeGateInput { all_endpoints_idle: true, @@ -796,7 +796,7 @@ mod tests { } #[test] - // fusa:test REQ-PWR-004 + //fusa:test REQ-PWR-004 fn rejected_when_defined_but_not_gated() { let gate = PowerModeGateInput { all_endpoints_idle: true, @@ -811,7 +811,7 @@ mod tests { // ── shutdown_to_unpowered ───────────────────────────────────────────── #[test] - // fusa:test REQ-PWR-005 + //fusa:test REQ-PWR-005 fn shutdown_is_unconditional_from_every_powered_mode() { assert_eq!( shutdown_to_unpowered(PowerMode::Normal), @@ -833,7 +833,7 @@ mod tests { /// sleep**)". Both origins, and Figure 17 labels both arrows /// "Cold start". Releases before v5.0.0 accepted only `Unpowered`. #[test] - // fusa:test REQ-PWRSTART-001 + //fusa:test REQ-PWRSTART-001 fn cold_start_succeeds_from_both_documented_origins_when_gated() { let gate = PowerModeGateInput { all_endpoints_idle: true, @@ -856,7 +856,7 @@ mod tests { /// procedure" only. Releases before v5.0.0 gated this path behind the /// handshake, blocking every wake-from-sleep that had not run one. #[test] - // fusa:test REQ-PWRSTART-001 + //fusa:test REQ-PWRSTART-001 fn cold_start_from_sleep_needs_no_wakeup_handshake() { let gate = PowerModeGateInput { all_endpoints_idle: true, @@ -874,7 +874,7 @@ mod tests { /// `StandBy` is the hot-start origin, not a cold-start one, and /// `Normal` is already started. #[test] - // fusa:test REQ-PWRSTART-001 + //fusa:test REQ-PWRSTART-001 fn cold_start_rejected_from_normal_and_standby() { let gate = PowerModeGateInput { all_endpoints_idle: true, @@ -886,7 +886,7 @@ mod tests { } #[test] - // fusa:test REQ-PWRSTART-001 + //fusa:test REQ-PWRSTART-001 fn cold_start_rejected_when_not_gated() { let gate = PowerModeGateInput::default(); for &m in &[PowerMode::Unpowered, PowerMode::Sleep] { @@ -900,7 +900,7 @@ mod tests { /// single "Hot start" arrow. Releases before v5.0.0 had this origin /// as `Sleep`. #[test] - // fusa:test REQ-PWRSTART-002 + //fusa:test REQ-PWRSTART-002 fn hot_start_succeeds_from_standby_when_acknowledged_and_gated() { let gate = PowerModeGateInput { all_endpoints_idle: true, @@ -913,7 +913,7 @@ mod tests { } #[test] - // fusa:test REQ-PWRSTART-002 + //fusa:test REQ-PWRSTART-002 fn hot_start_rejected_from_every_non_standby_mode() { let gate = PowerModeGateInput { all_endpoints_idle: true, @@ -931,7 +931,7 @@ mod tests { } #[test] - // fusa:test REQ-PWRSTART-002 + //fusa:test REQ-PWRSTART-002 fn hot_start_rejected_without_a_completed_handshake() { let gate = PowerModeGateInput { all_endpoints_idle: true, @@ -949,7 +949,7 @@ mod tests { } #[test] - // fusa:test REQ-PWRSTART-002 + //fusa:test REQ-PWRSTART-002 fn hot_start_rejected_when_not_gated() { let gate = PowerModeGateInput::default(); assert_eq!( @@ -962,7 +962,7 @@ mod tests { /// together cover exactly the three non-`Normal` modes, matching /// Figure 17's three inbound arrows to `Normal`. #[test] - // fusa:test REQ-PWRSTART-002 + //fusa:test REQ-PWRSTART-002 fn cold_and_hot_start_origins_partition_the_three_inbound_edges() { let gate = PowerModeGateInput { all_endpoints_idle: true, @@ -986,13 +986,13 @@ mod tests { // ── WakeUpHandshakeState progression ────────────────────────────────── #[test] - // fusa:test REQ-PWR-006 + //fusa:test REQ-PWR-006 fn default_wakeup_state_is_idle() { assert_eq!(WakeUpHandshakeState::default(), WakeUpHandshakeState::Idle); } #[test] - // fusa:test REQ-PWR-006 + //fusa:test REQ-PWR-006 fn handshake_advances_in_order() { let sent = send_wakeup_request(WakeUpHandshakeState::Idle).unwrap(); assert_eq!(sent, WakeUpHandshakeState::RequestSent); @@ -1001,7 +1001,7 @@ mod tests { } #[test] - // fusa:test REQ-PWR-006 + //fusa:test REQ-PWR-006 fn cannot_send_a_request_out_of_idle() { assert_eq!( send_wakeup_request(WakeUpHandshakeState::RequestSent), @@ -1014,7 +1014,7 @@ mod tests { } #[test] - // fusa:test REQ-PWR-006 + //fusa:test REQ-PWR-006 fn cannot_acknowledge_out_of_request_sent() { assert_eq!( acknowledge_wakeup_request(WakeUpHandshakeState::Idle), @@ -1029,7 +1029,7 @@ mod tests { // ── is_wakeup_handshake_complete ────────────────────────────────────── #[test] - // fusa:test REQ-PWR-007 + //fusa:test REQ-PWR-007 fn only_acknowledged_is_complete() { assert!(!is_wakeup_handshake_complete(WakeUpHandshakeState::Idle)); assert!(!is_wakeup_handshake_complete( @@ -1043,7 +1043,7 @@ mod tests { // ── Never-panics sweep ────────────────────────────────────────────── #[test] - // fusa:test REQ-PWR-008 + //fusa:test REQ-PWR-008 fn never_panics_for_any_sampled_input() { for &from in &ALL_MODES { for &to in &ALL_MODES { diff --git a/src/proxy.rs b/src/proxy.rs index c404501..a94c185 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -1,9 +1,9 @@ -// fusa:req REQ-PROXY-001 -// fusa:req REQ-PROXY-002 -// fusa:req REQ-PROXY-003 -// fusa:req REQ-PROXY-004 -// fusa:req REQ-PROXY-005 -// fusa:req REQ-PROXY-006 +//fusa:req REQ-PROXY-001 +//fusa:req REQ-PROXY-002 +//fusa:req REQ-PROXY-003 +//fusa:req REQ-PROXY-004 +//fusa:req REQ-PROXY-005 +//fusa:req REQ-PROXY-006 //! Transparent proxy endpoint — delegates to an interchangeable inner //! endpoint, allowing hot-swap without changing the call site. @@ -26,7 +26,7 @@ use crate::RcpError; // ── ProxyEndpoint ───────────────────────────────────────────────────────────── /// A proxy that forwards all calls to a replaceable inner endpoint. -// fusa:req REQ-PROXY-001 +//fusa:req REQ-PROXY-001 pub struct ProxyEndpoint { ep_type: EndpointType, inner: RwLock>>, @@ -34,7 +34,7 @@ pub struct ProxyEndpoint { impl ProxyEndpoint { /// Create a proxy backed by `inner`. - // fusa:req REQ-PROXY-002 + //fusa:req REQ-PROXY-002 pub fn new(inner: Arc) -> Self { let ep_type = inner.ep_type(); ProxyEndpoint { @@ -44,13 +44,13 @@ impl ProxyEndpoint { } /// Replace the inner endpoint atomically. - // fusa:req REQ-PROXY-005 + //fusa:req REQ-PROXY-005 pub fn swap(&self, new_inner: Arc) { *self.inner.write().unwrap() = Some(new_inner); } /// Detach the inner endpoint; subsequent calls return `Err(RcpError::NotConnected)`. - // fusa:req REQ-PROXY-006 + //fusa:req REQ-PROXY-006 pub fn detach(&self) { *self.inner.write().unwrap() = None; } @@ -61,7 +61,7 @@ impl Endpoint for ProxyEndpoint { self.ep_type } - // fusa:req REQ-PROXY-003 + //fusa:req REQ-PROXY-003 fn read(&self, read_size: u16) -> Result, RcpError> { let guard = self.inner.read().unwrap(); match guard.as_ref() { @@ -70,7 +70,7 @@ impl Endpoint for ProxyEndpoint { } } - // fusa:req REQ-PROXY-004 + //fusa:req REQ-PROXY-004 fn write(&self, payload: &[u8]) -> Result<(), RcpError> { let guard = self.inner.read().unwrap(); match guard.as_ref() { @@ -93,8 +93,8 @@ mod tests { } #[test] - // fusa:test REQ-PROXY-001 - // fusa:test REQ-PROXY-003 + //fusa:test REQ-PROXY-001 + //fusa:test REQ-PROXY-003 fn forwards_calls_to_inner() { let proxy = ProxyEndpoint::new(ok_ep(EndpointType::Gpio)); proxy.write(b"x").unwrap(); @@ -102,14 +102,14 @@ mod tests { } #[test] - // fusa:test REQ-PROXY-002 + //fusa:test REQ-PROXY-002 fn ep_type_matches_original_inner() { let proxy = ProxyEndpoint::new(ok_ep(EndpointType::Adc)); assert_eq!(proxy.ep_type(), EndpointType::Adc); } #[test] - // fusa:test REQ-PROXY-005 + //fusa:test REQ-PROXY-005 fn swap_replaces_inner() { let proxy = ProxyEndpoint::new(ok_ep(EndpointType::Gpio)); // Detach-and-reattach a fresh endpoint to observe the swap taking @@ -121,7 +121,7 @@ mod tests { } #[test] - // fusa:test REQ-PROXY-006 + //fusa:test REQ-PROXY-006 fn detach_returns_not_connected() { let proxy = ProxyEndpoint::new(ok_ep(EndpointType::Gpio)); proxy.detach(); @@ -130,14 +130,14 @@ mod tests { } #[test] - // fusa:test REQ-PROXY-004 + //fusa:test REQ-PROXY-004 fn read_forwarded() { let proxy = ProxyEndpoint::new(ok_ep(EndpointType::Gpio)); proxy.read(4).unwrap(); } #[test] - // fusa:test REQ-PROXY-006 + //fusa:test REQ-PROXY-006 fn read_detached_returns_not_connected() { let proxy = ProxyEndpoint::new(ok_ep(EndpointType::Gpio)); proxy.detach(); diff --git a/src/pwm.rs b/src/pwm.rs index 1b2667f..319c289 100644 --- a/src/pwm.rs +++ b/src/pwm.rs @@ -1,12 +1,12 @@ -// fusa:req REQ-PWM-001 -// fusa:req REQ-PWM-002 -// fusa:req REQ-PWM-003 -// fusa:req REQ-PWM-004 -// fusa:req REQ-PWM-005 -// fusa:req REQ-PWM-006 -// fusa:req REQ-PWM-007 -// fusa:req REQ-PWM-008 -// fusa:req REQ-PWM-009 +//fusa:req REQ-PWM-001 +//fusa:req REQ-PWM-002 +//fusa:req REQ-PWM-003 +//fusa:req REQ-PWM-004 +//fusa:req REQ-PWM-005 +//fusa:req REQ-PWM-006 +//fusa:req REQ-PWM-007 +//fusa:req REQ-PWM-008 +//fusa:req REQ-PWM-009 //! The PWM_OUT / PWM_IN endpoint types (`ep_type 0x07`/`0x08`) — //! `ROADMAP.md` Milestone 4 ("Basic Endpoint Types"), fifth checklist @@ -155,7 +155,7 @@ use crate::regmap::{EndpointType, PerEpTypeFunctionalConfig}; /// for why both fields are unconfirmed-width/units `u32` values rather than /// a specific bit width or physical unit. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-PWM-001 +//fusa:req REQ-PWM-001 pub struct PwmDurationPair { /// The full PWM cycle length. pub period: u32, @@ -173,7 +173,7 @@ pub struct PwmDurationPair { /// two endpoint types" for why it holds [`PwmDurationPair`] as a field /// rather than PWM_OUT/PWM_IN sharing one functional-config type outright. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-PWM-002 +//fusa:req REQ-PWM-002 pub struct PwmOutFunctionalConfig { /// The period+active-duration pair this endpoint drives onto the wire. pub target: PwmDurationPair, @@ -187,7 +187,7 @@ impl PwmOutFunctionalConfig { /// This module does not itself call that function — it only shows how a /// caller would obtain the matching tag, per /// [`crate::uart::UartFunctionalConfig::layer_tag`]'s own precedent. - // fusa:req REQ-PWM-003 + //fusa:req REQ-PWM-003 pub fn layer_tag(&self) -> PerEpTypeFunctionalConfig { PerEpTypeFunctionalConfig::new(EndpointType::PwmOut) } @@ -204,7 +204,7 @@ impl PwmOutFunctionalConfig { /// not a stored last-measured [`PwmDurationPair`] — and for why a zero /// threshold is not treated as "disabled." #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-PWM-004 +//fusa:req REQ-PWM-004 pub struct PwmInFunctionalConfig { /// How much time may elapse since the last observed signal edge before /// a read resolves to [`PwmInReadResolution::NoSignal`] instead of a @@ -216,7 +216,7 @@ impl PwmInFunctionalConfig { /// The [`crate::regmap::PerEpTypeFunctionalConfig`] generic-layer tag /// that matches this PWM_IN functional config, for use with /// [`crate::regmap::check_functional_config_matches_ep_type`]. - // fusa:req REQ-PWM-005 + //fusa:req REQ-PWM-005 pub fn layer_tag(&self) -> PerEpTypeFunctionalConfig { PerEpTypeFunctionalConfig::new(EndpointType::PwmIn) } @@ -232,8 +232,8 @@ impl PwmInFunctionalConfig { /// hanging, and stale data" for why this is a resolved measurement outcome /// rather than a [`crate::RcpError`] variant. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// fusa:req REQ-PWM-006 -// fusa:req REQ-PWM-007 +//fusa:req REQ-PWM-006 +//fusa:req REQ-PWM-007 pub enum PwmInReadResolution { /// A signal edge was observed within the configured timeout; the inner /// value is the measured period+active-duration pair. @@ -259,9 +259,10 @@ pub enum PwmInReadResolution { /// `Some(PwmInReadResolution::NoSignal)`, regardless of `last_measured` — a /// stale measurement is never returned past the timeout. Never panics for /// any input. -// fusa:req REQ-PWM-006 -// fusa:req REQ-PWM-007 -// fusa:req REQ-PWM-008 +//fusa:req REQ-PWM-006 +//fusa:req REQ-PWM-007 +//fusa:req REQ-PWM-008 +//fusa:req REQ-PWMI-003 pub fn resolve_pwm_in_read( config: &PwmInFunctionalConfig, last_measured: Option, @@ -282,7 +283,7 @@ mod tests { // ── PwmDurationPair ─────────────────────────────────────────────────── #[test] - // fusa:test REQ-PWM-001 + //fusa:test REQ-PWM-001 fn pwm_duration_pair_default_is_zeroed() { let pair = PwmDurationPair::default(); assert_eq!(pair.period, 0); @@ -290,7 +291,7 @@ mod tests { } #[test] - // fusa:test REQ-PWM-001 + //fusa:test REQ-PWM-001 fn pwm_duration_pair_fields_are_independently_settable() { let pair = PwmDurationPair { period: 1000, @@ -303,14 +304,14 @@ mod tests { // ── PwmOutFunctionalConfig / layer_tag ─────────────────────────────── #[test] - // fusa:test REQ-PWM-002 + //fusa:test REQ-PWM-002 fn pwm_out_functional_config_default_target_is_zeroed_pair() { let config = PwmOutFunctionalConfig::default(); assert_eq!(config.target, PwmDurationPair::default()); } #[test] - // fusa:test REQ-PWM-003 + //fusa:test REQ-PWM-003 fn pwm_out_functional_config_layer_tag_matches_ep_type_pwm_out() { let functional = PwmOutFunctionalConfig::default(); let generic = crate::regmap::PerEpConfigBlock::new(EndpointType::PwmOut); @@ -326,7 +327,7 @@ mod tests { } #[test] - // fusa:test REQ-PWM-003 + //fusa:test REQ-PWM-003 fn pwm_out_functional_config_layer_tag_rejects_mismatched_ep_type() { let functional = PwmOutFunctionalConfig::default(); // In particular, PWM_OUT's tag must not silently match PWM_IN's @@ -342,14 +343,14 @@ mod tests { // ── PwmInFunctionalConfig / layer_tag ──────────────────────────────── #[test] - // fusa:test REQ-PWM-004 + //fusa:test REQ-PWM-004 fn pwm_in_functional_config_default_timeout_is_zero() { let config = PwmInFunctionalConfig::default(); assert_eq!(config.no_signal_timeout, 0); } #[test] - // fusa:test REQ-PWM-005 + //fusa:test REQ-PWM-005 fn pwm_in_functional_config_layer_tag_matches_ep_type_pwm_in() { let functional = PwmInFunctionalConfig::default(); let generic = crate::regmap::PerEpConfigBlock::new(EndpointType::PwmIn); @@ -365,7 +366,7 @@ mod tests { } #[test] - // fusa:test REQ-PWM-005 + //fusa:test REQ-PWM-005 fn pwm_in_functional_config_layer_tag_rejects_mismatched_ep_type() { let functional = PwmInFunctionalConfig::default(); let generic = crate::regmap::PerEpConfigBlock::new(EndpointType::PwmOut); @@ -378,7 +379,7 @@ mod tests { // ── resolve_pwm_in_read: PWM_IN_NO_SIGNAL vs. measured vs. in-progress ── #[test] - // fusa:test REQ-PWM-006 + //fusa:test REQ-PWM-006 fn resolve_pwm_in_read_reports_measured_within_timeout() { let config = PwmInFunctionalConfig { no_signal_timeout: 1000, @@ -398,7 +399,7 @@ mod tests { } #[test] - // fusa:test REQ-PWM-007 + //fusa:test REQ-PWM-007 fn resolve_pwm_in_read_reports_no_signal_once_timed_out() { let config = PwmInFunctionalConfig { no_signal_timeout: 1000, @@ -414,7 +415,7 @@ mod tests { } #[test] - // fusa:test REQ-PWM-007 + //fusa:test REQ-PWM-007 fn resolve_pwm_in_read_never_returns_stale_measurement_past_timeout() { // A prior measurement exists, but the timeout has since elapsed — // the checklist's "instead of ... returning stale data" case. @@ -436,7 +437,7 @@ mod tests { } #[test] - // fusa:test REQ-PWM-007 + //fusa:test REQ-PWM-007 fn resolve_pwm_in_read_zeroed_config_resolves_no_signal_immediately() { // See this module's doc comment: zero is not treated as a // "disabled" sentinel for the timeout, mirroring @@ -450,7 +451,7 @@ mod tests { } #[test] - // fusa:test REQ-PWM-008 + //fusa:test REQ-PWM-008 fn resolve_pwm_in_read_returns_none_while_awaiting_first_edge_before_timeout() { // Genuinely in progress: no measurement yet, timeout not yet // elapsed — this is the "not a hang" case, resolved by returning @@ -463,7 +464,40 @@ mod tests { } #[test] - // fusa:test REQ-PWM-009 + //fusa:test REQ-PWMI-003 + fn resolve_pwm_in_read_invalidates_measurement_once_max_period_is_exceeded() { + // TC18 §13.7.6.2 Table 45, pwmi_err_on_max_period = 0b (TC18.txt + // line 4721): "if MAX PERIOD is exceeded, invalidate measurement and + // wait for new active phase of signal". pwmi_max_period (Table 45, + // relative address 0x000A, TC18.txt line 4735) is a 16-bit register, + // so 0xFFFF is the largest MAX PERIOD a conforming RC Server can be + // configured with. + let config = PwmInFunctionalConfig { + no_signal_timeout: 0xFFFF, + }; + // TC18 §13.7.6.3 (TC18.txt line 4758): both measured values are + // 16-bit, so a valid measurement fits 0x0000..=0xFFFF. + let measured = PwmDurationPair { + period: 0x8000, + active_duration: 0x4000, + }; + // One PWM_CLK cycle below MAX PERIOD the measurement is still valid. + assert_eq!( + resolve_pwm_in_read(&config, Some(measured), 0xFFFE), + Some(PwmInReadResolution::Measured(measured)) + ); + // At and beyond MAX PERIOD the measurement is invalid and must never + // be re-reported. + for elapsed in [0xFFFFu32, 0x1_0000] { + assert_eq!( + resolve_pwm_in_read(&config, Some(measured), elapsed), + Some(PwmInReadResolution::NoSignal) + ); + } + } + + #[test] + //fusa:test REQ-PWM-009 fn resolve_pwm_in_read_never_panics_for_any_sampled_input() { let configs = [ PwmInFunctionalConfig { diff --git a/src/ratelimit.rs b/src/ratelimit.rs index 32d2723..5b0b61f 100644 --- a/src/ratelimit.rs +++ b/src/ratelimit.rs @@ -1,10 +1,10 @@ -// fusa:req REQ-RL-001 -// fusa:req REQ-RL-002 -// fusa:req REQ-RL-003 -// fusa:req REQ-RL-004 -// fusa:req REQ-RL-005 -// fusa:req REQ-RL-006 -// fusa:req REQ-RL-008 +//fusa:req REQ-RL-001 +//fusa:req REQ-RL-002 +//fusa:req REQ-RL-003 +//fusa:req REQ-RL-004 +//fusa:req REQ-RL-005 +//fusa:req REQ-RL-006 +//fusa:req REQ-RL-008 //! Token-bucket rate limiter endpoint decorator. //! @@ -38,7 +38,7 @@ use crate::RcpError; // ── Config ──────────────────────────────────────────────────────────────────── /// Token-bucket configuration. -// fusa:req REQ-RL-001 +//fusa:req REQ-RL-001 #[derive(Clone, Debug)] pub struct Config { /// Sustained request rate (calls per second). @@ -48,7 +48,7 @@ pub struct Config { } /// Returns the default rate-limiter config: 100 calls/s, 20-call burst. -// fusa:req REQ-RL-002 +//fusa:req REQ-RL-002 pub fn default_config() -> Config { Config { rate: 100.0, @@ -95,7 +95,7 @@ impl Bucket { // ── RateLimitEndpoint ──────────────────────────────────────────────────────── /// Rate-limiting wrapper around an inner [`Endpoint`]. -// fusa:req REQ-RL-003 +//fusa:req REQ-RL-003 pub struct RateLimitEndpoint { inner: Arc, bucket: Mutex, @@ -103,7 +103,7 @@ pub struct RateLimitEndpoint { impl RateLimitEndpoint { /// Create a new `RateLimitEndpoint` with the given configuration. - // fusa:req REQ-RL-004 + //fusa:req REQ-RL-004 pub fn new(inner: Arc, cfg: Config) -> Self { RateLimitEndpoint { inner, @@ -132,15 +132,15 @@ impl Endpoint for RateLimitEndpoint { self.inner.ep_type() } - // fusa:req REQ-RL-005 - // fusa:req REQ-RL-006 + //fusa:req REQ-RL-005 + //fusa:req REQ-RL-006 fn read(&self, read_size: u16) -> Result, RcpError> { self.consume()?; self.inner.read(read_size) } - // fusa:req REQ-RL-005 - // fusa:req REQ-RL-006 + //fusa:req REQ-RL-005 + //fusa:req REQ-RL-006 fn write(&self, payload: &[u8]) -> Result<(), RcpError> { self.consume()?; self.inner.write(payload) @@ -168,7 +168,7 @@ mod tests { // ── Default config ──────────────────────────────────────────────────────── #[test] - // fusa:test REQ-RL-002 + //fusa:test REQ-RL-002 fn default_config_values() { let cfg = default_config(); assert_eq!(cfg.rate, 100.0); @@ -178,8 +178,8 @@ mod tests { // ── Burst allowed ───────────────────────────────────────────────────────── #[test] - // fusa:test REQ-RL-001 - // fusa:test REQ-RL-005 + //fusa:test REQ-RL-001 + //fusa:test REQ-RL-005 fn burst_capacity_is_honoured() { let rl = rl(1.0, 5.0); // 1 call/s, burst=5 for _ in 0..5 { @@ -193,7 +193,7 @@ mod tests { // ── Empty bucket returns Busy ───────────────────────────────────────────── #[test] - // fusa:test REQ-RL-006 + //fusa:test REQ-RL-006 fn bucket_exhaustion_returns_busy() { let rl = rl(0.0, 0.0); // zero tokens — always Busy let err = rl.write(b"x").unwrap_err(); @@ -201,7 +201,7 @@ mod tests { } #[test] - // fusa:test REQ-RL-006 + //fusa:test REQ-RL-006 fn burst_exhaustion_across_multiple_calls_returns_busy() { let rl = rl(0.0, 3.0); // 3 burst, no refill for _ in 0..3 { @@ -214,7 +214,7 @@ mod tests { // ── Read and write both obey bucket ─────────────────────────────────────── #[test] - // fusa:test REQ-RL-005 + //fusa:test REQ-RL-005 fn read_obeys_bucket() { let rl = rl(0.0, 0.0); let err = rl.read(1).unwrap_err(); @@ -222,7 +222,7 @@ mod tests { } #[test] - // fusa:test REQ-RL-005 + //fusa:test REQ-RL-005 fn write_obeys_bucket() { let rl = rl(0.0, 0.0); let err = rl.write(b"x").unwrap_err(); @@ -232,7 +232,7 @@ mod tests { // ── ep_type forwarded ──────────────────────────────────────────────────── #[test] - // fusa:test REQ-RL-003 + //fusa:test REQ-RL-003 fn ep_type_matches_inner() { let inner = MockEndpoint::new(EndpointType::Adc, vec![]) as Arc; let rl = RateLimitEndpoint::new_default(inner); @@ -242,7 +242,7 @@ mod tests { // ── Token replenishment ─────────────────────────────────────────────────── #[test] - // fusa:test REQ-RL-004 + //fusa:test REQ-RL-004 fn tokens_replenish_over_time() { let rl = rl(1000.0, 1.0); // very fast replenishment, burst=1 rl.write(b"x").unwrap(); // consume the one token @@ -254,7 +254,7 @@ mod tests { // ── Busy is a relay timeout sentinel ───────────────────────────────────── #[test] - // fusa:test REQ-RL-008 + //fusa:test REQ-RL-008 fn busy_is_relay_timeout_sentinel() { let err = RcpError::Busy; assert!( diff --git a/src/record.rs b/src/record.rs index 5d00090..e7f7dc3 100644 --- a/src/record.rs +++ b/src/record.rs @@ -1,8 +1,8 @@ -// fusa:req REQ-REC-001 -// fusa:req REQ-REC-002 -// fusa:req REQ-REC-003 -// fusa:req REQ-REC-004 -// fusa:req REQ-REC-005 +//fusa:req REQ-REC-001 +//fusa:req REQ-REC-002 +//fusa:req REQ-REC-003 +//fusa:req REQ-REC-004 +//fusa:req REQ-REC-005 //! Call recorder for replay, audit trails, and regression testing, wrapping //! an [`Endpoint`]. @@ -29,7 +29,7 @@ use crate::RcpError; // ── Record entry ────────────────────────────────────────────────────────────── /// A single recorded interaction. -// fusa:req REQ-REC-001 +//fusa:req REQ-REC-001 #[derive(Clone, Debug)] pub enum Entry { Read { @@ -65,7 +65,7 @@ impl Entry { // ── RecordEndpoint ───────────────────────────────────────────────────────────── /// Endpoint wrapper that records every read/write call and its result. -// fusa:req REQ-REC-002 +//fusa:req REQ-REC-002 pub struct RecordEndpoint { inner: Arc, log: Mutex>, @@ -80,13 +80,13 @@ impl RecordEndpoint { } /// All recorded entries in chronological order. - // fusa:req REQ-REC-003 + //fusa:req REQ-REC-003 pub fn entries(&self) -> Vec { self.log.lock().unwrap().clone() } /// Clear the recorded log. - // fusa:req REQ-REC-004 + //fusa:req REQ-REC-004 pub fn clear(&self) { self.log.lock().unwrap().clear(); } @@ -97,7 +97,7 @@ impl Endpoint for RecordEndpoint { self.inner.ep_type() } - // fusa:req REQ-REC-005 + //fusa:req REQ-REC-005 fn read(&self, read_size: u16) -> Result, RcpError> { let result = self.inner.read(read_size); self.log.lock().unwrap().push(Entry::Read { @@ -108,7 +108,7 @@ impl Endpoint for RecordEndpoint { result } - // fusa:req REQ-REC-005 + //fusa:req REQ-REC-005 fn write(&self, payload: &[u8]) -> Result<(), RcpError> { let result = self.inner.write(payload); self.log.lock().unwrap().push(Entry::Write { @@ -135,8 +135,8 @@ mod tests { } #[test] - // fusa:test REQ-REC-002 - // fusa:test REQ-REC-005 + //fusa:test REQ-REC-002 + //fusa:test REQ-REC-005 fn records_successful_writes() { let r = rec(); for i in 1u8..=3 { @@ -151,7 +151,7 @@ mod tests { } #[test] - // fusa:test REQ-REC-005 + //fusa:test REQ-REC-005 fn records_errors() { struct AlwaysFail; impl Endpoint for AlwaysFail { @@ -173,7 +173,7 @@ mod tests { } #[test] - // fusa:test REQ-REC-004 + //fusa:test REQ-REC-004 fn clear_empties_log() { let r = rec(); r.write(b"x").unwrap(); @@ -182,7 +182,7 @@ mod tests { } #[test] - // fusa:test REQ-REC-001 + //fusa:test REQ-REC-001 fn entry_timestamp_is_recent() { let r = rec(); r.write(b"x").unwrap(); @@ -192,7 +192,7 @@ mod tests { } #[test] - // fusa:test REQ-REC-003 + //fusa:test REQ-REC-003 fn entries_in_order() { let r = rec(); for i in 1u8..=5 { diff --git a/src/redundancy.rs b/src/redundancy.rs index 430aba6..aeaa8f7 100644 --- a/src/redundancy.rs +++ b/src/redundancy.rs @@ -1,11 +1,11 @@ -// fusa:req REQ-RED-001 -// fusa:req REQ-RED-002 -// fusa:req REQ-RED-003 -// fusa:req REQ-RED-004 -// fusa:req REQ-RED-005 -// fusa:req REQ-RED-006 -// fusa:req REQ-RED-007 -// fusa:req REQ-RED-008 +//fusa:req REQ-RED-001 +//fusa:req REQ-RED-002 +//fusa:req REQ-RED-003 +//fusa:req REQ-RED-004 +//fusa:req REQ-RED-005 +//fusa:req REQ-RED-006 +//fusa:req REQ-RED-007 +//fusa:req REQ-RED-008 //! Redundant endpoint pair with automatic failover (1-of-2 hot standby). //! @@ -39,7 +39,7 @@ struct Inner { } /// Hot-standby redundant endpoint. -// fusa:req REQ-RED-001 +//fusa:req REQ-RED-001 pub struct RedundancyEndpoint { ep_type: EndpointType, state: Mutex, @@ -47,7 +47,7 @@ pub struct RedundancyEndpoint { impl RedundancyEndpoint { /// Create with a primary and a secondary endpoint. - // fusa:req REQ-RED-002 + //fusa:req REQ-RED-002 pub fn new(primary: Arc, secondary: Arc) -> Self { let ep_type = primary.ep_type(); RedundancyEndpoint { @@ -61,13 +61,13 @@ impl RedundancyEndpoint { } /// Number of times failover has occurred. - // fusa:req REQ-RED-006 + //fusa:req REQ-RED-006 pub fn failover_count(&self) -> u32 { self.state.lock().unwrap().failovers } /// True if a secondary is still available. - // fusa:req REQ-RED-007 + //fusa:req REQ-RED-007 pub fn has_secondary(&self) -> bool { self.state.lock().unwrap().secondary.is_some() } @@ -109,14 +109,14 @@ impl Endpoint for RedundancyEndpoint { self.ep_type } - // fusa:req REQ-RED-003 - // fusa:req REQ-RED-004 - // fusa:req REQ-RED-005 + //fusa:req REQ-RED-003 + //fusa:req REQ-RED-004 + //fusa:req REQ-RED-005 fn read(&self, read_size: u16) -> Result, RcpError> { self.dispatch(|ep| ep.read(read_size)) } - // fusa:req REQ-RED-008 + //fusa:req REQ-RED-008 fn write(&self, payload: &[u8]) -> Result<(), RcpError> { self.dispatch(|ep| ep.write(payload)) } @@ -152,8 +152,8 @@ mod tests { } #[test] - // fusa:test REQ-RED-001 - // fusa:test REQ-RED-003 + //fusa:test REQ-RED-001 + //fusa:test REQ-RED-003 fn primary_success_no_failover() { let r = RedundancyEndpoint::new(ok_ep(EndpointType::Gpio), ok_ep(EndpointType::Gpio)); r.write(b"x").unwrap(); @@ -161,8 +161,8 @@ mod tests { } #[test] - // fusa:test REQ-RED-004 - // fusa:test REQ-RED-005 + //fusa:test REQ-RED-004 + //fusa:test REQ-RED-005 fn primary_failure_triggers_failover_to_secondary() { let r = RedundancyEndpoint::new(failing_ep(), ok_ep(EndpointType::Gpio)); r.write(b"x").unwrap(); @@ -170,7 +170,7 @@ mod tests { } #[test] - // fusa:test REQ-RED-006 + //fusa:test REQ-RED-006 fn failover_count_increments() { let r = RedundancyEndpoint::new(failing_ep(), ok_ep(EndpointType::Gpio)); r.write(b"x").unwrap(); // triggers failover @@ -180,7 +180,7 @@ mod tests { } #[test] - // fusa:test REQ-RED-007 + //fusa:test REQ-RED-007 fn no_secondary_after_failover() { let r = RedundancyEndpoint::new(failing_ep(), ok_ep(EndpointType::Gpio)); assert!(r.has_secondary()); @@ -189,7 +189,7 @@ mod tests { } #[test] - // fusa:test REQ-RED-005 + //fusa:test REQ-RED-005 fn both_failed_returns_error() { let r = RedundancyEndpoint::new(failing_ep(), failing_ep()); let err = r.write(b"x").unwrap_err(); @@ -197,14 +197,14 @@ mod tests { } #[test] - // fusa:test REQ-RED-002 + //fusa:test REQ-RED-002 fn ep_type_matches_primary() { let r = RedundancyEndpoint::new(ok_ep(EndpointType::Adc), ok_ep(EndpointType::Adc)); assert_eq!(r.ep_type(), EndpointType::Adc); } #[test] - // fusa:test REQ-RED-008 + //fusa:test REQ-RED-008 fn read_forwarded_to_primary() { let r = RedundancyEndpoint::new(ok_ep(EndpointType::Gpio), ok_ep(EndpointType::Gpio)); r.read(4).unwrap(); diff --git a/src/regmap.rs b/src/regmap.rs index 02daf57..93e18d4 100644 --- a/src/regmap.rs +++ b/src/regmap.rs @@ -1,33 +1,33 @@ -// fusa:req REQ-RMAP-001 -// fusa:req REQ-RMAP-002 -// fusa:req REQ-RMAP-003 -// fusa:req REQ-RMAP-004 -// fusa:req REQ-RMAP-005 -// fusa:req REQ-RMAP-006 -// fusa:req REQ-RMAP-007 -// fusa:req REQ-RMAP-008 -// fusa:req REQ-RMAP-009 -// fusa:req REQ-RMAP-010 -// fusa:req REQ-RMAP-011 -// fusa:req REQ-RMAP-012 -// fusa:req REQ-RMAP-013 -// fusa:req REQ-RMAP-014 -// fusa:req REQ-RMAP-015 -// fusa:req REQ-RMAP-016 -// fusa:req REQ-RMAP-017 -// fusa:req REQ-RMAP-018 -// fusa:req REQ-RMAP-019 -// fusa:req REQ-RMAP-020 -// fusa:req REQ-RMAP-021 -// fusa:req REQ-RMAP-022 -// fusa:req REQ-RMAP-023 -// fusa:req REQ-RMAP-024 -// fusa:req REQ-RMAP-025 -// fusa:req REQ-RMAP-026 -// fusa:req REQ-RMAP-027 -// fusa:req REQ-RMAP-028 -// fusa:req REQ-RMAP-029 -// fusa:req REQ-RMAP-030 +//fusa:req REQ-RMAP-001 +//fusa:req REQ-RMAP-002 +//fusa:req REQ-RMAP-003 +//fusa:req REQ-RMAP-004 +//fusa:req REQ-RMAP-005 +//fusa:req REQ-RMAP-006 +//fusa:req REQ-RMAP-007 +//fusa:req REQ-RMAP-008 +//fusa:req REQ-RMAP-009 +//fusa:req REQ-RMAP-010 +//fusa:req REQ-RMAP-011 +//fusa:req REQ-RMAP-012 +//fusa:req REQ-RMAP-013 +//fusa:req REQ-RMAP-014 +//fusa:req REQ-RMAP-015 +//fusa:req REQ-RMAP-016 +//fusa:req REQ-RMAP-017 +//fusa:req REQ-RMAP-018 +//fusa:req REQ-RMAP-019 +//fusa:req REQ-RMAP-020 +//fusa:req REQ-RMAP-021 +//fusa:req REQ-RMAP-022 +//fusa:req REQ-RMAP-023 +//fusa:req REQ-RMAP-024 +//fusa:req REQ-RMAP-025 +//fusa:req REQ-RMAP-026 +//fusa:req REQ-RMAP-027 +//fusa:req REQ-RMAP-028 +//fusa:req REQ-RMAP-029 +//fusa:req REQ-RMAP-030 //! Three-layer per-endpoint config taxonomy, the RC Server's general //! (whole-server) register-map fields, and the five child config tables @@ -275,6 +275,27 @@ //! layout (byte offsets, alignment, padding) must be reconciled against //! this guess (never against spec prose) before this encode/decode form //! is relied on for interop. +//! +//! **Known divergence (recorded, not fixed).** TC18 v0.5.1_RC §12.7.5 +//! Table 18 "RC Server configuration static part" (pp.51-53) *does* +//! carry an "Absolute address" column, and this sequential packing does +//! not reproduce it: Table 18 has an 8-bit `reserved` at `0x0017` (so +//! `svr_io_pin_count` sits at `0x0018`) and a 16-bit `reserved` at +//! `0x0022`; `svr_request_stream_cfg_capacity`/ +//! `svr_response_stream_cfg_capacity` are 8-bit and sit at +//! `0x001C`/`0x001D`, *before* their 16-bit pointers at `0x001E`/ +//! `0x0020`; `svr_hw_cfg_ptr` (`0x001A`) has no paired capacity row at +//! all; `svr_ep_generic_cfg_capacity` is 16-bit at `0x0026`; and +//! `svr_ep_bytebus_id_map_capacity` is 8-bit at `0x002A`. The +//! [`TableDescriptor`] shape (adjacent 16-bit ptr+capacity) therefore +//! matches none of those rows, and [`GeneralRegisters::ENCODED_LEN`] is +//! 65 bytes where Table 18 spans `0x0000`-`0x003F`, i.e. 64 (its final +//! eight 16-bit rows print no address cell, but the table's own +//! page-break continuation marker gives the next address as `0x0030`). +//! This is +//! recorded as an explicit not-implemented requirement rather than +//! silently reshaped here; see `REQ-RMAP-040` through `REQ-RMAP-045` in +//! `.fusa-reqs.json`. //! - **`svr_implemented_options` mostly left undecomposed.** The extraction //! names five option bundles the bitmask covers (compound&wait / //! triggered / chained / time-sync&timed / enhanced-cancel) but no @@ -290,8 +311,15 @@ //! this crate assigns the five bundles to bits `0`-`4` in the same //! top-to-bottom order the extraction itself lists them (compound&wait = //! bit `0`, triggered = bit `1`, chained = bit `2`, time-sync&timed = bit -//! `3`, enhanced-cancel = bit `4`) — a crate-local placeholder ordering, -//! not a confirmed bit-position assignment, and reconciled against a real +//! `3`, enhanced-cancel = bit `4`). TC18 §12.7.5 Table 18 writes the +//! `svr_implemented_options` byte as `abcdefgh` with `a` = compound & +//! wait requests, `b` = trigger requests, `c` = chained requests, `d` = +//! time synch and timed requests, `e` = enhanced request cancellation — +//! a left-to-right naming whose bit-numbering direction the extracted +//! text does not make unambiguous (`a` may be bit 7 or bit 0). This +//! crate's assignment therefore remains a crate-local placeholder +//! ordering, not a confirmed bit-position assignment (see +//! `REQ-RMAP-041`), and reconciled against a real //! RC Server (never against spec prose) before being relied on for //! interop, the same caveat as the sequential-byte-packing inference //! above. [`GeneralRegisters::claims_compound_wait_bundle`] is the one @@ -406,10 +434,12 @@ //! reserved quadlets get. A row therefore round-trips its *specified* //! fields, not arbitrary reserved content. //! -//! [`ResponseStreamConfigEntry`]'s own layout has **not** been reconciled -//! against a spec table in this pass and remains this crate's own working -//! inference; it must be checked against §12.7.9 before being relied on for -//! interop. +//! [`ResponseStreamConfigEntry`]'s own layout has since been reconciled +//! against TC18 §12.7.9 Table 24 "Responder QUEUE_config" (p.60) and +//! matches it exactly, field-for-field and stride-for-stride — see that +//! type's own doc comment. What Table 24 additionally states, and this +//! crate does **not** yet model, is that `STREAM_UID`'s 16 bits are the +//! stream identifier's bits `[63:48]` (see `REQ-RMAP-037`). //! //! ## `Serialize`/`Deserialize` derives (`ROADMAP.md` Milestone 9, `config` //! ## REPLACE cutover) @@ -446,7 +476,7 @@ use crate::RcpError; #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] #[non_exhaustive] -// fusa:req REQ-RMAP-001 +//fusa:req REQ-RMAP-001 pub enum EndpointType { /// `ep_type 0x01`. Wakeup control (`ROADMAP.md` Milestone 7). Wakeup = 0x01, @@ -498,17 +528,29 @@ pub enum EndpointType { impl EndpointType { /// Encode this endpoint type as its wire-level `ep_type` byte value. - // fusa:req REQ-RMAP-001 + /// + /// Every code produced here is the one TC18 v0.5.1_RC §13.2 Table 29 + /// "ep_type values" (p.73) assigns to the same endpoint type. + //fusa:req REQ-RMAP-001 + //fusa:req REQ-EPGEN-001 pub fn to_u8(self) -> u8 { self as u8 } /// Decode a wire-level `ep_type` byte value into an [`EndpointType`]. /// - /// Returns `Err(RcpError::Other(_))` for any byte outside `0x01..=0x0D`, - /// mirroring [`crate::lifecycle::RcServerState::from_u8`]'s handling of - /// an unrecognized state byte. Never panics for any input. - // fusa:req REQ-RMAP-002 + /// Accepts exactly TC18 §13.2 Table 29's codes `0x01` (WakeUp Ctrl) + /// through `0x0D` (MDIO). Returns `Err(RcpError::Other(_))` for any + /// byte outside that range, mirroring + /// [`crate::lifecycle::RcServerState::from_u8`]'s handling of an + /// unrecognized state byte. Never panics for any input. + /// + /// Table 29 additionally assigns `0x00` to "Server"; this crate does + /// not model it, so an `ep_generic_config` row carrying `ep_type + /// 0x00` is rejected here rather than decoded — recorded as + /// `REQ-EPGEN-002`, not resolved. + //fusa:req REQ-RMAP-002 + //fusa:req REQ-EPGEN-001 pub fn from_u8(raw: u8) -> Result { match raw { 0x01 => Ok(Self::Wakeup), @@ -535,7 +577,7 @@ impl EndpointType { /// /// True only for [`EndpointType::Dac`] — see this module's doc comment. /// Never panics for any input. - // fusa:req REQ-RMAP-001 + //fusa:req REQ-RMAP-001 pub fn is_reserved(self) -> bool { matches!(self, Self::Dac) } @@ -571,7 +613,7 @@ impl EndpointType { /// would silently narrow an existing, already-tested requirement instead of /// adding a new one. Callers that need both checks call this function /// alongside [`check_functional_config_matches_ep_type`], not instead of it. -// fusa:req REQ-RMAP-031 +//fusa:req REQ-RMAP-031 pub fn check_ep_type_supported(ep_type: EndpointType) -> Result<(), RcpError> { if ep_type.is_reserved() { Err(RcpError::UnsupportedCmd) @@ -588,7 +630,7 @@ pub fn check_ep_type_supported(ep_type: EndpointType) -> Result<(), RcpError> { /// See this module's doc comment for why `ep_type` is the only field this /// item gives it. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// fusa:req REQ-RMAP-003 +//fusa:req REQ-RMAP-003 pub struct PerEpConfigBlock { /// This endpoint's register-map type discriminant. pub ep_type: EndpointType, @@ -601,7 +643,7 @@ impl PerEpConfigBlock { } /// The [`ConfigLayer`] this type always belongs to. - // fusa:req REQ-RMAP-003 + //fusa:req REQ-RMAP-003 pub const LAYER: ConfigLayer = ConfigLayer::Generic; } @@ -625,17 +667,33 @@ impl PerEpConfigBlock { /// extend this struct then, not now. /// /// [`CommonFunctionalConfig::encode`]/[`CommonFunctionalConfig::decode`] -/// give this block a never-panicking, fixed-length wire form. Unlike -/// [`crate::acf::ByteMessageInfo`]'s flags (whose bit positions within a -/// shared byte `ROADMAP.md` states explicitly), this crate has no basis -/// for any bit-position assignment among these three fields, so — mirroring -/// [`RequestStreamConfigEntry`]'s own "meaningfully binary but -/// wire-width-unconfirmed" precedent for its eight on/off behaviors — each -/// field is given its own full byte (`0x00`/`0x01`) rather than a guessed -/// shared-byte bit-packing. +/// give this block a never-panicking, fixed-length wire form: each field +/// gets its own full byte (`0x00`/`0x01`), for a 3-byte block. +/// +/// **Known divergence (recorded, not fixed).** Earlier revisions of this +/// doc comment justified that one-byte-per-field shape by asserting the +/// crate had "no basis for any bit-position assignment among these three +/// fields". That is no longer true: TC18 v0.5.1_RC §13.7 Table 32 "EP +/// functional config common entries" (p.80) supplies exactly that basis, +/// and this encoding does not match it. Table 32 defines **two** +/// bit-addressed registers — `ep_enable&clr` at relative address `0x0002` +/// (`.0` `ep_enable`, `.1:3` reserved reading `000b`, `.4` +/// `ep_clear_req_storage`) and `ep_options` at `0x0003` (`.0` +/// `ep_req_crc_enable`, `.1` `ep_ack_crc_enable`, `.2` +/// `ep_response_crc_enable`, `.3` `ep_response_ts_enable`, `.4` +/// `ep_error_stream`, `.5` `ep_ack_ts_enable`, `.6` +/// `ep_supress_error_msgs`, `.7` `ep_supress_response`) — so the three +/// fields modeled here belong at three specific *bits* of two bytes, not +/// at three whole bytes, and the seven remaining `ep_options` bits are +/// unmodeled entirely. Table 32 further documents `ep_enable`'s power-on +/// default as `1b` (this type's derived `Default` gives `false`) and +/// `ep_clear_req_storage` as write-1-to-clear, "reads always 0" (this type +/// round-trips it). Recorded as explicit not-implemented requirements +/// rather than silently reshaped here; see `REQ-RMAP-051` and +/// `REQ-RMAP-052` in `.fusa-reqs.json`. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-RMAP-003 -// fusa:req REQ-RMAP-028 +//fusa:req REQ-RMAP-003 +//fusa:req REQ-RMAP-028 pub struct CommonFunctionalConfig { /// Whether this endpoint is enabled. pub ep_enable: bool, @@ -647,7 +705,7 @@ pub struct CommonFunctionalConfig { impl CommonFunctionalConfig { /// The [`ConfigLayer`] this type always belongs to. - // fusa:req REQ-RMAP-003 + //fusa:req REQ-RMAP-003 pub const LAYER: ConfigLayer = ConfigLayer::CommonFunctional; /// Encoded wire length in bytes: one full byte per field, per this @@ -657,7 +715,7 @@ impl CommonFunctionalConfig { /// Encode this config to its 3-byte wire representation: /// `ep_enable` then `ep_clear_req_storage` then `ep_req_crc_enable`, /// each a full `0x00`/`0x01` byte. Never panics. - // fusa:req REQ-RMAP-028 + //fusa:req REQ-RMAP-028 pub fn encode(&self) -> [u8; Self::ENCODED_LEN] { [ self.ep_enable as u8, @@ -673,7 +731,7 @@ impl CommonFunctionalConfig { /// [`CommonFunctionalConfig::ENCODED_LEN`] instead. Any nonzero byte /// decodes as `true`, matching [`crate::acf::decode_byte_message_info`]'s /// own bit-to-`bool` convention of treating "nonzero" as set. - // fusa:req REQ-RMAP-029 + //fusa:req REQ-RMAP-029 pub fn decode(b: &[u8]) -> Result { if b.len() < Self::ENCODED_LEN { return Err(RcpError::ShortFrame); @@ -693,7 +751,7 @@ impl CommonFunctionalConfig { /// doc comment for why no concrete per-type field (GPIO's write-semantics, /// SPI's channel configs, etc.) is modeled here yet. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// fusa:req REQ-RMAP-003 +//fusa:req REQ-RMAP-003 pub struct PerEpTypeFunctionalConfig { /// The [`EndpointType`] this functional-config shape is for. pub ep_type: EndpointType, @@ -708,7 +766,7 @@ impl PerEpTypeFunctionalConfig { /// The [`ConfigLayer`] this value belongs to, tagged with its /// [`EndpointType`]. - // fusa:req REQ-RMAP-003 + //fusa:req REQ-RMAP-003 pub fn layer(&self) -> ConfigLayer { ConfigLayer::PerTypeFunctional(self.ep_type) } @@ -722,7 +780,7 @@ impl PerEpTypeFunctionalConfig { /// `crate::lifecycle::RegisterCategory`" section for how this differs from, /// and composes with, [`crate::lifecycle::RegisterCategory`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// fusa:req REQ-RMAP-005 +//fusa:req REQ-RMAP-005 pub enum ConfigLayer { /// [`PerEpConfigBlock`]'s layer: generic, present for every endpoint. Generic, @@ -741,7 +799,7 @@ pub enum ConfigLayer { /// See this module's doc comment Provenance note for the two different /// confidence levels behind this mapping's two branches. Never panics for /// any input. -// fusa:req REQ-RMAP-005 +//fusa:req REQ-RMAP-005 pub fn register_category(layer: ConfigLayer) -> RegisterCategory { match layer { ConfigLayer::Generic => RegisterCategory::HwConfig, @@ -758,7 +816,7 @@ pub fn register_category(layer: ConfigLayer) -> RegisterCategory { /// The one relationship this taxonomy's three layers already have to each /// other before any concrete field exists — see this module's doc comment. /// Never panics for any input. -// fusa:req REQ-RMAP-004 +//fusa:req REQ-RMAP-004 pub fn functional_config_matches_ep_type( generic: &PerEpConfigBlock, per_type: &PerEpTypeFunctionalConfig, @@ -771,7 +829,7 @@ pub fn functional_config_matches_ep_type( /// Returns `Ok(())` if `per_type` belongs to the same [`EndpointType`] as /// `generic`, `Err(RcpError::InvalidParameter)` otherwise. Never panics /// for any input. -// fusa:req REQ-RMAP-004 +//fusa:req REQ-RMAP-004 pub fn check_functional_config_matches_ep_type( generic: &PerEpConfigBlock, per_type: &PerEpTypeFunctionalConfig, @@ -790,7 +848,7 @@ pub fn check_functional_config_matches_ep_type( /// See this module's doc comment "`GeneralRegisters` provenance note" for /// which `§3.6` rows this shape applies to (and which don't). #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] -// fusa:req REQ-RMAP-007 +//fusa:req REQ-RMAP-007 pub struct TableDescriptor { /// Register-map address of the child table's first entry. pub ptr: u16, @@ -803,7 +861,7 @@ impl TableDescriptor { pub const ENCODED_LEN: usize = 4; /// Encode as big-endian `[ptr, capacity]`. Never panics. - // fusa:req REQ-RMAP-007 + //fusa:req REQ-RMAP-007 pub fn encode(&self) -> [u8; Self::ENCODED_LEN] { let mut buf = [0u8; Self::ENCODED_LEN]; buf[0..2].copy_from_slice(&self.ptr.to_be_bytes()); @@ -816,7 +874,7 @@ impl TableDescriptor { /// Returns `Err(RcpError::ShortFrame)` if `bytes` is shorter than /// [`Self::ENCODED_LEN`]. Trailing bytes beyond `ENCODED_LEN` are /// ignored. Never panics for any input. - // fusa:req REQ-RMAP-007 + //fusa:req REQ-RMAP-007 pub fn decode(bytes: &[u8]) -> Result { if bytes.len() < Self::ENCODED_LEN { return Err(RcpError::ShortFrame); @@ -836,7 +894,7 @@ impl TableDescriptor { /// byte-layout and bitmask inferences [`Self::encode`]/[`Self::decode`] /// depend on. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] -// fusa:req REQ-RMAP-008 +//fusa:req REQ-RMAP-008 pub struct GeneralRegisters { /// Fixed constant this crate's own decoder can use to recognize an OA /// TC18 RC Server on the wire. @@ -911,7 +969,7 @@ impl GeneralRegisters { /// /// See this module's doc comment provenance note for the sequential /// byte-packing inference this encoding depends on. - // fusa:req REQ-RMAP-009 + //fusa:req REQ-RMAP-009 pub fn encode(&self) -> [u8; Self::ENCODED_LEN] { let mut buf = [0u8; Self::ENCODED_LEN]; let mut off = 0usize; @@ -966,7 +1024,7 @@ impl GeneralRegisters { /// Returns `Err(RcpError::ShortFrame)` if `bytes` is shorter than /// [`Self::ENCODED_LEN`]. Trailing bytes beyond `ENCODED_LEN` are /// ignored. Never panics for any input. - // fusa:req REQ-RMAP-010 + //fusa:req REQ-RMAP-010 pub fn decode(bytes: &[u8]) -> Result { if bytes.len() < Self::ENCODED_LEN { return Err(RcpError::ShortFrame); @@ -1083,7 +1141,7 @@ impl GeneralRegisters { /// checklist bullet names, and this module's doc comment "provenance /// note" for why bit `0` — rather than any other position — is this /// crate's own placeholder choice for this one bundle. - // fusa:req REQ-RMAP-030 + //fusa:req REQ-RMAP-030 pub fn claims_compound_wait_bundle(&self) -> bool { self.svr_implemented_options & (1 << Self::IMPLEMENTED_OPTIONS_COMPOUND_WAIT_BIT) != 0 } @@ -1116,7 +1174,7 @@ pub trait ConfigTableRow: Sized { /// Encode every row in `rows`, back-to-back, with no padding between rows. /// Never panics. -// fusa:req REQ-RMAP-027 +//fusa:req REQ-RMAP-027 pub fn encode_rows(rows: &[T]) -> Vec { let mut buf = Vec::with_capacity(rows.len() * T::ROW_LEN); for row in rows { @@ -1131,7 +1189,7 @@ pub fn encode_rows(rows: &[T]) -> Vec { /// multiple of `T::ROW_LEN` (including a non-empty remainder shorter than /// one row). An empty input decodes to an empty table. Never panics for any /// input. -// fusa:req REQ-RMAP-027 +//fusa:req REQ-RMAP-027 pub fn decode_rows(bytes: &[u8]) -> Result, RcpError> { debug_assert!(T::ROW_LEN > 0, "ConfigTableRow::ROW_LEN must not be zero"); if T::ROW_LEN == 0 || bytes.len() % T::ROW_LEN != 0 { @@ -1148,9 +1206,24 @@ pub fn decode_rows(bytes: &[u8]) -> Result, RcpError> // ── HwPinMappingEntry (§3.7) ───────────────────────────────────────────────── -/// One row of the HW pin-mapping table (`§3.7`): which endpoint owns a -/// physical I/O pin, which of that endpoint's named signal indices is bound -/// to it, and the pin's packed electrical properties. +/// One row of the HW pin-mapping table (TC18 v0.5.1_RC §12.7.6 "HW pin +/// mapping configuration", Table 19 "HW_config", p.54): which endpoint owns +/// a physical I/O pin, which of that endpoint's named signal indices is +/// bound to it, and the pin's packed electrical properties. +/// +/// # Wire layout (Table 19) +/// +/// ```text +/// IO_Pin 1 +/// 0x0000 hw_ep_nr 8 bit R/W* "Endpoint Nr using this IO" +/// 0x0001 hw_ep_pin_nr 8 bit R/W* "Endpoint Pin Nr mapped to this IO" +/// 0x0002 hw_pin_type 8 bit R/W* "Properties of the IO Pin" +/// IO_Pin 2 +/// 0x0003 (next row) +/// ``` +/// +/// The row is 3 bytes, with the stride fixed by IO_Pin 2's own `hw_ep_nr` +/// at relative address `0x0003` (and IO_Pin 3's at `0x0006`). /// /// See this module's doc comment "Config tables" section for this table's /// row-count source ([`GeneralRegisters::svr_hw_cfg`]'s `capacity`, @@ -1158,7 +1231,7 @@ pub fn decode_rows(bytes: &[u8]) -> Result, RcpError> /// "Config tables provenance note" for [`Self::hw_pin_props`]'s undecomposed /// byte. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] -// fusa:req REQ-RMAP-012 +//fusa:req REQ-RMAP-012 pub struct HwPinMappingEntry { /// Endpoint slot number this pin is assigned to. pub hw_ep_nr: u8, @@ -1172,14 +1245,18 @@ pub struct HwPinMappingEntry { } impl HwPinMappingEntry { - /// Encoded wire length in bytes. + /// Encoded wire length in bytes: Table 19's row stride, fixed by + /// IO_Pin 2's `hw_ep_nr` at relative address `0x0003`. + //fusa:req REQ-RMAP-032 pub const ENCODED_LEN: usize = 3; /// The [`RegisterCategory`] this table's rows belong to. pub const CATEGORY: RegisterCategory = RegisterCategory::HwConfig; - /// Encode as `[hw_ep_nr, hw_ep_pin_nr, hw_pin_props]`. Never panics. - // fusa:req REQ-RMAP-013 + /// Encode as Table 19's 3-byte row, `[hw_ep_nr (0x0000), + /// hw_ep_pin_nr (0x0001), hw_pin_type (0x0002)]`. Never panics. + //fusa:req REQ-RMAP-013 + //fusa:req REQ-RMAP-032 pub fn encode(&self) -> [u8; Self::ENCODED_LEN] { [self.hw_ep_nr, self.hw_ep_pin_nr, self.hw_pin_props] } @@ -1189,7 +1266,7 @@ impl HwPinMappingEntry { /// Returns `Err(RcpError::ShortFrame)` if `bytes` is shorter than /// [`Self::ENCODED_LEN`]. Trailing bytes beyond `ENCODED_LEN` are /// ignored. Never panics for any input. - // fusa:req REQ-RMAP-014 + //fusa:req REQ-RMAP-014 pub fn decode(bytes: &[u8]) -> Result { if bytes.len() < Self::ENCODED_LEN { return Err(RcpError::ShortFrame); @@ -1262,7 +1339,7 @@ impl ConfigTableRow for HwPinMappingEntry { /// `capacity`, cross-referenced against /// [`GeneralRegisters::svr_req_stream_max`]). #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] -// fusa:req REQ-RMAP-015 +//fusa:req REQ-RMAP-015 pub struct RequestStreamConfigEntry { /// 64-bit stream identifier this entry is bound to; a sentinel default /// value means "unconfigured, no reception." @@ -1342,7 +1419,7 @@ impl RequestStreamConfigEntry { /// Table 22's `0x000D` flags byte, assembled from the eight `bool` /// fields. Never panics. - // fusa:req REQ-RMAP-016 + //fusa:req REQ-RMAP-016 pub fn flags_byte(&self) -> u8 { let mut b = 0u8; if self.rx_enforce_e2e { @@ -1376,7 +1453,7 @@ impl RequestStreamConfigEntry { /// field at its tabulated relative address, the eight `1 bit` flags /// packed into the `0x000D` byte, and both reserved blocks written as /// zero. Never panics. - // fusa:req REQ-RMAP-016 + //fusa:req REQ-RMAP-016 pub fn encode(&self) -> [u8; Self::ENCODED_LEN] { let mut buf = [0u8; Self::ENCODED_LEN]; let mut off = 0usize; @@ -1411,7 +1488,7 @@ impl RequestStreamConfigEntry { /// Returns `Err(RcpError::ShortFrame)` if `bytes` is shorter than /// [`Self::ENCODED_LEN`]. Trailing bytes beyond `ENCODED_LEN` are /// ignored. Never panics for any input. - // fusa:req REQ-RMAP-017 + //fusa:req REQ-RMAP-017 pub fn decode(bytes: &[u8]) -> Result { if bytes.len() < Self::ENCODED_LEN { return Err(RcpError::ShortFrame); @@ -1523,7 +1600,7 @@ impl ConfigTableRow for RequestStreamConfigEntry { /// to be ensured by the instance that is sending the configuration to this /// table"). #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] -// fusa:req REQ-RMAP-018 +//fusa:req REQ-RMAP-018 pub struct EpByteBusIdMapEntry { /// Table 23 `0x0000`, "Index of request stream ID". pub map_stream_index: u8, @@ -1555,7 +1632,7 @@ impl EpByteBusIdMapEntry { /// This recognizes a fixed, stated wire convention only — it is not a /// row-ordering check. See this type's doc comment. Never panics for /// any input. - // fusa:req REQ-RMAP-018 + //fusa:req REQ-RMAP-018 pub fn is_end_of_table(&self) -> bool { self.map_stream_index == Self::END_OF_TABLE_STREAM_INDEX } @@ -1566,7 +1643,7 @@ impl EpByteBusIdMapEntry { /// Releases before v5.0.0 emitted `[stream_index, BBID_hi, BBID_lo, /// EP_Nr]` — `EP_Nr` and `BBID` transposed against the table's own /// relative addresses. - // fusa:req REQ-RMAP-019 + //fusa:req REQ-RMAP-019 pub fn encode(&self) -> [u8; Self::ENCODED_LEN] { let mut buf = [0u8; Self::ENCODED_LEN]; buf[0] = self.map_stream_index; // 0x0000 @@ -1580,7 +1657,7 @@ impl EpByteBusIdMapEntry { /// Returns `Err(RcpError::ShortFrame)` if `bytes` is shorter than /// [`Self::ENCODED_LEN`]. Trailing bytes beyond `ENCODED_LEN` are /// ignored. Never panics for any input. - // fusa:req REQ-RMAP-020 + //fusa:req REQ-RMAP-020 pub fn decode(bytes: &[u8]) -> Result { if bytes.len() < Self::ENCODED_LEN { return Err(RcpError::ShortFrame); @@ -1607,17 +1684,45 @@ impl ConfigTableRow for EpByteBusIdMapEntry { // ── ResponseStreamConfigEntry (§3.10) ──────────────────────────────────────── -/// One row of the response/acknowledge queue config table (`§3.10`). +/// One row of the response/acknowledge queue config table (TC18 v0.5.1_RC +/// §12.7.9 "Response stream configuration", Table 24 "Responder +/// QUEUE_config", p.60). +/// +/// # Wire layout (Table 24) +/// +/// ```text +/// Responder Queue 1 +/// 0x0000 STREAM_UID 16 bit R/W+ "[63:48] Unique stream ID of queue 1" +/// 0x0002 Max_AVTPDUsize 16 bit R/W* "Maximum length of an AVTPDU +/// generated in quadlets" +/// 0x0004 queue_size 16 bit R/W* "assigned memory in 32bit words" +/// 0x0006 flush_on_count 16 bit R/W+ "1: immediate / 2 to Queue_Size" +/// 0x0008 Flush_time 16 bit R/W+ "0: Flush only by count / nr: µs" +/// Responder Queue 2 +/// 0x000A (next row) +/// ``` +/// +/// The row is 10 bytes, with the stride fixed by Responder Queue 2's own +/// `STREAM_UID` at relative address `0x000A`. +/// +/// Table 24 describes `STREAM_UID` as bits `[63:48]` of the queue's +/// destination stream identifier; this type carries the 16-bit register +/// value only and derives nothing from a 64-bit +/// [`crate::avtp::StreamId`] — see this module's "Config tables provenance +/// note" and `REQ-RMAP-037`. /// /// See this module's doc comment "Config tables" section for this table's /// row-count source ([`GeneralRegisters::svr_response_stream_cfg`]'s /// `capacity`, cross-referenced against /// [`GeneralRegisters::svr_responder_streams_max`]). #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] -// fusa:req REQ-RMAP-021 +//fusa:req REQ-RMAP-021 pub struct ResponseStreamConfigEntry { - /// 16 least-significant bits of this queue's destination stream - /// identifier. + /// Table 24 `0x0000`, "`[63:48]` Unique stream ID of queue 1" — the 16 + /// **most**-significant bits of this queue's destination stream + /// identifier. Carried as the raw register value; this crate derives + /// no [`crate::avtp::StreamId`] from it and checks no such + /// relationship (`REQ-RMAP-037`). pub resp_stream_uid: u16, /// Largest single AVTPDU this queue may generate, respecting network /// MTU; larger payloads require fragmentation. @@ -1634,15 +1739,19 @@ pub struct ResponseStreamConfigEntry { } impl ResponseStreamConfigEntry { - /// Encoded wire length in bytes. + /// Encoded wire length in bytes: Table 24's row stride, fixed by + /// Responder Queue 2's `STREAM_UID` at relative address `0x000A`. + //fusa:req REQ-RMAP-036 pub const ENCODED_LEN: usize = 10; /// The [`RegisterCategory`] this table's rows belong to. pub const CATEGORY: RegisterCategory = RegisterCategory::RcpConfig; - /// Encode as a fixed-length, big-endian byte block, field order above, - /// with no padding between fields. Never panics. - // fusa:req REQ-RMAP-022 + /// Encode as Table 24's fixed-length, big-endian 10-byte row: each + /// field at its tabulated relative address, with no padding between + /// fields. Never panics. + //fusa:req REQ-RMAP-022 + //fusa:req REQ-RMAP-036 pub fn encode(&self) -> [u8; Self::ENCODED_LEN] { let mut buf = [0u8; Self::ENCODED_LEN]; buf[0..2].copy_from_slice(&self.resp_stream_uid.to_be_bytes()); @@ -1659,7 +1768,7 @@ impl ResponseStreamConfigEntry { /// Returns `Err(RcpError::ShortFrame)` if `bytes` is shorter than /// [`Self::ENCODED_LEN`]. Trailing bytes beyond `ENCODED_LEN` are /// ignored. Never panics for any input. - // fusa:req REQ-RMAP-023 + //fusa:req REQ-RMAP-023 pub fn decode(bytes: &[u8]) -> Result { if bytes.len() < Self::ENCODED_LEN { return Err(RcpError::ShortFrame); @@ -1714,7 +1823,7 @@ impl ConfigTableRow for ResponseStreamConfigEntry { /// [`GeneralRegisters`] pointer, since [`GeneralRegisters::svr_sequencer_state_ptr`] /// is pointer-only). #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -// fusa:req REQ-RMAP-024 +//fusa:req REQ-RMAP-024 pub struct SequencerStateEntry { /// Table 25 `0x0000`, "Recent state if sequencer no N; if manually set /// to 0 then disabled". §12.7.10: "Upon power-on reset all sequencer @@ -1740,7 +1849,7 @@ impl SequencerStateEntry { /// (§12.7.10: all sequencer state values are set to "1"). /// `request_stream_index` is left `0` — Table 25 documents no default /// for it. - // fusa:req REQ-RMAP-024 + //fusa:req REQ-RMAP-024 pub fn power_on_default() -> Self { Self { seq_state: 1, @@ -1750,7 +1859,7 @@ impl SequencerStateEntry { /// Encode as Table 25's 2-byte row, `[Seq_state, /// Request_stream_index]`. Never panics. - // fusa:req REQ-RMAP-025 + //fusa:req REQ-RMAP-025 pub fn encode(&self) -> [u8; Self::ENCODED_LEN] { [self.seq_state, self.request_stream_index] } @@ -1760,7 +1869,7 @@ impl SequencerStateEntry { /// Returns `Err(RcpError::ShortFrame)` if `bytes` is shorter than /// [`Self::ENCODED_LEN`]. Trailing bytes beyond `ENCODED_LEN` are /// ignored. Never panics for any input. - // fusa:req REQ-RMAP-026 + //fusa:req REQ-RMAP-026 pub fn decode(bytes: &[u8]) -> Result { if bytes.len() < Self::ENCODED_LEN { return Err(RcpError::ShortFrame); @@ -1818,7 +1927,7 @@ mod tests { // ── EndpointType: numeric encoding / round-trip ────────────────────── #[test] - // fusa:test REQ-RMAP-001 + //fusa:test REQ-RMAP-001 fn endpoint_type_encodings_match_roadmap_values() { assert_eq!(EndpointType::Wakeup.to_u8(), 0x01); assert_eq!(EndpointType::Gpio.to_u8(), 0x02); @@ -1836,7 +1945,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-001 + //fusa:test REQ-RMAP-001 fn from_u8_round_trips_every_defined_ep_type() { for ep_type in ALL_ENDPOINT_TYPES { let raw = ep_type.to_u8(); @@ -1845,7 +1954,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-001 + //fusa:test REQ-RMAP-001 fn all_thirteen_ep_type_codes_are_distinct() { let mut seen = std::collections::HashSet::new(); for ep_type in ALL_ENDPOINT_TYPES { @@ -1858,17 +1967,55 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-001 + //fusa:test REQ-RMAP-001 fn is_reserved_true_only_for_dac() { for ep_type in ALL_ENDPOINT_TYPES { assert_eq!(ep_type.is_reserved(), ep_type == EndpointType::Dac); } } + #[test] + //fusa:test REQ-EPGEN-001 + fn endpoint_type_codes_match_tc18_table_29() { + // Codes transcribed by hand from TC18 v0.5.1_RC §13.2 Table 29 + // "ep_type values" (p.73). Table 29's 0x00 "Server" row has no + // EndpointType variant by design -- see REQ-EPGEN-002 -- and is + // asserted rejected below rather than mapped. + let table_29: [(u8, EndpointType); 13] = [ + (0x01, EndpointType::Wakeup), // WakeUp Ctrl + (0x02, EndpointType::Gpio), // GPIO + (0x03, EndpointType::Spi), // SPI + (0x04, EndpointType::I2c), // I2C + (0x05, EndpointType::Uart), // UART + (0x06, EndpointType::Lin), // LIN + (0x07, EndpointType::PwmOut), // PWM_OUT + (0x08, EndpointType::PwmIn), // PWM_IN + (0x09, EndpointType::Adc), // ADC + (0x0A, EndpointType::Dac), // DAC + (0x0B, EndpointType::Can), // CAN + (0x0C, EndpointType::Iseled), // ISELED + (0x0D, EndpointType::Mdio), // MDIO + ]; + for (code, ep_type) in table_29 { + assert_eq!( + ep_type.to_u8(), + code, + "TC18 Table 29 assigns {ep_type:?} the ep_type code 0x{code:02X}" + ); + assert_eq!(EndpointType::from_u8(code), Ok(ep_type)); + } + + // Table 29's assigned range ends at 0x0D; 0x00 (Server) is + // assigned but unmodeled (REQ-EPGEN-002), and 0x0E upward is + // unassigned. + assert!(EndpointType::from_u8(0x00).is_err()); + assert!(EndpointType::from_u8(0x0E).is_err()); + } + // ── check_ep_type_supported: structural DAC rejection ───────────────── #[test] - // fusa:test REQ-RMAP-031 + //fusa:test REQ-RMAP-031 fn check_ep_type_supported_rejects_only_dac() { for ep_type in ALL_ENDPOINT_TYPES { let result = check_ep_type_supported(ep_type); @@ -1881,7 +2028,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-031 + //fusa:test REQ-RMAP-031 fn check_ep_type_supported_agrees_with_is_reserved() { for ep_type in ALL_ENDPOINT_TYPES { assert_eq!( @@ -1892,7 +2039,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-031 + //fusa:test REQ-RMAP-031 fn check_ep_type_supported_never_panics_for_any_ep_type() { for ep_type in ALL_ENDPOINT_TYPES { let _ = check_ep_type_supported(ep_type); @@ -1900,7 +2047,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-031 + //fusa:test REQ-RMAP-031 fn check_ep_type_supported_does_not_change_functional_config_matching() { // check_ep_type_supported is additive, composed alongside // check_functional_config_matches_ep_type, never folded into it — @@ -1921,7 +2068,7 @@ mod tests { // ── EndpointType: rejection of unrecognized encodings ──────────────── #[test] - // fusa:test REQ-RMAP-002 + //fusa:test REQ-RMAP-002 fn from_u8_rejects_every_byte_outside_the_defined_range() { for raw in 0u8..=255 { let result = EndpointType::from_u8(raw); @@ -1933,8 +2080,8 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-002 - // fusa:test REQ-RMAP-006 + //fusa:test REQ-RMAP-002 + //fusa:test REQ-RMAP-006 fn from_u8_never_panics_across_the_full_byte_range() { for raw in 0u8..=255 { let _ = EndpointType::from_u8(raw); @@ -1944,19 +2091,19 @@ mod tests { // ── The three layers: structural existence and tagging ─────────────── #[test] - // fusa:test REQ-RMAP-003 + //fusa:test REQ-RMAP-003 fn per_ep_config_block_layer_is_generic() { assert_eq!(PerEpConfigBlock::LAYER, ConfigLayer::Generic); } #[test] - // fusa:test REQ-RMAP-003 + //fusa:test REQ-RMAP-003 fn common_functional_config_layer_is_common_functional() { assert_eq!(CommonFunctionalConfig::LAYER, ConfigLayer::CommonFunctional); } #[test] - // fusa:test REQ-RMAP-003 + //fusa:test REQ-RMAP-003 fn per_ep_type_functional_config_layer_is_tagged_by_its_ep_type() { for ep_type in ALL_ENDPOINT_TYPES { let cfg = PerEpTypeFunctionalConfig::new(ep_type); @@ -1965,7 +2112,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-003 + //fusa:test REQ-RMAP-003 fn per_ep_config_block_new_round_trips_ep_type() { for ep_type in ALL_ENDPOINT_TYPES { assert_eq!(PerEpConfigBlock::new(ep_type).ep_type, ep_type); @@ -1973,7 +2120,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-003 + //fusa:test REQ-RMAP-003 fn common_functional_config_default_is_all_false() { let cfg = CommonFunctionalConfig::default(); assert!(!cfg.ep_enable); @@ -2009,8 +2156,8 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-028 - // fusa:test REQ-RMAP-029 + //fusa:test REQ-RMAP-028 + //fusa:test REQ-RMAP-029 fn common_functional_config_encode_decode_round_trips() { for cfg in sample_common_functional_configs() { let encoded = cfg.encode(); @@ -2020,7 +2167,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-028 + //fusa:test REQ-RMAP-028 fn common_functional_config_encode_is_one_full_byte_per_field() { let cfg = CommonFunctionalConfig { ep_enable: true, @@ -2031,7 +2178,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-029 + //fusa:test REQ-RMAP-029 fn common_functional_config_decode_rejects_short_input() { for len in 0..CommonFunctionalConfig::ENCODED_LEN { let bytes = vec![0u8; len]; @@ -2043,7 +2190,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-029 + //fusa:test REQ-RMAP-029 fn common_functional_config_decode_treats_any_nonzero_byte_as_true() { let bytes = [0x01, 0xFF, 0x02]; assert_eq!( @@ -2057,7 +2204,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-029 + //fusa:test REQ-RMAP-029 fn common_functional_config_decode_ignores_trailing_bytes() { let cfg = CommonFunctionalConfig { ep_enable: true, @@ -2072,7 +2219,7 @@ mod tests { // ── Cross-layer invariant ───────────────────────────────────────────── #[test] - // fusa:test REQ-RMAP-004 + //fusa:test REQ-RMAP-004 fn functional_config_matches_ep_type_true_only_when_tags_agree() { for generic_type in ALL_ENDPOINT_TYPES { for per_type_type in ALL_ENDPOINT_TYPES { @@ -2088,7 +2235,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-004 + //fusa:test REQ-RMAP-004 fn check_functional_config_matches_ep_type_agrees_with_the_bool_form() { for generic_type in ALL_ENDPOINT_TYPES { for per_type_type in ALL_ENDPOINT_TYPES { @@ -2111,7 +2258,7 @@ mod tests { // ── Relationship to lifecycle::RegisterCategory ────────────────────── #[test] - // fusa:test REQ-RMAP-005 + //fusa:test REQ-RMAP-005 fn register_category_matches_the_documented_mapping() { assert_eq!( register_category(ConfigLayer::Generic), @@ -2131,7 +2278,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-005 + //fusa:test REQ-RMAP-005 fn both_functional_layers_map_to_the_same_register_category() { // CommonFunctional and every PerTypeFunctional variant agree with // each other, matching this module's doc comment reasoning that @@ -2149,7 +2296,7 @@ mod tests { // ── Fuzz-style: arbitrary inputs never panic ────────────────────────── #[test] - // fusa:test REQ-RMAP-006 + //fusa:test REQ-RMAP-006 fn taxonomy_operations_never_panic_for_any_ep_type_pair() { for generic_type in ALL_ENDPOINT_TYPES { for per_type_type in ALL_ENDPOINT_TYPES { @@ -2191,7 +2338,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-007 + //fusa:test REQ-RMAP-007 fn table_descriptor_encode_decode_round_trips() { for d in sample_descriptors() { let encoded = d.encode(); @@ -2201,7 +2348,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-007 + //fusa:test REQ-RMAP-007 fn table_descriptor_decode_ignores_trailing_bytes() { let d = TableDescriptor { ptr: 0x0102, @@ -2213,7 +2360,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-007 + //fusa:test REQ-RMAP-007 fn table_descriptor_decode_rejects_short_input() { for len in 0..TableDescriptor::ENCODED_LEN { let bytes = vec![0u8; len]; @@ -2280,7 +2427,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-008 + //fusa:test REQ-RMAP-008 fn general_registers_names_the_six_roadmap_quoted_fields_independently() { // Every field ROADMAP.md's own checklist bullet names verbatim is // independently settable/readable and distinguishable from every @@ -2295,7 +2442,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-008 + //fusa:test REQ-RMAP-008 fn general_registers_default_is_all_zero() { let regs = GeneralRegisters::default(); assert_eq!(regs.svr_oa_tc18_magic_nr, 0); @@ -2304,7 +2451,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-008 + //fusa:test REQ-RMAP-008 fn general_registers_category_is_lifecycle_general() { assert_eq!(GeneralRegisters::CATEGORY, RegisterCategory::General); } @@ -2312,7 +2459,7 @@ mod tests { // ── GeneralRegisters: claims_compound_wait_bundle ─────────────────────── #[test] - // fusa:test REQ-RMAP-030 + //fusa:test REQ-RMAP-030 fn claims_compound_wait_bundle_reads_bit_zero_only() { let mut regs = sample_general_registers(); @@ -2334,7 +2481,7 @@ mod tests { // ── GeneralRegisters: encode/decode round-trip ───────────────────────── #[test] - // fusa:test REQ-RMAP-009 + //fusa:test REQ-RMAP-009 fn general_registers_encode_decode_round_trips() { let regs = sample_general_registers(); let encoded = regs.encode(); @@ -2343,7 +2490,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-009 + //fusa:test REQ-RMAP-009 fn general_registers_encode_decode_round_trips_default_and_max_values() { for regs in [ GeneralRegisters::default(), @@ -2407,7 +2554,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-009 + //fusa:test REQ-RMAP-009 fn general_registers_decode_ignores_trailing_bytes() { let regs = sample_general_registers(); let mut bytes = regs.encode().to_vec(); @@ -2418,7 +2565,7 @@ mod tests { // ── GeneralRegisters: short-input rejection ──────────────────────────── #[test] - // fusa:test REQ-RMAP-010 + //fusa:test REQ-RMAP-010 fn general_registers_decode_rejects_every_length_shorter_than_encoded_len() { for len in 0..GeneralRegisters::ENCODED_LEN { let bytes = vec![0u8; len]; @@ -2431,7 +2578,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-010 + //fusa:test REQ-RMAP-010 fn general_registers_decode_accepts_exactly_encoded_len() { let regs = sample_general_registers(); let encoded = regs.encode(); @@ -2441,7 +2588,7 @@ mod tests { // ── Fuzz-style: arbitrary byte inputs never panic ────────────────────── #[test] - // fusa:test REQ-RMAP-011 + //fusa:test REQ-RMAP-011 fn table_descriptor_decode_never_panics_across_arbitrary_lengths() { for len in 0..=300usize { let bytes: Vec = (0..len).map(|i| (i % 256) as u8).collect(); @@ -2450,7 +2597,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-011 + //fusa:test REQ-RMAP-011 fn general_registers_decode_never_panics_across_arbitrary_lengths() { for len in 0..=300usize { let bytes: Vec = (0..len).map(|i| ((i * 7) % 256) as u8).collect(); @@ -2459,7 +2606,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-011 + //fusa:test REQ-RMAP-011 fn general_registers_and_table_descriptor_encode_never_panic() { let regs = sample_general_registers(); let _ = regs.encode(); @@ -2487,13 +2634,13 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-012 + //fusa:test REQ-RMAP-012 fn hw_pin_mapping_entry_category_is_hw_config() { assert_eq!(HwPinMappingEntry::CATEGORY, RegisterCategory::HwConfig); } #[test] - // fusa:test REQ-RMAP-012 + //fusa:test REQ-RMAP-012 fn hw_pin_mapping_entry_fields_are_independently_settable() { let e = HwPinMappingEntry { hw_ep_nr: 7, @@ -2506,7 +2653,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-013 + //fusa:test REQ-RMAP-013 fn hw_pin_mapping_entry_encode_decode_round_trips() { for e in sample_hw_pin_mapping_entries() { let encoded = e.encode(); @@ -2516,7 +2663,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-013 + //fusa:test REQ-RMAP-013 fn hw_pin_mapping_entry_decode_ignores_trailing_bytes() { let e = HwPinMappingEntry { hw_ep_nr: 1, @@ -2529,7 +2676,75 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-014 + //fusa:test REQ-RMAP-032 + fn hw_pin_mapping_entry_matches_table_19_literal_bytes() { + // Expected bytes derived by hand from TC18 v0.5.1_RC §12.7.6 "HW + // pin mapping configuration", Table 19 "HW_config" (p.54), + // IO_Pin 1: + // 0x0000 hw_ep_nr 8 bit R/W* + // 0x0001 hw_ep_pin_nr 8 bit R/W* + // 0x0002 hw_pin_type 8 bit R/W* + // Distinct values per field so a transposition cannot pass. + let entry = HwPinMappingEntry { + hw_ep_nr: 0x03, + hw_ep_pin_nr: 0x04, + hw_pin_props: 0xB5, + }; + assert_eq!(entry.encode(), [0x03, 0x04, 0xB5]); + assert_eq!( + entry.encode()[0x00], + 0x03, + "Table 19 relative address 0x0000 is hw_ep_nr" + ); + assert_eq!( + entry.encode()[0x01], + 0x04, + "Table 19 relative address 0x0001 is hw_ep_pin_nr" + ); + assert_eq!( + entry.encode()[0x02], + 0xB5, + "Table 19 relative address 0x0002 is hw_pin_type" + ); + } + + #[test] + //fusa:test REQ-RMAP-032 + fn hw_pin_mapping_rows_land_on_table_19_absolute_addresses() { + // Table 19 tabulates IO_Pin 2's hw_ep_nr at relative address + // 0x0003 and IO_Pin 3's at 0x0006, which fixes the row stride at + // exactly 3 bytes. + assert_eq!(HwPinMappingEntry::ENCODED_LEN, 3); + + let rows = [ + HwPinMappingEntry { + hw_ep_nr: 0x11, + hw_ep_pin_nr: 0x12, + hw_pin_props: 0x13, + }, + HwPinMappingEntry { + hw_ep_nr: 0x21, + hw_ep_pin_nr: 0x22, + hw_pin_props: 0x23, + }, + HwPinMappingEntry { + hw_ep_nr: 0x31, + hw_ep_pin_nr: 0x32, + hw_pin_props: 0x33, + }, + ]; + let flat = encode_rows(&rows); + assert_eq!(flat.len(), 9); + // IO_Pin 2, Table 19 addresses 0x0003 / 0x0004 / 0x0005. + assert_eq!(flat[0x0003], 0x21); + assert_eq!(flat[0x0004], 0x22); + assert_eq!(flat[0x0005], 0x23); + // IO_Pin 3, Table 19 address 0x0006. + assert_eq!(flat[0x0006], 0x31); + } + + #[test] + //fusa:test REQ-RMAP-014 fn hw_pin_mapping_entry_decode_rejects_short_input() { for len in 0..HwPinMappingEntry::ENCODED_LEN { let bytes = vec![0u8; len]; @@ -2538,7 +2753,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-014 + //fusa:test REQ-RMAP-014 fn hw_pin_mapping_entry_never_panics_across_arbitrary_lengths() { for len in 0..=50usize { let bytes: Vec = (0..len).map(|i| (i % 256) as u8).collect(); @@ -2591,7 +2806,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-015 + //fusa:test REQ-RMAP-015 fn request_stream_config_entry_category_is_rcp_config() { assert_eq!( RequestStreamConfigEntry::CATEGORY, @@ -2600,7 +2815,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-015 + //fusa:test REQ-RMAP-015 fn request_stream_config_entry_fields_are_independently_settable() { let e = sample_request_stream_config_entries()[1]; assert_eq!(e.rx_stream_id, 0x0011_2233_4455_6677); @@ -2612,7 +2827,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-016 + //fusa:test REQ-RMAP-016 fn request_stream_config_entry_encode_decode_round_trips() { for e in sample_request_stream_config_entries() { let encoded = e.encode(); @@ -2622,7 +2837,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-016 + //fusa:test REQ-RMAP-016 fn request_stream_config_entry_decode_ignores_trailing_bytes() { let e = sample_request_stream_config_entries()[1]; let mut bytes = e.encode().to_vec(); @@ -2634,7 +2849,7 @@ mod tests { /// *next* row's first field — at relative address `0x0018`, which fixes /// the row stride at 24 bytes. Releases before v5.0.0 used 25. #[test] - // fusa:test REQ-RMAP-016 + //fusa:test REQ-RMAP-016 fn request_stream_config_entry_row_stride_is_table_22_next_row_address() { assert_eq!(RequestStreamConfigEntry::ENCODED_LEN, 0x18); assert_eq!(RequestStreamConfigEntry::FLAGS_OFFSET, 0x0D); @@ -2659,7 +2874,7 @@ mod tests { /// 0x0014 Reserved 32b zero /// ``` #[test] - // fusa:test REQ-RMAP-016 + //fusa:test REQ-RMAP-016 fn request_stream_config_entry_matches_table_22_literal_bytes() { let expected: [u8; 24] = [ 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, // 0x0000 @@ -2685,7 +2900,7 @@ mod tests { /// leave all 23 other bytes zero. Releases before v5.0.0 gave each flag /// its own byte, which this test would have caught. #[test] - // fusa:test REQ-RMAP-016 + //fusa:test REQ-RMAP-016 fn request_stream_config_entry_flag_bit_positions_match_table_22() { /// Sets one Table 22 flag on an otherwise-default row. type SetFlag = fn(&mut RequestStreamConfigEntry); @@ -2717,7 +2932,7 @@ mod tests { /// Reserved bytes `0x0012`-`0x0017` carry no modeled field: decode must /// ignore whatever they hold, and encode must emit zero. #[test] - // fusa:test REQ-RMAP-017 + //fusa:test REQ-RMAP-017 fn request_stream_config_entry_reserved_bytes_are_ignored_and_zeroed() { let e = sample_request_stream_config_entries()[1]; let mut dirty = e.encode(); @@ -2727,7 +2942,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-017 + //fusa:test REQ-RMAP-017 fn request_stream_config_entry_decode_rejects_short_input() { for len in 0..RequestStreamConfigEntry::ENCODED_LEN { let bytes = vec![0u8; len]; @@ -2739,7 +2954,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-017 + //fusa:test REQ-RMAP-017 fn request_stream_config_entry_never_panics_across_arbitrary_lengths() { for len in 0..=100usize { let bytes: Vec = (0..len).map(|i| ((i * 3) % 256) as u8).collect(); @@ -2766,13 +2981,13 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-018 + //fusa:test REQ-RMAP-018 fn ep_bytebus_id_map_entry_category_is_rcp_config() { assert_eq!(EpByteBusIdMapEntry::CATEGORY, RegisterCategory::RcpConfig); } #[test] - // fusa:test REQ-RMAP-018 + //fusa:test REQ-RMAP-018 fn ep_bytebus_id_map_entry_is_end_of_table_true_only_for_sentinel_stream_index() { for stream_index in 0u8..=255 { let e = EpByteBusIdMapEntry { @@ -2788,7 +3003,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-019 + //fusa:test REQ-RMAP-019 fn ep_bytebus_id_map_entry_encode_decode_round_trips() { for e in sample_ep_bytebus_id_map_entries() { let encoded = e.encode(); @@ -2798,7 +3013,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-019 + //fusa:test REQ-RMAP-019 fn ep_bytebus_id_map_entry_decode_ignores_trailing_bytes() { let e = EpByteBusIdMapEntry { map_stream_index: 1, @@ -2823,7 +3038,7 @@ mod tests { /// `[0x03, 0x05, 0x55, 0x04]` — the two transposed. The literal below /// uses distinct values in every byte so a transposition cannot pass. #[test] - // fusa:test REQ-RMAP-019 + //fusa:test REQ-RMAP-019 fn ep_bytebus_id_map_entry_matches_table_23_literal_bytes() { let e = EpByteBusIdMapEntry { map_stream_index: 3, @@ -2838,13 +3053,13 @@ mod tests { /// Table 23 tabulates `2_Request_Stream_Index` — the next row's first /// field — at relative address `0x0004`, fixing the row stride. #[test] - // fusa:test REQ-RMAP-019 + //fusa:test REQ-RMAP-019 fn ep_bytebus_id_map_entry_row_stride_is_table_23_next_row_address() { assert_eq!(EpByteBusIdMapEntry::ENCODED_LEN, 0x04); } #[test] - // fusa:test REQ-RMAP-020 + //fusa:test REQ-RMAP-020 fn ep_bytebus_id_map_entry_decode_rejects_short_input() { for len in 0..EpByteBusIdMapEntry::ENCODED_LEN { let bytes = vec![0u8; len]; @@ -2856,7 +3071,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-020 + //fusa:test REQ-RMAP-020 fn ep_bytebus_id_map_entry_never_panics_across_arbitrary_lengths() { for len in 0..=50usize { let bytes: Vec = (0..len).map(|i| (i % 256) as u8).collect(); @@ -2887,7 +3102,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-021 + //fusa:test REQ-RMAP-021 fn response_stream_config_entry_category_is_rcp_config() { assert_eq!( ResponseStreamConfigEntry::CATEGORY, @@ -2896,7 +3111,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-021 + //fusa:test REQ-RMAP-021 fn response_stream_config_entry_fields_are_independently_settable() { let e = sample_response_stream_config_entries()[1]; assert_eq!(e.resp_stream_uid, 0x1234); @@ -2907,7 +3122,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-022 + //fusa:test REQ-RMAP-022 fn response_stream_config_entry_encode_decode_round_trips() { for e in sample_response_stream_config_entries() { let encoded = e.encode(); @@ -2917,7 +3132,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-022 + //fusa:test REQ-RMAP-022 fn response_stream_config_entry_decode_ignores_trailing_bytes() { let e = sample_response_stream_config_entries()[1]; let mut bytes = e.encode().to_vec(); @@ -2926,7 +3141,78 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-023 + //fusa:test REQ-RMAP-036 + fn response_stream_config_entry_matches_table_24_literal_bytes() { + // Expected bytes derived by hand from TC18 v0.5.1_RC §12.7.9 + // "Response stream configuration", Table 24 "Responder + // QUEUE_config" (p.60), Responder Queue 1: + // 0x0000 STREAM_UID 16 bit + // 0x0002 Max_AVTPDUsize 16 bit + // 0x0004 queue_size 16 bit + // 0x0006 flush_on_count 16 bit + // 0x0008 Flush_time 16 bit + // Distinct byte pairs per field so any reordering fails. + let entry = ResponseStreamConfigEntry { + resp_stream_uid: 0xA1A2, + resp_max_avtpdu_size: 0xB1B2, + resp_queue_size: 0xC1C2, + resp_flush_on_count: 0xD1D2, + resp_flush_time: 0xE1E2, + }; + assert_eq!( + entry.encode(), + [0xA1, 0xA2, 0xB1, 0xB2, 0xC1, 0xC2, 0xD1, 0xD2, 0xE1, 0xE2] + ); + + let b = entry.encode(); + assert_eq!(&b[0x00..0x02], &[0xA1, 0xA2], "Table 24 0x0000 STREAM_UID"); + assert_eq!( + &b[0x02..0x04], + &[0xB1, 0xB2], + "Table 24 0x0002 Max_AVTPDUsize" + ); + assert_eq!(&b[0x04..0x06], &[0xC1, 0xC2], "Table 24 0x0004 queue_size"); + assert_eq!( + &b[0x06..0x08], + &[0xD1, 0xD2], + "Table 24 0x0006 flush_on_count" + ); + assert_eq!(&b[0x08..0x0A], &[0xE1, 0xE2], "Table 24 0x0008 Flush_time"); + } + + #[test] + //fusa:test REQ-RMAP-036 + fn response_stream_config_rows_land_on_table_24_addresses() { + // Table 24 tabulates Responder Queue 2's STREAM_UID at relative + // address 0x000A, which fixes the row stride at exactly 10 bytes. + assert_eq!(ResponseStreamConfigEntry::ENCODED_LEN, 0x000A); + + let rows = [ + ResponseStreamConfigEntry { + resp_stream_uid: 0x1111, + resp_max_avtpdu_size: 0x1122, + resp_queue_size: 0x1133, + resp_flush_on_count: 0x1144, + resp_flush_time: 0x1155, + }, + ResponseStreamConfigEntry { + resp_stream_uid: 0x2211, + resp_max_avtpdu_size: 0x2222, + resp_queue_size: 0x2233, + resp_flush_on_count: 0x2244, + resp_flush_time: 0x2255, + }, + ]; + let flat = encode_rows(&rows); + assert_eq!(flat.len(), 20); + // Responder Queue 2's STREAM_UID, Table 24 address 0x000A. + assert_eq!(&flat[0x000A..0x000C], &[0x22, 0x11]); + // ... and its Flush_time, 0x000A + 0x0008. + assert_eq!(&flat[0x0012..0x0014], &[0x22, 0x55]); + } + + #[test] + //fusa:test REQ-RMAP-023 fn response_stream_config_entry_decode_rejects_short_input() { for len in 0..ResponseStreamConfigEntry::ENCODED_LEN { let bytes = vec![0u8; len]; @@ -2938,7 +3224,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-023 + //fusa:test REQ-RMAP-023 fn response_stream_config_entry_never_panics_across_arbitrary_lengths() { for len in 0..=50usize { let bytes: Vec = (0..len).map(|i| (i % 256) as u8).collect(); @@ -2949,13 +3235,13 @@ mod tests { // ── SequencerStateEntry (§3.11) ───────────────────────────────────────── #[test] - // fusa:test REQ-RMAP-024 + //fusa:test REQ-RMAP-024 fn sequencer_state_entry_category_is_rcp_config() { assert_eq!(SequencerStateEntry::CATEGORY, RegisterCategory::RcpConfig); } #[test] - // fusa:test REQ-RMAP-024 + //fusa:test REQ-RMAP-024 fn sequencer_state_entry_power_on_default_is_state_one() { // TC18 §12.7.10: "Upon power-on reset all sequencer state values // are set to '1'." Table 25's Default column is blank for @@ -2966,7 +3252,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-024 + //fusa:test REQ-RMAP-024 fn sequencer_state_entry_default_matches_power_on_default() { assert_eq!( SequencerStateEntry::default(), @@ -2980,13 +3266,13 @@ mod tests { /// only `Seq_state` with a 1-byte stride, which both dropped the /// access-control field and misaligned every row after the first. #[test] - // fusa:test REQ-RMAP-024 + //fusa:test REQ-RMAP-024 fn sequencer_state_entry_row_stride_is_table_25_next_row_address() { assert_eq!(SequencerStateEntry::ENCODED_LEN, 0x02); } #[test] - // fusa:test REQ-RMAP-025 + //fusa:test REQ-RMAP-025 fn sequencer_state_entry_encode_decode_round_trips() { for seq_state in [0u8, 1, 2, 128, u8::MAX] { for request_stream_index in [0u8, 1, 200, u8::MAX] { @@ -3009,7 +3295,7 @@ mod tests { /// 0x0001 Request_stream_index 8b 4 = 0x04 /// ``` #[test] - // fusa:test REQ-RMAP-025 + //fusa:test REQ-RMAP-025 fn sequencer_state_entry_matches_table_25_literal_bytes() { let e = SequencerStateEntry { seq_state: 1, @@ -3023,7 +3309,7 @@ mod tests { /// A two-sequencer table read back through `decode_rows` must resolve /// `Seq_2`'s state from offset `0x0002`, not `0x0001`. #[test] - // fusa:test REQ-RMAP-025 + //fusa:test REQ-RMAP-025 fn sequencer_state_table_rows_land_on_table_25_addresses() { // Seq_1: state 1, client 9. Seq_2: state 3, client 2. let raw: [u8; 4] = [0x01, 0x09, 0x03, 0x02]; @@ -3043,7 +3329,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-025 + //fusa:test REQ-RMAP-025 fn sequencer_state_entry_decode_ignores_trailing_bytes() { let e = SequencerStateEntry { seq_state: 7, @@ -3055,7 +3341,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-026 + //fusa:test REQ-RMAP-026 fn sequencer_state_entry_decode_rejects_short_input() { for len in 0..SequencerStateEntry::ENCODED_LEN { let bytes = vec![0u8; len]; @@ -3067,7 +3353,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-026 + //fusa:test REQ-RMAP-026 fn sequencer_state_entry_never_panics_across_arbitrary_lengths() { for len in 0..=20usize { let bytes: Vec = (0..len).map(|i| (i % 256) as u8).collect(); @@ -3078,7 +3364,7 @@ mod tests { // ── ConfigTableRow / encode_rows / decode_rows (all five row types) ──── #[test] - // fusa:test REQ-RMAP-027 + //fusa:test REQ-RMAP-027 fn encode_rows_decode_rows_round_trip_hw_pin_mapping_entries() { let rows: Vec = sample_hw_pin_mapping_entries().to_vec(); let encoded = encode_rows(&rows); @@ -3087,7 +3373,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-027 + //fusa:test REQ-RMAP-027 fn encode_rows_decode_rows_round_trip_request_stream_config_entries() { let rows: Vec = sample_request_stream_config_entries().to_vec(); let encoded = encode_rows(&rows); @@ -3099,7 +3385,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-027 + //fusa:test REQ-RMAP-027 fn encode_rows_decode_rows_round_trip_ep_bytebus_id_map_entries() { let rows: Vec = sample_ep_bytebus_id_map_entries().to_vec(); let encoded = encode_rows(&rows); @@ -3108,7 +3394,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-027 + //fusa:test REQ-RMAP-027 fn encode_rows_decode_rows_round_trip_response_stream_config_entries() { let rows: Vec = sample_response_stream_config_entries().to_vec(); let encoded = encode_rows(&rows); @@ -3120,7 +3406,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-027 + //fusa:test REQ-RMAP-027 fn encode_rows_decode_rows_round_trip_sequencer_state_entries() { let rows: Vec = [(0u8, 0u8), (1, 7), (255, 255)] .into_iter() @@ -3135,7 +3421,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-027 + //fusa:test REQ-RMAP-027 fn decode_rows_empty_input_is_empty_table() { assert_eq!( decode_rows::(&[]), @@ -3148,7 +3434,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-027 + //fusa:test REQ-RMAP-027 fn decode_rows_rejects_length_not_a_multiple_of_row_len() { // One full HwPinMappingEntry row (3 bytes) plus one extra byte is // not a whole number of rows. @@ -3168,7 +3454,7 @@ mod tests { } #[test] - // fusa:test REQ-RMAP-027 + //fusa:test REQ-RMAP-027 fn encode_rows_decode_rows_never_panic_across_arbitrary_lengths() { for len in 0..=100usize { let bytes: Vec = (0..len).map(|i| ((i * 5) % 256) as u8).collect(); diff --git a/src/relay.rs b/src/relay.rs index cd06132..cf635d7 100644 --- a/src/relay.rs +++ b/src/relay.rs @@ -1,11 +1,11 @@ -// fusa:req REQ-RELAY-001 -// fusa:req REQ-RELAY-002 -// fusa:req REQ-RELAY-003 -// fusa:req REQ-RELAY-004 -// fusa:req REQ-RELAY-005 -// fusa:req REQ-RELAY-006 -// fusa:req REQ-RELAY-007 -// fusa:req REQ-RELAY-008 +//fusa:req REQ-RELAY-001 +//fusa:req REQ-RELAY-002 +//fusa:req REQ-RELAY-003 +//fusa:req REQ-RELAY-004 +//fusa:req REQ-RELAY-005 +//fusa:req REQ-RELAY-006 +//fusa:req REQ-RELAY-007 +//fusa:req REQ-RELAY-008 //! RELAY protocol types, bundled locally until a published `relay-rs` crate //! exists to depend on directly. @@ -26,7 +26,7 @@ use tokio::sync::mpsc; // ── Protocol ────────────────────────────────────────────────────────────────── /// Protocol identifier per RELAY spec §3. Serialises as its integer value. -// fusa:req REQ-RELAY-001 +//fusa:req REQ-RELAY-001 #[repr(i32)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Protocol { @@ -105,7 +105,7 @@ impl std::fmt::Display for Version { // ── Message ─────────────────────────────────────────────────────────────────── /// Universal message envelope per RELAY spec §4. -// fusa:req REQ-RELAY-002 +//fusa:req REQ-RELAY-002 #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Message { pub protocol: Protocol, @@ -143,7 +143,7 @@ impl Message { // ── Back-pressure policy ────────────────────────────────────────────────────── /// Back-pressure policy for subscriber channels per RELAY spec §14. -// fusa:req REQ-RELAY-003 +//fusa:req REQ-RELAY-003 #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] pub enum BackPressurePolicy { /// Drop the arriving message when the channel is full (default). @@ -158,7 +158,7 @@ pub enum BackPressurePolicy { // ── SubscriberOptions ───────────────────────────────────────────────────────── /// Subscriber channel configuration per RELAY spec §18.3. -// fusa:req REQ-RELAY-004 +//fusa:req REQ-RELAY-004 #[derive(Clone, Debug, Default)] pub struct SubscriberOptions { /// Buffer depth; 0 means use the default (64). @@ -182,7 +182,7 @@ impl SubscriberOptions { // ── Error ───────────────────────────────────────────────────────────────────── /// The four mandatory RELAY error sentinels per §5.1. -// fusa:req REQ-RELAY-005 +//fusa:req REQ-RELAY-005 #[derive(Debug, Error, Clone, PartialEq, Eq)] pub enum Error { #[error("relay: closed")] @@ -198,7 +198,7 @@ pub enum Error { // ── Context ─────────────────────────────────────────────────────────────────── /// Lightweight context carrying an optional deadline per RELAY spec §18.3. -// fusa:req REQ-RELAY-006 +//fusa:req REQ-RELAY-006 #[derive(Clone, Debug)] pub struct Context { pub deadline: Option, @@ -232,7 +232,7 @@ impl Default for Context { // ── Node and Caller traits ──────────────────────────────────────────────────── /// Protocol-agnostic pub/sub interface per RELAY spec §10.1. -// fusa:req REQ-RELAY-007 +//fusa:req REQ-RELAY-007 #[async_trait] pub trait Node: Send + Sync { fn protocol(&self) -> Protocol; @@ -245,7 +245,7 @@ pub trait Node: Send + Sync { } /// Extends [`Node`] with request/response semantics per RELAY spec §10.2. -// fusa:req REQ-RELAY-008 +//fusa:req REQ-RELAY-008 #[async_trait] pub trait Caller: Node { async fn call(&self, ctx: Context, req: Message) -> Result; @@ -259,14 +259,14 @@ mod tests { use super::*; #[test] - // fusa:test REQ-RELAY-001 + //fusa:test REQ-RELAY-001 fn protocol_display() { assert_eq!(Protocol::Rcp.to_string(), "RCP"); assert_eq!(Protocol::Can.to_string(), "CAN"); } #[test] - // fusa:test REQ-RELAY-001 + //fusa:test REQ-RELAY-001 fn protocol_serde_roundtrip() { let p = Protocol::Rcp; let json = serde_json::to_string(&p).unwrap(); @@ -276,14 +276,14 @@ mod tests { } #[test] - // fusa:test REQ-RELAY-001 + //fusa:test REQ-RELAY-001 fn protocol_deserialize_unknown_rejected() { let err = serde_json::from_str::("99"); assert!(err.is_err()); } #[test] - // fusa:test REQ-RELAY-002 + //fusa:test REQ-RELAY-002 fn message_new_defaults() { let m = Message::new(Protocol::Rcp, "stream-0001", vec![1, 2, 3]); assert_eq!(m.id, "stream-0001"); @@ -293,7 +293,7 @@ mod tests { } #[test] - // fusa:test REQ-RELAY-002 + //fusa:test REQ-RELAY-002 fn message_serde_base64_payload() { let m = Message::new(Protocol::Rcp, "stream-0002", vec![0xDE, 0xAD]); let json = serde_json::to_value(&m).unwrap(); @@ -302,14 +302,14 @@ mod tests { } #[test] - // fusa:test REQ-RELAY-006 + //fusa:test REQ-RELAY-006 fn context_background_not_done() { let ctx = Context::background(); assert!(!ctx.done()); } #[test] - // fusa:test REQ-RELAY-006 + //fusa:test REQ-RELAY-006 fn context_expired() { let ctx = Context::with_timeout(Duration::from_nanos(1)); std::thread::sleep(Duration::from_millis(1)); @@ -317,7 +317,7 @@ mod tests { } #[test] - // fusa:test REQ-RELAY-004 + //fusa:test REQ-RELAY-004 fn subscriber_options_chan_depth() { let opts = SubscriberOptions::default(); assert_eq!(opts.chan_depth(64), 64); @@ -329,7 +329,7 @@ mod tests { } #[test] - // fusa:test REQ-RELAY-003 + //fusa:test REQ-RELAY-003 fn back_pressure_default_is_drop_newest() { assert_eq!( BackPressurePolicy::default(), @@ -338,7 +338,7 @@ mod tests { } #[test] - // fusa:test REQ-RELAY-005 + //fusa:test REQ-RELAY-005 fn error_sentinels_are_distinct() { let sentinels = [ Error::Closed, diff --git a/src/request.rs b/src/request.rs index 10eae8f..3d75114 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,46 +1,46 @@ -// fusa:req REQ-CMP-001 -// fusa:req REQ-CMP-002 -// fusa:req REQ-CMP-003 -// fusa:req REQ-CMP-004 -// fusa:req REQ-CMP-005 -// fusa:req REQ-CMP-006 -// fusa:req REQ-CMP-007 -// fusa:req REQ-TRIG-001 -// fusa:req REQ-TRIG-002 -// fusa:req REQ-TRIG-003 -// fusa:req REQ-TRIG-004 -// fusa:req REQ-TRIG-005 -// fusa:req REQ-CHAIN-001 -// fusa:req REQ-CHAIN-002 -// fusa:req REQ-CHAIN-003 -// fusa:req REQ-TIME-001 -// fusa:req REQ-TIME-002 -// fusa:req REQ-TIME-003 -// fusa:req REQ-CANCEL-001 -// fusa:req REQ-CANCEL-002 -// fusa:req REQ-CANCEL-003 -// fusa:req REQ-CANCEL-004 -// fusa:req REQ-SEQ-001 -// fusa:req REQ-SEQ-002 -// fusa:req REQ-SEQ-003 -// fusa:req REQ-SEQ-004 -// fusa:req REQ-PRIO-001 -// fusa:req REQ-PRIO-002 -// fusa:req REQ-PRIO-003 -// fusa:req REQ-PRIO-004 -// fusa:req REQ-RLC-001 -// fusa:req REQ-RLC-002 -// fusa:req REQ-RLC-003 -// fusa:req REQ-RLC-004 -// fusa:req REQ-RLC-005 -// fusa:req REQ-RLC-006 -// fusa:req REQ-BUNDLE-001 -// fusa:req REQ-BUNDLE-002 -// fusa:req REQ-SAFETY-001 -// fusa:req REQ-SAFETY-002 -// fusa:req REQ-SAFETY-003 -// fusa:req REQ-SAFETY-004 -// fusa:req REQ-SAFETY-005 +//fusa:req REQ-CMP-001 +//fusa:req REQ-CMP-002 +//fusa:req REQ-CMP-003 +//fusa:req REQ-CMP-004 +//fusa:req REQ-CMP-005 +//fusa:req REQ-CMP-006 +//fusa:req REQ-CMP-007 +//fusa:req REQ-TRIG-001 +//fusa:req REQ-TRIG-002 +//fusa:req REQ-TRIG-003 +//fusa:req REQ-TRIG-004 +//fusa:req REQ-TRIG-005 +//fusa:req REQ-CHAIN-001 +//fusa:req REQ-CHAIN-002 +//fusa:req REQ-CHAIN-003 +//fusa:req REQ-TIME-001 +//fusa:req REQ-TIME-002 +//fusa:req REQ-TIME-003 +//fusa:req REQ-CANCEL-001 +//fusa:req REQ-CANCEL-002 +//fusa:req REQ-CANCEL-003 +//fusa:req REQ-CANCEL-004 +//fusa:req REQ-SEQ-001 +//fusa:req REQ-SEQ-002 +//fusa:req REQ-SEQ-003 +//fusa:req REQ-SEQ-004 +//fusa:req REQ-PRIO-001 +//fusa:req REQ-PRIO-002 +//fusa:req REQ-PRIO-003 +//fusa:req REQ-PRIO-004 +//fusa:req REQ-RLC-001 +//fusa:req REQ-RLC-002 +//fusa:req REQ-RLC-003 +//fusa:req REQ-RLC-004 +//fusa:req REQ-RLC-005 +//fusa:req REQ-RLC-006 +//fusa:req REQ-BUNDLE-001 +//fusa:req REQ-BUNDLE-002 +//fusa:req REQ-SAFETY-001 +//fusa:req REQ-SAFETY-002 +//fusa:req REQ-SAFETY-003 +//fusa:req REQ-SAFETY-004 +//fusa:req REQ-SAFETY-005 //! Conditional-request taxonomy: compound / compound-wait (`0x0F`/`0x0B`), //! triggered (`0x0E`), chained (`0x01`), timed (`0x0A`), the @@ -1315,12 +1315,13 @@ use crate::RcpError; /// less confidence than the other eight. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] -// fusa:req REQ-CMP-001 -// fusa:req REQ-TRIG-001 -// fusa:req REQ-CHAIN-001 -// fusa:req REQ-TIME-001 -// fusa:req REQ-CANCEL-001 -// fusa:req REQ-PRIO-001 +//fusa:req REQ-CMP-001 +//fusa:req REQ-TRIG-001 +//fusa:req REQ-CHAIN-001 +//fusa:req REQ-TIME-001 +//fusa:req REQ-CANCEL-001 +//fusa:req REQ-PRIO-001 +//fusa:req REQ-CMP-009 pub enum RequestKind { /// The crate-local placeholder discriminant byte assigned to /// [`RequestKind::Standard`] — see this module's doc comment @@ -1376,13 +1377,14 @@ pub enum RequestKind { impl RequestKind { /// Encode this request kind as its discriminant byte. - // fusa:req REQ-CMP-001 - // fusa:req REQ-TRIG-001 - // fusa:req REQ-CHAIN-001 - // fusa:req REQ-TIME-001 - // fusa:req REQ-CANCEL-001 - // fusa:req REQ-PRIO-001 - // fusa:req REQ-SAFETY-001 + //fusa:req REQ-CMP-001 + //fusa:req REQ-TRIG-001 + //fusa:req REQ-CHAIN-001 + //fusa:req REQ-TIME-001 + //fusa:req REQ-CANCEL-001 + //fusa:req REQ-PRIO-001 + //fusa:req REQ-SAFETY-001 + //fusa:req REQ-CMP-009 pub fn to_u8(self) -> u8 { self as u8 } @@ -1391,13 +1393,15 @@ impl RequestKind { /// /// Returns `Err(RcpError::InvalidParameter)` for any value other than /// the named discriminants. Never panics for any input. - // fusa:req REQ-CMP-002 - // fusa:req REQ-TRIG-001 - // fusa:req REQ-CHAIN-001 - // fusa:req REQ-TIME-001 - // fusa:req REQ-CANCEL-001 - // fusa:req REQ-PRIO-001 - // fusa:req REQ-SAFETY-001 + //fusa:req REQ-CMP-002 + //fusa:req REQ-TRIG-001 + //fusa:req REQ-CHAIN-001 + //fusa:req REQ-TIME-001 + //fusa:req REQ-CANCEL-001 + //fusa:req REQ-PRIO-001 + //fusa:req REQ-SAFETY-001 + //fusa:req REQ-CMP-009 + //fusa:req REQ-ERRH-001 pub fn from_u8(raw: u8) -> Result { match raw { 0x00 => Ok(Self::Standard), @@ -1423,7 +1427,7 @@ impl RequestKind { /// [`check_watchdog_overflow_purge`]/ /// [`purge_normal_priority_on_watchdog_overflow`] to exempt these three /// from the watchdog-overflow purge. Never panics for any input. - // fusa:req REQ-SAFETY-002 + //fusa:req REQ-SAFETY-002 pub fn is_safety_tagged(self) -> bool { matches!( self, @@ -1456,7 +1460,7 @@ impl RequestKind { /// discriminants, which — unlike `0x00` — have no such ambiguity: a /// standard request's `message_timestamp` is not expected to collide with /// one of them. Never panics for any input. - // fusa:req REQ-CMP-008 + //fusa:req REQ-CMP-008 pub fn from_gbb_message_timestamp(message_timestamp: u64) -> Option { let raw = (message_timestamp >> 56) as u8; if raw == 0x00 { @@ -1478,7 +1482,7 @@ impl RequestKind { /// discriminant"), so this function refuses to inject its `0x00` /// placeholder discriminant into a real `message_timestamp` value as /// though it meant something on the wire. Never panics for any input. - // fusa:req REQ-CMP-008 + //fusa:req REQ-CMP-008 pub fn to_gbb_message_timestamp(self, message_timestamp: u64) -> Result { if self == Self::Standard { return Err(RcpError::InvalidParameter); @@ -1497,14 +1501,15 @@ impl RequestKind { /// and the not-yet-built sequencer-state machine" for why this crate does /// not yet read this value from an actual register. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-CMP-003 +//fusa:req REQ-CMP-003 +//fusa:req REQ-SEQ-005 pub struct SequencerState(pub u8); /// A compound/compound-wait request's sequencer gate: which sequencer it /// names, and the persistent state that sequencer must hold for this /// request to execute. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-CMP-003 +//fusa:req REQ-CMP-003 pub struct CompoundGateConfig { /// The sequencer number this request is gated on, mirroring /// [`crate::regmap::RequestStreamConfigEntry::rx_safestate_sequencer`]'s @@ -1522,7 +1527,7 @@ pub struct CompoundGateConfig { /// less than `svr_sequencers_max` (`0` meaning no sequencers exist at all /// — every `sequencer_num` is then out of bounds). Never panics for any /// input. -// fusa:req REQ-CMP-004 +//fusa:req REQ-CMP-004 pub fn check_sequencer_num_in_bounds( sequencer_num: u8, svr_sequencers_max: u8, @@ -1538,7 +1543,7 @@ pub fn check_sequencer_num_in_bounds( /// `current_state` equals `gate.start_state`. /// /// Never panics for any input. -// fusa:req REQ-CMP-005 +//fusa:req REQ-CMP-005 pub fn is_gate_satisfied(current_state: SequencerState, gate: &CompoundGateConfig) -> bool { current_state == gate.start_state } @@ -1551,8 +1556,9 @@ pub fn is_gate_satisfied(current_state: SequencerState, gate: &CompoundGateConfi /// sequencer number, or `Err(RcpError::RequestRejected)` if the sequencer /// is known but not currently in the request's start state. Never panics /// for any input. -// fusa:req REQ-CMP-004 -// fusa:req REQ-CMP-005 +//fusa:req REQ-CMP-004 +//fusa:req REQ-CMP-005 +//fusa:req REQ-ERRH-001 pub fn check_compound_gate( current_state: SequencerState, gate: &CompoundGateConfig, @@ -1575,7 +1581,7 @@ pub fn check_compound_gate( /// `cmp_exec_delay`/`cmpw_exec_delay` width and units" for why both are /// plain `u32` placeholders. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-CMP-006 +//fusa:req REQ-CMP-006 pub struct CompoundExecDelays { /// The `0x0F` compound request kind's execution-delay timer. pub cmp_exec_delay: u32, @@ -1613,9 +1619,9 @@ pub struct CompoundExecDelays { /// (see this module's doc comment for why), so this is a safe /// additive-plumbing-stage widening, not a breaking change to any /// consumer. -// fusa:req REQ-CMP-006 -// fusa:req REQ-PRIO-002 -// fusa:req REQ-SAFETY-003 +//fusa:req REQ-CMP-006 +//fusa:req REQ-PRIO-002 +//fusa:req REQ-SAFETY-003 pub fn resolve_compound_exec_delay(kind: RequestKind, delays: &CompoundExecDelays) -> Option { match kind { RequestKind::Compound | RequestKind::SafetyCompound => Some(delays.cmp_exec_delay), @@ -1648,7 +1654,7 @@ pub fn resolve_compound_exec_delay(kind: RequestKind, delays: &CompoundExecDelay /// `next_state` is caller-supplied — see this module's doc comment for why /// no advancement convention (increment-by-one or otherwise) is guessed /// here. -// fusa:req REQ-CMP-007 +//fusa:req REQ-CMP-007 pub fn advance_sequencer_if_still_in_start_state( observed_state: SequencerState, gate: &CompoundGateConfig, @@ -1677,7 +1683,7 @@ pub fn advance_sequencer_if_still_in_start_state( /// [`advance_sequencer_if_still_in_start_state`]) this type composes rather /// than replaces. #[derive(Debug, Clone, PartialEq, Eq)] -// fusa:req REQ-SEQ-001 +//fusa:req REQ-SEQ-001 pub struct SequencerBank { states: Vec, } @@ -1693,7 +1699,8 @@ impl SequencerBank { /// exist" reading, so every [`Self::read`]/advance call against it /// returns `Err(RcpError::SequencerNotKnown)`. Never panics for any /// input. - // fusa:req REQ-SEQ-001 + //fusa:req REQ-SEQ-001 + //fusa:req REQ-SEQ-005 pub fn new(svr_sequencers_max: u8) -> Self { let power_on_state = SequencerState(SequencerStateEntry::power_on_default().seq_state); Self { @@ -1703,7 +1710,7 @@ impl SequencerBank { /// This bank's live sequencer-count bound — the `svr_sequencers_max` /// value it was constructed with via [`Self::new`]. - // fusa:req REQ-SEQ-001 + //fusa:req REQ-SEQ-001 pub fn svr_sequencers_max(&self) -> u8 { // `Self::new` takes `svr_sequencers_max` as a `u8`, so `states.len()` // never exceeds `u8::MAX` and this cast never truncates. @@ -1716,7 +1723,7 @@ impl SequencerBank { /// or beyond this bank's bound, reusing /// [`check_sequencer_num_in_bounds`]'s existing bound check rather than /// re-deriving it. Never panics for any input. - // fusa:req REQ-SEQ-002 + //fusa:req REQ-SEQ-002 pub fn read(&self, sequencer_num: u8) -> Result { check_sequencer_num_in_bounds(sequencer_num, self.svr_sequencers_max())?; Ok(self.states[sequencer_num as usize]) @@ -1735,7 +1742,7 @@ impl SequencerBank { /// (this bank is left unchanged) if some other request raced ahead and /// moved the sequencer out of that state first. Never panics for any /// input. - // fusa:req REQ-SEQ-003 + //fusa:req REQ-SEQ-003 pub fn advance_if_still_in_start_state( &mut self, gate: &CompoundGateConfig, @@ -1761,7 +1768,7 @@ impl SequencerBank { /// `gate.sequencer_num`, or `Err(RcpError::RequestRejected)` if the /// sequencer is known but not currently in `gate.start_state`. Never /// panics for any input. - // fusa:req REQ-SEQ-004 + //fusa:req REQ-SEQ-004 pub fn check_compound_gate(&self, gate: &CompoundGateConfig) -> Result<(), RcpError> { let current_state = self.read(gate.sequencer_num)?; check_compound_gate(current_state, gate, self.svr_sequencers_max()) @@ -1778,7 +1785,7 @@ impl SequencerBank { /// regardless of what the sequencer currently holds. Returns /// `Err(RcpError::SequencerNotKnown)` for an out-of-bounds /// `sequencer_num`. Never panics for any input. - // fusa:req REQ-SAFEMEAS-004 + //fusa:req REQ-SAFEMEAS-004 pub fn force_state( &mut self, sequencer_num: u8, @@ -1798,7 +1805,7 @@ impl SequencerBank { /// See this module's doc comment "Provenance note: exec-delay timer width /// and units" for why this is a plain `u32` placeholder. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-TRIG-002 +//fusa:req REQ-TRIG-002 pub struct TriggerExecDelay(pub u32); /// Select the execution-delay timer that applies to `kind`, if any. @@ -1809,9 +1816,9 @@ pub struct TriggerExecDelay(pub u32); /// per-kind-timer-selection shape, including [`RequestKind::Standard`] (a /// ninth and final variant, added alongside [`ExecutionPriorityTier`]). /// Never panics for any input. -// fusa:req REQ-TRIG-002 -// fusa:req REQ-PRIO-002 -// fusa:req REQ-SAFETY-003 +//fusa:req REQ-TRIG-002 +//fusa:req REQ-PRIO-002 +//fusa:req REQ-SAFETY-003 pub fn resolve_trigger_exec_delay(kind: RequestKind, delay: TriggerExecDelay) -> Option { match kind { RequestKind::Triggered | RequestKind::SafetyTriggered => Some(delay.0), @@ -1838,7 +1845,7 @@ pub fn resolve_trigger_exec_delay(kind: RequestKind, delay: TriggerExecDelay) -> /// sentinel" for the reasoning behind modeling this as an explicit enum, /// and for why a directly-constructed `Finite(0xFFFF)` does not round-trip. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// fusa:req REQ-TRIG-003 +//fusa:req REQ-TRIG-003 pub enum TriggerRepeatCount { /// A finite number of trigger occurrences this request repeats for. Finite(u16), @@ -1849,7 +1856,7 @@ pub enum TriggerRepeatCount { /// The raw wire value `ROADMAP.md`'s checklist bullet names as the /// infinite-repeat sentinel for a Triggered request's occurrence count. -// fusa:req REQ-TRIG-003 +//fusa:req REQ-TRIG-003 pub const TRIGGER_REPEAT_COUNT_INFINITE_SENTINEL: u16 = 0xFFFF; impl TriggerRepeatCount { @@ -1857,7 +1864,7 @@ impl TriggerRepeatCount { /// [`TriggerRepeatCount`]: [`Self::Infinite`] for /// [`TRIGGER_REPEAT_COUNT_INFINITE_SENTINEL`], [`Self::Finite`] /// otherwise. Never panics for any input. - // fusa:req REQ-TRIG-003 + //fusa:req REQ-TRIG-003 pub fn from_u16(raw: u16) -> Self { if raw == TRIGGER_REPEAT_COUNT_INFINITE_SENTINEL { Self::Infinite @@ -1872,7 +1879,7 @@ impl TriggerRepeatCount { /// sentinel" for why `Self::Finite(0xFFFF)` — not reachable via /// [`Self::from_u16`] — encodes to the same sentinel value as /// [`Self::Infinite`] rather than round-tripping to itself. - // fusa:req REQ-TRIG-003 + //fusa:req REQ-TRIG-003 pub fn to_u16(self) -> u16 { match self { Self::Finite(n) => n, @@ -1885,7 +1892,7 @@ impl TriggerRepeatCount { /// always `false` for [`TriggerRepeatCount::Infinite`] (it never exhausts on /// its own), and `true` once `occurrences_so_far` has reached or passed the /// finite configured target. Never panics for any input. -// fusa:req REQ-TRIG-004 +//fusa:req REQ-TRIG-004 pub fn is_trigger_repeat_exhausted( occurrences_so_far: u16, repeat_count: TriggerRepeatCount, @@ -1908,7 +1915,7 @@ pub fn is_trigger_repeat_exhausted( /// See this module's doc comment "Provenance note: busy/idle independence /// as a caller-supplied parameter" for why `endpoint_busy` is taken (and /// deliberately ignored) rather than omitted outright. -// fusa:req REQ-TRIG-005 +//fusa:req REQ-TRIG-005 pub fn should_count_trigger_occurrence(endpoint_busy: bool) -> bool { let _ = endpoint_busy; true @@ -1936,7 +1943,8 @@ pub fn should_count_trigger_occurrence(endpoint_busy: bool) -> bool { /// and "Provenance note: `CHAIN_ABORTED`/`CHAIN_ERROR` as new variants, and /// the distinction between them" for why this function only ever /// constructs [`RcpError::ChainAborted`], never [`RcpError::ChainError`]. -// fusa:req REQ-CHAIN-002 +//fusa:req REQ-CHAIN-002 +//fusa:req REQ-ERRH-001 pub fn check_chain_continuation(cs: bool, predecessor_errored: bool) -> Result<(), RcpError> { if cs && predecessor_errored { Err(RcpError::ChainAborted) @@ -1959,7 +1967,7 @@ pub fn check_chain_continuation(cs: bool, predecessor_errored: bool) -> Result<( /// duplicating its shape or introducing a new unconfirmed-width /// placeholder. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-TIME-002 +//fusa:req REQ-TIME-002 pub struct TimedExecutionTime(pub AvtpTimestamp); /// Whether a Timed request is ready to execute: `true` once `current` — a @@ -1978,7 +1986,7 @@ pub struct TimedExecutionTime(pub AvtpTimestamp); /// reads that case as always ready — mirroring the same fallback rule /// rather than treating an untimed `exec_time` as an unreachable instant /// far in the past or future. Never panics for any input. -// fusa:req REQ-TIME-003 +//fusa:req REQ-TIME-003 pub fn is_timed_request_ready(current: AvtpTimestamp, exec_time: TimedExecutionTime) -> bool { if exec_time.0.is_untimed() { return true; @@ -2000,7 +2008,8 @@ pub fn is_timed_request_ready(current: AvtpTimestamp, exec_time: TimedExecutionT /// `RequestCanceled` as this item's outcome signal" for why this /// constructs [`RcpError::RequestCanceled`] rather than /// [`RcpError::RequestRejected`] or a new variant of its own. -// fusa:req REQ-CANCEL-002 +//fusa:req REQ-CANCEL-002 +//fusa:req REQ-ERRH-001 pub fn check_clear_all_cancellation() -> Result<(), RcpError> { Err(RcpError::RequestCanceled) } @@ -2018,7 +2027,7 @@ pub fn check_clear_all_cancellation() -> Result<(), RcpError> { /// predicate as a caller-supplied parameter" for why `is_safestate_related` /// is taken as a plain caller-supplied `bool` rather than read from this /// crate's not-yet-built `rx_safety_measure`/safe-state machinery. -// fusa:req REQ-CANCEL-003 +//fusa:req REQ-CANCEL-003 pub fn check_clear_non_safestate_cancellation(is_safestate_related: bool) -> Result<(), RcpError> { if is_safestate_related { Ok(()) @@ -2035,7 +2044,7 @@ pub fn check_clear_non_safestate_cancellation(is_safestate_related: bool) -> Res /// `u8` matching [`crate::acf::ByteMessageInfo::transaction_num`]'s own /// width. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-CANCEL-004 +//fusa:req REQ-CANCEL-004 pub struct ClearTransactionNum(pub u8); /// The clear-single (`0x07`, optional) cancellation rule this checklist @@ -2048,7 +2057,7 @@ pub struct ClearTransactionNum(pub u8); /// `Err(RcpError::RequestCanceled)` when `candidate_transaction_num` equals /// `target.0`, and `Ok(())` (do not cancel) otherwise. Never panics for any /// input. -// fusa:req REQ-CANCEL-004 +//fusa:req REQ-CANCEL-004 pub fn check_clear_single_cancellation( candidate_transaction_num: u8, target: ClearTransactionNum, @@ -2079,7 +2088,7 @@ pub fn check_clear_single_cancellation( /// later-declared, lower-priority tier; [`select_next_pending_request`] /// relies on that ordering directly rather than re-deriving a rank number. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -// fusa:req REQ-PRIO-003 +//fusa:req REQ-PRIO-003 pub enum ExecutionPriorityTier { /// Highest priority: the cancellation trio (clear-all, /// clear-non-safestate, clear-single). @@ -2107,8 +2116,8 @@ pub enum ExecutionPriorityTier { /// comment "Provenance note: execution-priority tier and exec-delay-timer /// treatment for the three new variants" for why. Never panics for any /// input. -// fusa:req REQ-PRIO-003 -// fusa:req REQ-SAFETY-003 +//fusa:req REQ-PRIO-003 +//fusa:req REQ-SAFETY-003 pub fn execution_priority_tier(kind: RequestKind) -> ExecutionPriorityTier { match kind { RequestKind::ClearAll | RequestKind::ClearNonSafestate | RequestKind::ClearSingle => { @@ -2133,7 +2142,7 @@ pub fn execution_priority_tier(kind: RequestKind) -> ExecutionPriorityTier { /// caller-supplied sequence number" for why `arrival_seq` is a plain `u64` /// rather than this module owning a queue data structure of its own. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// fusa:req REQ-PRIO-004 +//fusa:req REQ-PRIO-004 pub struct PendingRequestKey { /// This pending request's kind, which [`execution_priority_tier`] maps /// to the tier it competes within. @@ -2157,7 +2166,7 @@ pub struct PendingRequestKey { /// comment "Provenance note: what execution priority ordering does not /// decide" for the scope and starvation/overflow questions this function /// deliberately does not answer. -// fusa:req REQ-PRIO-004 +//fusa:req REQ-PRIO-004 pub fn select_next_pending_request(pending: &[PendingRequestKey]) -> Option { pending .iter() @@ -2178,7 +2187,7 @@ pub fn select_next_pending_request(pending: &[PendingRequestKey]) -> Option `Finalized`) is unconditional for every kind. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// fusa:req REQ-RLC-003 -// fusa:req REQ-RLC-004 +//fusa:req REQ-RLC-003 +//fusa:req REQ-RLC-004 pub enum RequestLifecycleGuardInput { /// [`RequestKind::Standard`]: no gate at either hop. Standard, @@ -2322,9 +2331,9 @@ pub enum RequestLifecycleGuardInput { /// every [`RequestLifecycleGuardInput::Standard`]/`ClearAll`/ /// `ClearNonSafestate`/`ClearSingle` input at either guarded hop — is an /// unconditional pass. Never panics for any input. -// fusa:req REQ-RLC-003 -// fusa:req REQ-RLC-004 -// fusa:req REQ-RLC-005 +//fusa:req REQ-RLC-003 +//fusa:req REQ-RLC-004 +//fusa:req REQ-RLC-005 fn request_lifecycle_transition_guard( to: RequestLifecycleState, input: &RequestLifecycleGuardInput, @@ -2406,7 +2415,7 @@ impl RequestLifecycleState { /// request's guard is type-specific, so `input` carries kind-aware /// data rather than a single opaque predicate — see /// [`RequestLifecycleGuardInput`]. - // fusa:req REQ-RLC-002 + //fusa:req REQ-RLC-002 pub fn try_transition( self, target: Self, @@ -2436,7 +2445,7 @@ impl RequestLifecycleState { /// that check is mandatory and unconditional, `*current` always becomes /// `Finalized` and `Err(RcpError::RequestCanceled)` is always returned. /// Never panics for any input. -// fusa:req REQ-RLC-006 +//fusa:req REQ-RLC-006 pub fn try_force_cancel_all(current: &mut RequestLifecycleState) -> Result<(), RcpError> { if *current == RequestLifecycleState::Finalized { return Ok(()); @@ -2456,7 +2465,7 @@ pub fn try_force_cancel_all(current: &mut RequestLifecycleState) -> Result<(), R /// [`check_clear_non_safestate_cancellation`]. A `current` already at /// [`RequestLifecycleState::Finalized`] is left unchanged. Never panics /// for any input. -// fusa:req REQ-RLC-006 +//fusa:req REQ-RLC-006 pub fn try_force_cancel_non_safestate( current: &mut RequestLifecycleState, is_safestate_related: bool, @@ -2478,7 +2487,7 @@ pub fn try_force_cancel_non_safestate( /// `target`, delegating to [`check_clear_single_cancellation`]. A /// `current` already at [`RequestLifecycleState::Finalized`] is left /// unchanged. Never panics for any input. -// fusa:req REQ-RLC-006 +//fusa:req REQ-RLC-006 pub fn try_force_cancel_single( current: &mut RequestLifecycleState, candidate_transaction_num: u8, @@ -2537,8 +2546,8 @@ pub const MIN_SEQUENCERS_FOR_COMPOUND_BUNDLE: u8 = 4; /// than read from a live [`crate::regmap::GeneralRegisters`], and /// "Provenance note: `InvalidParameter` as the compound-bundle gate's /// rejection code" for the error-code choice. -// fusa:req REQ-BUNDLE-001 -// fusa:req REQ-BUNDLE-002 +//fusa:req REQ-BUNDLE-001 +//fusa:req REQ-BUNDLE-002 pub fn check_compound_bundle_claim( has_compound_wait: bool, svr_sequencers_max: u8, @@ -2569,7 +2578,7 @@ pub fn check_compound_bundle_claim( /// "Provenance note: `RequestCanceled` as this item's outcome signal" — /// only when `watchdog_overflowed` is `true` and `kind` is not /// safety-tagged. Never panics for any input. -// fusa:req REQ-SAFETY-004 +//fusa:req REQ-SAFETY-004 pub fn check_watchdog_overflow_purge( kind: RequestKind, watchdog_overflowed: bool, @@ -2601,7 +2610,7 @@ pub fn check_watchdog_overflow_purge( /// watchdog-overflow purge" section for how a kept-queued safety-tagged /// request composes with the rest of this crate's already-built /// pending-request machinery. -// fusa:req REQ-SAFETY-005 +//fusa:req REQ-SAFETY-005 pub fn purge_normal_priority_on_watchdog_overflow( pending: &[PendingRequestKey], watchdog_overflowed: bool, @@ -2623,7 +2632,7 @@ pub fn purge_normal_priority_on_watchdog_overflow( /// stream, selected by `rx_enforce_e2e`: see [`e2e_failure_scope`]/ /// [`check_rx_enforce_e2e`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// fusa:req REQ-E2EENF-001 +//fusa:req REQ-E2EENF-001 pub enum E2eFailureScope { /// Drop the one bad request; the rest of the stream is unaffected. DropRequest, @@ -2635,7 +2644,7 @@ pub enum E2eFailureScope { /// Select the [`E2eFailureScope`] `rx_enforce_e2e` names: [`Self::LatchStream`] /// when `rx_enforce_e2e` is `true`, [`Self::DropRequest`] otherwise. Never /// panics for any input. -// fusa:req REQ-E2EENF-001 +//fusa:req REQ-E2EENF-001 pub fn e2e_failure_scope(rx_enforce_e2e: bool) -> E2eFailureScope { if rx_enforce_e2e { E2eFailureScope::LatchStream @@ -2665,8 +2674,8 @@ pub fn e2e_failure_scope(rx_enforce_e2e: bool) -> E2eFailureScope { /// comment "Provenance note: `CrcError` as a new variant, distinct from the /// legacy `CrcMismatch` sentinel" for the full history (`ROADMAP.md` /// Milestone 6, "`CRC_ERROR` error path"). -// fusa:req REQ-E2EENF-002 -// fusa:req REQ-CRC-011 +//fusa:req REQ-E2EENF-002 +//fusa:req REQ-CRC-011 pub fn check_rx_enforce_e2e( coverage_buffer: &[u8], expected_crc: u32, @@ -2686,7 +2695,7 @@ pub fn check_rx_enforce_e2e( /// The safe-state mechanism a stream uses when driven to safe state, /// selected by `rx_safety_measure`: see [`resolve_safe_state_mechanism`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// fusa:req REQ-SAFEMEAS-001 +//fusa:req REQ-SAFEMEAS-001 pub enum SafeStateMechanism { /// Force every I/O pin on the stream's endpoints to high-impedance. HiZAllPins, @@ -2702,7 +2711,7 @@ pub enum SafeStateMechanism { /// safe state as a gate write, not a new mechanism" for why this reuses /// [`CompoundGateConfig`] rather than a safe-state-only type. Never panics /// for any input. -// fusa:req REQ-SAFEMEAS-002 +//fusa:req REQ-SAFEMEAS-002 pub fn safe_state_sequencer_gate( rx_safestate_sequencer: u8, rx_safe_sequencer_state: u8, @@ -2718,7 +2727,7 @@ pub fn safe_state_sequencer_gate( /// [`safe_state_sequencer_gate`]'s result) when `rx_safety_measure` is /// `true`, [`SafeStateMechanism::HiZAllPins`] otherwise. Never panics for /// any input. -// fusa:req REQ-SAFEMEAS-001 +//fusa:req REQ-SAFEMEAS-001 pub fn resolve_safe_state_mechanism( rx_safety_measure: bool, rx_safestate_sequencer: u8, @@ -2741,7 +2750,7 @@ pub fn resolve_safe_state_mechanism( /// /// Returns `Err(RcpError::SequencerNotKnown)` for an out-of-bounds /// `gate.sequencer_num`. Never panics for any input. -// fusa:req REQ-SAFEMEAS-003 +//fusa:req REQ-SAFEMEAS-003 pub fn enter_sequencer_driven_safe_state( bank: &mut SequencerBank, gate: &CompoundGateConfig, @@ -2754,7 +2763,7 @@ pub fn enter_sequencer_driven_safe_state( /// The result of evaluating a request-storage overflow against /// `rx_ovrflw_safestate_enable`: see [`evaluate_request_storage_overflow`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// fusa:req REQ-OVRFLW-001 +//fusa:req REQ-OVRFLW-001 pub enum OverflowOutcome { /// No overflow occurred. NoOverflow, @@ -2768,14 +2777,14 @@ pub enum OverflowOutcome { impl OverflowOutcome { /// True for either overflow variant. Never panics for any input. - // fusa:req REQ-OVRFLW-002 + //fusa:req REQ-OVRFLW-002 pub fn is_overflow(&self) -> bool { !matches!(self, Self::NoOverflow) } /// True only for [`Self::OverflowSafestate`]. Never panics for any /// input. - // fusa:req REQ-OVRFLW-002 + //fusa:req REQ-OVRFLW-002 pub fn drives_safestate(&self) -> bool { matches!(self, Self::OverflowSafestate) } @@ -2790,7 +2799,7 @@ impl OverflowOutcome { /// [`OverflowOutcome::OverflowSafestate`] or /// [`OverflowOutcome::OverflowNoSafestate`], selected by /// `rx_ovrflw_safestate_enable`. Never panics for any input. -// fusa:req REQ-OVRFLW-003 +//fusa:req REQ-OVRFLW-003 pub fn evaluate_request_storage_overflow( storage_overflowed: bool, rx_ovrflw_safestate_enable: bool, @@ -2810,7 +2819,7 @@ pub fn evaluate_request_storage_overflow( /// `rx_enforce_seq`/`rx_seq_safestate_enable`: see /// [`evaluate_rx_enforce_seq`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// fusa:req REQ-SEQENF-001 +//fusa:req REQ-SEQENF-001 pub enum SequenceEnforcementOutcome { /// Enforcement is disabled, or the candidate sequence number strictly /// exceeds the last accepted one: queue the request. @@ -2826,14 +2835,14 @@ pub enum SequenceEnforcementOutcome { impl SequenceEnforcementOutcome { /// True for either rejected variant. Never panics for any input. - // fusa:req REQ-SEQENF-002 + //fusa:req REQ-SEQENF-002 pub fn is_rejected(&self) -> bool { !matches!(self, Self::Accepted) } /// True only for [`Self::RejectedSafestate`]. Never panics for any /// input. - // fusa:req REQ-SEQENF-002 + //fusa:req REQ-SEQENF-002 pub fn drives_safestate(&self) -> bool { matches!(self, Self::RejectedSafestate) } @@ -2853,7 +2862,7 @@ impl SequenceEnforcementOutcome { /// module's doc comment "Provenance note: the enforced sequence number's /// own wire field and width" for why `last_accepted_seq`/`candidate_seq` /// are plain caller-supplied `u32` values. -// fusa:req REQ-SEQENF-003 +//fusa:req REQ-SEQENF-003 pub fn evaluate_rx_enforce_seq( last_accepted_seq: u32, candidate_seq: u32, @@ -2876,7 +2885,7 @@ pub fn evaluate_rx_enforce_seq( /// decided a stream should enter safe state: see /// [`resolve_safe_state_action`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// fusa:req REQ-SAFEACT-001 +//fusa:req REQ-SAFEACT-001 pub enum SafeStateAction { /// No safe-state entry is called for. None, @@ -2900,7 +2909,7 @@ pub enum SafeStateAction { /// [`SafeStateAction::ForceHiZAllPins`] or /// [`SafeStateAction::ForceSequencerState`], mirroring `mechanism`. Never /// panics for any input. -// fusa:req REQ-SAFEACT-002 +//fusa:req REQ-SAFEACT-002 pub fn resolve_safe_state_action( should_enter_safe_state: bool, mechanism: SafeStateMechanism, @@ -2936,13 +2945,13 @@ mod tests { ]; #[test] - // fusa:test REQ-CMP-001 - // fusa:test REQ-TRIG-001 - // fusa:test REQ-CHAIN-001 - // fusa:test REQ-TIME-001 - // fusa:test REQ-CANCEL-001 - // fusa:test REQ-PRIO-001 - // fusa:test REQ-SAFETY-001 + //fusa:test REQ-CMP-001 + //fusa:test REQ-TRIG-001 + //fusa:test REQ-CHAIN-001 + //fusa:test REQ-TIME-001 + //fusa:test REQ-CANCEL-001 + //fusa:test REQ-PRIO-001 + //fusa:test REQ-SAFETY-001 fn request_kind_round_trips_through_to_u8_from_u8() { for kind in ALL_REQUEST_KINDS { assert_eq!(RequestKind::from_u8(kind.to_u8()), Ok(kind)); @@ -2950,13 +2959,13 @@ mod tests { } #[test] - // fusa:test REQ-CMP-001 - // fusa:test REQ-TRIG-001 - // fusa:test REQ-CHAIN-001 - // fusa:test REQ-TIME-001 - // fusa:test REQ-CANCEL-001 - // fusa:test REQ-PRIO-001 - // fusa:test REQ-SAFETY-001 + //fusa:test REQ-CMP-001 + //fusa:test REQ-TRIG-001 + //fusa:test REQ-CHAIN-001 + //fusa:test REQ-TIME-001 + //fusa:test REQ-CANCEL-001 + //fusa:test REQ-PRIO-001 + //fusa:test REQ-SAFETY-001 fn request_kind_discriminants_match_roadmap_named_values() { assert_eq!(RequestKind::Compound.to_u8(), 0x0F); assert_eq!(RequestKind::CompoundWait.to_u8(), 0x0B); @@ -2978,7 +2987,7 @@ mod tests { } #[test] - // fusa:test REQ-SAFETY-001 + //fusa:test REQ-SAFETY-001 fn request_kind_safety_variants_are_exactly_0x80_or_their_base_kind() { assert_eq!( RequestKind::SafetyCompound.to_u8(), @@ -2995,8 +3004,8 @@ mod tests { } #[test] - // fusa:test REQ-CMP-002 - // fusa:test REQ-SAFETY-001 + //fusa:test REQ-CMP-002 + //fusa:test REQ-SAFETY-001 fn request_kind_from_u8_rejects_every_other_value() { for raw in [ 0x02u8, 0x03, 0x04, 0x08, 0x0C, 0x10, 0x7F, 0x80, 0x8A, 0x8C, 0x8D, 0xFF, @@ -3006,31 +3015,31 @@ mod tests { } #[test] - // fusa:test REQ-TRIG-001 + //fusa:test REQ-TRIG-001 fn request_kind_from_u8_accepts_triggered_discriminant() { assert_eq!(RequestKind::from_u8(0x0E), Ok(RequestKind::Triggered)); } #[test] - // fusa:test REQ-PRIO-001 + //fusa:test REQ-PRIO-001 fn request_kind_from_u8_accepts_standard_discriminant() { assert_eq!(RequestKind::from_u8(0x00), Ok(RequestKind::Standard)); } #[test] - // fusa:test REQ-CHAIN-001 + //fusa:test REQ-CHAIN-001 fn request_kind_from_u8_accepts_chained_discriminant() { assert_eq!(RequestKind::from_u8(0x01), Ok(RequestKind::Chained)); } #[test] - // fusa:test REQ-TIME-001 + //fusa:test REQ-TIME-001 fn request_kind_from_u8_accepts_timed_discriminant() { assert_eq!(RequestKind::from_u8(0x0A), Ok(RequestKind::Timed)); } #[test] - // fusa:test REQ-CANCEL-001 + //fusa:test REQ-CANCEL-001 fn request_kind_from_u8_accepts_all_three_cancellation_discriminants() { assert_eq!(RequestKind::from_u8(0x05), Ok(RequestKind::ClearAll)); assert_eq!( @@ -3041,7 +3050,7 @@ mod tests { } #[test] - // fusa:test REQ-CMP-002 + //fusa:test REQ-CMP-002 fn request_kind_from_u8_never_panics_across_the_full_byte_range() { for raw in 0u8..=255 { let _ = RequestKind::from_u8(raw); @@ -3065,7 +3074,7 @@ mod tests { ]; #[test] - // fusa:test REQ-CMP-008 + //fusa:test REQ-CMP-008 fn request_kind_gbb_message_timestamp_round_trips_for_every_non_standard_kind() { for kind in ALL_NON_STANDARD_REQUEST_KINDS { for message_timestamp in [0u64, u64::MAX, 0x0011_2233_4455_6677] { @@ -3076,7 +3085,7 @@ mod tests { } #[test] - // fusa:test REQ-CMP-008 + //fusa:test REQ-CMP-008 fn request_kind_gbb_message_timestamp_encode_preserves_low_56_bits() { let message_timestamp = 0x00AA_BBCC_DDEE_FF11; let encoded = RequestKind::Compound @@ -3087,7 +3096,7 @@ mod tests { } #[test] - // fusa:test REQ-CMP-008 + //fusa:test REQ-CMP-008 fn request_kind_gbb_message_timestamp_encode_rejects_standard() { assert_eq!( RequestKind::Standard.to_gbb_message_timestamp(0), @@ -3100,7 +3109,7 @@ mod tests { } #[test] - // fusa:test REQ-CMP-008 + //fusa:test REQ-CMP-008 fn request_kind_gbb_message_timestamp_decode_never_returns_standard() { // A leading byte of 0x00 is ambiguous between "genuinely a standard // request" and "a conditional request whose timestamp coincidentally @@ -3123,7 +3132,7 @@ mod tests { } #[test] - // fusa:test REQ-CMP-008 + //fusa:test REQ-CMP-008 fn request_kind_gbb_message_timestamp_decode_rejects_unrecognized_leading_byte() { for raw in [0x02u8, 0x03, 0x04, 0x08, 0x0C, 0x10, 0x7F, 0x80, 0xFF] { let ts = (raw as u64) << 56; @@ -3132,7 +3141,7 @@ mod tests { } #[test] - // fusa:test REQ-CMP-008 + //fusa:test REQ-CMP-008 fn request_kind_gbb_message_timestamp_decode_ignores_low_56_bits() { for kind in ALL_NON_STANDARD_REQUEST_KINDS { let leading = (kind.to_u8() as u64) << 56; @@ -3145,7 +3154,7 @@ mod tests { } #[test] - // fusa:test REQ-CMP-008 + //fusa:test REQ-CMP-008 fn request_kind_gbb_message_timestamp_decode_never_panics_across_the_full_byte_range() { for raw in 0u8..=255 { let ts = (raw as u64) << 56; @@ -3155,7 +3164,7 @@ mod tests { } #[test] - // fusa:test REQ-CMP-008 + //fusa:test REQ-CMP-008 fn request_kind_gbb_message_timestamp_encode_never_panics_for_any_kind_and_timestamp() { for kind in ALL_REQUEST_KINDS { for message_timestamp in [0u64, u64::MAX, 0x8000_0000_0000_0000] { @@ -3167,13 +3176,13 @@ mod tests { // ── SequencerState / CompoundGateConfig ────────────────────────────────── #[test] - // fusa:test REQ-CMP-003 + //fusa:test REQ-CMP-003 fn sequencer_state_default_is_zero() { assert_eq!(SequencerState::default(), SequencerState(0)); } #[test] - // fusa:test REQ-CMP-003 + //fusa:test REQ-CMP-003 fn compound_gate_config_default_is_sequencer_zero_state_zero() { let gate = CompoundGateConfig::default(); assert_eq!(gate.sequencer_num, 0); @@ -3183,7 +3192,7 @@ mod tests { // ── check_sequencer_num_in_bounds ──────────────────────────────────────── #[test] - // fusa:test REQ-CMP-004 + //fusa:test REQ-CMP-004 fn check_sequencer_num_in_bounds_accepts_every_num_below_max() { for max in [1u8, 4, 255] { for num in 0..max { @@ -3193,7 +3202,7 @@ mod tests { } #[test] - // fusa:test REQ-CMP-004 + //fusa:test REQ-CMP-004 fn check_sequencer_num_in_bounds_rejects_num_at_or_above_max() { for (num, max) in [(0u8, 0u8), (4, 4), (5, 4), (255, 4)] { assert_eq!( @@ -3204,7 +3213,7 @@ mod tests { } #[test] - // fusa:test REQ-CMP-004 + //fusa:test REQ-CMP-004 fn check_sequencer_num_in_bounds_never_panics_for_any_sampled_pair() { for num in [0u8, 1, 127, 255] { for max in [0u8, 1, 127, 255] { @@ -3223,7 +3232,7 @@ mod tests { } #[test] - // fusa:test REQ-CMP-005 + //fusa:test REQ-CMP-005 fn is_gate_satisfied_true_only_when_current_state_matches_start_state() { let gate = sample_gate(); assert!(is_gate_satisfied(SequencerState(1), &gate)); @@ -3232,14 +3241,14 @@ mod tests { } #[test] - // fusa:test REQ-CMP-005 + //fusa:test REQ-CMP-005 fn check_compound_gate_ok_when_sequencer_known_and_state_matches() { let gate = sample_gate(); assert_eq!(check_compound_gate(SequencerState(1), &gate, 4), Ok(())); } #[test] - // fusa:test REQ-CMP-004 + //fusa:test REQ-CMP-004 fn check_compound_gate_rejects_out_of_bounds_sequencer_before_checking_state() { let gate = sample_gate(); // svr_sequencers_max of 2 puts sequencer_num 2 out of bounds, even @@ -3251,7 +3260,7 @@ mod tests { } #[test] - // fusa:test REQ-CMP-005 + //fusa:test REQ-CMP-005 fn check_compound_gate_rejects_mismatched_state_for_a_known_sequencer() { let gate = sample_gate(); assert_eq!( @@ -3261,7 +3270,7 @@ mod tests { } #[test] - // fusa:test REQ-CMP-005 + //fusa:test REQ-CMP-005 fn check_compound_gate_never_panics_for_any_sampled_input() { let gate = sample_gate(); for state in [0u8, 1, 2, 255] { @@ -3274,7 +3283,7 @@ mod tests { // ── CompoundExecDelays / resolve_compound_exec_delay ───────────────────── #[test] - // fusa:test REQ-CMP-006 + //fusa:test REQ-CMP-006 fn compound_exec_delays_default_is_zero_for_both_timers() { let delays = CompoundExecDelays::default(); assert_eq!(delays.cmp_exec_delay, 0); @@ -3282,7 +3291,7 @@ mod tests { } #[test] - // fusa:test REQ-CMP-006 + //fusa:test REQ-CMP-006 fn resolve_compound_exec_delay_selects_the_matching_timer() { let delays = CompoundExecDelays { cmp_exec_delay: 100, @@ -3299,7 +3308,7 @@ mod tests { } #[test] - // fusa:test REQ-SAFETY-003 + //fusa:test REQ-SAFETY-003 fn resolve_compound_exec_delay_matches_the_safety_tagged_variants_base_kind() { let delays = CompoundExecDelays { cmp_exec_delay: 100, @@ -3320,7 +3329,7 @@ mod tests { } #[test] - // fusa:test REQ-CMP-006 + //fusa:test REQ-CMP-006 fn resolve_compound_exec_delay_is_none_for_triggered() { let delays = CompoundExecDelays { cmp_exec_delay: 100, @@ -3333,8 +3342,8 @@ mod tests { } #[test] - // fusa:test REQ-CMP-006 - // fusa:test REQ-CHAIN-001 + //fusa:test REQ-CMP-006 + //fusa:test REQ-CHAIN-001 fn resolve_compound_exec_delay_is_none_for_chained() { let delays = CompoundExecDelays { cmp_exec_delay: 100, @@ -3347,8 +3356,8 @@ mod tests { } #[test] - // fusa:test REQ-CMP-006 - // fusa:test REQ-TIME-001 + //fusa:test REQ-CMP-006 + //fusa:test REQ-TIME-001 fn resolve_compound_exec_delay_is_none_for_timed() { let delays = CompoundExecDelays { cmp_exec_delay: 100, @@ -3361,8 +3370,8 @@ mod tests { } #[test] - // fusa:test REQ-CMP-006 - // fusa:test REQ-CANCEL-001 + //fusa:test REQ-CMP-006 + //fusa:test REQ-CANCEL-001 fn resolve_compound_exec_delay_is_none_for_all_three_cancellation_kinds() { let delays = CompoundExecDelays { cmp_exec_delay: 100, @@ -3378,8 +3387,8 @@ mod tests { } #[test] - // fusa:test REQ-CMP-006 - // fusa:test REQ-PRIO-002 + //fusa:test REQ-CMP-006 + //fusa:test REQ-PRIO-002 fn resolve_compound_exec_delay_is_none_for_standard() { let delays = CompoundExecDelays { cmp_exec_delay: 100, @@ -3394,7 +3403,7 @@ mod tests { // ── advance_sequencer_if_still_in_start_state ──────────────────────────── #[test] - // fusa:test REQ-CMP-007 + //fusa:test REQ-CMP-007 fn advance_sequencer_advances_when_still_in_start_state() { let gate = sample_gate(); assert_eq!( @@ -3404,7 +3413,7 @@ mod tests { } #[test] - // fusa:test REQ-CMP-007 + //fusa:test REQ-CMP-007 fn advance_sequencer_refuses_when_race_moved_it_out_of_start_state() { let gate = sample_gate(); assert_eq!( @@ -3414,7 +3423,7 @@ mod tests { } #[test] - // fusa:test REQ-CMP-007 + //fusa:test REQ-CMP-007 fn advance_sequencer_never_panics_for_any_sampled_input() { let gate = sample_gate(); for observed in [0u8, 1, 9, 255] { @@ -3431,7 +3440,7 @@ mod tests { // ── SequencerBank ───────────────────────────────────────────────────────── #[test] - // fusa:test REQ-SEQ-001 + //fusa:test REQ-SEQ-001 fn sequencer_bank_new_sizes_the_bank_to_svr_sequencers_max() { for max in [0u8, 1, 4, 255] { let bank = SequencerBank::new(max); @@ -3440,7 +3449,7 @@ mod tests { } #[test] - // fusa:test REQ-SEQ-001 + //fusa:test REQ-SEQ-001 fn sequencer_bank_new_initializes_every_sequencer_to_the_power_on_default_state() { let bank = SequencerBank::new(4); for sequencer_num in 0..4u8 { @@ -3454,7 +3463,7 @@ mod tests { } #[test] - // fusa:test REQ-SEQ-001 + //fusa:test REQ-SEQ-001 fn sequencer_bank_new_with_zero_max_yields_an_empty_bank() { let bank = SequencerBank::new(0); assert_eq!(bank.svr_sequencers_max(), 0); @@ -3464,7 +3473,7 @@ mod tests { // ── SequencerBank::read ────────────────────────────────────────────────── #[test] - // fusa:test REQ-SEQ-002 + //fusa:test REQ-SEQ-002 fn sequencer_bank_read_rejects_sequencer_num_at_or_above_the_bound() { let bank = SequencerBank::new(4); for sequencer_num in [4u8, 5, 255] { @@ -3473,7 +3482,7 @@ mod tests { } #[test] - // fusa:test REQ-SEQ-002 + //fusa:test REQ-SEQ-002 fn sequencer_bank_read_never_panics_for_any_sampled_input() { let bank = SequencerBank::new(4); for sequencer_num in [0u8, 3, 4, 255] { @@ -3488,7 +3497,7 @@ mod tests { // ── SequencerBank::advance_if_still_in_start_state ─────────────────────── #[test] - // fusa:test REQ-SEQ-003 + //fusa:test REQ-SEQ-003 fn sequencer_bank_advance_mutates_the_store_when_still_in_start_state() { let mut bank = SequencerBank::new(4); let gate = CompoundGateConfig { @@ -3507,7 +3516,7 @@ mod tests { } #[test] - // fusa:test REQ-SEQ-003 + //fusa:test REQ-SEQ-003 fn sequencer_bank_advance_leaves_the_store_unchanged_when_race_lost() { let mut bank = SequencerBank::new(4); // First advance moves sequencer 2 out of start_state 1. @@ -3529,7 +3538,7 @@ mod tests { } #[test] - // fusa:test REQ-SEQ-003 + //fusa:test REQ-SEQ-003 fn sequencer_bank_advance_rejects_out_of_bounds_sequencer_num() { let mut bank = SequencerBank::new(2); let gate = CompoundGateConfig { @@ -3543,7 +3552,7 @@ mod tests { } #[test] - // fusa:test REQ-SEQ-003 + //fusa:test REQ-SEQ-003 fn sequencer_bank_advance_never_panics_for_any_sampled_input() { let mut bank = SequencerBank::new(4); for sequencer_num in [0u8, 3, 4, 255] { @@ -3560,7 +3569,7 @@ mod tests { // ── SequencerBank::check_compound_gate ─────────────────────────────────── #[test] - // fusa:test REQ-SEQ-004 + //fusa:test REQ-SEQ-004 fn sequencer_bank_check_compound_gate_ok_when_default_state_matches_gate() { let bank = SequencerBank::new(4); let gate = CompoundGateConfig { @@ -3571,7 +3580,7 @@ mod tests { } #[test] - // fusa:test REQ-SEQ-004 + //fusa:test REQ-SEQ-004 fn sequencer_bank_check_compound_gate_rejects_mismatched_state() { let bank = SequencerBank::new(4); let gate = CompoundGateConfig { @@ -3585,7 +3594,7 @@ mod tests { } #[test] - // fusa:test REQ-SEQ-004 + //fusa:test REQ-SEQ-004 fn sequencer_bank_check_compound_gate_rejects_out_of_bounds_sequencer() { let bank = SequencerBank::new(2); let gate = CompoundGateConfig { @@ -3599,7 +3608,7 @@ mod tests { } #[test] - // fusa:test REQ-SEQ-004 + //fusa:test REQ-SEQ-004 fn sequencer_bank_check_compound_gate_reflects_a_prior_advance() { let mut bank = SequencerBank::new(4); let gate = CompoundGateConfig { @@ -3623,7 +3632,7 @@ mod tests { } #[test] - // fusa:test REQ-SEQ-004 + //fusa:test REQ-SEQ-004 fn sequencer_bank_check_compound_gate_never_panics_for_any_sampled_input() { let bank = SequencerBank::new(4); for sequencer_num in [0u8, 3, 4, 255] { @@ -3640,13 +3649,13 @@ mod tests { // ── TriggerExecDelay / resolve_trigger_exec_delay ──────────────────────── #[test] - // fusa:test REQ-TRIG-002 + //fusa:test REQ-TRIG-002 fn trigger_exec_delay_default_is_zero() { assert_eq!(TriggerExecDelay::default(), TriggerExecDelay(0)); } #[test] - // fusa:test REQ-TRIG-002 + //fusa:test REQ-TRIG-002 fn resolve_trigger_exec_delay_selects_the_timer_only_for_triggered() { let delay = TriggerExecDelay(42); assert_eq!( @@ -3656,7 +3665,7 @@ mod tests { } #[test] - // fusa:test REQ-SAFETY-003 + //fusa:test REQ-SAFETY-003 fn resolve_trigger_exec_delay_matches_the_safety_tagged_variants_base_kind() { let delay = TriggerExecDelay(42); assert_eq!( @@ -3674,7 +3683,7 @@ mod tests { } #[test] - // fusa:test REQ-TRIG-002 + //fusa:test REQ-TRIG-002 fn resolve_trigger_exec_delay_is_none_for_every_other_kind() { let delay = TriggerExecDelay(42); assert_eq!( @@ -3693,8 +3702,8 @@ mod tests { } #[test] - // fusa:test REQ-TRIG-002 - // fusa:test REQ-CANCEL-001 + //fusa:test REQ-TRIG-002 + //fusa:test REQ-CANCEL-001 fn resolve_trigger_exec_delay_is_none_for_all_three_cancellation_kinds() { let delay = TriggerExecDelay(42); for kind in [ @@ -3707,8 +3716,8 @@ mod tests { } #[test] - // fusa:test REQ-TRIG-002 - // fusa:test REQ-PRIO-002 + //fusa:test REQ-TRIG-002 + //fusa:test REQ-PRIO-002 fn resolve_trigger_exec_delay_is_none_for_standard() { let delay = TriggerExecDelay(42); assert_eq!( @@ -3720,7 +3729,7 @@ mod tests { // ── TriggerRepeatCount ──────────────────────────────────────────────────── #[test] - // fusa:test REQ-TRIG-003 + //fusa:test REQ-TRIG-003 fn trigger_repeat_count_from_u16_maps_sentinel_to_infinite() { assert_eq!( TriggerRepeatCount::from_u16(TRIGGER_REPEAT_COUNT_INFINITE_SENTINEL), @@ -3733,7 +3742,7 @@ mod tests { } #[test] - // fusa:test REQ-TRIG-003 + //fusa:test REQ-TRIG-003 fn trigger_repeat_count_from_u16_maps_every_other_value_to_finite() { for raw in [0u16, 1, 42, 0xFFFE] { assert_eq!( @@ -3744,7 +3753,7 @@ mod tests { } #[test] - // fusa:test REQ-TRIG-003 + //fusa:test REQ-TRIG-003 fn trigger_repeat_count_finite_round_trips_through_to_u16_from_u16() { for raw in [0u16, 1, 42, 0xFFFE] { let count = TriggerRepeatCount::from_u16(raw); @@ -3753,7 +3762,7 @@ mod tests { } #[test] - // fusa:test REQ-TRIG-003 + //fusa:test REQ-TRIG-003 fn trigger_repeat_count_infinite_round_trips_through_to_u16_from_u16() { let count = TriggerRepeatCount::Infinite; assert_eq!(count.to_u16(), TRIGGER_REPEAT_COUNT_INFINITE_SENTINEL); @@ -3761,7 +3770,7 @@ mod tests { } #[test] - // fusa:test REQ-TRIG-003 + //fusa:test REQ-TRIG-003 fn trigger_repeat_count_directly_constructed_finite_sentinel_collapses_to_infinite() { // See this module's doc comment "Provenance note: the // infinite-repeat sentinel" — this is the one deliberate, @@ -3777,7 +3786,7 @@ mod tests { // ── is_trigger_repeat_exhausted ─────────────────────────────────────────── #[test] - // fusa:test REQ-TRIG-004 + //fusa:test REQ-TRIG-004 fn is_trigger_repeat_exhausted_is_always_false_for_infinite() { for occurrences in [0u16, 1, 100, u16::MAX] { assert!(!is_trigger_repeat_exhausted( @@ -3788,7 +3797,7 @@ mod tests { } #[test] - // fusa:test REQ-TRIG-004 + //fusa:test REQ-TRIG-004 fn is_trigger_repeat_exhausted_true_once_occurrences_reach_finite_target() { let target = TriggerRepeatCount::Finite(3); assert!(!is_trigger_repeat_exhausted(0, target)); @@ -3798,7 +3807,7 @@ mod tests { } #[test] - // fusa:test REQ-TRIG-004 + //fusa:test REQ-TRIG-004 fn is_trigger_repeat_exhausted_never_panics_for_any_sampled_input() { for occurrences in [0u16, 1, 3, u16::MAX] { for target in [0u16, 3, u16::MAX] { @@ -3812,7 +3821,7 @@ mod tests { // ── should_count_trigger_occurrence ─────────────────────────────────────── #[test] - // fusa:test REQ-TRIG-005 + //fusa:test REQ-TRIG-005 fn should_count_trigger_occurrence_is_always_true_regardless_of_busy_state() { assert!(should_count_trigger_occurrence(true)); assert!(should_count_trigger_occurrence(false)); @@ -3821,7 +3830,7 @@ mod tests { // ── check_chain_continuation ────────────────────────────────────────────── #[test] - // fusa:test REQ-CHAIN-002 + //fusa:test REQ-CHAIN-002 fn check_chain_continuation_aborts_only_when_cs_set_and_predecessor_errored() { assert_eq!( check_chain_continuation(true, true), @@ -3830,20 +3839,20 @@ mod tests { } #[test] - // fusa:test REQ-CHAIN-002 + //fusa:test REQ-CHAIN-002 fn check_chain_continuation_continues_when_cs_not_set_even_if_predecessor_errored() { assert_eq!(check_chain_continuation(false, true), Ok(())); } #[test] - // fusa:test REQ-CHAIN-002 + //fusa:test REQ-CHAIN-002 fn check_chain_continuation_continues_when_predecessor_did_not_error_regardless_of_cs() { assert_eq!(check_chain_continuation(true, false), Ok(())); assert_eq!(check_chain_continuation(false, false), Ok(())); } #[test] - // fusa:test REQ-CHAIN-002 + //fusa:test REQ-CHAIN-002 fn check_chain_continuation_never_panics_for_any_sampled_input() { for cs in [true, false] { for predecessor_errored in [true, false] { @@ -3855,7 +3864,7 @@ mod tests { // ── RcpError::ChainAborted / RcpError::ChainError ──────────────────────── #[test] - // fusa:test REQ-CHAIN-003 + //fusa:test REQ-CHAIN-003 fn chain_aborted_and_chain_error_are_distinct_rcp_error_variants() { assert_ne!(RcpError::ChainAborted, RcpError::ChainError); assert_eq!(RcpError::ChainAborted, RcpError::ChainAborted); @@ -3863,7 +3872,7 @@ mod tests { } #[test] - // fusa:test REQ-CHAIN-003 + //fusa:test REQ-CHAIN-003 fn chain_aborted_and_chain_error_carry_the_roadmap_named_codes_in_their_display_text() { assert!(RcpError::ChainAborted.to_string().contains("CHAIN_ABORTED")); assert!(RcpError::ChainError.to_string().contains("CHAIN_ERROR")); @@ -3872,7 +3881,7 @@ mod tests { // ── TimedExecutionTime / is_timed_request_ready ────────────────────────── #[test] - // fusa:test REQ-TIME-002 + //fusa:test REQ-TIME-002 fn timed_execution_time_default_is_untimed() { let exec_time = TimedExecutionTime::default(); assert_eq!(exec_time.0, AvtpTimestamp::default()); @@ -3880,35 +3889,35 @@ mod tests { } #[test] - // fusa:test REQ-TIME-002 + //fusa:test REQ-TIME-002 fn timed_execution_time_wraps_avtp_timestamp_by_value() { let exec_time = TimedExecutionTime(AvtpTimestamp::new(1_000)); assert_eq!(exec_time.0.to_u32(), 1_000); } #[test] - // fusa:test REQ-TIME-003 + //fusa:test REQ-TIME-003 fn is_timed_request_ready_false_before_exec_time_is_reached() { let exec_time = TimedExecutionTime(AvtpTimestamp::new(1_000)); assert!(!is_timed_request_ready(AvtpTimestamp::new(999), exec_time)); } #[test] - // fusa:test REQ-TIME-003 + //fusa:test REQ-TIME-003 fn is_timed_request_ready_true_exactly_at_exec_time() { let exec_time = TimedExecutionTime(AvtpTimestamp::new(1_000)); assert!(is_timed_request_ready(AvtpTimestamp::new(1_000), exec_time)); } #[test] - // fusa:test REQ-TIME-003 + //fusa:test REQ-TIME-003 fn is_timed_request_ready_true_after_exec_time_has_passed() { let exec_time = TimedExecutionTime(AvtpTimestamp::new(1_000)); assert!(is_timed_request_ready(AvtpTimestamp::new(1_001), exec_time)); } #[test] - // fusa:test REQ-TIME-003 + //fusa:test REQ-TIME-003 fn is_timed_request_ready_true_across_a_rollover() { // AvtpTimestamp::is_after is wraparound-aware; a current time that // just wrapped past u32::MAX back to a small value must still read @@ -3918,7 +3927,7 @@ mod tests { } #[test] - // fusa:test REQ-TIME-003 + //fusa:test REQ-TIME-003 fn is_timed_request_ready_always_true_for_an_untimed_exec_time() { let exec_time = TimedExecutionTime(AvtpTimestamp::default()); for current in [0u32, 1, 1_000, u32::MAX] { @@ -3930,7 +3939,7 @@ mod tests { } #[test] - // fusa:test REQ-TIME-003 + //fusa:test REQ-TIME-003 fn is_timed_request_ready_never_panics_for_any_sampled_input() { for current in [0u32, 1, 1_000, u32::MAX] { for target in [0u32, 1, 1_000, u32::MAX] { @@ -3945,7 +3954,7 @@ mod tests { // ── check_clear_all_cancellation ────────────────────────────────────────── #[test] - // fusa:test REQ-CANCEL-002 + //fusa:test REQ-CANCEL-002 fn check_clear_all_cancellation_always_cancels() { assert_eq!( check_clear_all_cancellation(), @@ -3956,13 +3965,13 @@ mod tests { // ── check_clear_non_safestate_cancellation ──────────────────────────────── #[test] - // fusa:test REQ-CANCEL-003 + //fusa:test REQ-CANCEL-003 fn check_clear_non_safestate_cancellation_spares_safestate_related_requests() { assert_eq!(check_clear_non_safestate_cancellation(true), Ok(())); } #[test] - // fusa:test REQ-CANCEL-003 + //fusa:test REQ-CANCEL-003 fn check_clear_non_safestate_cancellation_cancels_non_safestate_related_requests() { assert_eq!( check_clear_non_safestate_cancellation(false), @@ -3971,7 +3980,7 @@ mod tests { } #[test] - // fusa:test REQ-CANCEL-003 + //fusa:test REQ-CANCEL-003 fn check_clear_non_safestate_cancellation_never_panics_for_any_input() { for is_safestate_related in [true, false] { let _ = check_clear_non_safestate_cancellation(is_safestate_related); @@ -3981,13 +3990,13 @@ mod tests { // ── ClearTransactionNum / check_clear_single_cancellation ──────────────── #[test] - // fusa:test REQ-CANCEL-004 + //fusa:test REQ-CANCEL-004 fn clear_transaction_num_default_is_zero() { assert_eq!(ClearTransactionNum::default(), ClearTransactionNum(0)); } #[test] - // fusa:test REQ-CANCEL-004 + //fusa:test REQ-CANCEL-004 fn check_clear_single_cancellation_cancels_only_the_matching_transaction_num() { let target = ClearTransactionNum(0x42); assert_eq!( @@ -3997,7 +4006,7 @@ mod tests { } #[test] - // fusa:test REQ-CANCEL-004 + //fusa:test REQ-CANCEL-004 fn check_clear_single_cancellation_spares_every_non_matching_transaction_num() { let target = ClearTransactionNum(0x42); for candidate in [0x00u8, 0x01, 0x41, 0x43, 0xFF] { @@ -4006,7 +4015,7 @@ mod tests { } #[test] - // fusa:test REQ-CANCEL-004 + //fusa:test REQ-CANCEL-004 fn check_clear_single_cancellation_never_panics_for_any_sampled_input() { for candidate in [0x00u8, 0x42, 0xFF] { for target in [0x00u8, 0x42, 0xFF] { @@ -4018,7 +4027,7 @@ mod tests { // ── ExecutionPriorityTier / execution_priority_tier ────────────────────── #[test] - // fusa:test REQ-PRIO-003 + //fusa:test REQ-PRIO-003 fn execution_priority_tier_orders_tiers_cancellation_highest_standard_lowest() { // `ROADMAP.md`'s own stated order: cancellation > triggered > timed > // compound > compound-wait > chained > standard. Ord's derive makes @@ -4033,7 +4042,7 @@ mod tests { } #[test] - // fusa:test REQ-PRIO-003 + //fusa:test REQ-PRIO-003 fn execution_priority_tier_collapses_all_three_cancellation_kinds_onto_one_tier() { for kind in [ RequestKind::ClearAll, @@ -4048,7 +4057,7 @@ mod tests { } #[test] - // fusa:test REQ-PRIO-003 + //fusa:test REQ-PRIO-003 fn execution_priority_tier_maps_every_remaining_kind_to_its_own_named_tier() { assert_eq!( execution_priority_tier(RequestKind::Triggered), @@ -4077,7 +4086,7 @@ mod tests { } #[test] - // fusa:test REQ-SAFETY-003 + //fusa:test REQ-SAFETY-003 fn execution_priority_tier_maps_each_safety_tagged_variant_to_its_base_kinds_tier() { assert_eq!( execution_priority_tier(RequestKind::SafetyCompound), @@ -4094,7 +4103,7 @@ mod tests { } #[test] - // fusa:test REQ-PRIO-003 + //fusa:test REQ-PRIO-003 fn execution_priority_tier_never_panics_for_any_request_kind() { for kind in ALL_REQUEST_KINDS { let _ = execution_priority_tier(kind); @@ -4104,13 +4113,13 @@ mod tests { // ── PendingRequestKey / select_next_pending_request ────────────────────── #[test] - // fusa:test REQ-PRIO-004 + //fusa:test REQ-PRIO-004 fn select_next_pending_request_is_none_for_an_empty_slice() { assert_eq!(select_next_pending_request(&[]), None); } #[test] - // fusa:test REQ-PRIO-004 + //fusa:test REQ-PRIO-004 fn select_next_pending_request_picks_the_single_entry() { let pending = [PendingRequestKey { kind: RequestKind::Standard, @@ -4120,7 +4129,7 @@ mod tests { } #[test] - // fusa:test REQ-PRIO-004 + //fusa:test REQ-PRIO-004 fn select_next_pending_request_picks_the_highest_priority_tier_regardless_of_arrival_order() { // A later-arriving cancellation request must still win over an // earlier-arriving standard request — priority tier dominates FIFO, @@ -4143,7 +4152,7 @@ mod tests { } #[test] - // fusa:test REQ-PRIO-004 + //fusa:test REQ-PRIO-004 fn select_next_pending_request_respects_the_full_roadmap_tier_order() { // One entry per tier, deliberately listed out of priority order and // out of arrival order, so this test cannot pass by accident of @@ -4184,7 +4193,7 @@ mod tests { } #[test] - // fusa:test REQ-PRIO-004 + //fusa:test REQ-PRIO-004 fn select_next_pending_request_breaks_same_tier_ties_fifo_by_earliest_arrival() { let pending = [ PendingRequestKey { @@ -4206,7 +4215,7 @@ mod tests { } #[test] - // fusa:test REQ-PRIO-004 + //fusa:test REQ-PRIO-004 fn select_next_pending_request_prefers_cancellation_regardless_of_which_of_the_three_kinds() { for cancellation_kind in [ RequestKind::ClearAll, @@ -4228,7 +4237,7 @@ mod tests { } #[test] - // fusa:test REQ-PRIO-004 + //fusa:test REQ-PRIO-004 fn select_next_pending_request_never_panics_for_any_sampled_input() { for kind in ALL_REQUEST_KINDS { for arrival_seq in [0u64, 1, u64::MAX] { @@ -4248,7 +4257,7 @@ mod tests { ]; #[test] - // fusa:test REQ-RLC-001 + //fusa:test REQ-RLC-001 fn is_request_lifecycle_transition_defined_allows_only_the_three_linear_forward_hops() { let defined_pairs = [ ( @@ -4277,7 +4286,7 @@ mod tests { } #[test] - // fusa:test REQ-RLC-001 + //fusa:test REQ-RLC-001 fn is_request_lifecycle_transition_defined_rejects_every_backward_or_identity_pair() { for state in ALL_LIFECYCLE_STATES { // Identity: staying put is never a defined transition. @@ -4310,7 +4319,7 @@ mod tests { // ── RequestLifecycleState::try_transition: undefined-shape rejection ──── #[test] - // fusa:test REQ-RLC-002 + //fusa:test REQ-RLC-002 fn try_transition_rejects_every_undefined_shape_regardless_of_input() { for from in ALL_LIFECYCLE_STATES { for to in ALL_LIFECYCLE_STATES { @@ -4329,8 +4338,8 @@ mod tests { // ── RequestLifecycleState::try_transition: Pending -> Started guards ──── #[test] - // fusa:test REQ-RLC-002 - // fusa:test REQ-RLC-003 + //fusa:test REQ-RLC-002 + //fusa:test REQ-RLC-003 fn try_transition_pending_to_started_passes_unconditionally_for_ungated_kinds() { for input in [ RequestLifecycleGuardInput::Standard, @@ -4356,8 +4365,8 @@ mod tests { } #[test] - // fusa:test REQ-RLC-002 - // fusa:test REQ-RLC-003 + //fusa:test REQ-RLC-002 + //fusa:test REQ-RLC-003 fn try_transition_pending_to_started_gates_timed_on_is_timed_request_ready() { let exec_time = TimedExecutionTime(AvtpTimestamp::new(1_000)); @@ -4382,8 +4391,8 @@ mod tests { } #[test] - // fusa:test REQ-RLC-002 - // fusa:test REQ-RLC-003 + //fusa:test REQ-RLC-002 + //fusa:test REQ-RLC-003 fn try_transition_pending_to_started_gates_compound_and_compound_wait_on_check_compound_gate() { let gate = CompoundGateConfig { sequencer_num: 0, @@ -4438,8 +4447,8 @@ mod tests { // guards ────────────────────────────────────────────────────────────── #[test] - // fusa:test REQ-RLC-002 - // fusa:test REQ-RLC-004 + //fusa:test REQ-RLC-002 + //fusa:test REQ-RLC-004 fn try_transition_started_to_under_execution_passes_unconditionally_for_ungated_kinds() { let gate = CompoundGateConfig { sequencer_num: 0, @@ -4474,8 +4483,8 @@ mod tests { } #[test] - // fusa:test REQ-RLC-002 - // fusa:test REQ-RLC-004 + //fusa:test REQ-RLC-002 + //fusa:test REQ-RLC-004 fn try_transition_started_to_under_execution_gates_chained_on_check_chain_continuation() { let continues = RequestLifecycleGuardInput::Chained { cs: true, @@ -4499,8 +4508,8 @@ mod tests { } #[test] - // fusa:test REQ-RLC-002 - // fusa:test REQ-RLC-004 + //fusa:test REQ-RLC-002 + //fusa:test REQ-RLC-004 fn try_transition_started_to_under_execution_gates_triggered_on_repeat_exhaustion() { let not_exhausted = RequestLifecycleGuardInput::Triggered { endpoint_busy: true, @@ -4542,8 +4551,8 @@ mod tests { // is unconditional ─────────────────────────────────────────────────── #[test] - // fusa:test REQ-RLC-002 - // fusa:test REQ-RLC-005 + //fusa:test REQ-RLC-002 + //fusa:test REQ-RLC-005 fn try_transition_under_execution_to_finalized_is_unconditional_for_every_kind() { let gate = CompoundGateConfig { sequencer_num: 0, @@ -4588,7 +4597,7 @@ mod tests { } #[test] - // fusa:test REQ-RLC-002 + //fusa:test REQ-RLC-002 fn try_transition_never_panics_for_any_sampled_state_pair_or_input() { let gate = CompoundGateConfig { sequencer_num: 0, @@ -4635,7 +4644,7 @@ mod tests { // ── Cancellation trio: force-canceling a target request ───────────────── #[test] - // fusa:test REQ-RLC-006 + //fusa:test REQ-RLC-006 fn try_force_cancel_all_always_finalizes_and_returns_request_canceled() { for mut state in [ RequestLifecycleState::Pending, @@ -4649,7 +4658,7 @@ mod tests { } #[test] - // fusa:test REQ-RLC-006 + //fusa:test REQ-RLC-006 fn try_force_cancel_all_is_idempotent_once_already_finalized() { let mut state = RequestLifecycleState::Finalized; assert_eq!(try_force_cancel_all(&mut state), Ok(())); @@ -4657,7 +4666,7 @@ mod tests { } #[test] - // fusa:test REQ-RLC-006 + //fusa:test REQ-RLC-006 fn try_force_cancel_non_safestate_leaves_safestate_related_requests_untouched() { let mut state = RequestLifecycleState::UnderExecution; assert_eq!(try_force_cancel_non_safestate(&mut state, true), Ok(())); @@ -4665,7 +4674,7 @@ mod tests { } #[test] - // fusa:test REQ-RLC-006 + //fusa:test REQ-RLC-006 fn try_force_cancel_non_safestate_finalizes_non_safestate_related_requests() { let mut state = RequestLifecycleState::UnderExecution; assert_eq!( @@ -4676,7 +4685,7 @@ mod tests { } #[test] - // fusa:test REQ-RLC-006 + //fusa:test REQ-RLC-006 fn try_force_cancel_single_finalizes_only_the_matching_transaction() { let target = ClearTransactionNum(7); @@ -4696,7 +4705,7 @@ mod tests { } #[test] - // fusa:test REQ-RLC-006 + //fusa:test REQ-RLC-006 fn force_cancel_functions_never_panic_for_any_sampled_input() { for state in ALL_LIFECYCLE_STATES { let mut s = state; @@ -4717,7 +4726,7 @@ mod tests { // ── check_compound_bundle_claim ───────────────────────────────────────── #[test] - // fusa:test REQ-BUNDLE-001 + //fusa:test REQ-BUNDLE-001 fn check_compound_bundle_claim_accepts_all_three_prerequisites_together() { assert_eq!(check_compound_bundle_claim(true, 4, true), Ok(())); // More than the minimum sequencer count is also honest. @@ -4726,7 +4735,7 @@ mod tests { } #[test] - // fusa:test REQ-BUNDLE-001 + //fusa:test REQ-BUNDLE-001 fn check_compound_bundle_claim_rejects_missing_compound_wait() { assert_eq!( check_compound_bundle_claim(false, 4, true), @@ -4735,7 +4744,7 @@ mod tests { } #[test] - // fusa:test REQ-BUNDLE-002 + //fusa:test REQ-BUNDLE-002 fn check_compound_bundle_claim_rejects_too_few_sequencers() { assert_eq!( check_compound_bundle_claim(true, 3, true), @@ -4748,7 +4757,7 @@ mod tests { } #[test] - // fusa:test REQ-BUNDLE-001 + //fusa:test REQ-BUNDLE-001 fn check_compound_bundle_claim_rejects_missing_clear_non_safestate() { assert_eq!( check_compound_bundle_claim(true, 4, false), @@ -4757,7 +4766,7 @@ mod tests { } #[test] - // fusa:test REQ-BUNDLE-001 + //fusa:test REQ-BUNDLE-001 fn check_compound_bundle_claim_rejects_compound_message_parsing_alone() { // The checklist's own named failure case: none of the three real // prerequisites are met, only (implicitly) the ability to decode a @@ -4770,7 +4779,7 @@ mod tests { } #[test] - // fusa:test REQ-BUNDLE-002 + //fusa:test REQ-BUNDLE-002 fn check_compound_bundle_claim_never_panics_for_any_sampled_input() { for has_compound_wait in [false, true] { for svr_sequencers_max in [0, 1, 3, 4, 5, u8::MAX] { @@ -4788,7 +4797,7 @@ mod tests { // ── RequestKind::is_safety_tagged ───────────────────────────────────────── #[test] - // fusa:test REQ-SAFETY-002 + //fusa:test REQ-SAFETY-002 fn is_safety_tagged_is_true_only_for_the_three_safety_variants() { for kind in [ RequestKind::SafetyCompound, @@ -4815,7 +4824,7 @@ mod tests { // ── check_watchdog_overflow_purge ────────────────────────────────────────── #[test] - // fusa:test REQ-SAFETY-004 + //fusa:test REQ-SAFETY-004 fn check_watchdog_overflow_purge_keeps_everything_when_not_overflowed() { for kind in ALL_REQUEST_KINDS { assert_eq!(check_watchdog_overflow_purge(kind, false), Ok(())); @@ -4823,7 +4832,7 @@ mod tests { } #[test] - // fusa:test REQ-SAFETY-004 + //fusa:test REQ-SAFETY-004 fn check_watchdog_overflow_purge_purges_normal_priority_kinds_on_overflow() { for kind in [ RequestKind::Standard, @@ -4844,7 +4853,7 @@ mod tests { } #[test] - // fusa:test REQ-SAFETY-004 + //fusa:test REQ-SAFETY-004 fn check_watchdog_overflow_purge_exempts_safety_tagged_kinds_on_overflow() { for kind in [ RequestKind::SafetyCompound, @@ -4856,7 +4865,7 @@ mod tests { } #[test] - // fusa:test REQ-SAFETY-004 + //fusa:test REQ-SAFETY-004 fn check_watchdog_overflow_purge_never_panics_for_any_sampled_input() { for kind in ALL_REQUEST_KINDS { for watchdog_overflowed in [false, true] { @@ -4868,7 +4877,7 @@ mod tests { // ── purge_normal_priority_on_watchdog_overflow ───────────────────────────── #[test] - // fusa:test REQ-SAFETY-005 + //fusa:test REQ-SAFETY-005 fn purge_normal_priority_on_watchdog_overflow_is_a_no_op_for_an_empty_slice() { assert_eq!( purge_normal_priority_on_watchdog_overflow(&[], true), @@ -4881,7 +4890,7 @@ mod tests { } #[test] - // fusa:test REQ-SAFETY-005 + //fusa:test REQ-SAFETY-005 fn purge_normal_priority_on_watchdog_overflow_keeps_everything_without_overflow() { let pending = [ PendingRequestKey { @@ -4904,7 +4913,7 @@ mod tests { } #[test] - // fusa:test REQ-SAFETY-005 + //fusa:test REQ-SAFETY-005 fn purge_normal_priority_on_watchdog_overflow_purges_normal_keeps_safety_tagged() { let pending = [ PendingRequestKey { @@ -4939,7 +4948,7 @@ mod tests { } #[test] - // fusa:test REQ-SAFETY-005 + //fusa:test REQ-SAFETY-005 fn purge_normal_priority_on_watchdog_overflow_all_safety_tagged_keeps_all() { let pending = [ PendingRequestKey { @@ -4962,7 +4971,7 @@ mod tests { } #[test] - // fusa:test REQ-SAFETY-005 + //fusa:test REQ-SAFETY-005 fn purge_normal_priority_on_watchdog_overflow_all_normal_purges_all() { let pending = [ PendingRequestKey { @@ -4981,7 +4990,7 @@ mod tests { } #[test] - // fusa:test REQ-SAFETY-005 + //fusa:test REQ-SAFETY-005 fn purge_normal_priority_on_watchdog_overflow_never_panics_for_any_sampled_input() { for kind in ALL_REQUEST_KINDS { for watchdog_overflowed in [false, true] { @@ -4997,14 +5006,14 @@ mod tests { // ── Per-stream safety config: rx_enforce_e2e ───────────────────────────── #[test] - // fusa:test REQ-E2EENF-001 + //fusa:test REQ-E2EENF-001 fn e2e_failure_scope_selects_by_rx_enforce_e2e() { assert_eq!(e2e_failure_scope(false), E2eFailureScope::DropRequest); assert_eq!(e2e_failure_scope(true), E2eFailureScope::LatchStream); } #[test] - // fusa:test REQ-E2EENF-002 + //fusa:test REQ-E2EENF-002 fn check_rx_enforce_e2e_accepts_matching_crc() { let buffer = b"safe-point coverage bytes"; let expected = crate::e2e::crc32_tc18(buffer); @@ -5013,8 +5022,8 @@ mod tests { } #[test] - // fusa:test REQ-E2EENF-002 - // fusa:test REQ-CRC-011 + //fusa:test REQ-E2EENF-002 + //fusa:test REQ-CRC-011 fn check_rx_enforce_e2e_reports_scope_on_mismatch() { let buffer = b"safe-point coverage bytes"; let wrong = crate::e2e::crc32_tc18(buffer).wrapping_add(1); @@ -5029,7 +5038,7 @@ mod tests { } #[test] - // fusa:test REQ-E2EENF-002 + //fusa:test REQ-E2EENF-002 fn check_rx_enforce_e2e_never_panics_for_any_sampled_input() { for buffer in [&b""[..], &b"x"[..], &[0u8; 64][..]] { for expected in [0u32, 1, u32::MAX] { @@ -5043,7 +5052,7 @@ mod tests { // ── RcpError::CrcError ──────────────────────────────────────────────── #[test] - // fusa:test REQ-CRC-011 + //fusa:test REQ-CRC-011 fn crc_error_is_distinct_from_other_rcperror_variants() { // The legacy `RcpError::CrcMismatch` sentinel this variant was // originally kept distinct from (see this module's "Provenance @@ -5056,7 +5065,7 @@ mod tests { } #[test] - // fusa:test REQ-CRC-011 + //fusa:test REQ-CRC-011 fn crc_error_carries_the_roadmap_named_code_in_its_display_text() { assert!(RcpError::CrcError.to_string().contains("CRC_ERROR")); } @@ -5065,7 +5074,7 @@ mod tests { // rx_safe_sequencer_state ──────────────────────────────────────────── #[test] - // fusa:test REQ-SAFEMEAS-002 + //fusa:test REQ-SAFEMEAS-002 fn safe_state_sequencer_gate_carries_both_fields_through() { let gate = safe_state_sequencer_gate(3, 7); assert_eq!(gate.sequencer_num, 3); @@ -5073,7 +5082,7 @@ mod tests { } #[test] - // fusa:test REQ-SAFEMEAS-001 + //fusa:test REQ-SAFEMEAS-001 fn resolve_safe_state_mechanism_selects_by_rx_safety_measure() { assert_eq!( resolve_safe_state_mechanism(false, 3, 7), @@ -5086,7 +5095,7 @@ mod tests { } #[test] - // fusa:test REQ-SAFEMEAS-004 + //fusa:test REQ-SAFEMEAS-004 fn force_state_writes_unconditionally_even_outside_start_state() { let mut bank = SequencerBank::new(4); let gate = CompoundGateConfig { @@ -5101,7 +5110,7 @@ mod tests { } #[test] - // fusa:test REQ-SAFEMEAS-004 + //fusa:test REQ-SAFEMEAS-004 fn force_state_rejects_out_of_bounds_sequencer() { let mut bank = SequencerBank::new(2); assert_eq!( @@ -5111,7 +5120,7 @@ mod tests { } #[test] - // fusa:test REQ-SAFEMEAS-003 + //fusa:test REQ-SAFEMEAS-003 fn enter_sequencer_driven_safe_state_composes_force_state() { let mut bank = SequencerBank::new(4); let gate = safe_state_sequencer_gate(2, 5); @@ -5120,7 +5129,7 @@ mod tests { } #[test] - // fusa:test REQ-SAFEMEAS-003 + //fusa:test REQ-SAFEMEAS-003 fn enter_sequencer_driven_safe_state_never_panics_for_any_sampled_input() { for svr_sequencers_max in [0u8, 1, 4] { for sequencer_num in [0u8, 1, 4, u8::MAX] { @@ -5134,8 +5143,8 @@ mod tests { // ── Per-stream safety config: rx_ovrflw_safestate_enable ───────────────── #[test] - // fusa:test REQ-OVRFLW-001 - // fusa:test REQ-OVRFLW-003 + //fusa:test REQ-OVRFLW-001 + //fusa:test REQ-OVRFLW-003 fn evaluate_request_storage_overflow_no_overflow_ignores_safestate_flag() { assert_eq!( evaluate_request_storage_overflow(false, false), @@ -5148,7 +5157,7 @@ mod tests { } #[test] - // fusa:test REQ-OVRFLW-003 + //fusa:test REQ-OVRFLW-003 fn evaluate_request_storage_overflow_selects_by_safestate_flag() { assert_eq!( evaluate_request_storage_overflow(true, false), @@ -5161,7 +5170,7 @@ mod tests { } #[test] - // fusa:test REQ-OVRFLW-002 + //fusa:test REQ-OVRFLW-002 fn overflow_outcome_predicates_agree_with_variant_identity() { assert!(!OverflowOutcome::NoOverflow.is_overflow()); assert!(!OverflowOutcome::NoOverflow.drives_safestate()); @@ -5174,8 +5183,8 @@ mod tests { // ── Per-stream safety config: rx_enforce_seq / rx_seq_safestate_enable ─── #[test] - // fusa:test REQ-SEQENF-001 - // fusa:test REQ-SEQENF-003 + //fusa:test REQ-SEQENF-001 + //fusa:test REQ-SEQENF-003 fn evaluate_rx_enforce_seq_accepts_when_disabled_regardless_of_ordering() { assert_eq!( evaluate_rx_enforce_seq(10, 5, false, false), @@ -5188,7 +5197,7 @@ mod tests { } #[test] - // fusa:test REQ-SEQENF-003 + //fusa:test REQ-SEQENF-003 fn evaluate_rx_enforce_seq_accepts_strictly_increasing_sequence() { assert_eq!( evaluate_rx_enforce_seq(5, 6, true, true), @@ -5197,7 +5206,7 @@ mod tests { } #[test] - // fusa:test REQ-SEQENF-003 + //fusa:test REQ-SEQENF-003 fn evaluate_rx_enforce_seq_rejects_equal_or_decreasing_sequence() { assert_eq!( evaluate_rx_enforce_seq(5, 5, true, false), @@ -5214,7 +5223,7 @@ mod tests { } #[test] - // fusa:test REQ-SEQENF-002 + //fusa:test REQ-SEQENF-002 fn sequence_enforcement_outcome_predicates_agree_with_variant_identity() { assert!(!SequenceEnforcementOutcome::Accepted.is_rejected()); assert!(!SequenceEnforcementOutcome::Accepted.drives_safestate()); @@ -5225,7 +5234,7 @@ mod tests { } #[test] - // fusa:test REQ-SEQENF-003 + //fusa:test REQ-SEQENF-003 fn evaluate_rx_enforce_seq_never_panics_for_any_sampled_input() { let seqs = [0u32, 1, 5, u32::MAX]; for &last in &seqs { @@ -5247,8 +5256,8 @@ mod tests { // ── Per-stream safety config: the unifying safe-state action ───────────── #[test] - // fusa:test REQ-SAFEACT-001 - // fusa:test REQ-SAFEACT-002 + //fusa:test REQ-SAFEACT-001 + //fusa:test REQ-SAFEACT-002 fn resolve_safe_state_action_is_none_when_not_entering_safe_state() { assert_eq!( resolve_safe_state_action(false, SafeStateMechanism::HiZAllPins), @@ -5264,7 +5273,7 @@ mod tests { } #[test] - // fusa:test REQ-SAFEACT-002 + //fusa:test REQ-SAFEACT-002 fn resolve_safe_state_action_mirrors_the_mechanism_when_entering() { assert_eq!( resolve_safe_state_action(true, SafeStateMechanism::HiZAllPins), @@ -5278,7 +5287,7 @@ mod tests { } #[test] - // fusa:test REQ-SAFEACT-002 + //fusa:test REQ-SAFEACT-002 fn resolve_safe_state_action_never_panics_for_any_sampled_input() { let mechanisms = [ SafeStateMechanism::HiZAllPins, @@ -5291,4 +5300,184 @@ mod tests { } } } + + // ── TC18-literal conformance checks ────────────────────────────────────── + + #[test] + //fusa:test REQ-CMP-009 + fn request_kind_discriminants_match_tc18_table_5_condition_type_bytes() { + // TC18 §11.2.2, Table 5 "Different types of conditional requests" + // (TC18.txt line 1186): "The first byte in the message_timestamp + // field is used to indicate the type of condition." Table 5's own + // rows, transcribed as literals: + // 0x0F, 0x8F -> Compound + // 0x0B, 0x8B -> Compound wait + // 0x0E, 0x8E -> Triggered + // 0x01 -> Chained + // 0x0A -> Timed + assert_eq!(RequestKind::Compound.to_u8(), 0x0F); + assert_eq!(RequestKind::SafetyCompound.to_u8(), 0x8F); + assert_eq!(RequestKind::CompoundWait.to_u8(), 0x0B); + assert_eq!(RequestKind::SafetyCompoundWait.to_u8(), 0x8B); + assert_eq!(RequestKind::Triggered.to_u8(), 0x0E); + assert_eq!(RequestKind::SafetyTriggered.to_u8(), 0x8E); + assert_eq!(RequestKind::Chained.to_u8(), 0x01); + assert_eq!(RequestKind::Timed.to_u8(), 0x0A); + + // TC18 §11.2.3.1 Table 11 (line 1679) "request_type 0x05", + // §11.2.3.2 Table 12 (line 1733) "request_type 0x06", and + // §11.2.3.3 Table 13 (line 1792) "request_type 0x07". + assert_eq!(RequestKind::ClearAll.to_u8(), 0x05); + assert_eq!(RequestKind::ClearNonSafestate.to_u8(), 0x06); + assert_eq!(RequestKind::ClearSingle.to_u8(), 0x07); + + // TC18 §11.2.2 (line 1186): "If the MSB of the identifier (0x8x) is + // set the request is treated as a safety request". + for (safety, base) in [ + (RequestKind::SafetyCompound, RequestKind::Compound), + (RequestKind::SafetyCompoundWait, RequestKind::CompoundWait), + (RequestKind::SafetyTriggered, RequestKind::Triggered), + ] { + assert_eq!(safety.to_u8(), 0x80 | base.to_u8()); + assert!(safety.is_safety_tagged()); + assert!(!base.is_safety_tagged()); + } + + // Decode side: those same literal bytes, positioned as Table 5's + // "first byte in message_timestamp field" — i.e. bits 63:56 of the + // big-endian 8-octet ACF_GBB message_timestamp. + for (byte, expected) in [ + (0x0Fu8, RequestKind::Compound), + (0x8F, RequestKind::SafetyCompound), + (0x0B, RequestKind::CompoundWait), + (0x8B, RequestKind::SafetyCompoundWait), + (0x0E, RequestKind::Triggered), + (0x8E, RequestKind::SafetyTriggered), + (0x01, RequestKind::Chained), + (0x0A, RequestKind::Timed), + (0x05, RequestKind::ClearAll), + (0x06, RequestKind::ClearNonSafestate), + (0x07, RequestKind::ClearSingle), + ] { + let message_timestamp = (byte as u64) << 56; + assert_eq!( + RequestKind::from_gbb_message_timestamp(message_timestamp), + Some(expected), + "Table 5 condition-type byte {byte:#04X}" + ); + } + } + + #[test] + //fusa:test REQ-SEQ-005 + fn sequencer_bank_and_state_stay_within_tc18_256_sequencer_and_state_ceiling() { + // TC18 §12.10 "Sequencers" (TC18.txt line 3463): "The number of + // sequencers and states per sequencer are limited to 256 by this + // definition. An RC Server implementation may support only a lower + // number of sequencers." + const TC18_SEQUENCER_LIMIT: usize = 256; + const TC18_STATES_PER_SEQUENCER_LIMIT: usize = 256; + + // `SequencerState` is one octet wide, so it spans exactly TC18's + // 256-state ceiling and every one of those states is distinct. + let all_states: Vec = (0..=u8::MAX).map(SequencerState).collect(); + assert_eq!(all_states.len(), TC18_STATES_PER_SEQUENCER_LIMIT); + for (i, a) in all_states.iter().enumerate() { + for b in &all_states[i + 1..] { + assert_ne!(a, b); + } + } + + // The widest bank this crate can build is bounded by `u8`-valued + // `svr_sequencers_max`, so it never exceeds TC18's 256-sequencer + // ceiling. TC18 §12.10 also fixes the power-on state at 1: + // "After power-on/reset all sequencers are in state 1." + let bank = SequencerBank::new(u8::MAX); + assert!(bank.svr_sequencers_max() as usize <= TC18_SEQUENCER_LIMIT); + assert_eq!(bank.svr_sequencers_max(), 255); + assert_eq!(bank.read(0), Ok(SequencerState(1))); + assert_eq!(bank.read(254), Ok(SequencerState(1))); + // Sequencer number 255 is beyond this crate's own `u8` bound, and so + // is unreachable even though TC18's ceiling would admit it. + assert_eq!(bank.read(255), Err(RcpError::SequencerNotKnown)); + } + + #[test] + //fusa:test REQ-ERRH-001 + fn request_module_error_outcomes_carry_their_tc18_table_27_wire_codes() { + // TC18 §12.9.6 Table 27 "Error codes in responses" (TC18.txt line + // 3413), transcribed as literals: SEQUENCER_NOT_KNOWN = 2, + // REQUEST_CANCELED = 5, REQUEST_REJECTED = 11, + // INVALID_PARAMETER = 15, CHAIN_ABORTED = 16. + + // TC18 §11.2.2.1 (line 1203): a compound request naming a sequencer + // the RC Server does not have. + let unknown_sequencer = CompoundGateConfig { + sequencer_num: 4, + start_state: SequencerState(1), + }; + assert_eq!( + check_compound_gate(SequencerState(1), &unknown_sequencer, 4) + .unwrap_err() + .tc18_wire_code(), + Some(2) + ); + + // TC18 §11.2.2.1 (line 1203): the sequencer is known but is not in + // the request's cmp_start_state, so the request is not due. + let unmet_gate = CompoundGateConfig { + sequencer_num: 0, + start_state: SequencerState(3), + }; + assert_eq!( + check_compound_gate(SequencerState(1), &unmet_gate, 4) + .unwrap_err() + .tc18_wire_code(), + Some(11) + ); + + // TC18 §11.2.3 (line 1672): "Each request that is cancelled will send + // an error response with the error code = REQUEST_CANCELED." + assert_eq!( + check_clear_all_cancellation().unwrap_err().tc18_wire_code(), + Some(5) + ); + assert_eq!( + check_clear_non_safestate_cancellation(false) + .unwrap_err() + .tc18_wire_code(), + Some(5) + ); + assert_eq!( + check_clear_single_cancellation(7, ClearTransactionNum(7)) + .unwrap_err() + .tc18_wire_code(), + Some(5) + ); + // TC18 §11.2.2.1 (line 1203) / §11.2.2.4 Table 9 (line 1586): the + // watchdog-overflow purge of non-safety-tagged requests is likewise a + // cancellation. + assert_eq!( + check_watchdog_overflow_purge(RequestKind::Compound, true) + .unwrap_err() + .tc18_wire_code(), + Some(5) + ); + + // TC18 §11.2.2.4 Table 9 (line 1586): cs = 1 and "error occurred in + // one of the preceding requests" -> CHAIN_ABORTED. + assert_eq!( + check_chain_continuation(true, true) + .unwrap_err() + .tc18_wire_code(), + Some(16) + ); + + // TC18 §11.2.2 Table 5 (line 1186) names no 0x02 condition type, so + // decoding one is a parameter out of range -> INVALID_PARAMETER. + assert_eq!( + RequestKind::from_u8(0x02).unwrap_err().tc18_wire_code(), + Some(15) + ); + } } diff --git a/src/shmem.rs b/src/shmem.rs index 1c52ff8..5c8f641 100644 --- a/src/shmem.rs +++ b/src/shmem.rs @@ -1,8 +1,8 @@ -// fusa:req REQ-SHM-001 -// fusa:req REQ-SHM-002 -// fusa:req REQ-SHM-003 -// fusa:req REQ-SHM-004 -// fusa:req REQ-SHM-005 +//fusa:req REQ-SHM-001 +//fusa:req REQ-SHM-002 +//fusa:req REQ-SHM-003 +//fusa:req REQ-SHM-004 +//fusa:req REQ-SHM-005 //! Shared-memory transport bridge (intra-host IPC). //! @@ -32,7 +32,7 @@ use crate::RcpError; // ── ShmChannel trait ────────────────────────────────────────────────────────── /// Abstract shared-memory channel for testability. -// fusa:req REQ-SHM-001 +//fusa:req REQ-SHM-001 pub trait ShmChannel: Send + Sync { fn write(&self, data: &[u8]) -> Result<(), RcpError>; fn read(&self, timeout: Option) -> Result, RcpError>; @@ -41,7 +41,7 @@ pub trait ShmChannel: Send + Sync { // ── In-process ring buffer implementation ───────────────────────────────────── /// Simple in-process FIFO channel (for tests and integration). -// fusa:req REQ-SHM-002 +//fusa:req REQ-SHM-002 pub struct InProcChannel { buf: std::sync::Mutex>>, cvar: std::sync::Condvar, @@ -106,7 +106,7 @@ impl ShmChannel for InProcChannel { /// ([`crate::avtp::StreamId`]) rather than the legacy `Zone`. /// /// The caller must wire `tx` (write) and `rx` (read) channels to the peer process. -// fusa:req REQ-SHM-003 +//fusa:req REQ-SHM-003 pub struct ShmBridge { local_stream: StreamId, tx: Arc, @@ -131,7 +131,7 @@ impl ShmBridge { /// ACF_ABB response, verifying it echoes the request's `byte_bus_id` — /// the same framing `crate::tlstransport::TlsBridge::send_acf_abb` /// uses, over an [`ShmChannel`] pair instead of a TLS stream. - // fusa:req REQ-SHM-004 + //fusa:req REQ-SHM-004 pub fn send_acf_abb( &self, msg: &AcfAbbMessage, @@ -152,7 +152,7 @@ impl ShmBridge { } /// Same as [`Self::send_acf_abb`], for an ACF_GBB request/response pair. - // fusa:req REQ-SHM-004 + //fusa:req REQ-SHM-004 pub fn send_acf_gbb( &self, msg: &AcfGbbMessage, @@ -173,7 +173,7 @@ impl ShmBridge { } /// No-op, matching this module's pre-Milestone-9 behavior. - // fusa:req REQ-SHM-005 + //fusa:req REQ-SHM-005 pub fn close(&self) -> Result<(), RcpError> { Ok(()) } @@ -223,8 +223,8 @@ mod tests { } #[test] - // fusa:test REQ-SHM-003 - // fusa:test REQ-SHM-004 + //fusa:test REQ-SHM-003 + //fusa:test REQ-SHM-004 fn shm_bridge_send_acf_abb_ok() { let tx = InProcChannel::new() as Arc; let rx = InProcChannel::new() as Arc; @@ -236,7 +236,7 @@ mod tests { } #[test] - // fusa:test REQ-SHM-004 + //fusa:test REQ-SHM-004 fn shm_bridge_rejects_echo_back_mismatch() { let tx = InProcChannel::new() as Arc; let rx = InProcChannel::new() as Arc; @@ -247,7 +247,7 @@ mod tests { } #[test] - // fusa:test REQ-SHM-004 + //fusa:test REQ-SHM-004 fn zero_timeout_rejected() { let b = make_bridge(); let err = b @@ -257,8 +257,8 @@ mod tests { } #[test] - // fusa:test REQ-SHM-001 - // fusa:test REQ-SHM-002 + //fusa:test REQ-SHM-001 + //fusa:test REQ-SHM-002 fn in_proc_channel_fifo() { let ch = InProcChannel::new(); ch.write(b"first").unwrap(); @@ -268,7 +268,7 @@ mod tests { } #[test] - // fusa:test REQ-SHM-002 + //fusa:test REQ-SHM-002 fn in_proc_channel_timeout() { let ch = InProcChannel::new(); let err = ch.read(Some(Duration::from_millis(10))).unwrap_err(); @@ -276,7 +276,7 @@ mod tests { } #[test] - // fusa:test REQ-SHM-005 + //fusa:test REQ-SHM-005 fn close_is_noop() { let b = make_bridge(); assert!(b.close().is_ok()); diff --git a/src/sim.rs b/src/sim.rs index 7c77388..0df360c 100644 --- a/src/sim.rs +++ b/src/sim.rs @@ -1,10 +1,10 @@ -// fusa:req REQ-SIM-001 -// fusa:req REQ-SIM-002 -// fusa:req REQ-SIM-003 -// fusa:req REQ-SIM-004 -// fusa:req REQ-SIM-006 -// fusa:req REQ-SIM-007 -// fusa:req REQ-SIM-008 +//fusa:req REQ-SIM-001 +//fusa:req REQ-SIM-002 +//fusa:req REQ-SIM-003 +//fusa:req REQ-SIM-004 +//fusa:req REQ-SIM-006 +//fusa:req REQ-SIM-007 +//fusa:req REQ-SIM-008 //! Deterministic simulation endpoint for integration and hardware-in-loop //! tests. @@ -72,7 +72,7 @@ struct Inner { /// returns the endpoint's held buffer (mirroring /// [`crate::mock::MockEndpoint::read`]); a `write` with no queued response /// returns `Ok(())`. -// fusa:req REQ-SIM-001 +//fusa:req REQ-SIM-001 pub struct SimEndpoint { ep_type: EndpointType, buf: Mutex>, @@ -95,20 +95,20 @@ impl SimEndpoint { } /// Pre-program the next response returned by `read`. - // fusa:req REQ-SIM-002 + //fusa:req REQ-SIM-002 pub fn queue_read_response(&self, r: Result, RcpError>) { self.inner.lock().unwrap().read_responses.push_back(r); } /// Pre-program the next response returned by `write`. - // fusa:req REQ-SIM-002 + //fusa:req REQ-SIM-002 pub fn queue_write_response(&self, r: Result<(), RcpError>) { self.inner.lock().unwrap().write_responses.push_back(r); } /// Return all calls dispatched since creation (or last /// [`Self::clear_calls`]). - // fusa:req REQ-SIM-003 + //fusa:req REQ-SIM-003 pub fn calls(&self) -> Vec { self.inner.lock().unwrap().calls.clone() } @@ -122,7 +122,7 @@ impl SimEndpoint { /// `Err(RcpError::Closed)`. An inherent method (not part of /// [`Endpoint`], which defines no `close`) — see this module's doc /// comment. - // fusa:req REQ-SIM-008 + //fusa:req REQ-SIM-008 pub fn close(&self) { self.closed.store(true, Ordering::SeqCst); } @@ -133,7 +133,7 @@ impl Endpoint for SimEndpoint { self.ep_type } - // fusa:req REQ-SIM-004 + //fusa:req REQ-SIM-004 fn read(&self, read_size: u16) -> Result, RcpError> { if self.closed.load(Ordering::SeqCst) { return Err(RcpError::Closed); @@ -149,7 +149,7 @@ impl Endpoint for SimEndpoint { Ok(buf[..n].to_vec()) } - // fusa:req REQ-SIM-004 + //fusa:req REQ-SIM-004 fn write(&self, payload: &[u8]) -> Result<(), RcpError> { if self.closed.load(Ordering::SeqCst) { return Err(RcpError::Closed); @@ -175,14 +175,14 @@ mod tests { use super::*; #[test] - // fusa:test REQ-SIM-001 + //fusa:test REQ-SIM-001 fn new_sim_endpoint_accepts_writes() { let sim = SimEndpoint::new(EndpointType::Gpio); sim.write(b"hi").unwrap(); } #[test] - // fusa:test REQ-SIM-003 + //fusa:test REQ-SIM-003 fn records_dispatched_calls() { let sim = SimEndpoint::new(EndpointType::Gpio); for i in 1u8..=3 { @@ -197,7 +197,7 @@ mod tests { } #[test] - // fusa:test REQ-SIM-002 + //fusa:test REQ-SIM-002 fn queued_read_responses_delivered_in_order() { let sim = SimEndpoint::new(EndpointType::Gpio); sim.queue_read_response(Ok(vec![0xAB])); @@ -210,7 +210,7 @@ mod tests { } #[test] - // fusa:test REQ-SIM-002 + //fusa:test REQ-SIM-002 fn queued_write_responses_delivered_in_order() { let sim = SimEndpoint::new(EndpointType::Gpio); sim.queue_write_response(Err(RcpError::Busy)); @@ -219,7 +219,7 @@ mod tests { } #[test] - // fusa:test REQ-SIM-004 + //fusa:test REQ-SIM-004 fn write_then_read_round_trips_through_buffer() { let sim = SimEndpoint::new(EndpointType::Gpio); sim.write(b"test").unwrap(); @@ -228,8 +228,8 @@ mod tests { } #[test] - // fusa:test REQ-SIM-006 - // fusa:test REQ-SIM-007 + //fusa:test REQ-SIM-006 + //fusa:test REQ-SIM-007 fn clear_calls_empties_log() { let sim = SimEndpoint::new(EndpointType::Gpio); sim.write(b"x").unwrap(); @@ -239,7 +239,7 @@ mod tests { } #[test] - // fusa:test REQ-SIM-008 + //fusa:test REQ-SIM-008 fn call_after_close_returns_closed() { let sim = SimEndpoint::new(EndpointType::Gpio); sim.close(); diff --git a/src/spi.rs b/src/spi.rs index 2807e4d..f0d91a7 100644 --- a/src/spi.rs +++ b/src/spi.rs @@ -1,15 +1,15 @@ -// fusa:req REQ-SPI-001 -// fusa:req REQ-SPI-002 -// fusa:req REQ-SPI-003 -// fusa:req REQ-SPI-004 -// fusa:req REQ-SPI-005 -// fusa:req REQ-SPI-006 -// fusa:req REQ-SPI-007 -// fusa:req REQ-SPI-008 -// fusa:req REQ-SPI-009 -// fusa:req REQ-SPI-010 -// fusa:req REQ-SPI-011 -// fusa:req REQ-SPI-012 +//fusa:req REQ-SPI-001 +//fusa:req REQ-SPI-002 +//fusa:req REQ-SPI-003 +//fusa:req REQ-SPI-004 +//fusa:req REQ-SPI-005 +//fusa:req REQ-SPI-006 +//fusa:req REQ-SPI-007 +//fusa:req REQ-SPI-008 +//fusa:req REQ-SPI-009 +//fusa:req REQ-SPI-010 +//fusa:req REQ-SPI-011 +//fusa:req REQ-SPI-012 //! The SPI endpoint type (`ep_type 0x03`) — `ROADMAP.md` Milestone 4 //! ("Basic Endpoint Types"), second checklist bullet: "up to 6 @@ -155,7 +155,7 @@ pub const SPI_CHANNEL_COUNT: usize = 6; /// "spare" values. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] -// fusa:req REQ-SPI-001 +//fusa:req REQ-SPI-001 pub enum SpiChannelSelect { /// Pre-configured channel 0. Channel0 = 0, @@ -188,7 +188,7 @@ pub enum SpiChannelSelect { impl SpiChannelSelect { /// Encode this channel selection as its `evt.sub_opcode` value /// (`0..=7`). - // fusa:req REQ-SPI-001 + //fusa:req REQ-SPI-001 pub fn to_sub_opcode(self) -> u8 { self as u8 } @@ -200,7 +200,7 @@ impl SpiChannelSelect { /// (`> `[`crate::acf::EVT_SUB_OPCODE_MAX`]``), matching /// [`crate::gpio::GpioWriteSemantics::from_sub_opcode`]'s own range /// check. Never panics for any input. - // fusa:req REQ-SPI-002 + //fusa:req REQ-SPI-002 pub fn from_sub_opcode(raw: u8) -> Result { match raw { 0 => Ok(Self::Channel0), @@ -220,7 +220,7 @@ impl SpiChannelSelect { /// names. False for [`SpiChannelSelect::Reserved6`] (spec-reserved, /// rejected) and [`SpiChannelSelect::Reconfigure7`] (endpoint /// reconfiguration, not a channel selection). - // fusa:req REQ-SPI-003 + //fusa:req REQ-SPI-003 pub fn is_named(self) -> bool { !matches!(self, Self::Reserved6 | Self::Reconfigure7) } @@ -234,7 +234,7 @@ impl SpiChannelSelect { /// neither resolves to a real channel index — rather than guessing a /// channel for them; see this module's doc comment "Provenance note: /// channel selection via `evt.sub_opcode`". Never panics for any input. -// fusa:req REQ-SPI-004 +//fusa:req REQ-SPI-004 pub fn resolve_spi_channel_index(select: SpiChannelSelect) -> Result { match select { SpiChannelSelect::Channel0 => Ok(0), @@ -257,7 +257,7 @@ pub fn resolve_spi_channel_index(select: SpiChannelSelect) -> Result crate::regmap::PerEpTypeFunctionalConfig { crate::regmap::PerEpTypeFunctionalConfig::new(crate::regmap::EndpointType::Spi) } @@ -295,7 +295,7 @@ impl SpiFunctionalConfig { /// Returns `Err(RcpError::UnsupportedCmd)` for the two spare /// [`SpiChannelSelect`] values, via [`resolve_spi_channel_index`]. Never /// panics for any input. -// fusa:req REQ-SPI-006 +//fusa:req REQ-SPI-006 pub fn select_spi_channel_config( select: SpiChannelSelect, config: &SpiFunctionalConfig, @@ -317,7 +317,7 @@ pub fn select_spi_channel_config( /// any length has no invalid encoding, so [`SpiByteTransfer::decode`] is /// infallible. #[derive(Debug, Clone, PartialEq, Eq, Default)] -// fusa:req REQ-SPI-008 +//fusa:req REQ-SPI-008 pub struct SpiByteTransfer { /// The raw bytes sent from controller to peripheral. pub pico: Vec, @@ -326,7 +326,7 @@ pub struct SpiByteTransfer { impl SpiByteTransfer { /// Encode this transfer to its raw wire representation: `pico`'s bytes, /// unmodified and unframed. - // fusa:req REQ-SPI-008 + //fusa:req REQ-SPI-008 pub fn encode(&self) -> Vec { self.pico.clone() } @@ -335,7 +335,7 @@ impl SpiByteTransfer { /// /// Every possible byte slice, including an empty one, is a valid PICO /// transfer, so this never fails and never panics for any input. - // fusa:req REQ-SPI-008 + //fusa:req REQ-SPI-008 pub fn decode(b: &[u8]) -> Self { Self { pico: b.to_vec() } } @@ -348,7 +348,7 @@ impl SpiByteTransfer { /// variable-length byte-stream modeling for the opposite transfer /// direction. #[derive(Debug, Clone, PartialEq, Eq, Default)] -// fusa:req REQ-SPI-009 +//fusa:req REQ-SPI-009 pub struct SpiByteTransferResult { /// The raw bytes returned from peripheral to controller. pub poci: Vec, @@ -357,7 +357,7 @@ pub struct SpiByteTransferResult { impl SpiByteTransferResult { /// Encode this transfer result to its raw wire representation: `poci`'s /// bytes, unmodified and unframed. - // fusa:req REQ-SPI-009 + //fusa:req REQ-SPI-009 pub fn encode(&self) -> Vec { self.poci.clone() } @@ -366,7 +366,7 @@ impl SpiByteTransferResult { /// /// Every possible byte slice, including an empty one, is a valid POCI /// transfer result, so this never fails and never panics for any input. - // fusa:req REQ-SPI-009 + //fusa:req REQ-SPI-009 pub fn decode(b: &[u8]) -> Self { Self { poci: b.to_vec() } } @@ -388,12 +388,12 @@ pub const SPI_COMPOUND_WAIT_STATUS_LEN: usize = 4; /// truncation" — this crate does not otherwise interpret this status's /// byte layout. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// fusa:req REQ-SPI-010 +//fusa:req REQ-SPI-010 pub struct SpiStatus(pub [u8; SPI_STATUS_LEN]); impl SpiStatus { /// Encode this status to its 20-byte wire representation. - // fusa:req REQ-SPI-010 + //fusa:req REQ-SPI-010 pub fn encode(self) -> [u8; SPI_STATUS_LEN] { self.0 } @@ -405,7 +405,7 @@ impl SpiStatus { /// [`SPI_STATUS_LEN`] instead. Trailing bytes beyond the first 20 are /// ignored, matching [`crate::gpio::GpioBitmask::decode`]'s own handling /// of a longer-than-required slice. - // fusa:req REQ-SPI-010 + //fusa:req REQ-SPI-010 pub fn decode(b: &[u8]) -> Result { if b.len() < SPI_STATUS_LEN { return Err(RcpError::ShortFrame); @@ -419,12 +419,12 @@ impl SpiStatus { /// The 4-byte SPI status shape produced by truncating an [`SpiStatus`] for /// compound-wait, per [`truncate_spi_status_for_compound_wait`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// fusa:req REQ-SPI-011 +//fusa:req REQ-SPI-011 pub struct SpiCompoundWaitStatus(pub [u8; SPI_COMPOUND_WAIT_STATUS_LEN]); impl SpiCompoundWaitStatus { /// Encode this truncated status to its 4-byte wire representation. - // fusa:req REQ-SPI-011 + //fusa:req REQ-SPI-011 pub fn encode(self) -> [u8; SPI_COMPOUND_WAIT_STATUS_LEN] { self.0 } @@ -434,7 +434,7 @@ impl SpiCompoundWaitStatus { /// Never panics on short, truncated, or arbitrary input — always /// returns `Err(RcpError::ShortFrame)` for input shorter than /// [`SPI_COMPOUND_WAIT_STATUS_LEN`] instead. - // fusa:req REQ-SPI-011 + //fusa:req REQ-SPI-011 pub fn decode(b: &[u8]) -> Result { if b.len() < SPI_COMPOUND_WAIT_STATUS_LEN { return Err(RcpError::ShortFrame); @@ -454,7 +454,7 @@ impl SpiCompoundWaitStatus { /// for why this function is standalone plumbing not wired into any /// compound-wait execution path (`ROADMAP.md` Milestone 5, not yet built). /// Never panics for any input. -// fusa:req REQ-SPI-012 +//fusa:req REQ-SPI-012 pub fn truncate_spi_status_for_compound_wait(status: SpiStatus) -> SpiCompoundWaitStatus { let mut buf = [0u8; SPI_COMPOUND_WAIT_STATUS_LEN]; buf.copy_from_slice(&status.0[0..SPI_COMPOUND_WAIT_STATUS_LEN]); @@ -479,7 +479,7 @@ mod tests { ]; #[test] - // fusa:test REQ-SPI-001 + //fusa:test REQ-SPI-001 fn spi_channel_select_sub_opcode_round_trips_for_all_eight_values() { for select in ALL_CHANNEL_SELECTS { let raw = select.to_sub_opcode(); @@ -489,7 +489,7 @@ mod tests { } #[test] - // fusa:test REQ-SPI-001 + //fusa:test REQ-SPI-001 fn spi_channel_select_sub_opcode_values_are_the_full_0_to_7_range() { let mut raws: Vec = ALL_CHANNEL_SELECTS .iter() @@ -500,7 +500,7 @@ mod tests { } #[test] - // fusa:test REQ-SPI-002 + //fusa:test REQ-SPI-002 fn spi_channel_select_from_sub_opcode_rejects_out_of_range() { for raw in [8u8, 9, 0x7F, 0xFF] { assert_eq!( @@ -511,7 +511,7 @@ mod tests { } #[test] - // fusa:test REQ-SPI-003 + //fusa:test REQ-SPI-003 fn spi_channel_select_is_named_true_only_for_the_six_named_channels() { for select in ALL_CHANNEL_SELECTS { let expected = !matches!( @@ -523,7 +523,7 @@ mod tests { } #[test] - // fusa:test REQ-SPI-001 + //fusa:test REQ-SPI-001 fn spi_channel_select_reserved6_and_reconfigure7_are_distinct_values() { // Issue #100: the reserved and reconfigure high codes must no longer // be interchangeable "spare" values — they are distinct `sub_opcode` @@ -536,7 +536,7 @@ mod tests { // ── resolve_spi_channel_index ──────────────────────────────────────────── #[test] - // fusa:test REQ-SPI-004 + //fusa:test REQ-SPI-004 fn resolve_spi_channel_index_maps_named_channels_to_0_through_5() { let expected = [ (SpiChannelSelect::Channel0, 0usize), @@ -552,7 +552,7 @@ mod tests { } #[test] - // fusa:test REQ-SPI-004 + //fusa:test REQ-SPI-004 fn resolve_spi_channel_index_refuses_reserved_and_reconfigure() { for select in [SpiChannelSelect::Reserved6, SpiChannelSelect::Reconfigure7] { assert_eq!( @@ -565,7 +565,7 @@ mod tests { // ── SpiFunctionalConfig / select_spi_channel_config ───────────────────── #[test] - // fusa:test REQ-SPI-005 + //fusa:test REQ-SPI-005 fn spi_functional_config_has_exactly_six_channel_slots() { let config = SpiFunctionalConfig::default(); assert_eq!(config.channels.len(), SPI_CHANNEL_COUNT); @@ -573,7 +573,7 @@ mod tests { } #[test] - // fusa:test REQ-SPI-006 + //fusa:test REQ-SPI-006 fn select_spi_channel_config_resolves_named_channels() { let config = SpiFunctionalConfig::default(); for select in [ @@ -592,7 +592,7 @@ mod tests { } #[test] - // fusa:test REQ-SPI-006 + //fusa:test REQ-SPI-006 fn select_spi_channel_config_refuses_reserved_and_reconfigure_selections() { let config = SpiFunctionalConfig::default(); for select in [SpiChannelSelect::Reserved6, SpiChannelSelect::Reconfigure7] { @@ -604,7 +604,7 @@ mod tests { } #[test] - // fusa:test REQ-SPI-007 + //fusa:test REQ-SPI-007 fn spi_functional_config_layer_tag_matches_ep_type_spi() { let functional = SpiFunctionalConfig::default(); let generic = crate::regmap::PerEpConfigBlock::new(crate::regmap::EndpointType::Spi); @@ -620,7 +620,7 @@ mod tests { } #[test] - // fusa:test REQ-SPI-007 + //fusa:test REQ-SPI-007 fn spi_functional_config_layer_tag_rejects_mismatched_ep_type() { let functional = SpiFunctionalConfig::default(); let generic = crate::regmap::PerEpConfigBlock::new(crate::regmap::EndpointType::Gpio); @@ -633,7 +633,7 @@ mod tests { // ── SpiByteTransfer / SpiByteTransferResult: round-trip / never-panic ── #[test] - // fusa:test REQ-SPI-008 + //fusa:test REQ-SPI-008 fn spi_byte_transfer_round_trips_through_encode_decode() { for bytes in [vec![], vec![0x00], vec![0xAA; 3], (0u8..=255).collect()] { let transfer = SpiByteTransfer { @@ -644,7 +644,7 @@ mod tests { } #[test] - // fusa:test REQ-SPI-008 + //fusa:test REQ-SPI-008 fn spi_byte_transfer_decode_never_panics_for_any_sampled_input() { for len in [0usize, 1, 2, 7, 64] { let buf = vec![0x5Au8; len]; @@ -653,7 +653,7 @@ mod tests { } #[test] - // fusa:test REQ-SPI-009 + //fusa:test REQ-SPI-009 fn spi_byte_transfer_result_round_trips_through_encode_decode() { for bytes in [vec![], vec![0xFF], vec![0x01, 0x02, 0x03]] { let result = SpiByteTransferResult { @@ -664,7 +664,7 @@ mod tests { } #[test] - // fusa:test REQ-SPI-009 + //fusa:test REQ-SPI-009 fn spi_byte_transfer_result_decode_never_panics_for_any_sampled_input() { for len in [0usize, 1, 5, 32] { let buf = vec![0xA5u8; len]; @@ -683,14 +683,14 @@ mod tests { } #[test] - // fusa:test REQ-SPI-010 + //fusa:test REQ-SPI-010 fn spi_status_round_trips_through_encode_decode() { let status = sample_status(); assert_eq!(SpiStatus::decode(&status.encode()).unwrap(), status); } #[test] - // fusa:test REQ-SPI-010 + //fusa:test REQ-SPI-010 fn spi_status_decode_rejects_short_input() { for len in 0..SPI_STATUS_LEN { let short = vec![0xAAu8; len]; @@ -699,7 +699,7 @@ mod tests { } #[test] - // fusa:test REQ-SPI-010 + //fusa:test REQ-SPI-010 fn spi_status_decode_ignores_trailing_bytes() { let mut b = sample_status().encode().to_vec(); b.extend_from_slice(&[0xFF, 0xFF]); @@ -707,7 +707,7 @@ mod tests { } #[test] - // fusa:test REQ-SPI-011 + //fusa:test REQ-SPI-011 fn spi_compound_wait_status_round_trips_through_encode_decode() { let status = SpiCompoundWaitStatus([0x00, 0x01, 0x02, 0x03]); assert_eq!( @@ -717,7 +717,7 @@ mod tests { } #[test] - // fusa:test REQ-SPI-011 + //fusa:test REQ-SPI-011 fn spi_compound_wait_status_decode_rejects_short_input() { for len in 0..SPI_COMPOUND_WAIT_STATUS_LEN { let short = vec![0xAAu8; len]; @@ -731,7 +731,7 @@ mod tests { // ── truncate_spi_status_for_compound_wait ─────────────────────────────── #[test] - // fusa:test REQ-SPI-012 + //fusa:test REQ-SPI-012 fn truncate_spi_status_for_compound_wait_keeps_the_leading_four_bytes() { let status = sample_status(); let truncated = truncate_spi_status_for_compound_wait(status); @@ -740,7 +740,7 @@ mod tests { } #[test] - // fusa:test REQ-SPI-012 + //fusa:test REQ-SPI-012 fn truncate_spi_status_for_compound_wait_never_panics_for_any_sampled_input() { let samples = [ [0u8; SPI_STATUS_LEN], diff --git a/src/timestamp.rs b/src/timestamp.rs index a5a909e..b0a1319 100644 --- a/src/timestamp.rs +++ b/src/timestamp.rs @@ -1,9 +1,9 @@ -// fusa:req REQ-TS-001 -// fusa:req REQ-TS-002 -// fusa:req REQ-TS-003 -// fusa:req REQ-TS-004 -// fusa:req REQ-TS-005 -// fusa:req REQ-TS-006 +//fusa:req REQ-TS-001 +//fusa:req REQ-TS-002 +//fusa:req REQ-TS-003 +//fusa:req REQ-TS-004 +//fusa:req REQ-TS-005 +//fusa:req REQ-TS-006 //! Timestamp semantics — TC18 wire format core (`ROADMAP.md` Milestone 1, //! "Timestamp Semantics" subsection). @@ -99,7 +99,7 @@ /// /// Returned by [`AvtpTimestamp::semantics`] and [`MessageTimestamp::semantics`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -// fusa:req REQ-TS-004 +//fusa:req REQ-TS-004 pub enum TimestampMeaning { /// The raw value falls in this module's untimed fallback region (exact /// all-zero — see the module's provenance note) and must be treated as @@ -119,13 +119,23 @@ pub enum TimestampMeaning { /// A distinct type from [`MessageTimestamp`] by design — see the module doc /// comment's "Distinct widths, distinct rollover periods" section. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)] -// fusa:req REQ-TS-001 +//fusa:req REQ-TS-001 pub struct AvtpTimestamp(pub u32); impl AvtpTimestamp { /// This type's rollover period, in raw ticks: the field's full 32-bit - /// width. See the module's provenance note for what this does and does - /// not claim about real-world tick units. + /// width. + /// + /// TC18 §11.4.1 (TC18.txt lines 1952-1953) confirms both the tick unit + /// and the period: "avtp_timestamp = (AS_sec × 10^9 + AS_ns) mod 2^32 + /// where AS_sec is the gPTP seconds field and AS_ns is the gPTP + /// nanoseconds field (thus rolls over every 4 seconds)" — i.e. the + /// ticks are nanoseconds and the modulus is the field's full 2^32 + /// width, which at 1 ns/tick is 4.294967296 s. See the module's + /// provenance note (now partly reconciled by that clause) and + /// `REQ-TIME-004` for the gPTP-derivation half of the same clause, + /// which this crate does not implement. + //fusa:req REQ-TS-007 pub const ROLLOVER_PERIOD: u64 = 1u64 << 32; /// Wrap a raw `u32` value (e.g. from @@ -142,7 +152,7 @@ impl AvtpTimestamp { /// This value's fallback-rule interpretation. See /// [`TimestampMeaning`] and the module's provenance note. - // fusa:req REQ-TS-004 + //fusa:req REQ-TS-004 pub fn semantics(self) -> TimestampMeaning { if self.0 == 0 { TimestampMeaning::Untimed @@ -152,7 +162,7 @@ impl AvtpTimestamp { } /// Shorthand for `self.semantics() == TimestampMeaning::Untimed`. - // fusa:req REQ-TS-004 + //fusa:req REQ-TS-004 pub fn is_untimed(self) -> bool { self.semantics() == TimestampMeaning::Untimed } @@ -171,14 +181,14 @@ impl AvtpTimestamp { /// beyond the sign this arithmetic happens to produce for them (see /// the "exactly half a period apart" test below for the boundary this /// module resolves to). - // fusa:req REQ-TS-002 + //fusa:req REQ-TS-002 pub fn wrapping_delta(self, earlier: Self) -> i64 { i64::from(self.0.wrapping_sub(earlier.0) as i32) } /// `true` if `self` is logically after `other`, per /// [`Self::wrapping_delta`] — i.e. `self.wrapping_delta(other) > 0`. - // fusa:req REQ-TS-002 + //fusa:req REQ-TS-002 pub fn is_after(self, other: Self) -> bool { self.wrapping_delta(other) > 0 } @@ -204,14 +214,20 @@ impl From for u32 { /// A distinct type from [`AvtpTimestamp`] by design — see the module doc /// comment's "Distinct widths, distinct rollover periods" section. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)] -// fusa:req REQ-TS-001 +//fusa:req REQ-TS-001 pub struct MessageTimestamp(pub u64); impl MessageTimestamp { /// This type's rollover period, in raw ticks: the field's full 64-bit /// width — 2^32 times longer than [`AvtpTimestamp::ROLLOVER_PERIOD`]. - /// See the module's provenance note for what this does and does not - /// claim about real-world tick units. + /// + /// TC18 §11.4.1 (TC18.txt lines 1954-1955) confirms both the tick unit + /// and the period: "message_timestamp = (AS_sec × 10^9 + AS_ns) mod + /// 2^64 where AS_sec is the gPTP seconds field and AS_ns is the gPTP + /// nanoseconds field (thus rolls over every 584,9 years)" — i.e. the + /// ticks are nanoseconds and the modulus is the field's full 2^64 + /// width, which at 1 ns/tick is ~584.9 years of 365 days. + //fusa:req REQ-TS-007 pub const ROLLOVER_PERIOD: u128 = 1u128 << 64; /// Wrap a raw `u64` value (e.g. from @@ -228,7 +244,7 @@ impl MessageTimestamp { /// This value's fallback-rule interpretation. See /// [`TimestampMeaning`] and the module's provenance note. - // fusa:req REQ-TS-004 + //fusa:req REQ-TS-004 pub fn semantics(self) -> TimestampMeaning { if self.0 == 0 { TimestampMeaning::Untimed @@ -238,7 +254,7 @@ impl MessageTimestamp { } /// Shorthand for `self.semantics() == TimestampMeaning::Untimed`. - // fusa:req REQ-TS-004 + //fusa:req REQ-TS-004 pub fn is_untimed(self) -> bool { self.semantics() == TimestampMeaning::Untimed } @@ -252,14 +268,14 @@ impl MessageTimestamp { /// rollover period of one another — see /// [`AvtpTimestamp::wrapping_delta`]'s doc comment for the boundary /// case this module resolves to. - // fusa:req REQ-TS-003 + //fusa:req REQ-TS-003 pub fn wrapping_delta(self, earlier: Self) -> i64 { self.0.wrapping_sub(earlier.0) as i64 } /// `true` if `self` is logically after `other`, per /// [`Self::wrapping_delta`] — i.e. `self.wrapping_delta(other) > 0`. - // fusa:req REQ-TS-003 + //fusa:req REQ-TS-003 pub fn is_after(self, other: Self) -> bool { self.wrapping_delta(other) > 0 } @@ -291,7 +307,7 @@ mod tests { // ═══════════════════════════════════════════════════════════════════ #[test] - // fusa:test REQ-TS-001 + //fusa:test REQ-TS-001 fn avtp_timestamp_and_message_timestamp_are_distinct_types() { // This is primarily a compile-time property (there is no shared // trait or cross-type comparison between the two), but the two @@ -306,7 +322,7 @@ mod tests { } #[test] - // fusa:test REQ-TS-001 + //fusa:test REQ-TS-001 fn rollover_periods_are_distinct() { assert_eq!(AvtpTimestamp::ROLLOVER_PERIOD, 1u64 << 32); assert_eq!(MessageTimestamp::ROLLOVER_PERIOD, 1u128 << 64); @@ -321,7 +337,7 @@ mod tests { // ═══════════════════════════════════════════════════════════════════ #[test] - // fusa:test REQ-TS-004 + //fusa:test REQ-TS-004 fn avtp_timestamp_zero_is_untimed() { assert_eq!(AvtpTimestamp::new(0).semantics(), TimestampMeaning::Untimed); assert!(AvtpTimestamp::new(0).is_untimed()); @@ -329,7 +345,7 @@ mod tests { } #[test] - // fusa:test REQ-TS-004 + //fusa:test REQ-TS-004 fn avtp_timestamp_nonzero_is_timed() { for raw in [1u32, 2, 0x1234_5678, u32::MAX] { let ts = AvtpTimestamp::new(raw); @@ -343,7 +359,7 @@ mod tests { // ═══════════════════════════════════════════════════════════════════ #[test] - // fusa:test REQ-TS-004 + //fusa:test REQ-TS-004 fn message_timestamp_zero_is_untimed() { assert_eq!( MessageTimestamp::new(0).semantics(), @@ -354,7 +370,7 @@ mod tests { } #[test] - // fusa:test REQ-TS-004 + //fusa:test REQ-TS-004 fn message_timestamp_nonzero_is_timed() { for raw in [1u64, 2, 0x0123_4567_89AB_CDEF, u64::MAX] { let ts = MessageTimestamp::new(raw); @@ -368,7 +384,7 @@ mod tests { // ═══════════════════════════════════════════════════════════════════ #[test] - // fusa:test REQ-TS-002 + //fusa:test REQ-TS-002 fn avtp_timestamp_delta_without_wraparound() { let earlier = AvtpTimestamp::new(100); let later = AvtpTimestamp::new(140); @@ -379,7 +395,7 @@ mod tests { } #[test] - // fusa:test REQ-TS-002 + //fusa:test REQ-TS-002 fn avtp_timestamp_delta_across_rollover_boundary() { // 0 is one tick after u32::MAX, wrapping — not "4294967295 ticks // behind" if the rollover is accounted for. @@ -396,7 +412,7 @@ mod tests { } #[test] - // fusa:test REQ-TS-002 + //fusa:test REQ-TS-002 fn avtp_timestamp_delta_of_self_is_zero() { let ts = AvtpTimestamp::new(0xDEAD_BEEF); assert_eq!(ts.wrapping_delta(ts), 0); @@ -404,7 +420,7 @@ mod tests { } #[test] - // fusa:test REQ-TS-002 + //fusa:test REQ-TS-002 fn avtp_timestamp_exactly_half_period_apart_resolves_to_not_after() { // Exactly half a rollover period apart has no unambiguous // ordering; this module's arithmetic (bit-pattern reinterpreted as @@ -424,7 +440,7 @@ mod tests { // ═══════════════════════════════════════════════════════════════════ #[test] - // fusa:test REQ-TS-003 + //fusa:test REQ-TS-003 fn message_timestamp_delta_without_wraparound() { let earlier = MessageTimestamp::new(1_000); let later = MessageTimestamp::new(1_500); @@ -435,7 +451,7 @@ mod tests { } #[test] - // fusa:test REQ-TS-003 + //fusa:test REQ-TS-003 fn message_timestamp_delta_across_rollover_boundary() { let just_wrapped = MessageTimestamp::new(0); let just_before_wrap = MessageTimestamp::new(u64::MAX); @@ -450,7 +466,7 @@ mod tests { } #[test] - // fusa:test REQ-TS-003 + //fusa:test REQ-TS-003 fn message_timestamp_delta_of_self_is_zero() { let ts = MessageTimestamp::new(0xDEAD_BEEF_0000_0001); assert_eq!(ts.wrapping_delta(ts), 0); @@ -458,7 +474,7 @@ mod tests { } #[test] - // fusa:test REQ-TS-003 + //fusa:test REQ-TS-003 fn message_timestamp_exactly_half_period_apart_resolves_to_not_after() { let a = MessageTimestamp::new(0); let b = MessageTimestamp::new(1u64 << 63); @@ -473,7 +489,7 @@ mod tests { // ═══════════════════════════════════════════════════════════════════ #[test] - // fusa:test REQ-TS-005 + //fusa:test REQ-TS-005 fn avtp_timestamp_round_trips_through_u32() { for raw in [0u32, 1, 0x1234_5678, u32::MAX] { assert_eq!(AvtpTimestamp::new(raw).to_u32(), raw); @@ -482,7 +498,7 @@ mod tests { } #[test] - // fusa:test REQ-TS-005 + //fusa:test REQ-TS-005 fn message_timestamp_round_trips_through_u64() { for raw in [0u64, 1, 0x0123_4567_89AB_CDEF, u64::MAX] { assert_eq!(MessageTimestamp::new(raw).to_u64(), raw); @@ -491,7 +507,7 @@ mod tests { } #[test] - // fusa:test REQ-TS-005 + //fusa:test REQ-TS-005 fn avtp_timestamp_interoperates_with_tscf_header_field() { let hdr = TscfHeader { sequence_num: 1, @@ -505,7 +521,7 @@ mod tests { } #[test] - // fusa:test REQ-TS-005 + //fusa:test REQ-TS-005 fn message_timestamp_interoperates_with_acf_gbb_message_field() { let msg = AcfGbbMessage { info: ByteMessageInfo::default(), @@ -518,7 +534,7 @@ mod tests { } #[test] - // fusa:test REQ-TS-005 + //fusa:test REQ-TS-005 fn zero_message_timestamp_on_acf_gbb_message_is_untimed() { // The fallback rule applies identically to a message_timestamp // sourced from a real AcfGbbMessage, not just a bare u64. @@ -535,7 +551,7 @@ mod tests { // ═══════════════════════════════════════════════════════════════════ #[test] - // fusa:test REQ-TS-006 + //fusa:test REQ-TS-006 fn avtp_timestamp_operations_never_panic_across_arbitrary_input() { let mut state: u32 = 0x2468_ACE0; let mut next = || { @@ -561,7 +577,7 @@ mod tests { } #[test] - // fusa:test REQ-TS-006 + //fusa:test REQ-TS-006 fn message_timestamp_operations_never_panic_across_arbitrary_input() { let mut state: u64 = 0x1234_5678_9ABC_DEF0; let mut next = || { @@ -585,4 +601,50 @@ mod tests { } } } + + // ═══════════════════════════════════════════════════════════════════ + // TC18 §11.4.1 rollover periods + // ═══════════════════════════════════════════════════════════════════ + + #[test] + //fusa:test REQ-TS-007 + fn rollover_periods_match_tc18_11_4_1_nanosecond_derivation() { + // TC18 §11.4.1 (TC18.txt lines 1952-1955) states both moduli and + // both resulting real-world periods, with the tick unit fixed at + // nanoseconds by the `AS_sec × 10^9 + AS_ns` construction: + // + // avtp_timestamp = (...) mod 2^32 -> "rolls over every 4 seconds" + // message_timestamp = (...) mod 2^64 -> "rolls over every 584,9 years" + // + // The moduli below are written out as literal powers of two read + // from that clause, not from this crate's own constants. + const TC18_AVTP_MODULUS: u64 = 4_294_967_296; // 2^32 + const TC18_MESSAGE_MODULUS: u128 = 18_446_744_073_709_551_616; // 2^64 + const NANOS_PER_SECOND: u128 = 1_000_000_000; + const SECONDS_PER_365_DAY_YEAR: u128 = 365 * 24 * 60 * 60; + + assert_eq!(AvtpTimestamp::ROLLOVER_PERIOD, TC18_AVTP_MODULUS); + assert_eq!(MessageTimestamp::ROLLOVER_PERIOD, TC18_MESSAGE_MODULUS); + + // 2^32 ns = 4.294967296 s, i.e. TC18's "every 4 seconds" (whole + // seconds: 4, and strictly less than 5). + let avtp_seconds = u128::from(AvtpTimestamp::ROLLOVER_PERIOD) / NANOS_PER_SECOND; + assert_eq!(avtp_seconds, 4, "TC18 §11.4.1: rolls over every 4 seconds"); + + // 2^64 ns = 18 446 744 073.709551616 s = 584.94... 365-day years, + // i.e. TC18's "584,9 years" to one decimal place. + let message_tenths_of_a_year = (MessageTimestamp::ROLLOVER_PERIOD * 10) + / (NANOS_PER_SECOND * SECONDS_PER_365_DAY_YEAR); + assert_eq!( + message_tenths_of_a_year, 5849, + "TC18 §11.4.1: rolls over every 584,9 years" + ); + + // The two periods differ by exactly the 2^32 factor the two field + // widths imply. + assert_eq!( + MessageTimestamp::ROLLOVER_PERIOD / u128::from(AvtpTimestamp::ROLLOVER_PERIOD), + u128::from(TC18_AVTP_MODULUS) + ); + } } diff --git a/src/tlstransport.rs b/src/tlstransport.rs index 97f107d..97c5718 100644 --- a/src/tlstransport.rs +++ b/src/tlstransport.rs @@ -1,8 +1,8 @@ -// fusa:req REQ-TLS-001 -// fusa:req REQ-TLS-002 -// fusa:req REQ-TLS-003 -// fusa:req REQ-TLS-004 -// fusa:req REQ-TLS-005 +//fusa:req REQ-TLS-001 +//fusa:req REQ-TLS-002 +//fusa:req REQ-TLS-003 +//fusa:req REQ-TLS-004 +//fusa:req REQ-TLS-005 //! TLS transport bridge — wraps TC18 AVTPDU/ACF frames in a TLS stream. //! @@ -35,17 +35,17 @@ use crate::RcpError; // ── TLS configuration ───────────────────────────────────────────────────────── /// Minimum acceptable TLS version. -// fusa:req REQ-TLS-001 +//fusa:req REQ-TLS-001 pub const MIN_TLS_VERSION: &str = "TLSv1.2"; /// Whether mutual (client + server) authentication is required. -// fusa:req REQ-TLS-002 +//fusa:req REQ-TLS-002 pub const REQUIRE_MUTUAL_AUTH: bool = true; // ── TlsStream trait ─────────────────────────────────────────────────────────── /// Abstract TLS stream for bridge testability. -// fusa:req REQ-TLS-003 +//fusa:req REQ-TLS-003 pub trait TlsStream: Send + Sync { fn write_all(&self, data: &[u8]) -> Result<(), RcpError>; fn read_to_vec(&self, timeout: Option) -> Result, RcpError>; @@ -56,7 +56,7 @@ pub trait TlsStream: Send + Sync { /// TLS-secured transport, addressed by `local_stream` /// ([`crate::avtp::StreamId`]) rather than the legacy `Zone`. -// fusa:req REQ-TLS-004 +//fusa:req REQ-TLS-004 pub struct TlsBridge { local_stream: StreamId, stream: Arc, @@ -66,7 +66,7 @@ impl TlsBridge { /// Create a TLS bridge. Returns `Err(RcpError::NotConnected)` if mutual /// auth is required but the peer is not verified. Unchanged from this /// module's pre-Milestone-9 version, per its ADAPT scope. - // fusa:req REQ-TLS-002 + //fusa:req REQ-TLS-002 pub fn new(local_stream: StreamId, stream: Arc) -> Result { if REQUIRE_MUTUAL_AUTH && !stream.peer_verified() { return Err(RcpError::NotConnected); @@ -87,8 +87,8 @@ impl TlsBridge { /// ([`crate::acf::verify_echo_back`]) — the same framing /// `crate::udp::UdpTransport::send_acf_abb` uses, over a [`TlsStream`] /// instead of a UDP socket. - // fusa:req REQ-TLS-004 - // fusa:req REQ-WIRE-006 + //fusa:req REQ-TLS-004 + //fusa:req REQ-WIRE-006 pub fn send_acf_abb( &self, msg: &AcfAbbMessage, @@ -109,8 +109,8 @@ impl TlsBridge { } /// Same as [`Self::send_acf_abb`], for an ACF_GBB request/response pair. - // fusa:req REQ-TLS-004 - // fusa:req REQ-WIRE-006 + //fusa:req REQ-TLS-004 + //fusa:req REQ-WIRE-006 pub fn send_acf_gbb( &self, msg: &AcfGbbMessage, @@ -131,7 +131,7 @@ impl TlsBridge { } /// No-op, matching this module's pre-Milestone-9 behavior. - // fusa:req REQ-TLS-005 + //fusa:req REQ-TLS-005 pub fn close(&self) -> Result<(), RcpError> { Ok(()) } @@ -188,7 +188,7 @@ mod tests { } #[test] - // fusa:test REQ-TLS-002 + //fusa:test REQ-TLS-002 fn unverified_peer_rejected() { let stream = Arc::new(MockTls { verified: false, @@ -199,10 +199,10 @@ mod tests { } #[test] - // fusa:test REQ-TLS-002 - // fusa:test REQ-TLS-003 - // fusa:test REQ-TLS-004 - // fusa:test REQ-WIRE-006 + //fusa:test REQ-TLS-002 + //fusa:test REQ-TLS-003 + //fusa:test REQ-TLS-004 + //fusa:test REQ-WIRE-006 fn tls_send_acf_abb_ok_with_verified_peer() { let stream = Arc::new(MockTls { verified: true, @@ -215,7 +215,7 @@ mod tests { } #[test] - // fusa:test REQ-TLS-004 + //fusa:test REQ-TLS-004 fn tls_send_acf_abb_rejects_echo_back_mismatch() { let stream = Arc::new(MockTls { verified: true, @@ -227,13 +227,13 @@ mod tests { } #[test] - // fusa:test REQ-TLS-001 + //fusa:test REQ-TLS-001 fn min_tls_version_constant() { assert_eq!(MIN_TLS_VERSION, "TLSv1.2"); } #[test] - // fusa:test REQ-TLS-005 + //fusa:test REQ-TLS-005 fn close_is_noop() { let stream = Arc::new(MockTls { verified: true, diff --git a/src/uart.rs b/src/uart.rs index d05c6ec..fbf5150 100644 --- a/src/uart.rs +++ b/src/uart.rs @@ -1,13 +1,13 @@ -// fusa:req REQ-UART-001 -// fusa:req REQ-UART-002 -// fusa:req REQ-UART-003 -// fusa:req REQ-UART-004 -// fusa:req REQ-UART-005 -// fusa:req REQ-UART-006 -// fusa:req REQ-UART-007 -// fusa:req REQ-UART-008 -// fusa:req REQ-UART-009 -// fusa:req REQ-UART-010 +//fusa:req REQ-UART-001 +//fusa:req REQ-UART-002 +//fusa:req REQ-UART-003 +//fusa:req REQ-UART-004 +//fusa:req REQ-UART-005 +//fusa:req REQ-UART-006 +//fusa:req REQ-UART-007 +//fusa:req REQ-UART-008 +//fusa:req REQ-UART-009 +//fusa:req REQ-UART-010 //! The UART endpoint type (`ep_type 0x05`) — `ROADMAP.md` Milestone 4 //! ("Basic Endpoint Types"), fourth checklist bullet: "independent TX/RX @@ -182,7 +182,7 @@ use crate::RcpError; /// stream. Every possible byte slice, including an empty one, has a valid /// encoding, so [`UartTxQueue::decode`] is infallible. #[derive(Debug, Clone, PartialEq, Eq, Default)] -// fusa:req REQ-UART-001 +//fusa:req REQ-UART-001 pub struct UartTxQueue { /// The raw bytes queued for transmission. pub bytes: Vec, @@ -191,7 +191,7 @@ pub struct UartTxQueue { impl UartTxQueue { /// Encode this queue's bytes to their raw wire representation: /// `bytes`, unmodified and unframed. - // fusa:req REQ-UART-001 + //fusa:req REQ-UART-001 pub fn encode(&self) -> Vec { self.bytes.clone() } @@ -200,7 +200,7 @@ impl UartTxQueue { /// /// Every possible byte slice, including an empty one, is a valid TX /// queue payload, so this never fails and never panics for any input. - // fusa:req REQ-UART-001 + //fusa:req REQ-UART-001 pub fn decode(b: &[u8]) -> Self { Self { bytes: b.to_vec() } } @@ -212,7 +212,7 @@ impl UartTxQueue { /// See [`UartTxQueue`]'s doc comment — this is the same unstructured, /// variable-length byte-stream modeling for the opposite queue/direction. #[derive(Debug, Clone, PartialEq, Eq, Default)] -// fusa:req REQ-UART-002 +//fusa:req REQ-UART-002 pub struct UartRxQueue { /// The raw bytes collected from reception. pub bytes: Vec, @@ -221,7 +221,7 @@ pub struct UartRxQueue { impl UartRxQueue { /// Encode this queue's bytes to their raw wire representation: /// `bytes`, unmodified and unframed. - // fusa:req REQ-UART-002 + //fusa:req REQ-UART-002 pub fn encode(&self) -> Vec { self.bytes.clone() } @@ -230,7 +230,7 @@ impl UartRxQueue { /// /// Every possible byte slice, including an empty one, is a valid RX /// queue payload, so this never fails and never panics for any input. - // fusa:req REQ-UART-002 + //fusa:req REQ-UART-002 pub fn decode(b: &[u8]) -> Self { Self { bytes: b.to_vec() } } @@ -244,7 +244,7 @@ impl UartRxQueue { /// "Provenance note: two queues, one shared config block" for why the /// checklist names no concrete TX-side field. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-UART-003 +//fusa:req REQ-UART-003 pub struct UartTxQueueConfig; /// The RX queue's own config content: the `read_size`/`uart_timeout` @@ -256,7 +256,7 @@ pub struct UartTxQueueConfig; /// [`crate::acf::ReadSizeOrSegment`] and for `uart_timeout`'s own /// unconfirmed width/units. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-UART-003 +//fusa:req REQ-UART-003 pub struct UartRxQueueConfig { /// The read-size completion threshold, reusing the same wire field /// [`crate::acf::ByteMessageInfo::read_size_segment`] already @@ -279,7 +279,7 @@ pub struct UartRxQueueConfig { /// [`crate::spi::SpiFunctionalConfig`], and /// [`crate::i2c::I2cFunctionalConfig`] each used. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-UART-004 +//fusa:req REQ-UART-004 pub struct UartFunctionalConfig { /// The TX queue's own config content. pub tx: UartTxQueueConfig, @@ -295,7 +295,7 @@ impl UartFunctionalConfig { /// This module does not itself call that function — it only shows how a /// caller would obtain the matching tag, per this module's doc comment /// "Relationship to `crate::regmap`". - // fusa:req REQ-UART-004 + //fusa:req REQ-UART-004 pub fn layer_tag(&self) -> crate::regmap::PerEpTypeFunctionalConfig { crate::regmap::PerEpTypeFunctionalConfig::new(crate::regmap::EndpointType::Uart) } @@ -311,9 +311,9 @@ impl UartFunctionalConfig { /// exists as an explicit third outcome rather than either of the other two /// being silently preferred. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// fusa:req REQ-UART-005 -// fusa:req REQ-UART-006 -// fusa:req REQ-UART-007 +//fusa:req REQ-UART-005 +//fusa:req REQ-UART-006 +//fusa:req REQ-UART-007 pub enum UartReadCompletionReason { /// Only the `read_size` byte-count threshold was met. ReadSizeReached, @@ -334,10 +334,10 @@ pub enum UartReadCompletionReason { /// "Provenance note: the `read_size`/`uart_timeout` race" for why a /// zero-valued threshold is not treated as "disabled." Never panics for any /// input. -// fusa:req REQ-UART-005 -// fusa:req REQ-UART-006 -// fusa:req REQ-UART-007 -// fusa:req REQ-UART-008 +//fusa:req REQ-UART-005 +//fusa:req REQ-UART-006 +//fusa:req REQ-UART-007 +//fusa:req REQ-UART-008 pub fn resolve_uart_read_completion( rx: &UartRxQueueConfig, bytes_collected: u16, @@ -364,8 +364,8 @@ pub fn resolve_uart_read_completion( /// `RcpError::UnsupportedCmd`" for why this crate reads the checklist's /// `UNKNOWN_CMD` wording onto this already-defined variant. Never panics for /// any input. -// fusa:req REQ-UART-009 -// fusa:req REQ-UART-010 +//fusa:req REQ-UART-009 +//fusa:req REQ-UART-010 pub fn validate_uart_read_request(payload: &[u8]) -> Result<(), RcpError> { if payload.is_empty() { Ok(()) @@ -381,7 +381,7 @@ mod tests { // ── UartTxQueue / UartRxQueue: round-trip / never-panic ───────────────── #[test] - // fusa:test REQ-UART-001 + //fusa:test REQ-UART-001 fn uart_tx_queue_round_trips_through_encode_decode() { for bytes in [vec![], vec![0x00], vec![0xAA; 3], (0u8..=255).collect()] { let queue = UartTxQueue { @@ -392,7 +392,7 @@ mod tests { } #[test] - // fusa:test REQ-UART-001 + //fusa:test REQ-UART-001 fn uart_tx_queue_decode_never_panics_for_any_sampled_input() { for len in [0usize, 1, 2, 7, 64] { let buf = vec![0x5Au8; len]; @@ -401,7 +401,7 @@ mod tests { } #[test] - // fusa:test REQ-UART-002 + //fusa:test REQ-UART-002 fn uart_rx_queue_round_trips_through_encode_decode() { for bytes in [vec![], vec![0xFF], vec![0x01, 0x02, 0x03]] { let queue = UartRxQueue { @@ -412,7 +412,7 @@ mod tests { } #[test] - // fusa:test REQ-UART-002 + //fusa:test REQ-UART-002 fn uart_rx_queue_decode_never_panics_for_any_sampled_input() { for len in [0usize, 1, 5, 32] { let buf = vec![0xA5u8; len]; @@ -423,7 +423,7 @@ mod tests { // ── UartFunctionalConfig / layer_tag ───────────────────────────────────── #[test] - // fusa:test REQ-UART-003 + //fusa:test REQ-UART-003 fn uart_tx_and_rx_queue_configs_default_independently() { let config = UartFunctionalConfig::default(); assert_eq!(config.tx, UartTxQueueConfig); @@ -433,7 +433,7 @@ mod tests { } #[test] - // fusa:test REQ-UART-004 + //fusa:test REQ-UART-004 fn uart_functional_config_layer_tag_matches_ep_type_uart() { let functional = UartFunctionalConfig::default(); let generic = crate::regmap::PerEpConfigBlock::new(crate::regmap::EndpointType::Uart); @@ -449,7 +449,7 @@ mod tests { } #[test] - // fusa:test REQ-UART-004 + //fusa:test REQ-UART-004 fn uart_functional_config_layer_tag_rejects_mismatched_ep_type() { let functional = UartFunctionalConfig::default(); let generic = crate::regmap::PerEpConfigBlock::new(crate::regmap::EndpointType::I2c); @@ -469,7 +469,7 @@ mod tests { } #[test] - // fusa:test REQ-UART-005 + //fusa:test REQ-UART-005 fn resolve_uart_read_completion_reports_read_size_reached_only() { let rx = rx_config(10, 1000); assert_eq!( @@ -483,7 +483,7 @@ mod tests { } #[test] - // fusa:test REQ-UART-006 + //fusa:test REQ-UART-006 fn resolve_uart_read_completion_reports_timed_out_only() { let rx = rx_config(10, 1000); assert_eq!( @@ -497,7 +497,7 @@ mod tests { } #[test] - // fusa:test REQ-UART-007 + //fusa:test REQ-UART-007 fn resolve_uart_read_completion_reports_both_on_simultaneous_thresholds() { let rx = rx_config(10, 1000); assert_eq!( @@ -507,7 +507,7 @@ mod tests { } #[test] - // fusa:test REQ-UART-007 + //fusa:test REQ-UART-007 fn resolve_uart_read_completion_zeroed_config_resolves_both_immediately() { // See this module's doc comment: zero is not treated as a // "disabled" sentinel for either threshold. @@ -519,7 +519,7 @@ mod tests { } #[test] - // fusa:test REQ-UART-008 + //fusa:test REQ-UART-008 fn resolve_uart_read_completion_returns_none_before_either_threshold() { let rx = rx_config(10, 1000); assert_eq!(resolve_uart_read_completion(&rx, 9, 999), None); @@ -529,7 +529,7 @@ mod tests { } #[test] - // fusa:test REQ-UART-008 + //fusa:test REQ-UART-008 fn resolve_uart_read_completion_never_panics_for_any_sampled_input() { let configs = [ rx_config(0, 0), @@ -552,13 +552,13 @@ mod tests { // ── validate_uart_read_request: payload-less-read-only rule ───────────── #[test] - // fusa:test REQ-UART-009 + //fusa:test REQ-UART-009 fn validate_uart_read_request_accepts_empty_payload() { assert_eq!(validate_uart_read_request(&[]), Ok(())); } #[test] - // fusa:test REQ-UART-010 + //fusa:test REQ-UART-010 fn validate_uart_read_request_rejects_any_non_empty_payload() { for payload in [vec![0x00], vec![0x01, 0x02], vec![0xFF; 16]] { assert_eq!( @@ -569,7 +569,7 @@ mod tests { } #[test] - // fusa:test REQ-UART-010 + //fusa:test REQ-UART-010 fn validate_uart_read_request_never_panics_for_any_sampled_input() { for len in [0usize, 1, 2, 8, 64] { let buf = vec![0x5Au8; len]; diff --git a/src/udp.rs b/src/udp.rs index bf68181..c77ae56 100644 --- a/src/udp.rs +++ b/src/udp.rs @@ -1,17 +1,17 @@ -// fusa:req REQ-UDP-001 -// fusa:req REQ-UDP-002 -// fusa:req REQ-UDP-003 -// fusa:req REQ-UDP-004 -// fusa:req REQ-UDP-005 -// fusa:req REQ-UDP-006 -// fusa:req REQ-UDP-007 -// fusa:req REQ-UDP-008 -// fusa:req REQ-UDP-009 -// fusa:req REQ-UDP-010 -// fusa:req REQ-UDP-011 -// fusa:req REQ-UDP-012 -// fusa:req REQ-UDP-013 -// fusa:req REQ-UDP-014 +//fusa:req REQ-UDP-001 +//fusa:req REQ-UDP-002 +//fusa:req REQ-UDP-003 +//fusa:req REQ-UDP-004 +//fusa:req REQ-UDP-005 +//fusa:req REQ-UDP-006 +//fusa:req REQ-UDP-007 +//fusa:req REQ-UDP-008 +//fusa:req REQ-UDP-009 +//fusa:req REQ-UDP-010 +//fusa:req REQ-UDP-011 +//fusa:req REQ-UDP-012 +//fusa:req REQ-UDP-013 +//fusa:req REQ-UDP-014 //! UDP unicast transport for the TC18 AVTPDU/ACF wire format. //! @@ -108,7 +108,7 @@ use crate::RcpError; /// Abstract UDP socket for testability. Unchanged in shape from this /// module's pre-Milestone-9 version — only the bytes carried over it /// changed. -// fusa:req REQ-UDP-001 +//fusa:req REQ-UDP-001 pub trait UdpSocket: Send + Sync { fn send_to(&self, buf: &[u8], addr: SocketAddr) -> Result; fn recv_from(&self, timeout: Option) -> Result<(Vec, SocketAddr), RcpError>; @@ -123,7 +123,7 @@ pub trait UdpSocket: Send + Sync { /// doc comment, "Real OS-socket transport and IEEE 1722-2016 Annex J /// encapsulation", for this constant's provenance (public secondary /// sources, not the paywalled primary standard). -// fusa:req REQ-UDP-012 +//fusa:req REQ-UDP-012 pub const ANNEX_J_CONTROL_PORT: u16 = 17221; /// Standard destination UDP port for IEEE 1722-2016 Annex J "Continuous" @@ -139,7 +139,7 @@ pub const ANNEX_J_CONTINUOUS_PORT: u16 = 17220; /// crate's existing big-endian wire convention (e.g. /// [`crate::avtp::encode_ntscf_frame`]'s `stream_id` field, /// [`crate::acf`]'s `message_timestamp`). -// fusa:req REQ-UDP-012 +//fusa:req REQ-UDP-012 pub fn encode_annex_j_udp_payload(seq: u32, avtpdu: &[u8]) -> Vec { let mut buf = Vec::with_capacity(4 + avtpdu.len()); buf.extend_from_slice(&seq.to_be_bytes()); @@ -151,7 +151,7 @@ pub fn encode_annex_j_udp_payload(seq: u32, avtpdu: &[u8]) -> Vec { /// into its 4-byte encapsulation sequence number and the AVTPDU bytes that /// follow it. `Err(RcpError::ShortFrame)` for fewer than 4 bytes — never /// panics on truncated or empty input. -// fusa:req REQ-UDP-012 +//fusa:req REQ-UDP-012 pub fn decode_annex_j_udp_payload(buf: &[u8]) -> Result<(u32, &[u8]), RcpError> { if buf.len() < 4 { return Err(RcpError::ShortFrame); @@ -179,8 +179,8 @@ pub fn decode_annex_j_udp_payload(buf: &[u8]) -> Result<(u32, &[u8]), RcpError> /// increasing counter, starting at 0 on construction; it is not exposed to /// callers (see this module's own doc comment for why no receiver-side /// semantics are attached to it). -// fusa:req REQ-UDP-013 -// fusa:req REQ-UDP-014 +//fusa:req REQ-UDP-013 +//fusa:req REQ-UDP-014 pub struct StdUdpSocket { socket: std::net::UdpSocket, send_seq: AtomicU32, @@ -188,7 +188,7 @@ pub struct StdUdpSocket { impl StdUdpSocket { /// Bind a real UDP socket to `local_addr`. - // fusa:req REQ-UDP-013 + //fusa:req REQ-UDP-013 pub fn bind(local_addr: SocketAddr) -> Result { let socket = std::net::UdpSocket::bind(local_addr) .map_err(|e| RcpError::Other(format!("udp: bind {local_addr}: {e}")))?; @@ -203,7 +203,7 @@ impl StdUdpSocket { /// control-plane traffic. [`Self::bind`] remains available directly /// for an explicit port (testing, or a deployment that cannot use the /// standard port). - // fusa:req REQ-UDP-013 + //fusa:req REQ-UDP-013 pub fn new_default_port(bind_ip: std::net::IpAddr) -> Result { Self::bind(SocketAddr::new(bind_ip, ANNEX_J_CONTROL_PORT)) } @@ -223,7 +223,7 @@ impl UdpSocket for StdUdpSocket { /// including the prepended encapsulation sequence number — matching /// this trait's existing mock-implementation convention of echoing /// `buf.len()` back rather than any wire-framing overhead. - // fusa:req REQ-UDP-013 + //fusa:req REQ-UDP-013 fn send_to(&self, buf: &[u8], addr: SocketAddr) -> Result { let seq = self.send_seq.fetch_add(1, Ordering::Relaxed); let framed = encode_annex_j_udp_payload(seq, buf); @@ -238,7 +238,7 @@ impl UdpSocket for StdUdpSocket { /// indefinitely. A real OS-level timeout is mapped to /// `Err(RcpError::Timeout)`, matching every other timeout path in this /// crate. - // fusa:req REQ-UDP-014 + //fusa:req REQ-UDP-014 fn recv_from(&self, timeout: Option) -> Result<(Vec, SocketAddr), RcpError> { self.socket .set_read_timeout(timeout) @@ -269,7 +269,7 @@ impl UdpSocket for StdUdpSocket { /// matching this crate's existing discipline of taking such values as /// explicit parameters (e.g. `crate::discovery`'s `now: Instant`) rather /// than hiding a counter/clock behind an method that looks pure. -// fusa:req REQ-UDP-002 +//fusa:req REQ-UDP-002 pub struct UdpTransport { local_stream: StreamId, socket: Arc, @@ -299,9 +299,9 @@ impl UdpTransport { /// /// Returns `Err(RcpError::Timeout)` immediately for a zero `timeout`, /// matching this module's pre-Milestone-9 behavior. - // fusa:req REQ-UDP-003 - // fusa:req REQ-UDP-004 - // fusa:req REQ-WIRE-006 + //fusa:req REQ-UDP-003 + //fusa:req REQ-UDP-004 + //fusa:req REQ-WIRE-006 pub fn send_acf_abb( &self, msg: &AcfAbbMessage, @@ -322,9 +322,9 @@ impl UdpTransport { } /// Same as [`Self::send_acf_abb`], for an ACF_GBB request/response pair. - // fusa:req REQ-UDP-003 - // fusa:req REQ-UDP-004 - // fusa:req REQ-WIRE-006 + //fusa:req REQ-UDP-003 + //fusa:req REQ-UDP-004 + //fusa:req REQ-WIRE-006 pub fn send_acf_gbb( &self, msg: &AcfGbbMessage, @@ -345,7 +345,7 @@ impl UdpTransport { } /// No-op, matching this module's pre-Milestone-9 behavior. - // fusa:req REQ-UDP-005 + //fusa:req REQ-UDP-005 pub fn close(&self) -> Result<(), RcpError> { Ok(()) } @@ -358,7 +358,7 @@ impl UdpTransport { /// `DeviceEndpoint` case — unlike [`crate::ep0::RequestRoute`], which stops /// at the routing decision itself and never performs the lookup. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// fusa:req REQ-UDP-006 +//fusa:req REQ-UDP-006 pub enum ResolvedEndpoint { /// `byte_bus_id` was the reserved EP0 address. Ep0, @@ -375,8 +375,8 @@ pub enum ResolvedEndpoint { /// Returns `Err(RcpError::EpNotFound)` if `byte_bus_id` is not the reserved /// EP0 address and no endpoint is registered under `(stream_id, /// byte_bus_id)`. Never panics for any input. -// fusa:req REQ-UDP-006 -// fusa:req REQ-UDP-007 +//fusa:req REQ-UDP-006 +//fusa:req REQ-UDP-007 pub fn resolve_endpoint( endpoints: &EndpointTable, stream_id: StreamId, @@ -504,7 +504,7 @@ impl UdpRcServer { /// Starts with no discovery-stream claim held by anyone, matching /// [`crate::discovery::try_claim_discovery_stream`]'s own "`current` is /// `None`" unclaimed starting condition. - // fusa:req REQ-UDP-008 + //fusa:req REQ-UDP-008 pub fn new(local_stream: StreamId, socket: Arc, server: Arc) -> Self { UdpRcServer { local_stream, @@ -570,10 +570,10 @@ impl UdpRcServer { /// every response into the one outgoing frame in the same order. See /// [`crate::mock::RcServer::handle_ntscf_frame`]'s own doc comment for /// the same multi-request handling. - // fusa:req REQ-UDP-008 - // fusa:req REQ-UDP-009 - // fusa:req REQ-UDP-010 - // fusa:req REQ-UDP-011 + //fusa:req REQ-UDP-008 + //fusa:req REQ-UDP-009 + //fusa:req REQ-UDP-010 + //fusa:req REQ-UDP-011 pub fn serve_one( &self, recv_timeout: Option, @@ -608,10 +608,10 @@ impl UdpRcServer { /// The decoded-request half of [`Self::serve_one`] — see this type's own /// doc comment, "Discovery integration", for the three-case recognition /// order this implements. - // fusa:req REQ-UDP-008 - // fusa:req REQ-UDP-009 - // fusa:req REQ-UDP-010 - // fusa:req REQ-UDP-011 + //fusa:req REQ-UDP-008 + //fusa:req REQ-UDP-009 + //fusa:req REQ-UDP-010 + //fusa:req REQ-UDP-011 fn dispatch_request( &self, requester_stream: StreamId, @@ -676,7 +676,7 @@ mod tests { // ── Annex J encapsulation (pure byte manipulation, no socket) ───────── #[test] - // fusa:test REQ-UDP-012 + //fusa:test REQ-UDP-012 fn annex_j_encode_decode_round_trips() { let avtpdu = vec![0xDE, 0xAD, 0xBE, 0xEF, 0x01]; let encoded = encode_annex_j_udp_payload(7, &avtpdu); @@ -689,7 +689,7 @@ mod tests { } #[test] - // fusa:test REQ-UDP-012 + //fusa:test REQ-UDP-012 fn annex_j_encode_handles_empty_avtpdu() { let encoded = encode_annex_j_udp_payload(0xFFFF_FFFF, &[]); assert_eq!(encoded, vec![0xFF, 0xFF, 0xFF, 0xFF]); @@ -699,7 +699,7 @@ mod tests { } #[test] - // fusa:test REQ-UDP-012 + //fusa:test REQ-UDP-012 fn annex_j_decode_rejects_short_buffers() { for len in 0..4 { let buf = vec![0u8; len]; @@ -718,8 +718,8 @@ mod tests { // ── StdUdpSocket (real loopback sockets — no privileges required) ───── #[test] - // fusa:test REQ-UDP-013 - // fusa:test REQ-UDP-014 + //fusa:test REQ-UDP-013 + //fusa:test REQ-UDP-014 fn std_udp_socket_round_trips_over_real_loopback_socket() { let a = StdUdpSocket::bind("127.0.0.1:0".parse().unwrap()).unwrap(); let b = StdUdpSocket::bind("127.0.0.1:0".parse().unwrap()).unwrap(); @@ -733,8 +733,8 @@ mod tests { } #[test] - // fusa:test REQ-UDP-013 - // fusa:test REQ-UDP-014 + //fusa:test REQ-UDP-013 + //fusa:test REQ-UDP-014 fn std_udp_socket_and_udp_rc_server_serve_a_real_discovery_request_end_to_end() { // The same composition `src/bin/rcp.rs`'s `serve` command builds // (StdUdpSocket + UdpRcServer), but with both a real client and a @@ -779,7 +779,7 @@ mod tests { } #[test] - // fusa:test REQ-UDP-013 + //fusa:test REQ-UDP-013 fn std_udp_socket_send_seq_is_monotonically_increasing_on_the_wire() { // Inspect the real encapsulated bytes with a plain std socket // (bypassing StdUdpSocket's own recv_from, which strips the @@ -879,10 +879,10 @@ mod tests { // ── send_acf_abb ─────────────────────────────────────────────────────── #[test] - // fusa:test REQ-UDP-001 - // fusa:test REQ-UDP-002 - // fusa:test REQ-UDP-003 - // fusa:test REQ-WIRE-006 + //fusa:test REQ-UDP-001 + //fusa:test REQ-UDP-002 + //fusa:test REQ-UDP-003 + //fusa:test REQ-WIRE-006 fn send_acf_abb_round_trips_over_socket() { let addr: SocketAddr = "127.0.0.1:9000".parse().unwrap(); let socket = Arc::new(EchoUdp { mismatch: false }); @@ -893,7 +893,7 @@ mod tests { } #[test] - // fusa:test REQ-UDP-004 + //fusa:test REQ-UDP-004 fn send_acf_abb_rejects_echo_back_mismatch() { let addr: SocketAddr = "127.0.0.1:9000".parse().unwrap(); let socket = Arc::new(EchoUdp { mismatch: true }); @@ -903,7 +903,7 @@ mod tests { } #[test] - // fusa:test REQ-UDP-003 + //fusa:test REQ-UDP-003 fn send_acf_abb_rejects_zero_timeout() { let addr: SocketAddr = "127.0.0.1:9000".parse().unwrap(); let socket = Arc::new(EchoUdp { mismatch: false }); @@ -915,7 +915,7 @@ mod tests { } #[test] - // fusa:test REQ-UDP-002 + //fusa:test REQ-UDP-002 fn local_stream_getter_matches_constructor() { let addr: SocketAddr = "127.0.0.1:9000".parse().unwrap(); let socket = Arc::new(EchoUdp { mismatch: false }); @@ -925,7 +925,7 @@ mod tests { } #[test] - // fusa:test REQ-UDP-005 + //fusa:test REQ-UDP-005 fn close_is_noop() { let addr: SocketAddr = "127.0.0.1:9000".parse().unwrap(); let socket = Arc::new(EchoUdp { mismatch: false }); @@ -937,7 +937,7 @@ mod tests { // ── resolve_endpoint ─────────────────────────────────────────────────── #[test] - // fusa:test REQ-UDP-006 + //fusa:test REQ-UDP-006 fn resolve_endpoint_routes_ep0() { let endpoints = EndpointTable::new(); let resolved = resolve_endpoint(&endpoints, local_stream(), ep0::EP0_BYTE_BUS_ID).unwrap(); @@ -945,8 +945,8 @@ mod tests { } #[test] - // fusa:test REQ-UDP-006 - // fusa:test REQ-UDP-007 + //fusa:test REQ-UDP-006 + //fusa:test REQ-UDP-007 fn resolve_endpoint_routes_registered_device_endpoint() { let mut endpoints = EndpointTable::new(); let sid = local_stream(); @@ -956,7 +956,7 @@ mod tests { } #[test] - // fusa:test REQ-UDP-007 + //fusa:test REQ-UDP-007 fn resolve_endpoint_rejects_unregistered_device_endpoint() { let endpoints = EndpointTable::new(); let err = resolve_endpoint(&endpoints, local_stream(), 7).unwrap_err(); @@ -964,7 +964,7 @@ mod tests { } #[test] - // fusa:test REQ-UDP-007 + //fusa:test REQ-UDP-007 fn resolve_endpoint_does_not_leak_across_streams() { let mut endpoints = EndpointTable::new(); let sid_a = local_stream(); @@ -1061,7 +1061,7 @@ mod tests { // ── construction / accessors ──────────────────────────────────── #[test] - // fusa:test REQ-UDP-008 + //fusa:test REQ-UDP-008 fn new_server_holds_no_discovery_claim() { let socket = QueuedUdpSocket::with_inbound(Vec::new()); let rc = RcServer::new(GeneralRegisters::default()); @@ -1073,7 +1073,7 @@ mod tests { // ── register-map-driven dispatch ───────────────────────────────── #[test] - // fusa:test REQ-UDP-008 + //fusa:test REQ-UDP-008 fn serve_one_dispatches_ep0_read_through_rc_server() { let general = GeneralRegisters { svr_vendor_id: 0x1234, @@ -1102,7 +1102,7 @@ mod tests { } #[test] - // fusa:test REQ-UDP-008 + //fusa:test REQ-UDP-008 fn serve_one_dispatches_device_endpoint_write_through_rc_server() { let rc = RcServer::new(GeneralRegisters::default()); let sid = client_stream(2); @@ -1124,7 +1124,7 @@ mod tests { } #[test] - // fusa:test REQ-UDP-008 + //fusa:test REQ-UDP-008 fn serve_one_dispatches_multiple_requests_concatenated_in_one_frame() { // TC18 §12.9.1.1: an RC Server must support multiple requests // concatenated in a single frame (rust-RCP-W03). @@ -1164,7 +1164,7 @@ mod tests { } #[test] - // fusa:test REQ-UDP-008 + //fusa:test REQ-UDP-008 fn serve_one_answers_unregistered_endpoint_with_a_wire_error_response() { // rust-RCP-W04: EpNotFound has a TC18 Table 27 wire code, so it // is answered with a real err=1 response frame, not just @@ -1187,7 +1187,7 @@ mod tests { // ── discovery integration: broadcast read ──────────────────────── #[test] - // fusa:test REQ-UDP-009 + //fusa:test REQ-UDP-009 fn serve_one_answers_broadcast_discovery_request_in_any_lifecycle_state() { use crate::lifecycle::RcServerState; @@ -1233,7 +1233,7 @@ mod tests { } #[test] - // fusa:test REQ-UDP-009 + //fusa:test REQ-UDP-009 fn serve_one_answers_a_direct_non_broadcast_discovery_request_too() { let rc = RcServer::new(GeneralRegisters::default()); let request = discovery::build_discovery_request(0x22); @@ -1252,7 +1252,7 @@ mod tests { // ── discovery integration: configure / claim ───────────────────── #[test] - // fusa:test REQ-UDP-010 + //fusa:test REQ-UDP-010 fn serve_one_grants_a_discovery_configure_claim_to_the_first_requester() { let rc = RcServer::new(GeneralRegisters::default()); let mut request = discovery::build_discovery_request(0); @@ -1270,7 +1270,7 @@ mod tests { } #[test] - // fusa:test REQ-UDP-010 + //fusa:test REQ-UDP-010 fn serve_one_rejects_a_different_live_claimant() { let rc = RcServer::new(GeneralRegisters::default()); let now = Instant::now(); @@ -1311,7 +1311,7 @@ mod tests { // ── broadcast sentinel misuse ───────────────────────────────────── #[test] - // fusa:test REQ-UDP-011 + //fusa:test REQ-UDP-011 fn serve_one_rejects_a_non_discovery_request_under_the_broadcast_sentinel() { let rc = RcServer::new(GeneralRegisters::default()); let request = abb(7, false, Vec::new()); diff --git a/src/wakeup.rs b/src/wakeup.rs index 6e4fcc8..0551550 100644 --- a/src/wakeup.rs +++ b/src/wakeup.rs @@ -1,11 +1,11 @@ -// fusa:req REQ-WAKE-001 -// fusa:req REQ-WAKE-002 -// fusa:req REQ-WAKE-003 -// fusa:req REQ-WAKE-004 -// fusa:req REQ-WAKE-005 -// fusa:req REQ-WAKE-006 -// fusa:req REQ-WAKE-007 -// fusa:req REQ-WAKE-008 +//fusa:req REQ-WAKE-001 +//fusa:req REQ-WAKE-002 +//fusa:req REQ-WAKE-003 +//fusa:req REQ-WAKE-004 +//fusa:req REQ-WAKE-005 +//fusa:req REQ-WAKE-006 +//fusa:req REQ-WAKE-007 +//fusa:req REQ-WAKE-008 //! The Wakeup control endpoint type (`ep_type 0x01`) — `ROADMAP.md` //! Milestone 7 ("Remaining Endpoint Types"), fifth checklist bullet: a @@ -198,17 +198,17 @@ use crate::RcpError; /// [`SleepCmdRequest::encode`]/[`SleepCmdRequest::decode`] carry exactly /// one byte and no surrounding frame. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-WAKE-001 +//fusa:req REQ-WAKE-001 pub struct SleepCmdRequest; impl SleepCmdRequest { /// The fixed `SleepCMD` wire discriminant this checklist bullet names. - // fusa:req REQ-WAKE-001 + //fusa:req REQ-WAKE-001 pub const DISCRIMINANT: u8 = 0xA5; /// Encode this request to its one-byte wire representation: /// [`SleepCmdRequest::DISCRIMINANT`], always. - // fusa:req REQ-WAKE-001 + //fusa:req REQ-WAKE-001 pub fn encode(self) -> [u8; 1] { [Self::DISCRIMINANT] } @@ -220,7 +220,7 @@ impl SleepCmdRequest { /// is not [`SleepCmdRequest::DISCRIMINANT`] — matching /// [`crate::mdio::MdioAddressingMode::from_u8`]'s own short-vs-invalid /// split. Never panics for any input. - // fusa:req REQ-WAKE-002 + //fusa:req REQ-WAKE-002 pub fn decode(b: &[u8]) -> Result { match b.first() { None => Err(RcpError::ShortFrame), @@ -244,12 +244,12 @@ pub const WAKE_SOURCE_PIN_MASK_LEN: usize = 4; /// comment for why that mapping is [`crate::regmap::HwPinMappingEntry`]'s /// job, not this type's. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-WAKE-003 +//fusa:req REQ-WAKE-003 pub struct WakeSourcePinMask(pub u32); impl WakeSourcePinMask { /// Encode this bitmask to its 4-byte big-endian wire representation. - // fusa:req REQ-WAKE-003 + //fusa:req REQ-WAKE-003 pub fn encode(self) -> [u8; WAKE_SOURCE_PIN_MASK_LEN] { self.0.to_be_bytes() } @@ -261,7 +261,7 @@ impl WakeSourcePinMask { /// [`WAKE_SOURCE_PIN_MASK_LEN`] instead. Trailing bytes beyond the /// first four are ignored, matching [`crate::gpio::GpioBitmask::decode`]'s /// own handling of a longer-than-required slice. - // fusa:req REQ-WAKE-003 + //fusa:req REQ-WAKE-003 pub fn decode(b: &[u8]) -> Result { if b.len() < WAKE_SOURCE_PIN_MASK_LEN { return Err(RcpError::ShortFrame); @@ -280,7 +280,7 @@ impl WakeSourcePinMask { /// why this module does not transcribe GPIO's edge-triggered model onto a /// mechanism this crate has no confirmed basis for reading the same way. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-WAKE-004 +//fusa:req REQ-WAKE-004 pub struct WakeupTriggerConfig { /// Per-pin bitmask: this pin is armed to be monitored as a wake source. pub wake_enable: WakeSourcePinMask, @@ -289,7 +289,7 @@ pub struct WakeupTriggerConfig { impl WakeupTriggerConfig { /// Encode this config to its 4-byte big-endian wire representation: /// [`WakeupTriggerConfig::wake_enable`], unmodified. - // fusa:req REQ-WAKE-004 + //fusa:req REQ-WAKE-004 pub fn encode(self) -> [u8; WAKE_SOURCE_PIN_MASK_LEN] { self.wake_enable.encode() } @@ -300,7 +300,7 @@ impl WakeupTriggerConfig { /// returns `Err(RcpError::ShortFrame)` for input shorter than /// [`WAKE_SOURCE_PIN_MASK_LEN`] instead, delegating to /// [`WakeSourcePinMask::decode`]. - // fusa:req REQ-WAKE-004 + //fusa:req REQ-WAKE-004 pub fn decode(b: &[u8]) -> Result { Ok(Self { wake_enable: WakeSourcePinMask::decode(b)?, @@ -311,7 +311,7 @@ impl WakeupTriggerConfig { /// Which wake-source pins actually fired, as reported by /// [`evaluate_wake_source_signals`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-WAKE-005 +//fusa:req REQ-WAKE-005 pub struct WakeSourceSignals { /// Per-pin bitmask: this pin was armed and observed asserted. pub fired: WakeSourcePinMask, @@ -320,7 +320,7 @@ pub struct WakeSourceSignals { impl WakeSourceSignals { /// True if any pin fired at all — `fired`'s underlying bitmask is /// nonzero. Never panics for any input. - // fusa:req REQ-WAKE-005 + //fusa:req REQ-WAKE-005 pub fn any_fired(self) -> bool { self.fired.0 != 0 } @@ -333,7 +333,7 @@ impl WakeSourceSignals { /// across a previous/current pair — see this module's doc comment /// "Provenance note: wake-source pin count, width, and push-vs-poll" for /// why. Never panics for any input. -// fusa:req REQ-WAKE-005 +//fusa:req REQ-WAKE-005 pub fn evaluate_wake_source_signals( config: &WakeupTriggerConfig, observed: WakeSourcePinMask, @@ -352,7 +352,7 @@ pub fn evaluate_wake_source_signals( /// this is a dedicated type rather than content added directly to /// [`crate::regmap::PerEpTypeFunctionalConfig`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-WAKE-006 +//fusa:req REQ-WAKE-006 pub struct WakeupFunctionalConfig { /// This endpoint's per-pin wake-source trigger arming. pub trigger: WakeupTriggerConfig, @@ -366,7 +366,7 @@ impl WakeupFunctionalConfig { /// This module does not itself call that function — it only shows how a /// caller would obtain the matching tag, per this module's doc comment /// "Relationship to `crate::regmap`". - // fusa:req REQ-WAKE-006 + //fusa:req REQ-WAKE-006 pub fn layer_tag(&self) -> crate::regmap::PerEpTypeFunctionalConfig { crate::regmap::PerEpTypeFunctionalConfig::new(crate::regmap::EndpointType::Wakeup) } @@ -386,7 +386,14 @@ impl WakeupFunctionalConfig { /// no target mode of its own. Returns whatever /// [`crate::powerstate::try_enter_power_mode`] returns; never panics for /// any input. -// fusa:req REQ-WAKE-007 +/// +/// Because [`crate::powerstate::try_enter_power_mode`] admits the move only +/// when both [`crate::powerstate::PowerModeGateInput`] flags hold, a +/// `SleepCMD` reaches sleep mode only "as soon as all EPs are idle and the +/// responder queues are empty (all responses sent)" — TC18 §13.7.2.3's +/// third sleep-sequence step. +//fusa:req REQ-WAKE-007 +//fusa:req REQ-WAKE-009 pub fn request_sleep_via_sleep_cmd( _cmd: SleepCmdRequest, from: crate::powerstate::PowerMode, @@ -406,7 +413,7 @@ pub fn request_sleep_via_sleep_cmd( /// [`crate::powerstate::send_wakeup_request`], which itself only succeeds /// from [`crate::powerstate::WakeUpHandshakeState::Idle`]. Never panics for /// any input. -// fusa:req REQ-WAKE-008 +//fusa:req REQ-WAKE-008 pub fn wake_source_signals_trigger_handshake( state: crate::powerstate::WakeUpHandshakeState, signals: WakeSourceSignals, @@ -425,27 +432,27 @@ mod tests { // ── SleepCmdRequest: encode/decode round-trip, never panic ───────────── #[test] - // fusa:test REQ-WAKE-001 + //fusa:test REQ-WAKE-001 fn sleep_cmd_request_encodes_to_the_fixed_0xa5_discriminant() { assert_eq!(SleepCmdRequest.encode(), [0xA5u8]); assert_eq!(SleepCmdRequest::DISCRIMINANT, 0xA5); } #[test] - // fusa:test REQ-WAKE-001 + //fusa:test REQ-WAKE-001 fn sleep_cmd_request_round_trips_through_encode_decode() { let req = SleepCmdRequest; assert_eq!(SleepCmdRequest::decode(&req.encode()), Ok(req)); } #[test] - // fusa:test REQ-WAKE-002 + //fusa:test REQ-WAKE-002 fn sleep_cmd_request_decode_rejects_empty_input() { assert_eq!(SleepCmdRequest::decode(&[]), Err(RcpError::ShortFrame)); } #[test] - // fusa:test REQ-WAKE-002 + //fusa:test REQ-WAKE-002 fn sleep_cmd_request_decode_rejects_any_non_0xa5_first_byte() { for raw in [0x00u8, 0x01, 0x5A, 0xA4, 0xA6, 0xFF] { assert_eq!( @@ -456,7 +463,7 @@ mod tests { } #[test] - // fusa:test REQ-WAKE-002 + //fusa:test REQ-WAKE-002 fn sleep_cmd_request_decode_never_panics_for_any_sampled_input() { for buf in [ vec![], @@ -472,7 +479,7 @@ mod tests { // ── WakeSourcePinMask: round-trip / never-panic ───────────────────────── #[test] - // fusa:test REQ-WAKE-003 + //fusa:test REQ-WAKE-003 fn wake_source_pin_mask_round_trips_through_encode_decode() { for raw in [0u32, 1, 0x0000_0001, 0x8000_0000, 0xFFFF_FFFF, 0x1234_5678] { let mask = WakeSourcePinMask(raw); @@ -481,7 +488,7 @@ mod tests { } #[test] - // fusa:test REQ-WAKE-003 + //fusa:test REQ-WAKE-003 fn wake_source_pin_mask_decode_rejects_short_input() { for len in 0..WAKE_SOURCE_PIN_MASK_LEN { let buf = vec![0xAAu8; len]; @@ -490,14 +497,14 @@ mod tests { } #[test] - // fusa:test REQ-WAKE-003 + //fusa:test REQ-WAKE-003 fn wake_source_pin_mask_decode_ignores_trailing_bytes() { let buf = [0x00, 0x00, 0x00, 0x2A, 0xFF, 0xFF]; assert_eq!(WakeSourcePinMask::decode(&buf), Ok(WakeSourcePinMask(0x2A))); } #[test] - // fusa:test REQ-WAKE-003 + //fusa:test REQ-WAKE-003 fn wake_source_pin_mask_decode_never_panics_for_any_sampled_input() { for len in [0usize, 1, 2, 3, 4, 5, 9, 64] { let buf = vec![0x5Au8; len]; @@ -508,7 +515,7 @@ mod tests { // ── WakeupTriggerConfig: round-trip / never-panic ─────────────────────── #[test] - // fusa:test REQ-WAKE-004 + //fusa:test REQ-WAKE-004 fn wakeup_trigger_config_round_trips_through_encode_decode() { let cfg = WakeupTriggerConfig { wake_enable: WakeSourcePinMask(0x0000_0007), @@ -517,7 +524,7 @@ mod tests { } #[test] - // fusa:test REQ-WAKE-004 + //fusa:test REQ-WAKE-004 fn wakeup_trigger_config_decode_rejects_short_input() { for len in 0..WAKE_SOURCE_PIN_MASK_LEN { let buf = vec![0x00u8; len]; @@ -526,7 +533,7 @@ mod tests { } #[test] - // fusa:test REQ-WAKE-004 + //fusa:test REQ-WAKE-004 fn wakeup_trigger_config_default_arms_no_pins() { assert_eq!( WakeupTriggerConfig::default().wake_enable, @@ -537,7 +544,7 @@ mod tests { // ── evaluate_wake_source_signals ───────────────────────────────────── #[test] - // fusa:test REQ-WAKE-005 + //fusa:test REQ-WAKE-005 fn evaluate_wake_source_signals_reports_only_armed_and_observed_pins() { let config = WakeupTriggerConfig { wake_enable: WakeSourcePinMask(0b1010), @@ -548,7 +555,7 @@ mod tests { } #[test] - // fusa:test REQ-WAKE-005 + //fusa:test REQ-WAKE-005 fn evaluate_wake_source_signals_masks_out_disarmed_pins() { let config = WakeupTriggerConfig { wake_enable: WakeSourcePinMask(0b0000), @@ -559,7 +566,7 @@ mod tests { } #[test] - // fusa:test REQ-WAKE-005 + //fusa:test REQ-WAKE-005 fn evaluate_wake_source_signals_no_signal_when_nothing_observed() { let config = WakeupTriggerConfig { wake_enable: WakeSourcePinMask(0xFFFF_FFFF), @@ -570,7 +577,7 @@ mod tests { } #[test] - // fusa:test REQ-WAKE-005 + //fusa:test REQ-WAKE-005 fn evaluate_wake_source_signals_never_panics_for_any_sampled_input() { for armed in [0u32, 0x5555_5555, 0xFFFF_FFFF] { for observed in [0u32, 0xAAAA_AAAA, 0xFFFF_FFFF] { @@ -585,7 +592,7 @@ mod tests { // ── WakeupFunctionalConfig / layer_tag ────────────────────────────────── #[test] - // fusa:test REQ-WAKE-006 + //fusa:test REQ-WAKE-006 fn wakeup_functional_config_default_arms_no_pins_and_layer_tag_matches_ep_type_wakeup() { let functional = WakeupFunctionalConfig::default(); assert_eq!(functional.trigger, WakeupTriggerConfig::default()); @@ -603,7 +610,7 @@ mod tests { } #[test] - // fusa:test REQ-WAKE-006 + //fusa:test REQ-WAKE-006 fn wakeup_functional_config_layer_tag_rejects_mismatched_ep_type() { let functional = WakeupFunctionalConfig { trigger: WakeupTriggerConfig { @@ -620,7 +627,7 @@ mod tests { // ── request_sleep_via_sleep_cmd ────────────────────────────────────── #[test] - // fusa:test REQ-WAKE-007 + //fusa:test REQ-WAKE-007 fn request_sleep_via_sleep_cmd_succeeds_for_a_defined_gated_transition() { let gate = PowerModeGateInput { all_endpoints_idle: true, @@ -643,7 +650,7 @@ mod tests { } #[test] - // fusa:test REQ-WAKE-007 + //fusa:test REQ-WAKE-007 fn request_sleep_via_sleep_cmd_rejects_an_undefined_transition() { let gate = PowerModeGateInput { all_endpoints_idle: true, @@ -672,7 +679,7 @@ mod tests { } #[test] - // fusa:test REQ-WAKE-007 + //fusa:test REQ-WAKE-007 fn request_sleep_via_sleep_cmd_rejects_when_not_gated() { let gate = PowerModeGateInput::default(); assert_eq!( @@ -686,10 +693,44 @@ mod tests { ); } + /// TC18 §13.7.2.3 (TC18.txt lines 4151-4158): based on the sleep + /// request the RC Server brings the implementation to sleep mode only + /// "as soon as all EPs are idle and the responder queues are empty (all + /// responses sent)". Both conjuncts are required; neither alone admits + /// the move. + #[test] + //fusa:test REQ-WAKE-009 + fn sleep_cmd_reaches_sleep_only_when_all_eps_idle_and_responder_queues_empty() { + // TC18 §13.7.2.3's two conditions, enumerated exhaustively. + let cases = [ + (false, false, false), + (false, true, false), + (true, false, false), + (true, true, true), + ]; + for (all_endpoints_idle, no_pending_response, admitted) in cases { + let gate = PowerModeGateInput { + all_endpoints_idle, + no_pending_response, + }; + let result = request_sleep_via_sleep_cmd( + SleepCmdRequest, + PowerMode::Normal, + PowerMode::Sleep, + gate, + ); + if admitted { + assert_eq!(result, Ok(PowerMode::Sleep)); + } else { + assert_eq!(result, Err(RcpError::RequestRejected)); + } + } + } + // ── wake_source_signals_trigger_handshake ─────────────────────────── #[test] - // fusa:test REQ-WAKE-008 + //fusa:test REQ-WAKE-008 fn wake_source_signals_trigger_handshake_advances_idle_to_request_sent_when_fired() { let signals = WakeSourceSignals { fired: WakeSourcePinMask(1), @@ -701,7 +742,7 @@ mod tests { } #[test] - // fusa:test REQ-WAKE-008 + //fusa:test REQ-WAKE-008 fn wake_source_signals_trigger_handshake_rejects_when_nothing_fired() { let signals = WakeSourceSignals::default(); assert_eq!( @@ -711,7 +752,7 @@ mod tests { } #[test] - // fusa:test REQ-WAKE-008 + //fusa:test REQ-WAKE-008 fn wake_source_signals_trigger_handshake_rejects_when_handshake_not_idle() { let signals = WakeSourceSignals { fired: WakeSourcePinMask(1), @@ -728,7 +769,7 @@ mod tests { } #[test] - // fusa:test REQ-WAKE-008 + //fusa:test REQ-WAKE-008 fn wake_source_signals_trigger_handshake_never_panics_for_any_sampled_input() { for state in [ WakeUpHandshakeState::Idle, diff --git a/src/watchdog.rs b/src/watchdog.rs index 9860c41..4958534 100644 --- a/src/watchdog.rs +++ b/src/watchdog.rs @@ -1,11 +1,11 @@ -// fusa:req REQ-WDG-001 -// fusa:req REQ-WDG-002 -// fusa:req REQ-WDG-003 -// fusa:req REQ-WDG-004 -// fusa:req REQ-WDG-005 -// fusa:req REQ-WDG-006 -// fusa:req REQ-WDG-007 -// fusa:req REQ-WDG-008 +//fusa:req REQ-WDG-001 +//fusa:req REQ-WDG-002 +//fusa:req REQ-WDG-003 +//fusa:req REQ-WDG-004 +//fusa:req REQ-WDG-005 +//fusa:req REQ-WDG-006 +//fusa:req REQ-WDG-007 +//fusa:req REQ-WDG-008 //! Per-stream watchdog liveness model (`ROADMAP.md` Milestone 6, "Per-stream //! safety config" bullet). @@ -95,7 +95,7 @@ /// own width. See this module's doc comment "Provenance note: the /// timeout's clock-tick unit" for why the unit itself is left opaque. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-WDG-001 +//fusa:req REQ-WDG-001 pub struct StreamWatchdogTimeout(pub u16); /// One stream's watchdog liveness record: the tick at which it was last @@ -104,14 +104,14 @@ pub struct StreamWatchdogTimeout(pub u16); /// Never panics for any input. See this module's doc comment for the /// "reset on every request, no periodic poll" design this type backs. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -// fusa:req REQ-WDG-002 +//fusa:req REQ-WDG-002 pub struct StreamWatchdogState { last_reset_tick: u64, } impl StreamWatchdogState { /// Start a fresh liveness record, as if just reset at `now_tick`. - // fusa:req REQ-WDG-002 + //fusa:req REQ-WDG-002 pub fn new(now_tick: u64) -> Self { StreamWatchdogState { last_reset_tick: now_tick, @@ -122,7 +122,7 @@ impl StreamWatchdogState { /// as this stream's most recent liveness reset, replacing whatever was /// recorded before. Intended to be called once per request the stream /// receives, never on a periodic timer independent of real traffic. - // fusa:req REQ-WDG-002 + //fusa:req REQ-WDG-002 pub fn reset_on_request(self, now_tick: u64) -> Self { StreamWatchdogState { last_reset_tick: now_tick, @@ -130,7 +130,7 @@ impl StreamWatchdogState { } /// The tick this record was last reset at. - // fusa:req REQ-WDG-002 + //fusa:req REQ-WDG-002 pub fn last_reset_tick(&self) -> u64 { self.last_reset_tick } @@ -143,7 +143,7 @@ impl StreamWatchdogState { /// `state.last_reset_tick()` (a caller-supplied clock going backwards) is /// read as "no time has elapsed" rather than panicking or wrapping. Never /// panics for any input. -// fusa:req REQ-WDG-003 +//fusa:req REQ-WDG-003 pub fn is_stream_watchdog_expired( state: StreamWatchdogState, now_tick: u64, @@ -160,8 +160,8 @@ pub fn is_stream_watchdog_expired( /// See [`evaluate_stream_watchdog`] for the full `rx_wd_enable`/ /// `rx_wd_safestate_enable` gating rule this type's three variants encode. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// fusa:req REQ-WDG-004 -// fusa:req REQ-WDG-005 +//fusa:req REQ-WDG-004 +//fusa:req REQ-WDG-005 pub enum StreamWatchdogOutcome { /// The watchdog is disabled, or is enabled but has not yet expired. Alive, @@ -180,7 +180,7 @@ impl StreamWatchdogOutcome { /// [`crate::request::check_watchdog_overflow_purge`]/ /// [`crate::request::purge_normal_priority_on_watchdog_overflow`] /// already take as `watchdog_overflowed`. Never panics for any input. - // fusa:req REQ-WDG-006 + //fusa:req REQ-WDG-006 pub fn watchdog_overflowed(&self) -> bool { !matches!(self, Self::Alive) } @@ -188,7 +188,7 @@ impl StreamWatchdogOutcome { /// True only for [`Self::ExpiredSafestate`] — whether this outcome /// should drive the stream's endpoints to their configured safe state. /// Never panics for any input. - // fusa:req REQ-WDG-007 + //fusa:req REQ-WDG-007 pub fn drives_safestate(&self) -> bool { matches!(self, Self::ExpiredSafestate) } @@ -204,8 +204,8 @@ impl StreamWatchdogOutcome { /// Returns [`StreamWatchdogOutcome::ExpiredSafestate`] or /// [`StreamWatchdogOutcome::ExpiredNoSafestate`] on expiry, selected by /// `rx_wd_safestate_enable`. Never panics for any input. -// fusa:req REQ-WDG-004 -// fusa:req REQ-WDG-005 +//fusa:req REQ-WDG-004 +//fusa:req REQ-WDG-005 pub fn evaluate_stream_watchdog( state: StreamWatchdogState, now_tick: u64, @@ -233,14 +233,14 @@ mod tests { // ── StreamWatchdogState ────────────────────────────────────────────────── #[test] - // fusa:test REQ-WDG-002 + //fusa:test REQ-WDG-002 fn new_records_the_construction_tick() { let state = StreamWatchdogState::new(42); assert_eq!(state.last_reset_tick(), 42); } #[test] - // fusa:test REQ-WDG-002 + //fusa:test REQ-WDG-002 fn reset_on_request_overwrites_the_prior_tick() { let state = StreamWatchdogState::new(10).reset_on_request(99); assert_eq!(state.last_reset_tick(), 99); @@ -249,8 +249,8 @@ mod tests { // ── is_stream_watchdog_expired ─────────────────────────────────────────── #[test] - // fusa:test REQ-WDG-001 - // fusa:test REQ-WDG-003 + //fusa:test REQ-WDG-001 + //fusa:test REQ-WDG-003 fn not_expired_before_the_timeout_elapses() { let state = StreamWatchdogState::new(100); assert!(!is_stream_watchdog_expired( @@ -261,7 +261,7 @@ mod tests { } #[test] - // fusa:test REQ-WDG-003 + //fusa:test REQ-WDG-003 fn expired_exactly_at_the_timeout_boundary() { let state = StreamWatchdogState::new(100); assert!(is_stream_watchdog_expired( @@ -272,7 +272,7 @@ mod tests { } #[test] - // fusa:test REQ-WDG-003 + //fusa:test REQ-WDG-003 fn expired_well_past_the_timeout() { let state = StreamWatchdogState::new(0); assert!(is_stream_watchdog_expired( @@ -283,7 +283,7 @@ mod tests { } #[test] - // fusa:test REQ-WDG-003 + //fusa:test REQ-WDG-003 fn reset_pushes_expiry_back_out() { let state = StreamWatchdogState::new(100).reset_on_request(190); assert!(!is_stream_watchdog_expired( @@ -294,7 +294,7 @@ mod tests { } #[test] - // fusa:test REQ-WDG-003 + //fusa:test REQ-WDG-003 fn never_panics_when_now_tick_precedes_last_reset() { let state = StreamWatchdogState::new(1_000); assert!(!is_stream_watchdog_expired( @@ -307,7 +307,7 @@ mod tests { // ── evaluate_stream_watchdog / StreamWatchdogOutcome ───────────────────── #[test] - // fusa:test REQ-WDG-004 + //fusa:test REQ-WDG-004 fn disabled_watchdog_is_always_alive_even_when_long_expired() { let state = StreamWatchdogState::new(0); let outcome = @@ -318,7 +318,7 @@ mod tests { } #[test] - // fusa:test REQ-WDG-004 + //fusa:test REQ-WDG-004 fn enabled_watchdog_is_alive_before_expiry() { let state = StreamWatchdogState::new(0); let outcome = evaluate_stream_watchdog(state, 5, StreamWatchdogTimeout(100), true, true); @@ -326,7 +326,7 @@ mod tests { } #[test] - // fusa:test REQ-WDG-005 + //fusa:test REQ-WDG-005 fn expired_with_safestate_enabled_drives_safestate() { let state = StreamWatchdogState::new(0); let outcome = evaluate_stream_watchdog(state, 100, StreamWatchdogTimeout(100), true, true); @@ -336,7 +336,7 @@ mod tests { } #[test] - // fusa:test REQ-WDG-005 + //fusa:test REQ-WDG-005 fn expired_without_safestate_enabled_overflows_but_does_not_drive_safestate() { let state = StreamWatchdogState::new(0); let outcome = evaluate_stream_watchdog(state, 100, StreamWatchdogTimeout(100), true, false); @@ -346,8 +346,8 @@ mod tests { } #[test] - // fusa:test REQ-WDG-006 - // fusa:test REQ-WDG-007 + //fusa:test REQ-WDG-006 + //fusa:test REQ-WDG-007 fn watchdog_overflowed_and_drives_safestate_agree_with_variant_identity() { assert!(!StreamWatchdogOutcome::Alive.watchdog_overflowed()); assert!(!StreamWatchdogOutcome::Alive.drives_safestate()); @@ -358,7 +358,7 @@ mod tests { } #[test] - // fusa:test REQ-WDG-008 + //fusa:test REQ-WDG-008 fn evaluate_stream_watchdog_never_panics_for_any_sampled_input() { let ticks = [0u64, 1, 100, u64::MAX]; let timeouts = [0u16, 1, 100, u16::MAX];