Merge branch 'refactor/consolidate-proto-tests'
Rust CI / cargo fmt (push) Successful in 18s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Successful in 32s
Rust CI / cargo clippy (push) Successful in 33s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Successful in 1m14s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Failing after 9s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Failing after 8s
Rust CI / cargo doc (push) Successful in 26s
Fuzz Smoke / cargo fuzz smoke (push) Failing after 30s
Supply chain / actionlint (push) Successful in 3s
Supply chain / cargo-deny bans licenses sources (push) Successful in 8s
Supply chain / cargo-deny advisories (push) Successful in 9s
Netem Interop / netem interop (push) Successful in 1m7s
Rust CI / cargo fmt (push) Successful in 18s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Successful in 32s
Rust CI / cargo clippy (push) Successful in 33s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Successful in 1m14s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Failing after 9s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Failing after 8s
Rust CI / cargo doc (push) Successful in 26s
Fuzz Smoke / cargo fuzz smoke (push) Failing after 30s
Supply chain / actionlint (push) Successful in 3s
Supply chain / cargo-deny bans licenses sources (push) Successful in 8s
Supply chain / cargo-deny advisories (push) Successful in 9s
Netem Interop / netem interop (push) Successful in 1m7s
This commit is contained in:
13 files changed
+950
-1752
No files matched your search
@@ -304,172 +304,3 @@ fn read_i64(packet: &[u8], pos: &mut usize) -> i64 {
|
||||
*pos += TIMESTAMP_SIZE;
|
||||
value
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
hmac,
|
||||
params::{Clock, ReceivedStats, StampAt},
|
||||
write_header, FLAG_HMAC, HMAC_SIZE, RECV_COUNT_SIZE, RECV_WINDOW_SIZE,
|
||||
};
|
||||
|
||||
fn default_params() -> Params {
|
||||
Params {
|
||||
received_stats: ReceivedStats::Both,
|
||||
stamp_at: StampAt::Both,
|
||||
clock: Clock::Both,
|
||||
..Params::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn params_with_payload_space(payload_space: usize) -> Params {
|
||||
let header_len = PacketLayout::echo(false, &Params::default()).header_len();
|
||||
Params {
|
||||
length: (header_len + payload_space) as i64,
|
||||
..Params::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Zero-fills every negotiated reply field, matching what a peer that
|
||||
/// reports no optional values would put on the wire.
|
||||
fn push_zeroed_layout_tail(layout: PacketLayout, out: &mut Vec<u8>) {
|
||||
if layout.recv_count {
|
||||
out.extend_from_slice(&[0; RECV_COUNT_SIZE]);
|
||||
}
|
||||
if layout.recv_window {
|
||||
out.extend_from_slice(&[0; RECV_WINDOW_SIZE]);
|
||||
}
|
||||
for _ in 0..layout.timestamp_count() {
|
||||
out.extend_from_slice(&[0; TIMESTAMP_SIZE]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn echo_reply_decodes_default_fields() {
|
||||
let packet = [
|
||||
0x14, 0xa7, 0x5b, 0x02, 0x13, 0x52, 0x71, 0x87, 0xab, 0xb6, 0x96, 0x78, 0x02, 0x00,
|
||||
0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xb8, 0x1a, 0x33, 0x0c, 0x86, 0x6d, 0xaa, 0x18, 0xde, 0x26, 0x35, 0x95, 0x00, 0x00,
|
||||
0x00, 0x00, 0x80, 0x4d, 0x33, 0x0c, 0x86, 0x6d, 0xaa, 0x18, 0xb2, 0x57, 0x35, 0x95,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
];
|
||||
let reply = decode_echo_reply(&packet, &default_params(), None).unwrap();
|
||||
assert_eq!(reply.token, 0x7896_b6ab_8771_5213);
|
||||
assert_eq!(reply.sequence, 2);
|
||||
assert_eq!(reply.recv_count, Some(3));
|
||||
assert_eq!(reply.recv_window, Some(7));
|
||||
assert!(reply.timestamps.recv_wall.is_some());
|
||||
assert!(reply.timestamps.midpoint_wall.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn echo_reply_decodes_exact_negotiated_length_with_payload() {
|
||||
let params = params_with_payload_space(4);
|
||||
let mut packet = Vec::new();
|
||||
write_header(&mut packet, FLAG_REPLY);
|
||||
packet.extend_from_slice(&0x7896_b6ab_8771_5213u64.to_le_bytes());
|
||||
packet.extend_from_slice(&2_u32.to_le_bytes());
|
||||
packet.extend_from_slice(&[1, 2, 3, 4]);
|
||||
|
||||
let reply = decode_echo_reply(&packet, ¶ms, None).unwrap();
|
||||
assert_eq!(reply.payload, vec![1, 2, 3, 4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn echo_reply_rejects_shorter_than_negotiated_length() {
|
||||
let params = params_with_payload_space(4);
|
||||
let mut packet = Vec::new();
|
||||
write_header(&mut packet, FLAG_REPLY);
|
||||
packet.extend_from_slice(&0x7896_b6ab_8771_5213u64.to_le_bytes());
|
||||
packet.extend_from_slice(&2_u32.to_le_bytes());
|
||||
|
||||
assert_eq!(
|
||||
decode_echo_reply(&packet, ¶ms, None),
|
||||
Err(ProtoError::PacketLengthMismatch {
|
||||
expected: 20,
|
||||
actual: 16,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn echo_reply_rejects_longer_than_negotiated_length() {
|
||||
let params = Params::default();
|
||||
let mut packet = Vec::new();
|
||||
write_header(&mut packet, FLAG_REPLY);
|
||||
packet.extend_from_slice(&0x7896_b6ab_8771_5213u64.to_le_bytes());
|
||||
packet.extend_from_slice(&2_u32.to_le_bytes());
|
||||
packet.push(0);
|
||||
|
||||
assert_eq!(
|
||||
decode_echo_reply(&packet, ¶ms, None),
|
||||
Err(ProtoError::PacketLengthMismatch {
|
||||
expected: 16,
|
||||
actual: 17,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hmac_echo_reply_decodes_default_fields_after_hmac() {
|
||||
let params = default_params();
|
||||
let layout = PacketLayout::echo(true, ¶ms);
|
||||
let mut packet = Vec::with_capacity(layout.header_len());
|
||||
write_header(&mut packet, FLAG_REPLY | FLAG_HMAC);
|
||||
packet.extend_from_slice(&[0; HMAC_SIZE]);
|
||||
packet.extend_from_slice(&0x7896_b6ab_8771_5213u64.to_le_bytes());
|
||||
packet.extend_from_slice(&2_u32.to_le_bytes());
|
||||
push_zeroed_layout_tail(layout, &mut packet);
|
||||
hmac::compute_hmac_in_place(b"testkey", &mut packet, hmac::hmac_offset()).unwrap();
|
||||
|
||||
assert_eq!(packet.len(), 76);
|
||||
let reply = decode_echo_reply(&packet, ¶ms, Some(b"testkey")).unwrap();
|
||||
assert_eq!(reply.token, 0x7896_b6ab_8771_5213);
|
||||
assert_eq!(reply.sequence, 2);
|
||||
assert_eq!(reply.recv_count, Some(0));
|
||||
assert_eq!(reply.recv_window, Some(0));
|
||||
assert_eq!(reply.payload.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hmac_echo_reply_decodes_exact_negotiated_length_with_payload() {
|
||||
let mut params = Params {
|
||||
length: 48,
|
||||
..Params::default()
|
||||
};
|
||||
params.received_stats = ReceivedStats::Both;
|
||||
let layout = PacketLayout::echo(true, ¶ms);
|
||||
let mut packet = Vec::with_capacity(echo_packet_len(true, ¶ms).unwrap());
|
||||
write_header(&mut packet, FLAG_REPLY | FLAG_HMAC);
|
||||
packet.extend_from_slice(&[0; HMAC_SIZE]);
|
||||
packet.extend_from_slice(&0x7896_b6ab_8771_5213u64.to_le_bytes());
|
||||
packet.extend_from_slice(&2_u32.to_le_bytes());
|
||||
push_zeroed_layout_tail(layout, &mut packet);
|
||||
packet.extend_from_slice(&[1, 2, 3, 4]);
|
||||
hmac::compute_hmac_in_place(b"testkey", &mut packet, hmac::hmac_offset()).unwrap();
|
||||
|
||||
let reply = decode_echo_reply(&packet, ¶ms, Some(b"testkey")).unwrap();
|
||||
assert_eq!(reply.payload, vec![1, 2, 3, 4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hmac_echo_reply_rejects_length_mismatch_before_hmac_verification() {
|
||||
let params = Params::default();
|
||||
let mut packet = Vec::new();
|
||||
write_header(&mut packet, FLAG_REPLY | FLAG_HMAC);
|
||||
packet.extend_from_slice(&[0; HMAC_SIZE]);
|
||||
packet.extend_from_slice(&0x7896_b6ab_8771_5213u64.to_le_bytes());
|
||||
packet.extend_from_slice(&2_u32.to_le_bytes());
|
||||
hmac::compute_hmac_in_place(b"testkey", &mut packet, hmac::hmac_offset()).unwrap();
|
||||
packet.push(0);
|
||||
|
||||
assert_eq!(
|
||||
decode_echo_reply(&packet, ¶ms, Some(b"testkey")),
|
||||
Err(ProtoError::PacketLengthMismatch {
|
||||
expected: 32,
|
||||
actual: 33,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -136,101 +136,3 @@ fn with_hmac_flag(flags: u8, authenticated: bool) -> u8 {
|
||||
flags & !FLAG_HMAC
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
flags::{FLAG_OPEN, FLAG_REPLY},
|
||||
MAGIC,
|
||||
};
|
||||
|
||||
const KEY: &[u8] = b"testkey";
|
||||
const OPEN_REQUEST_RULES: &[FlagRule] =
|
||||
&[FlagRule::Require(FLAG_OPEN), FlagRule::Reject(FLAG_REPLY)];
|
||||
|
||||
fn header(flags: u8) -> Vec<u8> {
|
||||
let mut packet = MAGIC.to_vec();
|
||||
packet.push(flags);
|
||||
packet
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_rejects_bad_magic_and_reserved_flags() {
|
||||
assert_eq!(
|
||||
decode(&[0, MAGIC[1], MAGIC[2], FLAG_OPEN], None, &[]),
|
||||
Err(ProtoError::BadMagic)
|
||||
);
|
||||
assert_eq!(
|
||||
decode(&header(FLAG_OPEN | 0x10), None, &[]),
|
||||
Err(ProtoError::ReservedFlags(0x10))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_enforces_required_and_rejected_flags() {
|
||||
assert_eq!(
|
||||
decode(&header(0), None, OPEN_REQUEST_RULES),
|
||||
Err(ProtoError::MissingFlag(FLAG_OPEN))
|
||||
);
|
||||
assert_eq!(
|
||||
decode(&header(FLAG_OPEN | FLAG_REPLY), None, OPEN_REQUEST_RULES),
|
||||
Err(ProtoError::UnexpectedFlag(FLAG_REPLY))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_rejects_hmac_presence_mismatch() {
|
||||
let mut authenticated = header(FLAG_OPEN | FLAG_HMAC);
|
||||
authenticated.extend_from_slice(&[0; HMAC_SIZE]);
|
||||
assert_eq!(
|
||||
decode(&authenticated, None, OPEN_REQUEST_RULES),
|
||||
Err(ProtoError::HmacPresenceMismatch)
|
||||
);
|
||||
assert_eq!(
|
||||
decode(&header(FLAG_OPEN), Some(KEY), OPEN_REQUEST_RULES),
|
||||
Err(ProtoError::HmacPresenceMismatch)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn structural_decode_reports_hmac_presence_without_a_key() {
|
||||
let mut authenticated = header(FLAG_OPEN | FLAG_HMAC);
|
||||
authenticated.extend_from_slice(&[0xff; HMAC_SIZE]);
|
||||
|
||||
let envelope = decode_structural(&authenticated).unwrap();
|
||||
assert!(envelope.hmac_present);
|
||||
assert_eq!(envelope.body_offset, HEADER_SIZE + HMAC_SIZE);
|
||||
|
||||
let envelope = decode_structural(&header(FLAG_OPEN)).unwrap();
|
||||
assert!(!envelope.hmac_present);
|
||||
assert_eq!(envelope.body_offset, HEADER_SIZE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_rejects_bad_hmac() {
|
||||
let mut packet = begin_checked(FLAG_OPEN, Some(KEY), OPEN_REQUEST_RULES, 22).unwrap();
|
||||
packet.extend_from_slice(&[1, 2]);
|
||||
let mut packet = finish(packet, Some(KEY)).unwrap();
|
||||
*packet.last_mut().unwrap() ^= 0x80;
|
||||
|
||||
decode(&packet, Some(KEY), OPEN_REQUEST_RULES).unwrap();
|
||||
assert_eq!(verify(&packet, Some(KEY)), Err(ProtoError::BadHmac));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encoder_key_authoritatively_normalizes_hmac_flag() {
|
||||
let plain =
|
||||
begin_checked(FLAG_OPEN | FLAG_HMAC, None, OPEN_REQUEST_RULES, HEADER_SIZE).unwrap();
|
||||
assert_eq!(plain[3], FLAG_OPEN);
|
||||
|
||||
let authenticated = begin_checked(
|
||||
FLAG_OPEN,
|
||||
Some(KEY),
|
||||
OPEN_REQUEST_RULES,
|
||||
HEADER_SIZE + HMAC_SIZE,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(authenticated[3], FLAG_OPEN | FLAG_HMAC);
|
||||
}
|
||||
}
|
||||
@@ -96,76 +96,3 @@ pub fn verify_packet_hmac(key: &[u8], packet: &[u8]) -> Result<()> {
|
||||
pub(crate) fn hmac_offset() -> usize {
|
||||
HEADER_SIZE
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
flags::{FLAG_HMAC, FLAG_OPEN},
|
||||
MAGIC,
|
||||
};
|
||||
|
||||
fn reference_copy_zero_hmac(
|
||||
key: &[u8],
|
||||
packet: &[u8],
|
||||
hmac_offset: usize,
|
||||
) -> Result<[u8; HMAC_SIZE]> {
|
||||
if packet.len().saturating_sub(hmac_offset) < HMAC_SIZE {
|
||||
return Err(ProtoError::InvalidHmacOffset);
|
||||
}
|
||||
let mut copy = packet.to_vec();
|
||||
copy[hmac_offset..hmac_offset + HMAC_SIZE].fill(0);
|
||||
let mut mac = HmacMd5::new_from_slice(key).expect("HMAC accepts keys of any size");
|
||||
mac.update(©);
|
||||
let bytes = mac.finalize().into_bytes();
|
||||
let mut out = [0u8; HMAC_SIZE];
|
||||
out.copy_from_slice(&bytes);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn packet_with_hmac_field(hmac_bytes: [u8; HMAC_SIZE]) -> Vec<u8> {
|
||||
let mut packet = Vec::new();
|
||||
packet.extend_from_slice(&MAGIC);
|
||||
packet.push(FLAG_OPEN | FLAG_HMAC);
|
||||
packet.extend_from_slice(&hmac_bytes);
|
||||
packet.extend_from_slice(&[1, 2, 3, 4]);
|
||||
packet
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_and_verify_hmac() {
|
||||
let mut packet = packet_with_hmac_field([0; HMAC_SIZE]);
|
||||
|
||||
compute_hmac_in_place(b"testkey", &mut packet, hmac_offset()).unwrap();
|
||||
verify_hmac(b"testkey", &packet, hmac_offset()).unwrap();
|
||||
assert_eq!(
|
||||
verify_hmac(b"wrong", &packet, hmac_offset()),
|
||||
Err(ProtoError::BadHmac)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_hmac_matches_copy_and_zero_semantics() {
|
||||
let mut packet = packet_with_hmac_field([0xa5; HMAC_SIZE]);
|
||||
packet.extend_from_slice(&[5, 6, 7, 8, 9]);
|
||||
|
||||
let digest = compute_hmac(b"testkey", &packet, hmac_offset()).unwrap();
|
||||
let reference = reference_copy_zero_hmac(b"testkey", &packet, hmac_offset()).unwrap();
|
||||
|
||||
assert_eq!(digest, reference);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_hmac_rejects_modified_authenticated_byte() {
|
||||
let mut packet = packet_with_hmac_field([0; HMAC_SIZE]);
|
||||
compute_hmac_in_place(b"testkey", &mut packet, hmac_offset()).unwrap();
|
||||
|
||||
verify_hmac(b"testkey", &packet, hmac_offset()).unwrap();
|
||||
packet[hmac_offset() + HMAC_SIZE + 1] ^= 0x01;
|
||||
|
||||
assert_eq!(
|
||||
verify_hmac(b"testkey", &packet, hmac_offset()),
|
||||
Err(ProtoError::BadHmac)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -162,194 +162,3 @@ pub fn echo_packet_len(hmac: bool, params: &Params) -> Result<usize> {
|
||||
};
|
||||
Ok(header_len.max(requested))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::params::{Clock, ReceivedStats, StampAt};
|
||||
|
||||
fn params(stats: ReceivedStats, stamp_at: StampAt, clock: Clock) -> Params {
|
||||
Params {
|
||||
received_stats: stats,
|
||||
stamp_at,
|
||||
clock,
|
||||
..Params::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn expected_optional_len(stats: ReceivedStats, stamp_at: StampAt, clock: Clock) -> usize {
|
||||
let stats_len = match stats {
|
||||
ReceivedStats::None => 0,
|
||||
ReceivedStats::Count => RECV_COUNT_SIZE,
|
||||
ReceivedStats::Window => RECV_WINDOW_SIZE,
|
||||
ReceivedStats::Both => RECV_COUNT_SIZE + RECV_WINDOW_SIZE,
|
||||
};
|
||||
let clock_count = match clock {
|
||||
Clock::Unspecified => 0,
|
||||
Clock::Wall | Clock::Monotonic => 1,
|
||||
Clock::Both => 2,
|
||||
};
|
||||
let timestamp_groups = match stamp_at {
|
||||
StampAt::None => 0,
|
||||
StampAt::Send | StampAt::Receive | StampAt::Midpoint => 1,
|
||||
StampAt::Both => 2,
|
||||
};
|
||||
stats_len + timestamp_groups * clock_count * TIMESTAMP_SIZE
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verified_layout_lengths() {
|
||||
assert_eq!(PacketLayout::open_request(false).header_len(), 4);
|
||||
assert_eq!(PacketLayout::open_request(true).header_len(), 20);
|
||||
assert_eq!(PacketLayout::open_reply(false).header_len(), 12);
|
||||
assert_eq!(PacketLayout::open_reply(true).header_len(), 28);
|
||||
assert_eq!(PacketLayout::close_request(false).header_len(), 12);
|
||||
assert_eq!(PacketLayout::close_request(true).header_len(), 28);
|
||||
assert_eq!(
|
||||
echo_header_len(
|
||||
false,
|
||||
¶ms(ReceivedStats::None, StampAt::None, Clock::Both)
|
||||
),
|
||||
16
|
||||
);
|
||||
assert_eq!(
|
||||
echo_header_len(
|
||||
false,
|
||||
¶ms(ReceivedStats::Count, StampAt::Send, Clock::Wall)
|
||||
),
|
||||
28
|
||||
);
|
||||
assert_eq!(
|
||||
echo_header_len(
|
||||
false,
|
||||
¶ms(ReceivedStats::Window, StampAt::Receive, Clock::Monotonic)
|
||||
),
|
||||
32
|
||||
);
|
||||
assert_eq!(
|
||||
echo_header_len(
|
||||
false,
|
||||
¶ms(ReceivedStats::Both, StampAt::Midpoint, Clock::Both)
|
||||
),
|
||||
44
|
||||
);
|
||||
assert_eq!(
|
||||
echo_header_len(
|
||||
false,
|
||||
¶ms(ReceivedStats::Both, StampAt::Both, Clock::Both)
|
||||
),
|
||||
60
|
||||
);
|
||||
assert_eq!(
|
||||
echo_header_len(
|
||||
true,
|
||||
¶ms(ReceivedStats::Both, StampAt::Both, Clock::Both)
|
||||
),
|
||||
76
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_matrix_matches_stats_timestamps_clock_and_hmac_rules() {
|
||||
for stats in [
|
||||
ReceivedStats::None,
|
||||
ReceivedStats::Count,
|
||||
ReceivedStats::Window,
|
||||
ReceivedStats::Both,
|
||||
] {
|
||||
for stamp_at in [
|
||||
StampAt::None,
|
||||
StampAt::Send,
|
||||
StampAt::Receive,
|
||||
StampAt::Both,
|
||||
StampAt::Midpoint,
|
||||
] {
|
||||
for clock in [Clock::Wall, Clock::Monotonic, Clock::Both] {
|
||||
for hmac in [false, true] {
|
||||
let params = params(stats, stamp_at, clock);
|
||||
let layout = PacketLayout::echo(hmac, ¶ms);
|
||||
let expected_len = HEADER_SIZE
|
||||
+ TOKEN_SIZE
|
||||
+ SEQ_SIZE
|
||||
+ if hmac { HMAC_SIZE } else { 0 }
|
||||
+ expected_optional_len(stats, stamp_at, clock);
|
||||
|
||||
assert_eq!(
|
||||
layout.header_len(),
|
||||
expected_len,
|
||||
"unexpected length for stats={stats:?} stamp_at={stamp_at:?} clock={clock:?} hmac={hmac}"
|
||||
);
|
||||
assert_eq!(layout.recv_count, stats.has_count());
|
||||
assert_eq!(layout.recv_window, stats.has_window());
|
||||
assert_eq!(
|
||||
layout.recv_wall,
|
||||
matches!(stamp_at, StampAt::Receive | StampAt::Both)
|
||||
&& clock.has_wall()
|
||||
);
|
||||
assert_eq!(
|
||||
layout.recv_mono,
|
||||
matches!(stamp_at, StampAt::Receive | StampAt::Both)
|
||||
&& clock.has_mono()
|
||||
);
|
||||
assert_eq!(
|
||||
layout.midpoint_wall,
|
||||
matches!(stamp_at, StampAt::Midpoint) && clock.has_wall()
|
||||
);
|
||||
assert_eq!(
|
||||
layout.midpoint_mono,
|
||||
matches!(stamp_at, StampAt::Midpoint) && clock.has_mono()
|
||||
);
|
||||
assert_eq!(
|
||||
layout.send_wall,
|
||||
matches!(stamp_at, StampAt::Send | StampAt::Both) && clock.has_wall()
|
||||
);
|
||||
assert_eq!(
|
||||
layout.send_mono,
|
||||
matches!(stamp_at, StampAt::Send | StampAt::Both) && clock.has_mono()
|
||||
);
|
||||
|
||||
if hmac {
|
||||
assert_eq!(
|
||||
echo_header_len(true, ¶ms) - echo_header_len(false, ¶ms),
|
||||
HMAC_SIZE
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negotiated_length_is_floored_at_the_required_field_block() {
|
||||
// The 60-byte header of this layout is the floor. A negative negotiated
|
||||
// length is not an error: it asks for nothing beyond the mandatory
|
||||
// fields, exactly as zero does.
|
||||
let mut p = params(ReceivedStats::Both, StampAt::Both, Clock::Both);
|
||||
for (length, expected) in [(-4096, 60), (-1, 60), (0, 60), (20, 60), (92, 92)] {
|
||||
p.length = length;
|
||||
assert_eq!(
|
||||
echo_packet_len(false, &p),
|
||||
Ok(expected),
|
||||
"unexpected packet length for negotiated length {length}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A positive length wider than `usize` must stay an error rather than
|
||||
/// becoming a saturated buffer size. Only reachable where `usize` is
|
||||
/// narrower than `i64`; on a 64-bit target every positive `i64` converts.
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
#[test]
|
||||
fn a_length_wider_than_usize_is_rejected_rather_than_saturated() {
|
||||
let mut p = params(ReceivedStats::Both, StampAt::Both, Clock::Both);
|
||||
p.length = 5_000_000_000;
|
||||
|
||||
assert_eq!(
|
||||
echo_packet_len(false, &p),
|
||||
Err(ProtoError::PacketLengthUnrepresentable {
|
||||
length: 5_000_000_000
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -98,53 +98,3 @@ pub fn decode_open_reply(packet: &[u8], hmac_key: Option<&[u8]>) -> Result<OpenR
|
||||
params: Params::decode(&packet[pos..])?,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
hmac,
|
||||
params::{Clock, ReceivedStats, StampAt},
|
||||
FLAG_HMAC, HMAC_SIZE,
|
||||
};
|
||||
|
||||
fn default_params() -> Params {
|
||||
Params {
|
||||
protocol_version: 1,
|
||||
duration_ns: 3_000_000_000,
|
||||
interval_ns: 1_000_000_000,
|
||||
received_stats: ReceivedStats::Both,
|
||||
stamp_at: StampAt::Both,
|
||||
clock: Clock::Both,
|
||||
..Params::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_reply_decodes_token() {
|
||||
let mut packet = vec![0x14, 0xa7, 0x5b, 0x03];
|
||||
packet.extend_from_slice(&0x7896_b6ab_8771_5213u64.to_le_bytes());
|
||||
packet.extend_from_slice(&default_params().encode());
|
||||
|
||||
let reply = decode_open_reply(&packet, None).unwrap();
|
||||
assert_eq!(reply.token, 0x7896_b6ab_8771_5213);
|
||||
assert_eq!(reply.params, default_params());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hmac_open_reply_decodes_token_after_hmac_field() {
|
||||
let mut packet = vec![0x14, 0xa7, 0x5b, FLAG_OPEN | FLAG_REPLY | FLAG_HMAC];
|
||||
packet.extend_from_slice(&[0; HMAC_SIZE]);
|
||||
packet.extend_from_slice(&0x7896_b6ab_8771_5213u64.to_le_bytes());
|
||||
packet.extend_from_slice(&default_params().encode());
|
||||
hmac::compute_hmac_in_place(b"testkey", &mut packet, hmac::hmac_offset()).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
packet.len(),
|
||||
4 + HMAC_SIZE + TOKEN_SIZE + default_params().encode().len()
|
||||
);
|
||||
let reply = decode_open_reply(&packet, Some(b"testkey")).unwrap();
|
||||
assert_eq!(reply.token, 0x7896_b6ab_8771_5213);
|
||||
assert_eq!(reply.params, default_params());
|
||||
}
|
||||
}
|
||||
@@ -368,773 +368,3 @@ fn read_int(input: &[u8], pos: &mut usize) -> Result<i64> {
|
||||
*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() {
|
||||
// `Params::dscp` is a raw wire byte; encode/decode carry whatever
|
||||
// value is set without interpreting or shifting it, regardless of
|
||||
// whether it happens to look like a codepoint (46) or a raw TOS byte
|
||||
// (184).
|
||||
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);
|
||||
// The wire default clock is zero, not `Both`.
|
||||
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() {
|
||||
// Duration and Interval are the pair a server must tell apart: absent is
|
||||
// accepted, explicitly zero is not.
|
||||
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(),
|
||||
);
|
||||
// Malformed payloads must fail identically through both entry points.
|
||||
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));
|
||||
}
|
||||
|
||||
mod properties {
|
||||
use super::*;
|
||||
use proptest::prelude::*;
|
||||
|
||||
fn received_stats_strategy() -> impl Strategy<Value = ReceivedStats> {
|
||||
prop_oneof![
|
||||
Just(ReceivedStats::None),
|
||||
Just(ReceivedStats::Count),
|
||||
Just(ReceivedStats::Window),
|
||||
Just(ReceivedStats::Both),
|
||||
]
|
||||
}
|
||||
|
||||
fn stamp_at_strategy() -> impl Strategy<Value = StampAt> {
|
||||
prop_oneof![
|
||||
Just(StampAt::None),
|
||||
Just(StampAt::Send),
|
||||
Just(StampAt::Receive),
|
||||
Just(StampAt::Both),
|
||||
Just(StampAt::Midpoint),
|
||||
]
|
||||
}
|
||||
|
||||
/// A `Clock` valid for direct construction, including `Unspecified`
|
||||
/// (wire omission). Never produces an explicit zero on the wire; see
|
||||
/// `explicit_clock_strategy` for that.
|
||||
fn clock_strategy() -> impl Strategy<Value = Clock> {
|
||||
prop_oneof![
|
||||
Just(Clock::Unspecified),
|
||||
Just(Clock::Wall),
|
||||
Just(Clock::Monotonic),
|
||||
Just(Clock::Both),
|
||||
]
|
||||
}
|
||||
|
||||
/// A `Clock` value valid to encode *explicitly* on the wire (tag 7).
|
||||
/// Excludes `Unspecified`/zero, which is only ever reached by omission.
|
||||
fn explicit_clock_value_strategy() -> impl Strategy<Value = i64> {
|
||||
1_i64..=3
|
||||
}
|
||||
|
||||
fn server_fill_strategy() -> impl Strategy<Value = Option<ServerFill>> {
|
||||
prop_oneof![
|
||||
Just(None),
|
||||
proptest::collection::vec(proptest::char::any(), 0..=8).prop_map(|chars| Some(
|
||||
ServerFill {
|
||||
value: chars.into_iter().collect(),
|
||||
}
|
||||
)),
|
||||
]
|
||||
}
|
||||
|
||||
fn params_strategy() -> impl Strategy<Value = Params> {
|
||||
(
|
||||
any::<i64>(),
|
||||
any::<i64>(),
|
||||
any::<i64>(),
|
||||
any::<i64>(),
|
||||
any::<i64>(),
|
||||
received_stats_strategy(),
|
||||
stamp_at_strategy(),
|
||||
clock_strategy(),
|
||||
server_fill_strategy(),
|
||||
)
|
||||
.prop_map(
|
||||
|(
|
||||
protocol_version,
|
||||
duration_ns,
|
||||
interval_ns,
|
||||
length,
|
||||
dscp,
|
||||
received_stats,
|
||||
stamp_at,
|
||||
clock,
|
||||
server_fill,
|
||||
)| Params {
|
||||
protocol_version,
|
||||
duration_ns,
|
||||
interval_ns,
|
||||
length,
|
||||
received_stats,
|
||||
stamp_at,
|
||||
clock,
|
||||
dscp,
|
||||
server_fill,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn encode_tagged_int(tag: u64, value: i64, out: &mut Vec<u8>) {
|
||||
varint::encode_uvarint(tag, out);
|
||||
varint::encode_varint(value, out);
|
||||
}
|
||||
|
||||
fn encode_tagged_server_fill(value: &str, out: &mut Vec<u8>) {
|
||||
varint::encode_uvarint(9, out);
|
||||
varint::encode_uvarint(value.len() as u64, out);
|
||||
out.extend_from_slice(value.as_bytes());
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn valid_params_round_trip(params in params_strategy()) {
|
||||
let encoded = params.encode();
|
||||
let decoded = Params::decode(&encoded).unwrap();
|
||||
prop_assert_eq!(decoded, params);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encoded_param_presence_matches_omission_rules(params in params_strategy()) {
|
||||
let encoded = params.encode();
|
||||
let decoded = Params::decode_with_presence(&encoded).unwrap();
|
||||
prop_assert_eq!(&decoded.params, ¶ms);
|
||||
|
||||
prop_assert_eq!(decoded.presence.protocol_version, params.protocol_version != 0);
|
||||
prop_assert_eq!(decoded.presence.duration_ns, params.duration_ns != 0);
|
||||
prop_assert_eq!(decoded.presence.interval_ns, params.interval_ns != 0);
|
||||
prop_assert_eq!(decoded.presence.length, params.length != 0);
|
||||
prop_assert_eq!(decoded.presence.dscp, params.dscp != 0);
|
||||
prop_assert_eq!(
|
||||
decoded.presence.received_stats,
|
||||
params.received_stats != ReceivedStats::None
|
||||
);
|
||||
prop_assert_eq!(decoded.presence.stamp_at, params.stamp_at != StampAt::None);
|
||||
prop_assert_eq!(decoded.presence.clock, params.clock != Clock::Unspecified);
|
||||
prop_assert_eq!(decoded.presence.server_fill, params.server_fill.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_protocol_version_is_last_wins(first: i64, second: i64) {
|
||||
let mut encoded = Vec::new();
|
||||
encode_tagged_int(1, first, &mut encoded);
|
||||
encode_tagged_int(1, second, &mut encoded);
|
||||
let decoded = Params::decode_with_presence(&encoded).unwrap();
|
||||
prop_assert_eq!(decoded.params.protocol_version, second);
|
||||
prop_assert!(decoded.presence.protocol_version);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_duration_ns_is_last_wins(first: i64, second: i64) {
|
||||
let mut encoded = Vec::new();
|
||||
encode_tagged_int(2, first, &mut encoded);
|
||||
encode_tagged_int(2, second, &mut encoded);
|
||||
let decoded = Params::decode_with_presence(&encoded).unwrap();
|
||||
prop_assert_eq!(decoded.params.duration_ns, second);
|
||||
prop_assert!(decoded.presence.duration_ns);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_interval_ns_is_last_wins(first: i64, second: i64) {
|
||||
let mut encoded = Vec::new();
|
||||
encode_tagged_int(3, first, &mut encoded);
|
||||
encode_tagged_int(3, second, &mut encoded);
|
||||
let decoded = Params::decode_with_presence(&encoded).unwrap();
|
||||
prop_assert_eq!(decoded.params.interval_ns, second);
|
||||
prop_assert!(decoded.presence.interval_ns);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_length_is_last_wins(first: i64, second: i64) {
|
||||
let mut encoded = Vec::new();
|
||||
encode_tagged_int(4, first, &mut encoded);
|
||||
encode_tagged_int(4, second, &mut encoded);
|
||||
let decoded = Params::decode_with_presence(&encoded).unwrap();
|
||||
prop_assert_eq!(decoded.params.length, second);
|
||||
prop_assert!(decoded.presence.length);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_received_stats_is_last_wins(first in 0_i64..=3, second in 0_i64..=3) {
|
||||
let mut encoded = Vec::new();
|
||||
encode_tagged_int(5, first, &mut encoded);
|
||||
encode_tagged_int(5, second, &mut encoded);
|
||||
let decoded = Params::decode_with_presence(&encoded).unwrap();
|
||||
prop_assert_eq!(decoded.params.received_stats, ReceivedStats::try_from(second).unwrap());
|
||||
prop_assert!(decoded.presence.received_stats);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_stamp_at_is_last_wins(first in 0_i64..=4, second in 0_i64..=4) {
|
||||
let mut encoded = Vec::new();
|
||||
encode_tagged_int(6, first, &mut encoded);
|
||||
encode_tagged_int(6, second, &mut encoded);
|
||||
let decoded = Params::decode_with_presence(&encoded).unwrap();
|
||||
prop_assert_eq!(decoded.params.stamp_at, StampAt::try_from(second).unwrap());
|
||||
prop_assert!(decoded.presence.stamp_at);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_clock_is_last_wins(
|
||||
first in explicit_clock_value_strategy(),
|
||||
second in explicit_clock_value_strategy(),
|
||||
) {
|
||||
let mut encoded = Vec::new();
|
||||
encode_tagged_int(7, first, &mut encoded);
|
||||
encode_tagged_int(7, second, &mut encoded);
|
||||
let decoded = Params::decode_with_presence(&encoded).unwrap();
|
||||
prop_assert_eq!(decoded.params.clock, Clock::try_from(second).unwrap());
|
||||
prop_assert!(decoded.presence.clock);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_dscp_is_last_wins(first: i64, second: i64) {
|
||||
let mut encoded = Vec::new();
|
||||
encode_tagged_int(8, first, &mut encoded);
|
||||
encode_tagged_int(8, second, &mut encoded);
|
||||
let decoded = Params::decode_with_presence(&encoded).unwrap();
|
||||
prop_assert_eq!(decoded.params.dscp, second);
|
||||
prop_assert!(decoded.presence.dscp);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_server_fill_is_last_wins(
|
||||
first in proptest::collection::vec(proptest::char::any(), 0..=8)
|
||||
.prop_map(|chars| chars.into_iter().collect::<String>()),
|
||||
second in proptest::collection::vec(proptest::char::any(), 0..=8)
|
||||
.prop_map(|chars| chars.into_iter().collect::<String>()),
|
||||
) {
|
||||
let mut encoded = Vec::new();
|
||||
encode_tagged_server_fill(&first, &mut encoded);
|
||||
encode_tagged_server_fill(&second, &mut encoded);
|
||||
let decoded = Params::decode_with_presence(&encoded).unwrap();
|
||||
prop_assert_eq!(
|
||||
decoded.params.server_fill,
|
||||
Some(ServerFill { value: second })
|
||||
);
|
||||
prop_assert!(decoded.presence.server_fill);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_scalar_tag_prefix_does_not_disturb_known_params(
|
||||
params in params_strategy(),
|
||||
unknown_tag in 10_u64..=u16::MAX as u64,
|
||||
unknown_value: u64,
|
||||
) {
|
||||
let mut prefixed = Vec::new();
|
||||
varint::encode_uvarint(unknown_tag, &mut prefixed);
|
||||
varint::encode_uvarint(unknown_value, &mut prefixed);
|
||||
prefixed.extend_from_slice(¶ms.encode());
|
||||
|
||||
let decoded = Params::decode(&prefixed).unwrap();
|
||||
prop_assert_eq!(decoded, params.clone());
|
||||
|
||||
let decoded_with_presence = Params::decode_with_presence(&prefixed).unwrap();
|
||||
let expected_presence = Params::decode_with_presence(¶ms.encode()).unwrap().presence;
|
||||
prop_assert_eq!(decoded_with_presence.presence, expected_presence);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_scalar_tag_suffix_does_not_disturb_known_params(
|
||||
params in params_strategy(),
|
||||
unknown_tag in 10_u64..=u16::MAX as u64,
|
||||
unknown_value: u64,
|
||||
) {
|
||||
let mut suffixed = params.encode();
|
||||
varint::encode_uvarint(unknown_tag, &mut suffixed);
|
||||
varint::encode_uvarint(unknown_value, &mut suffixed);
|
||||
|
||||
let decoded = Params::decode(&suffixed).unwrap();
|
||||
prop_assert_eq!(decoded, params.clone());
|
||||
|
||||
let decoded_with_presence = Params::decode_with_presence(&suffixed).unwrap();
|
||||
let expected_presence = Params::decode_with_presence(¶ms.encode()).unwrap().presence;
|
||||
prop_assert_eq!(decoded_with_presence.presence, expected_presence);
|
||||
}
|
||||
}
|
||||
|
||||
/// Omitted-vs-explicit-zero presence, for the fields where explicit
|
||||
/// zero is valid on the wire. `Clock` is deliberately excluded: an
|
||||
/// explicit zero there is rejected outright, not merely "present with
|
||||
/// the default value" (see `explicit_clock_zero_is_rejected`).
|
||||
#[test]
|
||||
fn omitted_vs_explicit_zero_presence_for_zero_valid_fields() {
|
||||
let scalar_tags = [1_u64, 2, 3, 4, 8];
|
||||
for tag in scalar_tags {
|
||||
let omitted = Params::decode_with_presence(&[]).unwrap();
|
||||
let mut explicit_zero = Vec::new();
|
||||
encode_tagged_int(tag, 0, &mut explicit_zero);
|
||||
let explicit = Params::decode_with_presence(&explicit_zero).unwrap();
|
||||
|
||||
assert_eq!(omitted.params, explicit.params);
|
||||
assert_eq!(explicit.params, Params::default());
|
||||
|
||||
let (omitted_presence, explicit_presence) = match tag {
|
||||
1 => (
|
||||
omitted.presence.protocol_version,
|
||||
explicit.presence.protocol_version,
|
||||
),
|
||||
2 => (omitted.presence.duration_ns, explicit.presence.duration_ns),
|
||||
3 => (omitted.presence.interval_ns, explicit.presence.interval_ns),
|
||||
4 => (omitted.presence.length, explicit.presence.length),
|
||||
8 => (omitted.presence.dscp, explicit.presence.dscp),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
assert!(!omitted_presence);
|
||||
assert!(explicit_presence);
|
||||
}
|
||||
|
||||
let enum_tags = [5_u64, 6];
|
||||
for tag in enum_tags {
|
||||
let omitted = Params::decode_with_presence(&[]).unwrap();
|
||||
let mut explicit_zero = Vec::new();
|
||||
encode_tagged_int(tag, 0, &mut explicit_zero);
|
||||
let explicit = Params::decode_with_presence(&explicit_zero).unwrap();
|
||||
|
||||
assert_eq!(omitted.params, explicit.params);
|
||||
|
||||
let (omitted_presence, explicit_presence) = match tag {
|
||||
5 => (
|
||||
omitted.presence.received_stats,
|
||||
explicit.presence.received_stats,
|
||||
),
|
||||
6 => (omitted.presence.stamp_at, explicit.presence.stamp_at),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
assert!(!omitted_presence);
|
||||
assert!(explicit_presence);
|
||||
}
|
||||
|
||||
// `Clock` deliberately breaks the pattern above: explicit zero is
|
||||
// rejected rather than accepted-but-present.
|
||||
let omitted = Params::decode_with_presence(&[]).unwrap();
|
||||
assert_eq!(omitted.params.clock, Clock::Unspecified);
|
||||
assert!(!omitted.presence.clock);
|
||||
let mut explicit_clock_zero = Vec::new();
|
||||
encode_tagged_int(7, 0, &mut explicit_clock_zero);
|
||||
assert_eq!(
|
||||
Params::decode_with_presence(&explicit_clock_zero),
|
||||
Err(ProtoError::InvalidEnum {
|
||||
name: "Clock",
|
||||
value: 0,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,112 +59,3 @@ pub fn decode_varint(input: &[u8]) -> Result<(i64, usize)> {
|
||||
let (value, used) = decode_uvarint(input)?;
|
||||
Ok((zigzag_decode(value), used))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn uvarint_round_trips() {
|
||||
for value in [
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
127,
|
||||
128,
|
||||
255,
|
||||
16_384,
|
||||
u32::MAX as u64,
|
||||
i64::MAX as u64,
|
||||
u64::MAX,
|
||||
] {
|
||||
let mut encoded = Vec::new();
|
||||
encode_uvarint(value, &mut encoded);
|
||||
assert_eq!(decode_uvarint(&encoded), Ok((value, encoded.len())));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signed_varint_round_trips() {
|
||||
for value in [
|
||||
i64::MIN,
|
||||
-9_223_372_036,
|
||||
-2,
|
||||
-1,
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
1_000_000_000,
|
||||
3_000_000_000,
|
||||
i64::MAX,
|
||||
] {
|
||||
let mut encoded = Vec::new();
|
||||
encode_varint(value, &mut encoded);
|
||||
assert_eq!(decode_varint(&encoded), Ok((value, encoded.len())));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verified_byte_examples() {
|
||||
let cases: &[(i64, &[u8])] = &[
|
||||
(1, &[0x02]),
|
||||
(3_000_000_000, &[0x80, 0xf8, 0x82, 0xad, 0x16]),
|
||||
(1_000_000_000, &[0x80, 0xa8, 0xd6, 0xb9, 0x07]),
|
||||
(1472, &[0x80, 0x17]),
|
||||
(3, &[0x06]),
|
||||
(184, &[0xf0, 0x02]),
|
||||
];
|
||||
for (value, expected) in cases {
|
||||
let mut encoded = Vec::new();
|
||||
encode_varint(*value, &mut encoded);
|
||||
assert_eq!(&encoded, expected);
|
||||
assert_eq!(decode_varint(expected), Ok((*value, expected.len())));
|
||||
}
|
||||
|
||||
let mut tag = Vec::new();
|
||||
encode_uvarint(9, &mut tag);
|
||||
assert_eq!(tag, [0x09]);
|
||||
tag.clear();
|
||||
encode_uvarint(24, &mut tag);
|
||||
assert_eq!(tag, [0x18]);
|
||||
}
|
||||
|
||||
mod properties {
|
||||
use super::*;
|
||||
use proptest::prelude::*;
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn uvarint_round_trips(value: u64) {
|
||||
let mut encoded = Vec::new();
|
||||
encode_uvarint(value, &mut encoded);
|
||||
prop_assert!(!encoded.is_empty());
|
||||
prop_assert!(encoded.len() <= 10);
|
||||
let (decoded, used) = decode_uvarint(&encoded).unwrap();
|
||||
prop_assert_eq!(decoded, value);
|
||||
prop_assert_eq!(used, encoded.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signed_varint_round_trips(value: i64) {
|
||||
let mut encoded = Vec::new();
|
||||
encode_varint(value, &mut encoded);
|
||||
prop_assert!(!encoded.is_empty());
|
||||
prop_assert!(encoded.len() <= 10);
|
||||
let (decoded, used) = decode_varint(&encoded).unwrap();
|
||||
prop_assert_eq!(decoded, value);
|
||||
prop_assert_eq!(used, encoded.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zigzag_round_trips_from_signed(value: i64) {
|
||||
prop_assert_eq!(zigzag_decode(zigzag_encode(value)), value);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zigzag_round_trips_from_unsigned(value: u64) {
|
||||
prop_assert_eq!(zigzag_encode(zigzag_decode(value)), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
use irtt_proto::{compute_hmac, compute_hmac_in_place, verify_hmac, ProtoError};
|
||||
|
||||
fn captured_open() -> Vec<u8> {
|
||||
// Captured vector 6; provenance and independently checked digest are in
|
||||
// docs/protocol/test-vectors/README.md. This is an oracle for the primitive.
|
||||
"14 a7 5b 09 ff 90 16 a7 aa 53 78 16 9e c3 a2 d5 54 dc 30 36
|
||||
01 02 02 80 d0 ac f3 0e 03 80 a8 d6 b9 07 05 06 06 06 07 06"
|
||||
.split_whitespace()
|
||||
.map(|byte| u8::from_str_radix(byte, 16).unwrap())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn computing_hmac_ignores_existing_digest_and_only_replaces_its_field() {
|
||||
let expected = captured_open();
|
||||
let mut packet = expected.clone();
|
||||
packet[4..20].fill(0xa5);
|
||||
let before = packet.clone();
|
||||
assert_eq!(
|
||||
compute_hmac(b"testkey", &packet, 4).unwrap().as_slice(),
|
||||
&expected[4..20]
|
||||
);
|
||||
assert_eq!(packet, before);
|
||||
compute_hmac_in_place(b"testkey", &mut packet, 4).unwrap();
|
||||
assert_eq!(packet, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verification_rejects_wrong_key_and_modified_authenticated_bytes() {
|
||||
let packet = captured_open();
|
||||
verify_hmac(b"testkey", &packet, 4).unwrap();
|
||||
assert_eq!(verify_hmac(b"wrong", &packet, 4), Err(ProtoError::BadHmac));
|
||||
for offset in [0, 4, 21, packet.len() - 1] {
|
||||
let mut modified = packet.clone();
|
||||
modified[offset] ^= 1;
|
||||
assert_eq!(
|
||||
verify_hmac(b"testkey", &modified, 4),
|
||||
Err(ProtoError::BadHmac)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incomplete_or_out_of_bounds_digest_fields_are_rejected_without_mutation() {
|
||||
for offset in [5, 20, 21, usize::MAX] {
|
||||
let mut packet = vec![0xa5; 20];
|
||||
assert_eq!(
|
||||
compute_hmac(b"key", &packet, offset),
|
||||
Err(ProtoError::InvalidHmacOffset)
|
||||
);
|
||||
assert_eq!(
|
||||
verify_hmac(b"key", &packet, offset),
|
||||
Err(ProtoError::InvalidHmacOffset)
|
||||
);
|
||||
assert_eq!(
|
||||
compute_hmac_in_place(b"key", &mut packet, offset),
|
||||
Err(ProtoError::InvalidHmacOffset)
|
||||
);
|
||||
assert_eq!(packet, [0xa5; 20]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
use irtt_proto::params::{Clock, ReceivedStats, StampAt};
|
||||
use irtt_proto::{
|
||||
echo_header_len, echo_packet_len, PacketLayout, Params, HMAC_SIZE, RECV_COUNT_SIZE,
|
||||
RECV_WINDOW_SIZE, SEQ_SIZE, TIMESTAMP_SIZE, TOKEN_SIZE,
|
||||
};
|
||||
|
||||
const HEADER_SIZE: usize = 4;
|
||||
|
||||
fn params(stats: ReceivedStats, stamp_at: StampAt, clock: Clock) -> Params {
|
||||
Params {
|
||||
received_stats: stats,
|
||||
stamp_at,
|
||||
clock,
|
||||
..Params::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn expected_optional_len(stats: ReceivedStats, stamp_at: StampAt, clock: Clock) -> usize {
|
||||
let stats_len = match stats {
|
||||
ReceivedStats::None => 0,
|
||||
ReceivedStats::Count => RECV_COUNT_SIZE,
|
||||
ReceivedStats::Window => RECV_WINDOW_SIZE,
|
||||
ReceivedStats::Both => RECV_COUNT_SIZE + RECV_WINDOW_SIZE,
|
||||
};
|
||||
let clock_count = match clock {
|
||||
Clock::Unspecified => 0,
|
||||
Clock::Wall | Clock::Monotonic => 1,
|
||||
Clock::Both => 2,
|
||||
};
|
||||
let timestamp_groups = match stamp_at {
|
||||
StampAt::None => 0,
|
||||
StampAt::Send | StampAt::Receive | StampAt::Midpoint => 1,
|
||||
StampAt::Both => 2,
|
||||
};
|
||||
stats_len + timestamp_groups * clock_count * TIMESTAMP_SIZE
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_and_close_layout_lengths() {
|
||||
assert_eq!(PacketLayout::open_request(false).header_len(), 4);
|
||||
assert_eq!(PacketLayout::open_request(true).header_len(), 20);
|
||||
assert_eq!(PacketLayout::open_reply(false).header_len(), 12);
|
||||
assert_eq!(PacketLayout::open_reply(true).header_len(), 28);
|
||||
assert_eq!(PacketLayout::close_request(false).header_len(), 12);
|
||||
assert_eq!(PacketLayout::close_request(true).header_len(), 28);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_matrix_matches_stats_timestamps_clock_and_hmac_rules() {
|
||||
for stats in [
|
||||
ReceivedStats::None,
|
||||
ReceivedStats::Count,
|
||||
ReceivedStats::Window,
|
||||
ReceivedStats::Both,
|
||||
] {
|
||||
for stamp_at in [
|
||||
StampAt::None,
|
||||
StampAt::Send,
|
||||
StampAt::Receive,
|
||||
StampAt::Both,
|
||||
StampAt::Midpoint,
|
||||
] {
|
||||
for clock in [
|
||||
Clock::Unspecified,
|
||||
Clock::Wall,
|
||||
Clock::Monotonic,
|
||||
Clock::Both,
|
||||
] {
|
||||
for hmac in [false, true] {
|
||||
let params = params(stats, stamp_at, clock);
|
||||
let layout = PacketLayout::echo(hmac, ¶ms);
|
||||
let expected_len = HEADER_SIZE
|
||||
+ TOKEN_SIZE
|
||||
+ SEQ_SIZE
|
||||
+ if hmac { HMAC_SIZE } else { 0 }
|
||||
+ expected_optional_len(stats, stamp_at, clock);
|
||||
|
||||
assert_eq!(
|
||||
layout.header_len(),
|
||||
expected_len,
|
||||
"unexpected length for stats={stats:?} stamp_at={stamp_at:?} clock={clock:?} hmac={hmac}"
|
||||
);
|
||||
assert_eq!(
|
||||
layout.recv_count,
|
||||
matches!(stats, ReceivedStats::Count | ReceivedStats::Both)
|
||||
);
|
||||
assert_eq!(
|
||||
layout.recv_window,
|
||||
matches!(stats, ReceivedStats::Window | ReceivedStats::Both)
|
||||
);
|
||||
assert_eq!(
|
||||
layout.recv_wall,
|
||||
matches!(stamp_at, StampAt::Receive | StampAt::Both)
|
||||
&& matches!(clock, Clock::Wall | Clock::Both)
|
||||
);
|
||||
assert_eq!(
|
||||
layout.recv_mono,
|
||||
matches!(stamp_at, StampAt::Receive | StampAt::Both)
|
||||
&& matches!(clock, Clock::Monotonic | Clock::Both)
|
||||
);
|
||||
assert_eq!(
|
||||
layout.midpoint_wall,
|
||||
matches!(stamp_at, StampAt::Midpoint)
|
||||
&& matches!(clock, Clock::Wall | Clock::Both)
|
||||
);
|
||||
assert_eq!(
|
||||
layout.midpoint_mono,
|
||||
matches!(stamp_at, StampAt::Midpoint)
|
||||
&& matches!(clock, Clock::Monotonic | Clock::Both)
|
||||
);
|
||||
assert_eq!(
|
||||
layout.send_wall,
|
||||
matches!(stamp_at, StampAt::Send | StampAt::Both)
|
||||
&& matches!(clock, Clock::Wall | Clock::Both)
|
||||
);
|
||||
assert_eq!(
|
||||
layout.send_mono,
|
||||
matches!(stamp_at, StampAt::Send | StampAt::Both)
|
||||
&& matches!(clock, Clock::Monotonic | Clock::Both)
|
||||
);
|
||||
|
||||
if hmac {
|
||||
assert_eq!(
|
||||
echo_header_len(true, ¶ms) - echo_header_len(false, ¶ms),
|
||||
HMAC_SIZE
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negotiated_length_is_floored_at_the_required_field_block() {
|
||||
// The 60-byte header of this layout is the floor. A negative negotiated
|
||||
// length is not an error: it asks for nothing beyond the mandatory
|
||||
// fields, exactly as zero does.
|
||||
let mut p = params(ReceivedStats::Both, StampAt::Both, Clock::Both);
|
||||
for (length, expected) in [(-4096, 60), (-1, 60), (0, 60), (20, 60), (92, 92)] {
|
||||
p.length = length;
|
||||
assert_eq!(
|
||||
echo_packet_len(false, &p),
|
||||
Ok(expected),
|
||||
"unexpected packet length for negotiated length {length}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A positive length wider than `usize` must stay an error rather than
|
||||
/// becoming a saturated buffer size. Only reachable where `usize` is
|
||||
/// narrower than `i64`; on a 64-bit target every positive `i64` converts.
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
#[test]
|
||||
fn a_length_wider_than_usize_is_rejected_rather_than_saturated() {
|
||||
let mut p = params(ReceivedStats::Both, StampAt::Both, Clock::Both);
|
||||
p.length = 5_000_000_000;
|
||||
|
||||
assert_eq!(
|
||||
echo_packet_len(false, &p),
|
||||
Err(irtt_proto::ProtoError::PacketLengthUnrepresentable {
|
||||
length: 5_000_000_000
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
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, ¶ms);
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
+179
-127
@@ -54,34 +54,42 @@ fn echo_reply(params: &Params, flags: u8) -> EchoReply {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authenticated_reply_encoders_set_hmac_from_key() {
|
||||
fn reply_encoders_normalize_hmac_flags_from_the_key() {
|
||||
let params = params();
|
||||
let open_reply = OpenReply {
|
||||
flags: FLAG_OPEN | FLAG_REPLY,
|
||||
for key in [None, Some(KEY)] {
|
||||
for supplied_hmac in [0, FLAG_HMAC] {
|
||||
let auth = if key.is_some() { FLAG_HMAC } else { 0 };
|
||||
let open = OpenReply {
|
||||
flags: FLAG_OPEN | FLAG_REPLY | supplied_hmac,
|
||||
token: TOKEN,
|
||||
params: params.clone(),
|
||||
};
|
||||
let packet = encode_open_reply(&open_reply, Some(KEY)).unwrap();
|
||||
assert_eq!(packet[3], FLAG_OPEN | FLAG_REPLY | FLAG_HMAC);
|
||||
let packet = encode_open_reply(&open, key).unwrap();
|
||||
assert_eq!(packet[3], FLAG_OPEN | FLAG_REPLY | auth);
|
||||
let offset = if key.is_some() { 20 } else { 4 };
|
||||
assert_eq!(&packet[offset..offset + 8], &TOKEN.to_le_bytes());
|
||||
assert_eq!(
|
||||
decode_open_reply(&packet, Some(KEY)),
|
||||
Ok(OpenReply {
|
||||
flags: FLAG_OPEN | FLAG_REPLY | FLAG_HMAC,
|
||||
..open_reply
|
||||
})
|
||||
decode_open_reply(&packet, key).unwrap(),
|
||||
OpenReply {
|
||||
flags: packet[3],
|
||||
..open
|
||||
}
|
||||
);
|
||||
|
||||
let echo_reply = echo_reply(¶ms, FLAG_REPLY);
|
||||
let packet = encode_echo_reply(&echo_reply, ¶ms, Some(KEY)).unwrap();
|
||||
assert_eq!(packet[3], FLAG_REPLY | FLAG_HMAC);
|
||||
let echo = echo_reply(¶ms, FLAG_REPLY | supplied_hmac);
|
||||
let packet = encode_echo_reply(&echo, ¶ms, key).unwrap();
|
||||
assert_eq!(packet[3], FLAG_REPLY | auth);
|
||||
assert_eq!(&packet[offset..offset + 8], &TOKEN.to_le_bytes());
|
||||
assert_eq!(&packet[offset + 8..offset + 12], &u32::MAX.to_le_bytes());
|
||||
assert_eq!(
|
||||
decode_echo_reply(&packet, ¶ms, Some(KEY)),
|
||||
Ok(EchoReply {
|
||||
flags: FLAG_REPLY | FLAG_HMAC,
|
||||
..echo_reply
|
||||
})
|
||||
decode_echo_reply(&packet, ¶ms, key).unwrap(),
|
||||
EchoReply {
|
||||
flags: packet[3],
|
||||
..echo
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejected_open_reply_round_trips_and_zero_token_requires_close() {
|
||||
@@ -95,6 +103,12 @@ fn rejected_open_reply_round_trips_and_zero_token_requires_close() {
|
||||
};
|
||||
let packet = encode_open_reply(&rejected, None).unwrap();
|
||||
assert_eq!(decode_open_reply(&packet, None), Ok(rejected.clone()));
|
||||
let mut zero_without_close = packet.clone();
|
||||
zero_without_close[3] &= !FLAG_CLOSE;
|
||||
assert_eq!(
|
||||
decode_open_reply(&zero_without_close, None),
|
||||
Err(ProtoError::ZeroToken)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
encode_open_reply(
|
||||
@@ -119,63 +133,51 @@ fn peer_close_echo_reply_round_trips_without_a_close_reply_codec() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reply_encoders_supply_their_packet_type_rules() {
|
||||
fn reply_codecs_enforce_packet_type_and_reserved_flag_rules() {
|
||||
let params = Params::default();
|
||||
assert_eq!(
|
||||
encode_open_reply(
|
||||
&OpenReply {
|
||||
flags: FLAG_REPLY,
|
||||
for (flags, expected) in [
|
||||
(FLAG_REPLY, ProtoError::MissingFlag(FLAG_OPEN)),
|
||||
(FLAG_OPEN, ProtoError::MissingFlag(FLAG_REPLY)),
|
||||
(
|
||||
FLAG_OPEN | FLAG_REPLY | 0x10,
|
||||
ProtoError::ReservedFlags(0x10),
|
||||
),
|
||||
] {
|
||||
let open = OpenReply {
|
||||
flags,
|
||||
token: TOKEN,
|
||||
params: params.clone(),
|
||||
},
|
||||
None,
|
||||
),
|
||||
Err(ProtoError::MissingFlag(FLAG_OPEN))
|
||||
);
|
||||
assert_eq!(
|
||||
encode_echo_reply(
|
||||
&EchoReply {
|
||||
flags: 0,
|
||||
..echo_reply(¶ms, FLAG_REPLY)
|
||||
},
|
||||
¶ms,
|
||||
None,
|
||||
),
|
||||
Err(ProtoError::MissingFlag(FLAG_REPLY))
|
||||
);
|
||||
}
|
||||
|
||||
/// Every packet the reply encoders produce must be rejected by the inbound
|
||||
/// request decoder: a server never answers a datagram carrying `FLAG_REPLY`.
|
||||
#[test]
|
||||
fn encoded_replies_are_never_admitted_as_inbound_requests() {
|
||||
let params = Params::default();
|
||||
|
||||
for reply_flags in [FLAG_OPEN | FLAG_REPLY, FLAG_OPEN | FLAG_REPLY | FLAG_CLOSE] {
|
||||
let packet = encode_open_reply(
|
||||
};
|
||||
assert_eq!(encode_open_reply(&open, None).unwrap_err(), expected);
|
||||
let mut packet = encode_open_reply(
|
||||
&OpenReply {
|
||||
flags: reply_flags,
|
||||
token: if reply_flags & FLAG_CLOSE == 0 {
|
||||
TOKEN
|
||||
} else {
|
||||
0
|
||||
},
|
||||
params: params.clone(),
|
||||
flags: FLAG_OPEN | FLAG_REPLY,
|
||||
..open
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
decode_request(&packet),
|
||||
Err(ProtoError::UnexpectedFlag(FLAG_REPLY))
|
||||
);
|
||||
packet[3] = flags;
|
||||
assert_eq!(decode_open_reply(&packet, None).unwrap_err(), expected);
|
||||
}
|
||||
|
||||
for reply_flags in [FLAG_REPLY, FLAG_REPLY | FLAG_CLOSE] {
|
||||
let packet = encode_echo_reply(&echo_reply(¶ms, reply_flags), ¶ms, None).unwrap();
|
||||
for (flags, expected) in [
|
||||
(0, ProtoError::MissingFlag(FLAG_REPLY)),
|
||||
(
|
||||
FLAG_OPEN | FLAG_REPLY,
|
||||
ProtoError::UnexpectedFlag(FLAG_OPEN),
|
||||
),
|
||||
(FLAG_REPLY | 0x10, ProtoError::ReservedFlags(0x10)),
|
||||
] {
|
||||
assert_eq!(
|
||||
decode_request(&packet),
|
||||
Err(ProtoError::UnexpectedFlag(FLAG_REPLY))
|
||||
encode_echo_reply(&echo_reply(¶ms, flags), ¶ms, None).unwrap_err(),
|
||||
expected
|
||||
);
|
||||
let mut packet =
|
||||
encode_echo_reply(&echo_reply(¶ms, FLAG_REPLY), ¶ms, None).unwrap();
|
||||
packet[3] = flags;
|
||||
assert_eq!(
|
||||
decode_echo_reply(&packet, ¶ms, None).unwrap_err(),
|
||||
expected
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -207,7 +209,12 @@ fn echo_layout_combinations_round_trip() {
|
||||
StampAt::Both,
|
||||
StampAt::Midpoint,
|
||||
] {
|
||||
for clock in [Clock::Wall, Clock::Monotonic, Clock::Both] {
|
||||
for clock in [
|
||||
Clock::Unspecified,
|
||||
Clock::Wall,
|
||||
Clock::Monotonic,
|
||||
Clock::Both,
|
||||
] {
|
||||
for key in [None, Some(KEY)] {
|
||||
let params = Params {
|
||||
protocol_version: 1,
|
||||
@@ -265,45 +272,49 @@ fn echo_reply_requires_exact_negotiated_optional_fields() {
|
||||
|
||||
#[test]
|
||||
fn echo_payload_is_copied_zero_filled_and_bounded_in_both_directions() {
|
||||
let params = Params {
|
||||
length: 20,
|
||||
for stats in [ReceivedStats::None, ReceivedStats::Both] {
|
||||
for key in [None, Some(KEY)] {
|
||||
let mut params = Params {
|
||||
received_stats: stats,
|
||||
..Params::default()
|
||||
};
|
||||
|
||||
let packet = encode_request(echo_request(¶ms, &[1, 2]), None).unwrap();
|
||||
assert_eq!(&packet[16..], &[1, 2, 0, 0]);
|
||||
let offset = PacketLayout::echo(key.is_some(), ¶ms).header_len();
|
||||
params.length = (offset + 4) as i64;
|
||||
let packet = encode_request(echo_request(¶ms, &[1, 2]), key).unwrap();
|
||||
assert_eq!(&packet[offset..], &[1, 2, 0, 0]);
|
||||
assert_eq!(
|
||||
encode_request(echo_request(¶ms, &[0; 5]), None),
|
||||
encode_request(echo_request(¶ms, &[0; 5]), key),
|
||||
Err(ProtoError::PayloadTooLarge {
|
||||
available: 4,
|
||||
provided: 5,
|
||||
provided: 5
|
||||
})
|
||||
);
|
||||
|
||||
let reply = EchoReply {
|
||||
payload: vec![3, 4],
|
||||
..echo_reply(¶ms, FLAG_REPLY)
|
||||
};
|
||||
let packet = encode_echo_reply(&reply, ¶ms, None).unwrap();
|
||||
assert_eq!(&packet[16..], &[3, 4, 0, 0]);
|
||||
let decoded = decode_echo_reply(&packet, ¶ms, None).unwrap();
|
||||
let packet = encode_echo_reply(&reply, ¶ms, key).unwrap();
|
||||
assert_eq!(&packet[offset..], &[3, 4, 0, 0]);
|
||||
let decoded = decode_echo_reply(&packet, ¶ms, key).unwrap();
|
||||
assert_eq!(decoded.payload, vec![3, 4, 0, 0]);
|
||||
assert_eq!(encode_echo_reply(&decoded, ¶ms, None).unwrap(), packet);
|
||||
assert_eq!(encode_echo_reply(&decoded, ¶ms, key).unwrap(), packet);
|
||||
assert_eq!(
|
||||
encode_echo_reply(
|
||||
&EchoReply {
|
||||
payload: vec![0; 5],
|
||||
..echo_reply(¶ms, FLAG_REPLY)
|
||||
..reply
|
||||
},
|
||||
¶ms,
|
||||
None,
|
||||
key
|
||||
),
|
||||
Err(ProtoError::PayloadTooLarge {
|
||||
available: 4,
|
||||
provided: 5,
|
||||
provided: 5
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_negative_negotiated_length_still_encodes_in_both_directions() {
|
||||
@@ -321,6 +332,14 @@ fn a_negative_negotiated_length_still_encodes_in_both_directions() {
|
||||
let request = encode_request(echo_request(¶ms, &[]), None)
|
||||
.expect("a negative negotiated length must still encode a request");
|
||||
assert_eq!(request.len(), 16);
|
||||
assert_eq!(
|
||||
decode_request(&request).unwrap().kind,
|
||||
DecodedRequestKind::Echo {
|
||||
token: TOKEN,
|
||||
sequence: 17,
|
||||
tail: &[]
|
||||
}
|
||||
);
|
||||
|
||||
let reply = encode_echo_reply(&echo_reply(¶ms, FLAG_REPLY), ¶ms, None)
|
||||
.expect("a negative negotiated length must still encode a reply");
|
||||
@@ -505,13 +524,15 @@ fn midpoint_dual_field_reply_authenticates_over_full_packet() {
|
||||
let reply = decode_echo_reply(&packet, ¶ms, Some(KEY)).unwrap();
|
||||
assert_eq!(reply.timestamps.midpoint_wall, Some(555));
|
||||
|
||||
let mut corrupted = packet;
|
||||
corrupted[HMAC_OFFSET] ^= 0xFF;
|
||||
for offset in [HMAC_OFFSET, PacketLayout::echo(true, ¶ms).header_len()] {
|
||||
let mut corrupted = packet.clone();
|
||||
corrupted[offset] ^= 0xFF;
|
||||
assert_eq!(
|
||||
decode_echo_reply(&corrupted, ¶ms, Some(KEY)),
|
||||
Err(ProtoError::BadHmac)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_the_exact_compat_length_is_accepted_as_an_extension() {
|
||||
@@ -539,61 +560,92 @@ fn only_the_exact_compat_length_is_accepted_as_an_extension() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn midpoint_both_clock_reply_has_no_additional_extension() {
|
||||
let params = midpoint_params(Clock::Both, 0);
|
||||
let reply = echo_reply(¶ms, FLAG_REPLY);
|
||||
let packet = encode_echo_reply(&reply, ¶ms, None).unwrap();
|
||||
|
||||
let mut too_long = packet.clone();
|
||||
too_long.extend_from_slice(&[0; 8]);
|
||||
assert_eq!(
|
||||
decode_echo_reply(&too_long, ¶ms, None),
|
||||
Err(ProtoError::PacketLengthMismatch {
|
||||
expected: packet.len(),
|
||||
actual: too_long.len(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_midpoint_stamp_at_has_no_longer_form_exception() {
|
||||
fn other_layouts_have_no_longer_form_exception() {
|
||||
for (stamp_at, clock) in [
|
||||
(StampAt::Midpoint, Clock::Both),
|
||||
(StampAt::Receive, Clock::Wall),
|
||||
] {
|
||||
let params = Params {
|
||||
received_stats: ReceivedStats::None,
|
||||
stamp_at: StampAt::Receive,
|
||||
clock: Clock::Wall,
|
||||
stamp_at,
|
||||
clock,
|
||||
..Params::default()
|
||||
};
|
||||
let reply = echo_reply(¶ms, FLAG_REPLY);
|
||||
let packet = encode_echo_reply(&reply, ¶ms, None).unwrap();
|
||||
|
||||
let packet = encode_echo_reply(&echo_reply(¶ms, FLAG_REPLY), ¶ms, None).unwrap();
|
||||
let mut too_long = packet.clone();
|
||||
too_long.extend_from_slice(&[0; 8]);
|
||||
assert_eq!(
|
||||
decode_echo_reply(&too_long, ¶ms, None),
|
||||
Err(ProtoError::PacketLengthMismatch {
|
||||
expected: packet.len(),
|
||||
actual: too_long.len(),
|
||||
actual: too_long.len()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/// The midpoint compatibility length is a *reply* concept. An inbound ECHO
|
||||
/// request of that length is just a longer request, which a receiver accepts
|
||||
/// without applying any negotiated length rule.
|
||||
#[test]
|
||||
fn echo_request_decoding_has_no_compat_length_rule() {
|
||||
let params = midpoint_params(Clock::Wall, 0);
|
||||
let packet = encode_request(echo_request(¶ms, &[]), None).unwrap();
|
||||
|
||||
let mut too_long = packet.clone();
|
||||
too_long.extend_from_slice(&[0; 8]);
|
||||
let request = decode_request(&too_long).unwrap();
|
||||
assert_eq!(
|
||||
request.kind,
|
||||
DecodedRequestKind::Echo {
|
||||
token: TOKEN,
|
||||
sequence: 17,
|
||||
tail: &too_long[16..],
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reply_decoders_enforce_hmac_presence_and_authenticate_the_body() {
|
||||
let params = params();
|
||||
for key in [None, Some(KEY)] {
|
||||
let open = encode_open_reply(
|
||||
&OpenReply {
|
||||
flags: FLAG_OPEN | FLAG_REPLY,
|
||||
token: TOKEN,
|
||||
params: params.clone(),
|
||||
},
|
||||
key,
|
||||
)
|
||||
.unwrap();
|
||||
let echo = encode_echo_reply(&echo_reply(¶ms, FLAG_REPLY), ¶ms, key).unwrap();
|
||||
let mismatched_key = if key.is_some() { None } else { Some(KEY) };
|
||||
assert_eq!(
|
||||
decode_open_reply(&open, mismatched_key),
|
||||
Err(ProtoError::HmacPresenceMismatch)
|
||||
);
|
||||
assert_eq!(
|
||||
decode_echo_reply(&echo, ¶ms, mismatched_key),
|
||||
Err(ProtoError::HmacPresenceMismatch)
|
||||
);
|
||||
if let Some(key) = key {
|
||||
let mut bad_open = open;
|
||||
let mut bad_echo = echo;
|
||||
*bad_open.last_mut().unwrap() ^= 1;
|
||||
*bad_echo.last_mut().unwrap() ^= 1;
|
||||
assert_eq!(
|
||||
decode_open_reply(&bad_open, Some(key)),
|
||||
Err(ProtoError::BadHmac)
|
||||
);
|
||||
assert_eq!(
|
||||
decode_echo_reply(&bad_echo, ¶ms, Some(key)),
|
||||
Err(ProtoError::BadHmac)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn echo_reply_length_errors_precede_hmac_verification() {
|
||||
for key in [None, Some(KEY)] {
|
||||
let mut params = Params::default();
|
||||
let header = PacketLayout::echo(key.is_some(), ¶ms).header_len();
|
||||
params.length = (header + 4) as i64;
|
||||
let packet = encode_echo_reply(&echo_reply(¶ms, FLAG_REPLY), ¶ms, key).unwrap();
|
||||
for actual in [header - 1, header, packet.len() - 1, packet.len() + 1] {
|
||||
let mut malformed = packet.clone();
|
||||
malformed.resize(actual, 0);
|
||||
// Truncating or extending the signed packet also invalidates its MAC.
|
||||
let expected = if actual < header {
|
||||
ProtoError::PacketTooShort {
|
||||
needed: header,
|
||||
actual,
|
||||
}
|
||||
} else {
|
||||
ProtoError::PacketLengthMismatch {
|
||||
expected: packet.len(),
|
||||
actual,
|
||||
}
|
||||
};
|
||||
assert_eq!(decode_echo_reply(&malformed, ¶ms, key), Err(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,8 +47,6 @@ fn token_and_sequence() -> Vec<u8> {
|
||||
body
|
||||
}
|
||||
|
||||
// ---------- A. Classification ----------
|
||||
|
||||
fn classify(kind: &DecodedRequestKind<'_>) -> &'static str {
|
||||
match kind {
|
||||
DecodedRequestKind::Open { no_test: false, .. } => "open",
|
||||
@@ -71,6 +69,34 @@ fn flag_combinations_classify_requests_with_orthogonal_hmac_presence() {
|
||||
(FLAG_OPEN | FLAG_CLOSE | FLAG_HMAC, true, "no-test open"),
|
||||
];
|
||||
|
||||
for no_test in [false, true] {
|
||||
for key in [None, Some(KEY)] {
|
||||
let params = Params::default();
|
||||
let packet = encode_request(
|
||||
RequestToEncode::Open {
|
||||
params: ¶ms,
|
||||
no_test,
|
||||
},
|
||||
key,
|
||||
)
|
||||
.unwrap();
|
||||
let expected_flags = FLAG_OPEN
|
||||
| if no_test { FLAG_CLOSE } else { 0 }
|
||||
| if key.is_some() { FLAG_HMAC } else { 0 };
|
||||
assert_eq!(packet[3], expected_flags);
|
||||
assert_eq!(
|
||||
decode_request(&packet).unwrap().kind,
|
||||
DecodedRequestKind::Open {
|
||||
no_test,
|
||||
params: &[]
|
||||
}
|
||||
);
|
||||
if let Some(key) = key {
|
||||
verify_packet_hmac(key, &packet).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (flags, hmac_present, expected) in cases {
|
||||
// Long enough for any kind, so only the flags decide classification.
|
||||
let packet = raw(flags, &token_and_sequence(), 64);
|
||||
@@ -82,6 +108,10 @@ fn flag_combinations_classify_requests_with_orthogonal_hmac_presence() {
|
||||
request.hmac_present, hmac_present,
|
||||
"flags 0x{flags:02x} HMAC presence"
|
||||
);
|
||||
if let DecodedRequestKind::Open { params, .. } = request.kind {
|
||||
let offset = if hmac_present { 20 } else { 4 };
|
||||
assert_eq!(params, &packet[offset..]);
|
||||
}
|
||||
assert_eq!(
|
||||
classify(&request.kind),
|
||||
expected,
|
||||
@@ -90,8 +120,6 @@ fn flag_combinations_classify_requests_with_orthogonal_hmac_presence() {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- B. Invalid header / flags ----------
|
||||
|
||||
#[test]
|
||||
fn structurally_invalid_headers_are_rejected() {
|
||||
assert_eq!(
|
||||
@@ -135,8 +163,6 @@ fn any_reply_flagged_datagram_is_rejected_as_a_request() {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- C. Structural minimum lengths ----------
|
||||
|
||||
#[test]
|
||||
fn structural_minimum_lengths_are_enforced_per_kind() {
|
||||
// (flags, minimum accepted length)
|
||||
@@ -172,8 +198,6 @@ fn structural_minimum_lengths_are_enforced_per_kind() {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- D. Field offsets ----------
|
||||
|
||||
#[test]
|
||||
fn token_and_sequence_are_read_from_the_hmac_dependent_offsets() {
|
||||
for hmac in [false, true] {
|
||||
@@ -211,69 +235,25 @@ fn token_and_sequence_are_read_from_the_hmac_dependent_offsets() {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- E. Open parameter slice ----------
|
||||
|
||||
#[test]
|
||||
fn open_params_are_borrowed_after_the_header_and_decoded_later() {
|
||||
let encoded = params().encode();
|
||||
|
||||
// Structural parsing accepts even malformed Params; semantic errors belong
|
||||
// to Params::decode and are covered in params.rs.
|
||||
for body in [params().encode(), Vec::new(), vec![1, 0x80]] {
|
||||
for hmac in [false, true] {
|
||||
let flags = FLAG_OPEN | if hmac { FLAG_HMAC } else { 0 };
|
||||
let expected_offset = if hmac { 4 + HMAC_SIZE } else { 4 };
|
||||
let packet = raw(flags, &encoded, expected_offset + encoded.len());
|
||||
|
||||
let offset = if hmac { 20 } else { 4 };
|
||||
let packet = raw(flags, &body, offset + body.len());
|
||||
match decode_request(&packet).unwrap().kind {
|
||||
DecodedRequestKind::Open { params, .. } => {
|
||||
assert_eq!(params, &packet[expected_offset..]);
|
||||
assert_eq!(Params::decode(params).unwrap(), self::params());
|
||||
assert_eq!(params, &packet[offset..]);
|
||||
}
|
||||
other => panic!("expected an open request, got {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_and_malformed_open_params_decode_structurally() {
|
||||
let empty = header(FLAG_OPEN);
|
||||
match decode_request(&empty).unwrap().kind {
|
||||
DecodedRequestKind::Open { params, .. } => assert!(params.is_empty()),
|
||||
other => panic!("expected an open request, got {other:?}"),
|
||||
}
|
||||
|
||||
// A truncated varint: structurally fine, semantically not. A receiver must
|
||||
// be able to authenticate before spending effort on this.
|
||||
let malformed = raw(FLAG_OPEN, &[1, 0x80], 6);
|
||||
match decode_request(&malformed).unwrap().kind {
|
||||
DecodedRequestKind::Open { params, .. } => {
|
||||
assert_eq!(params, &[1, 0x80]);
|
||||
assert_eq!(Params::decode(params), Err(ProtoError::TruncatedVarint));
|
||||
}
|
||||
other => panic!("expected an open request, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- F. Open precedence ----------
|
||||
|
||||
#[test]
|
||||
fn open_precedence_beats_close_and_an_echo_shaped_body() {
|
||||
// An OPEN|CLOSE datagram whose body looks exactly like a token and
|
||||
// sequence number is still a no-test open, and those bytes are parameters.
|
||||
let packet = raw(FLAG_OPEN | FLAG_CLOSE, &token_and_sequence(), 16);
|
||||
|
||||
assert_eq!(
|
||||
decode_request(&packet).unwrap(),
|
||||
DecodedRequest {
|
||||
hmac_present: false,
|
||||
kind: DecodedRequestKind::Open {
|
||||
no_test: true,
|
||||
params: &packet[4..],
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- G. Trailing data ----------
|
||||
|
||||
#[test]
|
||||
fn trailing_bytes_are_tolerated_and_no_length_ceiling_applies() {
|
||||
let mut close = raw(FLAG_CLOSE, &TOKEN.to_le_bytes(), 12);
|
||||
@@ -303,8 +283,6 @@ fn trailing_bytes_are_tolerated_and_no_length_ceiling_applies() {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- H. Zero token ----------
|
||||
|
||||
#[test]
|
||||
fn a_zero_token_is_structurally_valid() {
|
||||
assert_eq!(
|
||||
@@ -323,8 +301,6 @@ fn a_zero_token_is_structurally_valid() {
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- HMAC presence is not authentication ----------
|
||||
|
||||
#[test]
|
||||
fn hmac_presence_is_reported_independently_of_mac_validity() {
|
||||
// A structurally complete authenticated echo whose MAC is garbage.
|
||||
@@ -378,106 +354,3 @@ fn packet_verification_requires_a_present_and_complete_hmac_field() {
|
||||
Err(ProtoError::BadMagic)
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- Encode -> decode cross-checks ----------
|
||||
|
||||
#[test]
|
||||
fn encoded_requests_decode_back_to_their_sender_side_identity() {
|
||||
for key in [None, Some(KEY)] {
|
||||
for no_test in [false, true] {
|
||||
let packet = encode_request(
|
||||
RequestToEncode::Open {
|
||||
params: ¶ms(),
|
||||
no_test,
|
||||
},
|
||||
key,
|
||||
)
|
||||
.unwrap();
|
||||
let request = decode_request(&packet).unwrap();
|
||||
assert_eq!(request.hmac_present, key.is_some());
|
||||
match request.kind {
|
||||
DecodedRequestKind::Open {
|
||||
no_test: decoded,
|
||||
params: encoded,
|
||||
} => {
|
||||
assert_eq!(decoded, no_test);
|
||||
assert_eq!(Params::decode(encoded).unwrap(), params());
|
||||
}
|
||||
other => panic!("expected an open request, got {other:?}"),
|
||||
}
|
||||
if let Some(key) = key {
|
||||
verify_packet_hmac(key, &packet).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
let packet = encode_request(RequestToEncode::Close { token: TOKEN }, key).unwrap();
|
||||
let request = decode_request(&packet).unwrap();
|
||||
assert_eq!(request.hmac_present, key.is_some());
|
||||
assert_eq!(request.kind, DecodedRequestKind::Close { token: TOKEN });
|
||||
|
||||
// The ECHO tail is deliberately not compared against the logical
|
||||
// payload: the sender placed it at a negotiated offset the receiver
|
||||
// cannot recover without `Params`.
|
||||
let echo_params = Params {
|
||||
length: 96,
|
||||
..params()
|
||||
};
|
||||
let packet = encode_request(
|
||||
RequestToEncode::Echo {
|
||||
token: TOKEN,
|
||||
sequence: SEQUENCE,
|
||||
params: &echo_params,
|
||||
payload: &[1, 2, 3, 4],
|
||||
},
|
||||
key,
|
||||
)
|
||||
.unwrap();
|
||||
let request = decode_request(&packet).unwrap();
|
||||
assert_eq!(request.hmac_present, key.is_some());
|
||||
match request.kind {
|
||||
DecodedRequestKind::Echo {
|
||||
token, sequence, ..
|
||||
} => {
|
||||
assert_eq!(token, TOKEN);
|
||||
assert_eq!(sequence, SEQUENCE);
|
||||
}
|
||||
other => panic!("expected an echo request, got {other:?}"),
|
||||
}
|
||||
if let Some(key) = key {
|
||||
verify_packet_hmac(key, &packet).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn echo_encoding_floors_a_negative_negotiated_length_at_the_field_block() {
|
||||
// A negative negotiated length is accepted during open and echoed back
|
||||
// unchanged, so a real session can carry one. It must therefore stay
|
||||
// encodable: it asks for no space beyond the mandatory fields.
|
||||
let params = Params {
|
||||
length: -1,
|
||||
..Params::default()
|
||||
};
|
||||
let packet = encode_request(
|
||||
RequestToEncode::Echo {
|
||||
token: TOKEN,
|
||||
sequence: SEQUENCE,
|
||||
params: ¶ms,
|
||||
payload: &[],
|
||||
},
|
||||
None,
|
||||
)
|
||||
.expect("a negative negotiated length must still encode");
|
||||
|
||||
// header (4) + token (8) + sequence (4).
|
||||
assert_eq!(packet.len(), 16);
|
||||
let request = decode_request(&packet).unwrap();
|
||||
assert_eq!(
|
||||
request.kind,
|
||||
DecodedRequestKind::Echo {
|
||||
token: TOKEN,
|
||||
sequence: SEQUENCE,
|
||||
tail: &[],
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
use irtt_proto::{varint::*, ProtoError};
|
||||
use proptest::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn boundary_values_round_trip() {
|
||||
for value in [
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
127,
|
||||
128,
|
||||
255,
|
||||
16_384,
|
||||
u32::MAX as u64,
|
||||
i64::MAX as u64,
|
||||
u64::MAX,
|
||||
] {
|
||||
let mut encoded = Vec::new();
|
||||
encode_uvarint(value, &mut encoded);
|
||||
assert_eq!(decode_uvarint(&encoded), Ok((value, encoded.len())));
|
||||
}
|
||||
for value in [
|
||||
i64::MIN,
|
||||
-9_223_372_036,
|
||||
-2,
|
||||
-1,
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
1_000_000_000,
|
||||
3_000_000_000,
|
||||
i64::MAX,
|
||||
] {
|
||||
let mut encoded = Vec::new();
|
||||
encode_varint(value, &mut encoded);
|
||||
assert_eq!(decode_varint(&encoded), Ok((value, encoded.len())));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verified_byte_examples() {
|
||||
let cases: &[(i64, &[u8])] = &[
|
||||
(1, &[0x02]),
|
||||
(3_000_000_000, &[0x80, 0xf8, 0x82, 0xad, 0x16]),
|
||||
(1_000_000_000, &[0x80, 0xa8, 0xd6, 0xb9, 0x07]),
|
||||
(1472, &[0x80, 0x17]),
|
||||
(3, &[0x06]),
|
||||
(184, &[0xf0, 0x02]),
|
||||
];
|
||||
for (value, expected) in cases {
|
||||
let mut encoded = Vec::new();
|
||||
encode_varint(*value, &mut encoded);
|
||||
assert_eq!(&encoded, expected);
|
||||
assert_eq!(decode_varint(expected), Ok((*value, expected.len())));
|
||||
}
|
||||
for tag in [9, 24] {
|
||||
let mut encoded = Vec::new();
|
||||
encode_uvarint(tag, &mut encoded);
|
||||
assert_eq!(encoded, [tag as u8]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncated_and_overflowing_varints_are_rejected() {
|
||||
// Incomplete input differs from a tenth byte whose bits cannot fit u64,
|
||||
// or an eleventh byte after ten continuation bytes.
|
||||
for (bytes, expected) in [
|
||||
(vec![], ProtoError::TruncatedVarint),
|
||||
(vec![0x80], ProtoError::TruncatedVarint),
|
||||
(vec![0x80; 10], ProtoError::TruncatedVarint),
|
||||
(
|
||||
[vec![0x80; 9], vec![2]].concat(),
|
||||
ProtoError::VarintOverflow,
|
||||
),
|
||||
(
|
||||
[vec![0x80; 10], vec![0]].concat(),
|
||||
ProtoError::VarintOverflow,
|
||||
),
|
||||
] {
|
||||
assert_eq!(decode_uvarint(&bytes), Err(expected), "{bytes:02x?}");
|
||||
assert_eq!(
|
||||
decode_varint(&bytes),
|
||||
decode_uvarint(&bytes).map(|(v, n)| (zigzag_decode(v), n))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn uvarint_round_trips(value: u64) {
|
||||
let mut encoded = Vec::new();
|
||||
encode_uvarint(value, &mut encoded);
|
||||
prop_assert!(!encoded.is_empty());
|
||||
prop_assert!(encoded.len() <= 10);
|
||||
prop_assert_eq!(decode_uvarint(&encoded).unwrap(), (value, encoded.len()));
|
||||
prop_assert_eq!(zigzag_encode(zigzag_decode(value)), value);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signed_varint_round_trips(value: i64) {
|
||||
let mut encoded = Vec::new();
|
||||
encode_varint(value, &mut encoded);
|
||||
prop_assert!(!encoded.is_empty());
|
||||
prop_assert!(encoded.len() <= 10);
|
||||
prop_assert_eq!(decode_varint(&encoded).unwrap(), (value, encoded.len()));
|
||||
prop_assert_eq!(zigzag_decode(zigzag_encode(value)), value);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user