moqtap_client/draft17/session/request_id.rs
1use moqtap_codec::varint::VarInt;
2
3/// Role of the endpoint (determines request ID parity).
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum Role {
6 /// Client uses even request IDs: 0, 2, 4, ...
7 Client,
8 /// Server uses odd request IDs: 1, 3, 5, ...
9 Server,
10}
11
12/// Errors from request ID allocation or validation.
13#[derive(Debug, thiserror::Error, PartialEq, Eq)]
14pub enum RequestIdError {
15 /// The request ID exceeds the current MAX_REQUEST_ID.
16 #[error("request ID {0} exceeds max {1}")]
17 ExceedsMax(u64, u64),
18 /// The request ID has the wrong parity for the given role.
19 #[error("request ID {0} has wrong parity for {1:?}")]
20 WrongParity(u64, Role),
21 /// MAX_REQUEST_ID must only increase; it decreased.
22 #[error("max request ID can only increase: was {0}, got {1}")]
23 Decreased(u64, u64),
24 /// No request IDs are available (max is 0 or exhausted).
25 #[error("no request IDs available (blocked)")]
26 Blocked,
27}
28
29/// Allocates and validates request IDs per the MoQT spec.
30///
31/// - Client: even IDs (0, 2, 4, ...)
32/// - Server: odd IDs (1, 3, 5, ...)
33/// - Default MAX_REQUEST_ID: 0 (no requests until increased)
34/// - MAX_REQUEST_ID can only increase
35pub struct RequestIdAllocator {
36 role: Role,
37 next_id: u64,
38 max_id: u64,
39}
40
41impl RequestIdAllocator {
42 /// Create a new allocator for the given role, starting at ID 0 or 1.
43 pub fn new(role: Role) -> Self {
44 let next_id = match role {
45 Role::Client => 0,
46 Role::Server => 1,
47 };
48 // Draft-17 removed MAX_REQUEST_ID; allocator is never blocked.
49 Self { role, next_id, max_id: u64::MAX }
50 }
51
52 /// Allocate the next request ID.
53 pub fn allocate(&mut self) -> Result<VarInt, RequestIdError> {
54 if self.max_id == 0 || self.next_id > self.max_id {
55 return Err(RequestIdError::Blocked);
56 }
57 let id = VarInt::from_u64(self.next_id).unwrap();
58 self.next_id += 2;
59 Ok(id)
60 }
61
62 /// Update the maximum allowed request ID (can only increase).
63 pub fn update_max(&mut self, new_max: u64) -> Result<(), RequestIdError> {
64 if new_max <= self.max_id {
65 return Err(RequestIdError::Decreased(self.max_id, new_max));
66 }
67 self.max_id = new_max;
68 Ok(())
69 }
70
71 /// Validate a request ID the peer put on the wire.
72 ///
73 /// Draft-17 Section 9.1: "The client generates even numbered Request IDs,
74 /// starting at 0, and the server generates odd numbered Request IDs,
75 /// starting at 1." The parity checked here is therefore the **peer's**,
76 /// the opposite of this allocator's own — a client validates odd ids.
77 ///
78 /// The same section makes a wrong bit fatal to the session: "If an
79 /// endpoint receives a Request ID where the least significant bit is
80 /// incorrect for the sender, or a duplicate Request ID, it MUST close the
81 /// session with INVALID_REQUEST_ID." Only the first half is answered here.
82 /// Duplicate detection needs memory of every id the peer has already
83 /// spent, which this allocator does not keep and cannot keep without
84 /// taking `&mut self`; it lives on the endpoint instead.
85 ///
86 /// The parity rule is also what makes one `HashMap` per request kind
87 /// enough for both directions: an id this endpoint allocates and an id the
88 /// peer allocates can never be equal, so a peer's request cannot collide
89 /// with one of ours.
90 ///
91 /// # Errors
92 ///
93 /// - [`RequestIdError::WrongParity`] carrying the id and the **peer's**
94 /// role, so the message names the endpoint that broke the rule rather
95 /// than the one that caught it.
96 /// - [`RequestIdError::ExceedsMax`] if the id is past the current
97 /// MAX_REQUEST_ID. Draft-17 removed MAX_REQUEST_ID and
98 /// [`RequestIdAllocator::new`] sets the ceiling to `u64::MAX`, so on
99 /// this draft the check cannot fire for any id a varint can carry.
100 pub fn validate_peer_id(&self, id: u64) -> Result<(), RequestIdError> {
101 // Peer has opposite parity
102 let expected_even = match self.role {
103 Role::Client => false, // peer is Server, expects odd
104 Role::Server => true, // peer is Client, expects even
105 };
106 let is_even = id.is_multiple_of(2);
107 if is_even != expected_even {
108 let peer_role = match self.role {
109 Role::Client => Role::Server,
110 Role::Server => Role::Client,
111 };
112 return Err(RequestIdError::WrongParity(id, peer_role));
113 }
114 if id > self.max_id {
115 return Err(RequestIdError::ExceedsMax(id, self.max_id));
116 }
117 Ok(())
118 }
119
120 /// Check if we are blocked (max_id is 0 or next_id > max_id).
121 pub fn is_blocked(&self) -> bool {
122 self.max_id == 0 || self.next_id > self.max_id
123 }
124
125 /// Get the current maximum request ID.
126 pub fn max_id(&self) -> u64 {
127 self.max_id
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 /// A client allocates the even ids and must accept only odd ones from the
136 /// peer; a server is the mirror. The two are checked together against the
137 /// ids each allocator actually hands out, so a parity that is inverted in
138 /// both places at once — which would let a client talk to itself — still
139 /// fails.
140 ///
141 /// Inverting `expected_even` in `validate_peer_id` fails with:
142 ///
143 /// ```text
144 /// a Client refused 1, which a Server allocates
145 /// ```
146 #[test]
147 fn each_role_accepts_only_the_ids_the_other_role_allocates() {
148 for (role, peer_role) in [(Role::Client, Role::Server), (Role::Server, Role::Client)] {
149 let mut allocator = RequestIdAllocator::new(role);
150 let mut peer = RequestIdAllocator::new(peer_role);
151 for _ in 0..4 {
152 let peer_id = peer.allocate().unwrap().into_inner();
153 assert!(
154 allocator.validate_peer_id(peer_id).is_ok(),
155 "a {role:?} refused {peer_id}, which a {peer_role:?} allocates",
156 );
157 let own_id = allocator.allocate().unwrap().into_inner();
158 assert_eq!(
159 allocator.validate_peer_id(own_id),
160 Err(RequestIdError::WrongParity(own_id, peer_role)),
161 "a {role:?} accepted {own_id} from the peer, an id it allocates itself",
162 );
163 }
164 }
165 }
166
167 /// The error names the peer, not the endpoint that caught it. Draft-17
168 /// Section 9.1 makes this a session close, so the reason phrase a peer
169 /// reads has to say whose bit was wrong.
170 #[test]
171 fn wrong_parity_names_the_peer_that_sent_the_id() {
172 let client = RequestIdAllocator::new(Role::Client);
173 let err = client.validate_peer_id(4).unwrap_err();
174 assert_eq!(err.to_string(), "request ID 4 has wrong parity for Server");
175
176 let server = RequestIdAllocator::new(Role::Server);
177 let err = server.validate_peer_id(7).unwrap_err();
178 assert_eq!(err.to_string(), "request ID 7 has wrong parity for Client");
179 }
180
181 /// Draft-17 removed MAX_REQUEST_ID, so the ceiling check in
182 /// `validate_peer_id` is unreachable on this draft: the largest id a MoQT
183 /// varint can carry is still accepted.
184 #[test]
185 fn no_ceiling_rejects_a_peer_id_on_this_draft() {
186 let client = RequestIdAllocator::new(Role::Client);
187 let largest_odd = moqtap_codec::varint::MAX_MOQT_VARINT | 1;
188 assert_eq!(client.validate_peer_id(largest_odd), Ok(()));
189 }
190}