feat(proto): add directional echo codecs

This commit is contained in:
Nils Andreas Svee committed 2026-08-04 00:34:12 +07:00
1 parent cc7f7fc9b2
commit cabe0f6c24
6 files changed
+447 -50

No files matched your search

+130 -19
View File
@@ -10,7 +10,6 @@ use crate::{
pub struct EchoRequest {
pub token: u64,
pub sequence: u32,
pub params: Params,
pub payload: Vec<u8>,
}
@@ -35,9 +34,13 @@ pub struct TimestampFields {
pub send_mono: Option<i64>,
}
pub fn encode_echo_request(request: &EchoRequest, hmac_key: Option<&[u8]>) -> Result<Vec<u8>> {
let layout = PacketLayout::echo(hmac_key.is_some(), &request.params);
let len = echo_packet_len(hmac_key.is_some(), &request.params)?;
pub fn encode_echo_request(
request: &EchoRequest,
params: &Params,
hmac_key: Option<&[u8]>,
) -> Result<Vec<u8>> {
let layout = PacketLayout::echo(hmac_key.is_some(), params);
let len = echo_packet_len(hmac_key.is_some(), params)?;
let payload_offset = layout.header_len();
let available_payload_len = len.saturating_sub(payload_offset);
if request.payload.len() > available_payload_len {
@@ -65,6 +68,66 @@ pub fn encode_echo_request(request: &EchoRequest, hmac_key: Option<&[u8]>) -> Re
envelope::finish(out, hmac_key)
}
pub fn decode_echo_request(
packet: &[u8],
params: &Params,
hmac_key: Option<&[u8]>,
) -> Result<EchoRequest> {
let envelope = envelope::decode(
packet,
hmac_key,
&[
FlagRule::Reject(FLAG_OPEN),
FlagRule::Reject(FLAG_REPLY),
FlagRule::Reject(FLAG_CLOSE),
],
)?;
let layout = PacketLayout::echo(hmac_key.is_some(), params);
validate_echo_length(packet, params, layout)?;
envelope::verify(packet, hmac_key)?;
let mut pos = envelope.body_offset;
let token = read_u64(packet, &mut pos);
let sequence = read_u32(packet, &mut pos);
Ok(EchoRequest {
token,
sequence,
payload: packet[layout.header_len()..].to_vec(),
})
}
pub fn encode_echo_reply(
reply: &EchoReply,
params: &Params,
hmac_key: Option<&[u8]>,
) -> Result<Vec<u8>> {
let layout = PacketLayout::echo(hmac_key.is_some(), params);
let len = echo_packet_len(hmac_key.is_some(), params)?;
let payload_offset = layout.header_len();
let available_payload_len = len.saturating_sub(payload_offset);
if reply.payload.len() > available_payload_len {
return Err(ProtoError::PayloadTooLarge {
available: available_payload_len,
provided: reply.payload.len(),
});
}
let mut out = envelope::begin(
reply.flags,
hmac_key,
&[FlagRule::Reject(FLAG_OPEN), FlagRule::Require(FLAG_REPLY)],
len,
)?;
out.extend_from_slice(&reply.token.to_le_bytes());
out.extend_from_slice(&reply.sequence.to_le_bytes());
push_echo_reply_tail(reply, layout, &mut out)?;
out.resize(len, 0);
out[payload_offset..payload_offset + reply.payload.len()].copy_from_slice(&reply.payload);
envelope::finish(out, hmac_key)
}
pub fn decode_echo_reply(
packet: &[u8],
params: &Params,
@@ -135,6 +198,47 @@ fn push_zeroed_layout_tail(layout: PacketLayout, out: &mut Vec<u8>) {
}
}
fn push_echo_reply_tail(reply: &EchoReply, layout: PacketLayout, out: &mut Vec<u8>) -> Result<()> {
if let Some(value) = field_value(layout.recv_count, reply.recv_count, "recv_count")? {
out.extend_from_slice(&value.to_le_bytes());
}
if let Some(value) = field_value(layout.recv_window, reply.recv_window, "recv_window")? {
out.extend_from_slice(&value.to_le_bytes());
}
let timestamps = &reply.timestamps;
for (present, value, name) in [
(layout.recv_wall, timestamps.recv_wall, "recv_wall"),
(layout.recv_mono, timestamps.recv_mono, "recv_mono"),
(
layout.midpoint_wall,
timestamps.midpoint_wall,
"midpoint_wall",
),
(
layout.midpoint_mono,
timestamps.midpoint_mono,
"midpoint_mono",
),
(layout.send_wall, timestamps.send_wall, "send_wall"),
(layout.send_mono, timestamps.send_mono, "send_mono"),
] {
if let Some(value) = field_value(present, value, name)? {
out.extend_from_slice(&value.to_le_bytes());
}
}
Ok(())
}
fn field_value<T: Copy>(present: bool, value: Option<T>, name: &'static str) -> Result<Option<T>> {
match (present, value) {
(true, Some(value)) => Ok(Some(value)),
(true, None) => Err(ProtoError::MissingField(name)),
(false, Some(_)) => Err(ProtoError::UnexpectedField(name)),
(false, None) => Ok(None),
}
}
fn read_u32(packet: &[u8], pos: &mut usize) -> u32 {
let value = u32::from_le_bytes(packet[*pos..*pos + SEQ_SIZE].try_into().unwrap());
*pos += SEQ_SIZE;
@@ -179,24 +283,24 @@ mod tests {
}
}
fn echo_request_with_payload(payload_space: usize, payload: Vec<u8>) -> EchoRequest {
fn echo_request_with_payload(payload: Vec<u8>) -> EchoRequest {
EchoRequest {
token: 0x7896_b6ab_8771_5213,
sequence: 9,
params: params_with_payload_space(payload_space),
payload,
}
}
#[test]
fn echo_request_encodes_default_placeholders() {
let params = default_params();
let packet = encode_echo_request(
&EchoRequest {
token: 0x7896_b6ab_8771_5213,
sequence: 0,
params: default_params(),
payload: Vec::new(),
},
&params,
None,
)
.unwrap();
@@ -208,29 +312,32 @@ mod tests {
#[test]
fn echo_request_encodes_exact_fit_payload() {
let request = echo_request_with_payload(4, vec![1, 2, 3, 4]);
let packet = encode_echo_request(&request, None).unwrap();
let payload_offset = PacketLayout::echo(false, &request.params).header_len();
let params = params_with_payload_space(4);
let request = echo_request_with_payload(vec![1, 2, 3, 4]);
let packet = encode_echo_request(&request, &params, None).unwrap();
let payload_offset = PacketLayout::echo(false, &params).header_len();
assert_eq!(&packet[payload_offset..], &[1, 2, 3, 4]);
}
#[test]
fn echo_request_encodes_shorter_payload_and_zero_fills_remainder() {
let request = echo_request_with_payload(4, vec![1, 2]);
let packet = encode_echo_request(&request, None).unwrap();
let payload_offset = PacketLayout::echo(false, &request.params).header_len();
let params = params_with_payload_space(4);
let request = echo_request_with_payload(vec![1, 2]);
let packet = encode_echo_request(&request, &params, None).unwrap();
let payload_offset = PacketLayout::echo(false, &params).header_len();
assert_eq!(&packet[payload_offset..], &[1, 2, 0, 0]);
}
#[test]
fn echo_request_rejects_oversized_payload() {
let request = echo_request_with_payload(4, vec![1, 2, 3, 4, 5]);
let params = params_with_payload_space(4);
let request = echo_request_with_payload(vec![1, 2, 3, 4, 5]);
let original = request.clone();
assert_eq!(
encode_echo_request(&request, None),
encode_echo_request(&request, &params, None),
Err(ProtoError::PayloadTooLarge {
available: 4,
provided: 5,
@@ -241,24 +348,28 @@ mod tests {
#[test]
fn echo_request_rejects_negative_requested_length() {
let mut request = echo_request_with_payload(0, Vec::new());
request.params.length = -1;
let request = echo_request_with_payload(Vec::new());
let params = Params {
length: -1,
..Params::default()
};
assert_eq!(
encode_echo_request(&request, None),
encode_echo_request(&request, &params, None),
Err(ProtoError::NegativePacketLength { length: -1 })
);
}
#[test]
fn hmac_echo_request_places_token_and_sequence_after_hmac() {
let params = default_params();
let packet = encode_echo_request(
&EchoRequest {
token: 0x7896_b6ab_8771_5213,
sequence: 9,
params: default_params(),
payload: Vec::new(),
},
&params,
Some(b"testkey"),
)
.unwrap();
+4
View File
@@ -40,4 +40,8 @@ pub enum ProtoError {
MalformedParams,
#[error("payload is too large: {provided} bytes provided, {available} bytes available")]
PayloadTooLarge { available: usize, provided: usize },
#[error("required packet field {0} is missing")]
MissingField(&'static str),
#[error("packet field {0} is not present in the negotiated layout")]
UnexpectedField(&'static str),
}
+4 -1
View File
@@ -32,7 +32,10 @@ pub mod params;
pub mod varint;
pub use close::{decode_close_request, encode_close_request, CloseRequest};
pub use echo::{decode_echo_reply, encode_echo_request, EchoReply, EchoRequest, TimestampFields};
pub use echo::{
decode_echo_reply, decode_echo_request, encode_echo_reply, encode_echo_request, EchoReply,
EchoRequest, TimestampFields,
};
pub use error::{ProtoError, Result};
pub use flags::*;
pub use hmac::{compute_hmac, compute_hmac_in_place, verify_hmac};