From 5bc27ef8947400b97b67307565e72f21e36ae0a8 Mon Sep 17 00:00:00 2001 From: Nils Andreas Svee Date: Tue, 6 Oct 2026 17:02:54 +0700 Subject: [PATCH] refactor(proto): consolidate tests --- crates/irtt-proto/src/echo.rs | 169 ---- crates/irtt-proto/src/envelope.rs | 98 --- crates/irtt-proto/src/hmac.rs | 73 -- crates/irtt-proto/src/layout.rs | 191 ----- crates/irtt-proto/src/open.rs | 50 -- crates/irtt-proto/src/params.rs | 770 ------------------ crates/irtt-proto/src/varint.rs | 109 --- crates/irtt-proto/tests/hmac.rs | 61 ++ crates/irtt-proto/tests/layout.rs | 165 ++++ crates/irtt-proto/tests/params.rs | 399 +++++++++ .../{directional_codecs.rs => reply_codec.rs} | 394 +++++---- crates/irtt-proto/tests/request_codec.rs | 215 +---- crates/irtt-proto/tests/varint.rs | 108 +++ 13 files changed, 1000 insertions(+), 1802 deletions(-) create mode 100644 crates/irtt-proto/tests/hmac.rs create mode 100644 crates/irtt-proto/tests/layout.rs create mode 100644 crates/irtt-proto/tests/params.rs rename crates/irtt-proto/tests/{directional_codecs.rs => reply_codec.rs} (66%) create mode 100644 crates/irtt-proto/tests/varint.rs diff --git a/crates/irtt-proto/src/echo.rs b/crates/irtt-proto/src/echo.rs index f65ad3e..8e8482b 100644 --- a/crates/irtt-proto/src/echo.rs +++ b/crates/irtt-proto/src/echo.rs @@ -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) { - 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, - }) - ); - } -} diff --git a/crates/irtt-proto/src/envelope.rs b/crates/irtt-proto/src/envelope.rs index 1b8a1b3..76aaa72 100644 --- a/crates/irtt-proto/src/envelope.rs +++ b/crates/irtt-proto/src/envelope.rs @@ -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 { - 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); - } -} diff --git a/crates/irtt-proto/src/hmac.rs b/crates/irtt-proto/src/hmac.rs index 7baae5f..383a7a8 100644 --- a/crates/irtt-proto/src/hmac.rs +++ b/crates/irtt-proto/src/hmac.rs @@ -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 { - 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) - ); - } -} diff --git a/crates/irtt-proto/src/layout.rs b/crates/irtt-proto/src/layout.rs index bf1749f..f655180 100644 --- a/crates/irtt-proto/src/layout.rs +++ b/crates/irtt-proto/src/layout.rs @@ -162,194 +162,3 @@ pub fn echo_packet_len(hmac: bool, params: &Params) -> Result { }; 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 - }) - ); - } -} diff --git a/crates/irtt-proto/src/open.rs b/crates/irtt-proto/src/open.rs index 7ed768f..7b6c457 100644 --- a/crates/irtt-proto/src/open.rs +++ b/crates/irtt-proto/src/open.rs @@ -98,53 +98,3 @@ pub fn decode_open_reply(packet: &[u8], hmac_key: Option<&[u8]>) -> Result 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()); - } -} diff --git a/crates/irtt-proto/src/params.rs b/crates/irtt-proto/src/params.rs index f579d41..f928201 100644 --- a/crates/irtt-proto/src/params.rs +++ b/crates/irtt-proto/src/params.rs @@ -368,773 +368,3 @@ fn read_int(input: &[u8], pos: &mut usize) -> Result { *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 { - 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 { - 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 { - prop_oneof![ - Just(ReceivedStats::None), - Just(ReceivedStats::Count), - Just(ReceivedStats::Window), - Just(ReceivedStats::Both), - ] - } - - fn stamp_at_strategy() -> impl Strategy { - 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 { - 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 { - 1_i64..=3 - } - - fn server_fill_strategy() -> impl Strategy> { - 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 { - ( - any::(), - any::(), - any::(), - any::(), - any::(), - 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) { - varint::encode_uvarint(tag, out); - varint::encode_varint(value, out); - } - - fn encode_tagged_server_fill(value: &str, out: &mut Vec) { - 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::()), - second in proptest::collection::vec(proptest::char::any(), 0..=8) - .prop_map(|chars| chars.into_iter().collect::()), - ) { - 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, - }) - ); - } - } -} diff --git a/crates/irtt-proto/src/varint.rs b/crates/irtt-proto/src/varint.rs index 82bf6f2..4cc9b2d 100644 --- a/crates/irtt-proto/src/varint.rs +++ b/crates/irtt-proto/src/varint.rs @@ -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); - } - } - } -} diff --git a/crates/irtt-proto/tests/hmac.rs b/crates/irtt-proto/tests/hmac.rs new file mode 100644 index 0000000..843cfa5 --- /dev/null +++ b/crates/irtt-proto/tests/hmac.rs @@ -0,0 +1,61 @@ +use irtt_proto::{compute_hmac, compute_hmac_in_place, verify_hmac, ProtoError}; + +fn captured_open() -> Vec { + // 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]); + } +} diff --git a/crates/irtt-proto/tests/layout.rs b/crates/irtt-proto/tests/layout.rs new file mode 100644 index 0000000..18bd247 --- /dev/null +++ b/crates/irtt-proto/tests/layout.rs @@ -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 + }) + ); +} diff --git a/crates/irtt-proto/tests/params.rs b/crates/irtt-proto/tests/params.rs new file mode 100644 index 0000000..a693dae --- /dev/null +++ b/crates/irtt-proto/tests/params.rs @@ -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 { + let mut out = Vec::new(); + varint::encode_uvarint(tag, &mut out); + varint::encode_varint(value, &mut out); + out +} + +fn fill(value: &[u8]) -> Vec { + 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::::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 { + // Eight arbitrary Unicode scalars occupy at most the allowed 32 bytes. + proptest::collection::vec(any::(), 0..=8).prop_map(|chars| chars.into_iter().collect()) +} + +fn params_strategy() -> impl Strategy { + ( + 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 { + 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()); + } + } +} diff --git a/crates/irtt-proto/tests/directional_codecs.rs b/crates/irtt-proto/tests/reply_codec.rs similarity index 66% rename from crates/irtt-proto/tests/directional_codecs.rs rename to crates/irtt-proto/tests/reply_codec.rs index 25b9a13..9d9c465 100644 --- a/crates/irtt-proto/tests/directional_codecs.rs +++ b/crates/irtt-proto/tests/reply_codec.rs @@ -54,33 +54,41 @@ 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, - 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); - assert_eq!( - decode_open_reply(&packet, Some(KEY)), - Ok(OpenReply { - flags: FLAG_OPEN | FLAG_REPLY | FLAG_HMAC, - ..open_reply - }) - ); - - 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); - assert_eq!( - decode_echo_reply(&packet, ¶ms, Some(KEY)), - Ok(EchoReply { - flags: FLAG_REPLY | FLAG_HMAC, - ..echo_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, 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, key).unwrap(), + OpenReply { + flags: packet[3], + ..open + } + ); + 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, key).unwrap(), + EchoReply { + flags: packet[3], + ..echo + } + ); + } + } } #[test] @@ -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, - token: TOKEN, - params: params.clone(), - }, - None, + for (flags, expected) in [ + (FLAG_REPLY, ProtoError::MissingFlag(FLAG_OPEN)), + (FLAG_OPEN, ProtoError::MissingFlag(FLAG_REPLY)), + ( + FLAG_OPEN | FLAG_REPLY | 0x10, + ProtoError::ReservedFlags(0x10), ), - 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( + ] { + let open = OpenReply { + flags, + token: TOKEN, + params: params.clone(), + }; + 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,44 +272,48 @@ 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, - ..Params::default() - }; - - let packet = encode_request(echo_request(¶ms, &[1, 2]), None).unwrap(); - assert_eq!(&packet[16..], &[1, 2, 0, 0]); - assert_eq!( - encode_request(echo_request(¶ms, &[0; 5]), None), - Err(ProtoError::PayloadTooLarge { - available: 4, - 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(); - assert_eq!(decoded.payload, vec![3, 4, 0, 0]); - assert_eq!(encode_echo_reply(&decoded, ¶ms, None).unwrap(), packet); - assert_eq!( - encode_echo_reply( - &EchoReply { - payload: vec![0; 5], + for stats in [ReceivedStats::None, ReceivedStats::Both] { + for key in [None, Some(KEY)] { + let mut params = Params { + received_stats: stats, + ..Params::default() + }; + 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]), key), + Err(ProtoError::PayloadTooLarge { + available: 4, + provided: 5 + }) + ); + let reply = EchoReply { + payload: vec![3, 4], ..echo_reply(¶ms, FLAG_REPLY) - }, - ¶ms, - None, - ), - Err(ProtoError::PayloadTooLarge { - available: 4, - provided: 5, - }) - ); + }; + 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, key).unwrap(), packet); + assert_eq!( + encode_echo_reply( + &EchoReply { + payload: vec![0; 5], + ..reply + }, + ¶ms, + key + ), + Err(ProtoError::PayloadTooLarge { + available: 4, + provided: 5 + }) + ); + } + } } #[test] @@ -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,12 +524,14 @@ 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; - assert_eq!( - decode_echo_reply(&corrupted, ¶ms, Some(KEY)), - Err(ProtoError::BadHmac) - ); + 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] @@ -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(), - }) - ); +fn other_layouts_have_no_longer_form_exception() { + for (stamp_at, clock) in [ + (StampAt::Midpoint, Clock::Both), + (StampAt::Receive, Clock::Wall), + ] { + let params = Params { + stamp_at, + clock, + ..Params::default() + }; + 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() + }) + ); + } } #[test] -fn non_midpoint_stamp_at_has_no_longer_form_exception() { - let params = Params { - received_stats: ReceivedStats::None, - stamp_at: StampAt::Receive, - clock: Clock::Wall, - ..Params::default() - }; - 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(), - }) - ); -} - -/// 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..], +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)); + } + } } diff --git a/crates/irtt-proto/tests/request_codec.rs b/crates/irtt-proto/tests/request_codec.rs index 2652732..0929be2 100644 --- a/crates/irtt-proto/tests/request_codec.rs +++ b/crates/irtt-proto/tests/request_codec.rs @@ -47,8 +47,6 @@ fn token_and_sequence() -> Vec { 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(); - - 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()); - - match decode_request(&packet).unwrap().kind { - DecodedRequestKind::Open { params, .. } => { - assert_eq!(params, &packet[expected_offset..]); - assert_eq!(Params::decode(params).unwrap(), self::params()); + // 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 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[offset..]); + } + other => panic!("expected an open request, got {other:?}"), } - 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: &[], - } - ); -} diff --git a/crates/irtt-proto/tests/varint.rs b/crates/irtt-proto/tests/varint.rs new file mode 100644 index 0000000..ee5e088 --- /dev/null +++ b/crates/irtt-proto/tests/varint.rs @@ -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); + } +}