use crate::{varint, ProtoError, Result, PROTOCOL_VERSION};
pub const MAX_SERVER_FILL_BYTES: usize = 32;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Params {
pub protocol_version: i64,
pub duration_ns: i64,
pub interval_ns: i64,
pub length: i64,
pub received_stats: ReceivedStats,
pub stamp_at: StampAt,
pub clock: Clock,
pub dscp: i64,
pub server_fill: Option<ServerFill>,
}
impl Params {
pub fn with_protocol_defaults() -> Self {
Self {
protocol_version: PROTOCOL_VERSION,
..Self::default()
}
}
pub fn encode(&self) -> Vec<u8> {
let mut out = Vec::new();
push_int(1, self.protocol_version, &mut out);
push_int(2, self.duration_ns, &mut out);
push_int(3, self.interval_ns, &mut out);
push_int(4, self.length, &mut out);
push_int(5, self.received_stats as i64, &mut out);
push_int(6, self.stamp_at as i64, &mut out);
push_int(7, self.clock as i64, &mut out);
push_int(8, self.dscp, &mut out);
if let Some(fill) = &self.server_fill {
varint::encode_uvarint(9, &mut out);
varint::encode_uvarint(fill.value.len() as u64, &mut out);
out.extend_from_slice(fill.value.as_bytes());
}
out
}
pub fn decode(input: &[u8]) -> Result<Self> {
Self::decode_with_presence(input).map(|decoded| decoded.params)
}
pub fn decode_with_presence(input: &[u8]) -> Result<DecodedParams> {
let mut params = Self::default();
let mut presence = ParamPresence::default();
let mut pos = 0;
while pos < input.len() {
let (tag, used) = varint::decode_uvarint(&input[pos..])?;
pos += used;
match tag {
1 => {
params.protocol_version = read_int(input, &mut pos)?;
presence.protocol_version = true;
}
2 => {
params.duration_ns = read_int(input, &mut pos)?;
presence.duration_ns = true;
}
3 => {
params.interval_ns = read_int(input, &mut pos)?;
presence.interval_ns = true;
}
4 => {
params.length = read_int(input, &mut pos)?;
presence.length = true;
}
5 => {
params.received_stats = ReceivedStats::try_from(read_int(input, &mut pos)?)?;
presence.received_stats = true;
}
6 => {
params.stamp_at = StampAt::try_from(read_int(input, &mut pos)?)?;
presence.stamp_at = true;
}
7 => {
params.clock = Clock::try_from(read_int(input, &mut pos)?)?;
presence.clock = true;
}
8 => {
params.dscp = read_int(input, &mut pos)?;
presence.dscp = true;
}
9 => {
let (len, used) = varint::decode_uvarint(&input[pos..])?;
pos += used;
let len = usize::try_from(len)
.map_err(|_| ProtoError::ParameterLengthTooLarge { tag, length: len })?;
if len > MAX_SERVER_FILL_BYTES {
return Err(ProtoError::ParameterLengthTooLarge {
tag,
length: len as u64,
});
}
if input.len().saturating_sub(pos) < len {
return Err(ProtoError::MalformedParams);
}
let value = std::str::from_utf8(&input[pos..pos + len])
.map_err(|_| ProtoError::InvalidUtf8)?
.to_owned();
pos += len;
params.server_fill = Some(ServerFill { value });
presence.server_fill = true;
}
_ => {
let (_, used) = varint::decode_uvarint(&input[pos..])?;
pos += used;
}
}
}
Ok(DecodedParams { params, presence })
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct DecodedParams {
pub params: Params,
pub presence: ParamPresence,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct ParamPresence {
pub protocol_version: bool,
pub duration_ns: bool,
pub interval_ns: bool,
pub length: bool,
pub received_stats: bool,
pub stamp_at: bool,
pub clock: bool,
pub dscp: bool,
pub server_fill: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ServerFill {
pub value: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(i64)]
pub enum ReceivedStats {
#[default]
None = 0,
Count = 1,
Window = 2,
Both = 3,
}
impl ReceivedStats {
pub fn has_count(self) -> bool {
matches!(self, Self::Count | Self::Both)
}
pub fn has_window(self) -> bool {
matches!(self, Self::Window | Self::Both)
}
}
impl TryFrom<i64> for ReceivedStats {
type Error = ProtoError;
fn try_from(value: i64) -> Result<Self> {
match value {
0 => Ok(Self::None),
1 => Ok(Self::Count),
2 => Ok(Self::Window),
3 => Ok(Self::Both),
_ => Err(ProtoError::InvalidEnum {
name: "ReceivedStats",
value,
}),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(i64)]
pub enum StampAt {
#[default]
None = 0,
Send = 1,
Receive = 2,
Both = 3,
Midpoint = 4,
}
impl TryFrom<i64> for StampAt {
type Error = ProtoError;
fn try_from(value: i64) -> Result<Self> {
match value {
0 => Ok(Self::None),
1 => Ok(Self::Send),
2 => Ok(Self::Receive),
3 => Ok(Self::Both),
4 => Ok(Self::Midpoint),
_ => Err(ProtoError::InvalidEnum {
name: "StampAt",
value,
}),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(i64)]
pub enum Clock {
#[default]
Unspecified = 0,
Wall = 1,
Monotonic = 2,
Both = 3,
}
impl Clock {
pub fn has_wall(self) -> bool {
matches!(self, Self::Wall | Self::Both)
}
pub fn has_mono(self) -> bool {
matches!(self, Self::Monotonic | Self::Both)
}
}
impl TryFrom<i64> for Clock {
type Error = ProtoError;
fn try_from(value: i64) -> Result<Self> {
match value {
1 => Ok(Self::Wall),
2 => Ok(Self::Monotonic),
3 => Ok(Self::Both),
_ => Err(ProtoError::InvalidEnum {
name: "Clock",
value,
}),
}
}
}
fn push_int(tag: u64, value: i64, out: &mut Vec<u8>) {
if value == 0 {
return;
}
varint::encode_uvarint(tag, out);
varint::encode_varint(value, out);
}
fn read_int(input: &[u8], pos: &mut usize) -> Result<i64> {
let (value, used) = varint::decode_varint(&input[*pos..])?;
*pos += used;
Ok(value)
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_round_trip(params: Params) {
assert_eq!(Params::decode(¶ms.encode()), Ok(params));
}
fn encode_server_fill_value(value: &[u8]) -> Vec<u8> {
let mut encoded = Vec::new();
varint::encode_uvarint(9, &mut encoded);
varint::encode_uvarint(value.len() as u64, &mut encoded);
encoded.extend_from_slice(value);
encoded
}
fn encode_int(tag: u64, value: i64) -> Vec<u8> {
let mut encoded = Vec::new();
varint::encode_uvarint(tag, &mut encoded);
varint::encode_varint(value, &mut encoded);
encoded
}
#[test]
fn params_round_trip() {
let params = Params {
protocol_version: 1,
duration_ns: 3_000_000_000,
interval_ns: 1_000_000_000,
length: 1472,
received_stats: ReceivedStats::Both,
stamp_at: StampAt::Both,
clock: Clock::Both,
dscp: 184,
server_fill: Some(ServerFill {
value: "pattern:abc".to_owned(),
}),
};
assert_round_trip(params);
}
#[test]
fn params_round_trip_negotiated_option_modes() {
for received_stats in [
ReceivedStats::None,
ReceivedStats::Count,
ReceivedStats::Window,
ReceivedStats::Both,
] {
assert_round_trip(Params {
protocol_version: 1,
received_stats,
..Params::default()
});
}
for stamp_at in [
StampAt::None,
StampAt::Send,
StampAt::Receive,
StampAt::Both,
StampAt::Midpoint,
] {
assert_round_trip(Params {
protocol_version: 1,
stamp_at,
clock: Clock::Both,
..Params::default()
});
}
for clock in [Clock::Wall, Clock::Monotonic, Clock::Both] {
assert_round_trip(Params {
protocol_version: 1,
stamp_at: StampAt::Both,
clock,
..Params::default()
});
}
}
#[test]
fn params_round_trip_dscp_values_are_encoded_without_shifting() {
for dscp in [0, 46, 63, 64, 184, -1] {
let params = Params {
protocol_version: 1,
dscp,
..Params::default()
};
assert_round_trip(params);
}
let params = Params {
protocol_version: 1,
dscp: 46,
..Params::default()
};
let encoded = params.encode();
assert!(
encoded.windows(2).any(|bytes| bytes == [8, 92]),
"dscp value 46 must be encoded as param value 46"
);
assert!(
!encoded.windows(3).any(|bytes| bytes == [8, 0xf0, 0x02]),
"Params::encode must not shift dscp 46 to 184; any codepoint shift is a caller concern"
);
assert_eq!(Params::decode(&encoded).unwrap().dscp, 46);
}
#[test]
fn server_fill_absent_short_and_max_length_round_trip() {
assert_round_trip(Params {
protocol_version: 1,
server_fill: None,
..Params::default()
});
assert_round_trip(Params {
protocol_version: 1,
server_fill: Some(ServerFill {
value: "rand".to_owned(),
}),
..Params::default()
});
assert_round_trip(Params {
protocol_version: 1,
server_fill: Some(ServerFill {
value: "0123456789abcdef0123456789abcdef".to_owned(),
}),
..Params::default()
});
}
#[test]
fn server_fill_decode_accepts_max_length() {
let value = b"0123456789abcdef0123456789abcdef";
let params = Params::decode(&encode_server_fill_value(value)).unwrap();
assert_eq!(
params.server_fill,
Some(ServerFill {
value: "0123456789abcdef0123456789abcdef".to_owned(),
})
);
}
#[test]
fn server_fill_decode_rejects_oversized_length() {
let value = b"0123456789abcdef0123456789abcdefx";
assert_eq!(
Params::decode(&encode_server_fill_value(value)),
Err(ProtoError::ParameterLengthTooLarge { tag: 9, length: 33 })
);
}
#[test]
fn server_fill_tag_and_length_are_encoded_before_utf8_bytes() {
let params = Params {
protocol_version: 1,
server_fill: Some(ServerFill {
value: "rand".to_owned(),
}),
..Params::default()
};
let encoded = params.encode();
assert!(encoded
.windows(6)
.any(|bytes| bytes == [9, 4, b'r', b'a', b'n', b'd']));
}
#[test]
fn unknown_tags_are_ignored() {
let mut encoded = Vec::new();
varint::encode_uvarint(99, &mut encoded);
varint::encode_varint(123, &mut encoded);
varint::encode_uvarint(1, &mut encoded);
varint::encode_varint(1, &mut encoded);
let params = Params::decode(&encoded).unwrap();
assert_eq!(params.protocol_version, 1);
}
#[test]
fn invalid_received_stats_value_is_rejected() {
let mut encoded = Vec::new();
varint::encode_uvarint(5, &mut encoded);
varint::encode_varint(4, &mut encoded);
assert_eq!(
Params::decode(&encoded),
Err(ProtoError::InvalidEnum {
name: "ReceivedStats",
value: 4,
})
);
}
#[test]
fn invalid_timestamp_value_is_rejected() {
let mut encoded = Vec::new();
varint::encode_uvarint(6, &mut encoded);
varint::encode_varint(5, &mut encoded);
assert_eq!(
Params::decode(&encoded),
Err(ProtoError::InvalidEnum {
name: "StampAt",
value: 5,
})
);
}
#[test]
fn explicit_clock_zero_is_rejected() {
let mut encoded = Vec::new();
varint::encode_uvarint(7, &mut encoded);
varint::encode_varint(0, &mut encoded);
assert_eq!(
Params::decode(&encoded),
Err(ProtoError::InvalidEnum {
name: "Clock",
value: 0,
})
);
assert_eq!(
Params::decode_with_presence(&encoded),
Err(ProtoError::InvalidEnum {
name: "Clock",
value: 0,
})
);
}
#[test]
fn explicit_clock_values_one_to_three_are_accepted() {
for (value, expected) in [(1, Clock::Wall), (2, Clock::Monotonic), (3, Clock::Both)] {
let decoded = Params::decode_with_presence(&encode_int(7, value)).unwrap();
assert_eq!(decoded.params.clock, expected);
assert!(decoded.presence.clock);
}
}
#[test]
fn empty_payload_decodes_to_wire_defaults_with_nothing_present() {
let decoded = Params::decode_with_presence(&[]).unwrap();
assert_eq!(decoded.params, Params::default());
assert_eq!(decoded.params.protocol_version, 0);
assert_eq!(decoded.params.duration_ns, 0);
assert_eq!(decoded.params.interval_ns, 0);
assert_eq!(decoded.params.length, 0);
assert_eq!(decoded.params.dscp, 0);
assert_eq!(decoded.params.received_stats, ReceivedStats::None);
assert_eq!(decoded.params.stamp_at, StampAt::None);
assert_eq!(decoded.params.clock, Clock::Unspecified);
assert!(!decoded.params.clock.has_wall());
assert!(!decoded.params.clock.has_mono());
assert_eq!(decoded.params.server_fill, None);
assert_eq!(decoded.presence, ParamPresence::default());
assert_eq!(
decoded.presence,
ParamPresence {
protocol_version: false,
duration_ns: false,
interval_ns: false,
length: false,
received_stats: false,
stamp_at: false,
clock: false,
dscp: false,
server_fill: false,
}
);
}
#[test]
fn absent_and_explicitly_zero_parameters_share_a_value_but_not_a_presence() {
let absent = Params::decode_with_presence(&encode_int(1, 1)).unwrap();
assert_eq!(absent.params.duration_ns, 0);
assert!(!absent.presence.duration_ns);
assert_eq!(absent.params.interval_ns, 0);
assert!(!absent.presence.interval_ns);
assert_eq!(absent.params.clock, Clock::Unspecified);
assert!(!absent.presence.clock);
let explicit_duration = Params::decode_with_presence(&encode_int(2, 0)).unwrap();
assert_eq!(explicit_duration.params.duration_ns, 0);
assert!(explicit_duration.presence.duration_ns);
let explicit_interval = Params::decode_with_presence(&encode_int(3, 0)).unwrap();
assert_eq!(explicit_interval.params.interval_ns, 0);
assert!(explicit_interval.presence.interval_ns);
}
#[test]
fn every_known_tag_reports_its_own_presence() {
let mut encoded = Vec::new();
for (tag, value) in [
(1, 1),
(2, 5),
(3, 6),
(4, 7),
(5, 3),
(6, 4),
(7, 3),
(8, 184),
] {
encoded.extend_from_slice(&encode_int(tag, value));
}
encoded.extend_from_slice(&encode_server_fill_value(b"rand"));
let decoded = Params::decode_with_presence(&encoded).unwrap();
assert_eq!(
decoded.presence,
ParamPresence {
protocol_version: true,
duration_ns: true,
interval_ns: true,
length: true,
received_stats: true,
stamp_at: true,
clock: true,
dscp: true,
server_fill: true,
}
);
}
#[test]
fn unspecified_clock_is_encoded_by_omission() {
let params = Params {
protocol_version: 1,
clock: Clock::Unspecified,
..Params::default()
};
let encoded = params.encode();
assert_eq!(encoded, encode_int(1, 1));
assert!(
!encoded.windows(2).any(|bytes| bytes[0] == 7),
"an unspecified clock must omit the tag rather than encode an explicit zero"
);
assert_round_trip(params);
}
#[test]
fn decode_and_decode_with_presence_agree() {
let mut payloads = vec![
Vec::new(),
encode_int(1, 1),
encode_int(2, 0),
encode_server_fill_value(b"rand"),
];
payloads.push(
Params {
protocol_version: 1,
duration_ns: 3_000_000_000,
interval_ns: 1_000_000_000,
length: 1472,
received_stats: ReceivedStats::Both,
stamp_at: StampAt::Both,
clock: Clock::Both,
dscp: 184,
server_fill: Some(ServerFill {
value: "rand".to_owned(),
}),
}
.encode(),
);
payloads.push(vec![1, 0x80]);
payloads.push(encode_int(7, 0));
for payload in payloads {
assert_eq!(
Params::decode(&payload),
Params::decode_with_presence(&payload).map(|decoded| decoded.params),
"decode and decode_with_presence disagreed on {payload:02x?}"
);
}
}
#[test]
fn malformed_server_fill_is_rejected() {
let encoded = [9, 4, b'a', b'b'];
assert_eq!(Params::decode(&encoded), Err(ProtoError::MalformedParams));
}
#[test]
fn non_utf8_server_fill_is_rejected() {
let encoded = [9, 1, 0xff];
assert_eq!(Params::decode(&encoded), Err(ProtoError::InvalidUtf8));
}
#[cfg(target_pointer_width = "32")]
#[test]
fn server_fill_length_too_large_for_usize_is_rejected() {
let mut encoded = Vec::new();
varint::encode_uvarint(9, &mut encoded);
varint::encode_uvarint(u64::from(u32::MAX) + 1, &mut encoded);
assert_eq!(
Params::decode(&encoded),
Err(ProtoError::ParameterLengthTooLarge {
tag: 9,
length: u64::from(u32::MAX) + 1,
})
);
}
#[test]
fn truncated_varint_parameter_is_rejected() {
let encoded = [1, 0x80];
assert_eq!(Params::decode(&encoded), Err(ProtoError::TruncatedVarint));
}
#[test]
fn truncated_unknown_parameter_value_is_rejected() {
let encoded = [99];
assert_eq!(Params::decode(&encoded), Err(ProtoError::TruncatedVarint));
}
}