diff --git a/.fusa-reqs.json b/.fusa-reqs.json index cce023c..d40a02a 100644 --- a/.fusa-reqs.json +++ b/.fusa-reqs.json @@ -391,7 +391,7 @@ { "id": "REQ-WIRE-001", "title": "NTSCF frame identifies itself by subtype", - "text": "encode_ntscf_frame's output starts with avtp::NTSCF_SUBTYPE (0x82) with the sv bit set, via its delegation to encode_ntscf_header; encode_ntscf_frame rejects an acf_payload exceeding the 11-bit ntscf_data_length field width with Err(InvalidSize)", + "text": "encode_ntscf_frame's output starts with avtp::NTSCF_SUBTYPE (0x82, TC18 v0.5.1_RC §11.1 Figure 6 (NTSCF-Header Version 0, p.22)) with the sv bit set, via its delegation to encode_ntscf_header; encode_ntscf_frame rejects an acf_payload exceeding the 11-bit ntscf_data_length field width (Figure 6, bits 13-23) with Err(InvalidSize)", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -409,7 +409,7 @@ { "id": "REQ-WIRE-003", "title": "NTSCF frame payload is opaque to the composition step", - "text": "encode_ntscf_frame/decode_ntscf_frame carry the ACF payload bytes verbatim without parsing them as any specific ACF message type \u2014 that remains acf::decode_acf_abb/decode_acf_gbb's job on the split payload decode_ntscf_frame returns", + "text": "encode_ntscf_frame/decode_ntscf_frame carry the ACF payload bytes verbatim without parsing them as any specific ACF message type — that remains acf::decode_acf_abb/decode_acf_gbb's job on the split payload decode_ntscf_frame returns", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -418,7 +418,7 @@ { "id": "REQ-WIRE-004", "title": "NTSCF frame length is header length plus payload length", - "text": "encode_ntscf_frame's output is always exactly avtp::NTSCF_HEADER_LEN + acf_payload.len() bytes, with no padding or reserved gap inserted", + "text": "encode_ntscf_frame's output is always exactly avtp::NTSCF_HEADER_LEN + acf_payload.len() bytes, with no padding or reserved gap inserted — TC18 v0.5.1_RC §11.1 Figure 6 (NTSCF-Header Version 0, p.22) places acf_payload_data immediately after stream_id, at octet 12", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -445,7 +445,7 @@ { "id": "REQ-WIRE-007", "title": "NTSCF frame payload size limit", - "text": "encode_ntscf_frame rejects an acf_payload longer than avtp::NTSCF_DATA_LENGTH_MAX (0x07FF, the 11-bit ntscf_data_length field width) with Err(InvalidSize)", + "text": "encode_ntscf_frame rejects an acf_payload longer than avtp::NTSCF_DATA_LENGTH_MAX (0x07FF, the 11-bit ntscf_data_length field width at bits 13-23 of TC18 v0.5.1_RC §11.1 Figure 6 (NTSCF-Header Version 0, p.22)) with Err(InvalidSize)", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -463,7 +463,7 @@ { "id": "REQ-WIRE-009", "title": "NTSCF frame functions never panic on arbitrary/truncated input", - "text": "decode_ntscf_frame never panics for input shorter than avtp::NTSCF_HEADER_LEN (returns Err(ShortFrame)) or for arbitrary byte content at any length, matching the fuzz-style discipline the deleted wire.rs originally established (carried forward by fuzz/fuzz_targets/fuzz_wire_decode.rs, repointed at this function)", + "text": "decode_ntscf_frame never panics for input shorter than avtp::NTSCF_HEADER_LEN (12 octets per TC18 v0.5.1_RC §11.1 Figure 6 (NTSCF-Header Version 0, p.22); returns Err(ShortFrame)) or for arbitrary byte content at any length, matching the fuzz-style discipline the deleted wire.rs originally established (carried forward by fuzz/fuzz_targets/fuzz_wire_decode.rs, repointed at this function)", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1165,7 +1165,7 @@ { "id": "REQ-CFG-005", "title": "JSON and YAML parsing", - "text": "from_json() and from_yaml() parse RcServerConfig (GeneralRegisters, the five \u00a73.7-\u00a73.11 config-table row lists, and initial_state) from text", + "text": "from_json() and from_yaml() parse RcServerConfig (GeneralRegisters, the five §3.7-§3.11 config-table row lists, and initial_state) from text", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1444,7 +1444,7 @@ { "id": "REQ-UDP-004", "title": "Echo-back mismatch rejected", - "text": "UdpTransport::send_acf_abb/send_acf_gbb reject a response whose byte_bus_id does not echo the request's, via acf::verify_echo_back (Err(EpError)) \u2014 the StreamId-addressed replacement for the old Zone-mismatch check", + "text": "UdpTransport::send_acf_abb/send_acf_gbb reject a response whose byte_bus_id does not echo the request's, via acf::verify_echo_back (Err(EpError)) — the StreamId-addressed replacement for the old Zone-mismatch check", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1471,7 +1471,7 @@ { "id": "REQ-UDP-007", "title": "resolve_endpoint rejects an unregistered device endpoint", - "text": "resolve_endpoint returns Err(EpNotFound) when byte_bus_id is not the reserved EP0 address and no endpoint is registered under (stream_id, byte_bus_id) \u2014 including when the same byte_bus_id is registered only under a different stream_id", + "text": "resolve_endpoint returns Err(EpNotFound) when byte_bus_id is not the reserved EP0 address and no endpoint is registered under (stream_id, byte_bus_id) — including when the same byte_bus_id is registered only under a different stream_id", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1588,7 +1588,7 @@ { "id": "REQ-TLS-004", "title": "TlsBridge uses NTSCF/ACF encoding, addressed by StreamId", - "text": "TlsBridge.send_acf_abb()/send_acf_gbb() use avtp::encode_ntscf_frame + acf::encode_acf_abb/encode_acf_gbb, and decode responses via avtp::decode_ntscf_frame + acf::decode_acf_abb/decode_acf_gbb, verified via acf::verify_echo_back \u2014 the same framing crate::udp::UdpTransport uses, over a TlsStream instead of a UDP socket", + "text": "TlsBridge.send_acf_abb()/send_acf_gbb() use avtp::encode_ntscf_frame + acf::encode_acf_abb/encode_acf_gbb, and decode responses via avtp::decode_ntscf_frame + acf::decode_acf_abb/decode_acf_gbb, verified via acf::verify_echo_back — the same framing crate::udp::UdpTransport uses, over a TlsStream instead of a UDP socket", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1732,7 +1732,7 @@ { "id": "REQ-CLI-006", "title": "version --format json", - "text": "The CLI must emit a valid \u00a712.1 JSON document when invoked with version --format json containing tool, version, spec_version, language, runtime fields", + "text": "The CLI must emit a valid §12.1 JSON document when invoked with version --format json containing tool, version, spec_version, language, runtime fields", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1741,7 +1741,7 @@ { "id": "REQ-CLI-007", "title": "capabilities subcommand", - "text": "The CLI must emit a valid \u00a712.2 capabilities JSON document when invoked with capabilities", + "text": "The CLI must emit a valid §12.2 capabilities JSON document when invoked with capabilities", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1750,7 +1750,7 @@ { "id": "REQ-CLI-008", "title": "status --format json", - "text": "The CLI must emit a valid \u00a712.3 status JSON document when invoked with status --format json containing protocol, tool, version, healthy, connected, endpoint, details fields", + "text": "The CLI must emit a valid §12.3 status JSON document when invoked with status --format json containing protocol, tool, version, healthy, connected, endpoint, details fields", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1759,7 +1759,7 @@ { "id": "REQ-CLI-009", "title": "convert --protocol RCP", - "text": "The CLI must accept an rcp.Message JSON (\u00a715.5) on stdin and emit the ToMessage() relay.Message JSON (\u00a715.7.5) on stdout when invoked with convert --protocol RCP, addressing by decimal byte_bus_id; exit 1 on invalid input, exit 2 on wrong/missing --protocol", + "text": "The CLI must accept an rcp.Message JSON (§15.5) on stdin and emit the ToMessage() relay.Message JSON (§15.7.5) on stdout when invoked with convert --protocol RCP, addressing by decimal byte_bus_id; exit 1 on invalid input, exit 2 on wrong/missing --protocol", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1768,7 +1768,7 @@ { "id": "REQ-RELAY-001", "title": "Protocol enum", - "text": "The relay module shall define a Protocol enum matching RELAY spec \u00a73 identifiers, serializing as its integer value", + "text": "The relay module shall define a Protocol enum matching RELAY spec §3 identifiers, serializing as its integer value", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1777,7 +1777,7 @@ { "id": "REQ-RELAY-002", "title": "Message envelope", - "text": "The relay module shall define a Message type matching the RELAY spec \u00a74 universal envelope, with base64-encoded payload", + "text": "The relay module shall define a Message type matching the RELAY spec §4 universal envelope, with base64-encoded payload", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1786,7 +1786,7 @@ { "id": "REQ-RELAY-003", "title": "BackPressurePolicy", - "text": "The relay module shall define a BackPressurePolicy enum (DropNewest/DropOldest/Block) per RELAY spec \u00a714, defaulting to DropNewest", + "text": "The relay module shall define a BackPressurePolicy enum (DropNewest/DropOldest/Block) per RELAY spec §14, defaulting to DropNewest", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1795,7 +1795,7 @@ { "id": "REQ-RELAY-004", "title": "SubscriberOptions", - "text": "The relay module shall define a SubscriberOptions type per RELAY spec \u00a718.3 with a channel_depth default of 64", + "text": "The relay module shall define a SubscriberOptions type per RELAY spec §18.3 with a channel_depth default of 64", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1804,7 +1804,7 @@ { "id": "REQ-RELAY-005", "title": "relay::Error sentinels", - "text": "The relay module shall define the four mandatory RELAY error sentinels (Closed, NotConnected, Timeout, PayloadTooLarge) per \u00a75.1, mutually distinct", + "text": "The relay module shall define the four mandatory RELAY error sentinels (Closed, NotConnected, Timeout, PayloadTooLarge) per §5.1, mutually distinct", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1813,7 +1813,7 @@ { "id": "REQ-RELAY-006", "title": "Context", - "text": "The relay module shall define a Context type carrying an optional deadline per RELAY spec \u00a718.3", + "text": "The relay module shall define a Context type carrying an optional deadline per RELAY spec §18.3", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1822,7 +1822,7 @@ { "id": "REQ-RELAY-007", "title": "relay::Node trait", - "text": "The relay module shall define an async Node trait (protocol/send/subscribe/close) per RELAY spec \u00a710.1", + "text": "The relay module shall define an async Node trait (protocol/send/subscribe/close) per RELAY spec §10.1", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1831,7 +1831,7 @@ { "id": "REQ-RELAY-008", "title": "relay::Caller trait", - "text": "The relay module shall define an async Caller trait extending Node with call() per RELAY spec \u00a710.2", + "text": "The relay module shall define an async Caller trait extending Node with call() per RELAY spec §10.2", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1840,7 +1840,7 @@ { "id": "REQ-RELAY-009", "title": "Base64 payload serde", - "text": "Message and Command/Response/Status payload fields shall serialize as base64 strings per RELAY spec \u00a715.1, matching Go's []byte JSON marshalling", + "text": "Message and Command/Response/Status payload fields shall serialize as base64 strings per RELAY spec §15.1, matching Go's []byte JSON marshalling", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1849,7 +1849,7 @@ { "id": "REQ-ADAPT-006", "title": "AcfAbbMessage response ToMessage()", - "text": "to_message() shall convert an addressed ACF_ABB response to a relay::Message per RELAY spec \u00a715.7.5 (stream_id+byte_bus_id encoded as ID, op surfaced via the rcp.op meta key, payload)", + "text": "to_message() shall convert an addressed ACF_ABB response to a relay::Message per RELAY spec §15.7.5 (stream_id+byte_bus_id encoded as ID, op surfaced via the rcp.op meta key, payload)", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1858,7 +1858,7 @@ { "id": "REQ-ADAPT-007", "title": "Message.FromMessage() to AcfAbbMessage", - "text": "from_message() shall convert a relay::Message to an addressed ACF_ABB request per RELAY spec \u00a715.7.5 (stream_id+byte_bus_id decoded from ID, op/read_size from meta keys), returning InvalidParameter for a malformed ID or an unrecognized rcp.op value", + "text": "from_message() shall convert a relay::Message to an addressed ACF_ABB request per RELAY spec §15.7.5 (stream_id+byte_bus_id decoded from ID, op/read_size from meta keys), returning InvalidParameter for a malformed ID or an unrecognized rcp.op value", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1867,7 +1867,7 @@ { "id": "REQ-ADAPT-008", "title": "AcfAbbMessage response.ToMessage() reply half", - "text": "response_to_message() shall convert an addressed ACF_ABB response to a relay::Message per RELAY spec \u00a715.7.5 (Caller.Call reply half), matching to_message()'s own conversion", + "text": "response_to_message() shall convert an addressed ACF_ABB response to a relay::Message per RELAY spec §15.7.5 (Caller.Call reply half), matching to_message()'s own conversion", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1876,7 +1876,7 @@ { "id": "REQ-ADAPT-009", "title": "Adapt()", - "text": "adapt() shall wrap an RcServer as a relay::Node/Caller per RELAY spec \u00a710.3, addressed by (StreamId, byte_bus_id), dispatching blocking RcServer calls via spawn_blocking, honoring an already-expired Context as Timeout, tracking its own closed state since RcServer has no open/closed connection concept of its own, and returning an immediately-closed channel from subscribe() since RcServer has no live-notification mechanism to forward", + "text": "adapt() shall wrap an RcServer as a relay::Node/Caller per RELAY spec §10.3, addressed by (StreamId, byte_bus_id), dispatching blocking RcServer calls via spawn_blocking, honoring an already-expired Context as Timeout, tracking its own closed state since RcServer has no open/closed connection concept of its own, and returning an immediately-closed channel from subscribe() since RcServer has no live-notification mechanism to forward", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1885,7 +1885,7 @@ { "id": "REQ-ADAPT-010", "title": "Caller.Call()", - "text": "The Adapt() adapter's call() shall dispatch an addressed ACF_ABB request derived from the request Message against the wrapped RcServer and return its response as a relay::Message per RELAY spec \u00a710.2/\u00a715.7.5", + "text": "The Adapt() adapter's call() shall dispatch an addressed ACF_ABB request derived from the request Message against the wrapped RcServer and return its response as a relay::Message per RELAY spec §10.2/§15.7.5", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1903,7 +1903,7 @@ { "id": "REQ-NTSCF-001", "title": "NtscfHeader field model", - "text": "NtscfHeader models sequence_num, ntscf_data_length, and stream_id for the NTSCF AVTPDU header variant", + "text": "NtscfHeader models sequence_num, ntscf_data_length, and stream_id for the NTSCF AVTPDU header variant — the three named fields TC18 v0.5.1_RC §11.1 Figure 6 (NTSCF-Header Version 0, p.22) defines, with no others.", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1912,7 +1912,7 @@ { "id": "REQ-NTSCF-002", "title": "NTSCF header encode/decode round-trip", - "text": "encode_ntscf_header(hdr) followed by decode_ntscf_header() recovers the original header, and out-of-range ntscf_data_length is rejected with InvalidSize", + "text": "encode_ntscf_header emits the exact 12-octet layout of TC18 v0.5.1_RC §11.1 Figure 6 (NTSCF-Header Version 0, p.22): octet 0 subtype, octet 1 sv(1)|version(3)|r(1)|ntscf_data_length[10:8](3), octet 2 ntscf_data_length[7:0], octet 3 sequence_num, octets 4-11 stream_id big-endian — with no reserved gap anywhere. decode_ntscf_header of that output recovers the original header, and an ntscf_data_length above the 11-bit field width (bits 13-23 of the first quadlet) is rejected with InvalidSize.", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1921,7 +1921,7 @@ { "id": "REQ-NTSCF-003", "title": "NTSCF subtype and sv bit", - "text": "Encoded NTSCF headers carry subtype 0x82 and set the sv bit, identifying the AVTPDU as NTSCF with a valid stream_id", + "text": "Encoded NTSCF headers carry subtype 0x82 and set the sv bit, identifying the AVTPDU as NTSCF with a valid stream_id. Both values are read from TC18 v0.5.1_RC §11.1 Figure 6 (NTSCF-Header Version 0, p.22) ('subtype(0x82)', bits 0-7; 'sv', bit 8) and confirmed by the worked example in TC18 Figure 20 (p.79).", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1930,7 +1930,7 @@ { "id": "REQ-NTSCF-004", "title": "NTSCF decode rejects malformed input", - "text": "decode_ntscf_header rejects frames shorter than 16 bytes, wrong subtype, or an unset sv bit", + "text": "decode_ntscf_header rejects frames shorter than avtp::NTSCF_HEADER_LEN (12 octets, the header length TC18 v0.5.1_RC §11.1 Figure 6 (NTSCF-Header Version 0, p.22) specifies), a subtype other than 0x82, or an unset sv bit.", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1957,7 +1957,7 @@ { "id": "REQ-TSCF-001", "title": "TscfHeader field model", - "text": "TscfHeader models sequence_num, avtp_timestamp, stream_data_length, and stream_id for the TSCF AVTPDU header variant", + "text": "TscfHeader models sequence_num, avtp_timestamp, stream_data_length, and stream_id for the TSCF AVTPDU header variant — the four value-carrying fields TC18 v0.5.1_RC §11.1 Figure 5 (TSCF-Header Version 0, p.22) defines. The figure's tv bit is derived from avtp_timestamp rather than stored, and its mr/tu/rsv/reserved bits are transmitted as zero.", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1966,7 +1966,7 @@ { "id": "REQ-TSCF-002", "title": "TSCF header encode/decode round-trip", - "text": "encode_tscf_header(hdr) followed by decode_tscf_header() recovers the original header, and out-of-range stream_data_length is rejected with InvalidSize", + "text": "encode_tscf_header emits the exact 24-octet layout of TC18 v0.5.1_RC §11.1 Figure 5 (TSCF-Header Version 0, p.22): octet 0 subtype, octet 1 sv(1)|version(3)|mr(1)|rsv(2)|tv(1), octet 2 sequence_num, octet 3 reserved(7)|tu(1), octets 4-11 stream_id, octets 12-15 avtp_timestamp, octets 16-19 reserved, octets 20-21 stream_data_length as a full 16-bit big-endian field, octets 22-23 reserved. decode_tscf_header of that output recovers the original header. Because stream_data_length's wire field is a full 16 bits (avtp::TSCF_DATA_LENGTH_MAX == u16::MAX), every representable value is in range and the call never returns InvalidSize.", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1975,7 +1975,7 @@ { "id": "REQ-TSCF-003", "title": "TSCF subtype and sv bit", - "text": "Encoded TSCF headers carry subtype 0x83 and set the sv bit, identifying the AVTPDU as TSCF with a valid stream_id, distinct from the NTSCF subtype", + "text": "Encoded TSCF headers carry subtype 0x05 and set the sv bit, identifying the AVTPDU as TSCF with a valid stream_id, distinct from the NTSCF subtype. The value is read from TC18 v0.5.1_RC §11.1 Figure 5 (TSCF-Header Version 0, p.22) ('subtype(0x05)', bits 0-7) and confirmed by the worked example in TC18 Figure 19 (p.79); it is unrelated to NTSCF's 0x82 and is the smaller of the two. The tv bit (Figure 5, bit 15) is set exactly when avtp_timestamp is non-zero.", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -1984,7 +1984,7 @@ { "id": "REQ-TSCF-004", "title": "TSCF decode rejects malformed input", - "text": "decode_tscf_header rejects frames shorter than 24 bytes, wrong subtype, or an unset sv bit", + "text": "decode_tscf_header rejects frames shorter than avtp::TSCF_HEADER_LEN (24 octets, the header length TC18 v0.5.1_RC §11.1 Figure 5 (TSCF-Header Version 0, p.22) specifies), a subtype other than 0x05, or an unset sv bit.", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -2631,8 +2631,8 @@ }, { "id": "REQ-RMAP-008", - "title": "GeneralRegisters models the full \u00a73.6 general register-map field set", - "text": "GeneralRegisters carries the six ROADMAP.md-quoted fields (svr_oa_tc18_magic_nr, svr_version, svr_vendor_id, svr_device_id, svr_ep_count, svr_implemented_options) plus the remaining rows of this crate's own \u00a73.6 table extraction, each independently settable/readable, defaults to an all-zero value, and reports CATEGORY == lifecycle::RegisterCategory::General", + "title": "GeneralRegisters models the full §3.6 general register-map field set", + "text": "GeneralRegisters carries the six ROADMAP.md-quoted fields (svr_oa_tc18_magic_nr, svr_version, svr_vendor_id, svr_device_id, svr_ep_count, svr_implemented_options) plus the remaining rows of this crate's own §3.6 table extraction, each independently settable/readable, defaults to an all-zero value, and reports CATEGORY == lifecycle::RegisterCategory::General", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -2667,7 +2667,7 @@ }, { "id": "REQ-RMAP-012", - "title": "HwPinMappingEntry models the \u00a73.7 per-pin mapping row", + "title": "HwPinMappingEntry models the §3.7 per-pin mapping row", "text": "HwPinMappingEntry carries hw_ep_nr, hw_ep_pin_nr, and an undecomposed hw_pin_props byte, and reports CATEGORY == lifecycle::RegisterCategory::HwConfig", "standard": "iso26262", "level": "HLR", @@ -2694,8 +2694,8 @@ }, { "id": "REQ-RMAP-015", - "title": "RequestStreamConfigEntry models the \u00a73.8 per-stream config row", - "text": "RequestStreamConfigEntry carries rx_stream_id and the remaining fifteen per-stream fields this crate's own \u00a73.8 extraction names, and reports CATEGORY == lifecycle::RegisterCategory::RcpConfig", + "title": "RequestStreamConfigEntry models the §3.8 per-stream config row", + "text": "RequestStreamConfigEntry carries rx_stream_id and the remaining fifteen per-stream fields this crate's own §3.8 extraction names, and reports CATEGORY == lifecycle::RegisterCategory::RcpConfig", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -2721,7 +2721,7 @@ }, { "id": "REQ-RMAP-018", - "title": "EpByteBusIdMapEntry models the \u00a73.9 stream/byte_bus_id-to-endpoint row and its end-of-table sentinel", + "title": "EpByteBusIdMapEntry models the §3.9 stream/byte_bus_id-to-endpoint row and its end-of-table sentinel", "text": "EpByteBusIdMapEntry carries map_stream_index, map_byte_bus_id, and map_ep_nr, reports CATEGORY == lifecycle::RegisterCategory::RcpConfig, and is_end_of_table returns true iff map_stream_index == END_OF_TABLE_STREAM_INDEX (0); this module defines no ascending-order validation or enforcement over a table's rows", "standard": "iso26262", "level": "HLR", @@ -2748,7 +2748,7 @@ }, { "id": "REQ-RMAP-021", - "title": "ResponseStreamConfigEntry models the \u00a73.10 response/ack queue config row", + "title": "ResponseStreamConfigEntry models the §3.10 response/ack queue config row", "text": "ResponseStreamConfigEntry carries resp_stream_uid, resp_max_avtpdu_size, resp_queue_size, resp_flush_on_count, and resp_flush_time, and reports CATEGORY == lifecycle::RegisterCategory::RcpConfig", "standard": "iso26262", "level": "HLR", @@ -2775,7 +2775,7 @@ }, { "id": "REQ-RMAP-024", - "title": "SequencerStateEntry models the \u00a73.11 per-sequencer state register and its power-on default", + "title": "SequencerStateEntry models the §3.11 per-sequencer state register and its power-on default", "text": "SequencerStateEntry carries seq_state, reports CATEGORY == lifecycle::RegisterCategory::RcpConfig, and power_on_default() returns seq_state == 1", "standard": "iso26262", "level": "HLR", @@ -3927,7 +3927,7 @@ }, { "id": "REQ-BUNDLE-001", - "title": "check_compound_bundle_claim requires compound-wait support, \u22654 sequencers, and clear-non-safestate support together before accepting a \"compound request support\" claim", + "title": "check_compound_bundle_claim requires compound-wait support, ≥4 sequencers, and clear-non-safestate support together before accepting a \"compound request support\" claim", "text": "check_compound_bundle_claim(has_compound_wait, svr_sequencers_max, has_clear_non_safestate) returns Ok(()) only when has_compound_wait is true, svr_sequencers_max is greater than or equal to MIN_SEQUENCERS_FOR_COMPOUND_BUNDLE, and has_clear_non_safestate is true, all three simultaneously; returns Err(RcpError::InvalidParameter) whenever any one of the three does not hold, including when none of them hold; never panics for any sampled input", "standard": "iso26262", "level": "HLR", @@ -3936,7 +3936,7 @@ }, { "id": "REQ-BUNDLE-002", - "title": "MIN_SEQUENCERS_FOR_COMPOUND_BUNDLE fixes the \"\u22654 sequencers\" threshold check_compound_bundle_claim enforces", + "title": "MIN_SEQUENCERS_FOR_COMPOUND_BUNDLE fixes the \"≥4 sequencers\" threshold check_compound_bundle_claim enforces", "text": "MIN_SEQUENCERS_FOR_COMPOUND_BUNDLE equals 4; check_compound_bundle_claim rejects every svr_sequencers_max value strictly below MIN_SEQUENCERS_FOR_COMPOUND_BUNDLE (with the other two prerequisites otherwise satisfied) and accepts every svr_sequencers_max value at or above it, up to and including u8::MAX; never panics for any sampled svr_sequencers_max value", "standard": "iso26262", "level": "HLR", @@ -4864,7 +4864,7 @@ { "id": "REQ-CONF-001", "title": "NTSCF header golden vector", - "text": "conformance::golden::NTSCF_GOLDEN_BYTES is a frozen literal byte array; avtp::encode_ntscf_header(conformance::golden::ntscf_header_fields()) must equal it, and avtp::decode_ntscf_header of it must recover ntscf_header_fields() unchanged", + "text": "conformance::golden::NTSCF_GOLDEN_BYTES is a frozen literal byte array whose 12 octets are derived by hand from TC18 v0.5.1_RC §11.1 Figure 6 (NTSCF-Header Version 0, p.22) and cross-checked against the worked example in TC18 Figure 20 (p.79), not captured from this crate's own encoder; avtp::encode_ntscf_header(conformance::golden::ntscf_header_fields()) must equal it, and avtp::decode_ntscf_header of it must recover ntscf_header_fields() unchanged", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -4873,7 +4873,7 @@ { "id": "REQ-CONF-002", "title": "TSCF header golden vector with non-degenerate avtp_timestamp", - "text": "conformance::golden::TSCF_GOLDEN_BYTES is a frozen literal byte array for a TscfHeader whose avtp_timestamp is non-zero; avtp::encode_tscf_header(conformance::golden::tscf_header_fields()) must equal it, and avtp::decode_tscf_header of it must recover tscf_header_fields() unchanged", + "text": "conformance::golden::TSCF_GOLDEN_BYTES is a frozen literal byte array for a TscfHeader whose avtp_timestamp is non-zero, whose 24 octets are derived by hand from TC18 v0.5.1_RC §11.1 Figure 5 (TSCF-Header Version 0, p.22) and cross-checked against the worked example in TC18 Figure 19 (p.79), not captured from this crate's own encoder; avtp::encode_tscf_header(conformance::golden::tscf_header_fields()) must equal it, and avtp::decode_tscf_header of it must recover tscf_header_fields() unchanged", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -4900,7 +4900,7 @@ { "id": "REQ-CONF-005", "title": "Composed NTSCF+ACF_ABB frame golden vector", - "text": "conformance::golden::NTSCF_ACF_ABB_FRAME_GOLDEN_BYTES is a frozen literal byte array combining ntscf_header_fields() and the encoded acf_abb_fields() message via avtp::encode_ntscf_frame; encoding must equal it, and avtp::decode_ntscf_frame of it must split back into the original header and ACF_ABB payload bytes", + "text": "conformance::golden::NTSCF_ACF_ABB_FRAME_GOLDEN_BYTES is a frozen literal byte array combining ntscf_header_fields() and the encoded acf_abb_fields() message via avtp::encode_ntscf_frame; its 24 octets are the concatenation of the spec-derived NTSCF_GOLDEN_BYTES (TC18 v0.5.1_RC §11.1 Figure 6, p.22) and ACF_ABB_GOLDEN_BYTES (TC18 §11.2.1 Figure 7 / Table 4, p.24), with nothing between them because Figure 6 places acf_payload_data immediately after stream_id; encoding must equal it, and avtp::decode_ntscf_frame of it must split back into the original header and ACF_ABB payload bytes", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -4918,7 +4918,7 @@ { "id": "REQ-CRC-012", "title": "finalize_crc_trailer places the CRC32 trailer after pad, matching TC18's real wire order", - "text": "finalize_crc_trailer(frame, crc), given an already-encoded CRC-free ACF_ABB/ACF_GBB frame built from the real payload alone, bumps that frame's own byte_message_info.acf_msg_length by CRC_TRAILER_QUADLETS (one quadlet) and appends crc's 4 big-endian octets after the encoder's own already-correctly-placed pad octets, producing the wire order header (+ message_timestamp), payload, pad, CRC \u2014 never payload, CRC, pad, which is what a caller gets by concatenating payload and CRC bytes into one blob before calling encode_acf_abb/encode_acf_gbb; returns Err(RcpError::ShortFrame) for a frame shorter than the byte_message_info header and Err(RcpError::InvalidSize) when bumping acf_msg_length would overflow its 9-bit field", + "text": "finalize_crc_trailer(frame, crc), given an already-encoded CRC-free ACF_ABB/ACF_GBB frame built from the real payload alone, bumps that frame's own byte_message_info.acf_msg_length by CRC_TRAILER_QUADLETS (one quadlet) and appends crc's 4 big-endian octets after the encoder's own already-correctly-placed pad octets, producing the wire order header (+ message_timestamp), payload, pad, CRC — never payload, CRC, pad, which is what a caller gets by concatenating payload and CRC bytes into one blob before calling encode_acf_abb/encode_acf_gbb; returns Err(RcpError::ShortFrame) for a frame shorter than the byte_message_info header and Err(RcpError::InvalidSize) when bumping acf_msg_length would overflow its 9-bit field", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", @@ -4963,7 +4963,7 @@ { "id": "REQ-L2-001", "title": "ETHERTYPE_AVTP names the real IEEE 1722 EtherType and is placed big-endian in the Ethernet header", - "text": "ETHERTYPE_AVTP is 0x22F0, matching TC18 \u00a710.1 (\u201can AVTPDU is marked by an EtherType value of 0x22F0\u201d); encode_ethernet_frame writes it as bytes 12-13 of the frame in big-endian order, and decode_ethernet_frame reads bytes 12-13 the same way when validating a frame's EtherType", + "text": "ETHERTYPE_AVTP is 0x22F0, matching TC18 §10.1 (“an AVTPDU is marked by an EtherType value of 0x22F0”); encode_ethernet_frame writes it as bytes 12-13 of the frame in big-endian order, and decode_ethernet_frame reads bytes 12-13 the same way when validating a frame's EtherType", "standard": "iso26262", "level": "HLR", "asil": "ASIL-B", diff --git a/CHANGELOG.md b/CHANGELOG.md index ea14524..0176571 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,101 @@ # Changelog -All notable changes to rust-RCP are documented here. Entries are grouped by -the roadmap milestone that produced them (see `ROADMAP.md`), since this -crate's `Cargo.toml` version does not move until the OPEN Alliance TC18 -core replacement reaches `v1.0.0`. +All notable changes to rust-RCP are documented here. Entries below `v1.0.0` +are grouped by the roadmap milestone that produced them (see `ROADMAP.md`), +because `Cargo.toml`'s version was deliberately held still for the whole +OPEN Alliance TC18 core replacement; from `v1.0.0` on, each entry is a real +release. See `docs/SEMVER.md` for the versioning scheme, including why a +wire-format change is a MAJOR bump even when it is a fix. + +## v4.0.0 (2026-07-31 TC18-conformant NTSCF/TSCF AVTPDU header) — closed + +**Breaking, on the wire.** `src/avtp.rs`'s NTSCF and TSCF header +encode/decode were never reconciled against the specification — that +module's own provenance note said as much, calling `TSCF_SUBTYPE` and "the +header's total length" this crate's "own placeholder values pending that +reconciliation" — and both were wrong. This matters more than the +comparable `v3.0.0` ACF finding did: TC18 §12.2 lists "NTSCF header +processing" as the first of exactly four **mandatory** features, and every +transport this crate ships (`udp`, `l2`, `shmem`, `tlstransport`, `mock`) +frames through it unconditionally. Every NTSCF/TSCF frame rust-RCP has ever +emitted was malformed; no release before this one could have interoperated +with a conformant RC Server, and there is correspondingly no +backward-compatibility constraint to preserve. + +Verified against the specification's own **normative** field diagrams, +§11.1 "Usage of IEEE1722 for RCP", page 22 — Figure 5 "TSCF-Header Version +0" and Figure 6 "NTSCF-Header Version 0" — and cross-checked against the +worked examples on page 79, Figure 19 (ACF_ABB under TSCF, +`stream_data_length(octets) = 0x003C`) and Figure 20 (ACF_GBB under NTSCF, +`ntscf_data_length = 0x038`). All four are vector images with no +extractable text layer in the source PDF; the bit-boundary tick marks were +counted from a 600 dpi render of both pages. + +- **rust-RCP-H01 (BREAKING):** `avtp::NTSCF_HEADER_LEN` is **12**, was 16. + Figure 6's NTSCF header is exactly three quadlets — one packed quadlet + plus a 64-bit `stream_id` — with `acf_payload_data` starting immediately + at octet 12. The previous layout inserted three fabricated reserved + octets between the length field and `stream_id` that the specification + does not have. +- **rust-RCP-H02 (BREAKING):** NTSCF's first quadlet is reordered to + Figure 6's actual field order. `ntscf_data_length` (11 bits) sits at bits + 13-23, i.e. *before* `sequence_num` (bits 24-31) — packed as the low 3 + bits of octet 1 followed by all of octet 2. The previous layout put + `sequence_num` at octet 2 and split the length across octets 3-4 with its + low 3 bits left-justified into octet 4's top 3 bits. Neither the order + nor the packing was right. +- **rust-RCP-H03 (BREAKING):** `avtp::TSCF_SUBTYPE` is **`0x05`**, was + `0x83`. Figures 5 and 19 both give `subtype(0x05)`. `0x83` was invented + as "`NTSCF_SUBTYPE` plus one"; the two subtypes are unrelated IEEE 1722 + code points and TSCF's is the smaller. +- **rust-RCP-H04 (BREAKING):** TSCF's field positions are corrected + throughout to Figure 5's six quadlets — `stream_id` at octets 4-11 (was + 8-15), `avtp_timestamp` at 12-15 (was 16-19), a reserved quadlet at + 16-19, and `stream_data_length` at 20-21 (was split across octets 3-4). + The 24-octet *total* was coincidentally right; nothing inside it was. +- **rust-RCP-H05 (BREAKING):** `avtp::TSCF_DATA_LENGTH_MAX` is + **`0xFFFF`**, was `0x07FF`. Figure 5 gives `stream_data_length` a full + 16-bit half-quadlet of its own; only NTSCF's `ntscf_data_length` is + 11 bits. `encode_tscf_header` previously rejected every legal length from + 2048 upward with `InvalidSize`, and now accepts the whole `u16` range — + it can no longer fail, though it keeps its `Result` return for symmetry + with `encode_ntscf_header` and for future validation headroom. +- **rust-RCP-H06:** `encode_tscf_header` now emits Figure 5's `tv` + ("timestamp valid") bit at bit 15, derived from `avtp_timestamp` being + non-zero — the same all-zero-is-untimed sentinel `timestamp::AvtpTimestamp` + already defines. Previously that bit was always transmitted as zero, so + every TSCF frame this crate sent declared its own timestamp invalid. +- **rust-RCP-H07 (test quality):** `conformance::golden`'s NTSCF/TSCF/frame + byte arrays were *captured from this crate's own encoder output*, which + made them tautological — they could only ever catch drift away from + whatever the encoder did first, never that it was wrong to begin with, + and in practice they certified all six defects above as correct. Every + golden array is now derived by hand from the TC18 figures, and each + constant's doc comment carries an octet-by-octet derivation table naming + the exact figure, page, and field. New spec-anchored tests + `ntscf_header_matches_figure_20_worked_example`, + `tscf_header_matches_figure_19_worked_example` and + `tscf_tv_bit_tracks_avtp_timestamp_presence` pin the worked examples + directly, mirroring `v3.0.0`'s `acf_*_matches_figure_*` tests. +- **rust-RCP-H08 (docs):** `src/conformance.rs`'s module doc comment + recorded this exact byte divergence against go-RCP (13 vs. 16 octets) and + explicitly declined to resolve it, calling reconciliation "out of scope + for this item". That was the wrong call: it was the real bug. The section + is rewritten as a resolved finding, including the observation that both + implementations *agreed* on the wrong `subtype 0x83` — a cross- + implementation comparison can show that one side is wrong, never which, + and when both agree it cannot show even that. go-RCP's own 13-octet + untimed header is likewise non-conformant and is now recorded as such. +- `.fusa-reqs.json`: `REQ-NTSCF-001..004`, `REQ-TSCF-001..004`, + `REQ-WIRE-001/004/007/009` and `REQ-CONF-001/002/005` are rewritten to + state the real wire format and cite the TC18 figure and page each derives + from, replacing text that specified the wrong constants as correct (e.g. + `REQ-NTSCF-004` read "rejects frames shorter than 16 bytes"). 564/564 + requirements still fully traced. +- `docs/PUBLIC_API.txt`: `encode_ntscf_header`'s return type narrows to + `Result<[u8; 12], RcpError>`. `docs/SEMVER.md` gains an explicit rule + that a wire-format change is a MAJOR bump even when it is a fix, and its + stale "the version does not move until `v1.0.0`" scheme note is retired. ## v3.2.0 (2026-07-31 real UDP socket + new L2 raw-Ethernet transport) — closed diff --git a/Cargo.lock b/Cargo.lock index d3e0c32..a8de7d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -333,7 +333,7 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rcp" -version = "3.2.0" +version = "4.0.0" dependencies = [ "async-trait", "base64", diff --git a/Cargo.toml b/Cargo.toml index f75293e..e098cf4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rcp" -version = "3.2.0" +version = "4.0.0" edition = "2021" rust-version = "1.75" license = "MPL-2.0" diff --git a/docs/PUBLIC_API.txt b/docs/PUBLIC_API.txt index 7232491..3e261fa 100644 --- a/docs/PUBLIC_API.txt +++ b/docs/PUBLIC_API.txt @@ -520,7 +520,7 @@ pub fn rcp::avtp::decode_ntscf_frame(&[u8]) -> core::result::Result<(rcp::avtp:: pub fn rcp::avtp::decode_ntscf_header(&[u8]) -> core::result::Result pub fn rcp::avtp::decode_tscf_header(&[u8]) -> core::result::Result pub fn rcp::avtp::encode_ntscf_frame(rcp::avtp::StreamId, u8, &[u8]) -> core::result::Result, rcp::RcpError> -pub fn rcp::avtp::encode_ntscf_header(&rcp::avtp::NtscfHeader) -> core::result::Result<[u8; 16], rcp::RcpError> +pub fn rcp::avtp::encode_ntscf_header(&rcp::avtp::NtscfHeader) -> core::result::Result<[u8; 12], rcp::RcpError> pub fn rcp::avtp::encode_tscf_header(&rcp::avtp::TscfHeader) -> core::result::Result<[u8; 24], rcp::RcpError> pub fn rcp::avtp::parse_stream_id(u64) -> ([u8; 6], u16) pub fn rcp::avtp::select_header_variant(&[u8], rcp::avtp::TimeSyncCapability) -> core::result::Result diff --git a/docs/SEMVER.md b/docs/SEMVER.md index 3fc6726..c5dd671 100644 --- a/docs/SEMVER.md +++ b/docs/SEMVER.md @@ -7,27 +7,35 @@ that commitment is enforced. ## Versioning scheme -This crate follows [Semantic Versioning 2.0.0](https://semver.org/), with -one repo-specific rule recorded here rather than left implicit: per -`CHANGELOG.md`'s own header note, `Cargo.toml`'s `version` field does not -move while the OPEN Alliance TC18 Remote Control Protocol Specification -v0.5.1_RC uplift (`ROADMAP.md` Milestones 1-10) is in progress — every -milestone's work lands under `CHANGELOG.md`'s `## Unreleased` heading -against the crate's last real release, `v0.3.0`. The version jumps directly -from `0.3.0` to `1.0.0` once Milestone 10's own two remaining checklist -items (this one, and conformance test vectors / interop verification) are -both done and the milestone's Success Criteria are met. This is a -deliberate choice, not an oversight: bumping `0.x` versions incrementally -for each milestone's intentionally breaking rewrite would claim a -stability signal ("this is now a coherent, checked-out point") that -Milestones 1-9's work did not yet provide, since earlier milestones each -built additive, standalone plumbing not yet wired into a live decode -> -route -> dispatch -> encode path (see e.g. `src/mock.rs`'s own doc comment -history). `1.0.0` is the first version number this crate assigns after -that path exists and this stability policy is enforced by CI (see -"Enforcement" below). - -After `v1.0.0`: +This crate follows [Semantic Versioning 2.0.0](https://semver.org/). + +Historically, one repo-specific rule applied: `Cargo.toml`'s `version` +field was held still for the whole OPEN Alliance TC18 Remote Control +Protocol Specification v0.5.1_RC uplift (`ROADMAP.md` Milestones 1-10), +with every milestone's work landing under `CHANGELOG.md`'s `## Unreleased` +heading against the crate's last real release, `v0.3.0`, and the version +jumping straight from `0.3.0` to `1.0.0` when Milestone 10's Success +Criteria were met. That was deliberate rather than an oversight: bumping +`0.x` versions for each milestone's intentionally breaking rewrite would +have claimed a stability signal ("this is now a coherent, checked-out +point") that Milestones 1-9 did not yet provide, since each built +additive, standalone plumbing not yet wired into a live decode -> route -> +dispatch -> encode path. That rule is now **spent** — `v1.0.0` shipped, +and the version has moved normally ever since. + +**Wire-format correctness is a MAJOR-version concern here, not a PATCH +one.** A change to the bytes this crate puts on the wire breaks +interoperation with any peer built against a prior release just as surely +as removing a `pub` item breaks compilation, so it takes a MAJOR bump even +when — as in `v3.0.0` (ACF message layout) and `v4.0.0` (NTSCF/TSCF header +layout) — the change is a *fix* and the previous bytes were nobody's +correct behavior. Calling such a release a PATCH would hide, behind a +version number that promises compatibility, the one thing a downstream +integrator most needs told. (`v3.1.0`, which reordered the E2E CRC +trailer's pad and CRC octets, predates this rule being written down and +shipped as a MINOR; it should have been a MAJOR by the rule above.) + +From `v1.0.0` onward: - **MAJOR**: any change that breaks a Tier 1 or Tier 2 stability guarantee below (removing/renaming a public item, adding a required field to a diff --git a/src/avtp.rs b/src/avtp.rs index 1aa3959..41edc02 100644 --- a/src/avtp.rs +++ b/src/avtp.rs @@ -92,43 +92,74 @@ //! parses or interprets the ACF payload itself; that remains `crate::acf`'s //! job. //! -//! ## Provenance note +//! ## Provenance //! -//! Field names (`ntscf_data_length`, `sequence_num`, `avtp_timestamp`, -//! `stream_data_length`) are taken from this crate's `ROADMAP.md`, which -//! itself cites the OPEN Alliance TC18 Remote Control Protocol -//! Specification v0.5.1_RC by section number only. The specific byte -//! offsets and bit widths implemented below are this crate's own working -//! interpretation of IEEE 1722 AVTPDU control-format framing, not a -//! transcription of that (confidential, OPEN-Members-only) document's text. -//! Per Guiding Principle 5, this is flagged for reconciliation against the -//! specification's behavior (never its prose) before being relied on for -//! interop with a real TC18 RC Server. In particular, `TSCF_SUBTYPE` -//! (`0x83`) and the header's total length are this crate's own placeholder -//! values pending that reconciliation. +//! Every byte offset, bit width and constant below is transcribed from the +//! OPEN Alliance TC18 "Remote Control Specification for Ethernet" +//! v0.5.1_RC, §11.1 "Usage of IEEE1722 for RCP", page 22: +//! +//! - **Figure 5 — TSCF-Header Version 0** (normative), six quadlets: +//! `subtype(0x05)` (bits 0-7), `sv` (8), `version(0x0)` (9-11), `mr` +//! (12), `rsv` (13-14), `tv` (15), `sequence_num` (16-23), `reserved` +//! (24-30), `tu` (31); then `stream_id` (2 quadlets), `avtp_timestamp` +//! (1 quadlet), `reserved` (1 quadlet), and a final quadlet holding +//! `stream_data_length(octets)` (16 bits) + `reserved` (16 bits). +//! Total: 24 octets, immediately followed by `acf_payload_data`. +//! - **Figure 6 — NTSCF-Header Version 0** (normative), three quadlets: +//! `subtype(0x82)` (bits 0-7), `sv` (8), `version(0x0)` (9-11), `r` +//! (12), `ntscf_data_length` (13-23), `sequence_num` (24-31); then +//! `stream_id` (2 quadlets). Total: 12 octets, immediately followed by +//! `acf_payload_data` — there is **no** reserved gap between the first +//! quadlet and `stream_id`, and no `avtp_timestamp`/`reserved` quadlets +//! at all. +//! +//! Both figures are cross-checked against the specification's own worked +//! examples on page 79 — Figure 19 (single ACF_ABB under a TSCF header, +//! `subtype(0x05)`, `stream_data_length(octets) = 0x003C`) and Figure 20 +//! (single ACF_GBB under an NTSCF header, `subtype(0x82)`, +//! `ntscf_data_length = 0x038`) — which reproduce the same field order +//! and widths. Figures 5/6/19/20 are vector images in the source PDF with +//! no extractable text layer; the bit-boundary tick marks were read +//! directly from a 600 dpi render of pages 22 and 79. +//! +//! Note the asymmetry, which is real and not a transcription slip: +//! NTSCF's `ntscf_data_length` is an **11-bit** field packed into the +//! first quadlet, while TSCF's `stream_data_length` is a **16-bit** field +//! occupying its own quadlet. See [`NTSCF_DATA_LENGTH_MAX`] and +//! [`TSCF_DATA_LENGTH_MAX`]. //! //! [`StreamId`]'s split — sender MAC in the upper 48 bits, locally-assigned //! unique-id suffix in the lower 16 bits — follows the widely used IEEE //! 1722 AVTP convention of that name (talker MAC high, per-talker stream -//! discriminant low). It has not been independently reconciled against the -//! OPEN Alliance TC18 Remote Control Protocol Specification's own -//! `stream_id` construction rule, and per Guiding Principle 5 is flagged -//! here as this crate's own working interpretation, not a spec-confirmed -//! fact, pending that reconciliation. +//! discriminant low), and is confirmed by TC18 v0.5.1_RC §12.6.1 Table 16 +//! ("Discovery request") and §12.6.2 Table 17 ("Discovery response"), +//! page 48, which both spell the field out as +//! `stream_id = streamMAC + unique_id` with `streamMAC: 6bytes` and +//! `unique_id = 0x0000` (2 bytes) — MAC first, suffix second, in a 64-bit +//! field. This was previously flagged under Guiding Principle 5 as an +//! unreconciled working interpretation; those two tables are the +//! reconciliation. use crate::RcpError; // ── Constants ───────────────────────────────────────────────────────────────── /// AVTPDU `subtype` value identifying an NTSCF-headed PDU. +/// +/// TC18 v0.5.1_RC §11.1 Figure 6 (p.22), `subtype(0x82)`. pub const NTSCF_SUBTYPE: u8 = 0x82; /// Total NTSCF header length in bytes (up to, but not including, the first /// ACF message). -pub const NTSCF_HEADER_LEN: usize = 16; +/// +/// TC18 v0.5.1_RC §11.1 Figure 6 (p.22): exactly three quadlets — one +/// packed quadlet plus a 64-bit `stream_id` — with `acf_payload_data` +/// starting immediately at octet 12. +pub const NTSCF_HEADER_LEN: usize = 12; -/// `ntscf_data_length` is an 11-bit field; this is its maximum representable -/// value. +/// `ntscf_data_length` is an 11-bit field (TC18 v0.5.1_RC §11.1 Figure 6, +/// p.22, bits 13-23 of the first quadlet); this is its maximum +/// representable value. pub const NTSCF_DATA_LENGTH_MAX: u16 = 0x07FF; // ── NtscfHeader ─────────────────────────────────────────────────────────────── @@ -167,7 +198,17 @@ fn get_u64_be(b: &[u8]) -> u64 { // ── Encode / decode ─────────────────────────────────────────────────────────── -/// Encode an [`NtscfHeader`] to its 16-byte wire representation. +/// Encode an [`NtscfHeader`] to its 12-byte wire representation. +/// +/// Layout, per TC18 v0.5.1_RC §11.1 Figure 6 (p.22): +/// +/// ```text +/// octet 0 : subtype = 0x82 +/// octet 1 : sv(1) | version(3) | r(1) | ntscf_data_length[10:8](3) +/// octet 2 : ntscf_data_length[7:0] +/// octet 3 : sequence_num +/// octets 4-11: stream_id (big-endian u64) +/// ``` /// /// Returns `Err(RcpError::InvalidSize)` if `ntscf_data_length` exceeds the /// 11-bit field width. @@ -179,13 +220,12 @@ pub fn encode_ntscf_header(hdr: &NtscfHeader) -> Result<[u8; NTSCF_HEADER_LEN], let mut buf = [0u8; NTSCF_HEADER_LEN]; buf[0] = NTSCF_SUBTYPE; - buf[1] = 0x80; // sv=1 (stream_id valid), version=000, reserved=0000 - buf[2] = hdr.sequence_num; - // ntscf_data_length (11 bits) = byte[3] (high 8 bits) + top 3 bits of byte[4]. - buf[3] = (hdr.ntscf_data_length >> 3) as u8; - buf[4] = ((hdr.ntscf_data_length & 0x07) as u8) << 5; - // bytes[5..8] reserved, left zeroed. - put_u64_be(&mut buf, 8, hdr.stream_id); + // sv=1 (stream_id valid), version=000, r=0, then ntscf_data_length's + // top 3 bits share this octet's low nibble-and-a-bit. + buf[1] = 0x80 | ((hdr.ntscf_data_length >> 8) as u8 & 0x07); + buf[2] = (hdr.ntscf_data_length & 0x00FF) as u8; + buf[3] = hdr.sequence_num; + put_u64_be(&mut buf, 4, hdr.stream_id); Ok(buf) } @@ -213,11 +253,11 @@ pub fn decode_ntscf_header(b: &[u8]) -> Result { )); } - let sequence_num = b[2]; - let len_hi = u16::from(b[3]); - let len_lo = u16::from(b[4] >> 5); - let ntscf_data_length = (len_hi << 3) | len_lo; - let stream_id = get_u64_be(&b[8..16]); + // ntscf_data_length (11 bits) = low 3 bits of octet 1 (its high bits) + // followed by all of octet 2. TC18 §11.1 Figure 6 (p.22), bits 13-23. + let ntscf_data_length = (u16::from(b[1] & 0x07) << 8) | u16::from(b[2]); + let sequence_num = b[3]; + let stream_id = get_u64_be(&b[4..12]); Ok(NtscfHeader { sequence_num, @@ -229,15 +269,29 @@ pub fn decode_ntscf_header(b: &[u8]) -> Result { // ── TscfHeader ──────────────────────────────────────────────────────────────── /// AVTPDU `subtype` value identifying a TSCF-headed PDU. -pub const TSCF_SUBTYPE: u8 = 0x83; +/// +/// TC18 v0.5.1_RC §11.1 Figure 5 (p.22), `subtype(0x05)`, confirmed by the +/// worked example in Figure 19 (p.79). Note that this is *not* `0x82 | 1` +/// or any other offset from [`NTSCF_SUBTYPE`] — the two subtypes are +/// unrelated code points assigned by IEEE 1722, and TSCF's is the smaller +/// of the two. +pub const TSCF_SUBTYPE: u8 = 0x05; /// Total TSCF header length in bytes (up to, but not including, the first -/// ACF message). Wider than [`NTSCF_HEADER_LEN`] to carry `avtp_timestamp`. +/// ACF message). Wider than [`NTSCF_HEADER_LEN`] to carry `avtp_timestamp` +/// and the two extra reserved/length quadlets. +/// +/// TC18 v0.5.1_RC §11.1 Figure 5 (p.22): six quadlets. pub const TSCF_HEADER_LEN: usize = 24; -/// `stream_data_length` is an 11-bit field; this is its maximum -/// representable value. -pub const TSCF_DATA_LENGTH_MAX: u16 = 0x07FF; +/// `stream_data_length` is a full 16-bit field occupying the first half of +/// TSCF's sixth quadlet (TC18 v0.5.1_RC §11.1 Figure 5, p.22); this is its +/// maximum representable value. +/// +/// This deliberately differs from [`NTSCF_DATA_LENGTH_MAX`]'s 11 bits — +/// the two header variants really do size their length fields +/// differently, so a value legal under TSCF may be too large for NTSCF. +pub const TSCF_DATA_LENGTH_MAX: u16 = 0xFFFF; /// Decoded TSCF AVTPDU header. /// @@ -259,7 +313,8 @@ pub struct TscfHeader { /// (see [`crate::timestamp`]). pub avtp_timestamp: u32, /// Length, in bytes, of the ACF message(s) carried after this header. - /// Valid range is `0..=TSCF_DATA_LENGTH_MAX` (11 bits). + /// The wire field is a full 16 bits wide + /// ([`TSCF_DATA_LENGTH_MAX`]), so every `u16` is representable. pub stream_data_length: u16, /// AVTP `stream_id`, carried as a plain `u64`. See /// [`NtscfHeader::stream_id`]. @@ -268,25 +323,51 @@ pub struct TscfHeader { /// Encode a [`TscfHeader`] to its 24-byte wire representation. /// -/// Returns `Err(RcpError::InvalidSize)` if `stream_data_length` exceeds the -/// 11-bit field width. +/// Layout, per TC18 v0.5.1_RC §11.1 Figure 5 (p.22): +/// +/// ```text +/// octet 0 : subtype = 0x05 +/// octet 1 : sv(1) | version(3) | mr(1) | rsv(2) | tv(1) +/// octet 2 : sequence_num +/// octet 3 : reserved(7) | tu(1) +/// octets 4-11 : stream_id (big-endian u64) +/// octets 12-15: avtp_timestamp (big-endian u32) +/// octets 16-19: reserved +/// octets 20-21: stream_data_length (big-endian u16) +/// octets 22-23: reserved +/// ``` +/// +/// `tv` ("timestamp valid") is derived rather than modeled as a field on +/// [`TscfHeader`]: it is set exactly when `avtp_timestamp` is non-zero, +/// which is the same all-zero-is-untimed sentinel +/// [`crate::timestamp::AvtpTimestamp`] already defines for this crate. A +/// peer's `tv` bit is not surfaced on decode for the same reason — there +/// is no field to surface it into, and the timestamp value itself already +/// carries the distinction. +/// +/// `mr`/`tu` are transmitted as zero: media-clock restart and +/// timestamp-uncertain signalling are base-IEEE-1722 stream concerns with +/// no RCP-level meaning this crate models. +/// +/// Always returns `Ok`. Unlike [`encode_ntscf_header`], there is no +/// oversize-rejection path to take: `stream_data_length`'s wire field is a +/// full 16 bits ([`TSCF_DATA_LENGTH_MAX`] == [`u16::MAX`]), so every value +/// the struct can hold is representable. The `Result` return type is kept +/// for signature symmetry with [`encode_ntscf_header`] and so that adding +/// a future validation rule is not itself a breaking change. // fusa:req REQ-TSCF-002 pub fn encode_tscf_header(hdr: &TscfHeader) -> Result<[u8; TSCF_HEADER_LEN], RcpError> { - if hdr.stream_data_length > TSCF_DATA_LENGTH_MAX { - return Err(RcpError::InvalidSize); - } - let mut buf = [0u8; TSCF_HEADER_LEN]; buf[0] = TSCF_SUBTYPE; - buf[1] = 0x80; // sv=1 (stream_id valid), version=000, reserved=0000 + // sv=1 (stream_id valid), version=000, mr=0, rsv=00, tv per timestamp. + buf[1] = 0x80 | u8::from(hdr.avtp_timestamp != 0); buf[2] = hdr.sequence_num; - // stream_data_length (11 bits) = byte[3] (high 8 bits) + top 3 bits of byte[4]. - buf[3] = (hdr.stream_data_length >> 3) as u8; - buf[4] = ((hdr.stream_data_length & 0x07) as u8) << 5; - // bytes[5..8] reserved, left zeroed. - put_u64_be(&mut buf, 8, hdr.stream_id); - buf[16..20].copy_from_slice(&hdr.avtp_timestamp.to_be_bytes()); - // bytes[20..24] reserved, left zeroed. + buf[3] = 0x00; // reserved(7) | tu=0 + put_u64_be(&mut buf, 4, hdr.stream_id); + buf[12..16].copy_from_slice(&hdr.avtp_timestamp.to_be_bytes()); + // octets 16..20 reserved, left zeroed. + buf[20..22].copy_from_slice(&hdr.stream_data_length.to_be_bytes()); + // octets 22..24 reserved, left zeroed. Ok(buf) } @@ -315,13 +396,13 @@ pub fn decode_tscf_header(b: &[u8]) -> Result { } let sequence_num = b[2]; - let len_hi = u16::from(b[3]); - let len_lo = u16::from(b[4] >> 5); - let stream_data_length = (len_hi << 3) | len_lo; - let stream_id = get_u64_be(&b[8..16]); + let stream_id = get_u64_be(&b[4..12]); let mut ts_bytes = [0u8; 4]; - ts_bytes.copy_from_slice(&b[16..20]); + ts_bytes.copy_from_slice(&b[12..16]); let avtp_timestamp = u32::from_be_bytes(ts_bytes); + // stream_data_length is a full 16-bit big-endian field in its own + // quadlet. TC18 §11.1 Figure 5 (p.22), octets 20-21. + let stream_data_length = u16::from_be_bytes([b[20], b[21]]); Ok(TscfHeader { sequence_num, @@ -647,7 +728,7 @@ mod tests { // fusa:test REQ-NTSCF-004 fn decode_rejects_wrong_subtype() { let mut frame = encode_ntscf_header(&NtscfHeader::default()).unwrap(); - frame[0] = 0x83; // TSCF subtype, not NTSCF + frame[0] = TSCF_SUBTYPE; // the other RCP subtype, not NTSCF assert!(matches!( decode_ntscf_header(&frame), Err(RcpError::Other(_)) @@ -674,13 +755,13 @@ mod tests { stream_id: 0xABCD, }; let mut frame = encode_ntscf_header(&hdr).unwrap(); - // Scribble over version/reserved bits and the reserved quadlet; - // decode must still succeed and recover the same named fields. - frame[1] |= 0x7F; // set everything but sv - frame[4] |= 0x1F; // set the 5 reserved low bits of byte 4 - frame[5] = 0xFF; - frame[6] = 0xFF; - frame[7] = 0xFF; + // Per TC18 v0.5.1_RC §11.1 Figure 6 (p.22), the only bits of this + // header this crate does not model are `version` (bits 9-11) and + // `r` (bit 12) — octet 1's `0x78` mask. Every other bit is a named + // field, so there is nothing else to scribble over: unlike the + // pre-v4.0.0 layout, there is no reserved quadlet after the first + // one. Setting them must not disturb any decoded field. + frame[1] |= 0x78; let decoded = decode_ntscf_header(&frame).unwrap(); assert_eq!(decoded, hdr); } @@ -780,12 +861,22 @@ mod tests { #[test] // fusa:test REQ-TSCF-002 - fn tscf_encode_rejects_oversized_data_length() { - let hdr = TscfHeader { - stream_data_length: TSCF_DATA_LENGTH_MAX + 1, - ..Default::default() - }; - assert_eq!(encode_tscf_header(&hdr), Err(RcpError::InvalidSize)); + fn tscf_accepts_the_full_16_bit_data_length_range() { + // TC18 v0.5.1_RC §11.1 Figure 5 (p.22) gives `stream_data_length` + // its own 16-bit half-quadlet, so — unlike NTSCF's 11-bit + // `ntscf_data_length` — there is no `u16` this field cannot carry + // and no oversize-rejection path to exercise. This is the + // positive-direction check that replaces the rejection test that + // used to live here, back when the field was modeled as 11 bits. + assert_eq!(TSCF_DATA_LENGTH_MAX, u16::MAX); + for len in [0u16, 1, 0x07FF, 0x0800, 0xFFFE, u16::MAX] { + let hdr = TscfHeader { + stream_data_length: len, + ..Default::default() + }; + let frame = encode_tscf_header(&hdr).expect("every u16 is in range"); + assert_eq!(decode_tscf_header(&frame).unwrap().stream_data_length, len); + } } #[test] @@ -796,6 +887,103 @@ mod tests { assert_eq!(frame[1] & 0x80, 0x80, "sv bit must be set for TSCF"); } + // ── Wire layout, pinned to the specification's worked examples ──────── + + #[test] + // fusa:test REQ-NTSCF-003 + fn ntscf_header_matches_figure_20_worked_example() { + // TC18 v0.5.1_RC page 79, Figure 20 ("Single ACF_GBB CRC32 + // mandatory fields"). Its AVTPDU header row reads, left to right: + // subtype(0x82) | sv | version(0x0) | r | + // ntscf_data_length=0x038 | sequence_num_lsb + // followed by a `stream_id` row spanning two quadlets. The example + // does not fix a `sequence_num` or `stream_id` value, so only the + // first three octets are pinned from it; the fourth is this + // test's own input. + let hdr = NtscfHeader { + ntscf_data_length: 0x038, + sequence_num: 0x5A, + stream_id: 0x1122_3344_5566_7788, + }; + let frame = encode_ntscf_header(&hdr).unwrap(); + + assert_eq!(frame[0], 0x82, "subtype(0x82), Figure 20 bits 0-7"); + // sv=1 (bit 8), version=000 (9-11), r=0 (12), + // ntscf_data_length[10:8] = 0x038 >> 8 = 0 (bits 13-15). + assert_eq!(frame[1], 0x80, "Figure 20 bits 8-15"); + assert_eq!( + frame[2], 0x38, + "ntscf_data_length[7:0], Figure 20 bits 16-23" + ); + assert_eq!(frame[3], 0x5A, "sequence_num, Figure 20 bits 24-31"); + assert_eq!( + &frame[4..12], + &0x1122_3344_5566_7788u64.to_be_bytes(), + "stream_id occupies quadlets 1-2 with no reserved gap before it" + ); + assert_eq!(frame.len(), 12, "Figure 20's header is three quadlets"); + } + + #[test] + // fusa:test REQ-TSCF-003 + fn tscf_header_matches_figure_19_worked_example() { + // TC18 v0.5.1_RC page 79, Figure 19 ("single ACF_ABB CRC32 + // mandatory fields"). Its AVTPDU header rows read: + // subtype(0x05) | sv | version(0x0) | mr | rsv | tv | + // sequence_num_lsb | reserved | tu + // stream_id (2 quadlets) / avtp_timestamp / reserved + // stream_data_length(octets) = 0x003C | reserved + let hdr = TscfHeader { + stream_data_length: 0x003C, + sequence_num: 0x5A, + avtp_timestamp: 0x1234_5678, + stream_id: 0x1122_3344_5566_7788, + }; + let frame = encode_tscf_header(&hdr).unwrap(); + + assert_eq!(frame[0], 0x05, "subtype(0x05), Figure 19 bits 0-7"); + // sv=1 (bit 8), version=000 (9-11), mr=0 (12), rsv=00 (13-14), + // tv=1 (15) because avtp_timestamp is non-zero. + assert_eq!(frame[1], 0x81, "Figure 19 bits 8-15"); + assert_eq!(frame[2], 0x5A, "sequence_num, Figure 19 bits 16-23"); + assert_eq!(frame[3], 0x00, "reserved(7)|tu, Figure 19 bits 24-31"); + assert_eq!(&frame[4..12], &0x1122_3344_5566_7788u64.to_be_bytes()); + assert_eq!(&frame[12..16], &0x1234_5678u32.to_be_bytes()); + assert_eq!(&frame[16..20], &[0u8; 4], "Figure 19's reserved quadlet"); + assert_eq!( + &frame[20..22], + &[0x00, 0x3C], + "stream_data_length(octets) = 0x003C, Figure 19's sixth quadlet" + ); + assert_eq!(&frame[22..24], &[0u8; 2], "Figure 19's trailing reserved"); + assert_eq!(frame.len(), 24, "Figure 19's header is six quadlets"); + } + + #[test] + // fusa:test REQ-TSCF-003 + fn tscf_tv_bit_tracks_avtp_timestamp_presence() { + // TC18 v0.5.1_RC §11.1 Figure 5 (p.22) bit 15, `tv`. See + // `encode_tscf_header` for why this crate derives the bit from + // `avtp_timestamp` rather than storing it on `TscfHeader`. + let untimed = encode_tscf_header(&TscfHeader::default()).unwrap(); + assert_eq!( + untimed[1] & 0x01, + 0, + "tv must be clear for a zero timestamp" + ); + + let timed = encode_tscf_header(&TscfHeader { + avtp_timestamp: 1, + ..Default::default() + }) + .unwrap(); + assert_eq!( + timed[1] & 0x01, + 1, + "tv must be set for a non-zero timestamp" + ); + } + #[test] // fusa:test REQ-TSCF-002 fn tscf_and_ntscf_headers_use_distinct_subtypes() { @@ -845,17 +1033,18 @@ mod tests { stream_id: 0xABCD, }; let mut frame = encode_tscf_header(&hdr).unwrap(); - // Scribble over version/reserved bits and the reserved bytes; - // decode must still succeed and recover the same named fields. - frame[1] |= 0x7F; // set everything but sv - frame[4] |= 0x1F; // set the 5 reserved low bits of byte 4 - frame[5] = 0xFF; - frame[6] = 0xFF; - frame[7] = 0xFF; - frame[20] = 0xFF; - frame[21] = 0xFF; - frame[22] = 0xFF; - frame[23] = 0xFF; + // Per TC18 v0.5.1_RC §11.1 Figure 5 (p.22), the bits of this + // header this crate does not surface as decoded fields are + // `version` (9-11), `mr` (12), `rsv` (13-14) and `tv` (15) in + // octet 1; `reserved`+`tu` in octet 3; the whole reserved quadlet + // at octets 16-19; and the reserved half-quadlet at octets 22-23. + // Setting all of them must not disturb any decoded field — + // `stream_data_length` at octets 20-21 in particular must survive + // its neighbours being scribbled on. + frame[1] |= 0x7F; + frame[3] = 0xFF; + frame[16..20].copy_from_slice(&[0xFF; 4]); + frame[22..24].copy_from_slice(&[0xFF; 2]); let decoded = decode_tscf_header(&frame).unwrap(); assert_eq!(decoded, hdr); } @@ -868,14 +1057,17 @@ mod tests { fn tscf_decode_never_panics_on_arbitrary_input() { let inputs: &[&[u8]] = &[ &[], - &[0x83], - &[0x83, 0x80], + &[0x05], + &[0x05, 0x80], &[0xFF; 24], &[0x00; 24], &[ - 0x83, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0x05, 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, ], + &[0x05; 40], + // The pre-v4.0.0 placeholder subtype: no longer recognized, + // and must be rejected rather than panicked on. &[0x83; 40], ]; for input in inputs { diff --git a/src/conformance.rs b/src/conformance.rs index 6655728..786109c 100644 --- a/src/conformance.rs +++ b/src/conformance.rs @@ -17,15 +17,23 @@ //! //! 1. **Golden vectors** — [`golden`]'s fixed byte-array constants, pinned //! as literal values rather than re-derived by calling this crate's own -//! `avtp`/`acf` encoders inside the test that checks them. This is the -//! "self-referential wire-format golden vectors" fallback the roadmap -//! bullet names explicitly: an accidental future change to -//! [`crate::avtp`]'s or [`crate::acf`]'s encode/decode logic that -//! happened to keep round-tripping internally consistent (encode/decode -//! still agree with *each other*) but silently drifted from today's -//! actual wire bytes would still be caught here, because the expected -//! bytes are frozen at the time this module was written, not recomputed -//! from the code under test. +//! `avtp`/`acf` encoders inside the test that checks them. An +//! accidental future change to [`crate::avtp`]'s or [`crate::acf`]'s +//! encode/decode logic that happened to keep round-tripping internally +//! consistent (encode/decode still agree with *each other*) but +//! silently drifted from the specified wire bytes is caught here, +//! because the expected bytes are literals, not recomputed from the +//! code under test. +//! +//! **Every vector's literal bytes are now derived by hand from the OPEN +//! Alliance TC18 v0.5.1_RC figures, and each constant's doc comment +//! names the exact figure, page, and field-by-field derivation.** This +//! is a change of kind, not just of value: these arrays used to be +//! *captured from this crate's own encoder output*, which made them a +//! tautology — they could only ever detect drift away from whatever +//! the encoder happened to do first, never that the encoder was wrong +//! to begin with. That is exactly how the NTSCF/TSCF header defects +//! fixed in `v4.0.0` survived: the golden vectors certified them. //! 2. **Interop cross-check against go-RCP** — this crate's one sibling //! x-RCP implementation that has, per its own `ROADMAP.md` item 59 ("TC18 //! Conformance Cutover & RELAY Re-Certification"), also uplifted to a @@ -38,18 +46,23 @@ //! [`tests::go_rcp_bytes_diverge_from_this_crates_own_encoding`] — an //! assertion that the two byte sequences differ, so a future change that //! accidentally made them agree (or diverge differently) would be -//! noticed rather than silently drift past this record — precisely -//! because reconciling the divergence itself is out of this item's scope -//! (see below). +//! noticed rather than silently drift past this record. The +//! AVTPDU-header portion of that divergence has since been resolved +//! *against the specification* (not against go-RCP) in `v4.0.0`, and +//! the divergence that remains is go-RCP's; see "Resolution" below. //! //! ## Cross-check methodology and result //! -//! [`crate::avtp`] and [`crate::acf`]'s own module doc comments already flag -//! (per Guiding Principle 5) that their byte offsets, field widths, and -//! header lengths are this crate's own working interpretation of IEEE 1722 -//! AVTPDU/ACF framing, pending reconciliation against a real TC18 -//! implementation. This item is that reconciliation attempt for the one -//! sibling implementation currently available. +//! When this cross-check was first written, [`crate::avtp`] and +//! [`crate::acf`]'s own module doc comments flagged (per Guiding +//! Principle 5) that their byte offsets, field widths, and header lengths +//! were this crate's own working interpretation of IEEE 1722 AVTPDU/ACF +//! framing, pending reconciliation against a real TC18 implementation. +//! Both have since been reconciled directly against the specification's +//! own normative field diagrams — [`crate::acf`] in `v3.0.0`, +//! [`crate::avtp`] in `v4.0.0` — so this section is now a record of a +//! *resolved* investigation rather than an open one. See "Resolution" +//! below. //! //! The cross-check was performed by writing a small, standalone Go program //! (not committed to this repository — go-RCP is a separate Go module and @@ -65,40 +78,57 @@ //! rather than re-deriving. //! //! Result: **not byte-identical**, at every one of the four vector types. -//! Per Guiding Principle 5, this divergence is recorded here rather than -//! silently resolved by rewriting either implementation to match the other, -//! or by picking one crate's interpretation as "correct" without a -//! documented reconciliation against the specification's own behavior. -//! Reconciling rust-RCP's byte-level choices in [`crate::avtp`]/ -//! [`crate::acf`] — either against go-RCP's independently-arrived-at -//! choices, or directly against the OPEN Alliance TC18 Remote Control -//! Protocol Specification's actual described behavior — remains out of -//! scope for this item and is left for a follow-up. The specific -//! structural differences found, common to every vector pair: +//! The specific structural differences found, common to every vector pair: //! //! - **Header/message length.** go-RCP's untimed (NTSCF-equivalent) header -//! is 13 bytes; this crate's [`crate::avtp::NtscfHeader`] encodes to -//! [`crate::avtp::NTSCF_HEADER_LEN`] = 16 bytes. go-RCP's timed -//! (TSCF-equivalent) header is 17 bytes; this crate's -//! [`crate::avtp::TscfHeader`] encodes to -//! [`crate::avtp::TSCF_HEADER_LEN`] = 24 bytes. This crate reserves three -//! zero bytes after the sequence/length field (and, for TSCF, four more -//! after the timestamp) that go-RCP's layout does not have at all. +//! is 13 bytes; go-RCP's timed (TSCF-equivalent) header is 17 bytes. +//! This crate's [`crate::avtp::NtscfHeader`] encoded to 16 bytes and +//! [`crate::avtp::TscfHeader`] to 24 bytes when this cross-check was +//! written — this crate reserved three zero bytes after the +//! sequence/length field (and, for TSCF, four more after the timestamp) +//! that go-RCP's layout does not have at all. **Resolved in `v4.0.0`: +//! both of this crate's numbers were wrong, and so is go-RCP's 13.** The +//! specification's own normative diagrams (TC18 v0.5.1_RC §11.1 Figure 6 +//! and Figure 5, page 22) give exactly 12 octets for NTSCF and 24 for +//! TSCF. [`crate::avtp::NTSCF_HEADER_LEN`] is now 12 — the three +//! fabricated reserved bytes are gone; +//! [`crate::avtp::TSCF_HEADER_LEN`]'s 24 was coincidentally the right +//! total, but its internal field *positions* were wrong and are now +//! corrected too. //! - **`data_length`/`ntscf_data_length`/`stream_data_length` field -//! packing.** go-RCP stores its 11-bit-valued length as a plain 16-bit -//! big-endian field (top 5 bits simply unused). This crate splits the -//! same 11 bits across two bytes — the field's top 8 bits in one byte, -//! its low 3 bits left-justified into the top 3 bits of the next — per -//! [`crate::avtp`]'s own provenance note. Same logical 11-bit value, -//! different concrete byte layout. +//! packing and position.** go-RCP stores its 11-bit-valued length as a +//! plain 16-bit big-endian field (top 5 bits simply unused), placed +//! after `sequence_num`. This crate split the same 11 bits across two +//! bytes — the field's top 8 bits in one byte, its low 3 bits +//! left-justified into the top 3 bits of the next — also after +//! `sequence_num`. **Resolved in `v4.0.0`: both were wrong.** Figure 6 +//! puts NTSCF's 11-bit `ntscf_data_length` at bits 13-23, i.e. *before* +//! `sequence_num` (bits 24-31), packed as the low 3 bits of octet 1 +//! followed by all of octet 2. Figure 5 gives TSCF a genuinely +//! different field: a full **16-bit** `stream_data_length` in its own +//! sixth quadlet (octets 20-21), with `sequence_num` at octet 2. +//! - **`subtype` for the timed header.** Not originally listed here as a +//! divergence because both implementations happened to agree on `0x83`. +//! **Both were wrong** — Figures 5 and 19 both give TSCF +//! `subtype(0x05)`, and [`crate::avtp::TSCF_SUBTYPE`] is now `0x05`. +//! Agreement between two implementations is not evidence of +//! correctness; this is the clearest illustration in the whole +//! cross-check of why the comparison had to be made against the +//! specification and not against a sibling. //! - **Timestamp-validity marker.** go-RCP's timed header carries an //! explicit 2-bit `TimestampStatus` marker (missing/valid/invalid/ //! uncertain) inside its flags byte. This crate's -//! [`crate::avtp::TscfHeader`] has no equivalent field at all — timestamp -//! validity is instead modeled entirely by +//! [`crate::avtp::TscfHeader`] had no equivalent field at all — timestamp +//! validity was modeled entirely by //! [`crate::timestamp::AvtpTimestamp`]'s all-zero-is-untimed convention, //! layered on top of the raw `avtp_timestamp` value rather than carried -//! as a separate wire bit. +//! as a separate wire bit. **Partially resolved in `v4.0.0`:** Figure 5 +//! defines a single-bit `tv` ("timestamp valid") flag at bit 15, which +//! [`crate::avtp::encode_tscf_header`] now emits, derived from that same +//! all-zero-is-untimed convention. Neither implementation's +//! *modeling* matches the specification's single bit — go-RCP's marker +//! is two bits wide, this crate's is a derived rather than stored value +//! — but this crate's *bytes* now do. //! - **Message-kind discriminant.** go-RCP tags its two ACF-equivalent //! message kinds `1` (short/no-timestamp) and `2` (long/timestamped). //! This crate uses [`crate::acf::ACF_ABB_MSG_TYPE`] = `0x0E` and @@ -131,6 +161,36 @@ //! of this cross-check where the two independently-arrived-at //! interpretations do coincide. //! +//! ## Resolution +//! +//! This section originally closed by declaring reconciliation "out of +//! scope for this item", on the reasoning that a divergence between two +//! independent readings of the same confidential specification was worth +//! *recording* but not worth *resolving* without the primary source in +//! hand. That was the wrong call, and the AVTPDU-header half of the +//! divergence turned out to be a genuine, mandatory-path defect in this +//! crate rather than a benign implementation difference: TC18 §12.2 lists +//! "NTSCF header processing" as the first of exactly four mandatory +//! features, every transport in this crate (`udp`, `l2`, `shmem`, +//! `tlstransport`, `mock`) frames unconditionally through it, and until +//! `v4.0.0` every frame this crate produced carried three fabricated +//! reserved octets, `sequence_num` and `ntscf_data_length` transposed, +//! and — for TSCF — an invented `subtype`. Nothing this crate emitted +//! could ever have interoperated with a conformant RC Server. +//! +//! What resolved it was reading the specification's own normative field +//! diagrams (§11.1 Figures 5 and 6, page 22) instead of comparing two +//! implementations against each other. The lesson is recorded here rather +//! than in a commit message because it generalises: a cross-implementation +//! byte comparison can tell you that at least one side is wrong, but never +//! which — and when both sides agree (as they did on `subtype 0x83`) it +//! cannot even tell you that much. +//! +//! The remaining entries above — the ACF-level field-set and +//! discriminant differences, and go-RCP's own 13-byte untimed header — +//! are go-RCP's to reconcile; they are recorded here as observations +//! about a sibling implementation, not as open work items for this crate. +//! //! ## Why `#[cfg(test)]` rather than `pub mod` //! //! Every other module in this crate is declared `pub mod` in `lib.rs` and @@ -173,13 +233,29 @@ pub mod golden { } } - /// Golden bytes for [`ntscf_header_fields`], captured from - /// `avtp::encode_ntscf_header` at the time this module was written. + /// Golden bytes for [`ntscf_header_fields`], derived by hand from + /// **OPEN Alliance TC18 v0.5.1_RC §11.1, Figure 6 "NTSCF-Header + /// Version 0", page 22** (the normative field diagram), cross-checked + /// against the worked example in **Figure 20, page 79** + /// (`subtype(0x82)`, `sv`, `version(0x0)`, `r`, + /// `ntscf_data_length=0x038`, `sequence_num_lsb`, then `stream_id`). /// Never recompute this array by calling the encoder — see this /// module's doc comment. - pub const NTSCF_GOLDEN_BYTES: [u8; 16] = [ - 0x82, 0x80, 0x07, 0x01, 0x80, 0x00, 0x00, 0x00, 0x02, 0x42, 0xAC, 0x11, 0x00, 0x02, 0x00, - 0x07, + /// + /// Octet-by-octet derivation from Figure 6's three quadlets: + /// + /// | octet(s) | value | Figure 6 field(s) | + /// |----------|--------|-------------------| + /// | 0 | `0x82` | `subtype` (bits 0-7) | + /// | 1 | `0x80` | `sv`=1 (bit 8), `version`=0 (9-11), `r`=0 (12), `ntscf_data_length[10:8]`=0 (13-15) | + /// | 2 | `0x0C` | `ntscf_data_length[7:0]` = 12 (bits 16-23) | + /// | 3 | `0x07` | `sequence_num` = 0x07 (bits 24-31) | + /// | 4..12 | MAC + suffix | `stream_id` (quadlets 1-2) | + /// + /// `acf_payload_data` begins at octet 12 — Figure 6 shows no reserved + /// gap anywhere in this header. + pub const NTSCF_GOLDEN_BYTES: [u8; 12] = [ + 0x82, 0x80, 0x0C, 0x07, 0x02, 0x42, 0xAC, 0x11, 0x00, 0x02, 0x00, 0x07, ]; /// A [`crate::avtp::TscfHeader`] whose encoding is pinned by @@ -198,12 +274,36 @@ pub mod golden { } } - /// Golden bytes for [`tscf_header_fields`], captured from - /// `avtp::encode_tscf_header`. Never recompute — see this module's doc - /// comment. + /// Golden bytes for [`tscf_header_fields`], derived by hand from + /// **OPEN Alliance TC18 v0.5.1_RC §11.1, Figure 5 "TSCF-Header + /// Version 0", page 22** (the normative field diagram), cross-checked + /// against the worked example in **Figure 19, page 79** + /// (`subtype(0x05)`, `sv`, `version(0x0)`, `mr`, `rsv`, `tv`, + /// `sequence_num_lsb`, `reserved`, `tu`; then `stream_id`, + /// `avtp_timestamp`, a `reserved` quadlet, and + /// `stream_data_length(octets) = 0x003C` + `reserved`). + /// Never recompute — see this module's doc comment. + /// + /// Octet-by-octet derivation from Figure 5's six quadlets: + /// + /// | octet(s) | value | Figure 5 field(s) | + /// |----------|--------|-------------------| + /// | 0 | `0x05` | `subtype` (bits 0-7) — *not* `0x82`-adjacent | + /// | 1 | `0x81` | `sv`=1 (bit 8), `version`=0 (9-11), `mr`=0 (12), `rsv`=00 (13-14), `tv`=1 (15) | + /// | 2 | `0x2A` | `sequence_num` (bits 16-23) | + /// | 3 | `0x00` | `reserved` (24-30), `tu`=0 (31) | + /// | 4..12 | MAC + suffix | `stream_id` (quadlets 1-2) | + /// | 12..16 | `1A 2B 3C 4D` | `avtp_timestamp` (quadlet 3) | + /// | 16..20 | zeros | `reserved` (quadlet 4) | + /// | 20..22 | `00 13` | `stream_data_length(octets)` = 19 (16 bits) | + /// | 22..24 | zeros | `reserved` (16 bits) | + /// + /// `tv` is 1 here because this vector's `avtp_timestamp` is non-zero; + /// see [`crate::avtp::encode_tscf_header`] for why that bit is derived + /// rather than modeled as a struct field. pub const TSCF_GOLDEN_BYTES: [u8; 24] = [ - 0x83, 0x80, 0x2A, 0x02, 0x60, 0x00, 0x00, 0x00, 0x02, 0x42, 0xAC, 0x11, 0x00, 0x03, 0x00, - 0x08, 0x1A, 0x2B, 0x3C, 0x4D, 0x00, 0x00, 0x00, 0x00, + 0x05, 0x81, 0x2A, 0x00, 0x02, 0x42, 0xAC, 0x11, 0x00, 0x03, 0x00, 0x08, 0x1A, 0x2B, 0x3C, + 0x4D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x00, 0x00, ]; /// An [`crate::acf::AcfAbbMessage`] whose encoding is pinned by @@ -300,15 +400,24 @@ pub mod golden { /// Golden bytes for a composed AVTPDU frame — [`ntscf_header_fields`]'s /// header (with its `stream_id`/`sequence_num`) immediately followed by - /// [`acf_abb_fields`]'s encoded ACF_ABB message — captured from - /// `avtp::encode_ntscf_frame`. This is the one vector exercising - /// Milestone 9's frame-composition step + /// [`acf_abb_fields`]'s encoded ACF_ABB message. This is the one vector + /// exercising Milestone 9's frame-composition step /// ([`crate::avtp::encode_ntscf_frame`]/ /// [`crate::avtp::decode_ntscf_frame`]) rather than a bare header or /// message alone. Never recompute — see this module's doc comment. - pub const NTSCF_ACF_ABB_FRAME_GOLDEN_BYTES: [u8; 28] = [ - 0x82, 0x80, 0x07, 0x01, 0x80, 0x00, 0x00, 0x00, 0x02, 0x42, 0xAC, 0x11, 0x00, 0x02, 0x00, - 0x07, 0x1C, 0x03, 0x00, 0x05, 0x80, 0x11, 0x40, 0x04, 0xDE, 0xAD, 0xBE, 0xEF, + /// + /// It is the concatenation, by construction, of + /// [`NTSCF_GOLDEN_BYTES`] (12 octets, derived from TC18 v0.5.1_RC + /// §11.1 Figure 6, p.22) and [`ACF_ABB_GOLDEN_BYTES`] (12 octets, + /// derived from TC18 §11.2.1 Figure 7 / Table 4, p.24) — 24 octets + /// total. Figure 6 places `acf_payload_data` immediately after + /// `stream_id`, so there is nothing between the two halves; the + /// header's `ntscf_data_length` (12) is exactly the ACF half's length. + pub const NTSCF_ACF_ABB_FRAME_GOLDEN_BYTES: [u8; 24] = [ + // NTSCF header (TC18 Figure 6, p.22) + 0x82, 0x80, 0x0C, 0x07, 0x02, 0x42, 0xAC, 0x11, 0x00, 0x02, 0x00, 0x07, + // ACF_ABB message (TC18 Figure 7 / Table 4, p.24) + 0x1C, 0x03, 0x00, 0x05, 0x80, 0x11, 0x40, 0x04, 0xDE, 0xAD, 0xBE, 0xEF, ]; } @@ -328,8 +437,12 @@ pub mod go_rcp_crosscheck { /// }) /// ``` /// — the same `sequence_num`/`stream_id`/logical data-length as - /// [`super::golden::ntscf_header_fields`]. 13 bytes, not 16: see this - /// module's doc comment's "Header/message length" divergence. + /// [`super::golden::ntscf_header_fields`]. 13 bytes; TC18 v0.5.1_RC + /// §11.1 Figure 6 (p.22) specifies 12, which is what + /// [`super::golden::NTSCF_GOLDEN_BYTES`] now is — go-RCP's extra octet + /// is a divergence from the specification, not merely from this crate. + /// See this module's doc comment's "Header/message length" bullet and + /// "Resolution" section. pub const GO_RCP_UNTIMED_HEADER_BYTES: [u8; 13] = [ 0x82, 0x80, 0x07, 0x00, 0x0D, 0x02, 0x42, 0xAC, 0x11, 0x00, 0x02, 0x00, 0x07, ]; @@ -348,8 +461,12 @@ pub mod go_rcp_crosscheck { /// differs by one, 18 here vs. 19 there, an artifact of this /// cross-check's own setup rather than a spec question — it has no /// effect on the structural comparison this module documents). 17 - /// bytes, not 24: see this module's doc comment's "Header/message - /// length" and "Timestamp-validity marker" divergences. + /// bytes; TC18 v0.5.1_RC §11.1 Figure 5 (p.22) specifies 24. Note also + /// the leading `0x83` — the subtype both implementations originally + /// agreed on, and which the specification contradicts: Figure 5 and + /// Figure 19 (p.79) both give `subtype(0x05)`. See this module's doc + /// comment's "Header/message length", "`subtype` for the timed + /// header", and "Timestamp-validity marker" bullets. pub const GO_RCP_TIMED_HEADER_BYTES: [u8; 17] = [ 0x83, 0x84, 0x2A, 0x00, 0x12, 0x02, 0x42, 0xAC, 0x11, 0x00, 0x03, 0x00, 0x08, 0x1A, 0x2B, 0x3C, 0x4D, @@ -546,18 +663,18 @@ mod tests { // The one part of the cross-check that *does* agree: both // implementations place the 6-byte sender MAC in the high-order // stream_id bytes and the 2-byte suffix in the low-order bytes. - // NTSCF-equivalent pair: bytes 8..16 of the rust vector (this - // crate's stream_id field position) against bytes 5..13 of the - // go-RCP vector (go-RCP's own stream_id field position) — the + // NTSCF-equivalent pair: bytes 4..12 of the rust vector (TC18 + // §11.1 Figure 6's stream_id position, p.22) against bytes 5..13 + // of the go-RCP vector (go-RCP's own stream_id position) — the // *positions* differ (documented above), but the 8 stream_id bytes // themselves must be identical. assert_eq!( - &golden::NTSCF_GOLDEN_BYTES[8..16], + &golden::NTSCF_GOLDEN_BYTES[4..12], &go_rcp_crosscheck::GO_RCP_UNTIMED_HEADER_BYTES[5..13], "stream_id bytes no longer agree between rust-RCP and go-RCP" ); assert_eq!( - &golden::TSCF_GOLDEN_BYTES[8..16], + &golden::TSCF_GOLDEN_BYTES[4..12], &go_rcp_crosscheck::GO_RCP_TIMED_HEADER_BYTES[5..13], "stream_id bytes no longer agree between rust-RCP and go-RCP" ); @@ -565,7 +682,7 @@ mod tests { // Cross-checked directly against StreamId's own composition, too. let sid1 = StreamId::new(golden::SENDER_MAC_1, golden::UNIQUE_ID_1); assert_eq!( - &golden::NTSCF_GOLDEN_BYTES[8..16], + &golden::NTSCF_GOLDEN_BYTES[4..12], sid1.to_u64().to_be_bytes() ); }