Skip to main content

sipx_testkit/
transaction_sequence.rs

1//! Structured event sequences for the transaction layer, and the invariants they are checked
2//! against.
3//!
4//! `sipx-sip` has four fuzz targets and all of them stop at the parser. That covers the half of
5//! the north star about adversarial *input*; the half about adversarial *timing* — what happens
6//! when messages, application calls and fired timers interleave in an order nobody wrote a test
7//! for — had nothing. This module is that instrument.
8//!
9//! # Programs, not bytes
10//!
11//! The fuzzer's bytes are decoded into a [`Program`]: a sequence of [`Event`]s over a small
12//! vocabulary, each of which the harness turns into a *well-formed* SIP message or a call on
13//! [`TransactionLayer`]. Reinterpreting the bytes as SIP instead would spend the whole budget
14//! producing messages that do not parse, which is `S-4`'s fuzz targets again with extra steps.
15//! Nothing here parses: messages are **built**, so every event reaches a state machine.
16//!
17//! The encoding is four bytes per event — opcode, target, and two operands — because libFuzzer
18//! mutates bytes and a fixed-width record keeps a byte flip to a single field instead of
19//! desynchronising the rest of the program. Every opcode byte is valid (it is taken modulo the
20//! opcode count), so no input is wasted on a decode failure.
21//!
22//! # The oracle
23//!
24//! A panic-only oracle finds almost nothing in a state machine: the machines are total, so
25//! almost any sequence "succeeds". What can go wrong is silent, so it is asserted explicitly —
26//! see [`Invariant`]. Violations are returned as data rather than panicked on, so the same code
27//! serves the fuzz target (which panics) and the regression tests (which assert).
28//!
29//! # Sans-IO
30//!
31//! No clock, no socket, no runtime, in keeping with `AGENTS.md`'s second non-negotiable. Time
32//! enters only as [`Event::FireTimer`], which is precisely what makes the timing half fuzzable.
33
34// The harness builds its fixtures from constants in this file. A fixture it cannot build is a
35// bug in the harness and must fail loudly rather than quietly drive nothing — the same reason
36// `AGENTS.md` lets test modules opt out of these lints.
37#![allow(clippy::expect_used)]
38
39use std::fmt::Write as _;
40use std::path::{Path, PathBuf};
41
42use bytes::Bytes;
43
44use sipx_sip::build::{RequestBuilder, ResponseBuilder};
45use sipx_sip::transaction::{
46    ClientState, Dispatch, Output, Reliability, ServerState, Timer, Timers, TransactionKey,
47    TransactionLayer, TuEvent,
48};
49use sipx_sip::{HeaderName, Host, HostName, Message, Method, Request, Response, StatusCode, Uri};
50
51// ------------------------------------------------------------------------------------------
52// The vocabulary
53// ------------------------------------------------------------------------------------------
54
55/// How many conversations the vocabulary can name.
56///
57/// Deliberately tiny. A large slot space would give almost every event a fresh transaction and
58/// the interesting behaviour — retransmission absorption, matching, a timer arriving for the
59/// transaction that has just gone — lives in collisions.
60pub const SLOTS: u8 = 4;
61
62/// Slots at or above this index use a branch with no RFC 3261 magic cookie, so they exercise
63/// the RFC 2543 matching fallback of `TransactionKey::Legacy`.
64pub const FIRST_LEGACY_SLOT: u8 = 2;
65
66/// Status codes the vocabulary can produce: provisional, 2xx, and non-2xx finals across the
67/// ranges the state tables branch on.
68pub const STATUSES: [u16; 8] = [100, 180, 200, 302, 404, 486, 500, 603];
69
70/// To-tags the vocabulary can produce. Three, because two are needed for a fork answering
71/// twice and the third distinguishes a legacy key.
72pub const TAGS: [&str; 3] = ["ta", "tb", "tc"];
73
74/// Every timer of RFC 3261 §17 Table 4, plus the unlettered 200 ms one.
75pub const TIMERS: [Timer; 13] = [
76    Timer::A,
77    Timer::B,
78    Timer::D,
79    Timer::E,
80    Timer::F,
81    Timer::G,
82    Timer::H,
83    Timer::I,
84    Timer::J,
85    Timer::K,
86    Timer::L,
87    Timer::M,
88    Timer::Trying100,
89];
90
91/// The methods the vocabulary can produce.
92///
93/// `ACK` and `CANCEL` earn their place: §17.2.3 folds `ACK` onto the `INVITE` it acknowledges
94/// and pointedly does not fold `CANCEL`, and getting either wrong is a call that never hangs up.
95fn method(index: u8) -> Method {
96    match index % METHOD_COUNT {
97        0 => Method::Invite,
98        1 => Method::Ack,
99        2 => Method::Bye,
100        3 => Method::Cancel,
101        4 => Method::Register,
102        _ => Method::Options,
103    }
104}
105
106/// How many methods [`method`] can return.
107const METHOD_COUNT: u8 = 6;
108
109/// The table sizes, as the `u8` the decoder needs. Checked against the tables below, because a
110/// count that drifts from its table silently narrows what the fuzzer can produce.
111const STATUS_COUNT: u8 = 8;
112const TAG_COUNT: u8 = 3;
113const TIMER_COUNT: u8 = 13;
114
115const _: () = {
116    assert!(STATUS_COUNT as usize == STATUSES.len());
117    assert!(TAG_COUNT as usize == TAGS.len());
118    assert!(TIMER_COUNT as usize == TIMERS.len());
119};
120
121/// Every `Timer` variant, matched exhaustively.
122///
123/// The const assert above proves `TIMER_COUNT` and `TIMERS` are the same length. It cannot prove the
124/// table covers the *enum*: a fourteenth variant leaves both the same size, so nothing fails and the
125/// new timer is simply never fuzzed — the drift the comment at the table says it guards against
126/// (`X-31`).
127///
128/// A match is the only construct the compiler checks for exhaustiveness. It is deliberately
129/// function-local rather than a trait impl, because what is being asserted is not "Timer has a
130/// table" but "this list of variants is complete *here*", which is the thing a global rule would
131/// silently stop being. A test (`the_table_and_the_enum_agree_row_for_row`) then ties each arm to
132/// its row, so the table and the enum cannot drift apart in either direction.
133const fn timer_row(timer: Timer) -> usize {
134    match timer {
135        Timer::A => 0,
136        Timer::B => 1,
137        Timer::D => 2,
138        Timer::E => 3,
139        Timer::F => 4,
140        Timer::G => 5,
141        Timer::H => 6,
142        Timer::I => 7,
143        Timer::J => 8,
144        Timer::K => 9,
145        Timer::L => 10,
146        Timer::M => 11,
147        Timer::Trying100 => 12,
148    }
149}
150
151#[cfg(test)]
152mod timer_coverage {
153    use super::{TIMERS, timer_row};
154
155    #[test]
156    fn the_table_and_the_enum_agree_row_for_row() {
157        // `timer_row` is an exhaustive match, so a fourteenth `Timer` fails to compile *there* and
158        // this test can only be reached by a table that names every variant. What it then catches
159        // is the two lists agreeing on order, which exhaustiveness alone cannot see: a row named in
160        // the match but filled with the wrong value would still compile.
161        let mut seen = [false; 13];
162        for (row, (timer, named)) in TIMERS.iter().zip(seen.iter_mut()).enumerate() {
163            // `timer_row` maps a variant back to this row, so the table and the match cannot
164            // disagree on order — the one drift exhaustiveness alone cannot see. Iterating both in
165            // step rather than indexing keeps the proof total: there is no index here that could be
166            // out of range for the array it walks.
167            assert_eq!(
168                timer_row(*timer),
169                row,
170                "timer_row({timer:?}) does not round-trip"
171            );
172            *named = true;
173        }
174        assert!(
175            seen.iter().all(|used| *used),
176            "a row of TIMERS is unreachable from any variant"
177        );
178    }
179}
180
181/// Distinct transaction keys the vocabulary can produce per branch space: `ACK` folds onto
182/// `INVITE`, so six methods name five keys.
183const FOLDED_METHODS: usize = 5;
184
185/// The most transactions that can be in flight at once, whatever the program does.
186///
187/// Two branch spaces (client and server), [`SLOTS`] branches in each, and five keys per branch
188/// — six methods, of which `ACK` folds onto `INVITE`. This is the bound behind
189/// [`Invariant::StoreGrowth`]: it depends on the
190/// *vocabulary* and not on the program's length, which is what "does not grow without bound
191/// over a bounded sequence" has to mean if it is to mean anything.
192/// The vocabulary's ceiling, kept because the doc below reads off it: the two `HashMap`s can hold
193/// at most `SLOTS × FOLDED_METHODS` keys each. The store-growth invariant no longer asserts against
194/// it, because it cannot fire — see the `X-31` note in `check_bound`.
195pub const MAX_LIVE_TRANSACTIONS: usize = 2 * SLOTS as usize * FOLDED_METHODS;
196
197/// Which branch space a message belongs to.
198///
199/// Requests the harness *receives* and responses it *sends* address server transactions;
200/// requests it sends and responses it receives address client ones. Giving each role its own
201/// branch keeps one key from naming a client and a server transaction at the same time, which
202/// would make every oracle below ambiguous about which machine it was talking about.
203#[derive(Debug, Clone, Copy, PartialEq, Eq)]
204enum Space {
205    Client,
206    Server,
207}
208
209impl Space {
210    fn letter(self) -> char {
211        match self {
212            Self::Client => 'c',
213            Self::Server => 's',
214        }
215    }
216}
217
218/// One step of a decoded program.
219///
220/// The three kinds the story names — a message arriving, the application asking for something,
221/// a timer firing — plus the two things a driver reports that no message can express.
222#[derive(Debug, Clone, Copy, PartialEq, Eq)]
223pub enum Event {
224    /// The application starts a client transaction.
225    SendRequest {
226        /// Which conversation, and therefore which branch.
227        slot: u8,
228        /// Index into the method vocabulary.
229        method: u8,
230        /// Whether the transport retransmits, which decides half the timer behaviour.
231        reliable: bool,
232    },
233    /// A request arrives from the network.
234    ReceiveRequest {
235        /// Which conversation, and therefore which branch.
236        slot: u8,
237        /// Index into the method vocabulary.
238        method: u8,
239        /// Whether the transport retransmits.
240        reliable: bool,
241        /// Index into [`TAGS`], for the `To` tag.
242        to_tag: u8,
243    },
244    /// A response arrives from the network, addressed at a client transaction.
245    ReceiveResponse {
246        /// Which conversation, and therefore which branch.
247        slot: u8,
248        /// Index into the method vocabulary, used for the `CSeq` method.
249        method: u8,
250        /// Index into [`STATUSES`].
251        status: u8,
252        /// Index into [`TAGS`], for the `To` tag.
253        to_tag: u8,
254    },
255    /// The application answers a server transaction.
256    SendResponse {
257        /// Which conversation, and therefore which branch.
258        slot: u8,
259        /// Index into the method vocabulary.
260        method: u8,
261        /// Index into [`STATUSES`].
262        status: u8,
263        /// Index into [`TAGS`], for the `To` tag.
264        to_tag: u8,
265    },
266    /// A timer the driver holds fires.
267    ///
268    /// The key is chosen from every key the layer has ever created, *including ones whose
269    /// transaction is gone* — a driver's timer wheel cannot cancel atomically, so a timer
270    /// firing into a retired machine is the normal case rather than the exotic one, and
271    /// [`Invariant::TimerForRemovedKey`] is what says it must be harmless.
272    FireTimer {
273        /// Index into the keys the layer has created.
274        key: u8,
275        /// Index into the timers.
276        timer: u8,
277        /// Choose from all thirteen timers rather than the ones currently armed. Off by
278        /// default so the budget mostly goes on timers a real driver would have set, and on
279        /// so that stray and never-armed ones are reachable too.
280        any: bool,
281    },
282    /// The transport reports it could not deliver for a transaction.
283    TransportError {
284        /// Index into the keys the layer has created.
285        key: u8,
286    },
287    /// The driver gives up on a server transaction the application never answered.
288    Abandon {
289        /// Index into the keys the layer has created.
290        key: u8,
291    },
292}
293
294/// How many opcodes the decoder recognises.
295const OPCODES: u8 = 7;
296
297/// Bytes per encoded event.
298const RECORD: usize = 4;
299
300/// A decoded sequence of events.
301#[derive(Debug, Clone, Default, PartialEq, Eq)]
302pub struct Program {
303    /// The events, in the order they are driven.
304    pub events: Vec<Event>,
305}
306
307impl Program {
308    /// Decode a fuzzer input.
309    ///
310    /// Total: every byte string is a program. A trailing partial record is ignored, which is
311    /// what lets libFuzzer shrink an input a byte at a time without the tail turning into
312    /// noise.
313    #[must_use]
314    pub fn decode(bytes: &[u8]) -> Self {
315        let events = bytes
316            .chunks_exact(RECORD)
317            .filter_map(|chunk| match chunk {
318                &[op, target, a, b] => Some(decode_event(op, target, a, b)),
319                _ => None,
320            })
321            .collect();
322        Self { events }
323    }
324
325    /// Encode a program back to the bytes that decode to it.
326    ///
327    /// The inverse of [`Program::decode`] on canonical inputs, which is what lets a seed
328    /// written as Rust be committed as a corpus file.
329    #[must_use]
330    pub fn encode(&self) -> Vec<u8> {
331        let mut out = Vec::with_capacity(self.events.len() * RECORD);
332        for event in &self.events {
333            out.extend_from_slice(&encode_event(*event));
334        }
335        out
336    }
337}
338
339fn decode_event(op: u8, target: u8, a: u8, b: u8) -> Event {
340    match op % OPCODES {
341        0 => Event::SendRequest {
342            slot: target % SLOTS,
343            method: a % METHOD_COUNT,
344            reliable: b & 1 != 0,
345        },
346        1 => Event::ReceiveRequest {
347            slot: target % SLOTS,
348            method: a % METHOD_COUNT,
349            reliable: b & 1 != 0,
350            to_tag: (b >> 4) % TAG_COUNT,
351        },
352        2 => Event::ReceiveResponse {
353            slot: target % SLOTS,
354            method: a % METHOD_COUNT,
355            status: b % STATUS_COUNT,
356            to_tag: (b >> 4) % TAG_COUNT,
357        },
358        3 => Event::SendResponse {
359            slot: target % SLOTS,
360            method: a % METHOD_COUNT,
361            status: b % STATUS_COUNT,
362            to_tag: (b >> 4) % TAG_COUNT,
363        },
364        4 => Event::FireTimer {
365            key: target,
366            timer: a % TIMER_COUNT,
367            any: b & 1 != 0,
368        },
369        5 => Event::TransportError { key: target },
370        _ => Event::Abandon { key: target },
371    }
372}
373
374fn encode_event(event: Event) -> [u8; RECORD] {
375    match event {
376        Event::SendRequest {
377            slot,
378            method,
379            reliable,
380        } => [0, slot, method, u8::from(reliable)],
381        Event::ReceiveRequest {
382            slot,
383            method,
384            reliable,
385            to_tag,
386        } => [1, slot, method, u8::from(reliable) | (to_tag << 4)],
387        Event::ReceiveResponse {
388            slot,
389            method,
390            status,
391            to_tag,
392        } => [2, slot, method, status | (to_tag << 4)],
393        Event::SendResponse {
394            slot,
395            method,
396            status,
397            to_tag,
398        } => [3, slot, method, status | (to_tag << 4)],
399        Event::FireTimer { key, timer, any } => [4, key, timer, u8::from(any)],
400        Event::TransportError { key } => [5, key, 0, 0],
401        Event::Abandon { key } => [6, key, 0, 0],
402    }
403}
404
405// ------------------------------------------------------------------------------------------
406// The oracle
407// ------------------------------------------------------------------------------------------
408
409/// The properties a transaction layer can break without panicking.
410///
411/// Each is something that would show up in production as a slow leak, a wedged call or a
412/// response answering the wrong request — never as a crash, which is why a panic-only fuzz
413/// target would run for a week and report nothing.
414#[derive(Debug, Clone, Copy, PartialEq, Eq)]
415pub enum Invariant {
416    /// No transaction outlives its terminal state.
417    ///
418    /// When a machine emits `Output::Terminated` the layer must have dropped it, and no
419    /// observable state may ever read `Terminated` — a transaction that reports its own death
420    /// and stays in the store is the leak this whole layer exists to avoid. Arming a timer in
421    /// the same batch that retires the transaction is the same fault seen from the other side.
422    OutlivedTermination,
423
424    /// No timer fires for a key that has been removed.
425    ///
426    /// A driver's timer wheel cannot cancel atomically: `ClearTimer` and the fired callback
427    /// race, and the transaction may be gone by the time the timer arrives. Firing into that
428    /// gap must produce nothing and above all must not resurrect the transaction. This is the
429    /// design record's "timer IDs must survive transaction termination without firing into a
430    /// dead machine", stated as something a test can fail.
431    TimerForRemovedKey,
432
433    /// The store does not grow without bound over a bounded sequence.
434    ///
435    /// Two claims. In flight, no more transactions than the vocabulary has keys — bounded by
436    /// [`MAX_LIVE_TRANSACTIONS`], not by how long the program is. And after every timer the
437    /// layer asked for has been driven to quiescence, the only transactions left are the ones
438    /// legitimately waiting on the application, because those are the only ones RFC 3261 gives
439    /// no timer of their own.
440    StoreGrowth,
441
442    /// No state is reachable that the RFC 3261 §17 tables, as amended by RFC 6026, do not name.
443    ///
444    /// `ClientState` and `ServerState` each cover two machines, so the enum alone proves
445    /// nothing: an INVITE client transaction reaching `Trying` — the *non*-INVITE waiting
446    /// state — type-checks and is meaningless. The legal set is per machine, taken from
447    /// `docs/specs/sip-transaction.md` §4.
448    UnnamedState,
449
450    /// A response to a request the layer sent must reach the transaction that sent it.
451    ///
452    /// Half of what the layer does is §17.1.3 matching, and a response that matches nothing is
453    /// a call that hangs until Timer F rather than a crash. Checked only where the harness
454    /// knows the answer: the response it builds carries the branch and `CSeq` method of the
455    /// request that created the transaction, so it must match.
456    UnroutableResponse,
457}
458
459/// One invariant broken, and where.
460#[derive(Debug, Clone)]
461pub struct Violation {
462    /// Which step of the program, or [`usize::MAX`] for the quiescence check that follows it.
463    pub step: usize,
464    /// Which invariant.
465    pub invariant: Invariant,
466    /// What was seen, in enough detail to name a defect from.
467    pub detail: String,
468}
469
470impl std::fmt::Display for Violation {
471    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
472        write!(
473            f,
474            "step {}: {:?}: {}",
475            self.step, self.invariant, self.detail
476        )
477    }
478}
479
480/// What driving a program produced.
481#[derive(Debug, Clone)]
482pub struct Run {
483    /// One line per step: the event, what the layer did with it, and the store's size.
484    ///
485    /// This is the artefact a crash report is read from, and the thing the replay test pins:
486    /// a harness whose trace is not a function of its input reports crashes nobody can
487    /// reproduce.
488    pub trace: Vec<String>,
489    /// Every invariant broken, in the order they were noticed.
490    pub violations: Vec<Violation>,
491}
492
493// ------------------------------------------------------------------------------------------
494// The driver
495// ------------------------------------------------------------------------------------------
496
497/// What the harness remembers about one key the layer has created.
498#[derive(Debug)]
499struct Tracked {
500    key: TransactionKey,
501    space: Space,
502    /// Whether this is one of the INVITE machines, which decides the legal state set.
503    invite: bool,
504    /// The timers the layer has asked for and not cleared. A `Vec` rather than a set because
505    /// `Timer` is not `Ord` and there are never more than three.
506    armed: Vec<Timer>,
507    /// `c0/INVITE`, for the trace.
508    label: String,
509}
510
511/// How many rounds the quiescence check will drive timers for before giving up.
512///
513/// Generous: a Timer A that doubles forever is retired by Timer B in the same round, so a
514/// transaction that survives this many rounds is not slow, it is stuck.
515const DRAIN_ROUNDS: usize = 64;
516
517struct Driver {
518    layer: TransactionLayer,
519    tracked: Vec<Tracked>,
520    trace: Vec<String>,
521    violations: Vec<Violation>,
522    /// The defects this run steps over.
523    ///
524    /// Always empty while [`Known`] is uninhabited, hence unread — kept, with the plumbing in
525    /// [`run_with`], because rebuilding it is what a campaign blocked behind a fresh defect
526    /// cannot afford to stop and do.
527    #[allow(dead_code)]
528    suppressed: Vec<Known>,
529}
530
531/// A defect the campaign knows about and steps over.
532///
533/// A fuzzer that crashes on the same open bug every time reports that bug forever and nothing
534/// behind it. Suppressing one is therefore necessary and also dangerous — a suppression nobody
535/// removes hides its whole class permanently — so each is named here, documented on its variant,
536/// and paired with an ignored regression test that fails until it is fixed. [`run_strict`]
537/// suppresses nothing, which is how those tests see the defect.
538///
539/// There are none at the moment: `LegacyClientResponseMatching` — a response to an RFC 2543
540/// client transaction matching nothing, because `TransactionKey::from_sent_request` derived the
541/// client key by §17.2.3's server rules — was the first and so far only entry, and `S-26` fixed
542/// it. The type stays because the next campaign will want it, and an empty enum is the honest
543/// way to say the campaign is currently suppressing nothing.
544#[derive(Debug, Clone, Copy, PartialEq, Eq)]
545#[non_exhaustive]
546pub enum Known {}
547
548/// Every defect [`run`] steps over.
549pub const KNOWN_DEFECTS: [Known; 0] = [];
550
551/// Drive a program and check it, stepping over [`KNOWN_DEFECTS`].
552///
553/// What the fuzz target calls. Never panics on a violation: it returns them, so the target can
554/// panic with a report and a regression test can assert on the list.
555#[must_use]
556pub fn run(program: &Program) -> Run {
557    run_with(program, &KNOWN_DEFECTS)
558}
559
560/// Drive a program and check it, suppressing nothing.
561///
562/// What the regression tests for the known defects call, so that "known" never quietly becomes
563/// "invisible".
564#[must_use]
565pub fn run_strict(program: &Program) -> Run {
566    run_with(program, &[])
567}
568
569/// Drive a program and check it, suppressing the named defects.
570#[must_use]
571pub fn run_with(program: &Program, suppressed: &[Known]) -> Run {
572    let mut driver = Driver {
573        layer: TransactionLayer::new(Timers::default()),
574        tracked: Vec::new(),
575        trace: Vec::new(),
576        violations: Vec::new(),
577        suppressed: suppressed.to_vec(),
578    };
579
580    for (step, event) in program.events.iter().enumerate() {
581        driver.step(step, *event);
582        driver.check_states(step);
583        driver.check_bound(step);
584    }
585    driver.drive_to_quiescence();
586
587    Run {
588        trace: driver.trace,
589        violations: driver.violations,
590    }
591}
592
593impl Driver {
594    fn step(&mut self, step: usize, event: Event) {
595        match event {
596            Event::SendRequest {
597                slot,
598                method: m,
599                reliable,
600            } => self.send_request(step, slot, m, reliable),
601            Event::ReceiveRequest {
602                slot,
603                method: m,
604                reliable,
605                to_tag,
606            } => self.receive_request(step, slot, m, reliable, to_tag),
607            Event::ReceiveResponse {
608                slot,
609                method: m,
610                status,
611                to_tag,
612            } => self.receive_response(step, slot, m, status, to_tag),
613            Event::SendResponse {
614                slot,
615                method: m,
616                status,
617                to_tag,
618            } => self.send_response(step, slot, m, status, to_tag),
619            Event::FireTimer { key, timer, any } => self.fire_timer(step, key, timer, any),
620            Event::TransportError { key } => self.transport_error(step, key),
621            Event::Abandon { key } => self.abandon(step, key),
622        }
623    }
624
625    // -- the events ------------------------------------------------------------------------
626
627    fn send_request(&mut self, step: usize, slot: u8, method_index: u8, reliable: bool) {
628        let m = method(method_index);
629        let request = build_request(Space::Client, slot, &m, None);
630        let reliability = reliability(reliable);
631        let Some((key, outputs)) = self.layer.send_request(request, reliability) else {
632            self.record(
633                step,
634                &format!("SendRequest({}) no key", label(slot, &m)),
635                "",
636            );
637            return;
638        };
639        let index = self.track(key, Space::Client, slot, &m, m == Method::Invite);
640        let outcome = format!(
641            "created client {} {}",
642            self.tracked
643                .get(index)
644                .map_or_else(String::new, |t| t.label.clone()),
645            self.apply(index, &outputs)
646        );
647        self.record(
648            step,
649            &format!(
650                "SendRequest({} {})",
651                label(slot, &m),
652                transport_name(reliable)
653            ),
654            &outcome,
655        );
656        self.check_termination(step, index, &outputs);
657    }
658
659    fn receive_request(
660        &mut self,
661        step: usize,
662        slot: u8,
663        method_index: u8,
664        reliable: bool,
665        to_tag: u8,
666    ) {
667        let m = method(method_index);
668        let request = build_request(Space::Server, slot, &m, Some(to_tag));
669        let dispatch = self
670            .layer
671            .receive(Message::Request(request), reliability(reliable));
672        let outcome = self.absorb(step, dispatch, Space::Server, slot, &m);
673        self.record(
674            step,
675            &format!(
676                "ReceiveRequest({} {})",
677                label(slot, &m),
678                transport_name(reliable)
679            ),
680            &outcome,
681        );
682    }
683
684    fn receive_response(
685        &mut self,
686        step: usize,
687        slot: u8,
688        method_index: u8,
689        status_index: u8,
690        to_tag: u8,
691    ) {
692        let m = method(method_index);
693        let status = status(status_index);
694        // The response carries the branch and CSeq of the request a `SendRequest` for this slot
695        // and method would have produced, which is what makes the matching claim below checkable.
696        let request = build_request(Space::Client, slot, &m, None);
697        let expected = TransactionKey::from_sent_request(&request);
698        let live = expected
699            .as_ref()
700            .and_then(|key| self.layer.client_state(key))
701            .is_some();
702
703        let response = build_response(&request, status, to_tag);
704        let dispatch = self
705            .layer
706            .receive(Message::Response(response), Reliability::Unreliable);
707        let matched = matches!(dispatch, Dispatch::Matched { .. });
708        let outcome = self.absorb(step, dispatch, Space::Client, slot, &m);
709
710        // The property holds on every slot, legacy ones included: §17.1.3 keys a client
711        // transaction on the branch and the `CSeq` method, which a response carries whether or
712        // not the branch has a magic cookie. Legacy slots used to be excluded here, under
713        // `Known::LegacyClientResponseMatching`, until `S-26` gave the client key its own
714        // derivation.
715        //
716        // A new suppression is consulted here: `if live && !matched && !self.suppressed.contains(…)`.
717        if live && !matched {
718            self.violate(
719                step,
720                Invariant::UnroutableResponse,
721                format!(
722                    "a {} response for {} matched nothing, but its client transaction is live",
723                    status.code(),
724                    label(slot, &m)
725                ),
726            );
727        }
728
729        self.record(
730            step,
731            &format!("ReceiveResponse({} {})", label(slot, &m), status.code()),
732            &outcome,
733        );
734    }
735
736    fn send_response(
737        &mut self,
738        step: usize,
739        slot: u8,
740        method_index: u8,
741        status_index: u8,
742        to_tag: u8,
743    ) {
744        let m = method(method_index);
745        let status = status(status_index);
746        let request = build_request(Space::Server, slot, &m, Some(to_tag));
747        let Some(key) = TransactionKey::from_request(&request) else {
748            self.record(
749                step,
750                &format!("SendResponse({}) no key", label(slot, &m)),
751                "",
752            );
753            return;
754        };
755        let response = build_response(&request, status, to_tag);
756        let outputs = self.layer.send_response(&key, response);
757        let outcome = match self.index_of(&key) {
758            Some(index) => {
759                let label = self
760                    .tracked
761                    .get(index)
762                    .map_or_else(String::new, |t| t.label.clone());
763                format!("{label} {}", self.apply(index, &outputs))
764            }
765            None => format!("no transaction store={}", store(&self.layer)),
766        };
767        self.record(
768            step,
769            &format!("SendResponse({} {})", label(slot, &m), status.code()),
770            &outcome,
771        );
772        if let Some(index) = self.index_of(&key) {
773            self.check_termination(step, index, &outputs);
774        }
775    }
776
777    fn fire_timer(&mut self, step: usize, key_index: u8, timer_index: u8, any: bool) {
778        if self.tracked.is_empty() {
779            self.record(step, "FireTimer(no keys)", "");
780            return;
781        }
782        let index = key_index as usize % self.tracked.len();
783        let Some(entry) = self.tracked.get(index) else {
784            return;
785        };
786        let armed = entry.armed.clone();
787        let key = entry.key.clone();
788        let label = entry.label.clone();
789        let space = entry.space;
790
791        let timer = if any || armed.is_empty() {
792            *TIMERS
793                .get(timer_index as usize % TIMERS.len())
794                .expect("the timer index is taken modulo the table")
795        } else {
796            *armed
797                .get(timer_index as usize % armed.len())
798                .expect("the armed index is taken modulo a non-empty list")
799        };
800
801        let live = self.is_live(space, &key);
802        let before = self.layer.len();
803        // A timer that fires is spent. Whatever the machine wants next it re-arms in its outputs.
804        if let Some(entry) = self.tracked.get_mut(index) {
805            entry.armed.retain(|t| *t != timer);
806        }
807        let outputs = self.layer.on_timer(&key, timer);
808        let after = self.layer.len();
809
810        if !live && (!outputs.is_empty() || before != after) {
811            self.violate(
812                step,
813                Invariant::TimerForRemovedKey,
814                format!(
815                    "timer {timer:?} for {label}, whose transaction is gone, produced \
816                     {} output(s) and left the store at {after:?} (was {before:?})",
817                    outputs.len()
818                ),
819            );
820        }
821
822        let outcome = format!("{label} {}", self.apply(index, &outputs));
823        self.record(
824            step,
825            &format!(
826                "FireTimer({label} {timer:?}{})",
827                if live { "" } else { " stale" }
828            ),
829            &outcome,
830        );
831        self.check_termination(step, index, &outputs);
832    }
833
834    fn transport_error(&mut self, step: usize, key_index: u8) {
835        if self.tracked.is_empty() {
836            self.record(step, "TransportError(no keys)", "");
837            return;
838        }
839        let index = key_index as usize % self.tracked.len();
840        let Some(entry) = self.tracked.get(index) else {
841            return;
842        };
843        let key = entry.key.clone();
844        let label = entry.label.clone();
845        let outputs = self.layer.on_transport_error(&key);
846        let outcome = format!("{label} {}", self.apply(index, &outputs));
847        self.record(step, &format!("TransportError({label})"), &outcome);
848        self.check_termination(step, index, &outputs);
849    }
850
851    fn abandon(&mut self, step: usize, key_index: u8) {
852        if self.tracked.is_empty() {
853            self.record(step, "Abandon(no keys)", "");
854            return;
855        }
856        let index = key_index as usize % self.tracked.len();
857        let Some(entry) = self.tracked.get(index) else {
858            return;
859        };
860        let key = entry.key.clone();
861        let label = entry.label.clone();
862        let gone = self.layer.abandon(&key);
863        if let Some(entry) = self.tracked.get_mut(index).filter(|_| gone) {
864            entry.armed.clear();
865        }
866        self.record(
867            step,
868            &format!("Abandon({label})"),
869            &format!(
870                "{} store={}",
871                if gone { "dropped" } else { "absent" },
872                store(&self.layer)
873            ),
874        );
875    }
876
877    // -- bookkeeping -----------------------------------------------------------------------
878
879    /// Fold a dispatch into the model, returning its summary for the trace.
880    fn absorb(
881        &mut self,
882        step: usize,
883        dispatch: Dispatch,
884        space: Space,
885        slot: u8,
886        m: &Method,
887    ) -> String {
888        match dispatch {
889            Dispatch::Created { key, outputs } => {
890                let index = self.track(key, space, slot, m, m == &Method::Invite);
891                let label = self
892                    .tracked
893                    .get(index)
894                    .map_or_else(String::new, |t| t.label.clone());
895                let summary = format!("created server {label} {}", self.apply(index, &outputs));
896                self.check_termination(step, index, &outputs);
897                summary
898            }
899            Dispatch::Matched { key, outputs } => match self.index_of(&key) {
900                Some(index) => {
901                    let label = self
902                        .tracked
903                        .get(index)
904                        .map_or_else(String::new, |t| t.label.clone());
905                    let summary = format!("matched {label} {}", self.apply(index, &outputs));
906                    self.check_termination(step, index, &outputs);
907                    summary
908                }
909                None => format!("matched untracked {}", summarise(&outputs)),
910            },
911            Dispatch::Unmatched(_) => format!("unmatched store={}", store(&self.layer)),
912        }
913    }
914
915    /// Record a key the layer has created, returning its index.
916    ///
917    /// A key can be created more than once — the layer's stores are maps, so a second
918    /// transaction under the same key replaces the first — in which case the entry is reset
919    /// rather than duplicated.
920    fn track(
921        &mut self,
922        key: TransactionKey,
923        space: Space,
924        slot: u8,
925        m: &Method,
926        invite: bool,
927    ) -> usize {
928        if let Some(index) = self.index_of(&key) {
929            if let Some(entry) = self.tracked.get_mut(index) {
930                entry.invite = invite;
931                entry.armed.clear();
932            }
933            return index;
934        }
935        self.tracked.push(Tracked {
936            key,
937            space,
938            invite,
939            armed: Vec::new(),
940            label: format!("{}{slot}/{}", space.letter(), method_name(m)),
941        });
942        self.tracked.len() - 1
943    }
944
945    fn index_of(&self, key: &TransactionKey) -> Option<usize> {
946        self.tracked.iter().position(|t| &t.key == key)
947    }
948
949    /// Fold a batch of outputs into the armed-timer model and summarise it for the trace.
950    fn apply(&mut self, index: usize, outputs: &[Output]) -> String {
951        if let Some(entry) = self.tracked.get_mut(index) {
952            for output in outputs {
953                match output {
954                    Output::SetTimer { timer, .. } => {
955                        if !entry.armed.contains(timer) {
956                            entry.armed.push(*timer);
957                        }
958                    }
959                    Output::ClearTimer(timer) => entry.armed.retain(|t| t != timer),
960                    Output::Terminated(_) => entry.armed.clear(),
961                    Output::Send(_) | Output::ToTu(_) => {}
962                }
963            }
964        }
965        let state =
966            self.tracked
967                .get(index)
968                .map_or_else(String::new, |entry| match self.state_of(entry) {
969                    Some(state) => format!(" state={state}"),
970                    None => String::new(),
971                });
972        format!("{}{state} store={}", summarise(outputs), store(&self.layer))
973    }
974
975    // -- the invariants --------------------------------------------------------------------
976
977    /// No transaction outlives its terminal state.
978    fn check_termination(&mut self, step: usize, index: usize, outputs: &[Output]) {
979        let Some(entry) = self.tracked.get(index) else {
980            return;
981        };
982        if !outputs.iter().any(|o| matches!(o, Output::Terminated(_))) {
983            return;
984        }
985        let label = entry.label.clone();
986        let still_there = self.state_of(entry).is_some();
987        if still_there {
988            self.violate(
989                step,
990                Invariant::OutlivedTermination,
991                format!("{label} reported Terminated and is still in the store"),
992            );
993        }
994        if let Some(timer) = outputs.iter().find_map(|o| match o {
995            Output::SetTimer { timer, .. } => Some(*timer),
996            _ => None,
997        }) {
998            self.violate(
999                step,
1000                Invariant::OutlivedTermination,
1001                format!("{label} armed timer {timer:?} in the batch that retired it"),
1002            );
1003        }
1004    }
1005
1006    /// No state is reachable that the §17 tables do not name for that machine.
1007    ///
1008    /// The two non-INVITE arms name the same three states today and are kept apart anyway: each
1009    /// is a different table in `docs/specs/sip-transaction.md`, and collapsing them would mean
1010    /// the next amendment to one of them silently changed the other.
1011    #[allow(clippy::match_same_arms)]
1012    fn check_states(&mut self, step: usize) {
1013        let mut found = Vec::new();
1014        for entry in &self.tracked {
1015            let Some(state) = self.state_of(entry) else {
1016                continue;
1017            };
1018            let legal = match (entry.space, entry.invite) {
1019                // §4.1: the INVITE client machine never waits in Trying — that is the
1020                // non-INVITE machine's state — and never reaches Terminated observably.
1021                (Space::Client, true) => {
1022                    ["Calling", "Proceeding", "Completed", "Accepted"].as_slice()
1023                }
1024                // §4.2: no Calling, and no Accepted — RFC 6026 adds Accepted to the INVITE
1025                // machines only, because only a 2xx to an INVITE can arrive twice.
1026                (Space::Client, false) => ["Trying", "Proceeding", "Completed"].as_slice(),
1027                // §4.3: an INVITE server transaction starts in Proceeding, never Trying.
1028                (Space::Server, true) => {
1029                    ["Proceeding", "Completed", "Confirmed", "Accepted"].as_slice()
1030                }
1031                // §4.4: no Confirmed and no Accepted; there is no ACK to wait for.
1032                (Space::Server, false) => ["Trying", "Proceeding", "Completed"].as_slice(),
1033            };
1034            if !legal.contains(&state.as_str()) {
1035                found.push(format!(
1036                    "{} is in {state}, which §17 does not name for {} machine",
1037                    entry.label,
1038                    machine_name(entry.space, entry.invite)
1039                ));
1040            }
1041        }
1042        for detail in found {
1043            self.violate(step, Invariant::UnnamedState, detail);
1044        }
1045    }
1046
1047    /// The store is bounded by the vocabulary, not by the program.
1048    fn check_bound(&mut self, step: usize) {
1049        let (client, server) = self.layer.len();
1050        let live = client + server;
1051        if live > self.tracked.len() {
1052            self.violate(
1053                step,
1054                Invariant::StoreGrowth,
1055                format!(
1056                    "{live} transactions in flight, but the layer has only reported \
1057                     {} keys",
1058                    self.tracked.len()
1059                ),
1060            );
1061        }
1062    }
1063
1064    /// Drive every timer the layer asked for until nothing is left to fire, then say what
1065    /// survived.
1066    ///
1067    /// The only transactions RFC 3261 leaves without a timer of their own are the ones waiting
1068    /// on the application: a server transaction the TU has not answered (§17.2.2, and §17.2.1
1069    /// once the 100 has gone out), and an INVITE client transaction in Proceeding, whose
1070    /// Timer B is cancelled by the first provisional on purpose (§17.1.1.2). Anything else
1071    /// still in the store after quiescence is a transaction nothing will ever retire, which is
1072    /// the slow quiet outage this invariant is about.
1073    fn drive_to_quiescence(&mut self) {
1074        for _ in 0..DRAIN_ROUNDS {
1075            let mut fired = false;
1076            for index in 0..self.tracked.len() {
1077                let Some(entry) = self.tracked.get(index) else {
1078                    continue;
1079                };
1080                if self.state_of(entry).is_none() {
1081                    continue;
1082                }
1083                let key = entry.key.clone();
1084                let armed = entry.armed.clone();
1085                for timer in armed {
1086                    if let Some(entry) = self.tracked.get_mut(index) {
1087                        entry.armed.retain(|t| *t != timer);
1088                    }
1089                    let outputs = self.layer.on_timer(&key, timer);
1090                    let _ = self.apply(index, &outputs);
1091                    fired = true;
1092                }
1093            }
1094            if !fired {
1095                break;
1096            }
1097        }
1098
1099        let mut found = Vec::new();
1100        for entry in &self.tracked {
1101            let Some(state) = self.state_of(entry) else {
1102                continue;
1103            };
1104            let waiting_on_the_application = match entry.space {
1105                Space::Server => state == "Trying" || state == "Proceeding",
1106                Space::Client => entry.invite && state == "Proceeding",
1107            };
1108            if !waiting_on_the_application {
1109                found.push(format!(
1110                    "{} is still in {state} with every timer fired; nothing will retire it",
1111                    entry.label
1112                ));
1113            }
1114        }
1115        for detail in found {
1116            self.violate(usize::MAX, Invariant::StoreGrowth, detail);
1117        }
1118        self.trace.push(format!(
1119            "quiescent store={} live={:?}",
1120            store(&self.layer),
1121            self.tracked
1122                .iter()
1123                .filter_map(|entry| self.state_of(entry).map(|s| format!("{}={s}", entry.label)))
1124                .collect::<Vec<_>>()
1125        ));
1126    }
1127
1128    // -- helpers ---------------------------------------------------------------------------
1129
1130    fn state_of(&self, entry: &Tracked) -> Option<String> {
1131        match entry.space {
1132            Space::Client => self.layer.client_state(&entry.key).map(client_state_name),
1133            Space::Server => self.layer.server_state(&entry.key).map(server_state_name),
1134        }
1135    }
1136
1137    fn is_live(&self, space: Space, key: &TransactionKey) -> bool {
1138        match space {
1139            Space::Client => self.layer.client_state(key).is_some(),
1140            Space::Server => self.layer.server_state(key).is_some(),
1141        }
1142    }
1143
1144    fn record(&mut self, step: usize, event: &str, outcome: &str) {
1145        let mut line = String::new();
1146        let _ = write!(line, "{step:03} {event}");
1147        if !outcome.is_empty() {
1148            let _ = write!(line, " | {outcome}");
1149        }
1150        self.trace.push(line);
1151    }
1152
1153    fn violate(&mut self, step: usize, invariant: Invariant, detail: String) {
1154        self.violations.push(Violation {
1155            step,
1156            invariant,
1157            detail,
1158        });
1159    }
1160}
1161
1162// ------------------------------------------------------------------------------------------
1163// Building the messages
1164// ------------------------------------------------------------------------------------------
1165
1166fn reliability(reliable: bool) -> Reliability {
1167    if reliable {
1168        Reliability::Reliable
1169    } else {
1170        Reliability::Unreliable
1171    }
1172}
1173
1174fn transport_name(reliable: bool) -> &'static str {
1175    if reliable { "tcp" } else { "udp" }
1176}
1177
1178fn status(index: u8) -> StatusCode {
1179    let code = *STATUSES
1180        .get(index as usize % STATUSES.len())
1181        .expect("the status index is taken modulo the table");
1182    StatusCode::new(code).expect("the status table holds valid codes")
1183}
1184
1185/// The branch a slot's messages carry.
1186///
1187/// Slots below [`FIRST_LEGACY_SLOT`] carry the RFC 3261 magic cookie; the rest deliberately do
1188/// not, so the RFC 2543 fallback in `TransactionKey::Legacy` is reachable — those senders are
1189/// still on the public internet, and the RFC 4475 corpus has one.
1190fn branch(space: Space, slot: u8) -> String {
1191    if slot < FIRST_LEGACY_SLOT {
1192        format!("z9hG4bK-fz{}{slot}", space.letter())
1193    } else {
1194        format!("fz{}{slot}-rfc2543", space.letter())
1195    }
1196}
1197
1198fn build_request(space: Space, slot: u8, m: &Method, to_tag: Option<u8>) -> Request {
1199    let uri = Uri::sip(Host::Name(
1200        HostName::new("example.com").expect("a valid host"),
1201    ));
1202    let to = match to_tag.and_then(|index| TAGS.get(index as usize % TAGS.len())) {
1203        Some(tag) => format!("<sip:callee@example.com>;tag={tag}"),
1204        None => "<sip:callee@example.com>".to_owned(),
1205    };
1206    RequestBuilder::new(m.clone(), uri)
1207        .header(
1208            HeaderName::Via,
1209            Bytes::from(format!(
1210                "SIP/2.0/UDP host.example.net;branch={}",
1211                branch(space, slot)
1212            )),
1213        )
1214        .expect("a valid Via")
1215        .header(
1216            HeaderName::From,
1217            Bytes::from(format!("<sip:caller@example.net>;tag=f{slot}")),
1218        )
1219        .expect("a valid From")
1220        .header(HeaderName::To, Bytes::from(to))
1221        .expect("a valid To")
1222        .header(
1223            HeaderName::CallId,
1224            Bytes::from(format!("fuzz-{slot}@example.net")),
1225        )
1226        .expect("a valid Call-ID")
1227        .cseq(1, m)
1228        .expect("a valid CSeq")
1229        .max_forwards(70)
1230        .build()
1231}
1232
1233fn build_response(request: &Request, status: StatusCode, to_tag: u8) -> Response {
1234    let mut builder = ResponseBuilder::to_request(request, status, "Sequence")
1235        .expect("a response can be built for any request the harness makes");
1236    if !status.is_provisional() {
1237        // A UAS tags To on its first final response — replacing the header, not adding a
1238        // second one, which would make the response invalid.
1239        let tag = TAGS
1240            .get(to_tag as usize % TAGS.len())
1241            .expect("the tag index is taken modulo the table");
1242        builder = builder
1243            .set_header(
1244                &HeaderName::To,
1245                Bytes::from(format!("<sip:callee@example.com>;tag={tag}")),
1246            )
1247            .expect("a valid To");
1248    }
1249    builder.build()
1250}
1251
1252// ------------------------------------------------------------------------------------------
1253// Naming things, for the trace
1254// ------------------------------------------------------------------------------------------
1255
1256fn method_name(m: &Method) -> String {
1257    String::from_utf8_lossy(m.as_bytes()).into_owned()
1258}
1259
1260fn label(slot: u8, m: &Method) -> String {
1261    format!("{slot}/{}", method_name(m))
1262}
1263
1264fn machine_name(space: Space, invite: bool) -> &'static str {
1265    match (space, invite) {
1266        (Space::Client, true) => "the INVITE client",
1267        (Space::Client, false) => "the non-INVITE client",
1268        (Space::Server, true) => "the INVITE server",
1269        (Space::Server, false) => "the non-INVITE server",
1270    }
1271}
1272
1273fn client_state_name(state: ClientState) -> String {
1274    match state {
1275        ClientState::Calling => "Calling",
1276        ClientState::Trying => "Trying",
1277        ClientState::Proceeding => "Proceeding",
1278        ClientState::Completed => "Completed",
1279        ClientState::Accepted => "Accepted",
1280        ClientState::Terminated => "Terminated",
1281    }
1282    .to_owned()
1283}
1284
1285fn server_state_name(state: ServerState) -> String {
1286    match state {
1287        ServerState::Trying => "Trying",
1288        ServerState::Proceeding => "Proceeding",
1289        ServerState::Completed => "Completed",
1290        ServerState::Confirmed => "Confirmed",
1291        ServerState::Accepted => "Accepted",
1292        ServerState::Terminated => "Terminated",
1293    }
1294    .to_owned()
1295}
1296
1297fn store(layer: &TransactionLayer) -> String {
1298    let (client, server) = layer.len();
1299    format!("{client}/{server}")
1300}
1301
1302fn summarise(outputs: &[Output]) -> String {
1303    let mut parts = Vec::new();
1304    let sends = outputs
1305        .iter()
1306        .filter(|o| matches!(o, Output::Send(_)))
1307        .count();
1308    if sends > 0 {
1309        parts.push(format!("send={sends}"));
1310    }
1311    let events: Vec<&str> = outputs
1312        .iter()
1313        .filter_map(|o| match o {
1314            Output::ToTu(event) => Some(match event.as_ref() {
1315                TuEvent::Request(_) => "request",
1316                TuEvent::Response(_) => "response",
1317                TuEvent::Ack(_) => "ack",
1318                TuEvent::Timeout => "timeout",
1319                TuEvent::TransportError => "transport-error",
1320            }),
1321            _ => None,
1322        })
1323        .collect();
1324    if !events.is_empty() {
1325        parts.push(format!("tu=[{}]", events.join(",")));
1326    }
1327    let set: Vec<String> = outputs
1328        .iter()
1329        .filter_map(|o| match o {
1330            Output::SetTimer { timer, .. } => Some(format!("{timer:?}")),
1331            _ => None,
1332        })
1333        .collect();
1334    if !set.is_empty() {
1335        parts.push(format!("set=[{}]", set.join(",")));
1336    }
1337    let cleared: Vec<String> = outputs
1338        .iter()
1339        .filter_map(|o| match o {
1340            Output::ClearTimer(timer) => Some(format!("{timer:?}")),
1341            _ => None,
1342        })
1343        .collect();
1344    if !cleared.is_empty() {
1345        parts.push(format!("clear=[{}]", cleared.join(",")));
1346    }
1347    if let Some(reason) = outputs.iter().find_map(|o| match o {
1348        Output::Terminated(reason) => Some(*reason),
1349        _ => None,
1350    }) {
1351        parts.push(format!("terminated={reason:?}"));
1352    }
1353    if parts.is_empty() {
1354        "absorbed".to_owned()
1355    } else {
1356        parts.join(" ")
1357    }
1358}
1359
1360// ------------------------------------------------------------------------------------------
1361// The seed corpus
1362// ------------------------------------------------------------------------------------------
1363
1364/// A named seed program, committed to the corpus as the bytes it encodes to.
1365#[derive(Debug, Clone)]
1366pub struct Seed {
1367    /// The corpus file's name.
1368    pub name: &'static str,
1369    /// The program.
1370    pub program: Program,
1371}
1372
1373/// Where the committed seed corpus lives, relative to the repository root.
1374pub const CORPUS_PATH: &str = "crates/sipx-testkit/corpus/transaction-sequences";
1375
1376/// The committed seed corpus directory.
1377#[must_use]
1378pub fn corpus_dir() -> PathBuf {
1379    Path::new(env!("CARGO_MANIFEST_DIR"))
1380        .join("corpus")
1381        .join("transaction-sequences")
1382}
1383
1384/// Write [`seeds`] to [`corpus_dir`], one file per seed.
1385///
1386/// The corpus is generated rather than hand-written so its provenance is reproducible, the way
1387/// `scripts/import-rfc4475-corpus.sh` makes the parser corpus reproducible from the RFC. What
1388/// makes that worth anything is the test on the other side:
1389/// `the_committed_corpus_is_exactly_the_seed_programs` fails if the two ever disagree.
1390pub fn write_corpus() -> std::io::Result<usize> {
1391    let dir = corpus_dir();
1392    std::fs::create_dir_all(&dir)?;
1393    let seeds = seeds();
1394    for seed in &seeds {
1395        std::fs::write(dir.join(seed.name), seed.program.encode())?;
1396    }
1397    Ok(seeds.len())
1398}
1399
1400/// The programs the corpus is seeded from.
1401///
1402/// Seeding from nothing means the first minutes of every campaign are spent rediscovering that
1403/// a response has to follow a request. These are the scenarios of
1404/// `docs/specs/sip-transaction.md` §7 — the rows the FSM table tests already walk — rewritten
1405/// as event programs, which is the same trick CI plays on the parser targets by seeding them
1406/// with the RFC 4475 corpus. The fuzzer starts from behaviour that reaches every machine and
1407/// mutates outwards from there.
1408///
1409/// One long function on purpose: it is a table of scenarios, and a table reads best as a table.
1410#[must_use]
1411#[allow(clippy::too_many_lines)]
1412pub fn seeds() -> Vec<Seed> {
1413    // Slots 0 and 1 carry the magic cookie; 2 and 3 do not.
1414    const INVITE: u8 = 0;
1415    const ACK: u8 = 1;
1416    const BYE: u8 = 2;
1417    const CANCEL: u8 = 3;
1418    const REGISTER: u8 = 4;
1419    const OPTIONS: u8 = 5;
1420
1421    // Indices into STATUSES.
1422    const S100: u8 = 0;
1423    const S180: u8 = 1;
1424    const S200: u8 = 2;
1425    const S486: u8 = 5;
1426    const S500: u8 = 6;
1427
1428    /// Fire a named timer for the key at `key`, whether or not the model thinks it is armed.
1429    fn timer(key: u8, which: Timer) -> Event {
1430        // `timer_row`, not a `position().expect()`: a variant missing from the table is a compile
1431        // error at the match rather than a panic here, and this is what keeps the row function
1432        // load-bearing rather than decoration.
1433        let index =
1434            u8::try_from(timer_row(which)).expect("the timer table is shorter than 256 entries");
1435        Event::FireTimer {
1436            key,
1437            timer: index,
1438            any: true,
1439        }
1440    }
1441    fn send(slot: u8, method: u8) -> Event {
1442        Event::SendRequest {
1443            slot,
1444            method,
1445            reliable: false,
1446        }
1447    }
1448    fn recv(slot: u8, method: u8) -> Event {
1449        Event::ReceiveRequest {
1450            slot,
1451            method,
1452            reliable: false,
1453            to_tag: 0,
1454        }
1455    }
1456    fn answer(slot: u8, method: u8, status: u8) -> Event {
1457        Event::SendResponse {
1458            slot,
1459            method,
1460            status,
1461            to_tag: 0,
1462        }
1463    }
1464    fn reply(slot: u8, method: u8, status: u8, to_tag: u8) -> Event {
1465        Event::ReceiveResponse {
1466            slot,
1467            method,
1468            status,
1469            to_tag,
1470        }
1471    }
1472
1473    let mut seeds = Vec::new();
1474    let mut seed = |name: &'static str, events: Vec<Event>| {
1475        seeds.push(Seed {
1476            name,
1477            program: Program { events },
1478        });
1479    };
1480
1481    // §7 T1: retransmission with doubling intervals, then the timeout.
1482    seed(
1483        "t1-invite-client-retransmits-then-times-out",
1484        vec![
1485            send(0, INVITE),
1486            timer(0, Timer::A),
1487            timer(0, Timer::A),
1488            timer(0, Timer::A),
1489            timer(0, Timer::B),
1490        ],
1491    );
1492    // §7 T2: a non-2xx is acknowledged by the transaction, which then waits out Timer D.
1493    seed(
1494        "t2-invite-client-acks-a-non-2xx",
1495        vec![
1496            send(0, INVITE),
1497            reply(0, INVITE, S486, 0),
1498            reply(0, INVITE, S486, 0),
1499            timer(0, Timer::D),
1500        ],
1501    );
1502    // §7 T3 and T4: a 2xx is not acknowledged here, and a fork's second 2xx still finds a
1503    // transaction to arrive at (RFC 6026).
1504    seed(
1505        "t3-invite-client-2xx-is-not-acked-and-a-fork-answers-twice",
1506        vec![
1507            send(0, INVITE),
1508            reply(0, INVITE, S200, 0),
1509            reply(0, INVITE, S200, 1),
1510            timer(0, Timer::M),
1511        ],
1512    );
1513    // A provisional cancels Timer B on purpose: the callee may ring for longer than 64·T1.
1514    seed(
1515        "invite-client-waits-in-proceeding-with-no-timeout",
1516        vec![
1517            send(0, INVITE),
1518            reply(0, INVITE, S180, 0),
1519            timer(0, Timer::B),
1520            reply(0, INVITE, S200, 0),
1521            timer(0, Timer::M),
1522        ],
1523    );
1524    // §4.2: Timer E backs off to T2 and Timer F retires the machine from Trying.
1525    seed(
1526        "non-invite-client-backs-off-then-times-out",
1527        vec![
1528            send(1, OPTIONS),
1529            timer(0, Timer::E),
1530            timer(0, Timer::E),
1531            timer(0, Timer::E),
1532            timer(0, Timer::F),
1533        ],
1534    );
1535    seed(
1536        "non-invite-client-completes-on-a-final-response",
1537        vec![
1538            send(1, BYE),
1539            reply(1, BYE, S200, 0),
1540            reply(1, BYE, S200, 0),
1541            timer(0, Timer::K),
1542        ],
1543    );
1544    // §7 T5 and T6: retransmissions are absorbed, and answered from the last response.
1545    seed(
1546        "t5-server-absorbs-request-retransmissions",
1547        vec![
1548            recv(0, REGISTER),
1549            recv(0, REGISTER),
1550            answer(0, REGISTER, S200),
1551            recv(0, REGISTER),
1552            timer(0, Timer::J),
1553        ],
1554    );
1555    // §7 T7 and T11: the transaction answers 100 itself, then a non-2xx, then absorbs the ACK.
1556    seed(
1557        "t7-invite-server-sends-100-then-absorbs-the-ack",
1558        vec![
1559            recv(0, INVITE),
1560            timer(0, Timer::Trying100),
1561            answer(0, INVITE, S486),
1562            timer(0, Timer::G),
1563            recv(0, ACK),
1564            timer(0, Timer::I),
1565        ],
1566    );
1567    // §7 T8 and T12: the TU answers first, so no 100 goes out, and the ACK for the 2xx is the
1568    // TU's business (RFC 6026).
1569    seed(
1570        "t8-invite-server-2xx-hands-the-ack-to-the-tu",
1571        vec![
1572            recv(0, INVITE),
1573            answer(0, INVITE, S180),
1574            timer(0, Timer::Trying100),
1575            answer(0, INVITE, S200),
1576            recv(0, ACK),
1577            timer(0, Timer::L),
1578        ],
1579    );
1580    // Timer H: no ACK ever comes.
1581    seed(
1582        "invite-server-times-out-waiting-for-an-ack",
1583        vec![
1584            recv(0, INVITE),
1585            answer(0, INVITE, S500),
1586            timer(0, Timer::G),
1587            timer(0, Timer::H),
1588        ],
1589    );
1590    // §7 T9: a reliable transport sets no retransmission timers and its absorption timers fire
1591    // at once.
1592    seed(
1593        "t9-reliable-transport-sets-no-retransmission-timers",
1594        vec![
1595            Event::SendRequest {
1596                slot: 0,
1597                method: INVITE,
1598                reliable: true,
1599            },
1600            reply(0, INVITE, S486, 0),
1601            timer(0, Timer::D),
1602            Event::ReceiveRequest {
1603                slot: 1,
1604                method: OPTIONS,
1605                reliable: true,
1606                to_tag: 0,
1607            },
1608            answer(1, OPTIONS, S200),
1609            timer(1, Timer::J),
1610        ],
1611    );
1612    // §7 T13: a sender from before the magic cookie meant anything is matched by the legacy key.
1613    seed(
1614        "t13-legacy-branch-matching",
1615        vec![
1616            recv(2, OPTIONS),
1617            recv(2, OPTIONS),
1618            answer(2, OPTIONS, S200),
1619            recv(2, OPTIONS),
1620            timer(0, Timer::J),
1621        ],
1622    );
1623    // §7 T14: a CANCEL names the INVITE's branch but runs in a transaction of its own.
1624    seed(
1625        "t14-cancel-runs-in-its-own-transaction",
1626        vec![
1627            recv(0, INVITE),
1628            recv(0, CANCEL),
1629            answer(0, CANCEL, S200),
1630            answer(0, INVITE, S486),
1631            recv(0, ACK),
1632            timer(0, Timer::I),
1633            timer(1, Timer::J),
1634        ],
1635    );
1636    // The transport failing, which no message can express.
1637    seed(
1638        "transport-error-terminates-and-tells-the-tu",
1639        vec![send(0, OPTIONS), Event::TransportError { key: 0 }],
1640    );
1641    // A server transaction the application never answered, dropped by the driver — §17.2.2
1642    // gives it no timer, so nothing else would.
1643    seed(
1644        "abandon-a-server-transaction-the-application-never-answered",
1645        vec![recv(1, REGISTER), Event::Abandon { key: 0 }],
1646    );
1647    // Timers arriving after the transaction has gone: the race a driver's timer wheel cannot
1648    // close, and what `Invariant::TimerForRemovedKey` is about.
1649    seed(
1650        "stale-timers-fire-after-the-transaction-is-gone",
1651        vec![
1652            send(1, OPTIONS),
1653            reply(1, OPTIONS, S200, 0),
1654            timer(0, Timer::K),
1655            timer(0, Timer::K),
1656            timer(0, Timer::F),
1657            timer(0, Timer::E),
1658        ],
1659    );
1660    // A provisional to a non-INVITE client, which keeps Timer F running — the asymmetry with
1661    // Timer B that §17.1.2.2 states outright.
1662    seed(
1663        "non-invite-client-times-out-from-proceeding-too",
1664        vec![
1665            send(1, REGISTER),
1666            reply(1, REGISTER, S100, 0),
1667            timer(0, Timer::E),
1668            timer(0, Timer::F),
1669        ],
1670    );
1671
1672    seeds
1673}