docs(irtt-proto): add runnable doctests for public API
Add executable doctests documenting the consumer-facing contract of irtt-proto: the directional request encode/decode/verify split, Params construction and round-trip, omitted-vs-explicit-zero presence, open and echo reply round trips, packet length calculation, and HMAC verification. No conventional tests are migrated: every existing irtt-proto test exercises wire vectors, flag matrices, malformed input, layout rules, HMAC error behavior, or encode/decode round trips, which the crate's AGENTS.md treats as durable compatibility coverage rather than documentation cleanup targets.
This commit is contained in:
8 files changed
+236
No files matched your search
@@ -60,6 +60,50 @@ pub struct TimestampFields {
|
||||
pub send_mono: Option<i64>,
|
||||
}
|
||||
|
||||
/// Encodes an ECHO reply.
|
||||
///
|
||||
/// `params` supplies the negotiated layout and length, exactly as on the
|
||||
/// request side, and every optional field the layout requires must be present
|
||||
/// in `reply`: a `Some` where the layout has no field, or a `None` where it has
|
||||
/// one, is an error. `hmac_key` is authoritative: `Some(key)` sets `FLAG_HMAC`
|
||||
/// and signs the finished datagram.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`ProtoError::MissingFlag`] or [`ProtoError::UnexpectedFlag`] when
|
||||
/// the reply does not carry exactly the packet-type flags this encoder
|
||||
/// requires, [`ProtoError::MissingField`] or [`ProtoError::UnexpectedField`]
|
||||
/// when `reply`'s optional fields do not match the negotiated layout, and
|
||||
/// [`ProtoError::PayloadTooLarge`] when the payload does not fit the negotiated
|
||||
/// length.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use irtt_proto::{
|
||||
/// decode_echo_reply, encode_echo_reply, EchoReply, Params, ReceivedStats,
|
||||
/// TimestampFields, FLAG_REPLY,
|
||||
/// };
|
||||
///
|
||||
/// // `received_stats` negotiates a receive counter; four payload bytes.
|
||||
/// let params = Params {
|
||||
/// length: 24,
|
||||
/// received_stats: ReceivedStats::Count,
|
||||
/// ..Params::default()
|
||||
/// };
|
||||
/// let reply = EchoReply {
|
||||
/// flags: FLAG_REPLY,
|
||||
/// token: 0x1234_5678_9abc_def0,
|
||||
/// sequence: 7,
|
||||
/// recv_count: Some(3),
|
||||
/// recv_window: None,
|
||||
/// timestamps: TimestampFields::default(),
|
||||
/// payload: vec![1, 2, 3, 4],
|
||||
/// };
|
||||
///
|
||||
/// let packet = encode_echo_reply(&reply, ¶ms, None).unwrap();
|
||||
/// assert_eq!(decode_echo_reply(&packet, ¶ms, None).unwrap(), reply);
|
||||
/// ```
|
||||
pub fn encode_echo_reply(
|
||||
reply: &EchoReply,
|
||||
params: &Params,
|
||||
|
||||
@@ -68,6 +68,23 @@ pub fn verify_hmac(key: &[u8], packet: &[u8], hmac_offset: usize) -> Result<()>
|
||||
/// [`ProtoError::InvalidHmacOffset`] when the field is truncated, and
|
||||
/// [`ProtoError::BadHmac`] when the MAC does not match. The packet is never
|
||||
/// modified.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use irtt_proto::{encode_request, verify_packet_hmac, Params, RequestToEncode};
|
||||
///
|
||||
/// // Synthetic example key — use a real shared secret in production.
|
||||
/// let key = b"example key";
|
||||
/// let params = Params::with_protocol_defaults();
|
||||
/// let packet = encode_request(
|
||||
/// RequestToEncode::Open { params: ¶ms, no_test: false },
|
||||
/// Some(key),
|
||||
/// ).unwrap();
|
||||
///
|
||||
/// verify_packet_hmac(key, &packet).unwrap();
|
||||
/// assert!(verify_packet_hmac(b"wrong key", &packet).is_err());
|
||||
/// ```
|
||||
pub fn verify_packet_hmac(key: &[u8], packet: &[u8]) -> Result<()> {
|
||||
let envelope = envelope::decode_structural(packet)?;
|
||||
if !envelope.hmac_present {
|
||||
|
||||
@@ -135,6 +135,20 @@ pub fn echo_header_len(hmac: bool, params: &Params) -> usize {
|
||||
/// A ceiling on what a negotiated length may legitimately *be* — an MTU, a
|
||||
/// resource bound, a maximum packet size — is deliberately not here. That is
|
||||
/// server and runtime policy, and it belongs where the negotiation happens.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use irtt_proto::{echo_packet_len, Params};
|
||||
///
|
||||
/// // The default layout's mandatory field block is 16 bytes (header, token,
|
||||
/// // sequence), so a smaller requested length is floored up to it.
|
||||
/// assert_eq!(echo_packet_len(false, &Params::default()).unwrap(), 16);
|
||||
/// assert_eq!(
|
||||
/// echo_packet_len(false, &Params { length: 40, ..Params::default() }).unwrap(),
|
||||
/// 40,
|
||||
/// );
|
||||
/// ```
|
||||
pub fn echo_packet_len(hmac: bool, params: &Params) -> Result<usize> {
|
||||
let header_len = echo_header_len(hmac, params);
|
||||
let requested = if params.length <= 0 {
|
||||
|
||||
@@ -42,6 +42,39 @@
|
||||
//! which reply kind to expect, and the two need genuinely different semantic
|
||||
//! context.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! The normal path is directional: a sender encodes with [`encode_request`], a
|
||||
//! receiver classifies with [`decode_request`], and authentication is a
|
||||
//! separate step once the applicable key is known.
|
||||
//!
|
||||
//! ```
|
||||
//! use irtt_proto::{
|
||||
//! decode_request, encode_request, verify_packet_hmac, DecodedRequestKind, Params,
|
||||
//! RequestToEncode,
|
||||
//! };
|
||||
//!
|
||||
//! // Sender side: encode an authenticated open request.
|
||||
//! let params = Params::with_protocol_defaults();
|
||||
//! let key = b"example key";
|
||||
//! let packet = encode_request(
|
||||
//! RequestToEncode::Open { params: ¶ms, no_test: false },
|
||||
//! Some(key),
|
||||
//! ).unwrap();
|
||||
//!
|
||||
//! // Receiver side: classify structurally, before any key is known.
|
||||
//! let request = decode_request(&packet).unwrap();
|
||||
//! assert!(request.hmac_present);
|
||||
//! let DecodedRequestKind::Open { no_test, params: encoded } = request.kind else {
|
||||
//! panic!("expected an open request");
|
||||
//! };
|
||||
//! assert!(!no_test);
|
||||
//! assert_eq!(Params::decode(encoded).unwrap(), params);
|
||||
//!
|
||||
//! // Authentication is a separate step, once the applicable key is known.
|
||||
//! verify_packet_hmac(key, &packet).unwrap();
|
||||
//! ```
|
||||
//!
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
pub mod echo;
|
||||
|
||||
@@ -13,6 +13,34 @@ pub struct OpenReply {
|
||||
pub params: Params,
|
||||
}
|
||||
|
||||
/// Encodes an open reply.
|
||||
///
|
||||
/// `hmac_key` is authoritative: `Some(key)` sets `FLAG_HMAC` and signs the
|
||||
/// finished datagram, while `None` produces an unauthenticated reply and clears
|
||||
/// any caller-supplied `FLAG_HMAC`. A zero token is rejected unless the reply
|
||||
/// also carries `FLAG_CLOSE` (a rejected open).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`ProtoError::MissingFlag`] when the reply lacks `FLAG_OPEN` or
|
||||
/// `FLAG_REPLY`, and [`ProtoError::ZeroToken`] for a zero token without
|
||||
/// `FLAG_CLOSE`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use irtt_proto::{
|
||||
/// decode_open_reply, encode_open_reply, OpenReply, Params, FLAG_OPEN, FLAG_REPLY,
|
||||
/// };
|
||||
///
|
||||
/// let reply = OpenReply {
|
||||
/// flags: FLAG_OPEN | FLAG_REPLY,
|
||||
/// token: 0x1234_5678_9abc_def0,
|
||||
/// params: Params::with_protocol_defaults(),
|
||||
/// };
|
||||
/// let packet = encode_open_reply(&reply, None).unwrap();
|
||||
/// assert_eq!(decode_open_reply(&packet, None).unwrap(), reply);
|
||||
/// ```
|
||||
pub fn encode_open_reply(reply: &OpenReply, hmac_key: Option<&[u8]>) -> Result<Vec<u8>> {
|
||||
let params = reply.params.encode();
|
||||
let mut out = envelope::begin_checked(
|
||||
@@ -30,6 +58,18 @@ pub fn encode_open_reply(reply: &OpenReply, hmac_key: Option<&[u8]>) -> Result<V
|
||||
envelope::finish(out, hmac_key)
|
||||
}
|
||||
|
||||
/// Decodes an open reply.
|
||||
///
|
||||
/// Requires `FLAG_OPEN` and `FLAG_REPLY`, and — when `hmac_key` is supplied —
|
||||
/// verifies the authentication field. The decoded [`Params`] are validated:
|
||||
/// malformed or incompatible parameter values are rejected here.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`ProtoError::MissingFlag`], [`ProtoError::HmacPresenceMismatch`],
|
||||
/// [`ProtoError::BadHmac`], [`ProtoError::PacketTooShort`],
|
||||
/// [`ProtoError::ZeroToken`], or a parameter decode error. See
|
||||
/// [`encode_open_reply`] for a round-trip example.
|
||||
pub fn decode_open_reply(packet: &[u8], hmac_key: Option<&[u8]>) -> Result<OpenReply> {
|
||||
let envelope = envelope::decode(
|
||||
packet,
|
||||
|
||||
@@ -16,6 +16,29 @@ pub const MAX_SERVER_FILL_BYTES: usize = 32;
|
||||
/// [`clock`](Params::clock) is [`Clock::Unspecified`], which is what an open
|
||||
/// request with an empty parameter payload means. It is not a set of sensible
|
||||
/// client settings; a client builds its request from its own configuration.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// Construct a `Params`, encode it, and decode it back:
|
||||
///
|
||||
/// ```
|
||||
/// use irtt_proto::{Clock, Params, ReceivedStats, ServerFill, StampAt};
|
||||
///
|
||||
/// 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,
|
||||
/// server_fill: Some(ServerFill { value: "rand".to_owned() }),
|
||||
/// ..Params::default()
|
||||
/// };
|
||||
///
|
||||
/// let encoded = params.encode();
|
||||
/// assert_eq!(Params::decode(&encoded).unwrap(), params);
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct Params {
|
||||
pub protocol_version: i64,
|
||||
@@ -88,6 +111,20 @@ impl Params {
|
||||
/// Presence means the tag appeared at least once. Repeated known tags keep
|
||||
/// last-value-wins, and unknown tags remain ignored and untracked.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use irtt_proto::Params;
|
||||
///
|
||||
/// // Wire bytes: tag 2 (duration) carrying an explicit zero. Interval
|
||||
/// // (tag 3) is absent, so both fields decode to zero but only one is
|
||||
/// // reported present.
|
||||
/// let decoded = Params::decode_with_presence(&[0x02, 0x00]).unwrap();
|
||||
/// assert_eq!(decoded.params.duration_ns, 0);
|
||||
/// assert!(decoded.presence.duration_ns);
|
||||
/// assert!(!decoded.presence.interval_ns);
|
||||
/// ```
|
||||
///
|
||||
/// [`decode`]: Params::decode
|
||||
pub fn decode_with_presence(input: &[u8]) -> Result<DecodedParams> {
|
||||
let mut params = Self::default();
|
||||
|
||||
@@ -88,6 +88,26 @@ pub enum DecodedRequestKind<'a> {
|
||||
/// the negotiated length is positive and wider than `usize`. A *negative*
|
||||
/// negotiated length is not an error: see [`echo_packet_len`], which floors it
|
||||
/// at the mandatory field block.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use irtt_proto::{encode_request, Params, RequestToEncode};
|
||||
///
|
||||
/// // An unauthenticated ECHO probe. `params` supplies the negotiated length,
|
||||
/// // which reserves four payload bytes beyond the 16-byte echo header.
|
||||
/// let params = Params { length: 20, ..Params::default() };
|
||||
/// let packet = encode_request(
|
||||
/// RequestToEncode::Echo {
|
||||
/// token: 0x1234_5678_9abc_def0,
|
||||
/// sequence: 7,
|
||||
/// params: ¶ms,
|
||||
/// payload: &[1, 2, 3, 4],
|
||||
/// },
|
||||
/// None,
|
||||
/// ).unwrap();
|
||||
/// assert_eq!(packet.len(), 20);
|
||||
/// ```
|
||||
pub fn encode_request(request: RequestToEncode<'_>, hmac_key: Option<&[u8]>) -> Result<Vec<u8>> {
|
||||
match request {
|
||||
RequestToEncode::Open { params, no_test } => encode_open(params, no_test, hmac_key),
|
||||
@@ -138,6 +158,21 @@ pub fn encode_request(request: RequestToEncode<'_>, hmac_key: Option<&[u8]>) ->
|
||||
/// Returns [`ProtoError::PacketTooShort`], [`ProtoError::BadMagic`],
|
||||
/// [`ProtoError::ReservedFlags`], or [`ProtoError::UnexpectedFlag`] for
|
||||
/// `FLAG_REPLY`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use irtt_proto::{decode_request, encode_request, DecodedRequestKind, RequestToEncode};
|
||||
///
|
||||
/// let packet = encode_request(
|
||||
/// RequestToEncode::Close { token: 0x1234_5678_9abc_def0 },
|
||||
/// None,
|
||||
/// ).unwrap();
|
||||
///
|
||||
/// let request = decode_request(&packet).unwrap();
|
||||
/// assert!(!request.hmac_present);
|
||||
/// assert_eq!(request.kind, DecodedRequestKind::Close { token: 0x1234_5678_9abc_def0 });
|
||||
/// ```
|
||||
pub fn decode_request(packet: &[u8]) -> Result<DecodedRequest<'_>> {
|
||||
let envelope = envelope::decode_structural(packet)?;
|
||||
if has(envelope.flags, FLAG_REPLY) {
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
//! Variable-length integer encoding for IRTT wire values.
|
||||
//!
|
||||
//! Tags use an unsigned base-128 varint ([`encode_uvarint`]), and signed
|
||||
//! integers (such as parameter values) use the same encoding after a zigzag
|
||||
//! transform ([`encode_varint`]), which keeps small negative numbers compact.
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```
|
||||
//! use irtt_proto::varint::{decode_varint, encode_varint};
|
||||
//!
|
||||
//! let mut out = Vec::new();
|
||||
//! encode_varint(1_000_000_000, &mut out);
|
||||
//! assert_eq!(decode_varint(&out).unwrap(), (1_000_000_000, out.len()));
|
||||
//! ```
|
||||
|
||||
use crate::{ProtoError, Result};
|
||||
|
||||
pub fn encode_uvarint(mut value: u64, out: &mut Vec<u8>) {
|
||||
|
||||
Reference in new issue
Block a user