From d0693007536e93250ddc843a76be636d0e5fb75c Mon Sep 17 00:00:00 2001 From: Matt <47545907+SoundMatt@users.noreply.github.com> Date: Fri, 31 Jul 2026 20:41:16 -0700 Subject: [PATCH] =?UTF-8?q?fix:=20v4.0.0=20=E2=80=94=20TC18-conformant=20N?= =?UTF-8?q?TSCF/TSCF=20AVTPDU=20header=20(BREAKING)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit src/avtp.rs's NTSCF and TSCF header encode/decode were never reconciled against the specification — that module's own provenance note said so outright, 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, so there is 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; the bit-boundary tick marks were counted from a 600 dpi render of both pages. - 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. - 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. - 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 smaller. - 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. - 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 future headroom. - 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 zero, so every TSCF frame this crate sent declared its own timestamp invalid. - 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 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, with 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. - 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, not a benign implementation difference. 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 recorded as such (to be raised upstream, not fixed here). - avtp's remaining Guiding Principle 5 flag on StreamId's MAC/suffix split is also resolved while in the area: TC18 §12.6.1 Table 16 and §12.6.2 Table 17 (p.48) both spell the field out as stream_id = streamMAC + unique_id, with streamMAC 6 bytes and unique_id 2 bytes. - .fusa-reqs.json: REQ-NTSCF-001..004, REQ-TSCF-001..004, REQ-WIRE-001/004/007/009 and REQ-CONF-001/002/005 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. MAJOR release: the wire format changes, and avtp is Tier 1 under docs/SEMVER.md. cargo test (debug and release), clippy -D warnings, fmt, fusa-gap-check and cyber-gap-check all pass locally. Signed-off-by: Matt <47545907+SoundMatt@users.noreply.github.com> --- .fusa-reqs.json | 100 ++++++------ CHANGELOG.md | 100 +++++++++++- Cargo.lock | 2 +- Cargo.toml | 2 +- docs/PUBLIC_API.txt | 2 +- docs/SEMVER.md | 50 +++--- src/avtp.rs | 366 +++++++++++++++++++++++++++++++++----------- src/conformance.rs | 257 ++++++++++++++++++++++--------- 8 files changed, 644 insertions(+), 235 deletions(-) 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() ); }