Skip to main content

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