moqtap_client/above_codec_rules.rs
1//! The rules an endpoint enforces that its own decoder cannot see, named once
2//! across the drafts that state them.
3//!
4//! # Why these do not live in the codec
5//!
6//! A decoder refuses a frame it cannot read. Every rule here is stated about a
7//! frame that reads perfectly well — an Object whose properties are legal bytes
8//! on a status that forbids properties, a GOAWAY that is a valid GOAWAY and is
9//! the second one, a Request ID that is a valid varint and belongs to the other
10//! endpoint's half of the number space. Being an *endpoint* rather than an
11//! observer is what turns each of them into an error, so each is raised on a
12//! receive path and never in the decoder.
13//!
14//! That placement is deliberate and it has a cost: a caller that reads
15//! [`crate::dispatch::ErrorCause::Codec`] to find out which rule a peer broke
16//! sees none of them, because the codec never raised anything.
17//! [`AboveCodecRule`] is the other half of that answer.
18//!
19//! # Two groups, one enum
20//!
21//! **What one frame says about itself.** Three rules, across six drafts, each
22//! comparing two fields of a single message: properties against a status, a
23//! payload against a status, a stream's first message type against the set of
24//! types that may open one. They reach here through
25//! `Connection::draft_specific_cause`, which every draft implements over its own
26//! `ConnectionError`.
27//!
28//! The drafts renamed two of the three and widened one, and none of the three
29//! changes altered what is required:
30//!
31//! - **Properties on a non-Normal status.** Drafts 15 and 16 call the block
32//! *extension headers* and drafts 17 through 20 call it *properties*. Same
33//! sentence, same consequence, one rule.
34//! - **A bidirectional stream's opening message.** Draft-16 permits exactly two
35//! openers and names SUBSCRIBE_NAMESPACE as the second; drafts 17 through 20
36//! permit any message that begins a request stream. The legal set grew, the
37//! requirement did not.
38//! - **A payload on a status that permits none.** Drafts 17 through 20 only,
39//! and the one of the three the drafts state *without* a close — see
40//! [`AboveCodecRule::PayloadOnStatusDatagram`].
41//!
42//! **What a message says about the session it arrived in.** The rest, across
43//! all the drafts, each comparing a message against state this endpoint
44//! has been keeping: a Request ID against the sequence the peer's own ids
45//! follow, a GOAWAY against whether one has already arrived, a Track Alias
46//! against the track it already names, an Object's Location against the one the
47//! track ended at. They reach here through `EndpointError::fault`, which every
48//! draft implements over its own `EndpointError`.
49//!
50//! No frame carries the fact that decides any of them, so no decoder built for
51//! any draft could refuse one — the same reason the first group is here, one
52//! layer further out.
53//!
54//! # Which draft states which is not asked here
55//!
56//! It is answered per draft, beside the variant whose doc comment quotes that
57//! draft's own sentence — the same division of labour
58//! `Connection::codec_session_error_code` already has for the decoder's errors.
59//! A rule named here on ten drafts and not on the other four is not a claim
60//! that the other four permit it; it is a claim that this build enforces it
61//! where the text says to.
62//!
63//! # And a second enum, for the rules the decoder *did* refuse
64//!
65//! [`CodecRule`] is at the foot of this file and is the other half of the same
66//! job. Everything above is a rule no decoder could have caught; everything
67//! there is one a decoder caught and nothing carried the draft's own sentence
68//! for. The two are kept apart because they are two kinds of evidence — see
69//! that enum's own doc — and they share this file for one reason:
70//! `scripts/check-drafts.py` rule 8 reads [`RuleCitation`] rows out of **this
71//! path**, so a citation written anywhere else is a citation nothing checks.
72
73/// A rule a peer broke that no decoder could have caught.
74///
75/// Deliberately **not** `#[non_exhaustive]`. A consumer matching on this — a
76/// conformance report deciding what it will name a relay for — should find out
77/// about a new rule by failing to build, not by silently filing it under a
78/// wildcard arm.
79///
80/// The `close` that travels beside it in
81/// [`crate::dispatch::ErrorCause::PeerViolation`] is the negotiated draft's
82/// own, and several of these are stated with a close on some drafts and without
83/// one on others. The rule is the same rule either way; what the draft does
84/// about it is the other field.
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
86pub enum AboveCodecRule {
87 // ── What one frame says about itself ───────────────────────
88 /// An Object arrived carrying properties on a status that is not Normal.
89 ///
90 /// Drafts 15 and 16 state it of *extension headers* and drafts 17 through
91 /// 20 of *properties*; the block was renamed and the rule was not. Every
92 /// draft that states it answers it with a close, so a caller reading
93 /// [`crate::dispatch::ErrorCause::PeerViolation`] finds `close: Some` on
94 /// all six.
95 PropertiesOnNonNormalStatus,
96 /// A datagram whose header declared a status permitting no payload arrived
97 /// with bytes after the header.
98 ///
99 /// The one rule here the drafts state as a property of a conforming Object
100 /// rather than as a "MUST close the session" case, so `close` is `None` on
101 /// every draft that states it, and the datagram is refused with the session
102 /// left running. A caller that gates on `close: Some` will therefore never
103 /// see this one, which is the correct behaviour and not an oversight: a
104 /// relay cannot be published for breaking a rule its draft attaches no
105 /// consequence to.
106 PayloadOnStatusDatagram,
107 /// A bidirectional stream the peer opened began with a message type the
108 /// draft does not permit one to begin with.
109 ///
110 /// The permitted set differs — draft-16 allows the control stream and
111 /// SUBSCRIBE_NAMESPACE, drafts 17 through 20 allow any message that begins
112 /// a request stream — and the sentence that forbids the rest is the same on
113 /// all four. The offending stream is reset and the session closed before
114 /// the error is returned.
115 ///
116 /// Reached from both halves of this module: a draft's own `ConnectionError`
117 /// raises it where the connection owns the stream, and `EndpointError`
118 /// where the endpoint does. One rule, so one name.
119 BidiStreamOpener,
120
121 // ── What a message says about the session ──────────────────
122 /// A message arrived on a stream the draft does not place it on.
123 ///
124 /// The control stream carrying one the draft puts on a request stream, a
125 /// request stream carrying one that may not follow a request there, a
126 /// response arriving where no request is outstanding. Draft-16 states it of
127 /// NAMESPACE and NAMESPACE_DONE; drafts 18 through 20 state it of every
128 /// message their Table 5 gives a Stream value to.
129 ///
130 /// Distinct from [`Self::BidiStreamOpener`], which is about the *first*
131 /// message on a stream and is what decides what that stream is. This is
132 /// about a later one arriving somewhere it does not belong.
133 ///
134 /// # No draft states a close for it
135 ///
136 /// The rule is real — a NAMESPACE on the control stream names no request
137 /// and there is nothing for a receiver to do with it — and the
138 /// **consequence** would be ours to invent. The tempting one is
139 /// `Some(PROTOCOL_VIOLATION)`, on the reading that Section 3.3's opener
140 /// sentence covers a message on the wrong stream. It does not: that
141 /// sentence is about what a bidirectional stream may *begin* with, and a
142 /// message arriving on the control stream begins nothing.
143 ///
144 /// Read across all texts for a sentence that closes a session over
145 /// a message being in the wrong place, there is none. The drafts state
146 /// placement per message, mostly descriptively; drafts 18 through 20 add a
147 /// Stream column to their message table, whose only MUST is that a message
148 /// marked First is the first message on a new request stream — which is
149 /// [`Self::BidiStreamOpener`]'s subject and names no code; and the only
150 /// sentence in the range that closes a session over misplacement is drafts
151 /// 19 and 20's REQUEST_UPDATE sentence, which is
152 /// [`Self::RequestUpdateForTheWrongRequest`]'s.
153 ///
154 /// So a close here would be this build's model of the protocol rather than
155 /// a draft's. `session_error_code` answers `None` for every variant that
156 /// reaches this rule on every draft that raises it, and the receive paths
157 /// that raise them do not fail the session: the message is refused, the
158 /// caller is told, and the session runs on — which it can, because a
159 /// control message carries its own length and the next boundary on the
160 /// stream is known however this one was refused.
161 ///
162 /// One variant is covered by a draft sentence and is filed under the rule
163 /// that carries it rather than here. A REQUEST_UPDATE on the control stream
164 /// is, on drafts 19 and 20, squarely inside the sentence that closes over a
165 /// REQUEST_UPDATE outside the two cases those drafts permit — the first of
166 /// which is the request's own bidi stream — so it is
167 /// [`Self::RequestUpdateForTheWrongRequest`] there, with the citation that
168 /// rule already carries. On drafts 17 and 18 the same message answers
169 /// `None`, because those drafts describe the placement and attach no
170 /// consequence to it.
171 ///
172 /// The rule stays named. It is enforced, it is the peer's doing, and a
173 /// draft that attaches a consequence to it should arrive here as a citation
174 /// rather than as a rediscovery.
175 MessageOnTheWrongStream,
176 /// A response arrived on one request's stream naming a different request.
177 ///
178 /// Draft-16 only, and only because that draft has both halves at once: a
179 /// stream per namespace subscription *and* a Request ID on the messages
180 /// that travel it, so the two can disagree. Drafts 17 and later deleted the
181 /// id from responses, which deletes the disagreement.
182 ResponseNamesAnotherRequest,
183 /// A request stream's response half opened with something other than the
184 /// response the draft requires first.
185 ///
186 /// Drafts 18 through 20, of SUBSCRIBE_NAMESPACE and SUBSCRIBE_TRACKS: "If
187 /// the subscriber receives any message other than a REQUEST_OK or a
188 /// REQUEST_ERROR as the first message on the response half of the stream,
189 /// then it MUST close the session with a PROTOCOL_VIOLATION."
190 ResponseBeforeItsFirstResponse,
191 /// A GOAWAY arrived at a server that had no standing to be sent one.
192 ///
193 /// Draft-07 states the rule about the message — a server may not be sent a
194 /// GOAWAY at all — and drafts 08 through 20 about the migration URI it
195 /// carries, since only a server may tell a client where to reconnect. The
196 /// rename is the same shape as [`Self::PropertiesOnNonNormalStatus`]'s: the
197 /// text moved the prohibition onto the field, and what a conforming client
198 /// may send did not change.
199 GoAwayAtServer,
200 /// A second GOAWAY arrived where the draft allows one.
201 ///
202 /// Per session on every draft, and from draft-18 also per request stream —
203 /// that draft lets a GOAWAY migrate a single request, so one on each of two
204 /// request streams is two first GOAWAYs and not a repeat. Both are the same
205 /// sentence read at two scopes.
206 RepeatedGoAway,
207 /// A server received a Redirect carrying a Connect URI.
208 ///
209 /// Drafts 18 through 20. The same standing [`Self::GoAwayAtServer`] is
210 /// about, stated of the message that replaced GOAWAY's migration half.
211 RedirectUriAtServer,
212 /// A Redirect answering a namespace-scoped request carried a Track Name.
213 ///
214 /// Drafts 18 through 20: "an endpoint that receives a non-empty Track Name
215 /// in a Redirect for a namespace-scoped request MUST close the session with
216 /// a PROTOCOL_VIOLATION."
217 ///
218 /// Which requests are namespace-scoped is named in the first half of that
219 /// same sentence, and is described here rather than quoted because the list
220 /// is the half of it that moves: draft-18 names SUBSCRIBE_NAMESPACE and
221 /// PUBLISH_NAMESPACE, and drafts 19 and 20 add SUBSCRIBE_TRACKS. An
222 /// ellipsis across that list would join the clause before it to the clause
223 /// after it and make a sentence none of the three drafts has — which a
224 /// checker reports as *in no draft at all*, and which a reader cannot tell
225 /// from a transcription.
226 RedirectTrackNameOnNamespaceRequest,
227 /// A Request ID arrived whose least significant bit belongs to this
228 /// endpoint's half of the number space.
229 ///
230 /// Drafts 11 and later, which split the space by parity. Drafts 07 through
231 /// 10 have one shared sequence and state no parity rule, so they cannot
232 /// break this one.
233 RequestIdParity,
234 /// A new request carried a Request ID that is not the next one the peer's
235 /// own sequence calls for.
236 ///
237 /// A repeat and a skip alike. Drafts 11 through 16 state it as "a new
238 /// request with a Request ID that is not expected" and drafts 17 through 20
239 /// as "a duplicate Request ID"; drafts 07 through 10 require the shared
240 /// Subscribe ID to be "unique and monotonically increasing". Three
241 /// phrasings of one requirement — that an id identifies exactly one request
242 /// for the life of a session.
243 RequestIdOutOfSequence,
244 /// A request carried a Request ID at or above the ceiling this endpoint
245 /// advertised.
246 ///
247 /// Drafts 07 through 16. The number measured against is the one **this**
248 /// endpoint sent, not the one the peer sent it, and the two are different
249 /// values. Draft-17 removed MAX_REQUEST_ID and the drafts after it have not
250 /// brought it back, so there is no ceiling left to exceed.
251 RequestIdCeiling,
252 /// A ceiling the peer raised did not increase.
253 ///
254 /// Every draft from 07 to 16 states it, and no one sentence covers them.
255 /// Drafts 11 through 13: "The Maximum Request ID MUST only increase within
256 /// a session, and receipt of a MAX_REQUEST_ID message with an equal or
257 /// smaller Request ID value is a 'Protocol Violation'."
258 ///
259 /// The other three eras say the same thing in different words. Drafts 07
260 /// through 10 state it of the Maximum Subscribe Id and MAX_SUBSCRIBE_ID;
261 /// drafts 14 and 15 carry draft-14's rename of the code, so the sentence
262 /// ends `is a PROTOCOL_VIOLATION` with no quotation marks on it; and
263 /// draft-16 splits it in two, keeping the first clause as a sentence of its
264 /// own and answering the second with a close. Draft-17 removed
265 /// MAX_REQUEST_ID and nothing after it restored one, so there is no ceiling
266 /// left for a peer to lower. A setup parameter carrying the same field is
267 /// held to the same sentence.
268 ///
269 /// The mirror — this endpoint asked to send a ceiling of its own that does
270 /// not increase — is a separate `EndpointError` variant on every draft that
271 /// has this one, so that the two never arrive as the same value. See
272 /// [`EndpointFault`] for why one variant covering both would have been a
273 /// silent wrong answer rather than an imprecise one.
274 MaxRequestIdDecreased,
275 /// A setup parameter arrived with a value of a kind the draft does not give
276 /// that parameter.
277 ///
278 /// Drafts 07 through 10, where MAX_SUBSCRIBE_ID is read out of the setup
279 /// block by hand. The frame decodes — a key-value pair holding bytes where
280 /// a varint was meant is a well-formed key-value pair — so the decoder
281 /// cannot see it and the endpoint reading the parameter is what does.
282 SetupParameterValue,
283 /// The peer used one Track Alias for two different tracks at once.
284 ///
285 /// Every draft states it, and every draft names a code of its own for it
286 /// rather than the general one — which is why the code travels beside the
287 /// rule instead of being assumed from it.
288 DuplicateTrackAlias,
289 /// Objects of one track arrived under more than one forwarding preference.
290 ///
291 /// Drafts 07 through 11: "it SHOULD close the session with an error of
292 /// 'Protocol Violation'". SHOULD, so the close is the caller's to make and
293 /// this build's is opt-in. The condition the drafts state it of narrows at
294 /// draft-11, from any two differing preferences to Objects arriving on both
295 /// Subgroup streams and datagrams for one SUBSCRIBE; the consequence is the
296 /// same clause either way, which is why the clause and not the whole
297 /// sentence is what is quoted.
298 ///
299 /// Drafts 12 through 15 raise it as well and answer it with nothing. That
300 /// is where a differing Forwarding Preference became one entry in the
301 /// Malformed Track list, and what those drafts require of a subscriber that
302 /// detects one is an UNSUBSCRIBE and an error to the application — see
303 /// [`Self::ObjectPastFinalObject`], which is another entry in the same
304 /// list. The quotation is held to drafts 07 through 11 for that reason:
305 /// ranging it across all nine would file four drafts' silence under a
306 /// sentence they dropped.
307 MixedForwardingPreference,
308 /// An Object with status END_OF_TRACK arrived somewhere the draft does not
309 /// allow one.
310 ///
311 /// Drafts 08 through 13: "the receiver MUST terminate the session".
312 EndOfTrackOutOfPlace,
313 /// An Object arrived past the Object the track had already ended at.
314 ///
315 /// Drafts 12 and later. A Malformed Track rather than a session error, and
316 /// what the drafts require of a subscriber that detects one moves twice
317 /// across that range. The words here are drafts 17 through 20's: "it MUST
318 /// cancel any corresponding subscription or fetches for that Track from
319 /// that publisher".
320 ///
321 /// Drafts 12 and 13 say UNSUBSCRIBE from the Track; drafts 14 through 16
322 /// say UNSUBSCRIBE any subscription and FETCH_CANCEL any fetch for that
323 /// Track from that publisher, which is the same operation named by the two
324 /// messages that perform it. All three are transport operations on the
325 /// requests and none of them is a close, so `close` is `None` wherever this
326 /// appears — the reading the range shares, and the reason one quotation
327 /// stands for the whole of it here.
328 ///
329 /// *Past* is the drafts' own Location comparison and not a reading of the
330 /// word: an Object in a later group is past the end whatever its own Object
331 /// ID is.
332 ObjectPastFinalObject,
333 /// An update arrived naming a request that cannot take one.
334 ///
335 /// A request the session has never carried, one that has already ended, one
336 /// of a kind the draft does not let a subscriber update. Drafts 12 through
337 /// 20 state some of these; drafts 19 and 20 gather them into one sentence —
338 /// "An endpoint that receives a REQUEST_UPDATE other than in the two cases
339 /// above MUST close the session with a PROTOCOL_VIOLATION."
340 RequestUpdateForTheWrongRequest,
341 /// More outstanding REQUEST_UPDATEs on one stream than this endpoint
342 /// advertised room for.
343 ///
344 /// Drafts 19 and 20: "If an endpoint receives a REQUEST_UPDATE on a stream
345 /// that already has MAX_REQUEST_UPDATES outstanding REQUEST_UPDATEs, it
346 /// MUST close the session with TOO_MANY_REQUEST_UPDATES."
347 ///
348 /// The ceiling beside it in the same section, MAX_FILTER_RANGES, is
349 /// answered with a REQUEST_ERROR instead, and nothing about either sentence
350 /// signals which — see [`EndpointFault::ThisEndpoint`], which is where that
351 /// one lands.
352 TooManyRequestUpdates,
353 /// Track Properties arrived on a REQUEST_OK answering something that is not
354 /// a TRACK_STATUS.
355 ///
356 /// Drafts 18 through 20: they "are empty in PUBLISH_OK, REQUEST_UPDATE_OK,
357 /// SUBSCRIBE_NAMESPACE_OK and PUBLISH_NAMESPACE_OK. If an endpoint receives
358 /// Track Properties in one of these messages it MUST close the session with
359 /// a PROTOCOL_VIOLATION."
360 TrackPropertiesOnNonTrackStatus,
361 /// A PUBLISH_STATE_NOTIFY arrived for something that is not a subscription,
362 /// or from the end of one that may not send it.
363 ///
364 /// Draft-20, one sentence covering both: "PUBLISH_STATE_NOTIFY applies only
365 /// to subscriptions, and is sent only by the publisher. An endpoint that
366 /// receives a PUBLISH_STATE_NOTIFY for any other request type, or from the
367 /// subscriber, MUST close the session with a PROTOCOL_VIOLATION."
368 StateNotifyOnTheWrongRequest,
369 /// A fill fetch stream opened against a request that asked for no fill.
370 ///
371 /// Draft-20, where `FILL_PARAMETERS` is the whole of the request: "Its
372 /// presence is what requests a fill fetch stream; a subscription with no
373 /// FILL_PARAMETERS opens none." Not a close — the draft states no
374 /// consequence, and the honest handling is `STOP_SENDING` on that stream
375 /// alone.
376 UnrequestedFillStream,
377 /// A SUBSCRIBE arrived for a namespace the peer had cancelled.
378 ///
379 /// Draft-07 alone: "it SHOULD close the session as a 'Protocol Violation'".
380 /// The mechanism it is stated about, ANNOUNCE_CANCEL against a namespace
381 /// the subscriber then subscribes under, survives the later drafts; the
382 /// sentence does not.
383 SubscribeAfterAnnounceCancel,
384 /// An UNSUBSCRIBE or REQUEST_UPDATE arrived naming a TRACK_STATUS.
385 ///
386 /// Drafts 13 through 18: a track status request is answered once and is
387 /// never a subscription, so there is nothing for either message to act on.
388 /// The drafts state it without a code, so `close` is `None`.
389 TrackStatusIsNotASubscription,
390 /// A message arrived naming a request this session has no record of.
391 ///
392 /// Stated by every draft of the messages that carry an id, and answered by
393 /// none of them with a code, so `close` is `None` throughout.
394 ///
395 /// This is narrower than it looks. Most of the variants that could carry it
396 /// are raised on both a receive path and a send path and answer
397 /// [`EndpointFault::EitherEnd`] instead; only the ones a draft raises on a
398 /// receive path alone reach this rule.
399 MessageNamesAnUnknownRequest,
400 /// The peer subscribed to a namespace prefix overlapping one it already has.
401 ///
402 /// Drafts 07 through 10, which catch it as the message arrives. From
403 /// draft-11 the same condition is caught where the answer is built, which
404 /// makes it a refusal this endpoint owes rather than a fault it observed —
405 /// see [`EndpointFault::ThisEndpoint`].
406 ///
407 /// Never a close on any draft: "it MUST respond with REQUEST_ERROR with
408 /// error code PREFIX_OVERLAP" is a reply, and a reply needs the request it
409 /// answers to have been taken.
410 NamespacePrefixOverlap,
411}
412
413/// One draft's own words for one rule: the sentence, the section it sits in,
414/// and what that draft calls the code it answers the rule with.
415///
416/// # A citation with no draft in it is a citation about no draft
417///
418/// [`AboveCodecRule`] names a rule across every draft that states it, and that
419/// is what makes one rule one row in a conformance report rather than rows
420/// that happen to rhyme. The *sentence* cannot be shared that way: one
421/// quoted sentence per rule, printed beside whichever draft was negotiated,
422/// publishes words the negotiated draft does not contain.
423///
424/// [`AboveCodecRule::DuplicateTrackAlias`] is the worked example. Draft-18
425/// states it in Section 11.1 and spells the code DUPLICATE_TRACK_ALIAS;
426/// draft-07 states it in Section 6.4, in different words, and spells the same
427/// code Duplicate Track Alias. Publishing draft-18's row against a draft-07
428/// session would be three wrong facts at once — sentence, section and name —
429/// each dressed as evidence, and the row would read as checked.
430///
431/// The close *code* is a separate field and is not flattened with them. It
432/// comes from each draft's own `EndpointError::session_error_code`, and those
433/// fourteen tables answer 0x4 on drafts 07 through 10 and 0x5 from draft-11 on.
434/// Draft-07 numbers 0x5 Parameter Length Mismatch, so a row that borrows a
435/// neighbour's *name* for its own number names a different error entirely. The
436/// sentence, the section and the name are the three things a reader uses to
437/// check the number, which is why each is stored per run.
438///
439/// # Runs, and not a row per draft
440///
441/// Twenty-eight rules with a citation per draft is several hundred rows, and
442/// most of them
443/// would be one sentence written out again. A row here covers a **run**: every
444/// draft over which one sentence sits under one section. The sentence itself is
445/// a named constant, so a wording shared by three runs — or by two different
446/// rules, which is what happens where one sentence states both the parity rule
447/// and the sequence rule — is written once and pointed at from each.
448///
449/// Seventy-eight rows over fifty-five sentences cover every rule this build can
450/// publish. The row count is a fact about the drafts rather than about the
451/// representation: runs break where draft-14 renamed Protocol Violation to
452/// PROTOCOL_VIOLATION, where draft-16 changed terminate to close, and — far
453/// more often than either — where a section number moved under a sentence that
454/// did not change at all. The Track Alias rule alone runs 6.4, 7.4, 8.6, 8.7,
455/// 8.8, 9.8, 9.10, 9.9, 11.1 across the drafts, and only four of those eight
456/// moves coincide with a change of wording.
457///
458/// # What checks it
459///
460/// `scripts/check-drafts.py` rule 8 reads this table out of this file and holds
461/// every row against the drafts it names: that the sentence is in every draft
462/// of the run, that it sits in the section the row gives, and that the code
463/// name the row carries is a name the sentence itself uses. It is the only rule
464/// in that script that reads a Rust value rather than a comment, and it is here
465/// because a citation the gate cannot see is not a checked citation. A
466/// catalogue of sentences kept outside this workspace is walked by no gate
467/// here, so its rows are read by no machine at all.
468///
469/// # What is deliberately not here
470///
471/// Whether the negotiated draft answers the rule with a session close.
472/// `session_error_code` answers that, per draft, quoting the sentence that
473/// names it, and a second copy of that answer here could disagree with it. So a
474/// row whose [`Self::code_name`] is `None` is a draft that states the rule and
475/// names no code for it — **not** a draft that states no consequence. Two rules
476/// here are in that position on some of their drafts and neither is a gap: the
477/// end-of-Track rule says the receiver MUST terminate the session without
478/// naming which code, and drafts 07 through 10 state the Subscribe ID
479/// uniqueness requirement with no consequence at all, which is why those
480/// drafts' `session_error_code` answers `None` for it and no row is ever
481/// published there.
482#[derive(Debug, Clone, Copy, PartialEq, Eq)]
483pub struct RuleCitation {
484 /// The first and last draft this citation is claimed for, inclusive.
485 pub drafts: (u8, u8),
486 /// The section every draft in [`Self::drafts`] files the sentence under.
487 ///
488 /// One section for the whole run, because the run is cut wherever the
489 /// number moves. That is why a rule can have more rows than it has
490 /// wordings.
491 pub section: &'static str,
492 /// The sentence, in those drafts' own words.
493 ///
494 /// Verbatim, including what a transcriber would want to correct: the
495 /// cross-references the renderings carry *inside* the sentence, draft-14's
496 /// SUBSCRIBE_UDPATE, the stray backtick draft-15 renders before
497 /// PROTOCOL_VIOLATION, and draft-11's missing full stop. Every one of those
498 /// is load-bearing — the gate compares against the rendering, so a sentence
499 /// tidied up is a sentence no draft has.
500 pub sentence: &'static str,
501 /// What these drafts call the session error code they answer the rule with,
502 /// where the sentence names one.
503 ///
504 /// The name and not the number. The number is
505 /// `EndpointError::session_error_code`'s answer and travels beside the rule
506 /// already; what a reader cannot get from the number is that draft-07 calls
507 /// 0x4 Duplicate Track Alias while draft-14 calls 0x5
508 /// DUPLICATE_TRACK_ALIAS.
509 pub code_name: Option<&'static str>,
510}
511
512impl RuleCitation {
513 /// Whether this citation is claimed for `draft`.
514 #[must_use]
515 pub fn covers(&self, draft: u8) -> bool {
516 self.drafts.0 <= draft && draft <= self.drafts.1
517 }
518}
519
520// The sentences, one constant per wording. A wording shared by several
521// runs -- or by two rules, which is what the Request ID sentence is -- is
522// named here once and pointed at from each row that carries it. That is the
523// claim `a_wording_shared_across_runs_is_written_once` holds.
524//
525// Transcribed from the renderings and not tidied. The cross-references the
526// drafts put inside these sentences are part of them, draft-14's
527// SUBSCRIBE_UDPATE is the drafts' typo, the backtick before draft-15's
528// PROTOCOL_VIOLATION is in the rendering, and draft-11's forwarding
529// preference sentence really does end without a full stop. `check-drafts.py`
530// rule 8 compares each of these against the rendering it came from, so a
531// correction of the drafts' spelling is a red gate.
532const EXTENSION_HEADERS_ON_STATUS: &str = "If an endpoint receives extension headers on Objects \
533 with status that is not Normal, it MUST close the \
534 session with a PROTOCOL_VIOLATION.";
535const PROPERTIES_ON_STATUS: &str = "If an endpoint receives properties on an Object with status \
536 that is not Normal, it MUST close the session with a \
537 PROTOCOL_VIOLATION.";
538const BIDI_OPENER_16: &str = "Bidirectional streams MUST NOT begin with any other message type \
539 unless negotiated. If they do, the peer MUST close the Session with \
540 a Protocol Violation.";
541const BIDI_OPENER_17: &str = "Bidirectional streams MUST NOT begin with any other message type \
542 unless negotiated. If they do, the peer MUST close the Session with \
543 a PROTOCOL_VIOLATION.";
544const FIRST_RESPONSE: &str = "If the subscriber receives any message other than a REQUEST_OK or a \
545 REQUEST_ERROR as the first message on the response half of the \
546 stream, then it MUST close the session with a PROTOCOL_VIOLATION.";
547const GOAWAY_AT_SERVER_07: &str =
548 "The server MUST terminate the session with a Protocol Violation \
549 (Section 3.5) if it receives a GOAWAY message.";
550const GOAWAY_URI_TERMINATE: &str = "If a server receives a GOAWAY with a non-zero New Session URI \
551 Length it MUST terminate the session with a Protocol \
552 Violation.";
553const GOAWAY_URI_TERMINATE_14: &str = "If a server receives a GOAWAY with a non-zero New Session \
554 URI Length it MUST terminate the session with a \
555 PROTOCOL_VIOLATION.";
556const GOAWAY_URI_CLOSE: &str = "If a server receives a GOAWAY with a non-zero New Session URI \
557 Length it MUST close the session with a PROTOCOL_VIOLATION.";
558const REPEATED_GOAWAY_07: &str = "The client MUST terminate the session with a Protocol Violation \
559 (Section 3.5) if it receives multiple GOAWAY messages.";
560const REPEATED_GOAWAY_08: &str = "The endpoint MUST terminate the session with a Protocol \
561 Violation (Section 3.5) if it receives multiple GOAWAY messages.";
562const REPEATED_GOAWAY_10: &str = "The endpoint MUST terminate the session with a Protocol \
563 Violation (Section 3.4) if it receives multiple GOAWAY messages.";
564const REPEATED_GOAWAY_14: &str = "The endpoint MUST terminate the session with a \
565 PROTOCOL_VIOLATION (Section 3.4) if it receives multiple GOAWAY \
566 messages.";
567const REPEATED_GOAWAY_16: &str = "The endpoint MUST close the session with a PROTOCOL_VIOLATION \
568 (Section 3.4) if it receives multiple GOAWAY messages.";
569const REPEATED_GOAWAY_17: &str = "The endpoint MUST close the session with a PROTOCOL_VIOLATION \
570 (Section 3.5) if it receives multiple GOAWAY messages.";
571const REPEATED_GOAWAY_18: &str = "The endpoint MUST close the session with a PROTOCOL_VIOLATION \
572 (Section 3.5) if it receives more than one GOAWAY on the control \
573 stream or on a single request stream.";
574const REPEATED_GOAWAY_21: &str = "The endpoint MUST close the session with a PROTOCOL_VIOLATION \
575 (Section 12.2) if it receives more than one GOAWAY on the \
576 control stream or on a single request stream.";
577const REDIRECT_URI_AT_SERVER: &str = "If a server receives a Redirect with a non-zero Connect URI \
578 Length it MUST close the session with a PROTOCOL_VIOLATION.";
579const REDIRECT_TRACK_NAME: &str = "an endpoint that receives a non-empty Track Name in a Redirect \
580 for a namespace-scoped request MUST close the session with a \
581 PROTOCOL_VIOLATION.";
582const REQUEST_ID_11: &str = "If an endpoint receives a Request ID that is not valid for the peer, \
583 or a new request with a Request ID that is not expected, it MUST \
584 close the session with Invalid Request ID.";
585const REQUEST_ID_14: &str = "If an endpoint receives a Request ID that is not valid for the peer, \
586 or a new request with a Request ID that is not expected, it MUST \
587 close the session with INVALID_REQUEST_ID.";
588const REQUEST_ID_15: &str = "If an endpoint receives a Request ID that is not valid for the peer, \
589 or a new request with a Request ID that is not the next in sequence \
590 or exceeds the received MAX_REQUEST_ID, it MUST close the session \
591 with INVALID_REQUEST_ID.";
592const REQUEST_ID_17: &str = "If an endpoint receives a Request ID where the least significant bit \
593 is incorrect for the sender, or a duplicate Request ID, it MUST close \
594 the session with INVALID_REQUEST_ID.";
595const SUBSCRIBE_ID_UNIQUE: &str = "Subscribe ID is a variable length integer that MUST be unique \
596 and monotonically increasing within a session and MUST be less \
597 than the session's Maximum Subscribe ID.";
598const CEILING_07: &str = "If a Subscribe ID equal or larger than this is received in any message, \
599 including SUBSCRIBE, the publisher MUST close the session with an error \
600 of 'Too Many Subscribes'.";
601const CEILING_08: &str = "If a Subscribe ID Section 7.4 equal or larger than this is received by \
602 the publisher that sent the MAX_SUBSCRIBE_ID, the publisher MUST close \
603 the session with an error of 'Too Many Subscribes'.";
604const CEILING_10: &str = "If a Subscribe ID Section 8.6 equal or larger than this is received by \
605 the publisher that sent the MAX_SUBSCRIBE_ID, the publisher MUST close \
606 the session with an error of 'Too Many Subscribes'.";
607const CEILING_11: &str = "If a Request ID equal or larger than this is received by the endpoint \
608 that sent the MAX_REQUEST_ID in any request message (ANNOUNCE, FETCH, \
609 SUBSCRIBE, SUBSCRIBE_ANNOUNCES or TRACK_STATUS_REQUEST), the endpoint \
610 MUST close the session with an error of 'Too Many Requests'.";
611const CEILING_12: &str =
612 "If a Request ID equal to or larger than this is received by the endpoint \
613 that sent the MAX_REQUEST_ID in any request message (ANNOUNCE, FETCH, \
614 SUBSCRIBE, SUBSCRIBE_ANNOUNCES or TRACK_STATUS_REQUEST), the endpoint \
615 MUST close the session with an error of 'Too Many Requests'.";
616const CEILING_13: &str = "If a Request ID equal or larger than this is received by the endpoint \
617 that sent the MAX_REQUEST_ID in any request message (ANNOUNCE, FETCH, \
618 SUBSCRIBE, SUBSCRIBE_NAMESPACE or TRACK_STATUS), the endpoint MUST close \
619 the session with an error of 'Too Many Requests'.";
620const CEILING_14: &str =
621 "If a Request ID equal to or larger than this is received by the endpoint \
622 that sent the MAX_REQUEST_ID in any request message (PUBLISH_NAMESPACE, \
623 FETCH, SUBSCRIBE, SUBSCRIBE_NAMESPACE, SUBSCRIBE_UDPATE or \
624 TRACK_STATUS), the endpoint MUST close the session with an error of \
625 TOO_MANY_REQUESTS.";
626const CEILING_16: &str =
627 "If a Request ID equal to or larger than this is received by the endpoint \
628 that sent the MAX_REQUEST_ID in any request message (PUBLISH_NAMESPACE, \
629 FETCH, SUBSCRIBE, SUBSCRIBE_NAMESPACE, REQUEST_UPDATE or TRACK_STATUS), \
630 the endpoint MUST close the session with an error of TOO_MANY_REQUESTS.";
631const MAX_ID_07: &str =
632 "The Maximum Subscribe Id MUST only increase within a session, and receipt \
633 of a MAX_SUBSCRIBE_ID message with an equal or smaller Subscribe ID value \
634 is a 'Protocol Violation'.";
635const MAX_ID_11: &str = "The Maximum Request ID MUST only increase within a session, and receipt \
636 of a MAX_REQUEST_ID message with an equal or smaller Request ID value is \
637 a 'Protocol Violation'.";
638const MAX_ID_14: &str = "The Maximum Request ID MUST only increase within a session, and receipt \
639 of a MAX_REQUEST_ID message with an equal or smaller Request ID value is \
640 a PROTOCOL_VIOLATION.";
641const MAX_ID_16: &str = "The Maximum Request ID MUST only increase within a session. If an \
642 endpoint receives MAX_REQUEST_ID message with an equal or smaller Request \
643 ID it MUST close the session with a PROTOCOL_VIOLATION.";
644const ALIAS_07: &str = "If the Track Alias is already being used for a different track, the \
645 publisher MUST close the session with a Duplicate Track Alias error \
646 (Section 3.5).";
647const ALIAS_10: &str = "If the Track Alias is already being used for a different track, the \
648 publisher MUST close the session with a Duplicate Track Alias error \
649 (Section 3.4).";
650const ALIAS_12: &str = "The same Track Alias MUST NOT be used to refer to two different Tracks \
651 simultaneously. If a subscriber receives a SUBSCRIBE_OK that uses the same \
652 Track Alias as a different track with an active subscription, it MUST \
653 close the session with error 'Duplicate Track Alias'.";
654const ALIAS_14: &str = "The same Track Alias MUST NOT be used to refer to two different Tracks \
655 simultaneously. If a subscriber receives a SUBSCRIBE_OK that uses the same \
656 Track Alias as a different track with an active subscription, it MUST \
657 close the session with error DUPLICATE_TRACK_ALIAS.";
658const ALIAS_15: &str = "The same Track Alias MUST NOT be used to refer to two different Tracks \
659 simultaneously. If a subscriber receives a SUBSCRIBE_OK that uses the same \
660 Track Alias as a different track with an Established subscription, it MUST \
661 close the session with error DUPLICATE_TRACK_ALIAS.";
662const ALIAS_17: &str = "The same Track Alias MUST NOT be used by a publisher to refer to two \
663 different Tracks simultaneously in the same session. If a subscriber \
664 receives a SUBSCRIBE_OK that uses the same Track Alias as a different \
665 track with an Established subscription, it MUST close the session with \
666 error DUPLICATE_TRACK_ALIAS.";
667const ALIAS_18: &str = "The same Track Alias MUST NOT be used by a publisher to refer to two \
668 different Tracks simultaneously in the same session. If a subscriber \
669 receives a PUBLISH or SUBSCRIBE_OK that uses the same Track Alias as a \
670 different Track with an Established subscription, it MUST close the \
671 session with error DUPLICATE_TRACK_ALIAS.";
672const MIXED_PREFERENCE_07: &str =
673 "Every Track has a single 'Object Forwarding Preference' and the \
674 Original Publisher MUST NOT mix different forwarding \
675 preferences within a single track. If a subscriber receives \
676 different forwarding preferences for a track, it SHOULD close \
677 the session with an error of 'Protocol Violation'.";
678const MIXED_PREFERENCE_11: &str =
679 "Every Track has a single 'Object Forwarding Preference' and the \
680 Original Publisher MUST NOT mix different forwarding \
681 preferences within a single track. If a subscriber receives \
682 Objects via both Subgroup streams and Datagrams in response to \
683 a SUBSCRIBE, it SHOULD close the session with an error of \
684 'Protocol Violation'";
685const END_OF_TRACK_08: &str = "An object with this status that has a Group ID less than any other \
686 Group ID, or an Object ID less than or equal to the largest in the \
687 group, is a protocol error, and the receiver MUST terminate the \
688 session.";
689const END_OF_TRACK_11: &str = "An object with this status that has a Group ID less than any other \
690 GroupID, or an ObjectID less than or equal to the largest in the \
691 specified group, is a protocol error, and the receiver MUST \
692 terminate the session.";
693const UPDATE_WRONG_12: &str = "A publisher MUST terminate the session with a 'Protocol Violation' \
694 if the SUBSCRIBE_UPDATE violates these rules or if the subscriber \
695 specifies a request ID that has not existed within the Session.";
696const UPDATE_WRONG_14: &str =
697 "A publisher MUST terminate the session with a PROTOCOL_VIOLATION if \
698 the SUBSCRIBE_UPDATE violates these rules or if the subscriber \
699 specifies a request ID that has not existed within the Session.";
700const UPDATE_WRONG_15: &str = "This MUST match an existing Request ID. The publisher MUST close \
701 the session with ` PROTOCOL_VIOLATION if the subscriber specifies \
702 an invalid Subscription Request ID.";
703const UPDATE_WRONG_16: &str =
704 "This MUST match the Request ID of an existing request. The receiver \
705 MUST close the session with PROTOCOL_VIOLATION if the sender \
706 specifies an invalid Existing Request ID, or if the parameters \
707 included in the REQUEST_UPDATE are invalid for the type of request \
708 being modified.";
709const UPDATE_WRONG_19: &str = "An endpoint that receives a REQUEST_UPDATE other than in the two \
710 cases above MUST close the session with a PROTOCOL_VIOLATION.";
711const TOO_MANY_UPDATES: &str = "If an endpoint receives a REQUEST_UPDATE on a stream that already \
712 has MAX_REQUEST_UPDATES outstanding REQUEST_UPDATEs, it MUST close \
713 the session with TOO_MANY_REQUEST_UPDATES.";
714const TRACK_PROPERTIES: &str = "Track Properties are populated in TRACK_STATUS_OK; they are empty \
715 in PUBLISH_OK, REQUEST_UPDATE_OK, SUBSCRIBE_NAMESPACE_OK and \
716 PUBLISH_NAMESPACE_OK. If an endpoint receives Track Properties in \
717 one of these messages it MUST close the session with a \
718 PROTOCOL_VIOLATION.";
719const STATE_NOTIFY: &str = "PUBLISH_STATE_NOTIFY applies only to subscriptions, and is sent only \
720 by the publisher. An endpoint that receives a PUBLISH_STATE_NOTIFY for \
721 any other request type, or from the subscriber, MUST close the session \
722 with a PROTOCOL_VIOLATION.";
723const SUBSCRIBE_AFTER_CANCEL: &str =
724 "If a publisher receives new subscriptions for that namespace \
725 after receiving an ANNOUNCE_CANCEL, it SHOULD close the \
726 session as a 'Protocol Violation'.";
727
728impl AboveCodecRule {
729 /// Every draft run this rule has a checked citation for, oldest first.
730 ///
731 /// Exhaustive with no wildcard arm, for the reason the enum is not
732 /// `#[non_exhaustive]`: a rule added to this file arrives here as an
733 /// `E0004` and a decision about which drafts state it, rather than as an
734 /// empty slice nobody chose.
735 ///
736 /// An empty slice is one of those decisions and not an oversight. Nine
737 /// rules answer with one, in two groups.
738 ///
739 /// **Eight are rules no draft in range answers with a session close.** The
740 /// datagram payload rule, a response naming another request, a setup
741 /// parameter whose value is not of its type's kind, an Object past the one
742 /// the track ended at, a fill stream nobody asked for, an UNSUBSCRIBE or
743 /// REQUEST_UPDATE naming a TRACK_STATUS, a message naming a request the
744 /// session has no record of, and a namespace prefix overlapping one the
745 /// peer already has. Every one is a real rule this endpoint enforces, and
746 /// `session_error_code` already answers `None` for all of them, so nothing
747 /// downstream could publish one whatever this table said. They are written
748 /// out so that a draft attaching a consequence to one arrives here as a
749 /// build failure.
750 ///
751 /// **The ninth is [`Self::MessageOnTheWrongStream`].** It is enforced on
752 /// drafts 16 through 20 and no draft in that range states it, so it is
753 /// named, enforced and attributed to the peer while publishing nothing:
754 /// there is no sentence to publish and `close` is `None` on every draft.
755 /// The variant's own doc carries the reading across all texts that
756 /// establishes it, and the one variant that really is covered by a draft
757 /// sentence is filed under the rule whose sentence covers it. A draft that
758 /// attaches a consequence to this one arrives as a citation rather than as
759 /// a rediscovery.
760 #[must_use]
761 pub fn citations(self) -> &'static [RuleCitation] {
762 match self {
763 Self::PropertiesOnNonNormalStatus => &[
764 RuleCitation {
765 drafts: (15, 16),
766 section: "10.2.1.2",
767 sentence: EXTENSION_HEADERS_ON_STATUS,
768 code_name: Some("PROTOCOL_VIOLATION"),
769 },
770 RuleCitation {
771 drafts: (17, 17),
772 section: "10.2.1.2",
773 sentence: PROPERTIES_ON_STATUS,
774 code_name: Some("PROTOCOL_VIOLATION"),
775 },
776 RuleCitation {
777 drafts: (18, 20),
778 section: "11.2.1.2",
779 sentence: PROPERTIES_ON_STATUS,
780 code_name: Some("PROTOCOL_VIOLATION"),
781 },
782 RuleCitation {
783 drafts: (21, 21),
784 section: "11.1.3",
785 sentence: PROPERTIES_ON_STATUS,
786 code_name: Some("PROTOCOL_VIOLATION"),
787 },
788 ],
789 Self::BidiStreamOpener => &[
790 RuleCitation {
791 drafts: (16, 16),
792 section: "3.3",
793 sentence: BIDI_OPENER_16,
794 code_name: Some("Protocol Violation"),
795 },
796 RuleCitation {
797 drafts: (17, 20),
798 section: "3.3",
799 sentence: BIDI_OPENER_17,
800 code_name: Some("PROTOCOL_VIOLATION"),
801 },
802 RuleCitation {
803 drafts: (21, 21),
804 section: "6.3",
805 sentence: BIDI_OPENER_17,
806 code_name: Some("PROTOCOL_VIOLATION"),
807 },
808 ],
809 Self::ResponseBeforeItsFirstResponse => &[
810 RuleCitation {
811 drafts: (18, 19),
812 section: "10.18",
813 sentence: FIRST_RESPONSE,
814 code_name: Some("PROTOCOL_VIOLATION"),
815 },
816 RuleCitation {
817 drafts: (20, 20),
818 section: "10.19",
819 sentence: FIRST_RESPONSE,
820 code_name: Some("PROTOCOL_VIOLATION"),
821 },
822 RuleCitation {
823 drafts: (21, 21),
824 section: "9.15",
825 sentence: FIRST_RESPONSE,
826 code_name: Some("PROTOCOL_VIOLATION"),
827 },
828 ],
829 Self::GoAwayAtServer => &[
830 RuleCitation {
831 drafts: (7, 7),
832 section: "6.3",
833 sentence: GOAWAY_AT_SERVER_07,
834 code_name: Some("Protocol Violation"),
835 },
836 RuleCitation {
837 drafts: (8, 9),
838 section: "7.3",
839 sentence: GOAWAY_URI_TERMINATE,
840 code_name: Some("Protocol Violation"),
841 },
842 RuleCitation {
843 drafts: (10, 10),
844 section: "8.3",
845 sentence: GOAWAY_URI_TERMINATE,
846 code_name: Some("Protocol Violation"),
847 },
848 RuleCitation {
849 drafts: (11, 13),
850 section: "8.4",
851 sentence: GOAWAY_URI_TERMINATE,
852 code_name: Some("Protocol Violation"),
853 },
854 RuleCitation {
855 drafts: (14, 15),
856 section: "9.4",
857 sentence: GOAWAY_URI_TERMINATE_14,
858 code_name: Some("PROTOCOL_VIOLATION"),
859 },
860 RuleCitation {
861 drafts: (16, 16),
862 section: "9.4",
863 sentence: GOAWAY_URI_CLOSE,
864 code_name: Some("PROTOCOL_VIOLATION"),
865 },
866 RuleCitation {
867 drafts: (17, 17),
868 section: "9.5",
869 sentence: GOAWAY_URI_CLOSE,
870 code_name: Some("PROTOCOL_VIOLATION"),
871 },
872 RuleCitation {
873 drafts: (18, 20),
874 section: "10.4",
875 sentence: GOAWAY_URI_CLOSE,
876 code_name: Some("PROTOCOL_VIOLATION"),
877 },
878 RuleCitation {
879 drafts: (21, 21),
880 section: "9.2",
881 sentence: GOAWAY_URI_CLOSE,
882 code_name: Some("PROTOCOL_VIOLATION"),
883 },
884 ],
885 Self::RepeatedGoAway => &[
886 RuleCitation {
887 drafts: (7, 7),
888 section: "6.3",
889 sentence: REPEATED_GOAWAY_07,
890 code_name: Some("Protocol Violation"),
891 },
892 RuleCitation {
893 drafts: (8, 9),
894 section: "7.3",
895 sentence: REPEATED_GOAWAY_08,
896 code_name: Some("Protocol Violation"),
897 },
898 RuleCitation {
899 drafts: (10, 10),
900 section: "8.3",
901 sentence: REPEATED_GOAWAY_10,
902 code_name: Some("Protocol Violation"),
903 },
904 RuleCitation {
905 drafts: (11, 13),
906 section: "8.4",
907 sentence: REPEATED_GOAWAY_10,
908 code_name: Some("Protocol Violation"),
909 },
910 RuleCitation {
911 drafts: (14, 15),
912 section: "9.4",
913 sentence: REPEATED_GOAWAY_14,
914 code_name: Some("PROTOCOL_VIOLATION"),
915 },
916 RuleCitation {
917 drafts: (16, 16),
918 section: "9.4",
919 sentence: REPEATED_GOAWAY_16,
920 code_name: Some("PROTOCOL_VIOLATION"),
921 },
922 RuleCitation {
923 drafts: (17, 17),
924 section: "9.5",
925 sentence: REPEATED_GOAWAY_17,
926 code_name: Some("PROTOCOL_VIOLATION"),
927 },
928 RuleCitation {
929 drafts: (18, 20),
930 section: "10.4",
931 sentence: REPEATED_GOAWAY_18,
932 code_name: Some("PROTOCOL_VIOLATION"),
933 },
934 RuleCitation {
935 drafts: (21, 21),
936 section: "9.2",
937 sentence: REPEATED_GOAWAY_21,
938 code_name: Some("PROTOCOL_VIOLATION"),
939 },
940 ],
941 Self::RedirectUriAtServer => &[
942 RuleCitation {
943 drafts: (18, 20),
944 section: "10.6.1",
945 sentence: REDIRECT_URI_AT_SERVER,
946 code_name: Some("PROTOCOL_VIOLATION"),
947 },
948 RuleCitation {
949 drafts: (21, 21),
950 section: "9.4.1",
951 sentence: REDIRECT_URI_AT_SERVER,
952 code_name: Some("PROTOCOL_VIOLATION"),
953 },
954 ],
955 Self::RedirectTrackNameOnNamespaceRequest => &[
956 RuleCitation {
957 drafts: (18, 20),
958 section: "10.6.1",
959 sentence: REDIRECT_TRACK_NAME,
960 code_name: Some("PROTOCOL_VIOLATION"),
961 },
962 RuleCitation {
963 drafts: (21, 21),
964 section: "9.4.1",
965 sentence: REDIRECT_TRACK_NAME,
966 code_name: Some("PROTOCOL_VIOLATION"),
967 },
968 ],
969 Self::RequestIdParity => &[
970 RuleCitation {
971 drafts: (11, 13),
972 section: "8.1",
973 sentence: REQUEST_ID_11,
974 code_name: Some("Invalid Request ID"),
975 },
976 RuleCitation {
977 drafts: (14, 14),
978 section: "9.1",
979 sentence: REQUEST_ID_14,
980 code_name: Some("INVALID_REQUEST_ID"),
981 },
982 RuleCitation {
983 drafts: (15, 16),
984 section: "9.1",
985 sentence: REQUEST_ID_15,
986 code_name: Some("INVALID_REQUEST_ID"),
987 },
988 RuleCitation {
989 drafts: (17, 17),
990 section: "9.1",
991 sentence: REQUEST_ID_17,
992 code_name: Some("INVALID_REQUEST_ID"),
993 },
994 RuleCitation {
995 drafts: (18, 20),
996 section: "10.1",
997 sentence: REQUEST_ID_17,
998 code_name: Some("INVALID_REQUEST_ID"),
999 },
1000 RuleCitation {
1001 drafts: (21, 21),
1002 section: "6.4.2.1",
1003 sentence: REQUEST_ID_17,
1004 code_name: Some("INVALID_REQUEST_ID"),
1005 },
1006 ],
1007 Self::RequestIdOutOfSequence => &[
1008 RuleCitation {
1009 drafts: (7, 7),
1010 section: "6.4",
1011 sentence: SUBSCRIBE_ID_UNIQUE,
1012 code_name: None,
1013 },
1014 RuleCitation {
1015 drafts: (8, 9),
1016 section: "7.4",
1017 sentence: SUBSCRIBE_ID_UNIQUE,
1018 code_name: None,
1019 },
1020 RuleCitation {
1021 drafts: (10, 10),
1022 section: "8.6",
1023 sentence: SUBSCRIBE_ID_UNIQUE,
1024 code_name: None,
1025 },
1026 RuleCitation {
1027 drafts: (11, 13),
1028 section: "8.1",
1029 sentence: REQUEST_ID_11,
1030 code_name: Some("Invalid Request ID"),
1031 },
1032 RuleCitation {
1033 drafts: (14, 14),
1034 section: "9.1",
1035 sentence: REQUEST_ID_14,
1036 code_name: Some("INVALID_REQUEST_ID"),
1037 },
1038 RuleCitation {
1039 drafts: (15, 16),
1040 section: "9.1",
1041 sentence: REQUEST_ID_15,
1042 code_name: Some("INVALID_REQUEST_ID"),
1043 },
1044 RuleCitation {
1045 drafts: (17, 17),
1046 section: "9.1",
1047 sentence: REQUEST_ID_17,
1048 code_name: Some("INVALID_REQUEST_ID"),
1049 },
1050 RuleCitation {
1051 drafts: (18, 20),
1052 section: "10.1",
1053 sentence: REQUEST_ID_17,
1054 code_name: Some("INVALID_REQUEST_ID"),
1055 },
1056 RuleCitation {
1057 drafts: (21, 21),
1058 section: "6.4.2.1",
1059 sentence: REQUEST_ID_17,
1060 code_name: Some("INVALID_REQUEST_ID"),
1061 },
1062 ],
1063 Self::RequestIdCeiling => &[
1064 RuleCitation {
1065 drafts: (7, 7),
1066 section: "6.20",
1067 sentence: CEILING_07,
1068 code_name: Some("Too Many Subscribes"),
1069 },
1070 RuleCitation {
1071 drafts: (8, 9),
1072 section: "7.20",
1073 sentence: CEILING_08,
1074 code_name: Some("Too Many Subscribes"),
1075 },
1076 RuleCitation {
1077 drafts: (10, 10),
1078 section: "8.4",
1079 sentence: CEILING_10,
1080 code_name: Some("Too Many Subscribes"),
1081 },
1082 RuleCitation {
1083 drafts: (11, 11),
1084 section: "8.5",
1085 sentence: CEILING_11,
1086 code_name: Some("Too Many Requests"),
1087 },
1088 RuleCitation {
1089 drafts: (12, 12),
1090 section: "8.5",
1091 sentence: CEILING_12,
1092 code_name: Some("Too Many Requests"),
1093 },
1094 RuleCitation {
1095 drafts: (13, 13),
1096 section: "8.5",
1097 sentence: CEILING_13,
1098 code_name: Some("Too Many Requests"),
1099 },
1100 RuleCitation {
1101 drafts: (14, 15),
1102 section: "9.5",
1103 sentence: CEILING_14,
1104 code_name: Some("TOO_MANY_REQUESTS"),
1105 },
1106 RuleCitation {
1107 drafts: (16, 16),
1108 section: "9.5",
1109 sentence: CEILING_16,
1110 code_name: Some("TOO_MANY_REQUESTS"),
1111 },
1112 ],
1113 Self::MaxRequestIdDecreased => &[
1114 RuleCitation {
1115 drafts: (7, 7),
1116 section: "6.20",
1117 sentence: MAX_ID_07,
1118 code_name: Some("Protocol Violation"),
1119 },
1120 RuleCitation {
1121 drafts: (8, 9),
1122 section: "7.20",
1123 sentence: MAX_ID_07,
1124 code_name: Some("Protocol Violation"),
1125 },
1126 RuleCitation {
1127 drafts: (10, 10),
1128 section: "8.4",
1129 sentence: MAX_ID_07,
1130 code_name: Some("Protocol Violation"),
1131 },
1132 RuleCitation {
1133 drafts: (11, 13),
1134 section: "8.5",
1135 sentence: MAX_ID_11,
1136 code_name: Some("Protocol Violation"),
1137 },
1138 RuleCitation {
1139 drafts: (14, 15),
1140 section: "9.5",
1141 sentence: MAX_ID_14,
1142 code_name: Some("PROTOCOL_VIOLATION"),
1143 },
1144 RuleCitation {
1145 drafts: (16, 16),
1146 section: "9.5",
1147 sentence: MAX_ID_16,
1148 code_name: Some("PROTOCOL_VIOLATION"),
1149 },
1150 ],
1151 Self::DuplicateTrackAlias => &[
1152 RuleCitation {
1153 drafts: (7, 7),
1154 section: "6.4",
1155 sentence: ALIAS_07,
1156 code_name: Some("Duplicate Track Alias"),
1157 },
1158 RuleCitation {
1159 drafts: (8, 9),
1160 section: "7.4",
1161 sentence: ALIAS_07,
1162 code_name: Some("Duplicate Track Alias"),
1163 },
1164 RuleCitation {
1165 drafts: (10, 10),
1166 section: "8.6",
1167 sentence: ALIAS_10,
1168 code_name: Some("Duplicate Track Alias"),
1169 },
1170 RuleCitation {
1171 drafts: (11, 11),
1172 section: "8.7",
1173 sentence: ALIAS_10,
1174 code_name: Some("Duplicate Track Alias"),
1175 },
1176 RuleCitation {
1177 drafts: (12, 13),
1178 section: "8.8",
1179 sentence: ALIAS_12,
1180 code_name: Some("Duplicate Track Alias"),
1181 },
1182 RuleCitation {
1183 drafts: (14, 14),
1184 section: "9.8",
1185 sentence: ALIAS_14,
1186 code_name: Some("DUPLICATE_TRACK_ALIAS"),
1187 },
1188 RuleCitation {
1189 drafts: (15, 16),
1190 section: "9.10",
1191 sentence: ALIAS_15,
1192 code_name: Some("DUPLICATE_TRACK_ALIAS"),
1193 },
1194 RuleCitation {
1195 drafts: (17, 17),
1196 section: "9.9",
1197 sentence: ALIAS_17,
1198 code_name: Some("DUPLICATE_TRACK_ALIAS"),
1199 },
1200 RuleCitation {
1201 drafts: (18, 20),
1202 section: "11.1",
1203 sentence: ALIAS_18,
1204 code_name: Some("DUPLICATE_TRACK_ALIAS"),
1205 },
1206 RuleCitation {
1207 drafts: (21, 21),
1208 section: "3.1.2",
1209 sentence: ALIAS_18,
1210 code_name: Some("DUPLICATE_TRACK_ALIAS"),
1211 },
1212 ],
1213 Self::MixedForwardingPreference => &[
1214 RuleCitation {
1215 drafts: (7, 7),
1216 section: "7",
1217 sentence: MIXED_PREFERENCE_07,
1218 code_name: Some("Protocol Violation"),
1219 },
1220 RuleCitation {
1221 drafts: (8, 9),
1222 section: "8",
1223 sentence: MIXED_PREFERENCE_07,
1224 code_name: Some("Protocol Violation"),
1225 },
1226 RuleCitation {
1227 drafts: (10, 10),
1228 section: "9",
1229 sentence: MIXED_PREFERENCE_07,
1230 code_name: Some("Protocol Violation"),
1231 },
1232 RuleCitation {
1233 drafts: (11, 11),
1234 section: "9",
1235 sentence: MIXED_PREFERENCE_11,
1236 code_name: Some("Protocol Violation"),
1237 },
1238 ],
1239 Self::EndOfTrackOutOfPlace => &[
1240 RuleCitation {
1241 drafts: (8, 9),
1242 section: "8.1.1.1",
1243 sentence: END_OF_TRACK_08,
1244 code_name: None,
1245 },
1246 RuleCitation {
1247 drafts: (10, 10),
1248 section: "9.1.1.1",
1249 sentence: END_OF_TRACK_08,
1250 code_name: None,
1251 },
1252 RuleCitation {
1253 drafts: (11, 11),
1254 section: "9.1.1.1",
1255 sentence: END_OF_TRACK_11,
1256 code_name: None,
1257 },
1258 RuleCitation {
1259 drafts: (12, 13),
1260 section: "9.2.1.1",
1261 sentence: END_OF_TRACK_11,
1262 code_name: None,
1263 },
1264 ],
1265 Self::RequestUpdateForTheWrongRequest => &[
1266 RuleCitation {
1267 drafts: (12, 13),
1268 section: "8.10",
1269 sentence: UPDATE_WRONG_12,
1270 code_name: Some("Protocol Violation"),
1271 },
1272 RuleCitation {
1273 drafts: (14, 14),
1274 section: "9.10",
1275 sentence: UPDATE_WRONG_14,
1276 code_name: Some("PROTOCOL_VIOLATION"),
1277 },
1278 RuleCitation {
1279 drafts: (15, 15),
1280 section: "9.11",
1281 sentence: UPDATE_WRONG_15,
1282 code_name: Some("PROTOCOL_VIOLATION"),
1283 },
1284 RuleCitation {
1285 drafts: (16, 16),
1286 section: "9.11",
1287 sentence: UPDATE_WRONG_16,
1288 code_name: Some("PROTOCOL_VIOLATION"),
1289 },
1290 RuleCitation {
1291 drafts: (19, 20),
1292 section: "10.9",
1293 sentence: UPDATE_WRONG_19,
1294 code_name: Some("PROTOCOL_VIOLATION"),
1295 },
1296 RuleCitation {
1297 drafts: (21, 21),
1298 section: "9.5",
1299 sentence: UPDATE_WRONG_19,
1300 code_name: Some("PROTOCOL_VIOLATION"),
1301 },
1302 ],
1303 Self::TooManyRequestUpdates => &[
1304 RuleCitation {
1305 drafts: (19, 20),
1306 section: "10.3.1.7",
1307 sentence: TOO_MANY_UPDATES,
1308 code_name: Some("TOO_MANY_REQUEST_UPDATES"),
1309 },
1310 RuleCitation {
1311 drafts: (21, 21),
1312 section: "9.1.7",
1313 sentence: TOO_MANY_UPDATES,
1314 code_name: Some("TOO_MANY_REQUEST_UPDATES"),
1315 },
1316 ],
1317 Self::TrackPropertiesOnNonTrackStatus => &[
1318 RuleCitation {
1319 drafts: (18, 20),
1320 section: "10.5",
1321 sentence: TRACK_PROPERTIES,
1322 code_name: Some("PROTOCOL_VIOLATION"),
1323 },
1324 RuleCitation {
1325 drafts: (21, 21),
1326 section: "9.3",
1327 sentence: TRACK_PROPERTIES,
1328 code_name: Some("PROTOCOL_VIOLATION"),
1329 },
1330 ],
1331 Self::StateNotifyOnTheWrongRequest => &[
1332 RuleCitation {
1333 drafts: (20, 20),
1334 section: "10.10",
1335 sentence: STATE_NOTIFY,
1336 code_name: Some("PROTOCOL_VIOLATION"),
1337 },
1338 RuleCitation {
1339 drafts: (21, 21),
1340 section: "9.10",
1341 sentence: STATE_NOTIFY,
1342 code_name: Some("PROTOCOL_VIOLATION"),
1343 },
1344 ],
1345 Self::SubscribeAfterAnnounceCancel => &[RuleCitation {
1346 drafts: (7, 7),
1347 section: "6.11",
1348 sentence: SUBSCRIBE_AFTER_CANCEL,
1349 code_name: Some("Protocol Violation"),
1350 }],
1351 // The eight with no consequence on any draft, and the one with no
1352 // sentence on any draft. See this function's own doc for which is
1353 // which and why they are written out rather than swallowed by a
1354 // wildcard.
1355 Self::PayloadOnStatusDatagram
1356 | Self::MessageOnTheWrongStream
1357 | Self::ResponseNamesAnotherRequest
1358 | Self::SetupParameterValue
1359 | Self::ObjectPastFinalObject
1360 | Self::UnrequestedFillStream
1361 | Self::TrackStatusIsNotASubscription
1362 | Self::MessageNamesAnUnknownRequest
1363 | Self::NamespacePrefixOverlap => &[],
1364 }
1365 }
1366
1367 /// This rule as `draft` states it, or `None` where that draft does not.
1368 ///
1369 /// `None` has two readings and the caller does not need to tell them apart:
1370 /// the draft may not state the rule at all, or it may state it in words
1371 /// nobody has yet checked. Both mean the same thing downstream — there is
1372 /// no sentence to publish, so there is no accusation to make — and that is
1373 /// the whole of why this returns an `Option` rather than falling back on a
1374 /// neighbouring draft's wording. A fallback publishes a sentence the
1375 /// negotiated draft does not contain, which is the defect this table exists
1376 /// to prevent.
1377 #[must_use]
1378 pub fn citation(self, draft: u8) -> Option<&'static RuleCitation> {
1379 self.citations().iter().find(|c| c.covers(draft))
1380 }
1381}
1382
1383/// A rule a peer broke that the negotiated draft's **decoder** did refuse,
1384/// named once across the drafts that state it.
1385///
1386/// # Why this is a second enum and not more variants of the first
1387///
1388/// [`AboveCodecRule`] is defined by what it is *not*: a rule stated about a
1389/// frame that reads perfectly well, which no decoder could refuse and which the
1390/// endpoint has to catch by comparing decoded fields against state it has been
1391/// keeping. Every rule here is the opposite. The frame did not read — a length
1392/// past its maximum, a discriminator the draft does not assign, a delta that
1393/// carries a number past the end of the varint space — and `CodecError` is what
1394/// says so. Folding the two into one enum would put *the decoder refused this*
1395/// and *the decoder could not have seen this* under one name, which is the
1396/// distinction [`crate::dispatch::ErrorCause`] exists to keep.
1397///
1398/// # Why it lives in this file rather than beside `CodecError`
1399///
1400/// Because of what reads it. `scripts/check-drafts.py` rule 8 walks **this
1401/// file** and holds every [`RuleCitation`] in it against the fourteen rendered
1402/// drafts: that the sentence is in every draft of the run, that it sits in the
1403/// section the row names, and that the code name is one the sentence itself
1404/// uses. A citation the gate cannot see is not a checked citation, and the
1405/// standard for adding one is deliberately steep: a sentence held against the
1406/// draft that will be cited, on each draft in the range claimed rather than on
1407/// a sample of it.
1408///
1409/// `moqtap-codec` is the other candidate and is the wrong one twice over: it is
1410/// where the *errors* live rather than where the drafts' answers to them do,
1411/// and nothing in `just drafts` reads a table there either.
1412///
1413/// # Where the quoted sentences are, and are not
1414///
1415/// In the constants below, one per wording, which is the only place the gate
1416/// looks. The variant docs here describe rather than quote, and use backticks
1417/// where they name a fragment of a draft's text. That is a departure from
1418/// [`AboveCodecRule`] above, and the reason is this group's ranges: nearly
1419/// every rule here says something different on the oldest drafts in its range
1420/// than on the newest, so a doc that quoted one wording beside a sentence
1421/// naming ten drafts would be claiming it for all ten. The checker has a rule
1422/// for exactly that and it would be right to fire.
1423///
1424/// # What is in this table
1425///
1426/// These are the rules a consumer's catalogue names as a list and does not
1427/// carry itself: a `rule_for` matching `CodecError` exhaustively has no
1428/// wildcard arm to file them under. Every one of them is a relay breaking a
1429/// rule the negotiated draft answers with a session close, and without a
1430/// citation here there is nothing to publish it as.
1431///
1432/// # Why the two Type-value rules are two
1433///
1434/// [`CodecRule::InvalidStreamTypeValue`] and
1435/// [`CodecRule::InvalidDatagramTypeValue`] are one condition as a reader thinks
1436/// of it — a Type inside its form holding a combination the draft rules out —
1437/// and two rules here, because drafts 16 through 21 state them as **two**
1438/// sentences in two sections, one about a datagram's Type and one about a
1439/// subgroup stream header's.
1440///
1441/// A [`RuleCitation`] is one sentence per draft: [`Self::citation`] takes the
1442/// first run that covers a draft and
1443/// `every_run_is_ordered_contiguous_and_in_range` forbids two runs claiming
1444/// one. So a single rule spanning both sentences could publish only one of
1445/// them, and would be right on half the frames it published and wrong on the
1446/// other half with nothing in the row to say which.
1447///
1448/// Splitting the rule here is only half of it, and the half that does not work
1449/// alone: the decoder has to hand over a value that says which sentence was
1450/// broken. `CodecError::InvalidStreamTypeValue` and
1451/// `CodecError::InvalidDatagramTypeValue` are that value. Deriving the
1452/// namespace from the raw number instead would be this build's reading rather
1453/// than the decoder's, because the two Type spaces overlap — which is the
1454/// reasoning `CodecError::InvalidField` already gets, arrived at from a
1455/// different direction.
1456///
1457/// # Two drafts where a close exists and no citation does
1458///
1459/// This table is not the inverse of `codec_session_error_code`, and two rows
1460/// are where the two come apart. Draft-20 answers both
1461/// [`Self::InvalidFilterType`] and [`Self::InvalidFetchType`] with
1462/// PROTOCOL_VIOLATION, and draft-20 states neither rule: it replaced the
1463/// subscription filter with a LOCATION_FILTER parameter whose optional fields
1464/// are found by length and carry no type discriminator, and it deleted the
1465/// Fetch Type field, both variant structures and the registry together.
1466///
1467/// Neither arm is reachable there. `moqtap-codec`'s draft-20 message reader
1468/// raises neither error and has nothing to raise it from, so the two closes are
1469/// inert rather than wrong, and both are kept for the reason draft-20's own
1470/// connection already gives: `CodecError` is shared across drafts and
1471/// is not `#[non_exhaustive]`, so every variant has to be answered on every
1472/// draft.
1473///
1474/// What the missing citation buys is that the inertness stops being
1475/// load-bearing. If a draft-20 decoder ever did raise one, a consumer would
1476/// find no sentence to publish and would name nobody — which is the right
1477/// answer for a rule draft-20 does not state, and it does not depend on an
1478/// unreachable arm staying unreachable.
1479///
1480/// Deliberately **not** `#[non_exhaustive]`, for the reason [`AboveCodecRule`]
1481/// is not: a consumer deciding what it will name a relay for should find out
1482/// about a new rule by failing to build.
1483#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1484pub enum CodecRule {
1485 // ── The four maxima ────────────────────────────────────────
1486 /// A Full Track Name longer than the drafts' maximum.
1487 ///
1488 /// Drafts 11 through 20, Section 2.4.1 throughout — the section never moves
1489 /// and the sentence changes three times, which is the opposite of the usual
1490 /// shape and why this rule has four runs where most have one wording per
1491 /// section.
1492 ///
1493 /// The maximum and its consequence are quoted together because the
1494 /// consequence alone is not evidence: through draft-15 it refers back to
1495 /// `exceeding this length`, and the length is in the sentence before it.
1496 ///
1497 /// One nuance the citation carries and this build's error message does not.
1498 /// On drafts 11 through 15 the sentence bounds the **Full Track Name**
1499 /// alone; draft-16 widens it to a Track Namespace or a Full Track Name.
1500 /// `CodecError::TrackNameTooLong` renders as *track namespace or full track
1501 /// name exceeds …* on all ten, which is the drafts' own scope only from 16
1502 /// on. A namespace over the maximum beside a short name is covered by no
1503 /// sentence on the older five, and their rows quote what those drafts do
1504 /// say rather than what this build measures.
1505 ///
1506 /// Drafts 07 through 10 state no maximum at all.
1507 TrackNameTooLong,
1508 /// A Reason Phrase longer than the drafts' maximum.
1509 ///
1510 /// Drafts 11 through 20. **The sentence has no full stop in any of them**:
1511 /// it runs straight into the next definition item, so every quotation here
1512 /// ends at the code name. Supplying the full stop a transcriber would want
1513 /// is exactly the truncation `check-drafts.py` rule 7 exists to catch, and
1514 /// it would file the row under a sentence no draft has.
1515 ///
1516 /// Four runs. The section moves twice — draft-14 inserts Section 1.3 Stream
1517 /// Management Terms and draft-17 inserts Section 1.4.1 Variable-Length
1518 /// Integers, pushing this down each time — and the wording changes twice,
1519 /// once for the code rename and once where the maximum stops being a
1520 /// `maximum length of 1024 bytes` and becomes a `maximum value of 1024
1521 /// bytes`.
1522 ReasonPhraseTooLong,
1523 /// A GOAWAY New Session URI longer than the drafts' maximum.
1524 ///
1525 /// Drafts 11 through 20, in the GOAWAY message's own section, which moves
1526 /// three times across the range.
1527 ///
1528 /// Drafts 11 through 16 spell it `The maxmimum length of the New Session
1529 /// URI is 8,192 bytes.` The typo is the drafts' and is reproduced in the
1530 /// constant, because the gate compares against the rendering: a sentence
1531 /// tidied up is a sentence no draft has. Draft-17 fixes it, which is one of
1532 /// the two wording breaks in this rule's four runs.
1533 GoAwayUriTooLong,
1534 /// A Key-Value-Pair whose value is longer than the drafts' maximum.
1535 ///
1536 /// Drafts 11 through 20, in the Key-Value-Pair structure's own section —
1537 /// 1.3.2 through draft-13, 1.4.2 on 14 through 16, 1.4.3 from draft-17,
1538 /// which is the same pair of insertions that moves the Reason Phrase rule.
1539 ///
1540 /// The sub-variant and not the arm. `CodecError::Kvp` also carries
1541 /// `MissingLength`, `UnexpectedEnd` and `VarInt`, which report how the bytes
1542 /// ran out rather than a rule an endpoint states, and all the drafts
1543 /// answer all three with no close. Only `ValueTooLong` reaches this rule,
1544 /// which is why the probe that consumes it matches the sub-variant.
1545 KvpValueTooLong,
1546
1547 // ── The key-value pair rules ───────────────────────────────
1548 /// A key-value pair whose value is not the serialization its own Type
1549 /// defines.
1550 ///
1551 /// Drafts 11 through 20, and the one rule in this table whose code is
1552 /// KEY_VALUE_FORMATTING_ERROR on every draft that states it rather than
1553 /// PROTOCOL_VIOLATION.
1554 ///
1555 /// Four runs for three reasons at once. Drafts 11 through 13 spell the code
1556 /// in prose inside quotation marks and drafts 14 and later in capitals;
1557 /// draft-16 changes the verb from terminate to close; and the section moves
1558 /// twice under both. Only the first of those three is visible in the code
1559 /// name a row publishes, which is the argument for publishing the sentence
1560 /// beside it.
1561 KeyValueFormatting,
1562 /// A control message carrying a Message Parameter whose type its draft does
1563 /// not define.
1564 ///
1565 /// Drafts 16 through 20, and **the rule whose answer reverses** rather than
1566 /// arrives. Drafts 07 through 10 say a receiver ignores an unrecognized
1567 /// parameter, and drafts 11 through 15 add `Receivers MUST allow duplicates
1568 /// of unknown parameters`, which presumes unknown parameters arrive and are
1569 /// carried. Raising this on any of those nine would close a session over an
1570 /// extension those drafts leave room for, so the decoder does not, and the
1571 /// run starts where the sentence does.
1572 ///
1573 /// Draft-16 is a run of its own for one word: it has the parameter
1574 /// negotiated via `Setup Parameters` where drafts 17 and later say `Setup
1575 /// Options`, which is the rename draft-17 made. Publishing the later
1576 /// wording for draft-16 would be quoting a sentence draft-16 has not got —
1577 /// the exact defect this table was built to remove, one draft wide.
1578 ///
1579 /// One namespace only. Every draft in range says a receiver ignores an
1580 /// unrecognised Setup Option, so an unknown type in a SETUP is carried and
1581 /// this is never raised for one.
1582 UnknownMessageParameter,
1583 /// A Message Parameter appearing in a message type its own definition does
1584 /// not name.
1585 ///
1586 /// Drafts 17 through 20. The other reversal, and a wider one: drafts 07
1587 /// through 16 end the same sentence `it MUST be ignored`.
1588 ///
1589 /// The sentence says close the **connection**, not close the session, on
1590 /// all four drafts. Reproduced as it stands. The drafts use the two words
1591 /// interchangeably here, and correcting one to the other would be this file
1592 /// speaking inside a draft's own quotation marks — which is a defect this
1593 /// table has already been found holding five times.
1594 ParameterOutOfScope,
1595 /// A parameter whose value is not the shape its own type implies.
1596 ///
1597 /// **Drafts 07 through 10 alone**, which makes it the only rule in either
1598 /// table that exists on the oldest four drafts and nowhere else. The
1599 /// sentence goes with the Parameter framing it describes, and drafts 11 and
1600 /// later replaced that framing with Key-Value-Pairs — where the
1601 /// neighbouring rule is [`Self::KeyValueFormatting`], under a different
1602 /// code.
1603 ///
1604 /// It is also the only rule here answered with neither PROTOCOL_VIOLATION
1605 /// nor KEY_VALUE_FORMATTING_ERROR: each of those four drafts assigns
1606 /// Parameter Length Mismatch a number of its own in the session termination
1607 /// registry, and the sentence names it.
1608 ParameterLengthMismatch,
1609
1610 // ── The discriminators a reader cannot get past ────────────
1611 /// A subscription filter naming a Filter Type the draft does not assign.
1612 ///
1613 /// Drafts 14 through 19, and two runs because **draft-14's sentence carries
1614 /// the draft's own missing word** — it has an endpoint `MUST be close the
1615 /// session`. Reproduced rather than corrected, for the reason
1616 /// [`RuleCitation::sentence`] gives.
1617 ///
1618 /// Drafts 07 through 13 state the rule and no consequence: a filter type
1619 /// other than the assigned ones `MUST be treated as error`, which names no
1620 /// code and no close. So the same value in the same place is a refused
1621 /// message on the first seven drafts and a dead session on the next six,
1622 /// and only the per-draft close table can tell them apart.
1623 ///
1624 /// Draft-20 states it nowhere. See this enum's own doc for why a close
1625 /// remains in draft-20's table and why the missing citation is what makes
1626 /// that safe.
1627 InvalidFilterType,
1628 /// A FETCH naming a Fetch Type the draft does not assign.
1629 ///
1630 /// The sentence next to the Filter Type one, moving the same way and
1631 /// carrying the same draft-14 missing word. Drafts 14 through 19; drafts 08
1632 /// through 13 say a Fetch Type outside the set `MUST be treated as an
1633 /// error` and state no consequence, and draft-07 has a FETCH with no Fetch
1634 /// Type field in it.
1635 ///
1636 /// Draft-20 deleted the field, both variant structures and the registry
1637 /// together, so it has nothing to state.
1638 ///
1639 /// Four runs for two wordings: the section moves three times across the
1640 /// range while the sentence stands still after draft-14.
1641 InvalidFetchType,
1642 /// A subscription filter parameter whose value is not a filter.
1643 ///
1644 /// Drafts 15 through 20, and the one rule here whose **code changes** within
1645 /// its own range rather than only being spelled differently. Drafts 15 and
1646 /// 16 state it of this parameter directly and answer PROTOCOL_VIOLATION;
1647 /// drafts 17 through 20 drop that sentence and leave the general key-value
1648 /// rule, which answers KEY_VALUE_FORMATTING_ERROR. A filter three bytes long
1649 /// inside a four-byte parameter ends a draft-16 session with one code and a
1650 /// draft-17 session with the other.
1651 ///
1652 /// The later run cites the same sentence [`Self::KeyValueFormatting`] does,
1653 /// which is what a shared constant is for. The reading that a malformed
1654 /// filter is a Value that does not match the serialization its Type defines
1655 /// is this build's, and it is the same reading `codec_session_error_code`
1656 /// already makes on those four drafts. Stated here rather than left to be
1657 /// noticed: the sentence is the drafts', and that this malformation is its
1658 /// case is a reading of it.
1659 ///
1660 /// A Filter Type outside the assigned set is not this rule. That is
1661 /// [`Self::InvalidFilterType`], whose own section names its own code.
1662 SubscriptionFilterMalformed,
1663 /// An AbsoluteRange filter whose End Group Delta carries the last Group ID
1664 /// past the end of the varint space.
1665 ///
1666 /// Drafts 18 through 20. Draft-17 introduced the delta and states the
1667 /// arithmetic with no consequence for overflowing it — that draft contains
1668 /// exactly one mention of the bound in the whole document and it is the
1669 /// Delta Type rule, not this one — so there an overflowing filter is a
1670 /// decode failure and nothing more.
1671 ///
1672 /// The draft-18 and draft-19 run keeps the clause before the consequence,
1673 /// because *the resulting Group ID* has no antecedent without it. The two
1674 /// are contiguous inside one paragraph, so it is a quotation and not a
1675 /// splice. Draft-20 rewrote the pair as one self-contained sentence naming
1676 /// its own operands, which is the second run.
1677 FilterEndGroupOverflow,
1678
1679 // ── The data plane ─────────────────────────────────────────
1680 /// A delta-encoded Object ID that would exceed the varint space once the
1681 /// delta is added to the previous Object ID on the same stream.
1682 ///
1683 /// Drafts 18 through 20 in Section 11.4.2 and draft-21 in Section 11.3.1,
1684 /// two runs: the wording does not change and the section number does.
1685 ///
1686 /// Drafts 14 through 17 carry the identical arithmetic in Section 10.4.2
1687 /// and state no consequence for overflowing it — all four of them, not
1688 /// draft-17 alone — and drafts 07 through 13 have no Object ID Delta to
1689 /// overflow.
1690 ///
1691 /// The three sentences are quoted unbroken, and no ellipsis may stand
1692 /// between the first and the third: eliding there splices across the
1693 /// sentence about the first Object in the Subgroup stream and makes a
1694 /// sentence none of the three drafts has.
1695 ObjectIdOverflow,
1696 /// A subgroup stream header's Type holding a combination its draft names as
1697 /// invalid.
1698 ///
1699 /// Drafts 16 through 21, four runs: the section moves twice and the wording
1700 /// changes once, and the two moves do not coincide. Sections 10.4.2 on
1701 /// drafts 16 and 17, 11.4.2 on 18, 19 and 20, and 11.3.1 on draft-21;
1702 /// drafts 16 through 19 enumerate the code points after the sentence and
1703 /// drafts 20 and 21 state the bit pattern alone.
1704 ///
1705 /// The sentence is quoted as far as the colon it ends on, because what
1706 /// follows it is a bulleted list of values rather than more sentence. That
1707 /// is the whole quotation and not an elision, so no ellipsis stands in for
1708 /// the list.
1709 ///
1710 /// Paired with [`CodecRule::InvalidDatagramTypeValue`], which is the same
1711 /// rule stated for the other Type space in a different section of the same
1712 /// drafts. They are two rules here because they are two sentences there,
1713 /// and because a consumer holding one refusal has to be able to publish the
1714 /// sentence it actually broke.
1715 InvalidStreamTypeValue,
1716 /// A datagram's Type holding a combination its draft names as invalid.
1717 ///
1718 /// Drafts 16 through 21, four runs, cut where
1719 /// [`CodecRule::InvalidStreamTypeValue`]'s are cut and at different
1720 /// numbers: Sections 10.3.1 on drafts 16 and 17, 11.3.1 on 18, 19 and 20,
1721 /// and 11.2.1 on draft-21.
1722 ///
1723 /// Draft-21 is why the two rules cannot share a run. It splits the section
1724 /// that held both, so the datagram sentence lands at 11.2.1 and the stream
1725 /// sentence at 11.3.1 — the one number that had been the datagram's on the
1726 /// three drafts before it.
1727 InvalidDatagramTypeValue,
1728 /// An end-of-Track Object stating an Object ID other than zero.
1729 ///
1730 /// **Drafts 08 through 10 alone**, and the one rule in this table whose
1731 /// sentence names no code: it ends with the receiver having to terminate
1732 /// the session and nothing more, so [`RuleCitation::code_name`] is `None`
1733 /// and a row publishes the number without a name for it. The number comes
1734 /// from `codec_session_error_code`, which answers PROTOCOL_VIOLATION — what
1735 /// those drafts answer every rule they close over without naming a code.
1736 /// The name is absent because the draft does not supply it, which is not
1737 /// the same thing as the draft stating no consequence.
1738 ///
1739 /// There is a look-alike sentence in the same subsection of the same three
1740 /// drafts, about Object Status 0x4 rather than 0x5, and the two differ by
1741 /// four words in two places: the neighbour has a Group ID *less than* any
1742 /// other and an Object ID *less than or equal to the largest in the group*,
1743 /// where this one has a Group ID *less than or equal to* any other and an
1744 /// Object ID *other than zero*. The neighbour is
1745 /// [`AboveCodecRule::EndOfTrackOutOfPlace`]'s sentence and is already cited
1746 /// above; citing it here would have put two rules under one wording and
1747 /// looked right.
1748 ///
1749 /// Draft-07 assigns no 0x5. Drafts 11 and later merge end of Track into
1750 /// 0x4 and permit either shape, so there is no longer a rule to break.
1751 EndOfTrackObjectId,
1752 /// An Object with Object Status 'Object Does Not Exist' carrying extension
1753 /// headers.
1754 ///
1755 /// Drafts 11 through 14, three runs: the section moves twice and draft-14
1756 /// respells the code. The rule names one status and no others, so
1757 /// extensions beside End of Group or End of Track are legal on those four
1758 /// drafts and are not this.
1759 ///
1760 /// Drafts 15 and later replaced the narrow form with the general one —
1761 /// extensions, later properties, permitted only beside Normal — which is a
1762 /// different rule with a different subject and reaches a consumer as
1763 /// [`AboveCodecRule::PropertiesOnNonNormalStatus`]. Drafts 07 through 10
1764 /// state neither form.
1765 ExtensionsOnNonExistentObject,
1766
1767 // ── And one that lived for exactly one draft ───────────────
1768 /// A request message's Required Request ID Delta naming a dependency below
1769 /// zero.
1770 ///
1771 /// **Draft-17 alone.** Draft-18 removed the field and mentions it only in
1772 /// its change log, so no other draft can state the rule.
1773 ///
1774 /// The only rule in this table answered with INVALID_REQUIRED_REQUEST_ID,
1775 /// and the only sentence in it carrying a character outside ASCII: the
1776 /// draft writes the comparison with a multiplication sign, and the gate
1777 /// compares against the rendering rather than against what a transcriber
1778 /// would have typed.
1779 InvalidRequiredRequestIdDelta,
1780}
1781
1782// The sentences these rules are stated in, one constant per wording, on the
1783// same terms as the block above: transcribed from the renderings and not
1784// tidied. Three of them carry something a transcriber would want to correct and
1785// must not -- drafts 11 through 16 render the GOAWAY URI maximum with
1786// `maxmimum`, and draft-14 renders both the Filter Type and the Fetch Type
1787// sentences with `MUST be close the session`. `check-drafts.py` rule 8 compares
1788// each of these against the rendering it came from, so a correction of the
1789// drafts' spelling is a red gate.
1790//
1791// Two more shapes worth naming before a reader meets them. The Reason Phrase
1792// sentences end without a full stop, because the rendering runs them into the
1793// next definition item and a supplied mark is a sentence no draft has. And
1794// `KVP_FORMATTING_16` is named from one rule and carried by two: the general
1795// key-value sentence is what drafts 17 through 20 leave a malformed
1796// subscription filter to, having dropped the filter's own.
1797const TRACK_NAME_MAX_11: &str = "The maximum total length of a Full Track Name is 4,096 bytes, \
1798 computed as the sum of the lengths of each Track Namespace tuple \
1799 field and the Track Name length field. If an endpoint receives a \
1800 Full Track Name exceeding this length, it MUST close the session \
1801 with a Protocol Violation.";
1802const TRACK_NAME_MAX_14: &str = "The maximum total length of a Full Track Name is 4,096 bytes, \
1803 computed as the sum of the lengths of each Track Namespace tuple \
1804 field and the Track Name length field. If an endpoint receives a \
1805 Full Track Name exceeding this length, it MUST close the session \
1806 with a PROTOCOL_VIOLATION.";
1807const TRACK_NAME_MAX_15: &str =
1808 "The maximum total length of a Full Track Name is 4,096 bytes. The \
1809 length of a Full Track Name is computed as the sum of the Track \
1810 Namespace Field Length fields and the Track Name Length field. If \
1811 an endpoint receives a Full Track Name exceeding this length, it \
1812 MUST close the session with a PROTOCOL_VIOLATION.";
1813const TRACK_NAME_MAX_16: &str =
1814 "The maximum total length of a Full Track Name is 4,096 bytes. The \
1815 length of a Full Track Name is computed as the sum of the Track \
1816 Namespace Field Length fields and the Track Name Length field. \
1817 The length of a Track Namespace is the sum of the Track Namespace \
1818 Field Length fields. If an endpoint receives a Track Namespace or \
1819 a Full Track Name exceeding 4,096 bytes, it MUST close the \
1820 session with a PROTOCOL_VIOLATION.";
1821const REASON_PHRASE_MAX_11: &str = "The reason phrase length has a maximum length of 1024 bytes. \
1822 If an endpoint receives a length exceeding the maximum, it \
1823 MUST close the session with a Protocol Violation";
1824const REASON_PHRASE_MAX_14: &str = "The reason phrase length has a maximum length of 1024 bytes. \
1825 If an endpoint receives a length exceeding the maximum, it \
1826 MUST close the session with a PROTOCOL_VIOLATION";
1827const REASON_PHRASE_MAX_15: &str =
1828 "The reason phrase length has a maximum value of 1024 bytes. If \
1829 an endpoint receives a length exceeding the maximum, it MUST \
1830 close the session with a PROTOCOL_VIOLATION";
1831const GOAWAY_URI_MAX_11: &str = "The maxmimum length of the New Session URI is 8,192 bytes. If an \
1832 endpoint receives a length exceeding the maximum, it MUST close \
1833 the session with a Protocol Violation.";
1834const GOAWAY_URI_MAX_14: &str = "The maxmimum length of the New Session URI is 8,192 bytes. If an \
1835 endpoint receives a length exceeding the maximum, it MUST close \
1836 the session with a PROTOCOL_VIOLATION.";
1837const GOAWAY_URI_MAX_17: &str = "The maximum length of the New Session URI is 8,192 bytes. If an \
1838 endpoint receives a length exceeding the maximum, it MUST close \
1839 the session with a PROTOCOL_VIOLATION.";
1840const KVP_VALUE_MAX_11: &str = "The maximum length of a value is 2^16-1 bytes. If an endpoint \
1841 receives a length larger than the maximum, it MUST close the \
1842 session with a Protocol Violation.";
1843const KVP_VALUE_MAX_16: &str = "The maximum length of a value is 2^16-1 bytes. If an endpoint \
1844 receives a length larger than the maximum, it MUST close the \
1845 session with a PROTOCOL_VIOLATION.";
1846const KVP_FORMATTING_11: &str = "If a receiver understands a Type, and the following Value or \
1847 Length/Value does not match the serialization defined by that \
1848 Type, the receiver MUST terminate the session with error code \
1849 \"Key-Value Formatting Error\".";
1850const KVP_FORMATTING_14: &str = "If a receiver understands a Type, and the following Value or \
1851 Length/Value does not match the serialization defined by that \
1852 Type, the receiver MUST terminate the session with error code \
1853 KEY_VALUE_FORMATTING_ERROR.";
1854const KVP_FORMATTING_16: &str = "If a receiver understands a Type, and the following Value or \
1855 Length/Value does not match the serialization defined by that \
1856 Type, the receiver MUST close the session with error code \
1857 KEY_VALUE_FORMATTING_ERROR.";
1858const UNKNOWN_PARAMETER_16: &str = "All Message Parameters MUST be defined in the negotiated \
1859 version of MOQT or negotiated via Setup Parameters. An \
1860 endpoint that receives an unknown Message Parameter MUST close \
1861 the session with PROTOCOL_VIOLATION.";
1862const UNKNOWN_PARAMETER_17: &str = "All Message Parameters MUST be defined in the negotiated \
1863 version of MOQT or negotiated via Setup Options. An endpoint \
1864 that receives an unknown Message Parameter MUST close the \
1865 session with PROTOCOL_VIOLATION.";
1866const PARAMETER_SCOPE: &str = "Each Message Parameter definition indicates the message types in \
1867 which it can appear. If it appears in some other type of message, \
1868 the receiving endpoint MUST close the connection with a \
1869 PROTOCOL_VIOLATION.";
1870const PARAMETER_LENGTH: &str = "If a receiver understands a parameter type, and the parameter \
1871 length implied by that type does not match the Parameter Length \
1872 field, the receiver MUST terminate the session with error code \
1873 'Parameter Length Mismatch'.";
1874const FILTER_TYPE_14: &str =
1875 "An endpoint that receives a filter type other than the above MUST be \
1876 close the session with PROTOCOL_VIOLATION.";
1877const FILTER_TYPE_15: &str = "An endpoint that receives a filter type other than the above MUST \
1878 close the session with PROTOCOL_VIOLATION.";
1879const FETCH_TYPE_14: &str = "An endpoint that receives a Fetch Type other than 0x1, 0x2 or 0x3 \
1880 MUST be close the session with a PROTOCOL_VIOLATION.";
1881const FETCH_TYPE_15: &str = "An endpoint that receives a Fetch Type other than 0x1, 0x2 or 0x3 \
1882 MUST close the session with a PROTOCOL_VIOLATION.";
1883const FILTER_PARAMETER_15: &str = "If the length of the Subscription Filter does not match the \
1884 parameter length, the publisher MUST close the session with \
1885 PROTOCOL_VIOLATION.";
1886const END_GROUP_WRAP_18: &str =
1887 "Otherwise, the last Group ID to be delivered will be the Group ID \
1888 in Start Location plus the End Group Delta. If the resulting \
1889 Group ID would be greater than 2^64 - 1, the endpoint MUST close \
1890 the session with a PROTOCOL_VIOLATION.";
1891const END_GROUP_WRAP_20: &str =
1892 "If StartGroup + EndGroupDelta exceeds 2^64 - 1, the endpoint MUST \
1893 close the session with a PROTOCOL_VIOLATION.";
1894const OBJECT_ID_WRAP: &str = "The Object ID Delta + 1 is added to the previous Object ID in the \
1895 Subgroup stream if there was one. The Object ID is the Object ID \
1896 Delta if it's the first Object in the Subgroup stream. If the \
1897 resulting Object ID would be greater than 2^64 - 1, the endpoint \
1898 MUST close the session with a PROTOCOL_VIOLATION.";
1899// The two Type-value rules, four wordings between them. Each draft states both
1900// as a sentence introducing a bulleted list of the values it rules out, and
1901// what is quoted is the sentence: the list is bullets rather than prose, and a
1902// quotation reaching into it would be a quotation of a list.
1903//
1904// The wording changes once, at draft-20, which stopped enumerating the code
1905// points and states the bit pattern alone — so "these Type values" becomes
1906// "these values" and the drafts part into two groups on each rule. A needle
1907// carrying the word Type finds drafts 16 through 19 and silently misses 20 and
1908// 21; the invariant clause is "receives a stream header with any of these".
1909const INVALID_STREAM_TYPE_16: &str = "If an endpoint receives a stream header with any of these \
1910 Type values, it MUST close the session with a \
1911 PROTOCOL_VIOLATION:";
1912const INVALID_STREAM_TYPE_20: &str = "If an endpoint receives a stream header with any of these \
1913 values, it MUST close the session with a \
1914 PROTOCOL_VIOLATION:";
1915const INVALID_DATAGRAM_TYPE_16: &str = "If an endpoint receives a datagram with any of these Type \
1916 values, it MUST close the session with a \
1917 PROTOCOL_VIOLATION:";
1918const INVALID_DATAGRAM_TYPE_20: &str = "If an endpoint receives a datagram with any of these \
1919 values, it MUST close the session with a \
1920 PROTOCOL_VIOLATION:";
1921const END_OF_TRACK_OBJECT_ID: &str = "An object with this status that has a Group ID less than or \
1922 equal to any other Group ID, or an Object ID other than \
1923 zero, is a protocol error, and the receiver MUST terminate \
1924 the session.";
1925const EXTENSIONS_ON_NONEXISTENT_11: &str = "Any Object may have extension headers except those \
1926 with Object Status 'Object Does Not Exist'. If an \
1927 endpoint receives a non-existent Object containing \
1928 extension headers it MUST close the session with a \
1929 Protocol Violation.";
1930const EXTENSIONS_ON_NONEXISTENT_14: &str = "Any Object may have extension headers except those \
1931 with Object Status 'Object Does Not Exist'. If an \
1932 endpoint receives a non-existent Object containing \
1933 extension headers it MUST close the session with a \
1934 PROTOCOL_VIOLATION.";
1935const REQUIRED_REQUEST_ID_DELTA: &str = "An endpoint MUST close the session with \
1936 INVALID_REQUIRED_REQUEST_ID if it receives a delta where \
1937 2 × Required Request ID Delta exceeds the Request ID.";
1938
1939impl CodecRule {
1940 /// Every draft run this rule has a checked citation for, oldest first.
1941 ///
1942 /// Exhaustive with no wildcard arm, for the reason the enum is not
1943 /// `#[non_exhaustive]`: a rule added to this file arrives here as an
1944 /// `E0004` and a decision about which drafts state it.
1945 ///
1946 /// No arm answers with an empty slice, which is the one structural
1947 /// difference from [`AboveCodecRule::citations`]. There, nine rules do, and
1948 /// eight of them are rules no draft answers with a close. Here a rule with
1949 /// nothing to cite would have no reason to exist: the whole set is chosen
1950 /// by reading the fourteen `codec_session_error_code` tables for variants
1951 /// answered `Some` on at least one draft, and a rule stated in no draft is
1952 /// left out rather than written down with an empty slice. One rule is left
1953 /// out on other grounds entirely, and the enum's doc names it and says
1954 /// which.
1955 ///
1956 /// The runs are **not** the drafts that answer with a close, and the enum's
1957 /// doc says which two rows differ and why. A draft with a close and no
1958 /// citation publishes nothing, which is the safe direction; a draft with a
1959 /// citation and no close is a row nothing can reach, and
1960 /// `a_cited_draft_is_a_draft_that_closes` below asserts there are none.
1961 #[must_use]
1962 pub fn citations(self) -> &'static [RuleCitation] {
1963 match self {
1964 Self::TrackNameTooLong => &[
1965 RuleCitation {
1966 drafts: (11, 13),
1967 section: "2.4.1",
1968 sentence: TRACK_NAME_MAX_11,
1969 code_name: Some("Protocol Violation"),
1970 },
1971 RuleCitation {
1972 drafts: (14, 14),
1973 section: "2.4.1",
1974 sentence: TRACK_NAME_MAX_14,
1975 code_name: Some("PROTOCOL_VIOLATION"),
1976 },
1977 RuleCitation {
1978 drafts: (15, 15),
1979 section: "2.4.1",
1980 sentence: TRACK_NAME_MAX_15,
1981 code_name: Some("PROTOCOL_VIOLATION"),
1982 },
1983 RuleCitation {
1984 drafts: (16, 20),
1985 section: "2.4.1",
1986 sentence: TRACK_NAME_MAX_16,
1987 code_name: Some("PROTOCOL_VIOLATION"),
1988 },
1989 RuleCitation {
1990 drafts: (21, 21),
1991 section: "8.7",
1992 sentence: TRACK_NAME_MAX_16,
1993 code_name: Some("PROTOCOL_VIOLATION"),
1994 },
1995 ],
1996 Self::ReasonPhraseTooLong => &[
1997 RuleCitation {
1998 drafts: (11, 13),
1999 section: "1.3.3",
2000 sentence: REASON_PHRASE_MAX_11,
2001 code_name: Some("Protocol Violation"),
2002 },
2003 RuleCitation {
2004 drafts: (14, 14),
2005 section: "1.4.3",
2006 sentence: REASON_PHRASE_MAX_14,
2007 code_name: Some("PROTOCOL_VIOLATION"),
2008 },
2009 RuleCitation {
2010 drafts: (15, 16),
2011 section: "1.4.3",
2012 sentence: REASON_PHRASE_MAX_15,
2013 code_name: Some("PROTOCOL_VIOLATION"),
2014 },
2015 RuleCitation {
2016 drafts: (17, 20),
2017 section: "1.4.4",
2018 sentence: REASON_PHRASE_MAX_15,
2019 code_name: Some("PROTOCOL_VIOLATION"),
2020 },
2021 RuleCitation {
2022 drafts: (21, 21),
2023 section: "8.5",
2024 sentence: REASON_PHRASE_MAX_15,
2025 code_name: Some("PROTOCOL_VIOLATION"),
2026 },
2027 ],
2028 Self::GoAwayUriTooLong => &[
2029 RuleCitation {
2030 drafts: (11, 13),
2031 section: "8.4",
2032 sentence: GOAWAY_URI_MAX_11,
2033 code_name: Some("Protocol Violation"),
2034 },
2035 RuleCitation {
2036 drafts: (14, 16),
2037 section: "9.4",
2038 sentence: GOAWAY_URI_MAX_14,
2039 code_name: Some("PROTOCOL_VIOLATION"),
2040 },
2041 RuleCitation {
2042 drafts: (17, 17),
2043 section: "9.5",
2044 sentence: GOAWAY_URI_MAX_17,
2045 code_name: Some("PROTOCOL_VIOLATION"),
2046 },
2047 RuleCitation {
2048 drafts: (18, 20),
2049 section: "10.4",
2050 sentence: GOAWAY_URI_MAX_17,
2051 code_name: Some("PROTOCOL_VIOLATION"),
2052 },
2053 RuleCitation {
2054 drafts: (21, 21),
2055 section: "9.2",
2056 sentence: GOAWAY_URI_MAX_17,
2057 code_name: Some("PROTOCOL_VIOLATION"),
2058 },
2059 ],
2060 Self::KvpValueTooLong => &[
2061 RuleCitation {
2062 drafts: (11, 13),
2063 section: "1.3.2",
2064 sentence: KVP_VALUE_MAX_11,
2065 code_name: Some("Protocol Violation"),
2066 },
2067 RuleCitation {
2068 drafts: (14, 15),
2069 section: "1.4.2",
2070 sentence: KVP_VALUE_MAX_11,
2071 code_name: Some("Protocol Violation"),
2072 },
2073 RuleCitation {
2074 drafts: (16, 16),
2075 section: "1.4.2",
2076 sentence: KVP_VALUE_MAX_16,
2077 code_name: Some("PROTOCOL_VIOLATION"),
2078 },
2079 RuleCitation {
2080 drafts: (17, 20),
2081 section: "1.4.3",
2082 sentence: KVP_VALUE_MAX_16,
2083 code_name: Some("PROTOCOL_VIOLATION"),
2084 },
2085 RuleCitation {
2086 drafts: (21, 21),
2087 section: "8.3",
2088 sentence: KVP_VALUE_MAX_16,
2089 code_name: Some("PROTOCOL_VIOLATION"),
2090 },
2091 ],
2092 Self::KeyValueFormatting => &[
2093 RuleCitation {
2094 drafts: (11, 13),
2095 section: "1.3.2",
2096 sentence: KVP_FORMATTING_11,
2097 code_name: Some("Key-Value Formatting Error"),
2098 },
2099 RuleCitation {
2100 drafts: (14, 15),
2101 section: "1.4.2",
2102 sentence: KVP_FORMATTING_14,
2103 code_name: Some("KEY_VALUE_FORMATTING_ERROR"),
2104 },
2105 RuleCitation {
2106 drafts: (16, 16),
2107 section: "1.4.2",
2108 sentence: KVP_FORMATTING_16,
2109 code_name: Some("KEY_VALUE_FORMATTING_ERROR"),
2110 },
2111 RuleCitation {
2112 drafts: (17, 20),
2113 section: "1.4.3",
2114 sentence: KVP_FORMATTING_16,
2115 code_name: Some("KEY_VALUE_FORMATTING_ERROR"),
2116 },
2117 RuleCitation {
2118 drafts: (21, 21),
2119 section: "8.3",
2120 sentence: KVP_FORMATTING_16,
2121 code_name: Some("KEY_VALUE_FORMATTING_ERROR"),
2122 },
2123 ],
2124 Self::UnknownMessageParameter => &[
2125 RuleCitation {
2126 drafts: (16, 16),
2127 section: "9.2",
2128 sentence: UNKNOWN_PARAMETER_16,
2129 code_name: Some("PROTOCOL_VIOLATION"),
2130 },
2131 RuleCitation {
2132 drafts: (17, 17),
2133 section: "9.3",
2134 sentence: UNKNOWN_PARAMETER_17,
2135 code_name: Some("PROTOCOL_VIOLATION"),
2136 },
2137 RuleCitation {
2138 drafts: (18, 20),
2139 section: "10.2",
2140 sentence: UNKNOWN_PARAMETER_17,
2141 code_name: Some("PROTOCOL_VIOLATION"),
2142 },
2143 RuleCitation {
2144 drafts: (21, 21),
2145 section: "9.20",
2146 sentence: UNKNOWN_PARAMETER_17,
2147 code_name: Some("PROTOCOL_VIOLATION"),
2148 },
2149 ],
2150 Self::ParameterOutOfScope => &[
2151 RuleCitation {
2152 drafts: (17, 17),
2153 section: "9.3.1",
2154 sentence: PARAMETER_SCOPE,
2155 code_name: Some("PROTOCOL_VIOLATION"),
2156 },
2157 RuleCitation {
2158 drafts: (18, 20),
2159 section: "10.2.1",
2160 sentence: PARAMETER_SCOPE,
2161 code_name: Some("PROTOCOL_VIOLATION"),
2162 },
2163 RuleCitation {
2164 drafts: (21, 21),
2165 section: "9.20.1",
2166 sentence: PARAMETER_SCOPE,
2167 code_name: Some("PROTOCOL_VIOLATION"),
2168 },
2169 ],
2170 Self::ParameterLengthMismatch => &[
2171 RuleCitation {
2172 drafts: (7, 7),
2173 section: "6.1",
2174 sentence: PARAMETER_LENGTH,
2175 code_name: Some("Parameter Length Mismatch"),
2176 },
2177 RuleCitation {
2178 drafts: (8, 9),
2179 section: "7.1",
2180 sentence: PARAMETER_LENGTH,
2181 code_name: Some("Parameter Length Mismatch"),
2182 },
2183 RuleCitation {
2184 drafts: (10, 10),
2185 section: "8.1",
2186 sentence: PARAMETER_LENGTH,
2187 code_name: Some("Parameter Length Mismatch"),
2188 },
2189 ],
2190 Self::InvalidFilterType => &[
2191 RuleCitation {
2192 drafts: (14, 14),
2193 section: "9.7",
2194 sentence: FILTER_TYPE_14,
2195 code_name: Some("PROTOCOL_VIOLATION"),
2196 },
2197 RuleCitation {
2198 drafts: (15, 19),
2199 section: "5.1.2",
2200 sentence: FILTER_TYPE_15,
2201 code_name: Some("PROTOCOL_VIOLATION"),
2202 },
2203 ],
2204 Self::InvalidFetchType => &[
2205 RuleCitation {
2206 drafts: (14, 14),
2207 section: "9.16",
2208 sentence: FETCH_TYPE_14,
2209 code_name: Some("PROTOCOL_VIOLATION"),
2210 },
2211 RuleCitation {
2212 drafts: (15, 16),
2213 section: "9.16",
2214 sentence: FETCH_TYPE_15,
2215 code_name: Some("PROTOCOL_VIOLATION"),
2216 },
2217 RuleCitation {
2218 drafts: (17, 17),
2219 section: "9.14",
2220 sentence: FETCH_TYPE_15,
2221 code_name: Some("PROTOCOL_VIOLATION"),
2222 },
2223 RuleCitation {
2224 drafts: (18, 19),
2225 section: "10.12",
2226 sentence: FETCH_TYPE_15,
2227 code_name: Some("PROTOCOL_VIOLATION"),
2228 },
2229 ],
2230 Self::SubscriptionFilterMalformed => &[
2231 RuleCitation {
2232 drafts: (15, 15),
2233 section: "9.2.1.7",
2234 sentence: FILTER_PARAMETER_15,
2235 code_name: Some("PROTOCOL_VIOLATION"),
2236 },
2237 RuleCitation {
2238 drafts: (16, 16),
2239 section: "9.2.2.5",
2240 sentence: FILTER_PARAMETER_15,
2241 code_name: Some("PROTOCOL_VIOLATION"),
2242 },
2243 RuleCitation {
2244 drafts: (17, 20),
2245 section: "1.4.3",
2246 sentence: KVP_FORMATTING_16,
2247 code_name: Some("KEY_VALUE_FORMATTING_ERROR"),
2248 },
2249 RuleCitation {
2250 drafts: (21, 21),
2251 section: "8.3",
2252 sentence: KVP_FORMATTING_16,
2253 code_name: Some("KEY_VALUE_FORMATTING_ERROR"),
2254 },
2255 ],
2256 Self::FilterEndGroupOverflow => &[
2257 RuleCitation {
2258 drafts: (18, 19),
2259 section: "5.1.2",
2260 sentence: END_GROUP_WRAP_18,
2261 code_name: Some("PROTOCOL_VIOLATION"),
2262 },
2263 RuleCitation {
2264 drafts: (20, 20),
2265 section: "5.1.2",
2266 sentence: END_GROUP_WRAP_20,
2267 code_name: Some("PROTOCOL_VIOLATION"),
2268 },
2269 RuleCitation {
2270 drafts: (21, 21),
2271 section: "9.20.10",
2272 sentence: END_GROUP_WRAP_20,
2273 code_name: Some("PROTOCOL_VIOLATION"),
2274 },
2275 ],
2276 Self::ObjectIdOverflow => &[
2277 RuleCitation {
2278 drafts: (18, 20),
2279 section: "11.4.2",
2280 sentence: OBJECT_ID_WRAP,
2281 code_name: Some("PROTOCOL_VIOLATION"),
2282 },
2283 RuleCitation {
2284 drafts: (21, 21),
2285 section: "11.3.1",
2286 sentence: OBJECT_ID_WRAP,
2287 code_name: Some("PROTOCOL_VIOLATION"),
2288 },
2289 ],
2290 Self::InvalidStreamTypeValue => &[
2291 RuleCitation {
2292 drafts: (16, 17),
2293 section: "10.4.2",
2294 sentence: INVALID_STREAM_TYPE_16,
2295 code_name: Some("PROTOCOL_VIOLATION"),
2296 },
2297 RuleCitation {
2298 drafts: (18, 19),
2299 section: "11.4.2",
2300 sentence: INVALID_STREAM_TYPE_16,
2301 code_name: Some("PROTOCOL_VIOLATION"),
2302 },
2303 RuleCitation {
2304 drafts: (20, 20),
2305 section: "11.4.2",
2306 sentence: INVALID_STREAM_TYPE_20,
2307 code_name: Some("PROTOCOL_VIOLATION"),
2308 },
2309 RuleCitation {
2310 drafts: (21, 21),
2311 section: "11.3.1",
2312 sentence: INVALID_STREAM_TYPE_20,
2313 code_name: Some("PROTOCOL_VIOLATION"),
2314 },
2315 ],
2316 Self::InvalidDatagramTypeValue => &[
2317 RuleCitation {
2318 drafts: (16, 17),
2319 section: "10.3.1",
2320 sentence: INVALID_DATAGRAM_TYPE_16,
2321 code_name: Some("PROTOCOL_VIOLATION"),
2322 },
2323 RuleCitation {
2324 drafts: (18, 19),
2325 section: "11.3.1",
2326 sentence: INVALID_DATAGRAM_TYPE_16,
2327 code_name: Some("PROTOCOL_VIOLATION"),
2328 },
2329 RuleCitation {
2330 drafts: (20, 20),
2331 section: "11.3.1",
2332 sentence: INVALID_DATAGRAM_TYPE_20,
2333 code_name: Some("PROTOCOL_VIOLATION"),
2334 },
2335 RuleCitation {
2336 drafts: (21, 21),
2337 section: "11.2.1",
2338 sentence: INVALID_DATAGRAM_TYPE_20,
2339 code_name: Some("PROTOCOL_VIOLATION"),
2340 },
2341 ],
2342 Self::EndOfTrackObjectId => &[
2343 RuleCitation {
2344 drafts: (8, 9),
2345 section: "8.1.1.1",
2346 sentence: END_OF_TRACK_OBJECT_ID,
2347 code_name: None,
2348 },
2349 RuleCitation {
2350 drafts: (10, 10),
2351 section: "9.1.1.1",
2352 sentence: END_OF_TRACK_OBJECT_ID,
2353 code_name: None,
2354 },
2355 ],
2356 Self::ExtensionsOnNonExistentObject => &[
2357 RuleCitation {
2358 drafts: (11, 11),
2359 section: "9.1.1.2",
2360 sentence: EXTENSIONS_ON_NONEXISTENT_11,
2361 code_name: Some("Protocol Violation"),
2362 },
2363 RuleCitation {
2364 drafts: (12, 13),
2365 section: "9.2.1.2",
2366 sentence: EXTENSIONS_ON_NONEXISTENT_11,
2367 code_name: Some("Protocol Violation"),
2368 },
2369 RuleCitation {
2370 drafts: (14, 14),
2371 section: "10.2.1.2",
2372 sentence: EXTENSIONS_ON_NONEXISTENT_14,
2373 code_name: Some("PROTOCOL_VIOLATION"),
2374 },
2375 ],
2376 Self::InvalidRequiredRequestIdDelta => &[RuleCitation {
2377 drafts: (17, 17),
2378 section: "9.2",
2379 sentence: REQUIRED_REQUEST_ID_DELTA,
2380 code_name: Some("INVALID_REQUIRED_REQUEST_ID"),
2381 }],
2382 }
2383 }
2384
2385 /// This rule as `draft` states it, or `None` where that draft does not.
2386 ///
2387 /// The same contract as [`AboveCodecRule::citation`], including the part
2388 /// that matters most: there is no fallback to a neighbouring draft's
2389 /// wording. `None` means there is no sentence to publish, so there is no
2390 /// accusation to make.
2391 #[must_use]
2392 pub fn citation(self, draft: u8) -> Option<&'static RuleCitation> {
2393 self.citations().iter().find(|c| c.covers(draft))
2394 }
2395}
2396
2397#[cfg(test)]
2398mod tests {
2399 use super::*;
2400
2401 /// Every rule this build enforces, so a sweep cannot silently narrow.
2402 ///
2403 /// Written out rather than derived, because there is nothing to derive it
2404 /// from: the enum is not iterable and adding a variant has to be a decision
2405 /// here as well as in `citations`. A rule added and not listed fails
2406 /// `every_variant_is_swept` below.
2407 const ALL: &[AboveCodecRule] = &[
2408 AboveCodecRule::PropertiesOnNonNormalStatus,
2409 AboveCodecRule::PayloadOnStatusDatagram,
2410 AboveCodecRule::BidiStreamOpener,
2411 AboveCodecRule::MessageOnTheWrongStream,
2412 AboveCodecRule::ResponseNamesAnotherRequest,
2413 AboveCodecRule::ResponseBeforeItsFirstResponse,
2414 AboveCodecRule::GoAwayAtServer,
2415 AboveCodecRule::RepeatedGoAway,
2416 AboveCodecRule::RedirectUriAtServer,
2417 AboveCodecRule::RedirectTrackNameOnNamespaceRequest,
2418 AboveCodecRule::RequestIdParity,
2419 AboveCodecRule::RequestIdOutOfSequence,
2420 AboveCodecRule::RequestIdCeiling,
2421 AboveCodecRule::MaxRequestIdDecreased,
2422 AboveCodecRule::SetupParameterValue,
2423 AboveCodecRule::DuplicateTrackAlias,
2424 AboveCodecRule::MixedForwardingPreference,
2425 AboveCodecRule::EndOfTrackOutOfPlace,
2426 AboveCodecRule::ObjectPastFinalObject,
2427 AboveCodecRule::RequestUpdateForTheWrongRequest,
2428 AboveCodecRule::TooManyRequestUpdates,
2429 AboveCodecRule::TrackPropertiesOnNonTrackStatus,
2430 AboveCodecRule::StateNotifyOnTheWrongRequest,
2431 AboveCodecRule::UnrequestedFillStream,
2432 AboveCodecRule::SubscribeAfterAnnounceCancel,
2433 AboveCodecRule::TrackStatusIsNotASubscription,
2434 AboveCodecRule::MessageNamesAnUnknownRequest,
2435 AboveCodecRule::NamespacePrefixOverlap,
2436 ];
2437
2438 /// Every rule the *decoder* raises that a draft answers with a close.
2439 ///
2440 /// The same shape as `ALL` and for the same reason. The two lists are kept
2441 /// apart rather than merged because the sweeps below ask both the same
2442 /// structural questions and nothing else: the two enums are two kinds of
2443 /// evidence and a test that could not say which one it had found would be
2444 /// the wrong test to fix a mix-up with.
2445 const ALL_CODEC: &[CodecRule] = &[
2446 CodecRule::TrackNameTooLong,
2447 CodecRule::ReasonPhraseTooLong,
2448 CodecRule::GoAwayUriTooLong,
2449 CodecRule::KvpValueTooLong,
2450 CodecRule::KeyValueFormatting,
2451 CodecRule::UnknownMessageParameter,
2452 CodecRule::ParameterOutOfScope,
2453 CodecRule::ParameterLengthMismatch,
2454 CodecRule::InvalidFilterType,
2455 CodecRule::InvalidFetchType,
2456 CodecRule::SubscriptionFilterMalformed,
2457 CodecRule::FilterEndGroupOverflow,
2458 CodecRule::ObjectIdOverflow,
2459 CodecRule::InvalidStreamTypeValue,
2460 CodecRule::InvalidDatagramTypeValue,
2461 CodecRule::EndOfTrackObjectId,
2462 CodecRule::ExtensionsOnNonExistentObject,
2463 CodecRule::InvalidRequiredRequestIdDelta,
2464 ];
2465
2466 /// Every run in both tables, so a sweep cannot be written for one of them
2467 /// and quietly answer for neither.
2468 ///
2469 /// The two enums do not share a trait and giving them one would be giving
2470 /// the library a shape it needs for nothing but this, so what is shared is
2471 /// the slice of runs rather than the rule that owns it. Where a test needs
2472 /// to name the rule it failed on, it asks its own list.
2473 fn every_run() -> Vec<&'static RuleCitation> {
2474 ALL.iter()
2475 .flat_map(|r| r.citations().iter())
2476 .chain(ALL_CODEC.iter().flat_map(|r| r.citations().iter()))
2477 .collect()
2478 }
2479
2480 /// The list above names every variant, so the sweeps below really are
2481 /// sweeps.
2482 ///
2483 /// `citations` is exhaustive, so a new variant is a build failure there;
2484 /// this list is not, so a new variant would silently drop out of every test
2485 /// in this module. Counting is the cheapest link between the two that does
2486 /// not need the enum to be iterable.
2487 #[test]
2488 fn every_variant_is_swept() {
2489 assert_eq!(ALL.len(), 28, "a rule was added or removed without updating ALL");
2490 }
2491
2492 /// And the same for the decoder's half.
2493 #[test]
2494 fn every_codec_rule_is_swept() {
2495 assert_eq!(ALL_CODEC.len(), 18, "a rule was added or removed without updating ALL_CODEC");
2496 }
2497
2498 /// A cited rule is a rule this build can actually raise on that draft.
2499 ///
2500 /// Not a claim about the drafts, and deliberately weaker than the one a
2501 /// reader might expect. What it rules out is a run written for a draft
2502 /// **outside** the range the enum's own doc claims for it — the failure
2503 /// that would put a sentence in front of a reader with no path to it. The
2504 /// stronger claim, that a cited draft is one whose
2505 /// `codec_session_error_code` answers `Some`, needs every draft's
2506 /// connection at once, and so belongs in a consumer that builds every
2507 /// draft rather than here.
2508 #[test]
2509 fn a_cited_draft_is_a_draft_that_closes() {
2510 for rule in ALL_CODEC {
2511 let runs = rule.citations();
2512 assert!(!runs.is_empty(), "{rule:?} is named with nothing to cite");
2513 for cite in runs {
2514 assert!(
2515 moqtap_codec::version::DraftVersion::from_number(cite.drafts.0).is_some()
2516 && moqtap_codec::version::DraftVersion::from_number(cite.drafts.1)
2517 .is_some(),
2518 "{rule:?} cites a draft outside the range this build implements"
2519 );
2520 }
2521 }
2522 }
2523
2524 /// A rule's runs are ordered, do not overlap, and name only drafts this
2525 /// build implements.
2526 ///
2527 /// Overlap is the failure that would be invisible otherwise: `citation`
2528 /// takes the first run that covers a draft, so two runs claiming draft-14
2529 /// would publish one of them and never say which.
2530 #[test]
2531 fn every_run_is_ordered_contiguous_and_in_range() {
2532 let runs: Vec<(String, &'static [RuleCitation])> = ALL
2533 .iter()
2534 .map(|r| (format!("{r:?}"), r.citations()))
2535 .chain(ALL_CODEC.iter().map(|r| (format!("{r:?}"), r.citations())))
2536 .collect();
2537 for (rule, citations) in runs {
2538 let mut last: Option<u8> = None;
2539 for cite in citations {
2540 let (lo, hi) = cite.drafts;
2541 assert!(lo <= hi, "{rule:?}: run ({lo}, {hi}) runs backwards");
2542 assert!(
2543 moqtap_codec::version::DraftVersion::from_number(lo).is_some()
2544 && moqtap_codec::version::DraftVersion::from_number(hi).is_some(),
2545 "{rule:?}: out of range"
2546 );
2547 if let Some(prev) = last {
2548 assert!(
2549 prev < lo,
2550 "{rule:?}: run at {lo} overlaps or repeats the one at {prev}"
2551 );
2552 }
2553 last = Some(hi);
2554 }
2555 }
2556 }
2557
2558 /// One wording is written once.
2559 ///
2560 /// The claim the run representation rests on. Two rows carrying the same
2561 /// sentence must be pointing at the same constant, not at two copies of it
2562 /// — because two copies is how a correction reaches one of them and not the
2563 /// other, and the second copy then reads as a second draft's wording that
2564 /// happens to be identical.
2565 ///
2566 /// Compared by pointer as well as by value, which is what makes this a test
2567 /// about the representation rather than about the text.
2568 #[test]
2569 fn a_wording_shared_across_runs_is_written_once() {
2570 let mut seen: Vec<&'static str> = Vec::new();
2571 for cite in every_run() {
2572 match seen.iter().find(|s| **s == cite.sentence) {
2573 Some(first) => assert!(
2574 std::ptr::eq(*first, cite.sentence),
2575 "a sentence is repeated instead of naming the constant: {}",
2576 &cite.sentence[..40.min(cite.sentence.len())]
2577 ),
2578 None => seen.push(cite.sentence),
2579 }
2580 }
2581 assert_eq!(seen.len(), 91, "the sentence count moved; re-run the draft sweep");
2582 }
2583
2584 /// The code name a row carries is a name its own sentence uses.
2585 ///
2586 /// The one claim in a row that the draft text cannot be searched for on its
2587 /// own: a section number resolves, a sentence resolves, and a code name is
2588 /// a word. Tying it to the sentence is what keeps it from drifting into
2589 /// this build's spelling of the code — which is exactly the direction it
2590 /// would drift, since `SessionErrorCode` spells every one of them in Rust.
2591 ///
2592 /// Underscores are folded to spaces because the drafts spell the same code
2593 /// both ways across the range, and this test is about the name being the
2594 /// sentence's rather than about which era it belongs to.
2595 #[test]
2596 fn the_code_name_is_a_name_the_sentence_uses() {
2597 for cite in every_run() {
2598 let Some(name) = cite.code_name else { continue };
2599 let sentence = cite.sentence.replace('_', " ").to_lowercase();
2600 let name = name.replace('_', " ").to_lowercase();
2601 assert!(sentence.contains(&name), "a run names a code its sentence does not: {name}");
2602 }
2603 }
2604
2605 /// The rule that made this table necessary, read at both ends of the range.
2606 ///
2607 /// One quoted sentence per rule published draft-18's words on every draft.
2608 /// This is the same rule asked twice, and every field of the answer differs
2609 /// — which is the whole claim, stated as an assertion rather than as a
2610 /// paragraph.
2611 #[test]
2612 fn the_track_alias_rule_is_a_different_citation_on_draft_07_and_draft_20() {
2613 let old = AboveCodecRule::DuplicateTrackAlias.citation(7).expect("draft-07 states it");
2614 let new = AboveCodecRule::DuplicateTrackAlias.citation(20).expect("draft-20 states it");
2615 assert_eq!(old.section, "6.4");
2616 assert_eq!(new.section, "11.1");
2617 assert_eq!(old.code_name, Some("Duplicate Track Alias"));
2618 assert_eq!(new.code_name, Some("DUPLICATE_TRACK_ALIAS"));
2619 assert_ne!(old.sentence, new.sentence);
2620 assert!(old.sentence.contains("already being used for a different track"));
2621 assert!(new.sentence.contains("PUBLISH or SUBSCRIBE_OK"));
2622 }
2623
2624 /// A draft that states a rule in no words this build has checked has no
2625 /// citation, and the gap is not filled from a neighbour.
2626 ///
2627 /// Drafts 17 and 18 are the concrete case. REQUEST_UPDATE moved onto the
2628 /// request's own stream and the field naming the request it modifies was
2629 /// deleted from the message, so there is nothing in either draft to name a
2630 /// request that cannot take an update — and neither draft closes a session
2631 /// over one. Draft-19 reintroduces the rule. A table that interpolated
2632 /// would answer draft-17 with draft-16's sentence about a field draft-17
2633 /// does not have.
2634 #[test]
2635 fn a_draft_that_does_not_state_a_rule_gets_no_citation_from_its_neighbours() {
2636 let rule = AboveCodecRule::RequestUpdateForTheWrongRequest;
2637 assert!(rule.citation(16).is_some());
2638 assert!(rule.citation(17).is_none(), "draft-17 deleted the field the rule is about");
2639 assert!(rule.citation(18).is_none(), "and draft-18 had not brought it back");
2640 assert!(rule.citation(19).is_some(), "draft-19 states it again, in new words");
2641 }
2642
2643 /// Rules the newest draft genuinely stopped stating.
2644 ///
2645 /// Empty is the normal state and the interesting one. A draft really can
2646 /// drop a rule - `RequestUpdateForTheWrongRequest` above is the worked
2647 /// case, deleted in drafts 17 and 18 along with the field it is about -
2648 /// so the sweep below cannot simply demand every rule on every draft. An
2649 /// entry here is the evidence for one of those, written down instead of
2650 /// absorbed; `RequestUpdateForTheWrongRequest` is not in it because
2651 /// draft-19 brought the rule back.
2652 const DROPPED_BY_THE_NEWEST_DRAFT: &[&str] = &[];
2653
2654 /// The newest draft is not quietly the first one to stop citing a rule.
2655 ///
2656 /// `check-draft-parity.py` rule 2 asks this of every draft list in the
2657 /// tree - a list naming draft N-1 and not draft N is the shape of one
2658 /// nobody brought forward - and it cannot ask it here: `citations`
2659 /// publishes runs as `(first, last)` tuples, and a tuple of integers is
2660 /// not a draft list. `check-drafts.py` rule 8 cannot ask it either. Rule 8
2661 /// holds every citation this table *makes* against that draft's rendered
2662 /// text, which is a strong check on the rows that exist and silent about
2663 /// the row that was never written. A rule left behind therefore publishes
2664 /// a finding with no sentence under it, on a green build.
2665 ///
2666 /// A run cannot be widened to cover the new draft, which is why this keeps
2667 /// happening: the drafts renumber, so a draft that keeps a sentence
2668 /// verbatim still files it under a different section and needs its own
2669 /// `RuleCitation`. Draft-21 moved every one of them.
2670 #[test]
2671 fn a_rule_the_previous_draft_states_is_cited_by_the_newest() {
2672 let newest = (7u8..=255)
2673 .take_while(|n| moqtap_codec::version::DraftVersion::from_number(*n).is_some())
2674 .last()
2675 .expect("this build implements at least one draft");
2676 let previous = newest - 1;
2677 assert!(
2678 moqtap_codec::version::DraftVersion::from_number(previous).is_some(),
2679 "the implemented drafts are contiguous, so the newest has a predecessor"
2680 );
2681
2682 let mut left_behind = Vec::new();
2683 for rule in ALL {
2684 if rule.citation(previous).is_some() && rule.citation(newest).is_none() {
2685 left_behind.push(format!("{rule:?}"));
2686 }
2687 }
2688 for rule in ALL_CODEC {
2689 if rule.citation(previous).is_some() && rule.citation(newest).is_none() {
2690 left_behind.push(format!("{rule:?}"));
2691 }
2692 }
2693 left_behind.retain(|name| !DROPPED_BY_THE_NEWEST_DRAFT.contains(&name.as_str()));
2694
2695 assert!(
2696 left_behind.is_empty(),
2697 "draft-{previous:02} states these rules and draft-{newest:02} cites none of \
2698 them: {left_behind:?}. Either that draft dropped the rule, in which case name \
2699 it in DROPPED_BY_THE_NEWEST_DRAFT beside the sentence that went away, or the \
2700 run was not brought forward - add a RuleCitation at the section the new draft \
2701 files the sentence under."
2702 );
2703 }
2704
2705 /// A rule with no sentence anywhere answers `None` on every draft.
2706 #[test]
2707 fn an_uncited_rule_names_no_draft() {
2708 for draft in 7..=21 {
2709 assert!(
2710 AboveCodecRule::MessageOnTheWrongStream.citation(draft).is_none(),
2711 "draft-{draft:02} acquired a citation for a rule no draft states"
2712 );
2713 }
2714 }
2715}
2716
2717/// What one of a draft's *own* `ConnectionError` variants is — the ones outside
2718/// the ten every draft shares.
2719///
2720/// Returned by `Connection::draft_specific_cause`, which every draft
2721/// implements exhaustively over its own error type. `None` from that function
2722/// means the variant is one of the shared ten, which
2723/// [`crate::dispatch::AnyConnectionError`] classifies itself and never asks a
2724/// draft about.
2725///
2726/// The split is the whole point: the two are opposite findings, one this build
2727/// declining to do something and the other a relay having done something the
2728/// draft forbids. Without it both reach a consumer as prose.
2729#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2730pub enum DraftSpecificCause {
2731 /// This endpoint refused the call. Nothing was written and no state moved.
2732 ///
2733 /// A message handed to the control stream that belongs on a request stream,
2734 /// a `respond_*` helper pointed at a request this endpoint opened, a FIN
2735 /// asked for before the response the draft requires first, an object asked
2736 /// for before the header it is framed against. Every one of them is this
2737 /// side being told to do something it will not do, and every one of them
2738 /// reaches [`crate::dispatch::ErrorCause::Facade`].
2739 ///
2740 /// # And one that is not a call at all
2741 ///
2742 /// `ConnectionError::ControlMessageNarrowing` is the odd member, and it is
2743 /// here for the reason the rest are rather than because it looks like them.
2744 /// Every draft's `recv_control` decodes into `AnyControlMessage` and then
2745 /// narrows the result to its own draft, and the arm that catches a
2746 /// narrowing that did not work is unreachable: the decoder was this draft's,
2747 /// so the variant can only be this draft's. Nothing pins that.
2748 ///
2749 /// Spelled as `CodecError::UnknownMessageType(0)` the placeholder would not
2750 /// stay inert. Every draft's `codec_session_error_code` answers that
2751 /// variant `Some(PROTOCOL_VIOLATION)`, so an unreachable arm being reached
2752 /// would reach a caller as *the peer sent a control message type this
2753 /// draft does not assign, and the session must be closed with a Protocol
2754 /// Violation* — with `0x00` riding along as the codepoint that proved it.
2755 /// A conformance report reading that publishes a named accusation against a
2756 /// relay, carrying a piece of evidence, for a defect in this build. Better
2757 /// dressed than any accusation it makes on real grounds, and false.
2758 ///
2759 /// So it is a variant of its own and it answers here. That does not make
2760 /// the arm reachable; it makes reaching it say *this build declined* rather
2761 /// than *the peer violated*, with no rule and no close code for anything
2762 /// downstream to read out of it.
2763 LocalRefusal,
2764 /// The peer broke a rule this endpoint enforces above its decoder.
2765 PeerViolation {
2766 /// Which rule, named the same way on every draft that states it.
2767 rule: AboveCodecRule,
2768 /// The session error code **this draft's own text** requires be sent
2769 /// for it, where it names one.
2770 ///
2771 /// Exactly the contract [`crate::dispatch::ErrorCause::Codec`] gives
2772 /// `close`, and for the same reason: a rule a draft attaches no
2773 /// consequence to is not grounds to name a peer.
2774 close: Option<u64>,
2775 },
2776}
2777
2778/// Whose doing one of a draft's `EndpointError` variants is.
2779///
2780/// Returned by `EndpointError::fault`, which every draft implements
2781/// exhaustively over its own error type — no wildcard arm, so a variant added
2782/// to a draft is a compile error beside the sentence it enforces rather than a
2783/// silent arrival on the wrong side of this answer.
2784///
2785/// # The question it answers, and the one it does not
2786///
2787/// The endpoint's error type mixes two opposite findings under one name, and
2788/// they read alike as prose. Some of its variants are raised while **reading**
2789/// what the peer sent, and they are the peer's doing. The rest are raised while
2790/// this endpoint is **writing**, or refusing to, and they are this side's — the
2791/// call failed, nothing reached the wire, and there is nothing to say about the
2792/// relay.
2793///
2794/// The code the draft requires be sent is deliberately not carried here.
2795/// `EndpointError::session_error_code` already answers it, per draft, quoting
2796/// the sentence that names it, and a second copy of that table beside this one
2797/// could disagree with it. [`crate::dispatch::AnyConnectionError`] reads both
2798/// and pairs them.
2799///
2800/// # Why `Some(code)` is not the test
2801///
2802/// It looks like one: a variant the draft answers with a session close is a
2803/// variant a peer broke a rule to reach. It is not, and the counterexample is
2804/// concrete. On every draft from 07 to 16, the send path refusing to advertise
2805/// a ceiling that does not increase — `send_max_subscribe_id` on drafts 07
2806/// through 10, `send_max_request_id` on 11 through 16 — and a peer sending a
2807/// ceiling of its own that does not increase are one variant apart: under a
2808/// single variant `session_error_code` answers both `Some(PROTOCOL_VIOLATION)`,
2809/// so a local refusal to write a message publishes as a relay breaking the
2810/// ceiling rule. That rule is stated of MAX_SUBSCRIBE_ID on drafts 07 through
2811/// 10 and of MAX_REQUEST_ID on 11 through 16, and the section moves under it
2812/// four times across the ten: 6.20, 7.20, 8.4, 8.5 and 9.5 — see
2813/// [`AboveCodecRule::MaxRequestIdDecreased`]'s citations for which drafts each
2814/// of those covers. Each side has a variant of its own for that reason, and
2815/// this enum is what makes the collision visible.
2816#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2817pub enum EndpointFault {
2818 /// This endpoint's own doing: a call it refused, or state of its own that
2819 /// would not take one.
2820 ///
2821 /// Nothing reached the wire. A response offered for a request the draft
2822 /// requires be refused, a message handed to the wrong writer, an alias this
2823 /// endpoint was asked to give to a second track, a session that is closed
2824 /// or draining. Every one of them is evidence about this build and none of
2825 /// them is evidence about a peer, so all of them reach
2826 /// [`crate::dispatch::ErrorCause::Endpoint`], which
2827 /// [`crate::dispatch::AnyConnectionError::is_local`] counts.
2828 ///
2829 /// A peer can *cause* one of these without having broken anything this
2830 /// endpoint may name it for. A request whose filters the draft says to
2831 /// reject is refused here when a REQUEST_OK is offered for it, because the
2832 /// refusal is a reply and a reply needs the request to have been taken
2833 /// first; what failed is this side's attempt to accept it.
2834 ThisEndpoint,
2835 /// The peer did what the draft forbids, and this endpoint caught it
2836 /// reading.
2837 Peer(AboveCodecRule),
2838 /// Both are reachable through this variant and the variant cannot say
2839 /// which.
2840 ///
2841 /// Answered like [`Self::ThisEndpoint`] — reaching
2842 /// [`crate::dispatch::ErrorCause::Endpoint`], counted by
2843 /// [`crate::dispatch::AnyConnectionError::is_local`] — because that is the
2844 /// safe direction and the one this facade already took: a failure that has
2845 /// not been told apart is not evidence against a relay.
2846 ///
2847 /// Kept as its own answer rather than folded into `ThisEndpoint` so that
2848 /// the ones still to be told apart are a list the compiler can produce.
2849 /// There are two kinds:
2850 ///
2851 /// - **The state machines.** `Session`, `Subscription`, `Fetch`,
2852 /// `Namespace`, `TrackStatus`, `PublishFlow`, `Setup` all render as
2853 /// *invalid transition from X on event Y*, and the same sentence covers
2854 /// this endpoint declining to send a message in a state that forbids it
2855 /// and a peer having sent one. Telling those apart is a pass of its own.
2856 /// - **The unknown-request errors.** `UnknownRequest` and its earlier name
2857 /// `UnknownSubscribe` are raised on more than sixty call sites per draft,
2858 /// about half of them reading a response that names an id this session
2859 /// never issued — the peer's doing — and about half of them a caller
2860 /// naming one of its own that does not exist.
2861 ///
2862 /// Neither is published as anything: both answer `is_local` true. What this
2863 /// enum adds is that they are countable.
2864 EitherEnd,
2865}
2866
2867impl EndpointFault {
2868 /// The rule this fault names, or `None` where it names none.
2869 ///
2870 /// `None` for both of the non-peer answers, which is the same collapse
2871 /// [`crate::dispatch::ErrorCause`] performs: neither is grounds to name a
2872 /// relay, and a caller that needs to tell them apart has the variant.
2873 pub fn rule(self) -> Option<AboveCodecRule> {
2874 match self {
2875 Self::Peer(rule) => Some(rule),
2876 Self::ThisEndpoint | Self::EitherEnd => None,
2877 }
2878 }
2879}