Skip to main content

irtt_proto/
params.rs

1use crate::{varint, ProtoError, Result, PROTOCOL_VERSION};
2
3/// Protocol compatibility bound for an encoded `server_fill` parameter.
4pub const MAX_SERVER_FILL_BYTES: usize = 32;
5
6/// Low-level wire representation of IRTT open parameters.
7///
8/// `Params` mirrors the protocol fields closely and can be constructed
9/// directly by callers. Direct construction can therefore produce values that
10/// should not be sent on the wire, such as an oversized `server_fill` value.
11/// Higher-level callers should validate user or configuration input before
12/// encoding. The normal `irtt-client` configuration path enforces
13/// [`MAX_SERVER_FILL_BYTES`] for `server_fill`.
14///
15/// [`Params::default`] is the **wire** default: every integer field is zero and
16/// [`clock`](Params::clock) is [`Clock::Unspecified`], which is what an open
17/// request with an empty parameter payload means. It is not a set of sensible
18/// client settings; a client builds its request from its own configuration.
19///
20/// # Example
21///
22/// Construct a `Params`, encode it, and decode it back:
23///
24/// ```
25/// use irtt_proto::{Clock, Params, ReceivedStats, ServerFill, StampAt};
26///
27/// let params = Params {
28///     protocol_version: 1,
29///     duration_ns: 3_000_000_000,
30///     interval_ns: 1_000_000_000,
31///     length: 1472,
32///     received_stats: ReceivedStats::Both,
33///     stamp_at: StampAt::Both,
34///     clock: Clock::Both,
35///     server_fill: Some(ServerFill { value: "rand".to_owned() }),
36///     ..Params::default()
37/// };
38///
39/// let encoded = params.encode();
40/// assert_eq!(Params::decode(&encoded).unwrap(), params);
41/// ```
42#[derive(Debug, Clone, PartialEq, Eq, Default)]
43pub struct Params {
44    pub protocol_version: i64,
45    pub duration_ns: i64,
46    pub interval_ns: i64,
47    pub length: i64,
48    pub received_stats: ReceivedStats,
49    pub stamp_at: StampAt,
50    pub clock: Clock,
51    /// Raw IP TOS / Traffic Class byte (`0..=255`), not a six-bit DSCP
52    /// codepoint. A codepoint occupies the upper six bits of this byte;
53    /// callers that accept a codepoint from users or configuration must shift
54    /// it left by two before assigning it here. `encode`/`decode` carry this
55    /// value as-is and never apply that shift themselves.
56    pub dscp: i64,
57    pub server_fill: Option<ServerFill>,
58}
59
60impl Params {
61    pub fn with_protocol_defaults() -> Self {
62        Self {
63            protocol_version: PROTOCOL_VERSION,
64            ..Self::default()
65        }
66    }
67
68    /// Encodes these parameters without performing additional validation.
69    ///
70    /// Callers that construct `Params` directly are responsible for validating
71    /// user or configuration input before encoding.
72    pub fn encode(&self) -> Vec<u8> {
73        let mut out = Vec::new();
74        push_int(1, self.protocol_version, &mut out);
75        push_int(2, self.duration_ns, &mut out);
76        push_int(3, self.interval_ns, &mut out);
77        push_int(4, self.length, &mut out);
78        push_int(5, self.received_stats as i64, &mut out);
79        push_int(6, self.stamp_at as i64, &mut out);
80        push_int(7, self.clock as i64, &mut out);
81        push_int(8, self.dscp, &mut out);
82        if let Some(fill) = &self.server_fill {
83            varint::encode_uvarint(9, &mut out);
84            varint::encode_uvarint(fill.value.len() as u64, &mut out);
85            out.extend_from_slice(fill.value.as_bytes());
86        }
87        out
88    }
89
90    /// Decodes parameters and rejects malformed or incompatible incoming values.
91    ///
92    /// This includes invalid enum values, malformed UTF-8, and `server_fill`
93    /// values longer than [`MAX_SERVER_FILL_BYTES`].
94    ///
95    /// Absent parameters take their wire default, so a caller cannot tell an
96    /// omitted tag from one explicitly encoded as zero. Use
97    /// [`decode_with_presence`](Params::decode_with_presence) when that
98    /// distinction matters; both share one parser.
99    pub fn decode(input: &[u8]) -> Result<Self> {
100        Self::decode_with_presence(input).map(|decoded| decoded.params)
101    }
102
103    /// Decodes parameters and additionally reports which known tags appeared.
104    ///
105    /// This is the same parser and the same validation as [`decode`], with the
106    /// presence of each known tag retained. A receiver needs it because the
107    /// protocol gives an omitted tag and an explicit zero the same value but
108    /// not the same meaning: an absent Duration or Interval is accepted as the
109    /// wire default zero, while one explicitly encoded as zero is invalid.
110    ///
111    /// Presence means the tag appeared at least once. Repeated known tags keep
112    /// last-value-wins, and unknown tags remain ignored and untracked.
113    ///
114    /// # Example
115    ///
116    /// ```
117    /// use irtt_proto::Params;
118    ///
119    /// // Wire bytes: tag 2 (duration) carrying an explicit zero. Interval
120    /// // (tag 3) is absent, so both fields decode to zero but only one is
121    /// // reported present.
122    /// let decoded = Params::decode_with_presence(&[0x02, 0x00]).unwrap();
123    /// assert_eq!(decoded.params.duration_ns, 0);
124    /// assert!(decoded.presence.duration_ns);
125    /// assert!(!decoded.presence.interval_ns);
126    /// ```
127    ///
128    /// [`decode`]: Params::decode
129    pub fn decode_with_presence(input: &[u8]) -> Result<DecodedParams> {
130        let mut params = Self::default();
131        let mut presence = ParamPresence::default();
132        let mut pos = 0;
133        while pos < input.len() {
134            let (tag, used) = varint::decode_uvarint(&input[pos..])?;
135            pos += used;
136            match tag {
137                1 => {
138                    params.protocol_version = read_int(input, &mut pos)?;
139                    presence.protocol_version = true;
140                }
141                2 => {
142                    params.duration_ns = read_int(input, &mut pos)?;
143                    presence.duration_ns = true;
144                }
145                3 => {
146                    params.interval_ns = read_int(input, &mut pos)?;
147                    presence.interval_ns = true;
148                }
149                4 => {
150                    params.length = read_int(input, &mut pos)?;
151                    presence.length = true;
152                }
153                5 => {
154                    params.received_stats = ReceivedStats::try_from(read_int(input, &mut pos)?)?;
155                    presence.received_stats = true;
156                }
157                6 => {
158                    params.stamp_at = StampAt::try_from(read_int(input, &mut pos)?)?;
159                    presence.stamp_at = true;
160                }
161                7 => {
162                    params.clock = Clock::try_from(read_int(input, &mut pos)?)?;
163                    presence.clock = true;
164                }
165                8 => {
166                    params.dscp = read_int(input, &mut pos)?;
167                    presence.dscp = true;
168                }
169                9 => {
170                    let (len, used) = varint::decode_uvarint(&input[pos..])?;
171                    pos += used;
172                    let len = usize::try_from(len)
173                        .map_err(|_| ProtoError::ParameterLengthTooLarge { tag, length: len })?;
174                    if len > MAX_SERVER_FILL_BYTES {
175                        return Err(ProtoError::ParameterLengthTooLarge {
176                            tag,
177                            length: len as u64,
178                        });
179                    }
180                    if input.len().saturating_sub(pos) < len {
181                        return Err(ProtoError::MalformedParams);
182                    }
183                    let value = std::str::from_utf8(&input[pos..pos + len])
184                        .map_err(|_| ProtoError::InvalidUtf8)?
185                        .to_owned();
186                    pos += len;
187                    params.server_fill = Some(ServerFill { value });
188                    presence.server_fill = true;
189                }
190                _ => {
191                    let (_, used) = varint::decode_uvarint(&input[pos..])?;
192                    pos += used;
193                }
194            }
195        }
196        Ok(DecodedParams { params, presence })
197    }
198}
199
200/// Decoded parameters together with which known tags the payload carried.
201///
202/// Produced by [`Params::decode_with_presence`].
203#[derive(Debug, Clone, PartialEq, Eq, Default)]
204pub struct DecodedParams {
205    /// The decoded values. Absent parameters hold their wire default.
206    pub params: Params,
207    /// Which known tags appeared in the payload.
208    pub presence: ParamPresence,
209}
210
211/// Which known parameter tags a decoded payload carried.
212///
213/// A field is `true` when its tag appeared at least once, whatever value it
214/// carried. Unknown tags are ignored and are not represented here.
215#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
216pub struct ParamPresence {
217    pub protocol_version: bool,
218    pub duration_ns: bool,
219    pub interval_ns: bool,
220    pub length: bool,
221    pub received_stats: bool,
222    pub stamp_at: bool,
223    pub clock: bool,
224    pub dscp: bool,
225    pub server_fill: bool,
226}
227
228#[derive(Debug, Clone, PartialEq, Eq)]
229pub struct ServerFill {
230    pub value: String,
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
234#[repr(i64)]
235pub enum ReceivedStats {
236    #[default]
237    None = 0,
238    Count = 1,
239    Window = 2,
240    Both = 3,
241}
242
243impl ReceivedStats {
244    pub fn has_count(self) -> bool {
245        matches!(self, Self::Count | Self::Both)
246    }
247
248    pub fn has_window(self) -> bool {
249        matches!(self, Self::Window | Self::Both)
250    }
251}
252
253impl TryFrom<i64> for ReceivedStats {
254    type Error = ProtoError;
255
256    fn try_from(value: i64) -> Result<Self> {
257        match value {
258            0 => Ok(Self::None),
259            1 => Ok(Self::Count),
260            2 => Ok(Self::Window),
261            3 => Ok(Self::Both),
262            _ => Err(ProtoError::InvalidEnum {
263                name: "ReceivedStats",
264                value,
265            }),
266        }
267    }
268}
269
270#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
271#[repr(i64)]
272pub enum StampAt {
273    #[default]
274    None = 0,
275    Send = 1,
276    Receive = 2,
277    Both = 3,
278    Midpoint = 4,
279}
280
281impl TryFrom<i64> for StampAt {
282    type Error = ProtoError;
283
284    fn try_from(value: i64) -> Result<Self> {
285        match value {
286            0 => Ok(Self::None),
287            1 => Ok(Self::Send),
288            2 => Ok(Self::Receive),
289            3 => Ok(Self::Both),
290            4 => Ok(Self::Midpoint),
291            _ => Err(ProtoError::InvalidEnum {
292                name: "StampAt",
293                value,
294            }),
295        }
296    }
297}
298
299/// Which server clock sources supply timestamp fields.
300///
301/// # The zero value
302///
303/// [`Clock::Unspecified`] is the **wire default**, meaning the Clock tag was
304/// absent from an open parameter payload — which is valid, and is what an empty
305/// payload decodes to. It does not mean a peer may send an explicit Clock tag
306/// encoding zero: [`TryFrom<i64>`](Clock::try_from) still rejects an explicit
307/// zero as an invalid enum value, so this state is only ever reached by
308/// omission. [`encode`](Params::encode) omits the tag for `Unspecified` rather
309/// than emitting an explicit zero, which round-trips absence faithfully.
310///
311/// `Unspecified` selects no clock, so [`has_wall`](Clock::has_wall) and
312/// [`has_mono`](Clock::has_mono) are both false for it and no timestamp field
313/// is laid out. A client that wants timestamps must request a real clock; the
314/// `irtt-client` configuration path rejects `Unspecified`.
315#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
316#[repr(i64)]
317pub enum Clock {
318    /// Wire default for an absent Clock tag. Never produced from an explicit
319    /// encoded value.
320    #[default]
321    Unspecified = 0,
322    Wall = 1,
323    Monotonic = 2,
324    Both = 3,
325}
326
327impl Clock {
328    pub fn has_wall(self) -> bool {
329        matches!(self, Self::Wall | Self::Both)
330    }
331
332    pub fn has_mono(self) -> bool {
333        matches!(self, Self::Monotonic | Self::Both)
334    }
335}
336
337impl TryFrom<i64> for Clock {
338    type Error = ProtoError;
339
340    /// Converts an **explicitly encoded** Clock value.
341    ///
342    /// Zero is rejected: it is only valid as an absent tag, never as an encoded
343    /// one. [`Clock::Unspecified`] is therefore unreachable through this
344    /// conversion by design.
345    fn try_from(value: i64) -> Result<Self> {
346        match value {
347            1 => Ok(Self::Wall),
348            2 => Ok(Self::Monotonic),
349            3 => Ok(Self::Both),
350            _ => Err(ProtoError::InvalidEnum {
351                name: "Clock",
352                value,
353            }),
354        }
355    }
356}
357
358fn push_int(tag: u64, value: i64, out: &mut Vec<u8>) {
359    if value == 0 {
360        return;
361    }
362    varint::encode_uvarint(tag, out);
363    varint::encode_varint(value, out);
364}
365
366fn read_int(input: &[u8], pos: &mut usize) -> Result<i64> {
367    let (value, used) = varint::decode_varint(&input[*pos..])?;
368    *pos += used;
369    Ok(value)
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375
376    fn assert_round_trip(params: Params) {
377        assert_eq!(Params::decode(&params.encode()), Ok(params));
378    }
379
380    fn encode_server_fill_value(value: &[u8]) -> Vec<u8> {
381        let mut encoded = Vec::new();
382        varint::encode_uvarint(9, &mut encoded);
383        varint::encode_uvarint(value.len() as u64, &mut encoded);
384        encoded.extend_from_slice(value);
385        encoded
386    }
387
388    fn encode_int(tag: u64, value: i64) -> Vec<u8> {
389        let mut encoded = Vec::new();
390        varint::encode_uvarint(tag, &mut encoded);
391        varint::encode_varint(value, &mut encoded);
392        encoded
393    }
394
395    #[test]
396    fn params_round_trip() {
397        let params = Params {
398            protocol_version: 1,
399            duration_ns: 3_000_000_000,
400            interval_ns: 1_000_000_000,
401            length: 1472,
402            received_stats: ReceivedStats::Both,
403            stamp_at: StampAt::Both,
404            clock: Clock::Both,
405            dscp: 184,
406            server_fill: Some(ServerFill {
407                value: "pattern:abc".to_owned(),
408            }),
409        };
410        assert_round_trip(params);
411    }
412
413    #[test]
414    fn params_round_trip_negotiated_option_modes() {
415        for received_stats in [
416            ReceivedStats::None,
417            ReceivedStats::Count,
418            ReceivedStats::Window,
419            ReceivedStats::Both,
420        ] {
421            assert_round_trip(Params {
422                protocol_version: 1,
423                received_stats,
424                ..Params::default()
425            });
426        }
427
428        for stamp_at in [
429            StampAt::None,
430            StampAt::Send,
431            StampAt::Receive,
432            StampAt::Both,
433            StampAt::Midpoint,
434        ] {
435            assert_round_trip(Params {
436                protocol_version: 1,
437                stamp_at,
438                clock: Clock::Both,
439                ..Params::default()
440            });
441        }
442
443        for clock in [Clock::Wall, Clock::Monotonic, Clock::Both] {
444            assert_round_trip(Params {
445                protocol_version: 1,
446                stamp_at: StampAt::Both,
447                clock,
448                ..Params::default()
449            });
450        }
451    }
452
453    #[test]
454    fn params_round_trip_dscp_values_are_encoded_without_shifting() {
455        // `Params::dscp` is a raw wire byte; encode/decode carry whatever
456        // value is set without interpreting or shifting it, regardless of
457        // whether it happens to look like a codepoint (46) or a raw TOS byte
458        // (184).
459        for dscp in [0, 46, 63, 64, 184, -1] {
460            let params = Params {
461                protocol_version: 1,
462                dscp,
463                ..Params::default()
464            };
465            assert_round_trip(params);
466        }
467
468        let params = Params {
469            protocol_version: 1,
470            dscp: 46,
471            ..Params::default()
472        };
473        let encoded = params.encode();
474        assert!(
475            encoded.windows(2).any(|bytes| bytes == [8, 92]),
476            "dscp value 46 must be encoded as param value 46"
477        );
478        assert!(
479            !encoded.windows(3).any(|bytes| bytes == [8, 0xf0, 0x02]),
480            "Params::encode must not shift dscp 46 to 184; any codepoint shift is a caller concern"
481        );
482        assert_eq!(Params::decode(&encoded).unwrap().dscp, 46);
483    }
484
485    #[test]
486    fn server_fill_absent_short_and_max_length_round_trip() {
487        assert_round_trip(Params {
488            protocol_version: 1,
489            server_fill: None,
490            ..Params::default()
491        });
492
493        assert_round_trip(Params {
494            protocol_version: 1,
495            server_fill: Some(ServerFill {
496                value: "rand".to_owned(),
497            }),
498            ..Params::default()
499        });
500
501        assert_round_trip(Params {
502            protocol_version: 1,
503            server_fill: Some(ServerFill {
504                value: "0123456789abcdef0123456789abcdef".to_owned(),
505            }),
506            ..Params::default()
507        });
508    }
509
510    #[test]
511    fn server_fill_decode_accepts_max_length() {
512        let value = b"0123456789abcdef0123456789abcdef";
513        let params = Params::decode(&encode_server_fill_value(value)).unwrap();
514
515        assert_eq!(
516            params.server_fill,
517            Some(ServerFill {
518                value: "0123456789abcdef0123456789abcdef".to_owned(),
519            })
520        );
521    }
522
523    #[test]
524    fn server_fill_decode_rejects_oversized_length() {
525        let value = b"0123456789abcdef0123456789abcdefx";
526
527        assert_eq!(
528            Params::decode(&encode_server_fill_value(value)),
529            Err(ProtoError::ParameterLengthTooLarge { tag: 9, length: 33 })
530        );
531    }
532
533    #[test]
534    fn server_fill_tag_and_length_are_encoded_before_utf8_bytes() {
535        let params = Params {
536            protocol_version: 1,
537            server_fill: Some(ServerFill {
538                value: "rand".to_owned(),
539            }),
540            ..Params::default()
541        };
542        let encoded = params.encode();
543        assert!(encoded
544            .windows(6)
545            .any(|bytes| bytes == [9, 4, b'r', b'a', b'n', b'd']));
546    }
547
548    #[test]
549    fn unknown_tags_are_ignored() {
550        let mut encoded = Vec::new();
551        varint::encode_uvarint(99, &mut encoded);
552        varint::encode_varint(123, &mut encoded);
553        varint::encode_uvarint(1, &mut encoded);
554        varint::encode_varint(1, &mut encoded);
555
556        let params = Params::decode(&encoded).unwrap();
557        assert_eq!(params.protocol_version, 1);
558    }
559
560    #[test]
561    fn invalid_received_stats_value_is_rejected() {
562        let mut encoded = Vec::new();
563        varint::encode_uvarint(5, &mut encoded);
564        varint::encode_varint(4, &mut encoded);
565
566        assert_eq!(
567            Params::decode(&encoded),
568            Err(ProtoError::InvalidEnum {
569                name: "ReceivedStats",
570                value: 4,
571            })
572        );
573    }
574
575    #[test]
576    fn invalid_timestamp_value_is_rejected() {
577        let mut encoded = Vec::new();
578        varint::encode_uvarint(6, &mut encoded);
579        varint::encode_varint(5, &mut encoded);
580
581        assert_eq!(
582            Params::decode(&encoded),
583            Err(ProtoError::InvalidEnum {
584                name: "StampAt",
585                value: 5,
586            })
587        );
588    }
589
590    #[test]
591    fn explicit_clock_zero_is_rejected() {
592        let mut encoded = Vec::new();
593        varint::encode_uvarint(7, &mut encoded);
594        varint::encode_varint(0, &mut encoded);
595
596        assert_eq!(
597            Params::decode(&encoded),
598            Err(ProtoError::InvalidEnum {
599                name: "Clock",
600                value: 0,
601            })
602        );
603        assert_eq!(
604            Params::decode_with_presence(&encoded),
605            Err(ProtoError::InvalidEnum {
606                name: "Clock",
607                value: 0,
608            })
609        );
610    }
611
612    #[test]
613    fn explicit_clock_values_one_to_three_are_accepted() {
614        for (value, expected) in [(1, Clock::Wall), (2, Clock::Monotonic), (3, Clock::Both)] {
615            let decoded = Params::decode_with_presence(&encode_int(7, value)).unwrap();
616            assert_eq!(decoded.params.clock, expected);
617            assert!(decoded.presence.clock);
618        }
619    }
620
621    #[test]
622    fn empty_payload_decodes_to_wire_defaults_with_nothing_present() {
623        let decoded = Params::decode_with_presence(&[]).unwrap();
624
625        assert_eq!(decoded.params, Params::default());
626        assert_eq!(decoded.params.protocol_version, 0);
627        assert_eq!(decoded.params.duration_ns, 0);
628        assert_eq!(decoded.params.interval_ns, 0);
629        assert_eq!(decoded.params.length, 0);
630        assert_eq!(decoded.params.dscp, 0);
631        assert_eq!(decoded.params.received_stats, ReceivedStats::None);
632        assert_eq!(decoded.params.stamp_at, StampAt::None);
633        // The wire default clock is zero, not `Both`.
634        assert_eq!(decoded.params.clock, Clock::Unspecified);
635        assert!(!decoded.params.clock.has_wall());
636        assert!(!decoded.params.clock.has_mono());
637        assert_eq!(decoded.params.server_fill, None);
638
639        assert_eq!(decoded.presence, ParamPresence::default());
640        assert_eq!(
641            decoded.presence,
642            ParamPresence {
643                protocol_version: false,
644                duration_ns: false,
645                interval_ns: false,
646                length: false,
647                received_stats: false,
648                stamp_at: false,
649                clock: false,
650                dscp: false,
651                server_fill: false,
652            }
653        );
654    }
655
656    #[test]
657    fn absent_and_explicitly_zero_parameters_share_a_value_but_not_a_presence() {
658        // Duration and Interval are the pair a server must tell apart: absent is
659        // accepted, explicitly zero is not.
660        let absent = Params::decode_with_presence(&encode_int(1, 1)).unwrap();
661        assert_eq!(absent.params.duration_ns, 0);
662        assert!(!absent.presence.duration_ns);
663        assert_eq!(absent.params.interval_ns, 0);
664        assert!(!absent.presence.interval_ns);
665        assert_eq!(absent.params.clock, Clock::Unspecified);
666        assert!(!absent.presence.clock);
667
668        let explicit_duration = Params::decode_with_presence(&encode_int(2, 0)).unwrap();
669        assert_eq!(explicit_duration.params.duration_ns, 0);
670        assert!(explicit_duration.presence.duration_ns);
671
672        let explicit_interval = Params::decode_with_presence(&encode_int(3, 0)).unwrap();
673        assert_eq!(explicit_interval.params.interval_ns, 0);
674        assert!(explicit_interval.presence.interval_ns);
675    }
676
677    #[test]
678    fn every_known_tag_reports_its_own_presence() {
679        let mut encoded = Vec::new();
680        for (tag, value) in [
681            (1, 1),
682            (2, 5),
683            (3, 6),
684            (4, 7),
685            (5, 3),
686            (6, 4),
687            (7, 3),
688            (8, 184),
689        ] {
690            encoded.extend_from_slice(&encode_int(tag, value));
691        }
692        encoded.extend_from_slice(&encode_server_fill_value(b"rand"));
693
694        let decoded = Params::decode_with_presence(&encoded).unwrap();
695        assert_eq!(
696            decoded.presence,
697            ParamPresence {
698                protocol_version: true,
699                duration_ns: true,
700                interval_ns: true,
701                length: true,
702                received_stats: true,
703                stamp_at: true,
704                clock: true,
705                dscp: true,
706                server_fill: true,
707            }
708        );
709    }
710
711    #[test]
712    fn unspecified_clock_is_encoded_by_omission() {
713        let params = Params {
714            protocol_version: 1,
715            clock: Clock::Unspecified,
716            ..Params::default()
717        };
718        let encoded = params.encode();
719
720        assert_eq!(encoded, encode_int(1, 1));
721        assert!(
722            !encoded.windows(2).any(|bytes| bytes[0] == 7),
723            "an unspecified clock must omit the tag rather than encode an explicit zero"
724        );
725        assert_round_trip(params);
726    }
727
728    #[test]
729    fn decode_and_decode_with_presence_agree() {
730        let mut payloads = vec![
731            Vec::new(),
732            encode_int(1, 1),
733            encode_int(2, 0),
734            encode_server_fill_value(b"rand"),
735        ];
736        payloads.push(
737            Params {
738                protocol_version: 1,
739                duration_ns: 3_000_000_000,
740                interval_ns: 1_000_000_000,
741                length: 1472,
742                received_stats: ReceivedStats::Both,
743                stamp_at: StampAt::Both,
744                clock: Clock::Both,
745                dscp: 184,
746                server_fill: Some(ServerFill {
747                    value: "rand".to_owned(),
748                }),
749            }
750            .encode(),
751        );
752        // Malformed payloads must fail identically through both entry points.
753        payloads.push(vec![1, 0x80]);
754        payloads.push(encode_int(7, 0));
755
756        for payload in payloads {
757            assert_eq!(
758                Params::decode(&payload),
759                Params::decode_with_presence(&payload).map(|decoded| decoded.params),
760                "decode and decode_with_presence disagreed on {payload:02x?}"
761            );
762        }
763    }
764
765    #[test]
766    fn malformed_server_fill_is_rejected() {
767        let encoded = [9, 4, b'a', b'b'];
768        assert_eq!(Params::decode(&encoded), Err(ProtoError::MalformedParams));
769    }
770
771    #[test]
772    fn non_utf8_server_fill_is_rejected() {
773        let encoded = [9, 1, 0xff];
774        assert_eq!(Params::decode(&encoded), Err(ProtoError::InvalidUtf8));
775    }
776
777    #[cfg(target_pointer_width = "32")]
778    #[test]
779    fn server_fill_length_too_large_for_usize_is_rejected() {
780        let mut encoded = Vec::new();
781        varint::encode_uvarint(9, &mut encoded);
782        varint::encode_uvarint(u64::from(u32::MAX) + 1, &mut encoded);
783
784        assert_eq!(
785            Params::decode(&encoded),
786            Err(ProtoError::ParameterLengthTooLarge {
787                tag: 9,
788                length: u64::from(u32::MAX) + 1,
789            })
790        );
791    }
792
793    #[test]
794    fn truncated_varint_parameter_is_rejected() {
795        let encoded = [1, 0x80];
796        assert_eq!(Params::decode(&encoded), Err(ProtoError::TruncatedVarint));
797    }
798
799    #[test]
800    fn truncated_unknown_parameter_value_is_rejected() {
801        let encoded = [99];
802        assert_eq!(Params::decode(&encoded), Err(ProtoError::TruncatedVarint));
803    }
804
805    mod properties {
806        use super::*;
807        use proptest::prelude::*;
808
809        fn received_stats_strategy() -> impl Strategy<Value = ReceivedStats> {
810            prop_oneof![
811                Just(ReceivedStats::None),
812                Just(ReceivedStats::Count),
813                Just(ReceivedStats::Window),
814                Just(ReceivedStats::Both),
815            ]
816        }
817
818        fn stamp_at_strategy() -> impl Strategy<Value = StampAt> {
819            prop_oneof![
820                Just(StampAt::None),
821                Just(StampAt::Send),
822                Just(StampAt::Receive),
823                Just(StampAt::Both),
824                Just(StampAt::Midpoint),
825            ]
826        }
827
828        /// A `Clock` valid for direct construction, including `Unspecified`
829        /// (wire omission). Never produces an explicit zero on the wire; see
830        /// `explicit_clock_strategy` for that.
831        fn clock_strategy() -> impl Strategy<Value = Clock> {
832            prop_oneof![
833                Just(Clock::Unspecified),
834                Just(Clock::Wall),
835                Just(Clock::Monotonic),
836                Just(Clock::Both),
837            ]
838        }
839
840        /// A `Clock` value valid to encode *explicitly* on the wire (tag 7).
841        /// Excludes `Unspecified`/zero, which is only ever reached by omission.
842        fn explicit_clock_value_strategy() -> impl Strategy<Value = i64> {
843            1_i64..=3
844        }
845
846        fn server_fill_strategy() -> impl Strategy<Value = Option<ServerFill>> {
847            prop_oneof![
848                Just(None),
849                proptest::collection::vec(proptest::char::any(), 0..=8).prop_map(|chars| Some(
850                    ServerFill {
851                        value: chars.into_iter().collect(),
852                    }
853                )),
854            ]
855        }
856
857        fn params_strategy() -> impl Strategy<Value = Params> {
858            (
859                any::<i64>(),
860                any::<i64>(),
861                any::<i64>(),
862                any::<i64>(),
863                any::<i64>(),
864                received_stats_strategy(),
865                stamp_at_strategy(),
866                clock_strategy(),
867                server_fill_strategy(),
868            )
869                .prop_map(
870                    |(
871                        protocol_version,
872                        duration_ns,
873                        interval_ns,
874                        length,
875                        dscp,
876                        received_stats,
877                        stamp_at,
878                        clock,
879                        server_fill,
880                    )| Params {
881                        protocol_version,
882                        duration_ns,
883                        interval_ns,
884                        length,
885                        received_stats,
886                        stamp_at,
887                        clock,
888                        dscp,
889                        server_fill,
890                    },
891                )
892        }
893
894        fn encode_tagged_int(tag: u64, value: i64, out: &mut Vec<u8>) {
895            varint::encode_uvarint(tag, out);
896            varint::encode_varint(value, out);
897        }
898
899        fn encode_tagged_server_fill(value: &str, out: &mut Vec<u8>) {
900            varint::encode_uvarint(9, out);
901            varint::encode_uvarint(value.len() as u64, out);
902            out.extend_from_slice(value.as_bytes());
903        }
904
905        proptest! {
906            #[test]
907            fn valid_params_round_trip(params in params_strategy()) {
908                let encoded = params.encode();
909                let decoded = Params::decode(&encoded).unwrap();
910                prop_assert_eq!(decoded, params);
911            }
912
913            #[test]
914            fn encoded_param_presence_matches_omission_rules(params in params_strategy()) {
915                let encoded = params.encode();
916                let decoded = Params::decode_with_presence(&encoded).unwrap();
917                prop_assert_eq!(&decoded.params, &params);
918
919                prop_assert_eq!(decoded.presence.protocol_version, params.protocol_version != 0);
920                prop_assert_eq!(decoded.presence.duration_ns, params.duration_ns != 0);
921                prop_assert_eq!(decoded.presence.interval_ns, params.interval_ns != 0);
922                prop_assert_eq!(decoded.presence.length, params.length != 0);
923                prop_assert_eq!(decoded.presence.dscp, params.dscp != 0);
924                prop_assert_eq!(
925                    decoded.presence.received_stats,
926                    params.received_stats != ReceivedStats::None
927                );
928                prop_assert_eq!(decoded.presence.stamp_at, params.stamp_at != StampAt::None);
929                prop_assert_eq!(decoded.presence.clock, params.clock != Clock::Unspecified);
930                prop_assert_eq!(decoded.presence.server_fill, params.server_fill.is_some());
931            }
932
933            #[test]
934            fn repeated_protocol_version_is_last_wins(first: i64, second: i64) {
935                let mut encoded = Vec::new();
936                encode_tagged_int(1, first, &mut encoded);
937                encode_tagged_int(1, second, &mut encoded);
938                let decoded = Params::decode_with_presence(&encoded).unwrap();
939                prop_assert_eq!(decoded.params.protocol_version, second);
940                prop_assert!(decoded.presence.protocol_version);
941            }
942
943            #[test]
944            fn repeated_duration_ns_is_last_wins(first: i64, second: i64) {
945                let mut encoded = Vec::new();
946                encode_tagged_int(2, first, &mut encoded);
947                encode_tagged_int(2, second, &mut encoded);
948                let decoded = Params::decode_with_presence(&encoded).unwrap();
949                prop_assert_eq!(decoded.params.duration_ns, second);
950                prop_assert!(decoded.presence.duration_ns);
951            }
952
953            #[test]
954            fn repeated_interval_ns_is_last_wins(first: i64, second: i64) {
955                let mut encoded = Vec::new();
956                encode_tagged_int(3, first, &mut encoded);
957                encode_tagged_int(3, second, &mut encoded);
958                let decoded = Params::decode_with_presence(&encoded).unwrap();
959                prop_assert_eq!(decoded.params.interval_ns, second);
960                prop_assert!(decoded.presence.interval_ns);
961            }
962
963            #[test]
964            fn repeated_length_is_last_wins(first: i64, second: i64) {
965                let mut encoded = Vec::new();
966                encode_tagged_int(4, first, &mut encoded);
967                encode_tagged_int(4, second, &mut encoded);
968                let decoded = Params::decode_with_presence(&encoded).unwrap();
969                prop_assert_eq!(decoded.params.length, second);
970                prop_assert!(decoded.presence.length);
971            }
972
973            #[test]
974            fn repeated_received_stats_is_last_wins(first in 0_i64..=3, second in 0_i64..=3) {
975                let mut encoded = Vec::new();
976                encode_tagged_int(5, first, &mut encoded);
977                encode_tagged_int(5, second, &mut encoded);
978                let decoded = Params::decode_with_presence(&encoded).unwrap();
979                prop_assert_eq!(decoded.params.received_stats, ReceivedStats::try_from(second).unwrap());
980                prop_assert!(decoded.presence.received_stats);
981            }
982
983            #[test]
984            fn repeated_stamp_at_is_last_wins(first in 0_i64..=4, second in 0_i64..=4) {
985                let mut encoded = Vec::new();
986                encode_tagged_int(6, first, &mut encoded);
987                encode_tagged_int(6, second, &mut encoded);
988                let decoded = Params::decode_with_presence(&encoded).unwrap();
989                prop_assert_eq!(decoded.params.stamp_at, StampAt::try_from(second).unwrap());
990                prop_assert!(decoded.presence.stamp_at);
991            }
992
993            #[test]
994            fn repeated_clock_is_last_wins(
995                first in explicit_clock_value_strategy(),
996                second in explicit_clock_value_strategy(),
997            ) {
998                let mut encoded = Vec::new();
999                encode_tagged_int(7, first, &mut encoded);
1000                encode_tagged_int(7, second, &mut encoded);
1001                let decoded = Params::decode_with_presence(&encoded).unwrap();
1002                prop_assert_eq!(decoded.params.clock, Clock::try_from(second).unwrap());
1003                prop_assert!(decoded.presence.clock);
1004            }
1005
1006            #[test]
1007            fn repeated_dscp_is_last_wins(first: i64, second: i64) {
1008                let mut encoded = Vec::new();
1009                encode_tagged_int(8, first, &mut encoded);
1010                encode_tagged_int(8, second, &mut encoded);
1011                let decoded = Params::decode_with_presence(&encoded).unwrap();
1012                prop_assert_eq!(decoded.params.dscp, second);
1013                prop_assert!(decoded.presence.dscp);
1014            }
1015
1016            #[test]
1017            fn repeated_server_fill_is_last_wins(
1018                first in proptest::collection::vec(proptest::char::any(), 0..=8)
1019                    .prop_map(|chars| chars.into_iter().collect::<String>()),
1020                second in proptest::collection::vec(proptest::char::any(), 0..=8)
1021                    .prop_map(|chars| chars.into_iter().collect::<String>()),
1022            ) {
1023                let mut encoded = Vec::new();
1024                encode_tagged_server_fill(&first, &mut encoded);
1025                encode_tagged_server_fill(&second, &mut encoded);
1026                let decoded = Params::decode_with_presence(&encoded).unwrap();
1027                prop_assert_eq!(
1028                    decoded.params.server_fill,
1029                    Some(ServerFill { value: second })
1030                );
1031                prop_assert!(decoded.presence.server_fill);
1032            }
1033
1034            #[test]
1035            fn unknown_scalar_tag_prefix_does_not_disturb_known_params(
1036                params in params_strategy(),
1037                unknown_tag in 10_u64..=u16::MAX as u64,
1038                unknown_value: u64,
1039            ) {
1040                let mut prefixed = Vec::new();
1041                varint::encode_uvarint(unknown_tag, &mut prefixed);
1042                varint::encode_uvarint(unknown_value, &mut prefixed);
1043                prefixed.extend_from_slice(&params.encode());
1044
1045                let decoded = Params::decode(&prefixed).unwrap();
1046                prop_assert_eq!(decoded, params.clone());
1047
1048                let decoded_with_presence = Params::decode_with_presence(&prefixed).unwrap();
1049                let expected_presence = Params::decode_with_presence(&params.encode()).unwrap().presence;
1050                prop_assert_eq!(decoded_with_presence.presence, expected_presence);
1051            }
1052
1053            #[test]
1054            fn unknown_scalar_tag_suffix_does_not_disturb_known_params(
1055                params in params_strategy(),
1056                unknown_tag in 10_u64..=u16::MAX as u64,
1057                unknown_value: u64,
1058            ) {
1059                let mut suffixed = params.encode();
1060                varint::encode_uvarint(unknown_tag, &mut suffixed);
1061                varint::encode_uvarint(unknown_value, &mut suffixed);
1062
1063                let decoded = Params::decode(&suffixed).unwrap();
1064                prop_assert_eq!(decoded, params.clone());
1065
1066                let decoded_with_presence = Params::decode_with_presence(&suffixed).unwrap();
1067                let expected_presence = Params::decode_with_presence(&params.encode()).unwrap().presence;
1068                prop_assert_eq!(decoded_with_presence.presence, expected_presence);
1069            }
1070        }
1071
1072        /// Omitted-vs-explicit-zero presence, for the fields where explicit
1073        /// zero is valid on the wire. `Clock` is deliberately excluded: an
1074        /// explicit zero there is rejected outright, not merely "present with
1075        /// the default value" (see `explicit_clock_zero_is_rejected`).
1076        #[test]
1077        fn omitted_vs_explicit_zero_presence_for_zero_valid_fields() {
1078            let scalar_tags = [1_u64, 2, 3, 4, 8];
1079            for tag in scalar_tags {
1080                let omitted = Params::decode_with_presence(&[]).unwrap();
1081                let mut explicit_zero = Vec::new();
1082                encode_tagged_int(tag, 0, &mut explicit_zero);
1083                let explicit = Params::decode_with_presence(&explicit_zero).unwrap();
1084
1085                assert_eq!(omitted.params, explicit.params);
1086                assert_eq!(explicit.params, Params::default());
1087
1088                let (omitted_presence, explicit_presence) = match tag {
1089                    1 => (
1090                        omitted.presence.protocol_version,
1091                        explicit.presence.protocol_version,
1092                    ),
1093                    2 => (omitted.presence.duration_ns, explicit.presence.duration_ns),
1094                    3 => (omitted.presence.interval_ns, explicit.presence.interval_ns),
1095                    4 => (omitted.presence.length, explicit.presence.length),
1096                    8 => (omitted.presence.dscp, explicit.presence.dscp),
1097                    _ => unreachable!(),
1098                };
1099                assert!(!omitted_presence);
1100                assert!(explicit_presence);
1101            }
1102
1103            let enum_tags = [5_u64, 6];
1104            for tag in enum_tags {
1105                let omitted = Params::decode_with_presence(&[]).unwrap();
1106                let mut explicit_zero = Vec::new();
1107                encode_tagged_int(tag, 0, &mut explicit_zero);
1108                let explicit = Params::decode_with_presence(&explicit_zero).unwrap();
1109
1110                assert_eq!(omitted.params, explicit.params);
1111
1112                let (omitted_presence, explicit_presence) = match tag {
1113                    5 => (
1114                        omitted.presence.received_stats,
1115                        explicit.presence.received_stats,
1116                    ),
1117                    6 => (omitted.presence.stamp_at, explicit.presence.stamp_at),
1118                    _ => unreachable!(),
1119                };
1120                assert!(!omitted_presence);
1121                assert!(explicit_presence);
1122            }
1123
1124            // `Clock` deliberately breaks the pattern above: explicit zero is
1125            // rejected rather than accepted-but-present.
1126            let omitted = Params::decode_with_presence(&[]).unwrap();
1127            assert_eq!(omitted.params.clock, Clock::Unspecified);
1128            assert!(!omitted.presence.clock);
1129            let mut explicit_clock_zero = Vec::new();
1130            encode_tagged_int(7, 0, &mut explicit_clock_zero);
1131            assert_eq!(
1132                Params::decode_with_presence(&explicit_clock_zero),
1133                Err(ProtoError::InvalidEnum {
1134                    name: "Clock",
1135                    value: 0,
1136                })
1137            );
1138        }
1139    }
1140}