Files

400 lines
12 KiB
Rust

use irtt_proto::{
varint, Clock, ParamPresence, Params, ProtoError, ReceivedStats, ServerFill, StampAt,
};
use proptest::prelude::*;
fn int(tag: u64, value: i64) -> Vec<u8> {
let mut out = Vec::new();
varint::encode_uvarint(tag, &mut out);
varint::encode_varint(value, &mut out);
out
}
fn fill(value: &[u8]) -> Vec<u8> {
let mut out = vec![9];
varint::encode_uvarint(value.len() as u64, &mut out);
out.extend_from_slice(value);
out
}
fn presence(p: ParamPresence) -> [bool; 9] {
[
p.protocol_version,
p.duration_ns,
p.interval_ns,
p.length,
p.received_stats,
p.stamp_at,
p.clock,
p.dscp,
p.server_fill,
]
}
fn assert_error(bytes: &[u8], expected: ProtoError) {
assert_eq!(Params::decode(bytes).unwrap_err(), expected, "{bytes:02x?}");
assert_eq!(
Params::decode_with_presence(bytes).unwrap_err(),
expected,
"{bytes:02x?}"
);
}
#[test]
fn known_encodings_preserve_raw_dscp_and_utf8_byte_lengths() {
for (params, bytes) in [
(
Params {
dscp: 46,
..Params::default()
},
vec![8, 92],
),
(
Params {
dscp: 184,
..Params::default()
},
vec![8, 0xf0, 2],
),
(
Params {
server_fill: Some(ServerFill {
value: "rand".into(),
}),
..Params::default()
},
vec![9, 4, b'r', b'a', b'n', b'd'],
),
(
Params {
server_fill: Some(ServerFill { value: "é".into() }),
..Params::default()
},
vec![9, 2, 0xc3, 0xa9],
),
] {
assert_eq!(params.encode(), bytes);
assert_eq!(Params::decode(&bytes).unwrap(), params);
}
}
#[test]
fn wire_defaults_and_explicit_zero_presence() {
let defaults = Params {
protocol_version: 0,
duration_ns: 0,
interval_ns: 0,
length: 0,
received_stats: ReceivedStats::None,
stamp_at: StampAt::None,
clock: Clock::Unspecified,
dscp: 0,
server_fill: None,
};
let absent = Params::decode_with_presence(&[]).unwrap();
assert_eq!(absent.params, defaults);
assert_eq!(presence(absent.presence), [false; 9]);
assert_eq!(defaults.encode(), Vec::<u8>::new());
assert!(!defaults.clock.has_wall());
assert!(!defaults.clock.has_mono());
// Clock is the sole integer tag for which explicit zero is invalid.
for tag in [1, 2, 3, 4, 5, 6, 8] {
let bytes = int(tag, 0);
let decoded = Params::decode_with_presence(&bytes).unwrap();
assert_eq!(decoded.params, defaults);
let mut expected = [false; 9];
expected[tag as usize - 1] = true;
assert_eq!(presence(decoded.presence), expected, "tag {tag}");
assert_eq!(Params::decode(&bytes).unwrap(), decoded.params);
}
let empty_fill = Params::decode_with_presence(&[9, 0]).unwrap();
assert_eq!(
empty_fill.params.server_fill,
Some(ServerFill {
value: String::new()
})
);
assert_eq!(
presence(empty_fill.presence),
[false, false, false, false, false, false, false, false, true]
);
assert_eq!(empty_fill.params.encode(), [9, 0]);
assert_eq!(
Params {
protocol_version: 1,
..defaults
}
.encode(),
[1, 2]
);
}
#[test]
fn known_tags_map_to_their_fields_and_report_isolated_presence() {
for (tag, value, expected) in [
(
1,
1,
Params {
protocol_version: 1,
..Params::default()
},
),
(
2,
5,
Params {
duration_ns: 5,
..Params::default()
},
),
(
3,
6,
Params {
interval_ns: 6,
..Params::default()
},
),
(
4,
7,
Params {
length: 7,
..Params::default()
},
),
(
8,
184,
Params {
dscp: 184,
..Params::default()
},
),
] {
let decoded = Params::decode_with_presence(&int(tag, value)).unwrap();
assert_eq!(decoded.params, expected);
let mut expected_presence = [false; 9];
expected_presence[tag as usize - 1] = true;
assert_eq!(presence(decoded.presence), expected_presence);
}
for (tag, modes) in [
(
5,
vec![
ReceivedStats::None as i64,
ReceivedStats::Count as i64,
ReceivedStats::Window as i64,
ReceivedStats::Both as i64,
],
),
(
6,
vec![
StampAt::None as i64,
StampAt::Send as i64,
StampAt::Receive as i64,
StampAt::Both as i64,
StampAt::Midpoint as i64,
],
),
(
7,
vec![
Clock::Wall as i64,
Clock::Monotonic as i64,
Clock::Both as i64,
],
),
] {
for (index, mode) in modes.into_iter().enumerate() {
let value = index as i64 + if tag == 7 { 1 } else { 0 };
let decoded = Params::decode_with_presence(&int(tag, value)).unwrap();
let actual = match tag {
5 => decoded.params.received_stats as i64,
6 => decoded.params.stamp_at as i64,
7 => decoded.params.clock as i64,
_ => unreachable!(),
};
assert_eq!(actual, mode);
let mut expected_presence = [false; 9];
expected_presence[tag as usize - 1] = true;
assert_eq!(presence(decoded.presence), expected_presence);
}
}
}
#[test]
fn malformed_values_and_invalid_enums_are_rejected_by_both_decoders() {
for (tag, value, name) in [
(5, 4, "ReceivedStats"),
(6, 5, "StampAt"),
(7, 0, "Clock"),
(7, 4, "Clock"),
(5, -1, "ReceivedStats"),
(6, -1, "StampAt"),
(7, -1, "Clock"),
] {
assert_error(&int(tag, value), ProtoError::InvalidEnum { name, value });
}
for bytes in [&[1, 0x80][..], &[99], &[0x80], &[9, 0x80]] {
assert_error(bytes, ProtoError::TruncatedVarint);
}
}
#[test]
fn server_fill_accepts_valid_bytes_and_enforces_length_and_utf8_boundaries() {
for value in [
"",
"rand",
"0123456789abcdef0123456789abcdef",
"éééééééééééééééé",
] {
let expected = Params {
server_fill: Some(ServerFill {
value: value.into(),
}),
..Params::default()
};
let bytes = fill(value.as_bytes());
assert_eq!(expected.encode(), bytes);
let decoded = Params::decode_with_presence(&bytes).unwrap();
assert_eq!(decoded.params, expected);
assert!(decoded.presence.server_fill);
assert_eq!(Params::decode(&bytes).unwrap(), expected);
}
assert_error(
&fill(b"0123456789abcdef0123456789abcdefx"),
ProtoError::ParameterLengthTooLarge { tag: 9, length: 33 },
);
assert_error(&[9, 4, b'a', b'b'], ProtoError::MalformedParams);
assert_error(&[9, 1, 0xff], ProtoError::InvalidUtf8);
}
#[cfg(target_pointer_width = "32")]
#[test]
fn server_fill_length_too_large_for_usize_is_rejected() {
let mut bytes = vec![9];
let length = u64::from(u32::MAX) + 1;
varint::encode_uvarint(length, &mut bytes);
assert_error(
&bytes,
ProtoError::ParameterLengthTooLarge { tag: 9, length },
);
}
fn string_strategy() -> impl Strategy<Value = String> {
// Eight arbitrary Unicode scalars occupy at most the allowed 32 bytes.
proptest::collection::vec(any::<char>(), 0..=8).prop_map(|chars| chars.into_iter().collect())
}
fn params_strategy() -> impl Strategy<Value = Params> {
(
any::<[i64; 5]>(),
prop::sample::select(vec![
ReceivedStats::None,
ReceivedStats::Count,
ReceivedStats::Window,
ReceivedStats::Both,
]),
prop::sample::select(vec![
StampAt::None,
StampAt::Send,
StampAt::Receive,
StampAt::Both,
StampAt::Midpoint,
]),
prop::sample::select(vec![
Clock::Unspecified,
Clock::Wall,
Clock::Monotonic,
Clock::Both,
]),
prop::option::of(string_strategy()),
)
.prop_map(
|(scalar, received_stats, stamp_at, clock, server_fill)| Params {
protocol_version: scalar[0],
duration_ns: scalar[1],
interval_ns: scalar[2],
length: scalar[3],
dscp: scalar[4],
received_stats,
stamp_at,
clock,
server_fill: server_fill.map(|value| ServerFill { value }),
},
)
}
fn all_tags(params: &Params) -> Vec<u8> {
let mut out = Vec::new();
// Deliberately bypass encode's omission: duplicate zero values must be present.
for (tag, value) in [
(1, params.protocol_version),
(2, params.duration_ns),
(3, params.interval_ns),
(4, params.length),
(5, params.received_stats as i64),
(6, params.stamp_at as i64),
(7, params.clock as i64),
(8, params.dscp),
] {
out.extend_from_slice(&int(tag, value));
}
out.extend_from_slice(&fill(params.server_fill.as_ref().unwrap().value.as_bytes()));
out
}
proptest! {
#[test]
fn valid_params_round_trip_with_omission_presence(params in params_strategy()) {
let encoded = params.encode();
let decoded = Params::decode_with_presence(&encoded).unwrap();
prop_assert_eq!(&decoded.params, &params);
prop_assert_eq!(Params::decode(&encoded).unwrap(), params.clone());
prop_assert_eq!(presence(decoded.presence), [
params.protocol_version != 0, params.duration_ns != 0,
params.interval_ns != 0, params.length != 0,
params.received_stats != ReceivedStats::None, params.stamp_at != StampAt::None,
params.clock != Clock::Unspecified, params.dscp != 0, params.server_fill.is_some(),
]);
}
#[test]
fn repeated_known_tags_are_last_wins(
mut first in params_strategy(), mut second in params_strategy(),
first_clock in 1_i64..=3, second_clock in 1_i64..=3,
first_fill in string_strategy(), second_fill in string_strategy(),
) {
first.clock = Clock::try_from(first_clock).unwrap();
second.clock = Clock::try_from(second_clock).unwrap();
first.server_fill = Some(ServerFill { value: first_fill });
second.server_fill = Some(ServerFill { value: second_fill });
let mut bytes = all_tags(&first);
bytes.extend_from_slice(&all_tags(&second));
let decoded = Params::decode_with_presence(&bytes).unwrap();
prop_assert_eq!(&decoded.params, &second);
prop_assert_eq!(Params::decode(&bytes).unwrap(), second);
prop_assert_eq!(presence(decoded.presence), [true; 9]);
}
#[test]
fn unknown_scalar_tags_do_not_disturb_known_params(
params in params_strategy(), tag in 10_u64..=u16::MAX as u64, value: u64,
) {
let encoded = params.encode();
let expected = Params::decode_with_presence(&encoded).unwrap();
let mut unknown = Vec::new();
varint::encode_uvarint(tag, &mut unknown);
varint::encode_uvarint(value, &mut unknown);
for bytes in [[unknown.as_slice(), &encoded].concat(), [&encoded, unknown.as_slice()].concat()] {
prop_assert_eq!(Params::decode_with_presence(&bytes).unwrap(), expected.clone());
prop_assert_eq!(Params::decode(&bytes).unwrap(), params.clone());
}
}
}