umsh_node_mgmt/
admin.rs

1//! The administrator side of an exchange: token choice, retransmission
2//! pacing, and the cursor continuation loop.
3//!
4//! The engine performs no I/O. It hands the caller payloads to send and
5//! reads the payloads that come back; the caller owns the transport, the
6//! clock, and the buffer the read reassembles into.
7//!
8//! One [`Exchange`] serves a whole operation, which may span several
9//! request/response round trips when a read does not fit one payload.
10//! An administrator MUST NOT have more than one exchange outstanding
11//! with a given device, so one of these per device is the whole
12//! bookkeeping.
13
14use umsh_ulcp::frame::{Cmd, Frame};
15use umsh_ulcp::status::Status;
16
17use crate::envelope::{Envelope, EnvelopeError, Token};
18
19/// How long to wait for a response before retransmitting, matching the
20/// request pacing `umsh-text` settled on for the same mesh.
21pub const RETRY_MS: u64 = 8_000;
22
23/// Retransmissions before an exchange is abandoned. Four attempts across
24/// half a minute is long enough for a multi-hop path to recover and
25/// short enough that a person waiting on it gets an answer.
26pub const MAX_ATTEMPTS: u32 = 4;
27
28/// How an exchange ended.
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub enum Failure {
31    /// Every attempt went unanswered.
32    TimedOut,
33    /// A response carried a critical option this implementation does not
34    /// recognize, so what it says cannot be trusted.
35    UnknownCriticalOption(u16),
36    /// A response could not be read as an envelope, or its frame could
37    /// not be parsed.
38    Malformed,
39    /// A continued read produced more than the reassembly buffer holds.
40    TooLarge,
41    /// The device refused the cursor. The caller restarts the read from
42    /// a cursor-less request.
43    CursorInvalid,
44}
45
46/// What the caller should do next.
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
48pub enum Step {
49    /// Send `out[..len]` as a Node Management Request payload.
50    Send { len: usize },
51    /// Nothing to do until `deadline_ms`, when [`Exchange::poll`] will
52    /// have another attempt to hand out.
53    Wait { deadline_ms: u64 },
54    /// The exchange is over.
55    Done(Outcome),
56}
57
58/// A finished exchange.
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
60pub enum Outcome {
61    /// The device answered. The reply frame is the reassembly buffer's
62    /// first `len` octets — one whole ULCP frame, its trailing content
63    /// the concatenation of every fragment.
64    Replied { len: usize },
65    /// A reset-class command, which is answered by no response payload.
66    /// Delivery is confirmed by the MAC acknowledgment, not here.
67    NoResponse,
68    /// The exchange failed.
69    Failed(Failure),
70}
71
72#[derive(Clone, Copy, PartialEq, Eq)]
73enum State {
74    /// Waiting for a response to the attempt now outstanding.
75    Awaiting {
76        deadline_ms: u64,
77        attempts: u32,
78    },
79    Finished(Outcome),
80}
81
82/// A buffer a continued read reassembles into.
83///
84/// The caller owns the storage, so a device firmware can put it in a
85/// static and a host can put it on the heap.
86pub struct Reassembly<'a> {
87    buf: &'a mut [u8],
88    /// Octets of the reply frame written so far: the frame's header and
89    /// its first fragment, then each further fragment's trailing content
90    /// appended.
91    len: usize,
92}
93
94impl<'a> Reassembly<'a> {
95    pub fn new(buf: &'a mut [u8]) -> Self {
96        Self { buf, len: 0 }
97    }
98
99    /// Take up a reassembly whose position the caller kept.
100    ///
101    /// A caller that cannot hold the borrow between exchanges — anything
102    /// awaiting a radio round trip — keeps the storage and the length it
103    /// reached, and hands both back for the next fragment.
104    pub fn resume(buf: &'a mut [u8], len: usize) -> Self {
105        let len = len.min(buf.len());
106        Self { buf, len }
107    }
108
109    /// The reply frame assembled so far.
110    pub fn frame(&self) -> &[u8] {
111        &self.buf[..self.len]
112    }
113
114    /// Octets assembled so far.
115    pub fn len(&self) -> usize {
116        self.len
117    }
118
119    /// Whether no fragment has been taken up yet.
120    pub fn is_empty(&self) -> bool {
121        self.len == 0
122    }
123
124    /// Discard what has been assembled, so the storage can serve
125    /// another exchange.
126    pub fn clear(&mut self) {
127        self.len = 0;
128    }
129
130    fn append(&mut self, bytes: &[u8]) -> bool {
131        let Some(end) = self.len.checked_add(bytes.len()) else {
132            return false;
133        };
134        if end > self.buf.len() {
135            return false;
136        }
137        self.buf[self.len..end].copy_from_slice(bytes);
138        self.len = end;
139        true
140    }
141}
142
143/// One operation against one device.
144pub struct Exchange<const REQUEST: usize> {
145    /// The request being sent, envelope excluded: one ULCP frame.
146    frame: [u8; REQUEST],
147    frame_len: usize,
148    /// The cursor to present with the next request, once a fragment has
149    /// asked for one.
150    cursor: [u8; crate::envelope::CURSOR_MAX],
151    cursor_len: usize,
152    token: Token,
153    /// Advances with each new exchange in the continuation loop, which
154    /// is how the next token differs from the previous one.
155    counter: u16,
156    /// A reset-class command is complete when it has been sent; there is
157    /// no response to wait for.
158    resets: bool,
159    /// The most recent REMAINING the device offered, for progress
160    /// reporting.
161    remaining: Option<u32>,
162    state: State,
163}
164
165impl<const REQUEST: usize> Exchange<REQUEST> {
166    /// Begin an operation carrying `frame`.
167    ///
168    /// `seed` picks the first token. A device retains its answer to every
169    /// recent token against retransmission, and answers a reused token
170    /// with the retained response instead of executing — so the seed must
171    /// come from above [`Exchange::counter`] of every exchange the device
172    /// may still remember: a counter carried across exchanges, itself
173    /// seeded unpredictably.
174    pub fn new(frame: &[u8], seed: u16, now_ms: u64) -> Result<Self, Failure> {
175        if frame.len() > REQUEST {
176            return Err(Failure::TooLarge);
177        }
178        let resets = Frame::parse(frame)
179            .ok()
180            .and_then(|parsed| parsed.command())
181            .is_some_and(|cmd| {
182                matches!(
183                    cmd,
184                    Cmd::Reset | Cmd::Restore | Cmd::FactoryReset | Cmd::Reboot
185                )
186            });
187
188        let mut stored = [0u8; REQUEST];
189        stored[..frame.len()].copy_from_slice(frame);
190        Ok(Self {
191            frame: stored,
192            frame_len: frame.len(),
193            cursor: [0; crate::envelope::CURSOR_MAX],
194            cursor_len: 0,
195            token: seed.to_be_bytes(),
196            counter: seed,
197            resets,
198            remaining: None,
199            state: State::Awaiting {
200                deadline_ms: now_ms,
201                attempts: 0,
202            },
203        })
204    }
205
206    /// The token now outstanding. A response carrying any other token
207    /// belongs to someone else's exchange.
208    pub fn token(&self) -> Token {
209        self.token
210    }
211
212    /// The counter behind the last token this exchange issued.
213    ///
214    /// A continued read issues a fresh token per fragment, so an exchange
215    /// consumes a caller-invisible stretch of the counter space. The next
216    /// exchange's seed must come from above this value: the device holds
217    /// every answered token against retransmission, and a new request
218    /// under any of them is answered with the old response instead of
219    /// running.
220    pub fn counter(&self) -> u16 {
221        self.counter
222    }
223
224    /// The device's most recent estimate of octets not yet returned,
225    /// advisory and present only during a continued read.
226    pub fn remaining(&self) -> Option<u32> {
227        self.remaining
228    }
229
230    /// When the outstanding attempt stops waiting, or `None` once the
231    /// exchange has finished.
232    pub fn deadline_ms(&self) -> Option<u64> {
233        match self.state {
234            State::Awaiting { deadline_ms, .. } => Some(deadline_ms),
235            State::Finished(_) => None,
236        }
237    }
238
239    /// What to do now: send an attempt, wait for the deadline, or stop.
240    ///
241    /// The first call hands out the first attempt; later calls hand out
242    /// retransmissions of the identical request under the identical
243    /// token, which the device answers from its retained response rather
244    /// than executing again.
245    pub fn poll(&mut self, now_ms: u64, out: &mut [u8]) -> Step {
246        let (deadline_ms, attempts) = match self.state {
247            State::Finished(outcome) => return Step::Done(outcome),
248            State::Awaiting {
249                deadline_ms,
250                attempts,
251            } => (deadline_ms, attempts),
252        };
253
254        if now_ms < deadline_ms {
255            return Step::Wait { deadline_ms };
256        }
257        if attempts >= MAX_ATTEMPTS {
258            return self.finish(Outcome::Failed(Failure::TimedOut));
259        }
260
261        let mut request = Envelope::new(self.token, &self.frame[..self.frame_len]);
262        if self.cursor_len > 0 {
263            request = request.with_cursor(&self.cursor[..self.cursor_len]);
264        }
265        let Ok(len) = request.encode(out) else {
266            return self.finish(Outcome::Failed(Failure::TooLarge));
267        };
268
269        self.state = State::Awaiting {
270            deadline_ms: now_ms + RETRY_MS,
271            attempts: attempts + 1,
272        };
273        Step::Send { len }
274    }
275
276    /// A reset-class command has been delivered — the MAC acknowledged
277    /// it — and no response is coming.
278    ///
279    /// Calling this for anything else discards a reply that may still
280    /// arrive; the caller checks [`Exchange::expects_response`] first.
281    pub fn delivered(&mut self) -> Step {
282        self.finish(Outcome::NoResponse)
283    }
284
285    /// Whether a response is guaranteed. False for the reset-class
286    /// commands, whose completion is observed by reading state in a
287    /// later exchange rather than by a reply.
288    ///
289    /// One that arrives anyway is still accepted — `CMD_RESTORE` on a
290    /// device with no saved snapshot resets nothing and answers like any
291    /// other command — so this says when to wait on the acknowledgment
292    /// instead, not when to stop listening.
293    pub fn expects_response(&self) -> bool {
294        !self.resets
295    }
296
297    /// Feed an arriving Node Management Response payload, the payload
298    /// type byte already stripped.
299    ///
300    /// A payload whose token does not match the outstanding one is not
301    /// this exchange's; the caller discards it with accounting. Returns
302    /// `None` in that case, and otherwise what to do next — which for a
303    /// fragmented read is the next continuation to send.
304    pub fn receive(
305        &mut self,
306        payload: &[u8],
307        reassembly: &mut Reassembly<'_>,
308        now_ms: u64,
309        out: &mut [u8],
310    ) -> Option<Step> {
311        if matches!(self.state, State::Finished(_)) {
312            return None;
313        }
314
315        // The token leads the payload, so even a response whose options
316        // are unreadable can be attributed — and one that cannot be
317        // attributed is not this exchange's problem.
318        let &[token0, token1, ..] = payload else {
319            return None;
320        };
321        if [token0, token1] != self.token {
322            return None;
323        }
324
325        let response = match Envelope::parse(payload) {
326            Ok(response) => response,
327            Err(EnvelopeError::UnknownCritical(number)) => {
328                return Some(self.finish(Outcome::Failed(Failure::UnknownCriticalOption(number))));
329            }
330            Err(_) => return Some(self.finish(Outcome::Failed(Failure::Malformed))),
331        };
332
333        let Ok(parsed) = Frame::parse(response.frame) else {
334            return Some(self.finish(Outcome::Failed(Failure::Malformed)));
335        };
336
337        // A refused cursor ends the read: the position is gone, and only
338        // the caller knows whether starting over is worth it.
339        if self.cursor_len > 0 && reported_status(&parsed) == Some(Status::CURSOR_INVALID) {
340            return Some(self.finish(Outcome::Failed(Failure::CursorInvalid)));
341        }
342
343        self.remaining = response.remaining;
344
345        // The first fragment contributes the whole frame; later ones
346        // contribute only their trailing content, which appends to what
347        // the first frame already carries.
348        let contribution = if reassembly.len == 0 {
349            response.frame
350        } else {
351            crate::fragment::trailing(response.frame)
352        };
353        if !reassembly.append(contribution) {
354            return Some(self.finish(Outcome::Failed(Failure::TooLarge)));
355        }
356
357        let Some(cursor) = response.cursor else {
358            let len = reassembly.len;
359            return Some(self.finish(Outcome::Replied { len }));
360        };
361
362        // Continue: same request, fresh token, the cursor returned
363        // byte for byte.
364        let stalled = contribution.is_empty();
365        self.cursor[..cursor.len()].copy_from_slice(cursor);
366        self.cursor_len = cursor.len();
367        self.counter = self.counter.wrapping_add(1);
368        self.token = self.counter.to_be_bytes();
369
370        if stalled {
371            // An empty fragment means nothing further is available yet,
372            // which suits data that accumulates over time. Asking again
373            // at once would spin; the deadline paces it instead, and
374            // the attempt budget still bounds the wait.
375            let deadline_ms = now_ms + RETRY_MS;
376            self.state = State::Awaiting {
377                deadline_ms,
378                attempts: 0,
379            };
380            return Some(Step::Wait { deadline_ms });
381        }
382
383        self.state = State::Awaiting {
384            deadline_ms: now_ms,
385            attempts: 0,
386        };
387        Some(self.poll(now_ms, out))
388    }
389
390    fn finish(&mut self, outcome: Outcome) -> Step {
391        self.state = State::Finished(outcome);
392        Step::Done(outcome)
393    }
394}
395
396/// The status a `CMD_PROP_IS` of `PROP_LAST_STATUS` reports.
397fn reported_status(parsed: &Frame<'_>) -> Option<Status> {
398    if parsed.command() != Some(Cmd::PropIs) {
399        return None;
400    }
401    let (key, consumed) = umsh_ulcp::pui::decode(parsed.payload).ok()?;
402    if key != umsh_ulcp::ids::prop::LAST_STATUS {
403        return None;
404    }
405    let (status, _) = umsh_ulcp::pui::decode(&parsed.payload[consumed..]).ok()?;
406    Some(Status(status))
407}
408
409#[cfg(test)]
410mod tests {
411    use super::*;
412    use crate::device::{DeviceEngine, Dispatch, Ingress, Produced, PublicKey};
413    use umsh_ulcp::frame;
414    use umsh_ulcp::ids::prop;
415
416    const PAYLOAD: usize = 180;
417    const ADMIN: PublicKey = [0xAA; 32];
418    type Device = DeviceEngine<PAYLOAD, 2>;
419    type Admin = Exchange<64>;
420
421    fn get(key: u32, buf: &mut [u8]) -> usize {
422        frame::prop_get(buf, 0, key).expect("encode")
423    }
424
425    #[test]
426    fn the_first_poll_hands_out_a_request_and_the_next_one_waits() {
427        let mut buf = [0u8; 8];
428        let frame_len = get(prop::CAPS, &mut buf);
429        let frame = &buf[..frame_len];
430        let mut exchange = Admin::new(frame, 0x2A, 1_000).expect("begin");
431
432        let mut out = [0u8; PAYLOAD];
433        let Step::Send { len } = exchange.poll(1_000, &mut out) else {
434            panic!("expected a request");
435        };
436        let request = Envelope::parse(&out[..len]).expect("envelope");
437        assert_eq!(request.token, exchange.token());
438        assert_eq!(request.cursor, None);
439        assert_eq!(request.frame, frame);
440
441        assert_eq!(
442            exchange.poll(1_001, &mut out),
443            Step::Wait {
444                deadline_ms: 1_000 + RETRY_MS
445            }
446        );
447    }
448
449    #[test]
450    fn a_retransmission_repeats_the_request_under_the_same_token() {
451        let mut buf = [0u8; 8];
452        let frame_len = get(prop::CAPS, &mut buf);
453        let frame = &buf[..frame_len];
454        let mut exchange = Admin::new(frame, 1, 0).expect("begin");
455
456        let mut first = [0u8; PAYLOAD];
457        let Step::Send { len } = exchange.poll(0, &mut first) else {
458            panic!("expected a request");
459        };
460        let first = first[..len].to_vec();
461
462        let mut again = [0u8; PAYLOAD];
463        let Step::Send { len } = exchange.poll(RETRY_MS, &mut again) else {
464            panic!("expected a retransmission");
465        };
466        assert_eq!(&again[..len], &first[..]);
467    }
468
469    #[test]
470    fn an_unanswered_exchange_times_out_after_its_attempts() {
471        let mut buf = [0u8; 8];
472        let frame_len = get(prop::CAPS, &mut buf);
473        let frame = &buf[..frame_len];
474        let mut exchange = Admin::new(frame, 1, 0).expect("begin");
475        let mut out = [0u8; PAYLOAD];
476
477        for attempt in 0..MAX_ATTEMPTS {
478            let now = u64::from(attempt) * RETRY_MS;
479            assert!(matches!(exchange.poll(now, &mut out), Step::Send { .. }));
480        }
481        assert_eq!(
482            exchange.poll(u64::from(MAX_ATTEMPTS) * RETRY_MS, &mut out),
483            Step::Done(Outcome::Failed(Failure::TimedOut))
484        );
485    }
486
487    #[test]
488    fn a_response_for_another_exchange_is_not_this_ones() {
489        let mut buf = [0u8; 8];
490        let frame_len = get(prop::CAPS, &mut buf);
491        let frame = &buf[..frame_len];
492        let mut exchange = Admin::new(frame, 1, 0).expect("begin");
493        let mut out = [0u8; PAYLOAD];
494        exchange.poll(0, &mut out);
495
496        let mut storage = [0u8; 256];
497        let mut reassembly = Reassembly::new(&mut storage);
498        let mut stranger = [0u8; PAYLOAD];
499        let len = Envelope::new([0xFF, 0xFF], &[0x80, 0x06])
500            .encode(&mut stranger)
501            .unwrap();
502        assert_eq!(
503            exchange.receive(&stranger[..len], &mut reassembly, 0, &mut out),
504            None
505        );
506    }
507
508    #[test]
509    fn a_reset_expects_no_response() {
510        let mut buf = [0u8; 8];
511        for len in [
512            frame::reset(&mut buf, 0).unwrap(),
513            frame::factory_reset(&mut buf, 0).unwrap(),
514            frame::reboot(&mut buf, 0).unwrap(),
515        ] {
516            let mut exchange = Admin::new(&buf[..len], 1, 0).expect("begin");
517            assert!(!exchange.expects_response());
518            assert_eq!(exchange.delivered(), Step::Done(Outcome::NoResponse));
519        }
520
521        let len = get(prop::CAPS, &mut buf);
522        assert!(Admin::new(&buf[..len], 1, 0).unwrap().expects_response());
523    }
524
525    /// Run a whole exchange against a real [`DeviceEngine`], with the
526    /// caller's dispatch serving `value` in fragments of at most
527    /// `chunk` octets.
528    fn converse(request: &[u8], value: &[u8], chunk: usize) -> (Outcome, Vec<u8>) {
529        let mut device = Device::new(0x5AA5);
530        let mut admin = Admin::new(request, 7, 0).expect("begin");
531        let mut storage = [0u8; 1024];
532        let mut reassembly = Reassembly::new(&mut storage);
533        let mut wire = [0u8; PAYLOAD];
534        let mut now = 0u64;
535
536        let mut step = admin.poll(now, &mut wire);
537        while let Step::Send { len } = step {
538            let mut response = [0u8; PAYLOAD];
539            let Ingress::Dispatch(Dispatch { resume, budget, .. }) =
540                device.begin(&ADMIN, &wire[..len], 1, now, &mut response)
541            else {
542                panic!("expected a dispatch");
543            };
544
545            // Serve the value from `resume`, in `chunk`-sized bites.
546            let resume = resume as usize;
547            let mut frame_buf = [0u8; PAYLOAD];
548            let head = frame::prop_is(&mut frame_buf, 0, prop::CAPS, &[]).unwrap();
549            let room = budget.saturating_sub(head).min(chunk);
550            let end = (resume + room).min(value.len());
551            let fragment = &value[resume..end];
552            let len = frame::prop_is(&mut frame_buf, 0, prop::CAPS, fragment).unwrap();
553            let remaining = (value.len() - end) as u32;
554            let len = device
555                .complete(
556                    Produced::fragment(&frame_buf[..len], fragment.len() as u32, remaining),
557                    &mut response,
558                )
559                .expect("complete")
560                .expect("a response");
561
562            now += 100;
563            step = admin
564                .receive(&response[..len], &mut reassembly, now, &mut wire)
565                .expect("this exchange's response");
566        }
567
568        let Step::Done(outcome) = step else {
569            panic!("expected a finished exchange, got {step:?}");
570        };
571        (outcome, reassembly.frame().to_vec())
572    }
573
574    #[test]
575    fn a_read_that_fits_one_payload_finishes_in_one_exchange() {
576        let mut buf = [0u8; 8];
577        let frame_len = get(prop::CAPS, &mut buf);
578        let frame = &buf[..frame_len];
579        let value: Vec<u8> = (0..16u8).collect();
580
581        let (outcome, assembled) = converse(frame, &value, 256);
582        let Outcome::Replied { len } = outcome else {
583            panic!("expected a reply, got {outcome:?}");
584        };
585        assert_eq!(len, assembled.len());
586        assert_eq!(crate::fragment::trailing(&assembled), &value[..]);
587    }
588
589    #[test]
590    fn a_read_spanning_several_exchanges_reassembles_to_the_whole_value() {
591        let mut buf = [0u8; 8];
592        let frame_len = get(prop::CAPS, &mut buf);
593        let frame = &buf[..frame_len];
594        let value: Vec<u8> = (0..500u32).map(|index| index as u8).collect();
595
596        for chunk in [1, 7, 64, 128] {
597            let (outcome, assembled) = converse(frame, &value, chunk);
598            assert!(
599                matches!(outcome, Outcome::Replied { .. }),
600                "chunk {chunk}: {outcome:?}"
601            );
602            assert_eq!(
603                crate::fragment::trailing(&assembled),
604                &value[..],
605                "chunk {chunk}"
606            );
607        }
608    }
609
610    #[test]
611    fn each_continuation_carries_a_fresh_token_and_the_cursor_verbatim() {
612        let mut buf = [0u8; 8];
613        let frame_len = get(prop::CAPS, &mut buf);
614        let frame = &buf[..frame_len];
615        let mut device = Device::new(1);
616        let mut admin = Admin::new(frame, 100, 0).expect("begin");
617        let mut storage = [0u8; 1024];
618        let mut reassembly = Reassembly::new(&mut storage);
619        let mut wire = [0u8; PAYLOAD];
620
621        let Step::Send { len } = admin.poll(0, &mut wire) else {
622            panic!("expected a request");
623        };
624        let first_token = Envelope::parse(&wire[..len]).unwrap().token;
625
626        let mut response = [0u8; PAYLOAD];
627        assert!(matches!(
628            device.begin(&ADMIN, &wire[..len], 1, 0, &mut response),
629            Ingress::Dispatch(_)
630        ));
631        let mut frame_buf = [0u8; 32];
632        let reply = frame::prop_is(&mut frame_buf, 0, prop::CAPS, &[1, 2, 3, 4]).unwrap();
633        let len = device
634            .complete(
635                Produced::fragment(&frame_buf[..reply], 4, 12),
636                &mut response,
637            )
638            .unwrap()
639            .unwrap();
640        let issued = Envelope::parse(&response[..len]).unwrap();
641        let cursor = issued.cursor.expect("a cursor").to_vec();
642        assert_eq!(admin.remaining(), None);
643
644        let Some(Step::Send { len }) =
645            admin.receive(&response[..len], &mut reassembly, 0, &mut wire)
646        else {
647            panic!("expected a continuation");
648        };
649        let continuation = Envelope::parse(&wire[..len]).unwrap();
650        assert_ne!(continuation.token, first_token);
651        assert_eq!(continuation.cursor, Some(&cursor[..]));
652        assert_eq!(continuation.frame, frame, "the read is repeated verbatim");
653        assert_eq!(admin.remaining(), Some(12));
654        assert_eq!(
655            admin.counter(),
656            101,
657            "the counter reports the continuation's token, so a successor \
658             seeded from it cannot reissue one the device has answered"
659        );
660    }
661
662    #[test]
663    fn a_refused_cursor_ends_the_exchange_so_the_caller_can_start_over() {
664        let mut buf = [0u8; 8];
665        let frame_len = get(prop::CAPS, &mut buf);
666        let frame = &buf[..frame_len];
667        let mut device = Device::new(1);
668        let mut admin = Admin::new(frame, 5, 0).expect("begin");
669        let mut storage = [0u8; 1024];
670        let mut reassembly = Reassembly::new(&mut storage);
671        let mut wire = [0u8; PAYLOAD];
672        let mut response = [0u8; PAYLOAD];
673
674        // One fragment at generation 1, issuing a cursor.
675        let Step::Send { len } = admin.poll(0, &mut wire) else {
676            panic!("expected a request");
677        };
678        assert!(matches!(
679            device.begin(&ADMIN, &wire[..len], 1, 0, &mut response),
680            Ingress::Dispatch(_)
681        ));
682        let mut frame_buf = [0u8; 32];
683        let reply = frame::prop_is(&mut frame_buf, 0, prop::CAPS, &[1, 2]).unwrap();
684        let len = device
685            .complete(Produced::fragment(&frame_buf[..reply], 2, 8), &mut response)
686            .unwrap()
687            .unwrap();
688        let Some(Step::Send { len }) =
689            admin.receive(&response[..len], &mut reassembly, 0, &mut wire)
690        else {
691            panic!("expected a continuation");
692        };
693
694        // The device domain moves before the continuation lands.
695        let Ingress::Respond { len } = device.begin(&ADMIN, &wire[..len], 2, 0, &mut response)
696        else {
697            panic!("expected a refusal");
698        };
699        assert_eq!(
700            admin.receive(&response[..len], &mut reassembly, 0, &mut wire),
701            Some(Step::Done(Outcome::Failed(Failure::CursorInvalid)))
702        );
703    }
704
705    #[test]
706    fn an_empty_fragment_paces_the_next_request_rather_than_spinning() {
707        let mut buf = [0u8; 8];
708        let frame_len = get(prop::CAPS, &mut buf);
709        let frame = &buf[..frame_len];
710        let mut device = Device::new(1);
711        let mut admin = Admin::new(frame, 11, 0).expect("begin");
712        let mut storage = [0u8; 256];
713        let mut reassembly = Reassembly::new(&mut storage);
714        let mut wire = [0u8; PAYLOAD];
715        let mut response = [0u8; PAYLOAD];
716        let mut frame_buf = [0u8; 32];
717
718        let Step::Send { len } = admin.poll(0, &mut wire) else {
719            panic!("expected a request");
720        };
721        assert!(matches!(
722            device.begin(&ADMIN, &wire[..len], 1, 0, &mut response),
723            Ingress::Dispatch(_)
724        ));
725        let reply = frame::prop_is(&mut frame_buf, 0, prop::CAPS, &[1, 2]).unwrap();
726        let len = device
727            .complete(Produced::fragment(&frame_buf[..reply], 2, 4), &mut response)
728            .unwrap()
729            .unwrap();
730        let Some(Step::Send { len }) =
731            admin.receive(&response[..len], &mut reassembly, 0, &mut wire)
732        else {
733            panic!("expected a continuation");
734        };
735
736        // Nothing further is available yet: the same cursor comes back
737        // with an empty fragment.
738        assert!(matches!(
739            device.begin(&ADMIN, &wire[..len], 1, 0, &mut response),
740            Ingress::Dispatch(_)
741        ));
742        let reply = frame::prop_is(&mut frame_buf, 0, prop::CAPS, &[]).unwrap();
743        let len = device
744            .complete(Produced::fragment(&frame_buf[..reply], 0, 4), &mut response)
745            .unwrap()
746            .unwrap();
747        assert_eq!(
748            admin.receive(&response[..len], &mut reassembly, 100, &mut wire),
749            Some(Step::Wait {
750                deadline_ms: 100 + RETRY_MS
751            })
752        );
753        // Nothing was contributed, and the read is still going.
754        assert_eq!(crate::fragment::trailing(reassembly.frame()), &[1u8, 2][..]);
755        assert!(matches!(
756            admin.poll(100 + RETRY_MS, &mut wire),
757            Step::Send { .. }
758        ));
759    }
760
761    #[test]
762    fn a_read_larger_than_the_reassembly_buffer_fails_rather_than_truncating() {
763        let mut buf = [0u8; 8];
764        let frame_len = get(prop::CAPS, &mut buf);
765        let frame = &buf[..frame_len];
766        let mut device = Device::new(1);
767        let mut admin = Admin::new(frame, 5, 0).expect("begin");
768        let mut storage = [0u8; 24];
769        let mut reassembly = Reassembly::new(&mut storage);
770        let mut wire = [0u8; PAYLOAD];
771        let mut response = [0u8; PAYLOAD];
772        let mut frame_buf = [0u8; 64];
773
774        let mut step = admin.poll(0, &mut wire);
775        for _ in 0..8 {
776            let Step::Send { len } = step else { break };
777            assert!(matches!(
778                device.begin(&ADMIN, &wire[..len], 1, 0, &mut response),
779                Ingress::Dispatch(_)
780            ));
781            let reply = frame::prop_is(&mut frame_buf, 0, prop::CAPS, &[0; 16]).unwrap();
782            let len = device
783                .complete(
784                    Produced::fragment(&frame_buf[..reply], 16, 999),
785                    &mut response,
786                )
787                .unwrap()
788                .unwrap();
789            step = admin
790                .receive(&response[..len], &mut reassembly, 0, &mut wire)
791                .expect("this exchange's response");
792        }
793        assert_eq!(step, Step::Done(Outcome::Failed(Failure::TooLarge)));
794    }
795
796    #[test]
797    fn an_unknown_critical_option_in_a_response_fails_the_exchange() {
798        let mut buf = [0u8; 8];
799        let frame_len = get(prop::CAPS, &mut buf);
800        let frame = &buf[..frame_len];
801        let mut admin = Admin::new(frame, 0x0102, 0).expect("begin");
802        let mut wire = [0u8; PAYLOAD];
803        admin.poll(0, &mut wire);
804        let token = admin.token();
805
806        let mut response = [0u8; PAYLOAD];
807        response[0] = token[0];
808        response[1] = token[1];
809        let len = {
810            let mut enc = umsh_core::options::OptionEncoder::new(&mut response[2..]);
811            enc.put(5, &[0]).unwrap();
812            enc.end_marker().unwrap();
813            2 + enc.finish()
814        };
815
816        let mut storage = [0u8; 64];
817        let mut reassembly = Reassembly::new(&mut storage);
818        assert_eq!(
819            admin.receive(&response[..len], &mut reassembly, 0, &mut wire),
820            Some(Step::Done(Outcome::Failed(Failure::UnknownCriticalOption(
821                5
822            ))))
823        );
824    }
825
826    #[test]
827    fn a_response_whose_frame_does_not_parse_fails_the_exchange() {
828        let mut buf = [0u8; 8];
829        let frame_len = get(prop::CAPS, &mut buf);
830        let frame = &buf[..frame_len];
831        let mut admin = Admin::new(frame, 3, 0).expect("begin");
832        let mut wire = [0u8; PAYLOAD];
833        admin.poll(0, &mut wire);
834
835        let mut response = [0u8; PAYLOAD];
836        let len = Envelope::new(admin.token(), &[0x00])
837            .encode(&mut response)
838            .unwrap();
839        let mut storage = [0u8; 64];
840        let mut reassembly = Reassembly::new(&mut storage);
841        assert_eq!(
842            admin.receive(&response[..len], &mut reassembly, 0, &mut wire),
843            Some(Step::Done(Outcome::Failed(Failure::Malformed)))
844        );
845    }
846
847    #[test]
848    fn a_finished_exchange_ignores_everything_that_arrives_after() {
849        let mut buf = [0u8; 8];
850        let frame_len = get(prop::CAPS, &mut buf);
851        let frame = &buf[..frame_len];
852        let mut admin = Admin::new(frame, 3, 0).expect("begin");
853        let mut wire = [0u8; PAYLOAD];
854        admin.poll(0, &mut wire);
855
856        let mut response = [0u8; PAYLOAD];
857        let mut frame_buf = [0u8; 16];
858        let reply = frame::prop_is(&mut frame_buf, 0, prop::CAPS, &[9]).unwrap();
859        let len = Envelope::new(admin.token(), &frame_buf[..reply])
860            .encode(&mut response)
861            .unwrap();
862
863        let mut storage = [0u8; 64];
864        let mut reassembly = Reassembly::new(&mut storage);
865        assert!(matches!(
866            admin.receive(&response[..len], &mut reassembly, 0, &mut wire),
867            Some(Step::Done(Outcome::Replied { .. }))
868        ));
869        assert_eq!(
870            admin.receive(&response[..len], &mut reassembly, 0, &mut wire),
871            None
872        );
873        assert!(matches!(
874            admin.poll(1_000_000, &mut wire),
875            Step::Done(Outcome::Replied { .. })
876        ));
877    }
878
879    #[test]
880    fn a_multi_get_reassembles_into_one_are_frame() {
881        let mut buf = [0u8; 16];
882        let len = frame::prop_multi_get(&mut buf, 0, &[prop::CAPS, prop::DEV_ADMINS]).unwrap();
883        let request = &buf[..len];
884
885        let mut device = Device::new(1);
886        let mut admin = Admin::new(request, 9, 0).expect("begin");
887        let mut storage = [0u8; 512];
888        let mut reassembly = Reassembly::new(&mut storage);
889        let mut wire = [0u8; PAYLOAD];
890        let mut response = [0u8; PAYLOAD];
891
892        // The whole entry list, served two entries at a time.
893        let mut whole = [0u8; 128];
894        let whole_len = {
895            let mut writer = frame::prop_are(&mut whole, 0).unwrap();
896            for index in 0..4u32 {
897                writer
898                    .write_entry(prop::CAPS + index, &[index as u8; 8])
899                    .unwrap();
900            }
901            writer.finish()
902        };
903        // Skip the two-octet frame header to get the entry list alone.
904        let entries = &whole[2..whole_len];
905
906        let mut served = 0usize;
907        let mut step = admin.poll(0, &mut wire);
908        while let Step::Send { len } = step {
909            assert!(matches!(
910                device.begin(&ADMIN, &wire[..len], 1, 0, &mut response),
911                Ingress::Dispatch(_)
912            ));
913            let end = (served + 22).min(entries.len());
914            let mut frame_buf = [0u8; 64];
915            let reply = {
916                let mut writer = frame::prop_are(&mut frame_buf, 0).unwrap();
917                writer.write_bytes(&entries[served..end]).unwrap();
918                writer.finish()
919            };
920            let produced = (end - served) as u32;
921            let remaining = (entries.len() - end) as u32;
922            served = end;
923            let len = device
924                .complete(
925                    Produced::fragment(&frame_buf[..reply], produced, remaining),
926                    &mut response,
927                )
928                .unwrap()
929                .unwrap();
930            step = admin
931                .receive(&response[..len], &mut reassembly, 0, &mut wire)
932                .expect("this exchange's response");
933        }
934
935        assert!(matches!(step, Step::Done(Outcome::Replied { .. })));
936        assert_eq!(reassembly.frame(), &whole[..whole_len]);
937    }
938}