refactor(proto): preserve open parameter presence

`Params::decode` seeded `Params::default()` and overwrote only the tags it
saw, so an omitted tag and one explicitly encoded as zero were
indistinguishable afterwards. The protocol gives those the same value but not
the same meaning, and a server cannot apply its open admission rules without
telling them apart: an absent Duration or Interval is accepted as the wire
default zero, while one explicitly encoded as zero is invalid.

Add `Params::decode_with_presence`, returning the decoded values alongside a
`ParamPresence` recording which known tags appeared. `Params::decode` now
delegates to it, so there is still exactly one parser and the two cannot
diverge. Unknown tags stay ignored and untracked, and repeated known tags keep
last-value-wins.

Add `Clock::Unspecified = 0` as the default, which is the wire default for an
absent Clock tag and what an empty parameter payload decodes to. An explicit
Clock tag encoding zero remains an invalid enum value, so `Unspecified` is
only ever reached by omission; `encode` omits the tag for it rather than
emitting an explicit zero. It selects no clock, so `has_wall`/`has_mono` are
false and no timestamp field is laid out.

`ClientConfig::clock` is public and therefore now constructible as
`Unspecified`, which is a wire default rather than a client mode. Reject it
during config validation. The client default stays `Clock::Both`, so normal
client-generated params and wire vectors are unchanged.
This commit is contained in:
Nils Andreas Svee committed 2026-08-12 00:23:14 +07:00
1 parent 78e503ffcc
commit 93491e7762
10 files changed
+325 -16

No files matched your search

+7 -2
View File
@@ -13,8 +13,13 @@ use crate::{
/// header.
const MIDPOINT_COMPAT_EXTRA: usize = TIMESTAMP_SIZE;
/// Whether the negotiation selects a midpoint timestamp from exactly one clock,
/// which is the only case the upstream dual-field compatibility form applies to.
///
/// [`Clock::Unspecified`] selects no clock and therefore lays out no midpoint
/// field at all, so it is not a single-clock negotiation.
fn is_midpoint_single_clock(params: &Params) -> bool {
params.stamp_at == StampAt::Midpoint && !matches!(params.clock, Clock::Both)
params.stamp_at == StampAt::Midpoint && matches!(params.clock, Clock::Wall | Clock::Monotonic)
}
/// Total datagram length of the upstream dual-midpoint compatibility form.
@@ -135,7 +140,7 @@ pub fn decode_echo_reply(
match params.clock {
Clock::Wall => (Some(wall), None),
Clock::Monotonic => (None, Some(mono)),
Clock::Both => {
Clock::Unspecified | Clock::Both => {
unreachable!("midpoint compat only applies to single-clock negotiations")
}
}