From a49896cec77566379ed8d7ca59206e58a097e54a Mon Sep 17 00:00:00 2001 From: Matt <47545907+SoundMatt@users.noreply.github.com> Date: Sat, 22 Aug 2026 03:26:41 -0700 Subject: [PATCH] =?UTF-8?q?rewrite(phase6):=20batch=201=20=E2=80=94=20ACF/?= =?UTF-8?q?AVTP/WIREERR=20catalog=20re-derivation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Part of the v3.0.0 ground-up rewrite (cpp-RCP #129, ROADMAP.md Phase 17), branch rewrite/v3-from-c-rcp. Phase 6 (requirement catalog), batch 1 of 13. ## Background cpp-RCP's .fusa-reqs.json (412 entries) is missing hundreds of requirement entries the code was already ported to implement in Phases 1-5, and cpp-RCP's code already references 1015 distinct REQ-* ids via //fusa:req/[REQ-XXX] tags against only 412 defined entries -- 603 orphaned citations left behind by porting agents as unresolved "ported from c-RCP REQ-XXX, see file.c:NNN" provenance notes. Phase 6 walks these existing citations prefix-by-prefix and turns each into a real catalog entry (or a documented gap), rather than re-deriving requirements from scratch. ## Prefix-scheme decision Kept REQ-ACF-*/REQ-AVTP-*/REQ-WIREERR-* as their own prefixes reusing c-RCP's exact id numbers 1:1, rather than folding into cpp-RCP's existing REQ-WIRE-*/REQ-ACF-*/REQ-ERR-* buckets -- the code already had orphaned citations using c-RCP's exact numbering (REQ-ACF-044, REQ-AVTP-014/021/022/023), signaling that was the intended scheme all along. Existing REQ-WIRE-*/REQ-ACF-*/REQ-ERR-* entries are untouched; duplicate-coverage mappings recorded below for anyone reconciling the two schemes later. ## New catalog entries (36) - REQ-ACF-{001,004,012,013,014,017,019,021,023,024,025-031,032,033, 038,044,047-053} (28 entries) - REQ-AVTP-{013,014,015,021,022,023,031} (7 entries) - REQ-WIREERR-001 (WireErrorCode enum matches TC18 Table 27/30 numbering) Each has a real //fusa:req tag in acf.hpp/avtp.hpp and //fusa:test + Catch2 bracket tag coverage in test_acf.cpp/test_avtp.cpp. 3 new tests added where coverage was genuinely missing: acf_category()/ avtp_category() message-uniqueness (REQ-ACF-001, REQ-AVTP-015), encoder-forces-correct-type-despite-wrong-caller-header (REQ-ACF-004/ 038), WireErrorCode numeric-value pinning (REQ-WIREERR-001). Also fixed a mislabeled inline comment in avtp.hpp that cited REQ-AVTP-022 for behavior that's actually REQ-AVTP-031's (tscf_reserved_all_zero). ## Genuine gaps found (9, filed to .fusa-reqs-pending.json, not the catalog) - REQ-ACF-006/040: encode_acf_abb/_gbb honor a caller-supplied nonzero acf_msg_length instead of always recomputing -- a documented, deliberate divergence needed by e2e.hpp's CRC-trailer accounting, flagged here for an explicit accept/reject call rather than silently carried forward. - REQ-ACF-009/041: decoders never validate declared acf_msg_length against buffer size -- matches the code's own pre-existing TODO(phase1-followup) comment. - REQ-ACF-011: encode_acf_gbb doesn't zero message_timestamp when mtv=false. - REQ-ACF-018: no read_size_or_segment_num kind-selector; existing doc comment contradicts TC18's op-based rule. - REQ-AVTP-012: no combined stream_id+byte_bus_id address-equality construct anywhere. - REQ-WIREERR-002: no WireErrorCode-to-string function. - REQ-WIREERR-005: SequencerNotKnown enumerator has zero consumers (owner-tracking not yet built). ## Judgment calls, not filed as gaps (documented here, not in the pending file) - REQ-ACF-037: cpp's op is a 2-state bool, not c-RCP's 3-state enum -- the OP_NONE fallback case is structurally inapplicable, not missing. - REQ-AVTP-016/027 (transport retain/release refcounting): cpp-RCP uses RAII via l2.hpp/udp.hpp/shmem.hpp instead of a manual-refcount transport abstraction -- an architecture divergence, and neither has a TC18 citation in c-RCP's own catalog, so they don't fit the pending file's TC18-scope contract. ## Existing-duplicate mappings (no action; c-RCP id -> cpp-RCP id already covering it) ACF: 002/022/034/035/036/037->WIRE-010, 005/039->WIRE-013, 007/008-> WIRE-014, 010->WIRE-004, 015/016/046->WIRE-006, 020->WIRE-004, 042/043/045->WIRE-005. AVTP: 001/003->WIRE-001, 002/004->WIRE-002, 005/006->WIRE-013, 007/008->WIRE-003, 010/011/025->WIRE-007. ## Out of scope (implemented, but in other files -- noted only, not this batch's concern) AVTP-009/017-020/024/028/029/030/032-034 (l2.hpp, udp.hpp, shmem.hpp, lifecycle.hpp, mock.hpp, server.hpp); WIREERR-003/004/006/007 (e2e.hpp, lifecycle.hpp, request.hpp, pwm.hpp) -- confirmed genuinely implemented there, will get their own tags when those prefixes' batches run. ## Verification .fusa-reqs.json: 412 -> 448 entries (+36, 3672 -> 4092 lines). .fusa-reqs-pending.json: 3 -> 12 entries (+9, 35 -> 123 lines). include/rcp/acf.hpp: +28 lines. include/rcp/avtp.hpp: +14 lines net. tests/test_acf.cpp: +84 net. tests/test_avtp.cpp: +22 net. Independently re-verified (not just the porting agent's own report): confirmed programmatically that zero of the 412 pre-existing catalog entries were modified or deleted (semantic id-keyed diff against the pre-batch base, not just a raw line diff -- the raw diff shows churn in unrelated CLI/RELAY/Discovery entries that's pure JSON re-serialization noise, not content change), and that zero orphaned REQ-ACF-*/REQ-AVTP-*/REQ-WIREERR-* tags remain anywhere in acf.hpp/ avtp.hpp/test_acf.cpp/test_avtp.cpp. Full tree rebuilt from scratch: 0 errors, 0 warnings, 58/58 ctest suites pass (100%). ## CI gate fix found while watching this PR's checks `cpfusa trace`'s sec-tested gate failed: it was a 17% floor (64/376 at the time it was written) against `secTestedRequirements/totalRequirements`, but its own comment already stated the real intent was "a regression floor against the exact, CYBERSECURITY.md-enumerated subset" losing test coverage -- not a claim about what fraction of the codebase is security-relevant. A percentage-of-total is the wrong shape for that intent once the total catalog is deliberately growing (exactly what Phase 6 does): this batch's 36 new, correctly non-cybersecurity REQ-ACF-*/REQ-AVTP-*/REQ-WIREERR-* entries dropped the ratio from 72/412=17.5% to 72/448=16.1%, failing a floor that was never meant to track total-catalog size. Converted ci.yml's "Enforce security-test coverage" step to an absolute-count floor (>= 72, the current CYBERSECURITY.md-enumerated total) instead -- this still catches the real regression the step exists for (an enumerated id losing its test tag, or being dropped from severity:cybersecurity) without being diluted by legitimate, intentional catalog growth elsewhere. Without this fix, every subsequent Phase 6 batch would hit the same false failure as the denominator keeps growing toward c-RCP's 1282-entry scale. Co-Authored-By: Claude Sonnet 5 Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com> --- .fusa-reqs-pending.json | 88 +++++++ .fusa-reqs.json | 486 ++++++++++++++++++++++++++++++++++++--- .github/workflows/ci.yml | 52 +++-- include/rcp/acf.hpp | 29 +++ include/rcp/avtp.hpp | 17 +- tests/test_acf.cpp | 156 ++++++++++--- tests/test_avtp.cpp | 41 +++- 7 files changed, 774 insertions(+), 95 deletions(-) diff --git a/.fusa-reqs-pending.json b/.fusa-reqs-pending.json index 3234a82..10498cd 100644 --- a/.fusa-reqs-pending.json +++ b/.fusa-reqs-pending.json @@ -30,6 +30,94 @@ "asil": "ASIL-B", "tc18": "§11.2.2.5 Figure 12 / Table 10 (\"presentation_time\"), TC18.txt L1596-1649", "gap": "Confirmed absent (2026-08-07): presentation_time appears nowhere in include/ or src/ except one comment. RequestTypeOpcode::Timed is recognized for opcode validity and priority category only (request.hpp:201,312) -- grepped for is_due/due_at/scheduled_time/delay_us/delay_ms and RequestTypeOpcode::Timed usage sitewide, found zero scheduling/dispatch logic anywhere. There is currently no mechanism in this codebase that defers a Timed request's execution until any point in time at all; the opcode is decodable but has no distinguishing runtime behavior from a standard request." + }, + { + "id": "REQ-ACF-006", + "title": "encode_acf_abb shall recompute acf_msg_length from the actual payload, ignoring any caller-supplied value", + "text": "rcp::acf::encode_acf_abb shall derive the encoded acf_msg_length field from payload.size(), ignoring any acf_msg_length value present in the caller-supplied AcfMessageInfo.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "tc18": "§11.2.1 Table 4 (\"acf_msg_length: number of quadlets of data contained in this message\"), TC18.txt L1154", + "gap": "Confirmed divergence (2026-08-22): encode_acf_abb (acf.hpp:428-447) only computes acf_msg_length via compute_acf_msg_length() when the caller leaves AcfMessageInfo::acf_msg_length at its default (0) -- see acf.hpp's own header comment, lines 44-64 (\"acf_msg_length auto-fill\"), which documents this as a DELIBERATE divergence from c-RCP: rcp/e2e.hpp's apply_acf_length_adjustment() needs to bake a trailing CRC32 trailer's +1 quadlet into the header before encoding, which requires encode_acf_abb() to honor a caller-supplied nonzero acf_msg_length rather than recomputing it unconditionally. c-RCP's own rcp_acf_encode_abb() always recomputes, ignoring the caller's header value entirely, which is what REQ-ACF-006 literally requires. This is a real, working, and load-bearing behavioral difference (not an oversight) -- filed here for FuSa reviewers to make an explicit accept/reject call on the divergence (accept the documented rationale, or require encode_acf_abb() to gain a separate strict-recompute entry point) rather than silently losing the c-RCP-conformant requirement text. Affects both ACF_ABB (this id) and ACF_GBB (REQ-ACF-040, same root cause in encode_acf_gbb, acf.hpp:486-499)." + }, + { + "id": "REQ-ACF-009", + "title": "decode_acf_abb shall reject a declared acf_msg_length that describes a payload extending past the buffer", + "text": "rcp::acf::decode_acf_abb shall return AvtpErrc::short_buffer whenever the header's declared acf_msg_length describes a payload extending past the end of the supplied buffer.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "tc18": "§11.2.1 Table 4's acf_msg_length field, TC18.txt L1154", + "gap": "Confirmed absent (2026-08-22): decode_acf_abb (acf.hpp:472-481) checks only that the buffer is at least kAcfCommonHeaderLen bytes long; it never compares the decoded acf_msg_length field against the actual buffer size, and treats everything from the header to the end of whatever buffer it was given as payload. acf.hpp's own \"TODO(phase1-followup)\" comment (lines 449-471) states this outright: c-RCP's stricter decode_abb()/decode_gbb() (which treat acf_msg_length*4 as the message's authoritative byte length and reject a short buffer) is 'not ported' because every real payload builder in this tree hands encode_acf_abb()/_gbb() an arbitrary-length payload without pre-padding to a quadlet boundary, so compute_acf_msg_length()'s ceiling-rounded value is not generally byte-exact and enforcing this strictly today would reject frames this library's own encoders legitimately produce. A genuine, currently-intentional gap versus TC18/c-RCP; the same TODO names the fix (make every payload builder pre-pad via pad_len() and set `pad` accordingly) as a later-phase prerequisite. Affects both ACF_ABB (this id) and ACF_GBB (REQ-ACF-041, same function pair, acf.hpp:501-512)." + }, + { + "id": "REQ-ACF-011", + "title": "encode_acf_gbb shall zero the message_timestamp region for an untimed (mtv=false) message", + "text": "rcp::acf::encode_acf_gbb shall write an all-zero message_timestamp region whenever the caller's AcfMessageInfo::mtv is false, regardless of the message_timestamp value passed in.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "gap": "Confirmed absent (2026-08-22): encode_acf_gbb (acf.hpp:486-499) writes whatever message_timestamp value the caller passes via avtp::detail::put_u64 unconditionally -- it never inspects info.mtv before writing the 8-byte timestamp region. A caller that passes mtv=false together with a nonzero message_timestamp gets that nonzero value serialized onto the wire verbatim, not zeroed. No test in test_acf.cpp exercises mtv=false with a nonzero message_timestamp argument. Real, currently-unguarded gap versus c-RCP's rcp_acf_encode_gbb()." + }, + { + "id": "REQ-ACF-018", + "title": "A read_size_or_segment_num classifier shall select read_size vs segment_num by the op bit, per TC18 Table 4/6", + "text": "A function equivalent to c-RCP's rcp_acf_read_size_or_segment_num_kind() shall interpret AcfMessageInfo::read_size_or_segment_num as read_size when op is false (read) and as segment_num otherwise (write), per TC18 §11.2.1 Table 4 and §11.2.2.1 Table 6.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "tc18": "§11.2.1, TC18.txt L1169 (Table 4); §11.2.2.1, TC18.txt L1280 (Table 6)", + "gap": "Confirmed absent (2026-08-22): acf.hpp has no read_size_or_segment_num_kind()-equivalent classifier at all -- the field is an opaque round-tripped uint16_t (acf.hpp:216). Its own doc comment reads \"read_size when !ms, segment_num when ms\" (gated on the `ms`/more-segments flag, acf.hpp:216 and the Message struct's own comment at acf.hpp:1073), which is a DIFFERENT selection criterion than TC18's op-based rule c-RCP implements -- for a plain (ms=0) write request, c-RCP's rule says the field is segment_num (op != READ) while cpp-RCP's own comment says it's read_size (ms == 0); these views only agree for reads. In practice this hasn't caused an observed bug because every real endpoint module (i2c.hpp:457/485, iseled.hpp:636/657, spi.hpp:756/788, uart.hpp:778/800) independently reimplements its own local read-vs-write gate instead of consulting one shared classifier, and fragmentation (which would set ms=1) is deferred to v2.8.0 per acf.hpp's own scope note -- so the field is only ever consumed as read_size today. Still a real, confirmed-missing shared primitive, and the ms-based doc comment should be reconciled with TC18's op-based rule before fragmentation support lands." + }, + { + "id": "REQ-ACF-040", + "title": "encode_acf_gbb shall recompute acf_msg_length from the actual payload, ignoring any caller-supplied value", + "text": "rcp::acf::encode_acf_gbb shall derive the encoded acf_msg_length field from payload.size(), ignoring any acf_msg_length value present in the caller-supplied AcfMessageInfo.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "tc18": "§11.2.1 Table 4 (\"acf_msg_length: number of quadlets of data contained in this message\"), TC18.txt L1154", + "gap": "GBB half of REQ-ACF-006's gap: encode_acf_gbb (acf.hpp:486-499) has the identical \"0 = auto, nonzero = caller override\" contract, documented for the same rcp/e2e.hpp CRC-trailer reason at acf.hpp:44-64. See REQ-ACF-006's gap text for the full rationale; both ids share one root cause and one fix." + }, + { + "id": "REQ-ACF-041", + "title": "decode_acf_gbb shall reject a declared acf_msg_length that describes a payload extending past the buffer", + "text": "rcp::acf::decode_acf_gbb shall return AvtpErrc::short_buffer whenever the header's declared acf_msg_length describes a payload extending past the end of the supplied buffer.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "tc18": "§11.2.1 Table 4's acf_msg_length field, TC18.txt L1154", + "gap": "GBB half of REQ-ACF-009's gap: decode_acf_gbb (acf.hpp:501-512) has the identical lenient contract (only the fixed kAcfGbbMessageInfoLen is checked, never the declared acf_msg_length against buffer size), governed by the same \"TODO(phase1-followup)\" comment at acf.hpp:449-471. See REQ-ACF-009's gap text for the full rationale; both ids share one root cause and one fix." + }, + { + "id": "REQ-AVTP-012", + "title": "AVTP address equality shall require a matching byte_bus_id, not just stream_id", + "text": "A combined stream_id + byte_bus_id address type shall compare equal only when both stream_id AND byte_bus_id match, so that a request is routed to an endpoint only when both TC18-required address components agree.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "tc18": "§13.1 (\"The byte_bus_id is only unique in the context of a STREAM_ID and requests are only passed to the endpoint if both match.\"), TC18.txt L3898-3899; confirming §12.9.1 (\"In dependence on the stream_id and byte_bus_id the RC Server determines the endpoint that is addressed. If the lookup of the byte_bus_id in the context of the stream_id does not point to an Endpoint, the request is dropped without further notification.\"), TC18.txt L3591-3593", + "gap": "Confirmed absent (2026-08-22): no AvtpAddress-equivalent struct combining StreamId and ByteBusId with its own equality operator exists anywhere in cpp-RCP (grepped every include/rcp/*.hpp for \"AvtpAddress\"/\"avtp_address\"/a combining Address struct -- zero hits; avtp::ByteBusId is a bare uint16_t alias, avtp.hpp:191, with no pairing construct). include/rcp/admin.hpp:99 has a comment explicitly noting c-RCP's rcp_avtp_addr_t as a thing not yet ported. The dispatch-time address matching this would support (an inbound frame's stream_id+byte_bus_id pair resolving to exactly one endpoint) happens ad hoc inside rcp/mock.hpp's dispatch tables (out of this batch's edit scope, and itself not confirmed to implement the combined-match rule as a single testable predicate). Real, TC18-cited (§13.1/§12.9.1) gap; natural home is rcp/avtp.hpp." + }, + { + "id": "REQ-WIREERR-002", + "title": "A wire_error_string-equivalent function shall return a unique, non-empty message per WireErrorCode", + "text": "A function equivalent to c-RCP's rcp_wire_error_string() shall return a unique, non-empty human-readable message for each rcp::acf::WireErrorCode enumerator.", + "standard": "iso26262", + "level": "QM", + "asil": "QM", + "gap": "Confirmed absent (2026-08-22): no to-string/message function exists for rcp::acf::WireErrorCode anywhere in cpp-RCP. acf_category() (acf.hpp:126-141) covers AcfErrc (decode-time errors: bad_acf_msg_type/bad_acf_msg_length) -- a completely different enum from WireErrorCode (the Table 27/30 numeric codes carried in an ErrorResponse's byte_msg_payload, acf.hpp:693-711). WireErrorCode is a plain enum class, not a std::error_code category, so it has no .message()-style accessor at all today. QM-level ergonomics gap (c-RCP's own REQ-WIREERR-002 is asil QM), genuinely missing rather than architecturally divergent; natural home is rcp/acf.hpp alongside the WireErrorCode definition." + }, + { + "id": "REQ-WIREERR-005", + "title": "A sequencer wire-error classifier shall distinguish SEQUENCER_NOT_KNOWN (unknown index) from UNAUTHORIZED_ACCESS (wrong owner)", + "text": "A function equivalent to c-RCP's rcp_sequencer_wire_error() shall map a sequencer-access rejection to WireErrorCode::SequencerNotKnown when the referenced sequencer index does not exist at all, and to WireErrorCode::UnauthorizedAccess when it exists but the requester is not its owner.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "tc18": "§12.7.10 Table 28 (own-access-control rule, \"Request_stream_index... allowed to access this sequencer\"), TC18.txt L3460-3493; §12.9.6 Table 30 (SEQUENCER_NOT_KNOWN=2, UNAUTHORIZED_ACCESS=3), TC18.txt L3812-3844", + "gap": "Confirmed absent (2026-08-22): rcp::acf::WireErrorCode::SequencerNotKnown (acf.hpp:695) is defined but grepped as having ZERO consumers anywhere in include/ or tests/ -- no caller ever produces it. This is consistent with rcp/request.hpp's own explicit \"TODO(phase1-followup)\" (lines 219-225) acknowledging that c-RCP's REQ-SEQ-013/014-style per-sequencer-owner/access-control gate is not yet ported: SequencerTable has no owner-per-sequencer concept to distinguish \"unknown index\" from \"real sequencer, wrong owner\" in the first place, so there is nothing yet to map to these two distinct wire codes. Real, TC18-cited (§12.7.10 Table 28) gap; the WireErrorCode enumerator itself lives in rcp/acf.hpp (in this batch's scope) but the classifier logic's natural home is rcp/request.hpp (out of this batch's scope, and already tracked there via that file's own TODO)." } ] } diff --git a/.fusa-reqs.json b/.fusa-reqs.json index 350d3db..5799011 100644 --- a/.fusa-reqs.json +++ b/.fusa-reqs.json @@ -2044,7 +2044,7 @@ }, { "id": "REQ-CLI-001", - "title": "CLI version document conforms to RELAY \u00a712.1", + "title": "CLI version document conforms to RELAY §12.1", "text": "The `version` subcommand with `--format json` shall emit a JSON object containing tool, version, spec_version, language, and runtime fields; language shall be \"cpp\", spec_version shall equal relay::kRelaySpecVersion, and protocol_int shall be 5 (RCP).", "standard": "relay", "level": "QM", @@ -2052,7 +2052,7 @@ }, { "id": "REQ-CLI-002", - "title": "CLI capabilities document conforms to RELAY \u00a712.2", + "title": "CLI capabilities document conforms to RELAY §12.2", "text": "The `capabilities` subcommand shall emit a JSON object with kind=\"capabilities\" and the fields tool, version, spec_version, commands, transports, features, interfaces, optional_interfaces, and adapt; commands shall include version, capabilities, and status.", "standard": "relay", "level": "QM", @@ -2060,7 +2060,7 @@ }, { "id": "REQ-CLI-003", - "title": "CLI status document conforms to RELAY \u00a712.3", + "title": "CLI status document conforms to RELAY §12.3", "text": "The `status` subcommand with `--format json` shall emit a JSON object containing tool, version, healthy, connected, endpoint, and details fields.", "standard": "relay", "level": "QM", @@ -2068,7 +2068,7 @@ }, { "id": "REQ-CLI-004", - "title": "CLI exit codes conform to RELAY \u00a711.3", + "title": "CLI exit codes conform to RELAY §11.3", "text": "The CLI shall return exit code 2 (invalid-args) when invoked with no command, an unknown command, or an invalid --format value.", "standard": "relay", "level": "QM", @@ -2076,7 +2076,7 @@ }, { "id": "REQ-RELAY-001", - "title": "RELAY spec version constant is current (\u00a719.4)", + "title": "RELAY spec version constant is current (§19.4)", "text": "relay::kRelaySpecVersion shall report the RELAY spec version the binding targets, kept in sync with spec/version.json (currently \"2.0\").", "standard": "relay", "level": "QM", @@ -2084,23 +2084,23 @@ }, { "id": "REQ-RELAY-002", - "title": "RELAY Protocol enum and mandatory error sentinels (\u00a73, \u00a75.1)", - "text": "The relay:: namespace shall define the Protocol enum (CAN=1..SOMEIP=6) and the four mandatory error_condition sentinels (ErrClosed, ErrNotConnected, ErrTimeout, ErrPayloadTooLarge) in a category named \"relay\" with the spec \u00a75.1 messages.", + "title": "RELAY Protocol enum and mandatory error sentinels (§3, §5.1)", + "text": "The relay:: namespace shall define the Protocol enum (CAN=1..SOMEIP=6) and the four mandatory error_condition sentinels (ErrClosed, ErrNotConnected, ErrTimeout, ErrPayloadTooLarge) in a category named \"relay\" with the spec §5.1 messages.", "standard": "relay", "level": "QM", "asil": "QM" }, { "id": "REQ-RELAY-003", - "title": "RCP-to-RELAY error equivalence mapping (\u00a75.2)", - "text": "rcp:: sentinel error codes shall be std::error_condition-equivalent to their RELAY counterparts: ErrClosed\u2192ErrClosed, ErrTimeout/ErrBusy\u2192ErrTimeout, ErrNotFound\u2192ErrNotConnected; ErrAlreadyExists shall remain standalone.", + "title": "RCP-to-RELAY error equivalence mapping (§5.2)", + "text": "rcp:: sentinel error codes shall be std::error_condition-equivalent to their RELAY counterparts: ErrClosed→ErrClosed, ErrTimeout/ErrBusy→ErrTimeout, ErrNotFound→ErrNotConnected; ErrAlreadyExists shall remain standalone.", "standard": "relay", "level": "QM", "asil": "QM" }, { "id": "REQ-RELAY-004", - "title": "Adapt() wraps a Controller as a relay::Caller (\u00a710.3)", + "title": "Adapt() wraps a Controller as a relay::Caller (§10.3)", "text": "rcp::Adapt(RequestFn) shall return a non-null relay::Caller whose protocol() is RCP and whose call()/send() delegate to the wrapped RequestFn, resolving the target endpoint (byte_bus_id) from relay::Message.id; subscribe() shall report std::errc::function_not_supported, and close() shall succeed unconditionally.", "standard": "relay", "level": "QM", @@ -2108,15 +2108,15 @@ }, { "id": "REQ-RELAY-005", - "title": "RELAY core bindings and RCP message mapping (\u00a718.2, \u00a714, \u00a715.7.5)", - "text": "rcp::Context shall alias relay::Context; SubscriberOptions shall default channel_depth=64 and expose topic_name; endpoint_id_to_relay_id()/relay_id_to_endpoint_id() shall round-trip a byte_bus_id through relay::Message.id as its plain decimal string form (RELAY spec v2.0 \u00a715.7.5), and message_to_request()/response_to_message() shall correctly decode/encode the op, evt_op, read_size_or_segment_num, and response_kind fields.", + "title": "RELAY core bindings and RCP message mapping (§18.2, §14, §15.7.5)", + "text": "rcp::Context shall alias relay::Context; SubscriberOptions shall default channel_depth=64 and expose topic_name; endpoint_id_to_relay_id()/relay_id_to_endpoint_id() shall round-trip a byte_bus_id through relay::Message.id as its plain decimal string form (RELAY spec v2.0 §15.7.5), and message_to_request()/response_to_message() shall correctly decode/encode the op, evt_op, read_size_or_segment_num, and response_kind fields.", "standard": "relay", "level": "QM", "asil": "QM" }, { "id": "REQ-CLI-005", - "title": "CLI send --format json streaming NDJSON sink (RELAY \u00a711.2)", + "title": "CLI send --format json streaming NDJSON sink (RELAY §11.2)", "text": "The `send --format json` subcommand shall read relay.Message values as NDJSON on stdin (one per line) and publish each via message_to_request to the matching rcp::mock::Server endpoint until EOF; malformed or undeliverable lines shall be reported and skipped without aborting the stream. `send` without `--format json` shall return exit code 2.", "standard": "relay", "level": "QM", @@ -2252,8 +2252,8 @@ }, { "id": "REQ-WIRE-015", - "title": "RELAY spec \u00a715.5 canonical ControlFlags/Message conversion", - "text": "rcp::acf::ControlFlags shall define FlagAck/FlagRead/FlagWrite/FlagResponse/FlagError/FlagMoreSegments at bit values 0x80/0x40/0x20/0x10/0x08/0x04 respectively, matching RELAY spec \u00a715.5's Go definition exactly; rcp::acf::to_message/from_message shall convert between AcfMessageInfo and the RELAY spec \u00a715.5 canonical Message shape, preserving byte_bus_id, transaction_num, read_size_or_segment, and body, and carrying message_timestamp only for the ACF_GBB encoding.", + "title": "RELAY spec §15.5 canonical ControlFlags/Message conversion", + "text": "rcp::acf::ControlFlags shall define FlagAck/FlagRead/FlagWrite/FlagResponse/FlagError/FlagMoreSegments at bit values 0x80/0x40/0x20/0x10/0x08/0x04 respectively, matching RELAY spec §15.5's Go definition exactly; rcp::acf::to_message/from_message shall convert between AcfMessageInfo and the RELAY spec §15.5 canonical Message shape, preserving byte_bus_id, transaction_num, read_size_or_segment, and body, and carrying message_timestamp only for the ACF_GBB encoding.", "standard": "relay", "level": "QM", "asil": "QM" @@ -2489,12 +2489,12 @@ }, { "id": "REQ-DISC-001", - "title": "Discovery request framing \u2014 byte_bus_id 0, register-map address 0", + "title": "Discovery request framing — byte_bus_id 0, register-map address 0", "text": "rcp::discovery::make_discovery_request shall build an unconditional ACF_ABB read (op=false) targeting byte_bus_id 0 (kDiscoveryByteBusId), and kDiscoveryRegisterAddress shall be 0, fixing discovery reads at the general bootstrap/magic-number register-map region.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B", - "tc18": "\u00a712.6.1 Table 18 (\"Byte_bus_id 00000000000b\"; \"op 1b (read request)\"), TC18.txt L2746-2758", + "tc18": "§12.6.1 Table 18 (\"Byte_bus_id 00000000000b\"; \"op 1b (read request)\"), TC18.txt L2746-2758", "tc18_master_id": "TC18-12.6.1-001", "severity": "cybersecurity" }, @@ -2505,7 +2505,7 @@ "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B", - "tc18": "\u00a712.6.1 (\"AVTPDUs having a TSCF header are dropped without further response, as well as requests in ACF_GBB format.\"), TC18.txt L2743", + "tc18": "§12.6.1 (\"AVTPDUs having a TSCF header are dropped without further response, as well as requests in ACF_GBB format.\"), TC18.txt L2743", "tc18_master_id": "TC18-12.6.1-002", "severity": "cybersecurity" }, @@ -2516,7 +2516,7 @@ "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B", - "tc18": "\u00a712.6 (\"The first discovery request the RC server receives in HW_UNCONFIGURED or HW_CONFIGURED state is used to set the discovery stream from which now on configuration requests will be accepted.\"), TC18.txt L2732-2733", + "tc18": "§12.6 (\"The first discovery request the RC server receives in HW_UNCONFIGURED or HW_CONFIGURED state is used to set the discovery stream from which now on configuration requests will be accepted.\"), TC18.txt L2732-2733", "tc18_master_id": "TC18-12.6-003", "severity": "cybersecurity" }, @@ -2527,7 +2527,7 @@ "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B", - "tc18": "\u00a712.6 (\"If the RC Server does not receive configuration requests on the stream for a certain time (Discovery_TimeOut) it will drop the discovery stream and is open for a new discovery.\"), TC18.txt L2733-2735", + "tc18": "§12.6 (\"If the RC Server does not receive configuration requests on the stream for a certain time (Discovery_TimeOut) it will drop the discovery stream and is open for a new discovery.\"), TC18.txt L2733-2735", "tc18_master_id": "TC18-12.6-003", "severity": "cybersecurity" }, @@ -2538,7 +2538,7 @@ "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B", - "tc18": "\u00a712.6 (\"If multiple discovery requests are received from different RC Clients all requests are responded with the server_config map, but only the first one will be allowed as configuration stream.\"), TC18.txt L2735-2737", + "tc18": "§12.6 (\"If multiple discovery requests are received from different RC Clients all requests are responded with the server_config map, but only the first one will be allowed as configuration stream.\"), TC18.txt L2735-2737", "tc18_master_id": "TC18-12.6-003", "severity": "cybersecurity" }, @@ -2549,7 +2549,7 @@ "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B", - "tc18": "\u00a712.6 (\"If others send configuration requests they will be rejected and get an error response.\"), TC18.txt L2736-2737", + "tc18": "§12.6 (\"If others send configuration requests they will be rejected and get an error response.\"), TC18.txt L2736-2737", "tc18_master_id": "TC18-12.6-003", "severity": "cybersecurity" }, @@ -2560,7 +2560,7 @@ "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B", - "tc18": "\u00a712.7.4 (\"Changes in configuration via a discovery request are no longer allowed.\"), TC18.txt L2879-2880", + "tc18": "§12.7.4 (\"Changes in configuration via a discovery request are no longer allowed.\"), TC18.txt L2879-2880", "tc18_master_id": "TC18-12.7.4-001", "severity": "cybersecurity" }, @@ -2789,7 +2789,7 @@ }, { "id": "REQ-I2C-001", - "title": "i2c_mode open item \u2014 coarse high-speed decode only", + "title": "i2c_mode open item — coarse high-speed decode only", "text": "rcp::i2c::i2c_mode_of shall map AcfMessageInfo::hs=false to I2cMode::Standard and hs=true to I2cMode::HighSpeed, and shall not attempt to distinguish any finer I2C speed grade, per this milestone's documented open item pending a future spec errata pass.", "standard": "iso26262", "level": "ASIL-B", @@ -2802,17 +2802,17 @@ "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B", - "tc18": "\u00a713.7.7.1 (\"Optionally the Client can request the I\u00b2C endpoint to wait for a pre-defined bit sequence has been received from a connected device.\"), TC18.txt L5203-5204", + "tc18": "§13.7.7.1 (\"Optionally the Client can request the I²C endpoint to wait for a pre-defined bit sequence has been received from a connected device.\"), TC18.txt L5203-5204", "tc18_master_id": "TC18-13.7.7.1-003" }, { "id": "REQ-I2C-003", "title": "I2C controller-only raw byte-stream transfer", - "text": "rcp::i2c::I2cEndpoint::transfer shall record the exact byte sequences passed as `out_bytes` and `in_bytes` \u2014 including any address byte(s) the caller embeds in `out_bytes` \u2014 retrievable via last_sent()/last_received().", + "text": "rcp::i2c::I2cEndpoint::transfer shall record the exact byte sequences passed as `out_bytes` and `in_bytes` — including any address byte(s) the caller embeds in `out_bytes` — retrievable via last_sent()/last_received().", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B", - "tc18": "\u00a713.7.7.3 (\"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.\"; Figure 30 \u2013 \"i2c request format\"), TC18.txt L5240-5257", + "tc18": "§13.7.7.3 (\"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.\"; Figure 30 – \"i2c request format\"), TC18.txt L5240-5257", "tc18_master_id": "TC18-13.7.7.3-001" }, { @@ -2822,7 +2822,7 @@ "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B", - "tc18": "\u00a713.7.7.4 (\"An I\u00b2C endpoint optionally generates HW trigger events\"; Table 50's own Event column is blank in the extraction, so the concrete TransferComplete/Nack signal identities below this clause are this implementation's own, not spec-named), TC18.txt L5262-5264", + "tc18": "§13.7.7.4 (\"An I²C endpoint optionally generates HW trigger events\"; Table 50's own Event column is blank in the extraction, so the concrete TransferComplete/Nack signal identities below this clause are this implementation's own, not spec-named), TC18.txt L5262-5264", "tc18_master_id": "TC18-13.7.7.4-001" }, { @@ -2961,11 +2961,11 @@ { "id": "REQ-ADC-003", "title": "ADC request-driven sampling only", - "text": "rcp::adc::AdcEndpoint::request_reading shall invoke its sample source exactly adc_avg_intervals_per_request * adc_combine_avg_values times per call \u2014 no more, no fewer \u2014 and shall return AdcErrc::no_signal (this endpoint's analog of PWM_IN_NO_SIGNAL on the request path) the first time the sample source reports a missing capture, without free-running or pushing samples outside a call.", + "text": "rcp::adc::AdcEndpoint::request_reading shall invoke its sample source exactly adc_avg_intervals_per_request * adc_combine_avg_values times per call — no more, no fewer — and shall return AdcErrc::no_signal (this endpoint's analog of PWM_IN_NO_SIGNAL on the request path) the first time the sample source reports a missing capture, without free-running or pushing samples outside a call.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B", - "tc18": "\u00a713.7.9.1 (\"The ADC only samples the input, when a request is executed. Consequently, no trigger will occur when there is no request to sample.\"), TC18.txt L5476-5477", + "tc18": "§13.7.9.1 (\"The ADC only samples the input, when a request is executed. Consequently, no trigger will occur when there is no request to sample.\"), TC18.txt L5476-5477", "tc18_master_id": "TC18-13.7.9.1-010" }, { @@ -2975,7 +2975,7 @@ "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B", - "tc18": "\u00a713.7.9.1 (\"Reading ADC values from an endpoint can be triggered e.g. by a PWM output trigger event by using a Trigger request, which triggers on the PWM EP.\"), TC18.txt L5446-5447", + "tc18": "§13.7.9.1 (\"Reading ADC values from an endpoint can be triggered e.g. by a PWM output trigger event by using a Trigger request, which triggers on the PWM EP.\"), TC18.txt L5446-5447", "tc18_master_id": "TC18-13.7.9.1-002" }, { @@ -3635,11 +3635,11 @@ { "id": "REQ-L2-007", "title": "MultiFrame round-trips two ACF_ABB messages packed into one L2 frame", - "text": "rcp::l2::encode_l2_multi_frame/decode_l2_multi_frame shall round-trip two or more ACF_ABB/ACF_GBB messages packed into a single Ethernet frame's AVTPDU, per extraction \u00a712.9.1.1's multiple-requests-in-one-frame requirement, the same as rcp::udp::MultiFrame's equivalent guarantee.", + "text": "rcp::l2::encode_l2_multi_frame/decode_l2_multi_frame shall round-trip two or more ACF_ABB/ACF_GBB messages packed into a single Ethernet frame's AVTPDU, per extraction §12.9.1.1's multiple-requests-in-one-frame requirement, the same as rcp::udp::MultiFrame's equivalent guarantee.", "standard": "iso26262", "level": "ASIL-B", "asil": "ASIL-B", - "tc18": "\u00a712.9.1.1 (\"An RCP frame may include multiple ACF-types (requests).\"), TC18.txt L3617", + "tc18": "§12.9.1.1 (\"An RCP frame may include multiple ACF-types (requests).\"), TC18.txt L3617", "tc18_master_id": "TC18-12.9.1.1-001" }, { @@ -3667,6 +3667,426 @@ "level": "ASIL-B", "asil": "ASIL-B", "tc18": "§12.3.1.1/.2/.3 (write request accepted only when its frame's destination MAC is unicast)" + }, + { + "id": "REQ-ACF-001", + "title": "acf_category() returns a unique, non-empty message per AcfErrc value", + "text": "rcp::acf::acf_category().message() shall return a unique, non-empty string for each defined AcfErrc value (bad_acf_msg_type, bad_acf_msg_length).", + "standard": "iso26262", + "level": "ASIL-A", + "asil": "ASIL-A", + "scope": "tc18", + "status": "implemented" + }, + { + "id": "REQ-ACF-004", + "title": "encode_acf_abb writes the correct acf_msg_type regardless of the caller-supplied header", + "text": "rcp::acf::encode_acf_abb shall write kAcfMsgTypeAbb into the encoded frame's acf_msg_type field, independent of any acf_msg_type value the caller's AcfMessageInfo happens to carry on entry.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§11.2.1 Table 4 (\"acf_msg_type 0x0E (ABB message)\"), TC18.txt L1152-1153; ACF_GBB acf_msg_type=0x0D, e.g. TC18.txt L1447" + }, + { + "id": "REQ-ACF-012", + "title": "AcfMessageInfo::mtv is a genuine two-state wire bit with no third \"uncertain\" state", + "text": "rcp::acf::AcfMessageInfo::mtv shall be represented as a bool (exactly the two wire-defined states, valid/not-valid) and rcp::acf::effective_timestamp shall gate ACF_GBB message_timestamp use strictly on mtv being set, never treating an untimed message as implicitly valid.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§11.3 Responses, Table 17 (\"mtv 0b - message_timestamp not valid (for ACF_GBB if PTP not working and for ACF_ABB) / 1b - message_timestamp valid (for ACF_GBB if PTP working correctly)\"), TC18.txt L2246-2247 -- the general, exhaustive two-value mtv definition; also §11.2.1 Figure 7/Table 4 (\"mtv 0b\"), TC18.txt L1208-1222, the fixed-context row the earlier citation-backfill pass (PR #172) had already found" + }, + { + "id": "REQ-ACF-013", + "title": "peek_msg_type reads the acf_msg_type byte without a full header decode", + "text": "rcp::acf::peek_msg_type shall report the ACF message's acf_msg_type byte from a buffer as short as 1 byte, and shall return AvtpErrc::short_buffer for a zero-length buffer.", + "standard": "iso26262", + "level": "ASIL-A", + "asil": "ASIL-A", + "scope": "tc18", + "status": "implemented", + "tc18": "§11.2.1 Figure 7 (acf_msg_type is byte 0 of byte_message_info), TC18.txt L1142-1153", + "tc18_master_id": "TC18-11.2.1-004" + }, + { + "id": "REQ-ACF-014", + "title": "Encoded ACF_ABB message length matches header length plus payload", + "text": "rcp::acf::encode_acf_abb shall produce a frame of exactly kAcfCommonHeaderLen + payload.size() bytes.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§11.2.1 Figure 7 (ABB byte_message_info, no timestamp), TC18.txt L1142-1149; ACF_GBB's additional 8-byte message_timestamp, e.g. TC18.txt L1447-1451" + }, + { + "id": "REQ-ACF-017", + "title": "kAcfMsgTypeAbb/kAcfMsgTypeGbb carry the specification's own numeric values", + "text": "rcp::acf::kAcfMsgTypeAbb shall equal 0x0E and rcp::acf::kAcfMsgTypeGbb shall equal 0x0D, the acf_msg_type values TC18 assigns to the ACF_ABB and ACF_GBB message formats respectively.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§11.2.1, TC18.txt L1153; §11.2.2.1, TC18.txt L1250" + }, + { + "id": "REQ-ACF-019", + "title": "AcfMessageInfo::op is a genuine two-state wire bit matching the specification's single op bit", + "text": "rcp::acf::AcfMessageInfo::op shall be represented as a bool (false = read, true = write), matching TC18's single-bit op field exactly rather than modeling a third encode-only state that could never appear on the wire.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§11.2.1, TC18.txt L1163-1164 (Table 4)", + "tc18_master_id": "TC18-11.2.1-005" + }, + { + "id": "REQ-ACF-021", + "title": "request_header_constraints_valid validates a request's fixed-value fields", + "text": "rcp::acf::request_header_constraints_valid(hdr, cs_has_meaning) shall return true iff hdr's hs, rsp, and err fields are all false, and cs is false unless cs_has_meaning is true (the compound-wait/chained exemption).", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§11.2.1, TC18.txt L1156-1167 (Table 4); §11.2.2.3, TC18.txt L1468-1474 (Table 8)", + "tc18_master_id": "TC18-11.2.1-005" + }, + { + "id": "REQ-ACF-023", + "title": "evt_row2_is_plain implements Table 33's shared ADC/PWM_IN/I2C/LIN/CAN/UART/ISELED/MDIO rule", + "text": "rcp::acf::evt_row2_is_plain(evt) shall return true iff (evt & 0x7) == 0, per TC18 Table 33's rule for the {ADC, PWM_IN, I2C, LIN, CAN, UART, ISELED, MDIO} endpoint-type row.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§13.5 Table 33, TC18.txt L3666-3710 (the reserved-range wording); §13.7.9.3 Figure 33/34 (\"RC Client sends a standard read request\" / ADC response with 8 measurements), the corroborating worked example resolving the ambiguity in Table 33's own favor for evt=000b", + "tc18_master_id": "TC18-13.5-001" + }, + { + "id": "REQ-ACF-024", + "title": "compound_wait_evt_valid rejects exactly the reserved 011b value", + "text": "rcp::acf::compound_wait_evt_valid(evt) shall return true iff (evt & 0x7) != 0x3, per TC18 §13.5.1: a compound-wait request's evt[2:0] selects one of eight comparison modes, of which 011b is reserved.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§13.5.1, TC18.txt L3735-3762 (evt[2:0] = 011b)", + "tc18_master_id": "TC18-13.5.1-002" + }, + { + "id": "REQ-ACF-025", + "title": "compound_wait_match applies the shared status-length rule before any mode-specific comparison", + "text": "rcp::acf::compound_wait_match shall return false, regardless of comparison mode or buffer contents, when status_len < payload_len; otherwise it shall compare against only status's own first payload_len bytes.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§13.5.1, TC18.txt L3735-3741 (length-capping rule and its own SPI example)", + "tc18_master_id": "TC18-13.5.1-001" + }, + { + "id": "REQ-ACF-026", + "title": "compound_wait_match evt[2:0]=000b: exact match", + "text": "rcp::acf::compound_wait_match shall, for evt[2:0] = 000b, return true iff payload[0..payload_len) matches the length-capped current status exactly, byte for byte.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§13.5.1, TC18.txt L3742 (evt[2:0] = 000b)", + "tc18_master_id": "TC18-13.5.1-002" + }, + { + "id": "REQ-ACF-027", + "title": "compound_wait_match evt[2:0]=001b: AND-with-1s-mask", + "text": "rcp::acf::compound_wait_match shall, for evt[2:0] = 001b, return true iff every bit position set to 1 in payload is also set to 1 in the length-capped current status.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§13.5.1, TC18.txt L3743-3746 (evt[2:0] = 001b)", + "tc18_master_id": "TC18-13.5.1-002" + }, + { + "id": "REQ-ACF-028", + "title": "compound_wait_match evt[2:0]=010b: AND-with-0s-mask", + "text": "rcp::acf::compound_wait_match shall, for evt[2:0] = 010b, return true iff every bit position set to 1 in payload is set to 0 in the length-capped current status.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§13.5.1, TC18.txt L3747-3750 (evt[2:0] = 010b)", + "tc18_master_id": "TC18-13.5.1-002" + }, + { + "id": "REQ-ACF-029", + "title": "compound_wait_match evt[2:0]=100b: leading-quadlet high-word >=", + "text": "rcp::acf::compound_wait_match shall, for evt[2:0] = 100b, return true iff the first two bytes of payload's own leading quadlet, read big-endian, are >= the same two bytes of status; it shall return false, never reading out of bounds, when payload_len < 4.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§13.5.1, TC18.txt L3753-3758 (evt[2:0] = 100b/101b)", + "tc18_master_id": "TC18-13.5.1-002" + }, + { + "id": "REQ-ACF-030", + "title": "compound_wait_match evt[2:0]=110b: leading-quadlet low-word >=", + "text": "rcp::acf::compound_wait_match shall, for evt[2:0] = 110b, return true iff the last two bytes of payload's own leading quadlet, read big-endian, are >= the same two bytes of status; it shall return false, never reading out of bounds, when payload_len < 4.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§13.5.1, TC18.txt L3759-3763 (evt[2:0] = 110b/111b)", + "tc18_master_id": "TC18-13.5.1-002" + }, + { + "id": "REQ-ACF-031", + "title": "build_error_response builds a TC18 §12.9.6 error response", + "text": "rcp::acf::build_error_response shall build an ACF_ABB message carrying the given byte_bus_id and transaction_num, err = true, rsp = true, evt != kEvtAcknowledge, and a one-octet byte_msg_payload equal to the given WireErrorCode's numeric value.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§12.9.6 (\"The error response shall contain the byte_bus_id and transaction number of the request. The error response shall contain a byte_msg_payload with an error code.\"), TC18.txt L3808-3809; §11.3.4, TC18.txt L2299-2301", + "tc18_master_id": "TC18-12.9.6-003" + }, + { + "id": "REQ-ACF-032", + "title": "peek_gbb_request_type peeks a GBB frame's own request_type without a full kind-specific decode", + "text": "rcp::acf::peek_gbb_request_type shall return the octet at frame offset kAcfCommonHeaderLen (the message_timestamp region's own repurposed leading octet, shared identically by every conditional-request kind) as out_request_type and return true, iff frame_len >= 9 and the frame's own header reports acf_msg_type == kAcfMsgTypeGbb; it shall return false, leaving out_request_type unchanged, for an ACF_ABB frame or a too-short buffer.", + "standard": "iso26262", + "level": "QM", + "asil": "QM", + "scope": "tc18", + "status": "implemented", + "tc18": "§11.2.2 (request_type/message_timestamp-repurposing scheme, common to every conditional request kind), TC18.txt L1299-1541", + "tc18_master_id": "TC18-11.2.2-002" + }, + { + "id": "REQ-ACF-033", + "title": "build_acknowledge_rejected_response builds TC18 §11.3.1's Acknowledge-shaped storage-admission-rejection response", + "text": "rcp::acf::build_acknowledge_rejected_response shall build a response with evt[3:0] == kEvtAcknowledge, err = true, rsp = true, and a one-octet byte_msg_payload equal to the given WireErrorCode's numeric value -- classifying via response_kind_of() as Acknowledge, not ErrorResponse, distinct from build_error_response()'s §11.3.4 Error Response shape even though both carry err=1.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§11.3.1 Acknowledge (err=1 rejection sub-case, distinct from §11.3.4 Error Response), TC18.txt", + "tc18_master_id": "TC18-11.3.1-002" + }, + { + "id": "REQ-ACF-038", + "title": "encode_acf_gbb writes the correct acf_msg_type regardless of the caller-supplied header", + "text": "rcp::acf::encode_acf_gbb shall write kAcfMsgTypeGbb into the encoded frame's acf_msg_type field, independent of any acf_msg_type value the caller's AcfMessageInfo happens to carry on entry.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§11.2.1 Table 4 (\"acf_msg_type 0x0E (ABB message)\"), TC18.txt L1152-1153; ACF_GBB acf_msg_type=0x0D, e.g. TC18.txt L1447" + }, + { + "id": "REQ-ACF-044", + "title": "Encoded ACF_GBB message length matches header length plus payload", + "text": "rcp::acf::encode_acf_gbb shall produce a frame of exactly kAcfGbbMessageInfoLen + payload.size() bytes, with kAcfGbbMessageInfoLen equal to kAcfCommonHeaderLen plus the 8-byte message_timestamp field.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§11.2.1 Figure 7 (ABB byte_message_info, no timestamp), TC18.txt L1142-1149; ACF_GBB's additional 8-byte message_timestamp, e.g. TC18.txt L1447-1451" + }, + { + "id": "REQ-ACF-047", + "title": "pad_len computes the zero-pad octet count to the next quadlet boundary", + "text": "rcp::acf::pad_len(unpadded_len) shall compute the zero-pad octet count (0-3) needed to bring unpadded_len octets up to a whole number of quadlets.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§11.2.1 Figure 7 / Table 4 (\"ABB message standard request\" / \"Usage of ABB message for standard requests\"), TC18.txt L1142-1170", + "tc18_master_id": "TC18-11.2.1-005" + }, + { + "id": "REQ-ACF-048", + "title": "kAcfMsgTypeGbb carries the specification's own numeric value", + "text": "rcp::acf::kAcfMsgTypeGbb shall equal 0x0D, the acf_msg_type value TC18 assigns to the ACF_GBB message format (see REQ-ACF-017 for the ACF_ABB / 0x0E counterpart, tested by the same TEST_CASE).", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§11.2.1, TC18.txt L1153; §11.2.2.1, TC18.txt L1250" + }, + { + "id": "REQ-ACF-049", + "title": "decode_acf_message_info decodes op as a genuine two-state field", + "text": "rcp::acf::decode_acf_message_info shall decode the wire op bit into AcfMessageInfo::op as a bool -- by construction, decode can never produce a third encode-only state distinct from read/write.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§11.2.1, TC18.txt L1163-1164 (Table 4)", + "tc18_master_id": "TC18-11.2.1-005" + }, + { + "id": "REQ-ACF-050", + "title": "header_is_request identifies a decoded message as a request, not a response", + "text": "rcp::acf::header_is_request(hdr) shall return true iff hdr's rsp bit is false; a received message whose rsp bit is set shall not be admitted as a request, per TC18's rsp=1b response marker.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§11.2.1, TC18.txt L1156-1167 (Table 4); §11.2.2.3, TC18.txt L1468-1474 (Table 8)", + "tc18_master_id": "TC18-11.2.1-005" + }, + { + "id": "REQ-ACF-051", + "title": "compound_wait_match returns false for the reserved evt[2:0]=0x3 mode as its own defensive fallback", + "text": "rcp::acf::compound_wait_match shall also return false for evt[2:0] = 0x3 (reserved), as its own defined fallback behaviour for a caller that does not gate on compound_wait_evt_valid() first.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§13.5.1, TC18.txt L3735-3741 (length-capping rule and its own SPI example)", + "tc18_master_id": "TC18-13.5.1-001" + }, + { + "id": "REQ-ACF-052", + "title": "compound_wait_match evt[2:0]=101b: leading-quadlet high-word <=", + "text": "rcp::acf::compound_wait_match shall, for evt[2:0] = 101b, return true iff the first two bytes of payload's own leading quadlet, read big-endian, are <= the same two bytes of status; it shall return false, never reading out of bounds, when payload_len < 4.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§13.5.1, TC18.txt L3753-3758 (evt[2:0] = 100b/101b)", + "tc18_master_id": "TC18-13.5.1-002" + }, + { + "id": "REQ-ACF-053", + "title": "compound_wait_match evt[2:0]=111b: leading-quadlet low-word <=", + "text": "rcp::acf::compound_wait_match shall, for evt[2:0] = 111b, return true iff the last two bytes of payload's own leading quadlet, read big-endian, are <= the same two bytes of status; it shall return false, never reading out of bounds, when payload_len < 4.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§13.5.1, TC18.txt L3759-3763 (evt[2:0] = 110b/111b)", + "tc18_master_id": "TC18-13.5.1-002" + }, + { + "id": "REQ-AVTP-013", + "title": "peek_subtype reads the subtype byte from a buffer as short as 1 byte, and rejects an empty buffer", + "text": "rcp::avtp::peek_subtype shall report the AVTPDU's subtype byte from a buffer as short as 1 byte, and shall return AvtpErrc::short_buffer for a zero-length buffer.", + "standard": "iso26262", + "level": "ASIL-A", + "asil": "ASIL-A", + "scope": "tc18", + "status": "implemented", + "tc18": "§11.1 (subtype is byte 0 of both NTSCF Figure 6 and TSCF Figure 5), TC18.txt L1118-1138" + }, + { + "id": "REQ-AVTP-014", + "title": "TSCF-without-time-sync is dropped under the default (DROP) policy", + "text": "rcp::avtp::should_drop_tscf shall return true iff subtype is kSubtypeTscf, server_time_sync_supported is false, and unsupported_time_sync_policy is TscfFallback::Drop -- this codec's own original, default disposition.", + "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", + "tc18_master_id": "TC18-11.1-005" + }, + { + "id": "REQ-AVTP-015", + "title": "avtp_category() returns a unique, non-empty message per AvtpErrc value", + "text": "rcp::avtp::avtp_category().message() shall return a unique, non-empty string for each defined AvtpErrc value (short_buffer, bad_subtype, length_mismatch).", + "standard": "iso26262", + "level": "ASIL-A", + "asil": "ASIL-A", + "scope": "tc18", + "status": "implemented" + }, + { + "id": "REQ-AVTP-021", + "title": "should_drop_tscf does not drop a TSCF-without-time-sync frame when policy is IGNORE", + "text": "rcp::avtp::should_drop_tscf shall return false for a TSCF frame with server_time_sync_supported == false when unsupported_time_sync_policy is TscfFallback::Ignore, per TC18 §13.3's configurable alternative to §11.1's unconditional drop wording.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§13.3 (\"Requests may come within an AVTPDU with a presentation time (i.e. TSCF header). In case the RC Server does not support time synchronization, the presentation time shall be ignored, and the request(s) executed as if no presentation time were included or dropped depending on the configuration of the RC Server.\"), TC18.txt L4035-4038", + "tc18_master_id": "TC18-13.3-001" + }, + { + "id": "REQ-AVTP-022", + "title": "decode_tscf_header populates the TSCF header's reserved0/reserved1 fields from the wire", + "text": "rcp::avtp::decode_tscf_header shall decode bytes 16-19 into TscfHeader::reserved0 and bytes 22-23 into TscfHeader::reserved1 exactly as received, so a caller can evaluate TC18 §13.3's reserved-bytes-all-zero rule (see tscf_reserved_all_zero(), REQ-AVTP-031) against the real wire content rather than an assumed-zero default.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§13.3 (\"If the reserved bytes in the header are all zero, then the request shall be queued as if the header was in NTSCF format or dropped, depending on configuration.\"), TC18.txt L4039-4040", + "tc18_master_id": "TC18-13.3-003" + }, + { + "id": "REQ-AVTP-023", + "title": "decode_tscf_header faithfully decodes tu regardless of its value (§13.3)", + "text": "rcp::avtp::decode_tscf_header shall decode the wire tu (timestamp-uncertain) bit into TscfHeader::tu unchanged for both tu=0 and tu=1, without itself branching on its value -- TC18 §13.3's 'execute as if tu=0' rule is a caller-level policy, not a decode-time transformation.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§13.3 (\"In case the time stamp is uncertain (i.e. tu = 1), then this shall be executed as if tu = 0.\"), TC18.txt L4040-4041", + "tc18_master_id": "TC18-13.3-004" + }, + { + "id": "REQ-AVTP-031", + "title": "tscf_reserved_all_zero returns true iff both reserved fields are zero (§13.3)", + "text": "rcp::avtp::tscf_reserved_all_zero(hdr) shall return true iff hdr.reserved0 and hdr.reserved1 are both zero, the predicate TC18 §13.3's own reserved-bytes rule is built on.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§13.3 (\"If the reserved bytes in the header are all zero, then the request shall be queued as if the header was in NTSCF format or dropped, depending on configuration.\"), TC18.txt L4039-4040", + "tc18_master_id": "TC18-13.3-003" + }, + { + "id": "REQ-WIREERR-001", + "title": "WireErrorCode assigns the governing spec's own numbered wire error codes", + "text": "rcp::acf::WireErrorCode shall assign each enumerator the exact numeric value TC18 Table 27/Table 30 defines for it (UnsupportedCmd=1 .. ChainError=17), so encode_error_payload()'s single-octet byte_msg_payload matches the wire value a peer expects.", + "standard": "iso26262", + "level": "ASIL-B", + "asil": "ASIL-B", + "scope": "tc18", + "status": "implemented", + "tc18": "§12.9.6 Table 30, TC18.txt L3804-3844", + "tc18_master_id": "TC18-12.9.6-004" } ] } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de4f94c..810184a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -523,11 +523,13 @@ jobs: echo "OK: $tested/$total tested" # Genuine security-test coverage: CYBERSECURITY.md's own six security - # layers cite exactly 64 requirement ids as security-relevant - # (REQ-TLS-001..010, REQ-AUTH-001..008, REQ-E2E-001..014, - # REQ-RL-001..008, REQ-DISC-001..009, REQ-REGMAP-001..015), each now - # marked `"severity": "cybersecurity"` in `.fusa-reqs.json` -- the - # field cpp-FuSa's own trace.cpp already reads to compute + # layers cite requirement ids as security-relevant (REQ-TLS-001..010, + # REQ-AUTH-001..008, REQ-E2E-001..014, REQ-RL-001..008, + # REQ-DISC-001..009, REQ-REGMAP-001..015, plus the 8 Table-30 Row-2 + # evt[2:0]-classification ids added after that doc's count was last + # written -- 72 total as of Phase 5), each marked + # `"severity": "cybersecurity"` in `.fusa-reqs.json` -- the field + # cpp-FuSa's own trace.cpp already reads to compute # `secTestedRequirements` (a test annotation on a cybersecurity-marked # requirement), previously never populated so this metric was always # zero. Deliberately NOT using cpfusa's own `--sec-tested` CLI flag: @@ -536,24 +538,42 @@ jobs: # sec_tested/total) -- the exact same bug class SoundMatt/c-FuSa#147 # fixed in c-FuSa, filed upstream as SoundMatt/cpp-FuSa#64, not yet # fixed. Parses `--format json`'s already-correct - # `secTestedRequirements` field directly instead, at a 17% floor - # (64/376 = 17.02%) -- NOT a claim that only 17% of this codebase is - # security-relevant, a regression floor against the exact, - # CYBERSECURITY.md-enumerated subset. Once cpp-FuSa#64 is fixed - # upstream, this step can switch to `cpfusa trace --sec-tested 17` - # directly. - - name: Enforce security-test coverage (17% floor, real CYBERSECURITY.md subset) + # `secTestedRequirements` field directly instead. + # + # Gated on an ABSOLUTE COUNT (>= 72), not a percentage of the total + # catalog: this step was originally a 17% floor (64/376 at the time), + # but its own comment already stated the actual intent was "a + # regression floor against the exact, CYBERSECURITY.md-enumerated + # subset" losing test coverage -- not a claim about what fraction of + # the codebase is security-relevant. A percentage-of-total + # implementation is the wrong shape for that intent: Phase 6 (cpp-RCP + # #129) is deliberately growing `.fusa-reqs.json` from 412 toward + # something closer to c-RCP's 1282-entry catalog by re-deriving + # entries for already-ported, non-cybersecurity-relevant content + # (wire framing, per-endpoint register semantics, etc.) -- exactly + # the kind of correct, intentional growth a percentage floor + # penalizes as if it were a regression. Found when Phase 6 batch 1 + # (PR #155) legitimately added 36 REQ-ACF-*/REQ-AVTP-*/REQ-WIREERR-* + # entries and the previously-passing ratio (72/412 = 17.5%) dropped + # to 72/448 = 16.1%, failing a floor that was never meant to track + # total-catalog size. An absolute-count floor at the current + # enumerated total (72) still catches the real regression this step + # exists for -- one of the enumerated ids losing its test tag, or an + # id being removed from the `severity: cybersecurity` set -- without + # being diluted by legitimate catalog growth elsewhere. Update this + # number (never lower it silently) if CYBERSECURITY.md's own + # enumerated set genuinely grows or shrinks. + - name: Enforce security-test coverage (absolute floor, real CYBERSECURITY.md subset) run: | cov="$(/tmp/cpfusa/build/cpfusa trace --format json | jq '.coverage')" echo "$cov" total=$(echo "$cov" | jq '.totalRequirements') sec=$(echo "$cov" | jq '.secTestedRequirements') - pct=$(( sec * 100 / total )) - if [ "$pct" -lt 17 ]; then - echo "::error::sec-tested gate failed: ${pct}% ($sec/$total) < required 17%" + if [ "$sec" -lt 72 ]; then + echo "::error::sec-tested gate failed: $sec/$total security-tested < required floor of 72 (CYBERSECURITY.md-enumerated subset)" exit 1 fi - echo "OK: ${pct}% ($sec/$total) security-tested" + echo "OK: $sec/$total security-tested (floor: 72)" # ── cpp-FuSa: ctest integration → .fusa-evidence.json ──────────────────────── cpfusa-verify: diff --git a/include/rcp/acf.hpp b/include/rcp/acf.hpp index 986b8fc..29180fa 100644 --- a/include/rcp/acf.hpp +++ b/include/rcp/acf.hpp @@ -8,6 +8,35 @@ // fusa:req REQ-WIRE-013 // fusa:req REQ-WIRE-014 // fusa:req REQ-WIRE-015 +// fusa:req REQ-ACF-001 +// fusa:req REQ-ACF-004 +// fusa:req REQ-ACF-012 +// fusa:req REQ-ACF-013 +// fusa:req REQ-ACF-014 +// fusa:req REQ-ACF-017 +// fusa:req REQ-ACF-019 +// fusa:req REQ-ACF-021 +// fusa:req REQ-ACF-023 +// fusa:req REQ-ACF-024 +// fusa:req REQ-ACF-025 +// fusa:req REQ-ACF-026 +// fusa:req REQ-ACF-027 +// fusa:req REQ-ACF-028 +// fusa:req REQ-ACF-029 +// fusa:req REQ-ACF-030 +// fusa:req REQ-ACF-031 +// fusa:req REQ-ACF-032 +// fusa:req REQ-ACF-033 +// fusa:req REQ-ACF-038 +// fusa:req REQ-ACF-044 +// fusa:req REQ-ACF-047 +// fusa:req REQ-ACF-048 +// fusa:req REQ-ACF-049 +// fusa:req REQ-ACF-050 +// fusa:req REQ-ACF-051 +// fusa:req REQ-ACF-052 +// fusa:req REQ-ACF-053 +// fusa:req REQ-WIREERR-001 // TC18 wire codec, message half — the ACF_ABB / ACF_GBB message types (ACF — // AVTP Control Format) the OPEN Alliance TC18 Remote Control Protocol diff --git a/include/rcp/avtp.hpp b/include/rcp/avtp.hpp index cf5fb51..cb5e5ea 100644 --- a/include/rcp/avtp.hpp +++ b/include/rcp/avtp.hpp @@ -4,6 +4,13 @@ // fusa:req REQ-WIRE-007 // fusa:req REQ-WIRE-011 // fusa:req REQ-WIRE-013 +// fusa:req REQ-AVTP-013 +// fusa:req REQ-AVTP-014 +// fusa:req REQ-AVTP-015 +// fusa:req REQ-AVTP-021 +// fusa:req REQ-AVTP-022 +// fusa:req REQ-AVTP-023 +// fusa:req REQ-AVTP-031 // TC18 wire codec, framing half — IEEE 1722 AVTPDU framing (NTSCF/TSCF) that // the OPEN Alliance TC18 Remote Control Protocol Specification v0.5.1_RC @@ -351,6 +358,10 @@ inline std::error_code decode_tscf_header(const uint8_t* b, size_t len, TscfHead out.tu = (b[3] & detail::kFlagTimestampUncertain) != 0; out.stream_id = StreamId::from_u64(detail::get_u64(&b[4])); out.avtp_timestamp = detail::get_u32(&b[12]); + // REQ-AVTP-022: reserved0/reserved1 are populated from the real wire + // bytes (16-19/22-23), not left at their struct default — see + // tscf_reserved_all_zero() below (REQ-AVTP-031), which is built on top + // of these two fields actually reflecting what was received. out.reserved0 = detail::get_u32(&b[16]); out.control_data_length = detail::get_u16(&b[20]); out.reserved1 = detail::get_u16(&b[22]); @@ -445,12 +456,16 @@ inline bool should_drop_tscf(bool server_time_sync_supported, uint8_t subtype, return false; } -// REQ-AVTP-022, TC18 §13.3's second configurable rule: "If the reserved +// REQ-AVTP-031, TC18 §13.3's second configurable rule: "If the reserved // bytes in the header are all zero, then the request shall be queued as if // the header was in NTSCF format or dropped, depending on configuration." // Returns true iff hdr's own reserved0/reserved1 are both zero — callers // combine this with a caller-owned TscfFallback exactly the way // should_drop_tscf()'s own unsupported_time_sync_policy parameter is used. +// (Corrected 2026-08-22: this comment previously mislabeled itself +// REQ-AVTP-022, which is actually decode_tscf_header()'s own population of +// hdr.reserved0/reserved1 from the wire, above — the predicate this +// function itself defines is REQ-AVTP-031.) inline bool tscf_reserved_all_zero(const TscfHeader& hdr) noexcept { return hdr.reserved0 == 0 && hdr.reserved1 == 0; } diff --git a/tests/test_acf.cpp b/tests/test_acf.cpp index 6def525..d4fd5a3 100644 --- a/tests/test_acf.cpp +++ b/tests/test_acf.cpp @@ -8,6 +8,35 @@ // fusa:test REQ-WIRE-013 // fusa:test REQ-WIRE-014 // fusa:test REQ-WIRE-015 +// fusa:test REQ-ACF-001 +// fusa:test REQ-ACF-004 +// fusa:test REQ-ACF-012 +// fusa:test REQ-ACF-013 +// fusa:test REQ-ACF-014 +// fusa:test REQ-ACF-017 +// fusa:test REQ-ACF-019 +// fusa:test REQ-ACF-021 +// fusa:test REQ-ACF-023 +// fusa:test REQ-ACF-024 +// fusa:test REQ-ACF-025 +// fusa:test REQ-ACF-026 +// fusa:test REQ-ACF-027 +// fusa:test REQ-ACF-028 +// fusa:test REQ-ACF-029 +// fusa:test REQ-ACF-030 +// fusa:test REQ-ACF-031 +// fusa:test REQ-ACF-032 +// fusa:test REQ-ACF-033 +// fusa:test REQ-ACF-038 +// fusa:test REQ-ACF-044 +// fusa:test REQ-ACF-047 +// fusa:test REQ-ACF-048 +// fusa:test REQ-ACF-049 +// fusa:test REQ-ACF-050 +// fusa:test REQ-ACF-051 +// fusa:test REQ-ACF-052 +// fusa:test REQ-ACF-053 +// fusa:test REQ-WIREERR-001 // Tests for rcp/acf.hpp — the ACF_ABB/ACF_GBB message-format half of the TC18 // wire codec (ROADMAP.md milestone 44, "Wire Format Core", v2.0.0; split @@ -23,7 +52,7 @@ using rcp::avtp::TscfHeader; // ── ACF_ABB ───────────────────────────────────────────────────────────────────── -TEST_CASE("ACF_ABB round-trips the shared header and payload with no timestamp field", "[acf][REQ-WIRE-004][REQ-WIRE-006]") { +TEST_CASE("ACF_ABB round-trips the shared header and payload with no timestamp field", "[acf][REQ-WIRE-004][REQ-WIRE-006][REQ-ACF-014][REQ-ACF-019][REQ-ACF-049]") { AcfMessageInfo info; info.byte_bus_id = 7; info.transaction_num = 42; @@ -68,7 +97,7 @@ TEST_CASE("ACF_ABB round-trips the shared header and payload with no timestamp f // ── ACF_GBB ───────────────────────────────────────────────────────────────────── -TEST_CASE("ACF_GBB round-trips a 64-bit message_timestamp alongside the shared header", "[acf][REQ-WIRE-005][REQ-WIRE-006]") { +TEST_CASE("ACF_GBB round-trips a 64-bit message_timestamp alongside the shared header", "[acf][REQ-WIRE-005][REQ-WIRE-006][REQ-ACF-044]") { AcfMessageInfo info; info.byte_bus_id = 3; info.transaction_num = 9; @@ -163,7 +192,7 @@ TEST_CASE("ACF_ABB hand-computed expected byte sequence", "[acf][REQ-WIRE-004][R REQUIRE(re_encoded == frame); } -TEST_CASE("ACF_GBB hand-computed expected byte sequence", "[acf][REQ-WIRE-005][REQ-WIRE-006]") { +TEST_CASE("ACF_GBB hand-computed expected byte sequence", "[acf][REQ-WIRE-005][REQ-WIRE-006][REQ-ACF-044]") { AcfMessageInfo info; info.acf_msg_length = 7; // 9 bits info.pad = 1; // 2 bits @@ -494,7 +523,7 @@ TEST_CASE("The four response kinds map onto distinct, recoverable header states" // ── Timestamp fallback rules ──────────────────────────────────────────────────── -TEST_CASE("effective_timestamp prefers a valid TSCF avtp_timestamp", "[acf][REQ-WIRE-012]") { +TEST_CASE("effective_timestamp prefers a valid TSCF avtp_timestamp", "[acf][REQ-WIRE-012][REQ-ACF-012]") { TscfHeader tscf; tscf.timestamp_valid = true; tscf.avtp_timestamp = 111; @@ -508,7 +537,7 @@ TEST_CASE("effective_timestamp prefers a valid TSCF avtp_timestamp", "[acf][REQ- REQUIRE(*ts == 111); } -TEST_CASE("effective_timestamp falls back to a valid ACF_GBB message_timestamp", "[acf][REQ-WIRE-012]") { +TEST_CASE("effective_timestamp falls back to a valid ACF_GBB message_timestamp", "[acf][REQ-WIRE-012][REQ-ACF-012]") { AcfMessageInfo gbb_info; gbb_info.acf_msg_type = kAcfMsgTypeGbb; gbb_info.mtv = true; @@ -518,7 +547,7 @@ TEST_CASE("effective_timestamp falls back to a valid ACF_GBB message_timestamp", REQUIRE(*ts == 333); } -TEST_CASE("effective_timestamp is nullopt, not zero, when neither source is valid", "[acf][REQ-WIRE-012]") { +TEST_CASE("effective_timestamp is nullopt, not zero, when neither source is valid", "[acf][REQ-WIRE-012][REQ-ACF-012]") { TscfHeader tscf; // timestamp_valid defaults to false AcfMessageInfo abb_info; abb_info.acf_msg_type = kAcfMsgTypeAbb; // ACF_ABB never has a timestamp @@ -599,7 +628,7 @@ TEST_CASE("to_message carries message_timestamp only for ACF_GBB", "[acf][relay- // ── Phase 17 (c-RCP port): message-type constants & GBB/ABB header-length relation ── -TEST_CASE("ACF_ABB/ACF_GBB message type wire values", "[acf]") { +TEST_CASE("ACF_ABB/ACF_GBB message type wire values", "[acf][REQ-ACF-017][REQ-ACF-048]") { REQUIRE(kAcfMsgTypeAbb == 0x0E); REQUIRE(kAcfMsgTypeGbb == 0x0D); } @@ -612,7 +641,7 @@ TEST_CASE("kAcfGbbMessageInfoLen is exactly kAcfCommonHeaderLen + 8", "[acf]") { // ── pad_len (ported from c-RCP's rcp_acf_pad_len()) ─────────────────────────── -TEST_CASE("pad_len computes the octets needed to reach the next quadlet boundary", "[acf]") { +TEST_CASE("pad_len computes the octets needed to reach the next quadlet boundary", "[acf][REQ-ACF-047]") { REQUIRE(pad_len(8) == 0); REQUIRE(pad_len(9) == 3); REQUIRE(pad_len(10) == 2); @@ -632,7 +661,7 @@ TEST_CASE("kAcfAbbMaxPayload/kAcfGbbMaxPayload are derived from the 9-bit acf_ms // ── peek_msg_type (ported from c-RCP's rcp_acf_peek_msg_type()) ────────────── -TEST_CASE("peek_msg_type reads the first byte's acf_msg_type", "[acf]") { +TEST_CASE("peek_msg_type reads the first byte's acf_msg_type", "[acf][REQ-ACF-013]") { AcfMessageInfo info; auto frame = encode_acf_abb(info, {}); uint8_t msg_type = 0; @@ -640,7 +669,7 @@ TEST_CASE("peek_msg_type reads the first byte's acf_msg_type", "[acf]") { REQUIRE(msg_type == kAcfMsgTypeAbb); } -TEST_CASE("peek_msg_type rejects an empty buffer", "[acf]") { +TEST_CASE("peek_msg_type rejects an empty buffer", "[acf][REQ-ACF-013]") { uint8_t msg_type = 0; REQUIRE(peek_msg_type(nullptr, 0, msg_type)); } @@ -648,7 +677,7 @@ TEST_CASE("peek_msg_type rejects an empty buffer", "[acf]") { // ── header_is_request / request_header_constraints_valid (ported from c-RCP's // rcp_acf_header_is_request()/_request_header_constraints_valid()) ─────────── -TEST_CASE("header_is_request is true for rsp=0 and false for rsp=1", "[acf]") { +TEST_CASE("header_is_request is true for rsp=0 and false for rsp=1", "[acf][REQ-ACF-050]") { AcfMessageInfo hdr; REQUIRE(header_is_request(hdr)); // rsp=0: a request @@ -656,12 +685,12 @@ TEST_CASE("header_is_request is true for rsp=0 and false for rsp=1", "[acf]") { REQUIRE_FALSE(header_is_request(hdr)); // rsp=1: a response } -TEST_CASE("request_header_constraints_valid accepts a fresh, unmodified request header", "[acf]") { +TEST_CASE("request_header_constraints_valid accepts a fresh, unmodified request header", "[acf][REQ-ACF-021]") { AcfMessageInfo hdr; REQUIRE(request_header_constraints_valid(hdr, /*cs_has_meaning=*/false)); } -TEST_CASE("request_header_constraints_valid rejects hs/rsp/err set on a request", "[acf]") { +TEST_CASE("request_header_constraints_valid rejects hs/rsp/err set on a request", "[acf][REQ-ACF-021]") { AcfMessageInfo hs_hdr; hs_hdr.hs = true; REQUIRE_FALSE(request_header_constraints_valid(hs_hdr, false)); @@ -675,7 +704,7 @@ TEST_CASE("request_header_constraints_valid rejects hs/rsp/err set on a request" REQUIRE_FALSE(request_header_constraints_valid(err_hdr, false)); } -TEST_CASE("request_header_constraints_valid rejects cs=1 unless cs_has_meaning", "[acf]") { +TEST_CASE("request_header_constraints_valid rejects cs=1 unless cs_has_meaning", "[acf][REQ-ACF-021]") { AcfMessageInfo hdr; hdr.cs = true; REQUIRE_FALSE(request_header_constraints_valid(hdr, /*cs_has_meaning=*/false)); @@ -685,7 +714,7 @@ TEST_CASE("request_header_constraints_valid rejects cs=1 unless cs_has_meaning", // ── evt_row2_is_plain (TC18 §13.5 Table 33's ADC/PWM_IN/I2C/LIN/CAN/UART/ // ISELED/MDIO row) ───────────────────────────────────────────────────────── -TEST_CASE("evt_row2_is_plain is true only for evt[2:0] == 0", "[acf]") { +TEST_CASE("evt_row2_is_plain is true only for evt[2:0] == 0", "[acf][REQ-ACF-023]") { REQUIRE(evt_row2_is_plain(0x0)); for (uint8_t v = 1; v <= 6; ++v) { REQUIRE_FALSE(evt_row2_is_plain(v)); @@ -693,7 +722,7 @@ TEST_CASE("evt_row2_is_plain is true only for evt[2:0] == 0", "[acf]") { REQUIRE_FALSE(evt_row2_is_plain(0x7)); // reserved config-write selector } -TEST_CASE("evt_row2_is_plain ignores evt[3] (the ack-request bit)", "[acf]") { +TEST_CASE("evt_row2_is_plain ignores evt[3] (the ack-request bit)", "[acf][REQ-ACF-023]") { // evt[3] is outside evt[2:0]'s 3-bit scope — a request with evt[3] set // but evt[2:0] = 000b is still plain. REQUIRE(evt_row2_is_plain(0x8)); @@ -712,7 +741,7 @@ TEST_CASE("evt_requests_acknowledge reflects evt[3] regardless of evt[2:0]", "[a // ── TC18 §13.5.1 compound-wait evt[2:0] comparison rule (ported from c-RCP's // rcp_acf_compound_wait_evt_valid()/_compound_wait_match()) ────────────────── -TEST_CASE("compound_wait_evt_valid is true for every mode but the reserved one", "[acf]") { +TEST_CASE("compound_wait_evt_valid is true for every mode but the reserved one", "[acf][REQ-ACF-024]") { REQUIRE(compound_wait_evt_valid(0x0)); REQUIRE(compound_wait_evt_valid(0x1)); REQUIRE(compound_wait_evt_valid(0x2)); @@ -722,13 +751,13 @@ TEST_CASE("compound_wait_evt_valid is true for every mode but the reserved one", REQUIRE(compound_wait_evt_valid(0x7)); } -TEST_CASE("compound_wait_evt_valid is false for evt[2:0] == 011b regardless of the upper bits", "[acf]") { +TEST_CASE("compound_wait_evt_valid is false for evt[2:0] == 011b regardless of the upper bits", "[acf][REQ-ACF-024]") { REQUIRE_FALSE(compound_wait_evt_valid(0x3)); REQUIRE_FALSE(compound_wait_evt_valid(0xB)); // 1011b REQUIRE_FALSE(compound_wait_evt_valid(0xFB & 0x0F)); } -TEST_CASE("compound_wait_match: status shorter than payload never matches", "[acf]") { +TEST_CASE("compound_wait_match: status shorter than payload never matches", "[acf][REQ-ACF-025]") { const uint8_t payload[4] = {0x01, 0x02, 0x03, 0x04}; const uint8_t status[3] = {0x01, 0x02, 0x03}; @@ -737,7 +766,7 @@ TEST_CASE("compound_wait_match: status shorter than payload never matches", "[ac REQUIRE_FALSE(compound_wait_match(0x0, payload, sizeof(payload), status, sizeof(status))); } -TEST_CASE("compound_wait_match caps status to payload_length (the specification's own SPI example)", "[acf]") { +TEST_CASE("compound_wait_match caps status to payload_length (the specification's own SPI example)", "[acf][REQ-ACF-025]") { const uint8_t payload[4] = {0x00, 0x00, 0x00, 0x02}; uint8_t status[20]; std::fill(std::begin(status), std::end(status), uint8_t{0xAA}); // tail bytes: never read @@ -750,7 +779,7 @@ TEST_CASE("compound_wait_match caps status to payload_length (the specification' REQUIRE_FALSE(compound_wait_match(0x0, payload, sizeof(payload), status, sizeof(status))); } -TEST_CASE("compound_wait_match exact-match mode (evt[2:0] = 000b)", "[acf]") { +TEST_CASE("compound_wait_match exact-match mode (evt[2:0] = 000b)", "[acf][REQ-ACF-026]") { const uint8_t payload[2] = {0x01, 0x02}; const uint8_t equal[2] = {0x01, 0x02}; const uint8_t differs[2] = {0x01, 0x03}; @@ -760,7 +789,7 @@ TEST_CASE("compound_wait_match exact-match mode (evt[2:0] = 000b)", "[acf]") { REQUIRE(compound_wait_match(0x0, nullptr, 0, nullptr, 0)); } -TEST_CASE("compound_wait_match AND-with-1s-mask mode (evt[2:0] = 001b)", "[acf]") { +TEST_CASE("compound_wait_match AND-with-1s-mask mode (evt[2:0] = 001b)", "[acf][REQ-ACF-027]") { // The specification's own example: byte_msg_payload = 0x00000002 checks // whether the second IO pin (bit 1) is asserted. const uint8_t payload[4] = {0x00, 0x00, 0x00, 0x02}; @@ -775,7 +804,7 @@ TEST_CASE("compound_wait_match AND-with-1s-mask mode (evt[2:0] = 001b)", "[acf]" REQUIRE(compound_wait_match(0x1, payload, 4, other_bits, 4)); } -TEST_CASE("compound_wait_match AND-with-0s-mask mode (evt[2:0] = 010b)", "[acf]") { +TEST_CASE("compound_wait_match AND-with-0s-mask mode (evt[2:0] = 010b)", "[acf][REQ-ACF-028]") { const uint8_t payload[4] = {0x00, 0x00, 0x00, 0x02}; const uint8_t bit_clear[4] = {0x00, 0x00, 0x00, 0x00}; const uint8_t bit_set[4] = {0x00, 0x00, 0x00, 0x02}; @@ -786,7 +815,7 @@ TEST_CASE("compound_wait_match AND-with-0s-mask mode (evt[2:0] = 010b)", "[acf]" REQUIRE(compound_wait_match(0x2, payload, 4, other_bits_only, 4)); } -TEST_CASE("compound_wait_match leading-quadlet hi-word >= mode (evt[2:0] = 100b)", "[acf]") { +TEST_CASE("compound_wait_match leading-quadlet hi-word >= mode (evt[2:0] = 100b)", "[acf][REQ-ACF-029]") { const uint8_t payload[4] = {0x00, 0x0A, 0x00, 0x00}; // hi word = 10 const uint8_t lower[4] = {0x00, 0x05, 0x00, 0x00}; // hi word = 5 const uint8_t higher[4] = {0x00, 0x0F, 0x00, 0x00}; // hi word = 15 @@ -797,7 +826,7 @@ TEST_CASE("compound_wait_match leading-quadlet hi-word >= mode (evt[2:0] = 100b) REQUIRE(compound_wait_match(0x4, payload, 4, equal, 4)); // 10>=10 } -TEST_CASE("compound_wait_match leading-quadlet hi-word <= mode (evt[2:0] = 101b)", "[acf]") { +TEST_CASE("compound_wait_match leading-quadlet hi-word <= mode (evt[2:0] = 101b)", "[acf][REQ-ACF-052]") { const uint8_t payload[4] = {0x00, 0x0A, 0x00, 0x00}; // hi word = 10 const uint8_t lower[4] = {0x00, 0x05, 0x00, 0x00}; const uint8_t higher[4] = {0x00, 0x0F, 0x00, 0x00}; @@ -808,7 +837,7 @@ TEST_CASE("compound_wait_match leading-quadlet hi-word <= mode (evt[2:0] = 101b) REQUIRE(compound_wait_match(0x5, payload, 4, equal, 4)); // 10<=10 } -TEST_CASE("compound_wait_match leading-quadlet lo-word >= mode (evt[2:0] = 110b)", "[acf]") { +TEST_CASE("compound_wait_match leading-quadlet lo-word >= mode (evt[2:0] = 110b)", "[acf][REQ-ACF-030]") { const uint8_t payload[4] = {0xFF, 0xFF, 0x00, 0x0A}; // lo word = 10 const uint8_t lower[4] = {0xFF, 0xFF, 0x00, 0x05}; // lo word = 5 const uint8_t higher[4] = {0xFF, 0xFF, 0x00, 0x0F}; // lo word = 15 @@ -817,7 +846,7 @@ TEST_CASE("compound_wait_match leading-quadlet lo-word >= mode (evt[2:0] = 110b) REQUIRE_FALSE(compound_wait_match(0x6, payload, 4, higher, 4)); // 10>=15 } -TEST_CASE("compound_wait_match leading-quadlet lo-word <= mode (evt[2:0] = 111b)", "[acf]") { +TEST_CASE("compound_wait_match leading-quadlet lo-word <= mode (evt[2:0] = 111b)", "[acf][REQ-ACF-053]") { const uint8_t payload[4] = {0xFF, 0xFF, 0x00, 0x0A}; // lo word = 10 const uint8_t lower[4] = {0xFF, 0xFF, 0x00, 0x05}; const uint8_t higher[4] = {0xFF, 0xFF, 0x00, 0x0F}; @@ -826,7 +855,7 @@ TEST_CASE("compound_wait_match leading-quadlet lo-word <= mode (evt[2:0] = 111b) REQUIRE(compound_wait_match(0x7, payload, 4, higher, 4)); // 10<=15 } -TEST_CASE("compound_wait_match ge/le modes reject a payload shorter than one quadlet", "[acf]") { +TEST_CASE("compound_wait_match ge/le modes reject a payload shorter than one quadlet", "[acf][REQ-ACF-029][REQ-ACF-030][REQ-ACF-052][REQ-ACF-053]") { const uint8_t payload[3] = {0x00, 0x0A, 0x00}; const uint8_t status[3] = {0x00, 0x00, 0x00}; @@ -836,7 +865,7 @@ TEST_CASE("compound_wait_match ge/le modes reject a payload shorter than one qua REQUIRE_FALSE(compound_wait_match(0x7, payload, 3, status, 3)); } -TEST_CASE("compound_wait_match reserved mode (evt[2:0] = 011b) always returns false", "[acf]") { +TEST_CASE("compound_wait_match reserved mode (evt[2:0] = 011b) always returns false", "[acf][REQ-ACF-051]") { // Callers must gate on compound_wait_evt_valid() first; this pins the // function's own defined (always-false) behavior if they don't. const uint8_t payload[2] = {0x01, 0x02}; @@ -860,7 +889,7 @@ TEST_CASE("reg_write_len fails safe to 0 rather than underflowing", "[acf]") { // ── peek_gbb_request_type (conditional-request modules' shared repurposed- // timestamp-region accessor) ────────────────────────────────────────────── -TEST_CASE("peek_gbb_request_type reads frame[8] for a genuine GBB frame", "[acf]") { +TEST_CASE("peek_gbb_request_type reads frame[8] for a genuine GBB frame", "[acf][REQ-ACF-032]") { AcfMessageInfo info; auto frame = encode_acf_gbb(info, /*message_timestamp=*/0, {}); // Repurpose the timestamp region's first octet, as the conditional- @@ -873,7 +902,7 @@ TEST_CASE("peek_gbb_request_type reads frame[8] for a genuine GBB frame", "[acf] REQUIRE(request_type == 0x0F); } -TEST_CASE("peek_gbb_request_type rejects an ACF_ABB frame outright", "[acf]") { +TEST_CASE("peek_gbb_request_type rejects an ACF_ABB frame outright", "[acf][REQ-ACF-032]") { AcfMessageInfo info; std::vector frame = encode_acf_abb(info, {0x0F}); @@ -882,7 +911,7 @@ TEST_CASE("peek_gbb_request_type rejects an ACF_ABB frame outright", "[acf]") { REQUIRE(request_type == 0xFF); // left unchanged } -TEST_CASE("peek_gbb_request_type rejects a GBB frame too short to hold the request_type octet", "[acf]") { +TEST_CASE("peek_gbb_request_type rejects a GBB frame too short to hold the request_type octet", "[acf][REQ-ACF-032]") { AcfMessageInfo info; auto frame = encode_acf_gbb(info, 0, {}); frame.resize(kAcfCommonHeaderLen); // exactly the 8-byte header, no request_type octet @@ -895,7 +924,7 @@ TEST_CASE("peek_gbb_request_type rejects a GBB frame too short to hold the reque // ── Response builders (ported from c-RCP's rcp_acf_build_error_response()/ // _build_acknowledge_response()/_build_acknowledge_rejected_response()) ────── -TEST_CASE("build_error_response carries byte_bus_id, transaction_num, and the error code", "[acf]") { +TEST_CASE("build_error_response carries byte_bus_id, transaction_num, and the error code", "[acf][REQ-ACF-031][REQ-WIREERR-001]") { auto resp = build_error_response(7, 200, WireErrorCode::ReqStorageOverflow); AcfMessageInfo hdr; @@ -910,7 +939,7 @@ TEST_CASE("build_error_response carries byte_bus_id, transaction_num, and the er REQUIRE(payload[0] == static_cast(WireErrorCode::ReqStorageOverflow)); } -TEST_CASE("build_error_response never classifies as Acknowledge", "[acf]") { +TEST_CASE("build_error_response never classifies as Acknowledge", "[acf][REQ-ACF-031]") { auto resp = build_error_response(1, 1, WireErrorCode::UnsupportedCmd); AcfMessageInfo hdr; @@ -922,7 +951,7 @@ TEST_CASE("build_error_response never classifies as Acknowledge", "[acf]") { } TEST_CASE("build_acknowledge_rejected_response carries byte_bus_id, transaction_num, and the error code", - "[acf]") { + "[acf][REQ-ACF-033][REQ-WIREERR-001]") { auto resp = build_acknowledge_rejected_response(7, 200, WireErrorCode::ReqStorageOverflow); AcfMessageInfo hdr; @@ -940,7 +969,7 @@ TEST_CASE("build_acknowledge_rejected_response carries byte_bus_id, transaction_ } TEST_CASE("build_acknowledge_rejected_response differs from build_error_response only in evt", - "[acf]") { + "[acf][REQ-ACF-033]") { // Pins the distinction directly: same transaction_num/error code, but // the two builders' own responses must classify differently // (Acknowledge vs Error), and only the rejected-acknowledge shape's own @@ -996,3 +1025,58 @@ TEST_CASE("ACF_GBB acf_msg_length/pad wire bits round-trip the specification's o REQUIRE(out.acf_msg_length == 7); REQUIRE(out.pad == 1); } + +// ── acf_category() unique-message-per-errc (ported from c-RCP's +// rcp_acf_strerror()) ────────────────────────────────────────────────────── + +TEST_CASE("acf_category() returns a unique, non-empty message per AcfErrc value", "[acf][REQ-ACF-001]") { + const auto& cat = acf_category(); + const std::string bad_type = cat.message(static_cast(AcfErrc::bad_acf_msg_type)); + const std::string bad_length = cat.message(static_cast(AcfErrc::bad_acf_msg_length)); + REQUIRE_FALSE(bad_type.empty()); + REQUIRE_FALSE(bad_length.empty()); + REQUIRE(bad_type != bad_length); +} + +// ── encode_acf_abb/encode_acf_gbb force the correct acf_msg_type regardless +// of the caller-supplied header (ported from c-RCP's rcp_acf_encode_abb()/ +// _encode_gbb()) ──────────────────────────────────────────────────────────── + +TEST_CASE("encode_acf_abb writes kAcfMsgTypeAbb even if the caller's header claims ACF_GBB", + "[acf][REQ-ACF-004]") { + AcfMessageInfo info; + info.acf_msg_type = kAcfMsgTypeGbb; // deliberately wrong on entry + auto frame = encode_acf_abb(info, {}); + REQUIRE(peek_acf_msg_type(frame.data()) == kAcfMsgTypeAbb); +} + +TEST_CASE("encode_acf_gbb writes kAcfMsgTypeGbb even if the caller's header claims ACF_ABB", + "[acf][REQ-ACF-038]") { + AcfMessageInfo info; + info.acf_msg_type = kAcfMsgTypeAbb; // deliberately wrong on entry + auto frame = encode_acf_gbb(info, 0, {}); + REQUIRE(peek_acf_msg_type(frame.data()) == kAcfMsgTypeGbb); +} + +// ── WireErrorCode numeric values pin TC18 Table 27/30 exactly (ported from +// c-RCP's rcp_wire_error_t) ───────────────────────────────────────────────── + +TEST_CASE("WireErrorCode enumerators carry TC18 Table 27/30's own numeric values", "[acf][REQ-WIREERR-001]") { + REQUIRE(static_cast(WireErrorCode::UnsupportedCmd) == 1); + REQUIRE(static_cast(WireErrorCode::SequencerNotKnown) == 2); + REQUIRE(static_cast(WireErrorCode::UnauthorizedAccess) == 3); + REQUIRE(static_cast(WireErrorCode::LockedMemAccess) == 4); + REQUIRE(static_cast(WireErrorCode::RequestCanceled) == 5); + REQUIRE(static_cast(WireErrorCode::RequestNotFound) == 6); + REQUIRE(static_cast(WireErrorCode::EpError) == 7); + REQUIRE(static_cast(WireErrorCode::EpNotFound) == 8); + REQUIRE(static_cast(WireErrorCode::PwmInNoSignal) == 9); + REQUIRE(static_cast(WireErrorCode::ReqStorageOverflow) == 10); + REQUIRE(static_cast(WireErrorCode::RequestRejected) == 11); + REQUIRE(static_cast(WireErrorCode::PociFailure) == 12); + REQUIRE(static_cast(WireErrorCode::PresentationTimeTooFar) == 13); + REQUIRE(static_cast(WireErrorCode::GptpFail) == 14); + REQUIRE(static_cast(WireErrorCode::InvalidParameter) == 15); + REQUIRE(static_cast(WireErrorCode::ChainAborted) == 16); + REQUIRE(static_cast(WireErrorCode::ChainError) == 17); +} diff --git a/tests/test_avtp.cpp b/tests/test_avtp.cpp index f1584d4..0eea300 100644 --- a/tests/test_avtp.cpp +++ b/tests/test_avtp.cpp @@ -4,6 +4,13 @@ // fusa:test REQ-WIRE-007 // fusa:test REQ-WIRE-011 // fusa:test REQ-WIRE-013 +// fusa:test REQ-AVTP-013 +// fusa:test REQ-AVTP-014 +// fusa:test REQ-AVTP-015 +// fusa:test REQ-AVTP-021 +// fusa:test REQ-AVTP-022 +// fusa:test REQ-AVTP-023 +// fusa:test REQ-AVTP-031 // Tests for rcp/avtp.hpp — the TC18 AVTPDU header-framing half of the wire // codec (ROADMAP.md milestone 44, "Wire Format Core", v2.0.0; split from a @@ -271,7 +278,7 @@ TEST_CASE("TSCF header round-trips sv/version/mr/tu", "[avtp]") { // ── §13.3 tu=1/tu=0 equivalence (REQ-AVTP-023) ──────────────────────────────── -TEST_CASE("TSCF header decode reports tu=1 and tu=0 faithfully", "[avtp]") { +TEST_CASE("TSCF header decode reports tu=1 and tu=0 faithfully", "[avtp][REQ-AVTP-023]") { TscfHeader hdr; hdr.stream_id = make_stream_id(0x02, 1); hdr.tu = true; @@ -344,7 +351,7 @@ TEST_CASE("extend_timestamp: exactly half a period behind prefers the un-wrapped // ── Subtype dispatch & the TSCF-without-time-sync drop rule (ported from // c-RCP's rcp_avtp_peek_subtype()/_should_drop_tscf()/_tscf_reserved_all_zero()) ── -TEST_CASE("peek_subtype reads the first byte", "[avtp]") { +TEST_CASE("peek_subtype reads the first byte", "[avtp][REQ-AVTP-013]") { NtscfHeader hdr; hdr.stream_id = make_stream_id(0x02, 1); auto buf = encode_ntscf_header(hdr); @@ -354,13 +361,13 @@ TEST_CASE("peek_subtype reads the first byte", "[avtp]") { REQUIRE(subtype == kSubtypeNtscf); } -TEST_CASE("peek_subtype rejects an empty buffer", "[avtp]") { +TEST_CASE("peek_subtype rejects an empty buffer", "[avtp][REQ-AVTP-013]") { uint8_t subtype = 0; REQUIRE(peek_subtype(nullptr, 0, subtype)); } TEST_CASE("should_drop_tscf drops a TSCF frame when time sync is unsupported and the policy is Drop", - "[avtp]") { + "[avtp][REQ-AVTP-014]") { // TC18 §11.1's own unconditional wording, and this codec's original // (still default) disposition. REQUIRE(should_drop_tscf(false, kSubtypeTscf, TscfFallback::Drop)); @@ -378,18 +385,18 @@ TEST_CASE("should_drop_tscf never drops an NTSCF frame regardless of policy or t } TEST_CASE("should_drop_tscf with TscfFallback::Ignore does not drop an unsupported-time-sync TSCF frame", - "[avtp]") { + "[avtp][REQ-AVTP-021]") { // TC18 §13.3's own configurable alternative to §11.1's unconditional // wording — same inputs as the Drop-policy test above, only the policy // differs, isolating this behavior from every other case. REQUIRE_FALSE(should_drop_tscf(false, kSubtypeTscf, TscfFallback::Ignore)); } -TEST_CASE("should_drop_tscf: Ignore policy is irrelevant once time sync is supported", "[avtp]") { +TEST_CASE("should_drop_tscf: Ignore policy is irrelevant once time sync is supported", "[avtp][REQ-AVTP-021]") { REQUIRE_FALSE(should_drop_tscf(true, kSubtypeTscf, TscfFallback::Ignore)); } -TEST_CASE("tscf_reserved_all_zero is true for a freshly decoded conformant header", "[avtp]") { +TEST_CASE("tscf_reserved_all_zero is true for a freshly decoded conformant header", "[avtp][REQ-AVTP-031]") { TscfHeader hdr; hdr.stream_id = make_stream_id(0x02, 1); auto buf = encode_tscf_header(hdr); @@ -403,7 +410,7 @@ TEST_CASE("tscf_reserved_all_zero is true for a freshly decoded conformant heade REQUIRE(tscf_reserved_all_zero(decoded)); } -TEST_CASE("tscf_reserved_all_zero is false when reserved0 or reserved1 is nonzero", "[avtp]") { +TEST_CASE("tscf_reserved_all_zero is false when reserved0 or reserved1 is nonzero", "[avtp][REQ-AVTP-031]") { TscfHeader hdr; hdr.reserved0 = 1; REQUIRE_FALSE(tscf_reserved_all_zero(hdr)); @@ -413,7 +420,7 @@ TEST_CASE("tscf_reserved_all_zero is false when reserved0 or reserved1 is nonzer REQUIRE_FALSE(tscf_reserved_all_zero(hdr2)); } -TEST_CASE("decode_tscf_header reads nonzero reserved bytes off the wire", "[avtp]") { +TEST_CASE("decode_tscf_header reads nonzero reserved bytes off the wire", "[avtp][REQ-AVTP-022]") { // Decoding a hand-built wire frame whose own bytes 16-19 are nonzero // (simulating a non-conformant or future-revision sender) proves decode // actually reads the reserved octets off the wire, not merely that a @@ -430,3 +437,19 @@ TEST_CASE("decode_tscf_header reads nonzero reserved bytes off the wire", "[avtp REQUIRE(decoded.reserved1 == 0xAABBu); REQUIRE_FALSE(tscf_reserved_all_zero(decoded)); } + +// ── avtp_category() unique-message-per-errc (ported from c-RCP's +// rcp_avtp_strerror()) ──────────────────────────────────────────────────── + +TEST_CASE("avtp_category() returns a unique, non-empty message per AvtpErrc value", "[avtp][REQ-AVTP-015]") { + const auto& cat = avtp_category(); + const std::string short_buffer = cat.message(static_cast(AvtpErrc::short_buffer)); + const std::string bad_subtype = cat.message(static_cast(AvtpErrc::bad_subtype)); + const std::string length_mismatch = cat.message(static_cast(AvtpErrc::length_mismatch)); + REQUIRE_FALSE(short_buffer.empty()); + REQUIRE_FALSE(bad_subtype.empty()); + REQUIRE_FALSE(length_mismatch.empty()); + REQUIRE(short_buffer != bad_subtype); + REQUIRE(bad_subtype != length_mismatch); + REQUIRE(short_buffer != length_mismatch); +}