From 3e75801e02acfdb8e1d61702794306924cd26684 Mon Sep 17 00:00:00 2001 From: Pedro Henrique Penna Date: Wed, 21 Oct 2026 11:05:06 -0300 Subject: [PATCH] nvx-rs: write the NVXS record header with one function encode_outer, in protocol/outer.rs, and nvxs::encode_record, in protocol/nvxs/record.rs, each wrote the 44-byte header of an NVXS record in the same eight statements: the magic, the version, the record type, a zero flags byte, the instance ID, the epoch, the sequence number, and the payload length. So the header's wire layout had a definition in each encoder. A private function of protocol/outer.rs, encode_header, now returns the header as an array, with each field at the offset at which decode_outer_header and the record parser read it, and both encoders append it before the payload. Each encoder keeps its own checks, which differ: encode_outer checks only the payload length of a record of any type, and encode_record everything that RecordParser checks, so neither encoder can call the other. Behavior is unchanged: both encoders write the same bytes after the same checks, into a vector of the same capacity, and encode_record still leaves its output unchanged on an error, since every check runs before its first write. No public item changes. Golden byte tests pin both encoders independently of the codec: those of protocol/tests.rs for encode_outer, and the broker's vectors of every record type in protocol/nvxs/tests/record.rs and the golden attachments of the session tests for encode_record. The managed agent's dispatch tests and the CLI's fake-OpenVMM scenarios run each encoder against the other's decoder, and the hypervisor tests ran the guest agent and the host, built from this change, through OpenVMM's own NVXS broker. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- nvx-rs/src/protocol/nvxs/record.rs | 11 +++------- nvx-rs/src/protocol/outer.rs | 33 ++++++++++++++++++++++-------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/nvx-rs/src/protocol/nvxs/record.rs b/nvx-rs/src/protocol/nvxs/record.rs index 769a9068d..adea7d8ee 100644 --- a/nvx-rs/src/protocol/nvxs/record.rs +++ b/nvx-rs/src/protocol/nvxs/record.rs @@ -2,6 +2,7 @@ use std::fmt; +use crate::protocol::outer::encode_header; use crate::protocol::{ CAPABILITY_LEN, OUTER_ACK, OUTER_CREDIT, OUTER_DATA, OUTER_ERROR, OUTER_GUEST_ATTACH, OUTER_HEADER_LEN, OUTER_HOST_ATTACH, OUTER_MAGIC, OUTER_MAX_PAYLOAD, OUTER_MAX_RECEIVE_WINDOW, @@ -315,15 +316,9 @@ pub fn encode_record( check_payload(record_type, payload)?; // The length check bounds the payload by `OUTER_MAX_PAYLOAD`, so it fits the header's field. let length = payload.len() as u32; + let header = encode_header(record_type.code(), instance_id, epoch, sequence, length); output.reserve(OUTER_HEADER_LEN + payload.len()); - output.extend_from_slice(&OUTER_MAGIC); - output.extend_from_slice(&OUTER_VERSION.to_le_bytes()); - output.push(record_type.code()); - output.push(0); - output.extend_from_slice(instance_id); - output.extend_from_slice(&epoch.to_le_bytes()); - output.extend_from_slice(&sequence.to_le_bytes()); - output.extend_from_slice(&length.to_le_bytes()); + output.extend_from_slice(&header); output.extend_from_slice(payload); Ok(()) } diff --git a/nvx-rs/src/protocol/outer.rs b/nvx-rs/src/protocol/outer.rs index 6f766fe4e..3f8731535 100644 --- a/nvx-rs/src/protocol/outer.rs +++ b/nvx-rs/src/protocol/outer.rs @@ -82,19 +82,36 @@ pub fn encode_outer( .ok() .filter(|&length| length as usize <= OUTER_MAX_PAYLOAD) .ok_or_else(|| violation("control payload exceeds the outer protocol limit"))?; + let header = encode_header(record_type, instance_id, epoch, sequence, length); let mut record = Vec::with_capacity(OUTER_HEADER_LEN + payload.len()); - record.extend_from_slice(&OUTER_MAGIC); - record.extend_from_slice(&OUTER_VERSION.to_le_bytes()); - record.push(record_type); - record.push(0); - record.extend_from_slice(instance_id); - record.extend_from_slice(&epoch.to_le_bytes()); - record.extend_from_slice(&sequence.to_le_bytes()); - record.extend_from_slice(&length.to_le_bytes()); + record.extend_from_slice(&header); record.extend_from_slice(payload); Ok(record) } +/// Returns the header of a record: [`OUTER_MAGIC`], [`OUTER_VERSION`], `record_type`, a zero +/// flags byte, `instance_id`, `epoch`, `sequence`, and the payload's `length`, with the integers +/// little-endian. It checks nothing: [`encode_outer`] and +/// [`encode_record`](super::nvxs::encode_record) each validate their record first. +pub(super) fn encode_header( + record_type: u8, + instance_id: &[u8; 16], + epoch: u64, + sequence: u64, + length: u32, +) -> [u8; OUTER_HEADER_LEN] { + let mut header = [0; OUTER_HEADER_LEN]; + header[..4].copy_from_slice(&OUTER_MAGIC); + header[4..6].copy_from_slice(&OUTER_VERSION.to_le_bytes()); + header[6] = record_type; + // The flags byte, header[7], stays zero. + header[8..24].copy_from_slice(instance_id); + header[24..32].copy_from_slice(&epoch.to_le_bytes()); + header[32..40].copy_from_slice(&sequence.to_le_bytes()); + header[40..].copy_from_slice(&length.to_le_bytes()); + header +} + /// Decodes and validates an outer record header. pub fn decode_outer_header(header: &[u8; OUTER_HEADER_LEN]) -> Result { let version = u16::from_le_bytes([header[4], header[5]]);