diff --git a/.fusa-reqs-pending.json b/.fusa-reqs-pending.json index f89971a..265dd29 100644 --- a/.fusa-reqs-pending.json +++ b/.fusa-reqs-pending.json @@ -198,6 +198,114 @@ "asil": "ASIL-B", "tc18": "§12.7.7, TC18.txt L2936-L2938", "gap": "NOT a behavioral gap -- a traceability-tag gap (2026-08-22, cpp-RCP Phase 6 batch 5, watchdog module): the behavior itself is genuinely, robustly implemented and tested. rcp/mock.hpp's dispatch_e2e_core() (the reference RC Server's actual request-reception path) calls rx_watchdog_kick(stream_id, ...) unconditionally, before any admission/validation check, at both of its call sites (mock.hpp:1147 inside dispatch_e2e_core, and mock.hpp:1343 inside dispatch_frame_e2e's per-member loop) -- rx_watchdog_kick() itself (mock.hpp:2097) forwards straight to rcp::watchdog::Manager::on_request_received(). tests/test_mock.cpp has a dedicated test, \"dispatch_e2e kicks the per-stream RxWatchdog (REQ-WDG-010) on every call...\" (test_mock.cpp:1739-1742+), that exercises exactly this behavior end-to-end (kick before an overflow, elapsed-time overflow after). rcp/sim.hpp's Simulator::dispatch/register_stream (sim.hpp:86-153) independently wires the same rcp::watchdog::Manager hook for the simulation harness. The blocker is purely mechanical: cpp-FuSa's trace tool (src/trace/trace.cpp) only counts a requirement as traced/tested when a literal `// fusa:req REQ-WDG-010` / `// fusa:test REQ-WDG-010` comment line exists somewhere in the tree (regex `//\\s*fusa:req\\s+`/`//\\s*fusa:test\\s+` followed by a REQ-\\S+ token) -- the existing `REQ-WDG-010:` prose mentions in mock.hpp's plain comments and the `[REQ-WDG-010]` Catch2 tag string in test_mock.cpp do NOT match that regex, so this id currently has ZERO formal annotations anywhere in the repo. CI hard-gates on this: .github/workflows/ci.yml's cpfusa-trace job runs `cpfusa trace --req-coverage 100` (fails outright, no `|| true`) and a second step that requires testedRequirements == totalRequirements (also a hard `exit 1` gate) -- adding this id to .fusa-reqs.json today, with no real tag anywhere, would fail CI. Fixing this requires adding the two formal tags to include/rcp/mock.hpp and tests/test_mock.cpp, both out of this batch's file-scope (batch 5 is restricted to .fusa-reqs.json/.fusa-reqs-pending.json/include/rcp/watchdog.hpp/tests/test_watchdog.cpp only) -- filed here so a future mock.hpp-scoped batch can add those two tags and migrate this entry into .fusa-reqs.json in the same change." + }, + { + "id": "REQ-E2E-004", + "title": "length_with_crc() applies the length-accounting pre-adjustment, saturating on overflow", + "text": "rcp::e2e::length_with_crc(payload_len) shall return payload_len + kCrcLengthAdjustOctets, saturating at SIZE_MAX rather than wrapping when payload_len is within kCrcLengthAdjustOctets of SIZE_MAX.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "tc18": "§13.6 (\"When calculating the CRC, it needs to be considered that adding the CRC to the byte_msg_payload increases the acf_msg_length by 1.\"), OA_TC18_specification_v_0.5.1_RC_5_3624.pdf p.88", + "gap": "NOT a behavioral gap -- an id-collision traceability gap (2026-08-22, cpp-RCP Phase 6 batch 6, e2e module). The behavior itself is genuinely implemented and tested in include/rcp/e2e.hpp / tests/test_e2e.cpp. The blocker is that cpp-RCP's pre-existing REQ-E2E-001..014 catalog range predates this rewrite's later convention (established from REQ-E2E-021 onward) of reusing c-RCP's exact ids: those 14 entries were numbered independently, by the order features appeared in e2e.hpp at the time, and this id is already taken in .fusa-reqs.json by a materially different, already-catalogued requirement. Adding a second, conflicting entry under the same id would corrupt the catalog's own id uniqueness and is not attempted here. Filed so a future renumbering-cleanup batch (out of this batch's own scope, which is restricted to e2e.hpp/test_e2e.cpp/lifecycle.hpp/test_lifecycle.cpp) can resolve the REQ-E2E-001..014 collision zone and migrate this entry in. Already taken by: this catalog's own REQ-E2E-004 (\"verify_crc detects a mismatched or corrupted CRC\", corresponding to c-RCP's REQ-E2E-008). Genuinely implemented+tested: rcp::e2e::length_with_crc (e2e.hpp:356-359) is a real, directly-testable pure function -- e2e.hpp's own top-of-file note already names it \"length_with_crc() (REQ-E2E-004)\" in prose, anticipating c-RCP's numbering -- and tests/test_e2e.cpp's \"length_with_crc adds exactly kCrcLengthAdjustOctets and saturates on overflow\" TEST_CASE exercises it, including the saturation edge case." + }, + { + "id": "REQ-E2E-005", + "title": "wrap() appends a big-endian CRC32 trailer computed via compute_crc()", + "text": "rcp::e2e::wrap shall return acf_frame's freshly-encoded header + the real (unpadded) payload, followed by a 4-octet big-endian CRC32 trailer computed via compute_crc() over exactly that header-and-real-payload region.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "tc18": "§13.6 Figure 20/Figure 21 (\"CRC32\" trailer quadlet in the ACF_ABB/ACF_GBB message layout), OA_TC18_specification_v_0.5.1_RC_5_3624.pdf p.88-89", + "gap": "NOT a behavioral gap -- an id-collision traceability gap (2026-08-22, cpp-RCP Phase 6 batch 6, e2e module). The behavior itself is genuinely implemented and tested in include/rcp/e2e.hpp / tests/test_e2e.cpp. The blocker is that cpp-RCP's pre-existing REQ-E2E-001..014 catalog range predates this rewrite's later convention (established from REQ-E2E-021 onward) of reusing c-RCP's exact ids: those 14 entries were numbered independently, by the order features appeared in e2e.hpp at the time, and this id is already taken in .fusa-reqs.json by a materially different, already-catalogued requirement. Adding a second, conflicting entry under the same id would corrupt the catalog's own id uniqueness and is not attempted here. Filed so a future renumbering-cleanup batch (out of this batch's own scope, which is restricted to e2e.hpp/test_e2e.cpp/lifecycle.hpp/test_lifecycle.cpp) can resolve the REQ-E2E-001..014 collision zone and migrate this entry in. Already taken by: this catalog's own REQ-E2E-005 (\"Per-endpoint CRC safe-mode toggles gate per message role\", corresponding to c-RCP's REQ-E2E-031). Genuinely implemented+tested: rcp::e2e::wrap (e2e.hpp:567-598) and its own doc comment describe exactly this behavior; tests/test_e2e.cpp's wrap/unwrap TEST_CASE block (lines ~577-820, e.g. \"wrap/unwrap round trips an unpadded ACF_ABB payload\") exercises it end-to-end, including the trailer's exact byte position." + }, + { + "id": "REQ-E2E-006", + "title": "wrap() fails safe on a pad count exceeding the payload it was given", + "text": "rcp::e2e::wrap shall return an empty vector, leaving info unmodified, when info.pad exceeds payload.size() -- malformed input, nothing to reseat.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "gap": "NOT a behavioral gap -- an id-collision traceability gap (2026-08-22, cpp-RCP Phase 6 batch 6, e2e module). The behavior itself is genuinely implemented and tested in include/rcp/e2e.hpp / tests/test_e2e.cpp. The blocker is that cpp-RCP's pre-existing REQ-E2E-001..014 catalog range predates this rewrite's later convention (established from REQ-E2E-021 onward) of reusing c-RCP's exact ids: those 14 entries were numbered independently, by the order features appeared in e2e.hpp at the time, and this id is already taken in .fusa-reqs.json by a materially different, already-catalogued requirement. Adding a second, conflicting entry under the same id would corrupt the catalog's own id uniqueness and is not attempted here. Filed so a future renumbering-cleanup batch (out of this batch's own scope, which is restricted to e2e.hpp/test_e2e.cpp/lifecycle.hpp/test_lifecycle.cpp) can resolve the REQ-E2E-001..014 collision zone and migrate this entry in. Already taken by: this catalog's own REQ-E2E-006 (\"RxStreamGuard implements rx_enforce_e2e's drop-vs-latch rule\", corresponding to c-RCP's REQ-E2E-020). Genuinely implemented+tested: rcp::e2e::wrap's own pad_octets > payload.size() guard (e2e.hpp:572) and tests/test_e2e.cpp's \"wrap rejects a pad count that exceeds the payload it was given\" TEST_CASE exercise exactly this fail-safe path." + }, + { + "id": "REQ-E2E-007", + "title": "unwrap() rejects a frame shorter than the CRC32 trailer", + "text": "rcp::e2e::unwrap shall return UnwrapResult{short_frame, {}} whenever frame is too short to contain both a full ACF header and the CRC32 trailer plus every claimed pad octet.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "gap": "NOT a behavioral gap -- an id-collision traceability gap (2026-08-22, cpp-RCP Phase 6 batch 6, e2e module). The behavior itself is genuinely implemented and tested in include/rcp/e2e.hpp / tests/test_e2e.cpp. The blocker is that cpp-RCP's pre-existing REQ-E2E-001..014 catalog range predates this rewrite's later convention (established from REQ-E2E-021 onward) of reusing c-RCP's exact ids: those 14 entries were numbered independently, by the order features appeared in e2e.hpp at the time, and this id is already taken in .fusa-reqs.json by a materially different, already-catalogued requirement. Adding a second, conflicting entry under the same id would corrupt the catalog's own id uniqueness and is not attempted here. Filed so a future renumbering-cleanup batch (out of this batch's own scope, which is restricted to e2e.hpp/test_e2e.cpp/lifecycle.hpp/test_lifecycle.cpp) can resolve the REQ-E2E-001..014 collision zone and migrate this entry in. Already taken by: this catalog's own REQ-E2E-007 (\"RxSequenceGuard enforces rx_enforce_seq independently of the watchdog\", corresponding to c-RCP's REQ-E2E-028 -- itself now separately, correctly catalogued in this same batch). Genuinely implemented+tested: rcp::e2e::unwrap's own two length guards (e2e.hpp:645-659) and tests/test_e2e.cpp's \"unwrap fails safe on a frame too short to contain a header and CRC trailer\" / \"unwrap fails safe when the header claims more pad octets than the frame can hold\" TEST_CASEs exercise exactly this." + }, + { + "id": "REQ-E2E-008", + "title": "unwrap() reports crc_error on a mismatched trailer", + "text": "rcp::e2e::unwrap shall return UnwrapResult{crc_error, body} (body still populated, for diagnostic use) whenever the trailing 4 bytes of frame do not match compute_crc() recomputed over the reconstructed header-and-real-payload region.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "tc18": "§13.6 (\"If a request is received by an endpoint that is in 'Safe command mode' and the CRC does not match, the request is not executed, and an error response is generated with error code: CRC_ERROR.\"), OA_TC18_specification_v_0.5.1_RC_5_3624.pdf p.87", + "gap": "NOT a behavioral gap -- an id-collision traceability gap (2026-08-22, cpp-RCP Phase 6 batch 6, e2e module). The behavior itself is genuinely implemented and tested in include/rcp/e2e.hpp / tests/test_e2e.cpp. The blocker is that cpp-RCP's pre-existing REQ-E2E-001..014 catalog range predates this rewrite's later convention (established from REQ-E2E-021 onward) of reusing c-RCP's exact ids: those 14 entries were numbered independently, by the order features appeared in e2e.hpp at the time, and this id is already taken in .fusa-reqs.json by a materially different, already-catalogued requirement. Adding a second, conflicting entry under the same id would corrupt the catalog's own id uniqueness and is not attempted here. Filed so a future renumbering-cleanup batch (out of this batch's own scope, which is restricted to e2e.hpp/test_e2e.cpp/lifecycle.hpp/test_lifecycle.cpp) can resolve the REQ-E2E-001..014 collision zone and migrate this entry in. Already taken by: this catalog's own REQ-E2E-008 (\"RxWatchdog overflow detection respects rx_wd_enable and rx_wd_timeout_interval\", corresponding to c-RCP's REQ-E2E-024/025). Genuinely implemented+tested: rcp::e2e::unwrap's own CRC comparison (e2e.hpp:676-703) and tests/test_e2e.cpp's \"unwrap detects CRC corruption but still returns a body for diagnostic use\" / \"unwrap detects a wrong stream_id used to verify\" TEST_CASEs exercise exactly this." + }, + { + "id": "REQ-E2E-009", + "title": "unwrap() round-trips a frame produced by wrap()", + "text": "For a frame produced by wrap(avtp_subtype, header_octet1, tu, stream_id, avtp_timestamp, info, message_timestamp, payload), unwrap() called with the same avtp_subtype/header_octet1/tu/stream_id/avtp_timestamp shall return a default (empty) error code with acf_frame describing exactly the header-and-payload region wrap() encoded, acf_msg_length un-adapted back down by one quadlet.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "tc18": "§13.6 (\"The CRC calculation for request and response follows the identical scheme.\"), TC18.txt L3828", + "gap": "NOT a behavioral gap -- an id-collision traceability gap (2026-08-22, cpp-RCP Phase 6 batch 6, e2e module). The behavior itself is genuinely implemented and tested in include/rcp/e2e.hpp / tests/test_e2e.cpp. The blocker is that cpp-RCP's pre-existing REQ-E2E-001..014 catalog range predates this rewrite's later convention (established from REQ-E2E-021 onward) of reusing c-RCP's exact ids: those 14 entries were numbered independently, by the order features appeared in e2e.hpp at the time, and this id is already taken in .fusa-reqs.json by a materially different, already-catalogued requirement. Adding a second, conflicting entry under the same id would corrupt the catalog's own id uniqueness and is not attempted here. Filed so a future renumbering-cleanup batch (out of this batch's own scope, which is restricted to e2e.hpp/test_e2e.cpp/lifecycle.hpp/test_lifecycle.cpp) can resolve the REQ-E2E-001..014 collision zone and migrate this entry in. Already taken by: this catalog's own REQ-E2E-009 (\"Watchdog overflow purges normal requests and retains safety-tagged ones\", corresponding to c-RCP's REQ-E2E-014). Genuinely implemented+tested: tests/test_e2e.cpp's \"wrap/unwrap round trips an unpadded ACF_ABB payload\" / \"...a padded ACF_ABB payload...\" / \"...an ACF_GBB payload...\" TEST_CASEs each assert byte-identical round-tripping end-to-end." + }, + { + "id": "REQ-E2E-033", + "title": "Each ACF message in a multi-ACF AVTPDU carries and is verified against its own CRC32", + "text": "When an AVTPDU carries more than one ACF message, a separate CRC32 shall be computed, appended to, and verified for each E2E-protected ACF message individually -- never one CRC across the whole AVTPDU payload.", + "standard": "iso26262", + "level": "QM", + "asil": "QM", + "tc18": "§13.6, TC18.txt L3789-3791", + "gap": "NOT a behavioral gap -- an out-of-scope-file traceability gap (2026-08-22, cpp-RCP Phase 6 batch 6, e2e module), the same pattern as this file's own pre-existing REQ-WDG-010 entry. The behavior is genuinely implemented and tested, but in rcp/mock.hpp / tests/test_mock.cpp -- both out of this batch's file-scope (restricted to e2e.hpp/test_e2e.cpp/lifecycle.hpp/test_lifecycle.cpp). Server::dispatch_frame_e2e() (mock.hpp, per-member loop citing \"REQ-E2E-033\" in its own doc comment) routes each ACF member independently through dispatch_e2e(), so a corrupted trailer on one member neither blocks nor is masked by another member's valid one; tests/test_mock.cpp carries a Catch2 [REQ-E2E-033] tag on at least one TEST_CASE exercising this. No formal `// fusa:req`/`// fusa:test` comment tag exists anywhere for this id -- filed here so a future mock.hpp-scoped batch can add the two formal tags and migrate this entry in." + }, + { + "id": "REQ-E2E-039", + "title": "The ms bit decides whether a message carries a CRC32 trailer in its last quadlet", + "text": "A message whose ACF header ms bit is 0 shall carry the CRC32 in its last quadlet, and a message whose ms bit is 1 shall carry no CRC32 at all, the CRC appearing only on the final (ms=0) fragment of a multi-segment message.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "tc18": "§13.6, TC18.txt L3821-3823", + "gap": "NOT a behavioral gap -- an out-of-scope-file traceability gap (2026-08-22, cpp-RCP Phase 6 batch 6, e2e module), the same pattern as this file's own pre-existing REQ-WDG-010 entry. The behavior is genuinely implemented and tested, but in rcp/mock.hpp / tests/test_mock.cpp -- both out of this batch's file-scope. dispatch_e2e_fragment_core() (mock.hpp) reads the ms bit itself (via the decoded ACF header, not e2e::fragment_carries_crc()'s own abstract is_last_fragment boolean) to decide whether a fragment carries a trailer, citing \"REQ-E2E-039\" in its own doc comment; tests/test_mock.cpp carries a Catch2 [REQ-E2E-039] tag on at least one TEST_CASE exercising this. No formal `// fusa:req`/`// fusa:test` comment tag exists anywhere for this id -- filed here so a future mock.hpp-scoped batch can add the two formal tags and migrate this entry in. rcp::e2e::fragment_carries_crc() itself (e2e.hpp) is the abstract, is_last_fragment-parameterized primitive this dispatch behavior is built on, already correctly catalogued at REQ-E2E-038's own new entry (this batch)." + }, + { + "id": "REQ-E2E-041", + "title": "A CRC mismatch on a protected stream generates an error response as well as skipping execution", + "text": "When a request arrives on an E2E-protected stream and its CRC32 does not match, the RC Server shall not execute the request AND shall generate an error response carrying the CRC-mismatch wire error code (POCI_FAILURE, 12).", + "standard": "iso26262", + "level": "QM", + "asil": "QM", + "tc18": "§13.6, TC18.txt L3827-3828", + "gap": "NOT a behavioral gap -- an out-of-scope-file traceability gap (2026-08-22, cpp-RCP Phase 6 batch 6, e2e module), the same pattern as this file's own pre-existing REQ-WDG-010 entry. The behavior is genuinely implemented in rcp/mock.hpp -- both Server::dispatch_e2e_core() and dispatch_e2e_fragment_core() build a real acf::WireErrorCode::PociFailure ErrorResponse via acf::encode_error_payload() on a e2e::E2eErrc::crc_error verdict from unwrap()/compute_fragmented_crc(), never calling further dispatch, exactly matching this requirement's text (mock.hpp, dispatch_e2e_core and dispatch_e2e_fragment_core bodies) -- but neither rcp/mock.hpp nor tests/test_mock.cpp carries any tag (formal or Catch2-bracket) for this specific id at all, and both files are out of this batch's file-scope. Filed here so a future mock.hpp-scoped batch can add the formal tags (and, if none exists yet, a dedicated test asserting the wire error code specifically) and migrate this entry in." + }, + { + "id": "REQ-E2E-047", + "title": "A reassembled fragmented request too large to re-encode as a single ACF frame is rejected with a genuine error response, not silently dropped", + "text": "When a fragmented request's reassembled payload cannot be re-encoded as a single ACF_ABB/ACF_GBB frame because it exceeds that message type's own maximum payload, the RC Server shall answer with a genuine RequestRejected error response addressed to the request's own byte_bus_id/transaction_num, not silently drop the request.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "tc18": "§13.7.11.3 (\"...which requires segmentation, which is supported by the fields 'ms' and 'segment_num'.\"), TC18.txt L5859-5860", + "gap": "NOT a behavioral gap -- an out-of-scope-file traceability gap (2026-08-22, cpp-RCP Phase 6 batch 6, e2e module), the same pattern as this file's own pre-existing REQ-WDG-010 entry. The behavior is genuinely implemented in rcp/mock.hpp: dispatch_e2e_fragment_core() checks the reassembled payload's size against acf::kAcfAbbMaxPayload/kAcfGbbMaxPayload BEFORE attempting to re-encode it, and on overflow builds a real regmap::RegMapErrc::request_rejected error response via set_error_response() rather than returning silently (mock.hpp, citing \"REQ-E2E-038/c-RCP issue #614/#616\" in its own doc comment for the check) -- but neither rcp/mock.hpp nor tests/test_mock.cpp carries any tag (formal or Catch2-bracket) for this specific id, and both files are out of this batch's file-scope. Filed here so a future mock.hpp-scoped batch can add the tags (and confirm/extend the test coverage specifically for this oversized-reassembly path) and migrate this entry in." + }, + { + "id": "REQ-LIFECYCLE-023", + "title": "HW_config's own lock rule (HW_GENERIC) also governs EP_GEN_CFG and QUEUE_CFG writes, per Figure 17", + "text": "TC18 Figure 17's own HW_CONFIGURED-box transition reads \"Request ... for configuration to HW_CONFIG or QUEUE_CFG or EP_GEN_CFG -> send error response LOCKED_CONFIG_ACCESS\" -- ep_generic_cfg and response_queue_cfg (the register-map tables TC18 calls EP_GEN_CFG/QUEUE_CFG) must be governed by the SAME lock rule as HW_config itself (rcp::lifecycle::FieldKind::HwGeneric: writable only in HwUnconfigured via the discovery stream), not by the generic FieldKind::FunctionalWStar rule Table 20/31's own R/W* legend would otherwise suggest by default.", + "standard": "iso26262", + "level": "QM", + "asil": "QM", + "tc18": "§12.3, TC18.txt L2485-L2488 (Figure 17, \"RC Server lifecycle states\", TC18.txt L2517)", + "gap": "Confirmed absent (2026-08-22, cpp-RCP Phase 6 batch 6): this id was a PRE-EXISTING orphan tag in lifecycle.hpp's/test_lifecycle.cpp's own top-of-file `// fusa:req`/`// fusa:test REQ-LIFECYCLE-023` manifest lines, but on investigation the code and tests actually carrying that tag (the four field_writable() TEST_CASEs covering HwGeneric/FunctionalW/FunctionalWStar/ReadOnly) test rcp::lifecycle::field_writable()'s own GENERAL per-FieldKind mechanism -- which matches c-RCP's REQ-LIFECYCLE-018/019/020 far more precisely, and has been re-tagged accordingly in this same batch -- not this id's own, narrower, SPECIFIC claim (that TC18's real register tables EP_GEN_CFG/QUEUE_CFG are mapped to the HwGeneric FieldKind rather than FunctionalWStar). That mapping decision is not lifecycle.hpp's own job: FieldKind is a caller-supplied classification, and the actual 'which live register-map table maps to which FieldKind' decision belongs to rcp/regmap.hpp's EP0 address-routed dispatcher -- which regmap.hpp's own header comment (lines ~1793-1808) explicitly documents as 'DELIBERATELY NOT ported in this batch', naming exactly 'three separate table-specific authorization carve-outs' (i.e. this id's own EP_GEN_CFG/QUEUE_CFG carve-out among them) as deferred, cross-cutting, out-of-scope-for-Phase-4-batch-B work. Grepped regmap.hpp for any ep_generic_cfg/response_queue_cfg call into lifecycle::field_writable(): none exists. The mislabeled `REQ-LIFECYCLE-023` tags have been removed from lifecycle.hpp's/test_lifecycle.cpp's own manifests in this batch (replaced by the correct REQ-LIFECYCLE-018/019/020 ids); regmap.hpp/mock.hpp are both out of this batch's own file-scope (restricted to e2e.hpp/test_e2e.cpp/lifecycle.hpp/test_lifecycle.cpp). Filed here so a future regmap.hpp-scoped batch (the one that ports the EP0 address-routed dispatcher) can wire this specific table-to-FieldKind mapping, add real tags for it, and migrate this entry in." } ] } \ No newline at end of file diff --git a/.fusa-reqs.json b/.fusa-reqs.json index e4b0e5a..f58662a 100644 --- a/.fusa-reqs.json +++ b/.fusa-reqs.json @@ -5199,6 +5199,466 @@ "scope": "tc18", "status": "implemented", "tc18": "§12.9.1.1 (\"As the AVTPDU has either a TSCF or NTSCF header a mixture of requests with and without presentation time in a single frame is not possible. A presentation time from a TSCF header will be applied to all ACFtypes within the frame.\"), TC18.txt L3622-3624" + }, + { + "id": "REQ-E2E-021", + "title": "StreamFaultTracker is a bounded, multi-stream keyed wrapper that tracks each request stream's own independent CRC-fault latch", + "text": "rcp::e2e::StreamFaultTracker::on_crc_error(stream_id, rx_enforce_e2e) shall apply a CRC_ERROR observed on stream_id to that stream's own independently-tracked RxStreamGuard (registering the stream on first touch if capacity remains, and returning false with no side effects only when stream_id is untracked and every one of its kMaxStreams slots is already in use); is_faulted(stream_id) shall report that stream's own latched state, vacuously false for a stream never seen; reset(stream_id) shall release only that stream's own latched fault, leaving every other tracked stream's state unaffected.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§12.7.7 Table 24 (\"rx_enforce_e2e ... 1b: stream is blocked until released, when CRC check at EP fails. Safe state will be entered\"), TC18.txt L2865-2867", + "tc18_master_id": "TC18-12.7.7-001" + }, + { + "id": "REQ-E2E-028", + "title": "RxSequenceGuard::evaluate enforces per-stream sequence-number admission (rx_enforce_seq) via RFC-1982 forward-distance comparison", + "text": "rcp::e2e::RxSequenceGuard::evaluate shall accept every sequence number when RequestStreamConfig::rx_enforce_seq is clear, and shall set SeqResult::accept iff the received sequence number's modular forward distance from the last accepted value lies in [1, 127] when rx_enforce_seq is set -- an RFC-1982-style comparison that correctly handles the 8-bit AVTPDU sequence_num field's 0xFF->0x00 wraparound rather than a plain always-greater-than test.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§12.7.7 Table 24, TC18.txt L2868-L2870", + "tc18_master_id": "TC18-12.7.7-001" + }, + { + "id": "REQ-E2E-029", + "title": "RxSequenceGuard::evaluate escalates a sequence discontinuity to safe-state entry, independent of the same call's own accept/reject verdict", + "text": "rcp::e2e::RxSequenceGuard::evaluate shall set SeqResult::enter_safe_state to (discontinuity && RequestStreamConfig::rx_seq_safestate_enable), where discontinuity is true whenever the received sequence number did not advance by exactly one increment from the last accepted value -- computed regardless of whether that same call's own SeqResult::accept verdict was true or false, since a gap is evidence of a problem even when ordering itself was preserved.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§12.7.7 Table 24, TC18.txt L2871-L2873", + "tc18_master_id": "TC18-12.7.7-001" + }, + { + "id": "REQ-E2E-030", + "title": "overflow_should_enter_safe_state names rx_ovrflw_safestate_enable as its own directly-testable predicate", + "text": "rcp::e2e::overflow_should_enter_safe_state(rx_ovrflw_safestate_enable) shall return rx_ovrflw_safestate_enable unchanged -- the same verdict apply_queue_overflow() already computes inline, exposed standalone for a caller (e.g. StreamStatus::note_overflow()) that already knows a request-storage overflow occurred and wants only the safe-state verdict without driving a full RxWatchdog/RequestLedger purge.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§12.7.7 Table 24, TC18.txt L3252-L3256", + "tc18_master_id": "TC18-12.7.7-001" + }, + { + "id": "REQ-E2E-032", + "title": "Safe command mode changes nothing but the appended CRC trailer and the length accounting", + "text": "rcp::e2e::wrap shall produce a frame byte-identical, in its AVTPDU header fields, ACF message type, byte_message_info header, and byte_msg_payload, to a plain acf::encode_acf_abb()/encode_acf_gbb() encoding of the same inputs -- differing only by the appended CRC32 trailer and the +1-quadlet length-accounting adaptation that trailer requires; rcp::e2e::unwrap shall reverse exactly those two differences and no others.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§13.6, TC18.txt L3788" + }, + { + "id": "REQ-E2E-035", + "title": "An NTSCF-framed message contributes an all-zero avtp_timestamp to the CRC", + "text": "rcp::e2e::wrap_framed/unwrap_framed/compute_crc_framed/verify_crc_framed shall force the CRC's avtp_timestamp contribution to four all-zero octets whenever is_ntscf_framed is true, regardless of what avtp_timestamp value the caller passes -- the lower-level compute_crc()/wrap()/unwrap() primitives remain general-purpose (a plain std::optional) and still trust the caller to pass std::nullopt for NTSCF traffic; the framing-aware wrappers are the conformant entry point for a caller that already knows a message's framing.", + "standard": "iso26262", + "level": "QM", + "asil": "QM", + "scope": "tc18", + "status": "implemented", + "tc18": "§13.6, TC18.txt L3811", + "tc18_master_id": "TC18-13.6-005" + }, + { + "id": "REQ-E2E-036", + "title": "wrap adapts acf_msg_length by +1 quadlet before computing the CRC, and unwrap reverses it", + "text": "rcp::e2e::wrap shall increment the acf_msg_length field of the AcfMessageInfo copy it encodes by kCrcLengthAdjustQuadlets (1) via apply_acf_length_adjustment before computing the CRC, so the CRC is computed over the length-adapted frame and a conformant receiver reads a length that already includes the trailer; rcp::e2e::unwrap shall decrement it by the same amount on the AcfMessageInfo it reconstructs, so the result is byte-identical to the frame originally passed to wrap.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§13.6, TC18.txt L3813-3814", + "tc18_master_id": "TC18-13.6-006" + }, + { + "id": "REQ-E2E-037", + "title": "AVTPDU data length grows by 4 octets for every E2E-protected ACF message it carries", + "text": "An AVTPDU carrying E2E-protected ACF messages shall have its ntscf_data_length (NTSCF header) or stream_data_length (TSCF header) increased by kCrcLengthAdjustOctets (4) for every ACF message in its payload that is E2E-protected; rcp::e2e::data_length_for_protected_members(count) is this rule's own pure, directly-testable expression (count * kCrcLengthAdjustOctets, saturating on overflow), usable by a caller wanting to pre-size a buffer or independently cross-check a peer's own encoded length -- rcp::avtp::encode_ntscf/encode_tscf already satisfy the rule automatically since both recompute their own length field from the actual payload buffer they are given.", + "standard": "iso26262", + "level": "QM", + "asil": "QM", + "scope": "tc18", + "status": "implemented", + "tc18": "§13.6, TC18.txt L3814-3815", + "tc18_master_id": "TC18-13.6-006" + }, + { + "id": "REQ-E2E-038", + "title": "A fragmented message's CRC covers the first fragment's AVTPDU/ACF header plus every segment's payload", + "text": "rcp::e2e::compute_fragmented_crc shall compute the CRC32 over avtp_subtype + header_octet1 + tu + stream_id + avtp_timestamp (as always) followed by the FIRST fragment's own encoded ACF header, followed by the concatenated byte_msg_payload of every segment in order -- the one case rcp::e2e::compute_crc alone cannot express, since the resulting CRC is appended only to the last segment's message even though its coverage begins with the first fragment's own header; rcp::e2e::fragment_carries_crc(is_last_fragment) is the pure expression of which fragment carries that trailer.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§13.6, TC18.txt L3816-3819", + "tc18_master_id": "TC18-13.6-007" + }, + { + "id": "REQ-E2E-040", + "title": "Requests and responses use one identical CRC scheme", + "text": "The CRC32 coverage span, parameterization, length adaptation, and trailer placement shall be identical for requests and for responses: rcp::e2e::wrap and rcp::e2e::unwrap take no direction parameter and behave identically whichever direction a frame travels in.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§13.6, TC18.txt L3826", + "tc18_master_id": "TC18-13.6-008" + }, + { + "id": "REQ-E2E-042", + "title": "CRC coverage excludes the byte_msg_payload's pad octets; the trailer lands immediately after the real payload, with padding appended after it", + "text": "rcp::e2e::wrap shall compute the CRC32 over the byte_message_info header and the byte_msg_payload's REAL (unpadded) bytes only -- excluding AcfMessageInfo::pad's quadlet-alignment pad octets -- and shall place the resulting CRC32 trailer immediately after the last real payload byte, with pad's own pad octets (copied unchanged from the caller's payload) appended strictly after the complete trailer: [header][real payload][CRC32][pad], never [header][real payload][pad][CRC32]; rcp::e2e::unwrap shall reverse this exactly, reading the wire-format pad field to locate the real/pad boundary.", + "standard": "iso26262", + "level": "QM", + "asil": "QM", + "scope": "tc18", + "status": "implemented", + "tc18": "§13.6 Figures 20 and 21 (\"a CRC32 is calculated ... across ... the entire payload (except padding)\"), TC18.txt L3854-3863, L3872-3881" + }, + { + "id": "REQ-E2E-045", + "title": "crc_error_should_enter_safe_state names rx_enforce_e2e's own second, independent consequence", + "text": "rcp::e2e::crc_error_should_enter_safe_state(rx_enforce_e2e) shall return rx_enforce_e2e unchanged: TC18's rx_enforce_e2e 1b value triggers BOTH 'stream is blocked until released' (RxStreamGuard's/StreamFaultTracker's own latch) AND, in the same sentence, 'Safe state will be entered' -- unlike its watchdog/overflow/sequence siblings, rx_enforce_e2e has no separate dedicated safestate-enable bit of its own gating this second consequence; the one bit drives both.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§12.7.7 Table 24 (\"rx_enforce_crc ... 1b: stream is blocked until released, when CRC check at EP fails and Safe state will be entered\"), TC18.txt L3239-L3243", + "tc18_master_id": "TC18-12.7.7-001" + }, + { + "id": "REQ-E2E-046", + "title": "StreamStatus aggregates the four fault causes into TC18 0.5.1_RC5's own rx_stream_status blocked-status bit", + "text": "rcp::e2e::StreamStatus::rx_blocked shall return true iff any of its four independent latches (CRC, sequence, watchdog, request-storage-overflow) is currently set, each latched via its own note_*(enter_safe_state) call and cleared via its own independent reset_*() -- a passive, client-pollable aggregate distinct from each cause's own one-shot safe-state verdict, matching TC18 0.5.1_RC5's rx_stream_status register (0x000D.7, read-only), set automatically as a reaction to whichever of CRC error/sequence error/watchdog overflow/request-storage overflow is currently enabled and latched for that stream.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§12.7.7 Table 24, spec revision 0.5.1_RC5 PDF p.67 (\"0x000D.7 rx_stream_status ... set automatically as a reaction to either CRC error, sequence error, watchdog overflow, EP overflow, when enabled\", ticket-tagged 051RC4 \"config bit added\")" + }, + { + "id": "REQ-LIFECYCLE-013", + "title": "A same-state lifecycle transition is a no-op success", + "text": "rcp::lifecycle::ServerLifecycle::transition shall return success and leave state() unchanged, without consulting writer, snap, or all_other_eps_idle, whenever target equals the current state() -- deliberately unlike advance(), whose own documented design treats a same-state request as a client bug and rejects it with LifecycleErrc::invalid_transition; both coexist (see transition()'s own doc comment) rather than one silently overriding the other's long-standing behavior.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented" + }, + { + "id": "REQ-LIFECYCLE-014", + "title": "HW_UNCONFIGURED accepts an ACF_ABB request at the discovery byte_bus_id under NTSCF", + "text": "rcp::lifecycle::should_accept shall return Disposition::Accept for ServerState::HwUnconfigured when avtp_subtype is avtp::kSubtypeNtscf, acf_msg_type is acf::kAcfMsgTypeAbb, and byte_bus_id equals kDiscoveryByteBusId.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§12.3.1.1 (\"In this state only AVTPDUs having a NTSCF header are accepted... Within the stream only requests ACF_ABB are supported for configuration, and all requests need to be directed to EP0 via the discovery byte_bus_id 0x00.\"), TC18.txt L2539-2541", + "tc18_master_id": "TC18-12.3.1.1-003" + }, + { + "id": "REQ-LIFECYCLE-015", + "title": "HW_UNCONFIGURED silently drops any misaddressed frame, and rejects (with a response) a non-ABB request correctly addressed to EP0", + "text": "rcp::lifecycle::should_accept shall return Disposition::Drop for ServerState::HwUnconfigured whenever avtp_subtype is not avtp::kSubtypeNtscf or byte_bus_id does not equal kDiscoveryByteBusId; when both conditions are satisfied but acf_msg_type is not acf::kAcfMsgTypeAbb, it shall instead return Disposition::Reject -- a genuinely different outcome (an error response, not silence), per TC18 §12.7's more specific EP0-scoped REQUEST_REJECTED rule overriding §12.3.1.1's general silent-drop rule for exactly this case.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§12.3.1.1 (\"In this state only AVTPDUs having a NTSCF header are accepted... Within the stream only requests ACF_ABB are supported for configuration, and all requests need to be directed to EP0 via the discovery byte_bus_id 0x00.\"), TC18.txt L2539-2541 -- governs the avtp_subtype and byte_bus_id conditions only (both still produce Drop); the acf_msg_type condition no longer does, since TC18 §12.7's more specific EP0-scoped rule (\"REQUEST_REJECTED\", TC18.txt L2830) overrides §12.3.1.1's general drop rule for exactly this case (REQ-LIFECYCLE-033).", + "tc18_master_id": "TC18-12.3.1.1-003" + }, + { + "id": "REQ-LIFECYCLE-016", + "title": "HW_UNCONFIGURED drops any TSCF-headed frame regardless of time-sync support", + "text": "rcp::lifecycle::should_accept shall return Disposition::Drop for ServerState::HwUnconfigured whenever avtp_subtype is avtp::kSubtypeTscf, regardless of time_sync_supported -- presentation-time semantics presuppose a configured request stream, which cannot exist yet in this state.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§12.3.1.1 (\"In this state only AVTPDUs having a NTSCF header are accepted and all others are dropped without further response.\" -- TSCF is one of those \"others\"), TC18.txt L2539", + "tc18_master_id": "TC18-12.3.1.1-002" + }, + { + "id": "REQ-LIFECYCLE-017", + "title": "RCP_CONFIGURED applies the ordinary TSCF/time-sync drop rule; HW_CONFIGURED drops TSCF unconditionally", + "text": "rcp::lifecycle::should_accept shall, for ServerState::RcpConfigured, return Disposition::Drop iff avtp::should_drop_tscf would return true for the given time_sync_supported/avtp_subtype/unsupported_time_sync_policy, and shall otherwise accept; ServerState::HwConfigured instead drops every TSCF-headed frame unconditionally regardless of what avtp::should_drop_tscf would return (the stricter rule tracked separately at REQ-LIFECYCLE-028).", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§11.1 (\"In case time-synchronization is not supported, AVTPDUs having a TSCF header are dropped, and no response send.\"), TC18.txt L1140 -- same general rule as REQ-AVTP-014, scoped to RcpConfigured only; TC18 §12.3.1.2 separately specifies HW_CONFIGURED drops TSCF unconditionally regardless of time-sync support (REQ-LIFECYCLE-028)", + "tc18_master_id": "TC18-11.1-005" + }, + { + "id": "REQ-LIFECYCLE-018", + "title": "HW_GENERIC fields are writable only via the discovery stream while HW_UNCONFIGURED, and read-only thereafter", + "text": "rcp::lifecycle::field_writable shall return true for FieldKind::HwGeneric only when state is ServerState::HwUnconfigured and writer.via_discovery_stream is true, and shall return false for that kind unconditionally in both ServerState::HwConfigured and ServerState::RcpConfigured, for any writer.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§12.3.1.2 (\"In this state access to the HW configuration of the RC Server shall have been concluded and as such all access to the HW_config and EP_generic_config are locked.\"), TC18.txt L2553-2554; §12.3.1.1 (\"All configurations must be run via the stream which was used for discovery.\"), TC18.txt L2537, the basis for the HW_UNCONFIGURED via_discovery_stream gate (REQ-LIFECYCLE-026/035)", + "tc18_master_id": "TC18-12.3.1.2-001" + }, + { + "id": "REQ-LIFECYCLE-019", + "title": "FUNCTIONAL_W fields require an authorized writer once HW_CONFIGURED or RCP_CONFIGURED", + "text": "rcp::lifecycle::field_writable shall return false for FieldKind::FunctionalW in ServerState::HwUnconfigured for every writer; shall return true in ServerState::RcpConfigured iff writer.via_root_client_ep0 or writer.via_owning_stream is true; and shall return true in ServerState::HwConfigured iff writer.via_root_client_ep0, writer.via_owning_stream, or writer.via_discovery_stream is true -- one condition wider than the RcpConfigured case.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§12.3.1.3 (\"...only write access to the EP's functional configurations is allowed via their dedicated stream_id/byte_bus_id combinations or via root client access EP0... Write access to some parameters might be prohibited in this life-cycle state, this is marked by * in the R/W-column of the respective tables.\"), TC18.txt L2581-2582, L2601-2602; §12.3.1.2 (TC18.txt L2552), the basis for HW_CONFIGURED's wider via_discovery_stream allowance (REQ-LIFECYCLE-030/036)", + "tc18_master_id": "TC18-12.3.1.3-001" + }, + { + "id": "REQ-LIFECYCLE-020", + "title": "FUNCTIONAL_W_STAR fields require the same authorized writer as FUNCTIONAL_W while HW_CONFIGURED, and are permanently locked once RCP_CONFIGURED", + "text": "rcp::lifecycle::field_writable shall return false for FieldKind::FunctionalWStar in ServerState::RcpConfigured for every writer, including one satisfying every other authorization condition; shall return true unconditionally in ServerState::HwUnconfigured; and shall apply the identical writer.via_root_client_ep0 || writer.via_owning_stream || writer.via_discovery_stream gate REQ-LIFECYCLE-019 defines for FunctionalW in ServerState::HwConfigured.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§12.3.1.3 (\"Write access to some parameters might be prohibited in this life-cycle state, this is marked by * in the R/W-column of the respective tables (W*: write prohibited in depending on lifecycle state.)\"), TC18.txt L2601-2602 -- TC18's W* marker is general; c-RCP's (and this port's) split into FUNCTIONAL_W (conditionally writable via authorization) versus FUNCTIONAL_W_STAR (unconditionally locked once RCP_CONFIGURED) is an implementation refinement of which W* fields fall into which case, not itself named in the cited text", + "tc18_master_id": "TC18-12.3.1.3-005" + }, + { + "id": "REQ-LIFECYCLE-021", + "title": "lifecycle_category()'s message() returns a unique, non-empty message per LifecycleErrc value", + "text": "rcp::lifecycle::lifecycle_category()'s message() shall return a non-empty string for every rcp::lifecycle::LifecycleErrc value, including an unrecognized ordinal, with each defined error code mapped to a message distinct from every other's.", + "standard": "iso26262", + "level": "ASIL-A", + "asil": "ASIL-A", + "scope": "tc18", + "status": "implemented" + }, + { + "id": "REQ-LIFECYCLE-022", + "title": "A lifecycle-state change is refused with EPS_NOT_IDLE while any other endpoint is busy", + "text": "rcp::lifecycle::ServerLifecycle::transition shall return LifecycleErrc::eps_not_idle, leaving state() unchanged, whenever all_other_eps_idle is false for the RcpConfigured->HwConfigured demotion, the HwConfigured->HwUnconfigured reset, or the RcpConfigured->HwUnconfigured reset -- the three transitions TC18 Figure 17's own diagram idle-gates. PARTIAL, matching c-RCP's own tracked finding: TC18's diagram names this failure outcome 'EPs_NOT_IDLE' but that name maps to none of TC18's numbered wire error codes -- a genuine TC18 inconsistency this library cannot resolve by inventing a mapping, so LifecycleErrc::eps_not_idle remains a local-only error code, not yet wired to a wire response (mirrored by this codebase's own LifecycleErrc::eps_not_idle doc comment).", + "standard": "iso26262", + "level": "QM", + "asil": "QM", + "scope": "tc18-gap", + "status": "partial", + "tc18": "§12.3, TC18.txt L2492-L2495 and L2504-L2506 (Figure 17, \"RC Server lifecycle states\", TC18.txt L2517)", + "tc18_master_id": "TC18-12.3-002" + }, + { + "id": "REQ-LIFECYCLE-024", + "title": "A denied configuration write is answered with LOCKED_MEM_ACCESS (state-locked) or UNAUTHORIZED_ACCESS (writer-denied)", + "text": "rcp::lifecycle::field_write_error shall return acf::WireErrorCode::LockedMemAccess when state alone forbids the write (even a maximally-privileged writer would still be denied), and acf::WireErrorCode::UnauthorizedAccess when state would otherwise permit the write but writer specifically does not qualify -- computed by comparing the real writer's field_writable() verdict against a maximally-privileged writer's verdict for the same state/kind, so the two classifications can never drift out of sync with each other.", + "standard": "iso26262", + "level": "QM", + "asil": "QM", + "scope": "tc18", + "status": "implemented", + "tc18": "§13.7.1.2, TC18.txt L4039-L4041; Figure 17 (\"RC Server lifecycle states\", TC18.txt L2517), LOCKED_CONFIG_ACCESS transition unambiguously matching acf::WireErrorCode::LockedMemAccess (4), the only numbered wire code with a semantically matching name" + }, + { + "id": "REQ-LIFECYCLE-025", + "title": "Requests on an unknown stream/byte_bus_id are ignored in HW_CONFIGURED", + "text": "PARTIAL, matching c-RCP's own tracked finding: TC18 §12.3.1.3's RCP_CONFIGURED prose defines what IS allowed (an endpoint's own dedicated stream_id/byte_bus_id association, or root-client access) but specifies no disposition for a request matching neither case. rcp::lifecycle::should_accept's own conservative resolution -- restricting ServerState::HwConfigured acceptance to kDiscoveryByteBusId (EP0) only, REQ-LIFECYCLE-032 -- is stricter than TC18 literally requires but has no practical effect today, since this codebase has no wire-level functional-configuration read/write codec for any non-EP0 endpoint yet; genuinely open only for the theoretical future case once one exists, not an active gap given current scope.", + "standard": "iso26262", + "level": "QM", + "asil": "QM", + "scope": "tc18-gap", + "status": "partial", + "tc18": "§12.3, TC18.txt L2476-L2480 (Figure 17, \"RC Server lifecycle states\", TC18.txt L2517)", + "tc18_master_id": "TC18-12.3-002" + }, + { + "id": "REQ-LIFECYCLE-026", + "title": "In HW_UNCONFIGURED only the discovery claimant's stream may configure the server", + "text": "rcp::lifecycle::field_writable's FieldKind::HwGeneric rule (REQ-LIFECYCLE-018) is gated on writer.via_discovery_stream specifically -- a caller derives that member from a real frame's stream_id against the discovery-claim primitive that tracks which stream first claimed discovery, so an HW_UNCONFIGURED configuration write from any OTHER stream is correctly denied even though should_accept()'s own frame-admission filter (subtype/msg-type/byte_bus_id only) would otherwise let the frame through.", + "standard": "iso26262", + "level": "QM", + "asil": "QM", + "scope": "tc18", + "status": "implemented", + "tc18": "§12.3.1.1, TC18.txt L2537", + "tc18_master_id": "TC18-12.3.1.1-001" + }, + { + "id": "REQ-LIFECYCLE-027", + "title": "Write requests are accepted only from unicast frames", + "text": "rcp::lifecycle::field_writable shall AND !writer.via_non_unicast_frame into every FieldKind/state combination that would otherwise be writable, uniformly across HwGeneric, FunctionalW, and FunctionalWStar -- a request carried in a non-unicast (multicast/broadcast) frame is never treated as an authorized write, regardless of what writer's other members claim.", + "standard": "iso26262", + "level": "QM", + "asil": "QM", + "scope": "tc18", + "status": "implemented", + "tc18": "§12.3.1.1, TC18.txt L2543 (repeated §12.3.1.2 L2557, §12.3.1.3 L2599)", + "tc18_master_id": "TC18-12.3.1.1-005" + }, + { + "id": "REQ-LIFECYCLE-028", + "title": "HW_CONFIGURED drops TSCF-headed AVTPDUs regardless of time-sync support", + "text": "rcp::lifecycle::should_accept shall return Disposition::Drop for ServerState::HwConfigured whenever avtp_subtype is avtp::kSubtypeTscf, regardless of time_sync_supported -- the same unconditional rule REQ-LIFECYCLE-016 already applies to HwUnconfigured, stricter than the ordinary time-sync-dependent rule REQ-LIFECYCLE-017 applies once RcpConfigured.", + "standard": "iso26262", + "level": "QM", + "asil": "QM", + "scope": "tc18", + "status": "implemented", + "tc18": "§12.3.1.2, TC18.txt L2574-L2575", + "tc18_master_id": "TC18-12.3.1.2-002" + }, + { + "id": "REQ-LIFECYCLE-029", + "title": "HW_CONFIGURED drops ACF_GBB-format requests without response outside EP0, and rejects them at EP0", + "text": "rcp::lifecycle::should_accept's ServerState::HwConfigured branch shall return Disposition::Drop for any byte_bus_id other than kDiscoveryByteBusId (REQ-LIFECYCLE-032's own EP0-only restriction, unconditional of acf_msg_type), and, for a non-ACF_ABB message addressed to kDiscoveryByteBusId itself, shall return Disposition::Reject rather than Drop -- reconciling TC18 §12.3.1.2's general 'ACF_GBB requests are dropped' rule with §12.7's more specific EP0-scoped REQUEST_REJECTED override.", + "standard": "iso26262", + "level": "QM", + "asil": "QM", + "scope": "tc18", + "status": "implemented", + "tc18": "§12.3.1.2, TC18.txt L2556-L2557", + "tc18_master_id": "TC18-12.3.1.2-002" + }, + { + "id": "REQ-LIFECYCLE-030", + "title": "In HW_CONFIGURED, EP0 write access to another endpoint's configuration requires the root client or the discovery stream", + "text": "rcp::lifecycle::field_writable's FieldKind::FunctionalW rule (REQ-LIFECYCLE-019) permits ServerState::HwConfigured writability via writer.via_discovery_stream in addition to the RcpConfigured-shared writer.via_root_client_ep0 || writer.via_owning_stream authorization -- TC18 permits cross-endpoint write access via EP0 in HW_CONFIGURED only for a configured root client or via the discovery stream.", + "standard": "iso26262", + "level": "QM", + "asil": "QM", + "scope": "tc18", + "status": "implemented", + "tc18": "§12.3.1.2, TC18.txt L2560", + "tc18_master_id": "TC18-12.3.1.2-004" + }, + { + "id": "REQ-LIFECYCLE-031", + "title": "A lifecycle-state write is accepted only from the discovery stream, the root client, or (with no root client configured) any currently-valid stream_id/byte_bus_id association", + "text": "rcp::lifecycle::ServerLifecycle::transition shall compute authorized as writer.via_discovery_stream || writer.via_root_client_ep0 || writer.via_valid_stream_association, and shall require it (returning LifecycleErrc::unauthorized otherwise) for the HwConfigured->RcpConfigured advance and the HwConfigured->HwUnconfigured reset -- the two transitions TC18 §12.3.1.2's identical wording governs. writer.via_valid_stream_association bakes in the 'only when no root client is configured at all' narrowing at its own construction site, so it can never wrongly widen access when a root client IS configured.", + "standard": "iso26262", + "level": "QM", + "asil": "QM", + "scope": "tc18", + "status": "implemented", + "tc18": "§12.3.1.2, TC18.txt L2562-L2565 and L2574-L2577", + "tc18_master_id": "TC18-12.3.1.2-005" + }, + { + "id": "REQ-LIFECYCLE-032", + "title": "In HW_CONFIGURED, non-configuration requests to endpoints other than EP0 are dropped", + "text": "rcp::lifecycle::should_accept shall return Disposition::Drop for ServerState::HwConfigured whenever byte_bus_id does not equal kDiscoveryByteBusId (EP0) -- the honestly-achievable form of TC18's 'requests to EPs other than EP0 that are not config requests will be ignored and dropped' rule, given that this codebase has no wire-level functional-configuration read/write codec for any endpoint yet, so every currently-decodable non-EP0 request is, by construction, operational rather than a configuration request.", + "standard": "iso26262", + "level": "QM", + "asil": "QM", + "scope": "tc18", + "status": "implemented", + "tc18": "§12.3.1.2, TC18.txt L2561", + "tc18_master_id": "TC18-12.3.1.2-004" + }, + { + "id": "REQ-LIFECYCLE-033", + "title": "Non-STANDARD requests to EP0 before full configuration must be answered with REQUEST_REJECTED, not dropped", + "text": "rcp::lifecycle::should_accept shall return Disposition::Reject -- not Disposition::Drop -- for a non-ACF_ABB message addressed to kDiscoveryByteBusId while ServerState::HwUnconfigured or ServerState::HwConfigured; Disposition::Reject's own documented contract answers with acf::WireErrorCode::RequestRejected and processes no further, distinct from Disposition::Drop's silent discard.", + "standard": "iso26262", + "level": "QM", + "asil": "QM", + "scope": "tc18", + "status": "implemented", + "tc18": "§12.7 Configuration, TC18.txt L2827-2830", + "tc18_master_id": "TC18-12.7-002" + }, + { + "id": "REQ-LIFECYCLE-034", + "title": "A direct-to-EP configuration request without a valid stream_id/byte_bus_id association must be dropped", + "text": "PARTIAL, matching c-RCP's own tracked finding: TC18 §12.7 permits direct-to-EP configuration requests unconditionally once RcpConfigured, and only while HwConfigured with a valid stream_id/byte_bus_id association; rcp::lifecycle::should_accept's REQ-LIFECYCLE-032 fix (HwConfigured admits only kDiscoveryByteBusId) satisfies the HwConfigured half conservatively -- more strictly than TC18 literally requires, since this codebase has no wire-level functional-configuration codec for any endpoint yet, so no properly-associated non-EP0 traffic can exist in current scope regardless. Genuinely open only for the theoretical future case once that capability exists; not an active gap given current scope.", + "standard": "iso26262", + "level": "QM", + "asil": "QM", + "scope": "tc18-gap", + "status": "partial", + "tc18": "§12.7 Configuration, TC18.txt L2833-L2834", + "tc18_master_id": "TC18-12.7-003" + }, + { + "id": "REQ-LIFECYCLE-035", + "title": "In HW_UNCONFIGURED only the claimed discovery stream may configure the server", + "text": "Shares its underlying implementation with REQ-LIFECYCLE-026 (the same rcp::lifecycle::field_writable FieldKind::HwGeneric writer.via_discovery_stream gate) -- two distinct TC18 citations (§12.3.1.1's general configuration-stream rule and §12.7.2's HW_UNCONFIGURED-specific configuration section) converging on one behavior.", + "standard": "iso26262", + "level": "QM", + "asil": "QM", + "scope": "tc18", + "status": "implemented", + "tc18": "§12.7.2 Configuration in HW_UNCONFIGURED state, TC18.txt L2868-2870", + "tc18_master_id": "TC18-12.7.2-001" + }, + { + "id": "REQ-LIFECYCLE-036", + "title": "HW_CONFIGURED configuration access is restricted to the discovery stream or a known stream_id/byte_bus_id pair", + "text": "Shares its underlying implementation with REQ-LIFECYCLE-030 (the same rcp::lifecycle::field_writable authorized || writer.via_discovery_stream gate for FieldKind::FunctionalW and FieldKind::FunctionalWStar's HwConfigured branches) -- TC18 §12.7.3 permits configuration access in HW_CONFIGURED only via a discovery request on the discovery stream (writer.via_discovery_stream) or via a known stream_id/byte_bus_id pair addressed directly to the endpoint (writer.via_owning_stream, already modeled).", + "standard": "iso26262", + "level": "QM", + "asil": "QM", + "scope": "tc18", + "status": "implemented", + "tc18": "§12.7.3 Configuration in HW_CONFIGURED state, TC18.txt L2872-2874", + "tc18_master_id": "TC18-12.7.3-001" + }, + { + "id": "REQ-LIFECYCLE-037", + "title": "Once RCP_CONFIGURED, a discovery request may no longer change configuration although it is still answered", + "text": "rcp::lifecycle::field_writable's FieldKind::FunctionalW rule already excludes writer.via_discovery_stream from ServerState::RcpConfigured's authorized computation (via_root_client_ep0 || via_owning_stream only); rcp::lifecycle::ServerLifecycle::transition's RcpConfigured->HwUnconfigured reset requires writer.via_root_client_ep0 specifically -- narrower than the HwConfigured->HwUnconfigured reset's wider authorized gate (REQ-LIFECYCLE-031) -- since TC18 §12.7.4 states changes in configuration via a discovery request are no longer allowed once RcpConfigured.", + "standard": "iso26262", + "level": "QM", + "asil": "QM", + "scope": "tc18", + "status": "implemented", + "tc18": "§12.7.4 Configuration in RCP_CONFIGURED state, TC18.txt L2876-2879", + "tc18_master_id": "TC18-12.7.4-001" + }, + { + "id": "REQ-LIFECYCLE-038", + "title": "RCP_CFG_INCONSISTENT's own third plausibility bullet -- every configured request stream has at least one endpoint using it", + "text": "rcp::lifecycle::check_rcp_cfg shall return LifecycleErrc::rcp_cfg_inconsistent when any RequestStreamPlausibility with configured set has no EndpointPlausibility in the same PlausibilitySnapshot with ep_used && has_stream_assoc && request_stream_index equal to that stream's own index -- an orphaned, unused configured stream slot with no endpoint actually referencing it is inconsistent, the mirror-image of the ep_used-with-no-association check REQ-LIFECYCLE-005 already covers; only a genuinely in-use (ep_used) endpoint counts toward covering a stream.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§12.3.1.2 (\"Only if the configuration so far is plausible the state will be advanced. Plausibility check contains a check that: ... For each configured stream at least one stream_id/byte_bus_id is configured ... If not plausible the request will be rejected and an error response sent with RCP_CFG_INCONSITENT\"), TC18.txt L2566-2572", + "tc18_master_id": "TC18-12.3.1.2-006" + }, + { + "id": "REQ-LIFECYCLE-039", + "title": "RCP_CONFIGURED to HW_CONFIGURED demotion is gated by EP0 writer authorization and idleness", + "text": "rcp::lifecycle::ServerLifecycle::transition shall accept a ServerState::RcpConfigured -> ServerState::HwConfigured demotion only when writer.via_root_client_ep0 or writer.via_valid_stream_association is true (writer.via_discovery_stream deliberately does NOT authorize this demotion, per REQ-LIFECYCLE-037's same finding that the discovery stream no longer authorizes a configuration change once RcpConfigured) AND all_other_eps_idle is true, with no plausibility recheck -- matching TC18 Figure 17's explicit RCP_CONFIGURED -> HW_CONFIGURED arrow, a transition this state machine previously did not implement at all before falling through to invalid_transition.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§12.3, TC18.txt L2492-L2495 (Figure 17, \"RC Server lifecycle states\", TC18.txt L2517; page 51 of OA_TC18_specification_v_0.5.1_RC_5_3624.pdf)", + "tc18_master_id": "TC18-12.3-002" } ] } \ No newline at end of file diff --git a/include/rcp/e2e.hpp b/include/rcp/e2e.hpp index c7c404b..1057780 100644 --- a/include/rcp/e2e.hpp +++ b/include/rcp/e2e.hpp @@ -15,8 +15,14 @@ // fusa:req REQ-E2E-021 // fusa:req REQ-E2E-028 // fusa:req REQ-E2E-029 +// fusa:req REQ-E2E-030 +// fusa:req REQ-E2E-032 // fusa:req REQ-E2E-035 +// fusa:req REQ-E2E-036 +// fusa:req REQ-E2E-037 // fusa:req REQ-E2E-038 +// fusa:req REQ-E2E-040 +// fusa:req REQ-E2E-042 // fusa:req REQ-E2E-045 // fusa:req REQ-E2E-046 diff --git a/include/rcp/lifecycle.hpp b/include/rcp/lifecycle.hpp index 3d17545..5e9d141 100644 --- a/include/rcp/lifecycle.hpp +++ b/include/rcp/lifecycle.hpp @@ -5,15 +5,32 @@ // fusa:req REQ-LIFECYCLE-005 // fusa:req REQ-LIFECYCLE-006 // fusa:req REQ-LIFECYCLE-007 +// fusa:req REQ-LIFECYCLE-013 // fusa:req REQ-LIFECYCLE-014 +// fusa:req REQ-LIFECYCLE-015 +// fusa:req REQ-LIFECYCLE-016 +// fusa:req REQ-LIFECYCLE-017 +// fusa:req REQ-LIFECYCLE-018 +// fusa:req REQ-LIFECYCLE-019 +// fusa:req REQ-LIFECYCLE-020 +// fusa:req REQ-LIFECYCLE-021 // fusa:req REQ-LIFECYCLE-022 -// fusa:req REQ-LIFECYCLE-023 // fusa:req REQ-LIFECYCLE-024 +// fusa:req REQ-LIFECYCLE-025 +// fusa:req REQ-LIFECYCLE-026 // fusa:req REQ-LIFECYCLE-027 +// fusa:req REQ-LIFECYCLE-028 +// fusa:req REQ-LIFECYCLE-029 +// fusa:req REQ-LIFECYCLE-030 // fusa:req REQ-LIFECYCLE-031 +// fusa:req REQ-LIFECYCLE-032 // fusa:req REQ-LIFECYCLE-033 +// fusa:req REQ-LIFECYCLE-034 +// fusa:req REQ-LIFECYCLE-035 +// fusa:req REQ-LIFECYCLE-036 // fusa:req REQ-LIFECYCLE-037 // fusa:req REQ-LIFECYCLE-038 +// fusa:req REQ-LIFECYCLE-039 // fusa:req REQ-RMAP-049 // fusa:req REQ-RMAP-055 diff --git a/tests/test_e2e.cpp b/tests/test_e2e.cpp index 2c40b50..3810d50 100644 --- a/tests/test_e2e.cpp +++ b/tests/test_e2e.cpp @@ -15,8 +15,14 @@ // fusa:test REQ-E2E-021 // fusa:test REQ-E2E-028 // fusa:test REQ-E2E-029 +// fusa:test REQ-E2E-030 +// fusa:test REQ-E2E-032 // fusa:test REQ-E2E-035 +// fusa:test REQ-E2E-036 +// fusa:test REQ-E2E-037 // fusa:test REQ-E2E-038 +// fusa:test REQ-E2E-040 +// fusa:test REQ-E2E-042 // fusa:test REQ-E2E-045 // fusa:test REQ-E2E-046 @@ -309,7 +315,7 @@ TEST_CASE("length_with_crc adds exactly kCrcLengthAdjustOctets and saturates on TEST_CASE("data_length_for_protected_members multiplies by kCrcLengthAdjustOctets and saturates " "on overflow", - "[e2e][REQ-E2E-004]") { + "[e2e][REQ-E2E-037]") { REQUIRE(data_length_for_protected_members(0) == 0); REQUIRE(data_length_for_protected_members(3) == 12); REQUIRE(data_length_for_protected_members(static_cast(-1)) == static_cast(-1)); // saturates @@ -568,7 +574,8 @@ TEST_CASE("append_crc appends exactly 4 big-endian octets", "[e2e][REQ-E2E-004]" // ── wrap / unwrap (c-RCP issue #420) ────────────────────────────────────────── -TEST_CASE("wrap/unwrap round trips an unpadded ACF_ABB payload", "[e2e][REQ-E2E-005][REQ-E2E-006]") { +TEST_CASE("wrap/unwrap round trips an unpadded ACF_ABB payload", + "[e2e][REQ-E2E-005][REQ-E2E-006][REQ-E2E-032][REQ-E2E-036][REQ-E2E-040]") { auto sid = make_stream_id(0x02, 0x0010); AcfMessageInfo info; info.byte_bus_id = 11; @@ -588,7 +595,8 @@ TEST_CASE("wrap/unwrap round trips an unpadded ACF_ABB payload", "[e2e][REQ-E2E- REQUIRE(result.acf_frame == rcp::acf::encode_acf_abb(original, payload)); } -TEST_CASE("wrap places the CRC before trailing pad octets, not after", "[e2e][REQ-E2E-005][REQ-E2E-006]") { +TEST_CASE("wrap places the CRC before trailing pad octets, not after", + "[e2e][REQ-E2E-005][REQ-E2E-006][REQ-E2E-042]") { auto sid = make_stream_id(0x02, 0x0011); AcfMessageInfo info; info.byte_bus_id = 12; @@ -610,7 +618,7 @@ TEST_CASE("wrap places the CRC before trailing pad octets, not after", "[e2e][RE } TEST_CASE("wrap/unwrap round trips a padded ACF_ABB payload with the pad re-seated after the CRC", - "[e2e][REQ-E2E-005][REQ-E2E-006]") { + "[e2e][REQ-E2E-005][REQ-E2E-006][REQ-E2E-042]") { auto sid = make_stream_id(0x02, 0x0012); AcfMessageInfo info; info.byte_bus_id = 13; @@ -741,7 +749,7 @@ TEST_CASE("wrap_framed/unwrap_framed force the NTSCF zero-timestamp/false-tu sta // ── Fragmentation/CRC interaction ───────────────────────────────────────────── -TEST_CASE("fragment_carries_crc is true only for the last fragment", "[e2e][REQ-E2E-010]") { +TEST_CASE("fragment_carries_crc is true only for the last fragment", "[e2e][REQ-E2E-038]") { REQUIRE_FALSE(fragment_carries_crc(/*is_last_fragment=*/false)); REQUIRE(fragment_carries_crc(/*is_last_fragment=*/true)); } @@ -1195,7 +1203,7 @@ TEST_CASE("apply_queue_overflow implements the same purge-normal/retain-safety r } TEST_CASE("overflow_should_enter_safe_state mirrors rx_ovrflw_safestate_enable directly", - "[e2e][REQ-E2E-010]") { + "[e2e][REQ-E2E-030]") { REQUIRE_FALSE(overflow_should_enter_safe_state(false)); REQUIRE(overflow_should_enter_safe_state(true)); } diff --git a/tests/test_lifecycle.cpp b/tests/test_lifecycle.cpp index 2c68fa4..575cffe 100644 --- a/tests/test_lifecycle.cpp +++ b/tests/test_lifecycle.cpp @@ -5,15 +5,32 @@ // fusa:test REQ-LIFECYCLE-005 // fusa:test REQ-LIFECYCLE-006 // fusa:test REQ-LIFECYCLE-007 +// fusa:test REQ-LIFECYCLE-013 // fusa:test REQ-LIFECYCLE-014 +// fusa:test REQ-LIFECYCLE-015 +// fusa:test REQ-LIFECYCLE-016 +// fusa:test REQ-LIFECYCLE-017 +// fusa:test REQ-LIFECYCLE-018 +// fusa:test REQ-LIFECYCLE-019 +// fusa:test REQ-LIFECYCLE-020 +// fusa:test REQ-LIFECYCLE-021 // fusa:test REQ-LIFECYCLE-022 -// fusa:test REQ-LIFECYCLE-023 // fusa:test REQ-LIFECYCLE-024 +// fusa:test REQ-LIFECYCLE-025 +// fusa:test REQ-LIFECYCLE-026 // fusa:test REQ-LIFECYCLE-027 +// fusa:test REQ-LIFECYCLE-028 +// fusa:test REQ-LIFECYCLE-029 +// fusa:test REQ-LIFECYCLE-030 // fusa:test REQ-LIFECYCLE-031 +// fusa:test REQ-LIFECYCLE-032 // fusa:test REQ-LIFECYCLE-033 +// fusa:test REQ-LIFECYCLE-034 +// fusa:test REQ-LIFECYCLE-035 +// fusa:test REQ-LIFECYCLE-036 // fusa:test REQ-LIFECYCLE-037 // fusa:test REQ-LIFECYCLE-038 +// fusa:test REQ-LIFECYCLE-039 // fusa:test REQ-RMAP-049 // fusa:test REQ-RMAP-055 @@ -360,7 +377,7 @@ TEST_CASE("check_rcp_cfg: a fully-consistent snapshot passes", "[lifecycle][REQ- // advance() above — see transition()'s own doc comment for why. TEST_CASE("transition: same-state is always a no-op success, unlike advance()", - "[lifecycle][REQ-LIFECYCLE-002]") { + "[lifecycle][REQ-LIFECYCLE-002][REQ-LIFECYCLE-013]") { ServerLifecycle lc; PlausibilitySnapshot snap; WriterCtx writer; // everything false — deliberately unauthorized/non-idle @@ -424,7 +441,7 @@ TEST_CASE("transition: HwConfigured -> RcpConfigured is also guarded by check_rc TEST_CASE("transition: RcpConfigured -> HwConfigured demotion requires root client or valid " "stream association, NOT the discovery stream alone", - "[lifecycle][REQ-LIFECYCLE-002][REQ-LIFECYCLE-037]") { + "[lifecycle][REQ-LIFECYCLE-002][REQ-LIFECYCLE-022][REQ-LIFECYCLE-037][REQ-LIFECYCLE-039]") { ServerLifecycle lc; PlausibilitySnapshot snap; WriterCtx via_root; @@ -471,7 +488,7 @@ TEST_CASE("transition: HwConfigured -> HwUnconfigured reset accepts discovery-st TEST_CASE("transition: RcpConfigured -> HwUnconfigured reset requires the root client ALONE — " "discovery stream is no longer sufficient", - "[lifecycle][REQ-LIFECYCLE-002][REQ-LIFECYCLE-037]") { + "[lifecycle][REQ-LIFECYCLE-002][REQ-LIFECYCLE-022][REQ-LIFECYCLE-037]") { ServerLifecycle lc; PlausibilitySnapshot snap; WriterCtx via_root; @@ -546,7 +563,7 @@ TEST_CASE("should_accept: a TSCF frame is dropped when time sync is unsupported TEST_CASE("should_accept: HwUnconfigured drops TSCF outright and only accepts ACF_ABB on the " "discovery byte_bus_id via NTSCF", - "[lifecycle][REQ-LIFECYCLE-014][REQ-LIFECYCLE-033]") { + "[lifecycle][REQ-LIFECYCLE-014][REQ-LIFECYCLE-015][REQ-LIFECYCLE-016][REQ-LIFECYCLE-033]") { // TSCF is dropped even with time sync supported. REQUIRE(should_accept(ServerState::HwUnconfigured, true, rcp::avtp::kSubtypeTscf, rcp::acf::kAcfMsgTypeAbb, kDiscoveryByteBusId, @@ -570,7 +587,8 @@ TEST_CASE("should_accept: HwUnconfigured drops TSCF outright and only accepts AC TEST_CASE("should_accept: HwConfigured drops TSCF and non-EP0 traffic, accepts EP0 ACF_ABB, " "rejects EP0 ACF_GBB", - "[lifecycle][REQ-LIFECYCLE-014][REQ-LIFECYCLE-033]") { + "[lifecycle][REQ-LIFECYCLE-014][REQ-LIFECYCLE-028][REQ-LIFECYCLE-029][REQ-LIFECYCLE-032]" + "[REQ-LIFECYCLE-033][REQ-LIFECYCLE-034]") { REQUIRE(should_accept(ServerState::HwConfigured, true, rcp::avtp::kSubtypeTscf, rcp::acf::kAcfMsgTypeAbb, kDiscoveryByteBusId, rcp::avtp::TscfFallback::Drop) == Disposition::Drop); @@ -589,7 +607,7 @@ TEST_CASE("should_accept: HwConfigured drops TSCF and non-EP0 traffic, accepts E } TEST_CASE("should_accept: RcpConfigured accepts everything beyond the general time-sync rule", - "[lifecycle][REQ-LIFECYCLE-014]") { + "[lifecycle][REQ-LIFECYCLE-014][REQ-LIFECYCLE-017][REQ-LIFECYCLE-025]") { REQUIRE(should_accept(ServerState::RcpConfigured, true, rcp::avtp::kSubtypeNtscf, rcp::acf::kAcfMsgTypeGbb, /*byte_bus_id=*/42, rcp::avtp::TscfFallback::Drop) == Disposition::Accept); @@ -601,7 +619,7 @@ TEST_CASE("should_accept: RcpConfigured accepts everything beyond the general ti // ── field_writable / field_write_error — register-locking-by-state ────────────── TEST_CASE("field_writable: HwGeneric is writable only in HwUnconfigured via the discovery stream", - "[lifecycle][REQ-LIFECYCLE-023]") { + "[lifecycle][REQ-LIFECYCLE-018][REQ-LIFECYCLE-026][REQ-LIFECYCLE-035]") { WriterCtx via_discovery; via_discovery.via_discovery_stream = true; REQUIRE(field_writable(ServerState::HwUnconfigured, FieldKind::HwGeneric, via_discovery)); @@ -617,7 +635,7 @@ TEST_CASE("field_writable: HwGeneric is writable only in HwUnconfigured via the TEST_CASE("field_writable: FunctionalW is unwritable in HwUnconfigured, needs authorization " "(incl. discovery) while HwConfigured, and needs authorization (excl. discovery) once " "RcpConfigured", - "[lifecycle][REQ-LIFECYCLE-023]") { + "[lifecycle][REQ-LIFECYCLE-019][REQ-LIFECYCLE-030][REQ-LIFECYCLE-036][REQ-LIFECYCLE-037]") { WriterCtx via_discovery; via_discovery.via_discovery_stream = true; WriterCtx via_owning; @@ -637,7 +655,7 @@ TEST_CASE("field_writable: FunctionalW is unwritable in HwUnconfigured, needs au TEST_CASE("field_writable: FunctionalWStar is unconditionally writable in HwUnconfigured and " "permanently locked once RcpConfigured", - "[lifecycle][REQ-LIFECYCLE-023]") { + "[lifecycle][REQ-LIFECYCLE-020]") { WriterCtx none; REQUIRE(field_writable(ServerState::HwUnconfigured, FieldKind::FunctionalWStar, none)); @@ -651,7 +669,7 @@ TEST_CASE("field_writable: FunctionalWStar is unconditionally writable in HwUnco } TEST_CASE("field_writable: ReadOnly is never writable, in any state, by any writer", - "[lifecycle][REQ-LIFECYCLE-023]") { + "[lifecycle]") { WriterCtx via_owning; via_owning.via_owning_stream = true; REQUIRE_FALSE(field_writable(ServerState::HwUnconfigured, FieldKind::ReadOnly, via_owning)); @@ -660,7 +678,7 @@ TEST_CASE("field_writable: ReadOnly is never writable, in any state, by any writ } TEST_CASE("field_writable: a non-unicast frame denies an otherwise-writable field", - "[lifecycle][REQ-LIFECYCLE-023][REQ-LIFECYCLE-027]") { + "[lifecycle][REQ-LIFECYCLE-019][REQ-LIFECYCLE-027]") { WriterCtx writer; writer.via_owning_stream = true; writer.via_non_unicast_frame = true; @@ -735,3 +753,37 @@ TEST_CASE("field_write_error_w_plus distinguishes locked/state-locked from write via_owning.via_owning_stream = true; REQUIRE_FALSE(field_write_error_w_plus(ServerState::HwConfigured, via_owning, false).has_value()); } + +// ── Error category ──────────────────────────────────────────────────────────── + +TEST_CASE("LifecycleErrc is a distinct error category with non-empty, distinct messages", + "[lifecycle][REQ-LIFECYCLE-021]") { + std::error_code invalid = make_error_code(LifecycleErrc::invalid_transition); + std::error_code hw_cfg = make_error_code(LifecycleErrc::hw_cfg_inconsistent); + std::error_code rcp_cfg = make_error_code(LifecycleErrc::rcp_cfg_inconsistent); + std::error_code unauth = make_error_code(LifecycleErrc::unauthorized); + std::error_code not_idle = make_error_code(LifecycleErrc::eps_not_idle); + + REQUIRE(invalid.category() == lifecycle_category()); + REQUIRE(hw_cfg.category() == lifecycle_category()); + REQUIRE(rcp_cfg.category() == lifecycle_category()); + REQUIRE(unauth.category() == lifecycle_category()); + REQUIRE(not_idle.category() == lifecycle_category()); + + REQUIRE_FALSE(invalid.message().empty()); + REQUIRE_FALSE(hw_cfg.message().empty()); + REQUIRE_FALSE(rcp_cfg.message().empty()); + REQUIRE_FALSE(unauth.message().empty()); + REQUIRE_FALSE(not_idle.message().empty()); + + // Every message is distinct from every other. + const std::vector messages{invalid.message(), hw_cfg.message(), rcp_cfg.message(), + unauth.message(), not_idle.message()}; + for (size_t i = 0; i < messages.size(); ++i) + for (size_t j = i + 1; j < messages.size(); ++j) REQUIRE(messages[i] != messages[j]); + + // An unrecognized ordinal still returns a non-empty message (the + // category's own default: branch), not an empty/garbage string. + std::error_code unknown(99, lifecycle_category()); + REQUIRE_FALSE(unknown.message().empty()); +}