1use umsh_ulcp::frame::{self, Cmd, Frame};
16use umsh_ulcp::status::Status;
17
18use crate::envelope::{Envelope, EnvelopeError, Token};
19use crate::fragment::continuable;
20
21pub type PublicKey = [u8; 32];
23
24pub const CACHE_ENTRIES: usize = 4;
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub enum DropReason {
34 NoToken,
37 NoRoom,
40}
41
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44pub enum Ingress<'p> {
45 Drop(DropReason),
47 Respond { len: usize },
50 Dispatch(Dispatch<'p>),
52}
53
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
56pub struct Dispatch<'p> {
57 pub frame: &'p [u8],
60 pub resume: u32,
64 pub budget: usize,
67 pub resets: bool,
71}
72
73impl Dispatch<'_> {
74 pub fn command(&self) -> Option<Cmd> {
78 Frame::parse(self.frame).ok().and_then(|f| f.command())
79 }
80}
81
82#[derive(Clone, Copy, Debug, PartialEq, Eq)]
84pub struct Produced<'a> {
85 pub frame: &'a [u8],
87 pub produced: u32,
90 pub remaining: u32,
93}
94
95impl<'a> Produced<'a> {
96 pub const fn complete(frame: &'a [u8]) -> Self {
98 Self {
99 frame,
100 produced: 0,
101 remaining: 0,
102 }
103 }
104
105 pub const fn fragment(frame: &'a [u8], produced: u32, remaining: u32) -> Self {
107 Self {
108 frame,
109 produced,
110 remaining,
111 }
112 }
113
114 pub const fn no_response() -> Self {
116 Self {
117 frame: &[],
118 produced: 0,
119 remaining: 0,
120 }
121 }
122}
123
124#[derive(Clone, Copy, Debug, PartialEq, Eq)]
126pub enum CompleteError {
127 NotDispatched,
130 TooLarge,
133}
134
135const CURSOR_LEN: usize = 8;
151
152fn encode_cursor(nonce: u16, generation: u16, tag: u16, offset: u16) -> [u8; CURSOR_LEN] {
153 let mut out = [0u8; CURSOR_LEN];
154 out[0..2].copy_from_slice(&nonce.to_be_bytes());
155 out[2..4].copy_from_slice(&generation.to_be_bytes());
156 out[4..6].copy_from_slice(&tag.to_be_bytes());
157 out[6..8].copy_from_slice(&offset.to_be_bytes());
158 out
159}
160
161fn request_tag(frame: &[u8]) -> u16 {
166 let mut hash: u32 = 0x811C_9DC5;
167 for &byte in frame {
168 hash ^= u32::from(byte);
169 hash = hash.wrapping_mul(0x0100_0193);
170 }
171 ((hash >> 16) ^ hash) as u16
174}
175
176const fn reset_class(cmd: Cmd) -> bool {
187 matches!(
188 cmd,
189 Cmd::Reset | Cmd::Restore | Cmd::FactoryReset | Cmd::Reboot
190 )
191}
192
193#[derive(Clone, Copy)]
195struct Entry<const PAYLOAD: usize> {
196 key: PublicKey,
197 token: Token,
198 payload: [u8; PAYLOAD],
199 len: usize,
200 last_ms: u64,
202 occupied: bool,
203}
204
205impl<const PAYLOAD: usize> Default for Entry<PAYLOAD> {
207 fn default() -> Self {
208 Self {
209 key: [0; 32],
210 token: [0; 2],
211 payload: [0; PAYLOAD],
212 len: 0,
213 last_ms: 0,
214 occupied: false,
215 }
216 }
217}
218
219#[derive(Clone, Copy)]
222struct InFlight {
223 key: PublicKey,
224 token: Token,
225 generation: u16,
226 tag: u16,
227 resume: u32,
228 budget: usize,
229 now_ms: u64,
230}
231
232pub struct DeviceEngine<const PAYLOAD: usize, const ENTRIES: usize = CACHE_ENTRIES> {
238 nonce: u16,
239 entries: [Entry<PAYLOAD>; ENTRIES],
240 in_flight: Option<InFlight>,
241}
242
243impl<const PAYLOAD: usize, const ENTRIES: usize> DeviceEngine<PAYLOAD, ENTRIES> {
244 pub fn new(nonce: u16) -> Self {
249 Self {
250 nonce,
251 entries: [Entry::default(); ENTRIES],
252 in_flight: None,
253 }
254 }
255
256 pub fn begin<'p>(
263 &mut self,
264 from: &PublicKey,
265 payload: &'p [u8],
266 generation: u16,
267 now_ms: u64,
268 out: &mut [u8],
269 ) -> Ingress<'p> {
270 self.in_flight = None;
271
272 let &[token0, token1, ..] = payload else {
277 return Ingress::Drop(DropReason::NoToken);
278 };
279 let token = [token0, token1];
280
281 let request = match Envelope::parse(payload) {
282 Ok(request) => request,
283 Err(EnvelopeError::UnknownCritical(_)) => {
284 return self.answer(from, token, Status::UNIMPLEMENTED, now_ms, out);
285 }
286 Err(EnvelopeError::InvalidOptionValue(crate::envelope::OPT_CURSOR)) => {
287 return self.answer(from, token, Status::CURSOR_INVALID, now_ms, out);
288 }
289 Err(_) => return self.answer(from, token, Status::PARSE_ERROR, now_ms, out),
290 };
291
292 if let Some(entry) = self.retained(from, token) {
296 entry.last_ms = now_ms;
297 let len = entry.len;
298 if out.len() < len {
299 return Ingress::Drop(DropReason::NoRoom);
300 }
301 out[..len].copy_from_slice(&entry.payload[..len]);
302 return Ingress::Respond { len };
303 }
304
305 let Ok(parsed) = Frame::parse(request.frame) else {
308 return self.answer(from, token, Status::PARSE_ERROR, now_ms, out);
309 };
310 let cmd = parsed.command();
311
312 let mut resume = 0;
313 if let Some(cursor) = request.cursor {
314 if !cmd.is_some_and(continuable) {
317 return self.answer(from, token, Status::INVALID_ARGUMENT, now_ms, out);
318 }
319 match self.parse_cursor(cursor, generation, request.frame) {
320 Some(offset) => resume = offset,
321 None => return self.answer(from, token, Status::CURSOR_INVALID, now_ms, out),
322 }
323 }
324
325 let budget = PAYLOAD
326 .min(out.len())
327 .saturating_sub(crate::envelope::OVERHEAD_MAX);
328 self.in_flight = Some(InFlight {
329 key: *from,
330 token,
331 generation,
332 tag: request_tag(request.frame),
335 resume,
336 budget,
337 now_ms,
338 });
339 Ingress::Dispatch(Dispatch {
340 frame: request.frame,
341 resume,
342 budget,
346 resets: cmd.is_some_and(reset_class),
347 })
348 }
349
350 pub fn complete(
356 &mut self,
357 produced: Produced<'_>,
358 out: &mut [u8],
359 ) -> Result<Option<usize>, CompleteError> {
360 let in_flight = self.in_flight.take().ok_or(CompleteError::NotDispatched)?;
361
362 if produced.frame.is_empty() {
363 self.entries = [Entry::default(); ENTRIES];
367 return Ok(None);
368 }
369
370 if produced.frame.len() > in_flight.budget {
371 return Err(CompleteError::TooLarge);
372 }
373
374 let mut response = Envelope::new(in_flight.token, produced.frame);
375 let cursor;
376 if produced.remaining > 0 {
377 let offset = in_flight
378 .resume
379 .checked_add(produced.produced)
380 .and_then(|offset| u16::try_from(offset).ok())
381 .ok_or(CompleteError::TooLarge)?;
382 cursor = encode_cursor(self.nonce, in_flight.generation, in_flight.tag, offset);
383 response = response
384 .with_cursor(&cursor)
385 .with_remaining(produced.remaining);
386 }
387
388 let len = response.encode(out).map_err(|_| CompleteError::TooLarge)?;
389 self.retain(
390 &in_flight.key,
391 in_flight.token,
392 &out[..len],
393 in_flight.now_ms,
394 );
395 Ok(Some(len))
396 }
397
398 pub fn forget_retained(&mut self) {
400 self.entries = [Entry::default(); ENTRIES];
401 }
402
403 fn answer(
406 &mut self,
407 from: &PublicKey,
408 token: Token,
409 status: Status,
410 now_ms: u64,
411 out: &mut [u8],
412 ) -> Ingress<'static> {
413 let mut buf = [0u8; 8];
414 let Ok(len) = frame::last_status(&mut buf, frame::TID_UNSOLICITED, status) else {
415 return Ingress::Drop(DropReason::NoRoom);
416 };
417 let Ok(len) = Envelope::new(token, &buf[..len]).encode(out) else {
418 return Ingress::Drop(DropReason::NoRoom);
419 };
420 self.retain(from, token, &out[..len], now_ms);
421 Ingress::Respond { len }
422 }
423
424 fn parse_cursor(&self, cursor: &[u8], generation: u16, frame: &[u8]) -> Option<u32> {
426 let cursor: [u8; CURSOR_LEN] = cursor.try_into().ok()?;
427 let nonce = u16::from_be_bytes([cursor[0], cursor[1]]);
428 let issued_at = u16::from_be_bytes([cursor[2], cursor[3]]);
429 let tag = u16::from_be_bytes([cursor[4], cursor[5]]);
430 let offset = u16::from_be_bytes([cursor[6], cursor[7]]);
431 if nonce != self.nonce || issued_at != generation || tag != request_tag(frame) {
432 return None;
433 }
434 Some(u32::from(offset))
435 }
436
437 fn retained(&mut self, key: &PublicKey, token: Token) -> Option<&mut Entry<PAYLOAD>> {
438 self.entries
439 .iter_mut()
440 .find(|entry| entry.occupied && &entry.key == key && entry.token == token)
441 }
442
443 fn retain(&mut self, key: &PublicKey, token: Token, payload: &[u8], now_ms: u64) {
444 if payload.len() > PAYLOAD {
445 return;
449 }
450 let slot = self
451 .entries
452 .iter_mut()
453 .position(|entry| entry.occupied && &entry.key == key)
454 .or_else(|| self.entries.iter().position(|entry| !entry.occupied))
455 .or_else(|| {
456 self.entries
459 .iter()
460 .enumerate()
461 .min_by_key(|(_, entry)| entry.last_ms)
462 .map(|(index, _)| index)
463 });
464 let Some(slot) = slot else { return };
465 let entry = &mut self.entries[slot];
466 entry.key = *key;
467 entry.token = token;
468 entry.payload[..payload.len()].copy_from_slice(payload);
469 entry.len = payload.len();
470 entry.last_ms = now_ms;
471 entry.occupied = true;
472 }
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478 use umsh_ulcp::ids::prop;
479
480 const PAYLOAD: usize = 180;
481 type Engine = DeviceEngine<PAYLOAD, 2>;
482
483 const ALICE: PublicKey = [0xAA; 32];
484 const BOB: PublicKey = [0xBB; 32];
485
486 fn request(token: Token, frame: &[u8], out: &mut [u8]) -> usize {
487 Envelope::new(token, frame).encode(out).expect("encode")
488 }
489
490 fn get(key: u32, buf: &mut [u8]) -> usize {
491 frame::prop_get(buf, 0, key).expect("encode")
492 }
493
494 fn reported_status(payload: &[u8]) -> Status {
496 let envelope = Envelope::parse(payload).expect("envelope");
497 let parsed = Frame::parse(envelope.frame).expect("frame");
498 assert_eq!(parsed.command(), Some(Cmd::PropIs));
499 let (key, consumed) = umsh_ulcp::pui::decode(parsed.payload).expect("key");
500 assert_eq!(key, prop::LAST_STATUS);
501 let (status, _) = umsh_ulcp::pui::decode(&parsed.payload[consumed..]).expect("status");
502 Status(status)
503 }
504
505 #[test]
506 fn a_plain_request_is_dispatched_and_its_reply_comes_back_whole() {
507 let mut engine = Engine::new(0x1234);
508 let mut frame_buf = [0u8; 8];
509 let frame_len = get(prop::CAPS, &mut frame_buf);
510 let frame = &frame_buf[..frame_len];
511 let mut payload = [0u8; PAYLOAD];
512 let len = request([1, 2], frame, &mut payload);
513
514 let mut out = [0u8; PAYLOAD];
515 let Ingress::Dispatch(dispatch) = engine.begin(&ALICE, &payload[..len], 7, 0, &mut out)
516 else {
517 panic!("expected a dispatch");
518 };
519 assert_eq!(dispatch.frame, frame);
520 assert_eq!(dispatch.resume, 0);
521 assert!(!dispatch.resets);
522 assert_eq!(dispatch.budget, PAYLOAD - crate::envelope::OVERHEAD_MAX);
523
524 let mut reply_buf = [0u8; 16];
525 let reply_len = frame::prop_is(&mut reply_buf, 0, prop::CAPS, &[1, 2, 3]).unwrap();
526 let len = engine
527 .complete(Produced::complete(&reply_buf[..reply_len]), &mut out)
528 .expect("complete")
529 .expect("a response");
530
531 let response = Envelope::parse(&out[..len]).expect("envelope");
532 assert_eq!(response.token, [1, 2]);
533 assert_eq!(response.cursor, None);
534 assert_eq!(response.frame, &reply_buf[..reply_len]);
535 }
536
537 #[test]
538 fn completing_without_a_dispatch_is_an_error() {
539 let mut engine = Engine::new(1);
540 let mut out = [0u8; PAYLOAD];
541 assert_eq!(
542 engine.complete(Produced::complete(&[0x80, 0x06]), &mut out),
543 Err(CompleteError::NotDispatched)
544 );
545 }
546
547 #[test]
548 fn a_repeated_token_is_answered_from_the_retained_response() {
549 let mut engine = Engine::new(1);
550 let mut frame_buf = [0u8; 8];
551 let frame_len = get(prop::CAPS, &mut frame_buf);
552 let frame = &frame_buf[..frame_len];
553 let mut payload = [0u8; PAYLOAD];
554 let len = request([9, 9], frame, &mut payload);
555
556 let mut out = [0u8; PAYLOAD];
557 assert!(matches!(
558 engine.begin(&ALICE, &payload[..len], 0, 0, &mut out),
559 Ingress::Dispatch(_)
560 ));
561 let mut reply_buf = [0u8; 16];
562 let reply_len = frame::prop_is(&mut reply_buf, 0, prop::CAPS, &[7]).unwrap();
563 let first = engine
564 .complete(Produced::complete(&reply_buf[..reply_len]), &mut out)
565 .unwrap()
566 .unwrap();
567 let first = out[..first].to_vec();
568
569 let mut again = [0u8; PAYLOAD];
571 match engine.begin(&ALICE, &payload[..len], 0, 1_000, &mut again) {
572 Ingress::Respond { len } => assert_eq!(&again[..len], &first[..]),
573 other => panic!("expected the retained response, got {other:?}"),
574 }
575
576 assert!(matches!(
579 engine.begin(&BOB, &payload[..len], 0, 2_000, &mut again),
580 Ingress::Dispatch(_)
581 ));
582 }
583
584 #[test]
585 fn a_new_token_from_the_same_administrator_replaces_the_retained_entry() {
586 let mut engine = Engine::new(1);
587 let mut frame_buf = [0u8; 8];
588 let frame_len = get(prop::CAPS, &mut frame_buf);
589 let frame = &frame_buf[..frame_len];
590 let mut out = [0u8; PAYLOAD];
591 let mut reply_buf = [0u8; 16];
592 let reply_len = frame::prop_is(&mut reply_buf, 0, prop::CAPS, &[7]).unwrap();
593
594 for token in [[1, 1], [2, 2]] {
595 let mut payload = [0u8; PAYLOAD];
596 let len = request(token, frame, &mut payload);
597 assert!(matches!(
598 engine.begin(&ALICE, &payload[..len], 0, 0, &mut out),
599 Ingress::Dispatch(_)
600 ));
601 engine
602 .complete(Produced::complete(&reply_buf[..reply_len]), &mut out)
603 .unwrap();
604 }
605
606 let mut payload = [0u8; PAYLOAD];
609 let len = request([1, 1], frame, &mut payload);
610 assert!(matches!(
611 engine.begin(&ALICE, &payload[..len], 0, 0, &mut out),
612 Ingress::Dispatch(_)
613 ));
614 assert_eq!(engine.entries.iter().filter(|e| e.occupied).count(), 1);
615 }
616
617 #[test]
618 fn the_least_recently_active_administrator_is_evicted() {
619 let mut engine = Engine::new(1);
620 let mut frame_buf = [0u8; 8];
621 let frame_len = get(prop::CAPS, &mut frame_buf);
622 let frame = &frame_buf[..frame_len];
623 let mut out = [0u8; PAYLOAD];
624 let mut reply_buf = [0u8; 16];
625 let reply_len = frame::prop_is(&mut reply_buf, 0, prop::CAPS, &[7]).unwrap();
626 let carol: PublicKey = [0xCC; 32];
627
628 for (key, now) in [(&ALICE, 0u64), (&BOB, 10), (&carol, 20)] {
630 let mut payload = [0u8; PAYLOAD];
631 let len = request([1, 1], frame, &mut payload);
632 assert!(matches!(
633 engine.begin(key, &payload[..len], 0, now, &mut out),
634 Ingress::Dispatch(_)
635 ));
636 engine
637 .complete(Produced::complete(&reply_buf[..reply_len]), &mut out)
638 .unwrap();
639 }
640
641 let mut payload = [0u8; PAYLOAD];
642 let len = request([1, 1], frame, &mut payload);
643 assert!(matches!(
645 engine.begin(&BOB, &payload[..len], 0, 30, &mut out),
646 Ingress::Respond { .. }
647 ));
648 assert!(matches!(
650 engine.begin(&ALICE, &payload[..len], 0, 40, &mut out),
651 Ingress::Dispatch(_)
652 ));
653 }
654
655 #[test]
656 fn a_frame_that_does_not_parse_is_answered_parse_error() {
657 let mut engine = Engine::new(1);
658 let mut out = [0u8; PAYLOAD];
659
660 for frame in [&[][..], &[0x00, 0x02][..], &[0x80][..]] {
661 let mut payload = [0u8; PAYLOAD];
662 let len = request([3, 3], frame, &mut payload);
663 let Ingress::Respond { len } = engine.begin(&ALICE, &payload[..len], 0, 0, &mut out)
664 else {
665 panic!("expected an answer for {frame:?}");
666 };
667 assert_eq!(reported_status(&out[..len]), Status::PARSE_ERROR);
668 engine.forget_retained();
671 }
672 }
673
674 #[test]
675 fn a_payload_too_short_for_a_token_is_dropped() {
676 let mut engine = Engine::new(1);
677 let mut out = [0u8; PAYLOAD];
678 for payload in [&[][..], &[0x01][..]] {
679 assert_eq!(
680 engine.begin(&ALICE, payload, 0, 0, &mut out),
681 Ingress::Drop(DropReason::NoToken)
682 );
683 }
684 }
685
686 #[test]
687 fn a_malformed_option_block_is_answered_because_the_token_precedes_it() {
688 let mut engine = Engine::new(1);
689 let mut out = [0u8; PAYLOAD];
690 let Ingress::Respond { len } = engine.begin(&ALICE, &[4, 5, 0x1F, 0x00], 0, 0, &mut out)
692 else {
693 panic!("expected an answer");
694 };
695 assert_eq!(reported_status(&out[..len]), Status::PARSE_ERROR);
696 assert_eq!(Envelope::parse(&out[..len]).unwrap().token, [4, 5]);
697 }
698
699 #[test]
700 fn a_cursor_of_an_impossible_width_is_answered_cursor_invalid() {
701 let mut payload = [0u8; PAYLOAD];
702 payload[0] = 1;
703 payload[1] = 2;
704 let len = {
705 let mut enc = umsh_core::options::OptionEncoder::new(&mut payload[2..]);
706 enc.put(crate::envelope::OPT_CURSOR, &[0u8; 9]).unwrap();
707 enc.end_marker().unwrap();
708 2 + enc.finish()
709 };
710
711 let mut engine = Engine::new(1);
712 let mut out = [0u8; PAYLOAD];
713 let Ingress::Respond { len } = engine.begin(&ALICE, &payload[..len], 0, 0, &mut out) else {
714 panic!("expected an answer");
715 };
716 assert_eq!(reported_status(&out[..len]), Status::CURSOR_INVALID);
717 }
718
719 #[test]
720 fn an_unknown_critical_option_is_answered_unimplemented() {
721 let mut payload = [0u8; PAYLOAD];
722 payload[0] = 5;
723 payload[1] = 6;
724 let len = {
725 let mut enc = umsh_core::options::OptionEncoder::new(&mut payload[2..]);
726 enc.put(7, &[0]).unwrap();
727 enc.end_marker().unwrap();
728 2 + enc.finish()
729 };
730
731 let mut engine = Engine::new(1);
732 let mut out = [0u8; PAYLOAD];
733 let Ingress::Respond { len } = engine.begin(&ALICE, &payload[..len], 0, 0, &mut out) else {
734 panic!("expected an answer");
735 };
736 assert_eq!(reported_status(&out[..len]), Status::UNIMPLEMENTED);
737 assert_eq!(Envelope::parse(&out[..len]).unwrap().token, [5, 6]);
738 }
739
740 fn fragment(
743 engine: &mut Engine,
744 token: Token,
745 frame: &[u8],
746 cursor: Option<&[u8]>,
747 generation: u16,
748 (produced, remaining): (u32, u32),
750 out: &mut [u8],
751 ) -> (u32, usize) {
752 let mut payload = [0u8; PAYLOAD];
753 let mut envelope = Envelope::new(token, frame);
754 if let Some(cursor) = cursor {
755 envelope = envelope.with_cursor(cursor);
756 }
757 let len = envelope.encode(&mut payload).expect("encode");
758 let Ingress::Dispatch(dispatch) = engine.begin(&ALICE, &payload[..len], generation, 0, out)
759 else {
760 panic!("expected a dispatch");
761 };
762 let mut reply_buf = [0u8; 32];
763 let reply_len = frame::prop_is(&mut reply_buf, 0, prop::CAPS, &[0; 4]).unwrap();
764 let len = engine
765 .complete(
766 Produced::fragment(&reply_buf[..reply_len], produced, remaining),
767 out,
768 )
769 .expect("complete")
770 .expect("a response");
771 (dispatch.resume, len)
772 }
773
774 #[test]
775 fn a_cursor_carries_the_position_from_one_exchange_to_the_next() {
776 let mut engine = Engine::new(0xBEEF);
777 let mut frame_buf = [0u8; 8];
778 let frame_len = get(prop::CAPS, &mut frame_buf);
779 let frame = &frame_buf[..frame_len];
780 let mut out = [0u8; PAYLOAD];
781
782 let (resume, len) = fragment(&mut engine, [1, 0], frame, None, 3, (40, 90), &mut out);
783 assert_eq!(resume, 0);
784 let first = Envelope::parse(&out[..len]).unwrap();
785 assert_eq!(first.remaining, Some(90));
786 let cursor = first.cursor.expect("a cursor").to_vec();
787
788 let (resume, len) = fragment(
789 &mut engine,
790 [2, 0],
791 frame,
792 Some(&cursor),
793 3,
794 (40, 50),
795 &mut out,
796 );
797 assert_eq!(resume, 40);
798 let second = Envelope::parse(&out[..len]).unwrap();
799 let cursor = second.cursor.expect("a cursor").to_vec();
800
801 let mut payload = [0u8; PAYLOAD];
803 let len = Envelope::new([3, 0], frame)
804 .with_cursor(&cursor)
805 .encode(&mut payload)
806 .unwrap();
807 let Ingress::Dispatch(dispatch) = engine.begin(&ALICE, &payload[..len], 3, 0, &mut out)
808 else {
809 panic!("expected a dispatch");
810 };
811 assert_eq!(dispatch.resume, 80);
812 let mut reply_buf = [0u8; 32];
813 let reply_len = frame::prop_is(&mut reply_buf, 0, prop::CAPS, &[0; 4]).unwrap();
814 let len = engine
815 .complete(Produced::complete(&reply_buf[..reply_len]), &mut out)
816 .unwrap()
817 .unwrap();
818 assert_eq!(Envelope::parse(&out[..len]).unwrap().cursor, None);
819 }
820
821 #[test]
822 fn a_cursor_is_refused_once_the_device_domain_moves() {
823 let mut engine = Engine::new(1);
824 let mut frame_buf = [0u8; 8];
825 let frame_len = get(prop::CAPS, &mut frame_buf);
826 let frame = &frame_buf[..frame_len];
827 let mut out = [0u8; PAYLOAD];
828
829 let (_, len) = fragment(&mut engine, [1, 0], frame, None, 5, (10, 10), &mut out);
830 let cursor = Envelope::parse(&out[..len])
831 .unwrap()
832 .cursor
833 .unwrap()
834 .to_vec();
835
836 let mut payload = [0u8; PAYLOAD];
837 let len = Envelope::new([2, 0], frame)
838 .with_cursor(&cursor)
839 .encode(&mut payload)
840 .unwrap();
841 let Ingress::Respond { len } = engine.begin(&ALICE, &payload[..len], 6, 0, &mut out) else {
842 panic!("a cursor issued at generation 5 must not be honored at 6");
843 };
844 assert_eq!(reported_status(&out[..len]), Status::CURSOR_INVALID);
845 }
846
847 #[test]
848 fn a_cursor_is_refused_for_a_read_other_than_the_one_it_began() {
849 let mut engine = Engine::new(1);
850 let mut frame_buf = [0u8; 8];
851 let frame_len = get(prop::CAPS, &mut frame_buf);
852 let frame = &frame_buf[..frame_len];
853 let mut out = [0u8; PAYLOAD];
854 let (_, len) = fragment(&mut engine, [1, 0], frame, None, 0, (10, 10), &mut out);
855 let cursor = Envelope::parse(&out[..len])
856 .unwrap()
857 .cursor
858 .unwrap()
859 .to_vec();
860
861 let mut other_buf = [0u8; 8];
863 let other_len = get(prop::PROTOCOL_VERSION, &mut other_buf);
864 let other = &other_buf[..other_len];
865 let mut payload = [0u8; PAYLOAD];
866 let len = Envelope::new([2, 0], other)
867 .with_cursor(&cursor)
868 .encode(&mut payload)
869 .unwrap();
870 let Ingress::Respond { len } = engine.begin(&ALICE, &payload[..len], 0, 0, &mut out) else {
871 panic!("expected a refusal");
872 };
873 assert_eq!(reported_status(&out[..len]), Status::CURSOR_INVALID);
874 }
875
876 #[test]
877 fn a_cursor_from_before_a_reboot_is_refused() {
878 let mut engine = Engine::new(0x1111);
879 let mut frame_buf = [0u8; 8];
880 let frame_len = get(prop::CAPS, &mut frame_buf);
881 let frame = &frame_buf[..frame_len];
882 let mut out = [0u8; PAYLOAD];
883 let (_, len) = fragment(&mut engine, [1, 0], frame, None, 0, (10, 10), &mut out);
884 let cursor = Envelope::parse(&out[..len])
885 .unwrap()
886 .cursor
887 .unwrap()
888 .to_vec();
889
890 let mut rebooted = Engine::new(0x2222);
893 let mut payload = [0u8; PAYLOAD];
894 let len = Envelope::new([2, 0], frame)
895 .with_cursor(&cursor)
896 .encode(&mut payload)
897 .unwrap();
898 let Ingress::Respond { len } = rebooted.begin(&ALICE, &payload[..len], 0, 0, &mut out)
899 else {
900 panic!("expected a refusal");
901 };
902 assert_eq!(reported_status(&out[..len]), Status::CURSOR_INVALID);
903 }
904
905 #[test]
906 fn a_malformed_cursor_is_refused_rather_than_read_from_the_beginning() {
907 let mut engine = Engine::new(1);
908 let mut frame_buf = [0u8; 8];
909 let frame_len = get(prop::CAPS, &mut frame_buf);
910 let frame = &frame_buf[..frame_len];
911 let mut out = [0u8; PAYLOAD];
912
913 for cursor in [&[0u8][..], &[0u8; 7][..], &[0xFF; 8][..]] {
914 let mut payload = [0u8; PAYLOAD];
915 let len = Envelope::new([1, 0], frame)
916 .with_cursor(cursor)
917 .encode(&mut payload)
918 .unwrap();
919 let Ingress::Respond { len } = engine.begin(&ALICE, &payload[..len], 0, 0, &mut out)
920 else {
921 panic!("expected a refusal for {cursor:?}");
922 };
923 assert_eq!(reported_status(&out[..len]), Status::CURSOR_INVALID);
924 engine.forget_retained();
925 }
926 }
927
928 #[test]
929 fn a_cursor_on_a_request_that_is_not_a_read_is_an_invalid_argument() {
930 let mut engine = Engine::new(1);
931 let mut out = [0u8; PAYLOAD];
932 let mut frame_buf = [0u8; 8];
933
934 for len in [
935 frame::prop_set(&mut frame_buf, 0, prop::CAPS, &[1]).unwrap(),
936 frame::save(&mut frame_buf, 0).unwrap(),
937 ] {
938 let mut payload = [0u8; PAYLOAD];
939 let len = Envelope::new([1, 0], &frame_buf[..len])
940 .with_cursor(&[0; 8])
941 .encode(&mut payload)
942 .unwrap();
943 let Ingress::Respond { len } = engine.begin(&ALICE, &payload[..len], 0, 0, &mut out)
944 else {
945 panic!("expected a refusal");
946 };
947 assert_eq!(reported_status(&out[..len]), Status::INVALID_ARGUMENT);
948 engine.forget_retained();
949 }
950 }
951
952 #[test]
953 fn a_multi_get_continues_the_same_way_a_get_does() {
954 let mut engine = Engine::new(1);
955 let mut frame_buf = [0u8; 16];
956 let len =
957 frame::prop_multi_get(&mut frame_buf, 0, &[prop::CAPS, prop::DEV_ADMINS]).unwrap();
958 let frame = &frame_buf[..len];
959 let mut out = [0u8; PAYLOAD];
960
961 let (_, len) = fragment(&mut engine, [1, 0], frame, None, 0, (60, 20), &mut out);
962 let cursor = Envelope::parse(&out[..len])
963 .unwrap()
964 .cursor
965 .unwrap()
966 .to_vec();
967 let (resume, _) = fragment(
968 &mut engine,
969 [2, 0],
970 frame,
971 Some(&cursor),
972 0,
973 (20, 0),
974 &mut out,
975 );
976 assert_eq!(resume, 60);
977 }
978
979 #[test]
980 fn a_reset_is_answered_by_nothing_and_forgets_what_was_retained() {
981 let mut engine = Engine::new(1);
982 let mut out = [0u8; PAYLOAD];
983 let mut frame_buf = [0u8; 8];
984
985 let get_len = get(prop::CAPS, &mut frame_buf);
987 let mut payload = [0u8; PAYLOAD];
988 let len = request([1, 1], &frame_buf[..get_len], &mut payload);
989 assert!(matches!(
990 engine.begin(&ALICE, &payload[..len], 0, 0, &mut out),
991 Ingress::Dispatch(_)
992 ));
993 let mut reply_buf = [0u8; 16];
994 let reply_len = frame::prop_is(&mut reply_buf, 0, prop::CAPS, &[7]).unwrap();
995 engine
996 .complete(Produced::complete(&reply_buf[..reply_len]), &mut out)
997 .unwrap();
998
999 let mut frame_buf = [0u8; 8];
1000 for len in [
1001 frame::reset(&mut frame_buf, 0).unwrap(),
1002 frame::restore(&mut frame_buf, 0).unwrap(),
1003 frame::factory_reset(&mut frame_buf, 0).unwrap(),
1004 frame::reboot(&mut frame_buf, 0).unwrap(),
1005 ] {
1006 let mut payload = [0u8; PAYLOAD];
1007 let len = request([2, 2], &frame_buf[..len], &mut payload);
1008 let Ingress::Dispatch(dispatch) = engine.begin(&ALICE, &payload[..len], 0, 0, &mut out)
1009 else {
1010 panic!("expected a dispatch");
1011 };
1012 assert!(dispatch.resets);
1013 assert_eq!(engine.complete(Produced::no_response(), &mut out), Ok(None));
1014 }
1015
1016 let mut payload = [0u8; PAYLOAD];
1019 let len = request([1, 1], &frame_buf[..get_len], &mut payload);
1020 assert!(matches!(
1021 engine.begin(&ALICE, &payload[..len], 0, 0, &mut out),
1022 Ingress::Dispatch(_)
1023 ));
1024 }
1025
1026 #[test]
1027 fn a_reply_that_overruns_the_budget_is_refused_rather_than_truncated() {
1028 let mut engine = Engine::new(1);
1029 let mut frame_buf = [0u8; 8];
1030 let frame_len = get(prop::CAPS, &mut frame_buf);
1031 let frame = &frame_buf[..frame_len];
1032 let mut payload = [0u8; PAYLOAD];
1033 let len = request([1, 2], frame, &mut payload);
1034
1035 let mut out = [0u8; PAYLOAD];
1036 let Ingress::Dispatch(dispatch) = engine.begin(&ALICE, &payload[..len], 0, 0, &mut out)
1037 else {
1038 panic!("expected a dispatch");
1039 };
1040
1041 let oversized = [0u8; PAYLOAD];
1042 assert_eq!(
1043 engine.complete(
1044 Produced::complete(&oversized[..dispatch.budget + 1]),
1045 &mut out[..PAYLOAD]
1046 ),
1047 Err(CompleteError::TooLarge)
1048 );
1049 }
1050
1051 #[test]
1052 fn a_reply_exactly_at_the_budget_fits_even_when_it_acquires_a_cursor() {
1053 let mut engine = Engine::new(1);
1054 let mut frame_buf = [0u8; 8];
1055 let frame_len = get(prop::CAPS, &mut frame_buf);
1056 let frame = &frame_buf[..frame_len];
1057 let mut payload = [0u8; PAYLOAD];
1058 let len = request([1, 2], frame, &mut payload);
1059
1060 let mut out = [0u8; PAYLOAD];
1061 let Ingress::Dispatch(dispatch) = engine.begin(&ALICE, &payload[..len], 0, 0, &mut out)
1062 else {
1063 panic!("expected a dispatch");
1064 };
1065 let full = [0u8; PAYLOAD];
1066 let len = engine
1067 .complete(
1068 Produced::fragment(&full[..dispatch.budget], 100, 200),
1069 &mut out,
1070 )
1071 .expect("the budget already allows for the worst envelope")
1072 .expect("a response");
1073 assert!(len <= PAYLOAD);
1074 }
1075}