Skip to main content

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}