asyn_rs/interpose/mod.rs
1#![allow(dead_code)]
2//! Interpose (middleware) framework for layered I/O processing.
3//!
4//! Currently implements octet-level interpose only. The pattern is designed
5//! so that other interface types (e.g., `int32`) can follow the same structure.
6//!
7//! # Architecture
8//!
9//! An [`OctetInterposeStack`] holds a chain of [`OctetInterpose`] layers.
10//! When I/O is dispatched, an `InterposeChain` cursor walks the stack
11//! from outermost to innermost, finally reaching the base driver (which
12//! implements [`OctetNext`]).
13
14pub mod com;
15pub mod delay;
16pub mod echo;
17pub mod eos;
18pub mod flush;
19
20use bitflags::bitflags;
21
22use crate::error::AsynResult;
23use crate::user::AsynUser;
24
25bitflags! {
26 /// End-of-message reason flags (mirrors C asyn's asynEomReason).
27 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
28 pub struct EomReason: u32 {
29 /// Transfer completed because byte count was reached.
30 const CNT = 0x01;
31 /// Transfer completed because EOS character was detected.
32 const EOS = 0x02;
33 /// Transfer completed because END indicator (e.g. EOI) was asserted.
34 const END = 0x04;
35 }
36}
37
38/// Result of an octet read operation.
39#[derive(Debug, Clone)]
40pub struct OctetReadResult {
41 /// Number of bytes actually transferred into the buffer.
42 pub nbytes_transferred: usize,
43 /// Reason(s) the read terminated.
44 pub eom_reason: EomReason,
45}
46
47/// The transfer an octet read completed *before* it failed — C's
48/// `*nbytesTransfered` / `*eomReason`, which `asynOctet::read` writes out
49/// even when it returns a failing `asynStatus`
50/// (`asynInterposeEos.c:242-253`).
51///
52/// This owns the bytes rather than pointing at the caller's buffer: it rides
53/// inside [`crate::error::AsynError::PartialRead`], so the data and the
54/// status are one value and a `?` on the read cannot deliver the failure
55/// while dropping the bytes. Every dispatch hop between the interpose and a
56/// record (`port_actor` → `PortHandle` → device support / `SyncIO`) hands the
57/// error on by value, so the transfer arrives with it.
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct PartialOctetRead {
60 /// The bytes transferred into the caller's buffer before the failure.
61 pub data: Vec<u8>,
62 /// The end-of-message reason accumulated up to the failure.
63 pub eom_reason: EomReason,
64}
65
66impl PartialOctetRead {
67 /// C's `*nbytesTransfered` for this failed read.
68 pub fn nbytes_transferred(&self) -> usize {
69 self.data.len()
70 }
71}
72
73/// "Next layer" interface — implemented by both the base driver adapter
74/// and by `InterposeChain` to allow recursive dispatch.
75pub trait OctetNext: Send + Sync {
76 fn read(&mut self, user: &AsynUser, buf: &mut [u8]) -> AsynResult<OctetReadResult>;
77 fn write(&mut self, user: &mut AsynUser, data: &[u8]) -> AsynResult<usize>;
78 fn flush(&mut self, user: &mut AsynUser) -> AsynResult<()>;
79}
80
81/// Interpose layer for octet (byte-stream) I/O.
82///
83/// Each layer receives the `next` handle to delegate to the layer below.
84pub trait OctetInterpose: Send + Sync {
85 fn read(
86 &mut self,
87 user: &AsynUser,
88 buf: &mut [u8],
89 next: &mut dyn OctetNext,
90 ) -> AsynResult<OctetReadResult>;
91
92 fn write(
93 &mut self,
94 user: &mut AsynUser,
95 data: &[u8],
96 next: &mut dyn OctetNext,
97 ) -> AsynResult<usize>;
98
99 fn flush(&mut self, user: &mut AsynUser, next: &mut dyn OctetNext) -> AsynResult<()>;
100
101 /// Told the port's device model when the layer is installed. Default no-op;
102 /// only a layer that holds per-device state needs it.
103 ///
104 /// C creates one interpose instance per (port, addr) — the addr is an
105 /// argument of `asynInterposeEosConfig` (asynInterposeEos.c:84-110) — so a
106 /// layer's state is per device by construction. A Rust stack is per port, so
107 /// a layer with device state keys it by the `asynUser`'s addr instead, and
108 /// [`crate::port::eos_device_key`] needs this flag to know whether the port
109 /// has devices to key by at all.
110 fn attach_port(&mut self, _multi_device: bool) {}
111
112 /// Notify the layer of an input end-of-string change *for the device the
113 /// `asynUser` addressed*. Default no-op; only EOS-aware layers
114 /// (`eos::EosInterpose`) act on it. C asyn routes `setInputEos` through
115 /// every interpose via `pasynOctet->setInputEos`, `asynUser` and all
116 /// (asynInterposeEos.c:288); this is the Rust equivalent so a runtime IEOS
117 /// change reaches the installed EOS interpose on the right device.
118 fn set_input_eos(&mut self, _addr: i32, _eos: &[u8]) {}
119
120 /// Notify the layer of an output end-of-string change (see
121 /// [`Self::set_input_eos`]). Default no-op.
122 fn set_output_eos(&mut self, _addr: i32, _eos: &[u8]) {}
123
124 /// Drop every piece of state scoped to the *current* link, because the
125 /// port's connection state just changed (connected → disconnected or
126 /// back). Default no-op: a layer whose state is pure configuration
127 /// (`delay`, `flush`, `echo`) survives a reconnect unchanged.
128 ///
129 /// C parity: `asynInterposeEos.c:110` registers `eosInExceptionHandler`
130 /// via `exceptionCallbackAdd`, and `:142-151` clears `inBufHead`,
131 /// `inBufTail` and `eosInMatch` on `asynExceptionConnect` — which
132 /// `exceptionConnect` (asynManager.c:2158) *and* `exceptionDisconnect`
133 /// (asynManager.c:2185) both fire, so the reset happens on either edge.
134 /// [`crate::port::PortDriverBase::set_connected`] is the Rust owner of
135 /// that transition and drives this hook for the whole stack.
136 fn connection_changed(&mut self) {}
137}
138
139/// The key of the port's own interpose chain — C's `pport->dpc`, the
140/// `dpCommon` every addr that names no device resolves to
141/// (`findDpCommon`/`findInterface`, asynManager.c:536-551, 1493-1501).
142pub const PORT_CHAIN: i32 = -1;
143
144/// The octet interpose layers of one port — C's `dpCommon.interposeInterfaceList`,
145/// which exists once per *device* as well as once per port.
146///
147/// `interposeInterface` takes an `addr` (asynManager.c:2190-2220): `addr >= 0` on
148/// a multi-device port puts the layer on that DEVICE's list (:2202-2206), and
149/// `findInterface` resolves a request device-first, port-second (:1493-1501). So
150/// `asynInterposeDelay("gpib", 4, 0.01)` slows device 4 and nothing else — a
151/// single port-wide chain slowed every device on the bus (R15-48).
152///
153/// A device's chain **shadows** the port's rather than extending it, because
154/// that is what C builds: a layer installed on a device whose list was empty
155/// takes its `pPrev` from the *driver's* `interfaceList` (:2211-2215), not from
156/// the port's interposes, so it delegates straight down to the driver.
157pub struct OctetInterposeStack {
158 /// [`PORT_CHAIN`] plus one entry per device that has an interpose of its own.
159 chains: std::collections::BTreeMap<i32, Vec<Box<dyn OctetInterpose>>>,
160 /// The port's device model, handed to every layer at install time
161 /// ([`OctetInterpose::attach_port`]) and what decides whether an addr can
162 /// name a device at all — C `locateDevice` returns none for a port that is
163 /// not `ASYN_MULTIDEVICE` (asynManager.c:574).
164 multi_device: bool,
165}
166
167impl OctetInterposeStack {
168 pub fn new(multi_device: bool) -> Self {
169 Self {
170 chains: std::collections::BTreeMap::new(),
171 multi_device,
172 }
173 }
174
175 /// Install an interpose layer on the device `addr` names — C
176 /// `interposeInterface` (asynManager.c:2190-2220). `addr < 0`, or any addr on
177 /// a port that is not multi-device, installs on the port itself
178 /// ([`crate::port::eos_device_key`], C's `locateDevice` at :574).
179 ///
180 /// The new layer *becomes* that `dpCommon`'s octet interface (C overwrites the
181 /// interpose node's `pasynInterface` with it, :2217) and the interface it
182 /// displaced — the previously installed interpose at the same level, or the
183 /// driver's own interface when there was none — becomes the one it delegates
184 /// down to (C hands it back as `pPrev`, :2209-2215).
185 ///
186 /// So the **last layer installed is the outermost**: a caller enters it first,
187 /// and it calls down through the earlier layers to the driver. An
188 /// `asynInterposeEcho` installed from iocsh after the driver's configure-time
189 /// EOS layer therefore sits *above* EOS, exactly as in C. Dispatch walks index
190 /// 0 first, so the new layer goes to the front.
191 pub fn install(&mut self, addr: i32, mut layer: Box<dyn OctetInterpose>) {
192 layer.attach_port(self.multi_device);
193 self.chains
194 .entry(crate::port::eos_device_key(self.multi_device, addr))
195 .or_default()
196 .insert(0, layer);
197 }
198
199 /// The chain a request on `addr` runs through — C `findInterface`
200 /// (asynManager.c:1493-1501): the device's own interposes if it has any,
201 /// otherwise the port's.
202 fn chain_key(&self, addr: i32) -> i32 {
203 let key = crate::port::eos_device_key(self.multi_device, addr);
204 if key != PORT_CHAIN && self.chains.get(&key).is_some_and(|c| !c.is_empty()) {
205 key
206 } else {
207 PORT_CHAIN
208 }
209 }
210
211 fn chain_mut(&mut self, addr: i32) -> Option<&mut Vec<Box<dyn OctetInterpose>>> {
212 let key = self.chain_key(addr);
213 self.chains.get_mut(&key).filter(|c| !c.is_empty())
214 }
215
216 /// Total number of interpose layers on the port, across every device's chain
217 /// — what `asynReport` counts (asynManager.c:993-1005 walks each `dpCommon`'s
218 /// list).
219 pub fn len(&self) -> usize {
220 self.chains.values().map(Vec::len).sum()
221 }
222
223 pub fn is_empty(&self) -> bool {
224 self.len() == 0
225 }
226
227 /// Number of layers on the chain the given addr resolves to.
228 pub fn len_for(&self, addr: i32) -> usize {
229 self.chains
230 .get(&self.chain_key(addr))
231 .map_or(0, |c| c.len())
232 }
233
234 /// Forward an input EOS change to the chain the addressed device resolves to.
235 /// EOS-aware layers update that device's terminator; others ignore it (trait
236 /// default). C routes `setInputEos` through the `asynOctet` interface
237 /// `findInterface` returned for that `asynUser`, so it reaches exactly the
238 /// layers that will serve the device's reads.
239 pub fn set_input_eos(&mut self, addr: i32, eos: &[u8]) {
240 if let Some(chain) = self.chain_mut(addr) {
241 for layer in chain {
242 layer.set_input_eos(addr, eos);
243 }
244 }
245 }
246
247 /// Forward an output EOS change (see [`Self::set_input_eos`]).
248 pub fn set_output_eos(&mut self, addr: i32, eos: &[u8]) {
249 if let Some(chain) = self.chain_mut(addr) {
250 for layer in chain {
251 layer.set_output_eos(addr, eos);
252 }
253 }
254 }
255
256 /// Tell every layer, on every device's chain, that the port's connection
257 /// state changed, so any link-scoped state (read-ahead buffers, partial
258 /// terminator match position) is dropped before the new link delivers its
259 /// first byte. Mirrors C's per-interpose `asynExceptionConnect` handlers
260 /// (`asynInterposeEos.c:142-151`); the only caller is the transition owner
261 /// [`crate::port::PortDriverBase::set_connected`], and the link it moved is
262 /// the one under *all* of them.
263 pub fn connection_changed(&mut self) {
264 for chain in self.chains.values_mut() {
265 for layer in chain {
266 layer.connection_changed();
267 }
268 }
269 }
270
271 /// Dispatch a read through the addressed device's interpose chain, ending at
272 /// `base`.
273 pub fn dispatch_read(
274 &mut self,
275 user: &AsynUser,
276 buf: &mut [u8],
277 base: &mut dyn OctetNext,
278 ) -> AsynResult<OctetReadResult> {
279 let addr = user.addr;
280 let Some(layers) = self.chain_mut(addr) else {
281 return base.read(user, buf);
282 };
283 InterposeChain {
284 layers: layers.as_mut_slice(),
285 base,
286 }
287 .read(user, buf)
288 }
289
290 /// Dispatch a write through the addressed device's interpose chain.
291 pub fn dispatch_write(
292 &mut self,
293 user: &mut AsynUser,
294 data: &[u8],
295 base: &mut dyn OctetNext,
296 ) -> AsynResult<usize> {
297 let addr = user.addr;
298 let Some(layers) = self.chain_mut(addr) else {
299 return base.write(user, data);
300 };
301 InterposeChain {
302 layers: layers.as_mut_slice(),
303 base,
304 }
305 .write(user, data)
306 }
307
308 /// Dispatch a flush through the addressed device's interpose chain.
309 pub fn dispatch_flush(
310 &mut self,
311 user: &mut AsynUser,
312 base: &mut dyn OctetNext,
313 ) -> AsynResult<()> {
314 let addr = user.addr;
315 let Some(layers) = self.chain_mut(addr) else {
316 return base.flush(user);
317 };
318 InterposeChain {
319 layers: layers.as_mut_slice(),
320 base,
321 }
322 .flush(user)
323 }
324}
325
326impl Default for OctetInterposeStack {
327 /// A stack on a single-device port — the common case, and the one where
328 /// every addr collapses onto one EOS entry.
329 fn default() -> Self {
330 Self::new(false)
331 }
332}
333
334/// Cursor that walks the interpose stack via recursive `split_first_mut`.
335struct InterposeChain<'a> {
336 layers: &'a mut [Box<dyn OctetInterpose>],
337 base: &'a mut dyn OctetNext,
338}
339
340impl OctetNext for InterposeChain<'_> {
341 fn read(&mut self, user: &AsynUser, buf: &mut [u8]) -> AsynResult<OctetReadResult> {
342 if let Some((first, rest)) = self.layers.split_first_mut() {
343 let mut next = InterposeChain {
344 layers: rest,
345 base: self.base,
346 };
347 first.read(user, buf, &mut next)
348 } else {
349 self.base.read(user, buf)
350 }
351 }
352
353 fn write(&mut self, user: &mut AsynUser, data: &[u8]) -> AsynResult<usize> {
354 if let Some((first, rest)) = self.layers.split_first_mut() {
355 let mut next = InterposeChain {
356 layers: rest,
357 base: self.base,
358 };
359 first.write(user, data, &mut next)
360 } else {
361 self.base.write(user, data)
362 }
363 }
364
365 fn flush(&mut self, user: &mut AsynUser) -> AsynResult<()> {
366 if let Some((first, rest)) = self.layers.split_first_mut() {
367 let mut next = InterposeChain {
368 layers: rest,
369 base: self.base,
370 };
371 first.flush(user, &mut next)
372 } else {
373 self.base.flush(user)
374 }
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381 use crate::user::AsynUser;
382
383 /// A base driver that just records calls.
384 struct MockBase {
385 read_data: Vec<u8>,
386 written: Vec<u8>,
387 flushed: bool,
388 }
389
390 impl MockBase {
391 fn new(data: &[u8]) -> Self {
392 Self {
393 read_data: data.to_vec(),
394 written: Vec::new(),
395 flushed: false,
396 }
397 }
398 }
399
400 impl OctetNext for MockBase {
401 fn read(&mut self, _user: &AsynUser, buf: &mut [u8]) -> AsynResult<OctetReadResult> {
402 let n = self.read_data.len().min(buf.len());
403 buf[..n].copy_from_slice(&self.read_data[..n]);
404 Ok(OctetReadResult {
405 nbytes_transferred: n,
406 eom_reason: EomReason::CNT,
407 })
408 }
409
410 fn write(&mut self, _user: &mut AsynUser, data: &[u8]) -> AsynResult<usize> {
411 self.written.extend_from_slice(data);
412 Ok(data.len())
413 }
414
415 fn flush(&mut self, _user: &mut AsynUser) -> AsynResult<()> {
416 self.flushed = true;
417 Ok(())
418 }
419 }
420
421 /// A simple pass-through interpose layer.
422 struct PassthroughInterpose;
423
424 impl OctetInterpose for PassthroughInterpose {
425 fn read(
426 &mut self,
427 user: &AsynUser,
428 buf: &mut [u8],
429 next: &mut dyn OctetNext,
430 ) -> AsynResult<OctetReadResult> {
431 next.read(user, buf)
432 }
433 fn write(
434 &mut self,
435 user: &mut AsynUser,
436 data: &[u8],
437 next: &mut dyn OctetNext,
438 ) -> AsynResult<usize> {
439 next.write(user, data)
440 }
441 fn flush(&mut self, user: &mut AsynUser, next: &mut dyn OctetNext) -> AsynResult<()> {
442 next.flush(user)
443 }
444 }
445
446 /// An interpose that uppercases data on write.
447 struct UppercaseInterpose;
448
449 impl OctetInterpose for UppercaseInterpose {
450 fn read(
451 &mut self,
452 user: &AsynUser,
453 buf: &mut [u8],
454 next: &mut dyn OctetNext,
455 ) -> AsynResult<OctetReadResult> {
456 next.read(user, buf)
457 }
458 fn write(
459 &mut self,
460 user: &mut AsynUser,
461 data: &[u8],
462 next: &mut dyn OctetNext,
463 ) -> AsynResult<usize> {
464 let upper: Vec<u8> = data.iter().map(|b| b.to_ascii_uppercase()).collect();
465 next.write(user, &upper)
466 }
467 fn flush(&mut self, user: &mut AsynUser, next: &mut dyn OctetNext) -> AsynResult<()> {
468 next.flush(user)
469 }
470 }
471
472 #[test]
473 fn test_empty_stack_passthrough() {
474 let mut stack = OctetInterposeStack::new(false);
475 let mut base = MockBase::new(b"hello");
476 let user = AsynUser::default();
477 let mut buf = [0u8; 32];
478
479 let result = stack.dispatch_read(&user, &mut buf, &mut base).unwrap();
480 assert_eq!(result.nbytes_transferred, 5);
481 assert_eq!(&buf[..5], b"hello");
482 }
483
484 #[test]
485 fn test_single_passthrough_layer() {
486 let mut stack = OctetInterposeStack::new(false);
487 stack.install(-1, Box::new(PassthroughInterpose));
488
489 let mut base = MockBase::new(b"world");
490 let user = AsynUser::default();
491 let mut buf = [0u8; 32];
492
493 let result = stack.dispatch_read(&user, &mut buf, &mut base).unwrap();
494 assert_eq!(result.nbytes_transferred, 5);
495 assert_eq!(&buf[..5], b"world");
496 }
497
498 #[test]
499 fn test_uppercase_interpose_write() {
500 let mut stack = OctetInterposeStack::new(false);
501 stack.install(-1, Box::new(UppercaseInterpose));
502
503 let mut base = MockBase::new(b"");
504 let mut user = AsynUser::default();
505
506 let n = stack
507 .dispatch_write(&mut user, b"hello", &mut base)
508 .unwrap();
509 assert_eq!(n, 5);
510 assert_eq!(&base.written, b"HELLO");
511 }
512
513 #[test]
514 fn test_multi_layer_chain() {
515 let mut stack = OctetInterposeStack::new(false);
516 stack.install(-1, Box::new(PassthroughInterpose));
517 stack.install(-1, Box::new(UppercaseInterpose));
518 assert_eq!(stack.len(), 2);
519
520 let mut base = MockBase::new(b"");
521 let mut user = AsynUser::default();
522
523 // UppercaseInterpose (installed last, so outermost) -> Passthrough -> base
524 stack.dispatch_write(&mut user, b"test", &mut base).unwrap();
525 assert_eq!(&base.written, b"TEST");
526 }
527
528 #[test]
529 fn test_flush_dispatch() {
530 let mut stack = OctetInterposeStack::new(false);
531 stack.install(-1, Box::new(PassthroughInterpose));
532
533 let mut base = MockBase::new(b"");
534 let mut user = AsynUser::default();
535
536 stack.dispatch_flush(&mut user, &mut base).unwrap();
537 assert!(base.flushed);
538 }
539
540 /// R9-56. C `interposeInterface` (asynManager.c:2209-2217) makes each newly
541 /// installed layer the port's octet interface and hands it the one it
542 /// displaced to call down into, so the *last* install is the *outermost*.
543 /// This stack appended and dispatched from index 0, so a later install landed
544 /// *innermost* — the exact inverse. Under the inverted rule an
545 /// `asynInterposeEcho`/`asynInterposeDelay` installed from iocsh sank *below*
546 /// the EOS layer the driver installs at configure time, when C puts it above.
547 #[test]
548 fn the_last_layer_installed_is_the_outermost() {
549 /// Marks the payload with its own tag on the way down, so the base's
550 /// buffer records the order the layers ran in.
551 struct Tag(u8);
552 impl OctetInterpose for Tag {
553 fn read(
554 &mut self,
555 user: &AsynUser,
556 buf: &mut [u8],
557 next: &mut dyn OctetNext,
558 ) -> AsynResult<OctetReadResult> {
559 next.read(user, buf)
560 }
561 fn write(
562 &mut self,
563 user: &mut AsynUser,
564 data: &[u8],
565 next: &mut dyn OctetNext,
566 ) -> AsynResult<usize> {
567 let mut tagged = vec![self.0];
568 tagged.extend_from_slice(data);
569 next.write(user, &tagged)
570 }
571 fn flush(&mut self, user: &mut AsynUser, next: &mut dyn OctetNext) -> AsynResult<()> {
572 next.flush(user)
573 }
574 }
575
576 let mut stack = OctetInterposeStack::new(false);
577 stack.install(-1, Box::new(Tag(b'A')));
578 stack.install(-1, Box::new(Tag(b'B')));
579 stack.install(-1, Box::new(Tag(b'C')));
580
581 let mut base = MockBase::new(b"");
582 let mut user = AsynUser::default();
583 stack.dispatch_write(&mut user, b"x", &mut base).unwrap();
584
585 // C's chain is C -> B -> A -> driver: the caller enters the last-installed
586 // layer first, and each calls down into the one it displaced, so the
587 // driver sees the tags in install order. The inverted stack ran A first
588 // and delivered b"CBAx".
589 assert_eq!(&base.written, b"ABCx");
590 }
591
592 /// R15-48: an interpose installed with an `addr` serves that DEVICE, not the
593 /// whole port.
594 ///
595 /// C `interposeInterface(portName, addr, ...)` puts the layer on the device's
596 /// `dpCommon.interposeInterfaceList` when `addr >= 0` (asynManager.c:2202-2206),
597 /// and `findInterface` resolves a request device-first, port-second
598 /// (:1493-1501). Both iocsh interposes pass their addr
599 /// (asynInterposeEcho.c:176, asynInterposeDelay.c:187) — so
600 /// `asynInterposeDelay("gpib",4,0.01)` slows device 4 and nothing else. The
601 /// stack was one port-wide chain, so it slowed every device on the bus.
602 ///
603 /// One case per boundary: the addressed device, an unaddressed sibling, the
604 /// port-level chain as fallback, and the single-device port that collapses
605 /// every addr onto one chain.
606 #[test]
607 fn a_device_addressed_interpose_serves_only_that_device() {
608 struct Tag(u8);
609 impl OctetInterpose for Tag {
610 fn read(
611 &mut self,
612 user: &AsynUser,
613 buf: &mut [u8],
614 next: &mut dyn OctetNext,
615 ) -> AsynResult<OctetReadResult> {
616 next.read(user, buf)
617 }
618 fn write(
619 &mut self,
620 user: &mut AsynUser,
621 data: &[u8],
622 next: &mut dyn OctetNext,
623 ) -> AsynResult<usize> {
624 let mut tagged = vec![self.0];
625 tagged.extend_from_slice(data);
626 next.write(user, &tagged)
627 }
628 fn flush(&mut self, user: &mut AsynUser, next: &mut dyn OctetNext) -> AsynResult<()> {
629 next.flush(user)
630 }
631 }
632
633 let write_from = |stack: &mut OctetInterposeStack, addr: i32| {
634 let mut base = MockBase::new(b"");
635 let mut user = AsynUser::new(0).with_addr(addr);
636 stack.dispatch_write(&mut user, b"x", &mut base).unwrap();
637 base.written
638 };
639
640 // A multi-device port: a layer on device 4, another on device 2.
641 let mut stack = OctetInterposeStack::new(true);
642 stack.install(4, Box::new(Tag(b'D')));
643 stack.install(2, Box::new(Tag(b'E')));
644
645 assert_eq!(
646 write_from(&mut stack, 4),
647 b"Dx",
648 "device 4 runs its own layer"
649 );
650 assert_eq!(
651 write_from(&mut stack, 2),
652 b"Ex",
653 "device 2 runs its own layer"
654 );
655 assert_eq!(
656 write_from(&mut stack, 7),
657 b"x",
658 "a device with no interpose of its own runs none — C's findInterface \
659 falls back to the port's list, which is empty here"
660 );
661 assert_eq!(stack.len(), 2, "two layers on the port, one per device");
662 assert_eq!(stack.len_for(4), 1);
663 assert_eq!(stack.len_for(7), 0);
664
665 // The port's own chain (addr < 0) is what every unaddressed device falls
666 // back to — C `findInterface`'s second lookup (:1499-1501).
667 stack.install(PORT_CHAIN, Box::new(Tag(b'P')));
668 assert_eq!(
669 write_from(&mut stack, 7),
670 b"Px",
671 "a device with no chain of its own falls back to the port's"
672 );
673 assert_eq!(
674 write_from(&mut stack, 4),
675 b"Dx",
676 "...and a device WITH one keeps running only its own: C gives it the \
677 driver's interface as pPrev, not the port's interposes (:2211-2215)"
678 );
679
680 // A port that never declared ASYN_MULTIDEVICE has no devices to key by:
681 // C's `locateDevice` returns none for it (:574), so every addr — the
682 // iocsh default 0 included — lands on the port itself.
683 let mut single = OctetInterposeStack::new(false);
684 single.install(0, Box::new(Tag(b'S')));
685 assert_eq!(write_from(&mut single, 0), b"Sx");
686 assert_eq!(write_from(&mut single, 4), b"Sx");
687 assert_eq!(write_from(&mut single, -1), b"Sx");
688 }
689}