Skip to main content

darkbio_wire/memory/
mod.rs

1// wire-rs: encrypted protocol between Ark and host
2// Copyright 2026 Dark Bio AG. All rights reserved.
3//
4// Use of this source code is governed by a BSD-style
5// license that can be found in the LICENSE file.
6
7//! Portable in-memory streams for local connections, emulators and tests.
8//!
9//! These streams synchronize through the clock's locks, with no sockets or
10//! worker threads. Split an endpoint with [`Duplex::into_halves`] for plain I/O.
11
12use crate::transport::{Read, Stream, Write};
13use darkbio_clock::Clock;
14use darkbio_clock::sync::{Condvar, Mutex, MutexGuard};
15use std::collections::VecDeque;
16use std::io;
17use std::sync::Arc;
18use std::time::Instant;
19
20/// One endpoint of an in-memory duplex connection, ready for a client or server.
21pub type Duplex = Stream<Reader, Writer>;
22
23impl Duplex {
24    /// Takes the reader and writer out of the stream without closing either half.
25    ///
26    /// Each half closes its own direction on drop. Dropping the writer lets the
27    /// peer drain accepted output before EOF; dropping the reader refuses further
28    /// peer writes. Obtain a [`crate::transport::Closer`] with [`Self::closer`] before splitting
29    /// if you need to shut down both halves from another thread.
30    ///
31    /// The stream's write timeout is discarded. Any deadlines already installed
32    /// on the halves are retained; new halves have no deadline until configured.
33    ///
34    /// ```
35    /// use darkbio_wire::{clock::Clock, memory};
36    /// use darkbio_wire::transport::Client;
37    ///
38    /// let (host, bus) = memory::duplex(64 * 1024, &Clock::real());
39    /// let (reader, writer) = bus.into_halves();
40    /// let client = Client::new(host);
41    /// // Move `reader` and `writer` to the bus's input and output pumps.
42    /// ```
43    pub fn into_halves(self) -> (Reader, Writer) {
44        let (reader, writer, _, _) = self.into_parts();
45        (reader, writer)
46    }
47}
48
49/// Creates two connected streams with `capacity` bytes of buffering per direction.
50/// Both measure their deadlines on `clock`, and so does any transport on them.
51///
52/// Reads wait for data and writes wait for buffer space, bounded by the deadlines
53/// installed by Wire. Partial progress never refreshes a deadline. A timeout
54/// leaves the connection reusable and preserves any bytes already accepted.
55/// Flush checks its deadline but does not wait for the peer to consume output.
56/// Reads deliver available bytes, EOF and empty reads even after their deadline;
57/// the deadline only limits waiting for input. Writes and flushes refuse expired
58/// deadlines even if buffer space is available. Wire enforces its own deadlines
59/// before calling either half.
60///
61/// Closing or dropping an endpoint wakes blocked I/O on both sides. Its unread
62/// input is discarded; the peer can drain its accepted output before receiving
63/// EOF. Further nonempty writes fail with [`io::ErrorKind::BrokenPipe`]. A flush
64/// fails the same way only if the peer closed with accepted output still unread.
65/// Output the peer had consumed before closing flushes fine afterwards.
66///
67/// Both peers must run concurrently when exchanging data. Allow enough capacity
68/// for the handshake's initial output; `64 * 1024` is a useful starting point.
69/// Small buffers can cause handshake backpressure, just as a real stream can.
70///
71/// # Panics
72///
73/// Panics if `capacity` is zero.
74pub fn duplex(capacity: usize, clock: &Clock) -> (Duplex, Duplex) {
75    // Validate capacity before allocating either direction
76    assert!(capacity > 0, "duplex capacity must be nonzero");
77
78    // Give both endpoints the same clock and independent buffers
79    let incoming = Arc::new(Pipe::new(capacity, clock));
80    let outgoing = Arc::new(Pipe::new(capacity, clock));
81    (
82        endpoint(incoming.clone(), outgoing.clone()),
83        endpoint(outgoing, incoming),
84    )
85}
86
87/// Bundles independent I/O halves with shutdown that wakes both directions.
88fn endpoint(incoming: Arc<Pipe>, outgoing: Arc<Pipe>) -> Duplex {
89    Stream::new(
90        Reader {
91            pipe: incoming.clone(),
92            deadline: None,
93        },
94        Writer {
95            pipe: outgoing.clone(),
96            deadline: None,
97        },
98        move || {
99            incoming.close_reader();
100            outgoing.close_writer();
101        },
102    )
103}
104
105/// Receiving half of a [`Duplex`], with an independently configured read deadline.
106#[derive(Debug)]
107pub struct Reader {
108    pipe: Arc<Pipe>,
109    deadline: Option<Instant>,
110}
111
112impl io::Read for Reader {
113    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
114        // Recheck the buffer and closure state after every wake
115        let mut state = self.pipe.lock();
116        loop {
117            // Empty reads and local closure need no buffered input
118            if buf.is_empty() || !state.reader_open {
119                return Ok(0);
120            }
121
122            // Deliver ready bytes even after the installed read deadline
123            if !state.bytes.is_empty() {
124                let count = buf.len().min(state.bytes.len());
125                for (out, byte) in buf.iter_mut().zip(state.bytes.drain(..count)) {
126                    *out = byte;
127                }
128                drop(state);
129                self.pipe.changed.notify_all();
130                return Ok(count);
131            }
132
133            // End at peer closure or wait within the remaining clock budget
134            if !state.writer_open {
135                return Ok(0);
136            }
137            self.pipe.check_deadline(self.deadline)?;
138            state = self.pipe.wait(state, self.deadline);
139        }
140    }
141}
142
143impl Read for Reader {
144    fn clock(&self) -> Clock {
145        self.pipe.clock.clone()
146    }
147
148    fn set_read_deadline(&mut self, deadline: Option<Instant>) -> io::Result<()> {
149        self.deadline = deadline;
150        Ok(())
151    }
152}
153
154impl Drop for Reader {
155    fn drop(&mut self) {
156        self.pipe.close_reader();
157    }
158}
159
160#[cfg(test)]
161impl Reader {
162    /// Makes any later wait for input panic instead of blocking, so a test
163    /// expecting an immediate return fails rather than hangs.
164    pub(crate) fn forbid_waits(&self) {
165        self.pipe.lock().waits_forbidden = true;
166    }
167}
168
169/// Sending half of a [`Duplex`], sharing one deadline across writes and flushes.
170#[derive(Debug)]
171pub struct Writer {
172    pipe: Arc<Pipe>,
173    deadline: Option<Instant>,
174}
175
176impl io::Write for Writer {
177    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
178        // Recheck capacity and the same deadline after every wake
179        let mut state = self.pipe.lock();
180        loop {
181            // Refuse expiry and closure before accepting any bytes
182            self.pipe.check_deadline(self.deadline)?;
183            if buf.is_empty() {
184                return Ok(0);
185            }
186            if !state.reader_open || !state.writer_open {
187                return Err(io::ErrorKind::BrokenPipe.into());
188            }
189
190            // Fill available space and wake the reader after releasing the lock
191            let count = buf.len().min(self.pipe.capacity - state.bytes.len());
192            if count > 0 {
193                state.bytes.extend(&buf[..count]);
194                drop(state);
195                self.pipe.changed.notify_all();
196                return Ok(count);
197            }
198
199            // Wait for space without extending the deadline
200            state = self.pipe.wait(state, self.deadline);
201        }
202    }
203
204    fn flush(&mut self) -> io::Result<()> {
205        // Check the installed deadline while observing a consistent pipe state
206        let state = self.pipe.lock();
207        self.pipe.check_deadline(self.deadline)?;
208
209        // Reject local closure or output lost when the peer closed
210        if !state.writer_open || state.lost {
211            return Err(io::ErrorKind::BrokenPipe.into());
212        }
213        Ok(())
214    }
215}
216
217impl Write for Writer {
218    fn clock(&self) -> Clock {
219        self.pipe.clock.clone()
220    }
221
222    fn set_write_deadline(&mut self, deadline: Instant) -> io::Result<()> {
223        self.deadline = Some(deadline);
224        Ok(())
225    }
226}
227
228impl Drop for Writer {
229    fn drop(&mut self) {
230        self.pipe.close_writer();
231    }
232}
233
234/// Shared bounded buffer for one direction. Waiting always releases the mutex.
235#[derive(Debug)]
236struct Pipe {
237    /// Clock used to check the installed I/O deadlines.
238    clock: Clock,
239    capacity: usize,
240    state: Mutex<State>,
241    changed: Condvar,
242}
243
244#[derive(Debug)]
245struct State {
246    bytes: VecDeque<u8>,
247    reader_open: bool,
248    writer_open: bool,
249    lost: bool, // Accepted output the reader closed without consuming
250    #[cfg(test)]
251    waiting: usize,
252    #[cfg(test)]
253    waits_forbidden: bool, // Fails a wait instead of blocking, so a test cannot hang
254}
255
256impl Pipe {
257    fn new(capacity: usize, clock: &Clock) -> Self {
258        Self {
259            clock: clock.clone(),
260            capacity,
261            state: Mutex::new(State {
262                bytes: VecDeque::with_capacity(capacity),
263                reader_open: true,
264                writer_open: true,
265                lost: false,
266                #[cfg(test)]
267                waiting: 0,
268                #[cfg(test)]
269                waits_forbidden: false,
270            }),
271            changed: Condvar::new(clock),
272        }
273    }
274
275    fn lock(&self) -> MutexGuard<'_, State> {
276        // Shutdown must remain usable even after a panic in an I/O operation.
277        self.state.lock().unwrap_or_else(|err| err.into_inner())
278    }
279
280    /// Releases the buffer until notified or the installed deadline is reached.
281    fn wait<'a>(
282        &self,
283        state: MutexGuard<'a, State>,
284        deadline: Option<Instant>,
285    ) -> MutexGuard<'a, State> {
286        // Let tests observe the wait before it releases the mutex, or refuse it
287        #[cfg(test)]
288        let state = {
289            let mut state = state;
290            assert!(!state.waits_forbidden, "memory pipe waited");
291            state.waiting += 1;
292            self.changed.notify_all();
293            state
294        };
295
296        // Release the buffer lock until notified or the current budget runs out
297        let state = match deadline {
298            Some(deadline) => {
299                self.changed
300                    .wait_deadline(state, deadline)
301                    .unwrap_or_else(|err| err.into_inner())
302                    .0
303            }
304            None => self
305                .changed
306                .wait(state)
307                .unwrap_or_else(|err| err.into_inner()),
308        };
309
310        // Remove the test's waiter count after the buffer lock is retaken
311        #[cfg(test)]
312        let state = {
313            let mut state = state;
314            state.waiting -= 1;
315            state
316        };
317        state
318    }
319
320    /// Refuses an expired deadline using the pipe's clock on every attempt.
321    fn check_deadline(&self, deadline: Option<Instant>) -> io::Result<()> {
322        if deadline.is_some_and(|deadline| self.clock.now() >= deadline) {
323            return Err(io::ErrorKind::TimedOut.into());
324        }
325        Ok(())
326    }
327
328    fn close_reader(&self) {
329        let mut state = self.lock();
330        state.reader_open = false;
331        state.lost |= !state.bytes.is_empty();
332        state.bytes.clear();
333        drop(state);
334        self.changed.notify_all();
335    }
336
337    fn close_writer(&self) {
338        self.lock().writer_open = false;
339        self.changed.notify_all();
340    }
341}
342
343#[cfg(test)]
344#[cfg_attr(coverage_nightly, coverage(off))]
345mod tests {
346    use super::*;
347    use crate::transport::Closer;
348    use crate::transport::testing::test_clock;
349    use darkbio_clock::TestClock;
350    use std::io::{Read as _, Write as _};
351    use std::sync::mpsc;
352    use std::thread;
353    use std::time::Duration;
354
355    /// Deadline exercised by timeout and reuse checks.
356    const TIMEOUT: Duration = Duration::from_millis(50);
357
358    /// Exposes standard I/O for adapter tests without closing the stream.
359    fn halves(stream: Duplex) -> (Reader, Writer, Closer) {
360        let closer = stream.closer();
361        let (reader, writer) = stream.into_halves();
362        (reader, writer, closer)
363    }
364
365    /// Waits until an operation has released its mutex in a condition-variable wait.
366    fn blocked(pipe: &Pipe) {
367        let mut state = pipe.lock();
368        while state.waiting == 0 {
369            state = pipe.changed.wait(state).unwrap();
370        }
371    }
372
373    // A blocked empty read expires when its clock reaches the installed deadline.
374    #[test]
375    fn test_blocked_read_expires_on_clock_deadline() {
376        // Park an empty read with a deadline a day ahead of real time
377        let mut tester = TestClock::new();
378        tester.advance(Duration::from_secs(86400));
379        let clock = tester.clock();
380        let (host, _peer) = duplex(1, &clock);
381        let (mut reader, _writer) = host.into_halves();
382        let deadline = clock.now() + Duration::from_secs(5);
383        reader.set_read_deadline(Some(deadline)).unwrap();
384        let reading = thread::spawn(move || reader.read(&mut [0]));
385        tester.wait_blocked(1);
386        assert_eq!(tester.next_deadline(), Some(deadline));
387
388        // Advance exactly to expiry and observe the blocked operation returning
389        tester.advance_to(deadline);
390        assert_eq!(
391            reading.join().unwrap().unwrap_err().kind(),
392            io::ErrorKind::TimedOut
393        );
394    }
395
396    // A blocked full write expires when its clock reaches the installed deadline.
397    #[test]
398    fn test_blocked_write_expires_on_clock_deadline() {
399        // Fill the pipe and park another write on the same fixed deadline
400        let mut tester = TestClock::new();
401        tester.advance(Duration::from_secs(86400));
402        let clock = tester.clock();
403        let (host, _peer) = duplex(1, &clock);
404        let (_reader, mut writer) = host.into_halves();
405        writer.write_all(b"a").unwrap();
406        let deadline = clock.now() + Duration::from_secs(5);
407        writer.set_write_deadline(deadline).unwrap();
408        let writing = thread::spawn(move || writer.write(b"b"));
409        tester.wait_blocked(1);
410        assert_eq!(tester.next_deadline(), Some(deadline));
411
412        // Advance exactly to expiry and observe the blocked operation returning
413        tester.advance_to(deadline);
414        assert_eq!(
415            writing.join().unwrap().unwrap_err().kind(),
416            io::ErrorKind::TimedOut
417        );
418    }
419
420    // An expired read delivers available input before reporting expiry on the empty pipe.
421    #[test]
422    fn test_read_delivers_buffered_bytes_after_deadline() {
423        // Buffer input before the installed read deadline
424        let mut tester = test_clock();
425        let clock = tester.clock();
426        let (host, peer) = duplex(1, &clock);
427        let (mut reader, _writer) = host.into_halves();
428        let (_peer_reader, mut writer) = peer.into_halves();
429        let deadline = clock.now() + Duration::from_secs(5);
430        reader.set_read_deadline(Some(deadline)).unwrap();
431        writer.write_all(b"a").unwrap();
432
433        // Read buffered bytes at expiry without entering a condvar wait
434        tester.advance_to(deadline);
435        reader.forbid_waits();
436        let mut byte = [0];
437        assert_eq!(reader.read(&mut byte).unwrap(), 1);
438        assert_eq!(&byte, b"a");
439
440        // Derive expiry from the clock once input is exhausted
441        assert_eq!(
442            reader.read(&mut byte).unwrap_err().kind(),
443            io::ErrorKind::TimedOut
444        );
445    }
446
447    // A zero-capacity pipe rejects construction before any I/O can wait
448    #[test]
449    #[should_panic(expected = "duplex capacity must be nonzero")]
450    fn test_zero_capacity() {
451        duplex(0, &test_clock().clock());
452    }
453
454    // Partial reads and writes preserve byte order across queue wraparound. Read
455    // and write deadlines remain independent, including empty I/O and flush.
456    #[test]
457    fn test_byte_stream_and_independent_deadlines() {
458        // Create both directions on one paused clock
459        let tester = test_clock();
460        let clock = tester.clock();
461        let (host, ark) = duplex(4, &clock);
462        let (mut host_read, mut host_write, _host_close) = halves(host);
463        let (mut ark_read, mut ark_write, _ark_close) = halves(ark);
464
465        // Preserve byte order across partial writes and queue wraparound
466        assert_eq!(host_read.read(&mut []).unwrap(), 0);
467        assert_eq!(host_write.write(b"abcdef").unwrap(), 4);
468        assert_eq!(host_write.write(&[]).unwrap(), 0);
469        // Flush succeeds even while the queue is full.
470        host_write.flush().unwrap();
471        let mut first = [0; 2];
472        ark_read.read_exact(&mut first).unwrap();
473        assert_eq!(&first, b"ab");
474        host_write.write_all(b"ef").unwrap();
475        let mut rest = [0; 4];
476        ark_read.read_exact(&mut rest).unwrap();
477        assert_eq!(&rest, b"cdef");
478
479        // Deliver buffered input before reporting its expired deadline
480        ark_write.write_all(b"xy").unwrap();
481        host_read.set_read_deadline(Some(clock.now())).unwrap();
482        host_read.read_exact(&mut first).unwrap();
483        assert_eq!(&first, b"xy");
484        assert_eq!(
485            host_read.read(&mut first).unwrap_err().kind(),
486            io::ErrorKind::TimedOut
487        );
488        assert_eq!(host_read.read(&mut []).unwrap(), 0);
489        host_write.write_all(b"q").unwrap();
490
491        // Expire output while input remains usable
492        host_write.set_write_deadline(clock.now()).unwrap();
493        host_read.set_read_deadline(None).unwrap();
494        ark_write.write_all(b"uv").unwrap();
495        host_read.read_exact(&mut first).unwrap();
496        assert_eq!(&first, b"uv");
497        for bytes in [b"t".as_slice(), b""] {
498            assert_eq!(
499                host_write.write(bytes).unwrap_err().kind(),
500                io::ErrorKind::TimedOut
501            );
502        }
503        assert_eq!(
504            host_write.flush().unwrap_err().kind(),
505            io::ErrorKind::TimedOut
506        );
507
508        // Resume output with a fresh deadline
509        host_write
510            .set_write_deadline(clock.now() + Duration::from_secs(5))
511            .unwrap();
512        host_write.write_all(b"rs").unwrap();
513        host_write.flush().unwrap();
514        let mut accepted = [0; 3];
515        ark_read.read_exact(&mut accepted).unwrap();
516        assert_eq!(&accepted, b"qrs");
517    }
518
519    // An idle timed read neither reports EOF nor changes the caller's buffer.
520    // Clearing that deadline restores an indefinite read, woken by fresh output.
521    #[test]
522    fn test_read_timeout_and_reuse() {
523        // Park an empty read on a deadline ahead of the paused clock
524        let mut tester = test_clock();
525        let clock = tester.clock();
526        let deadline = clock.now() + TIMEOUT;
527        let (host, ark) = duplex(1, &clock);
528        let (mut reader, _host_write, _host_close) = halves(host);
529        let (_ark_read, mut writer, _ark_close) = halves(ark);
530        let pipe = reader.pipe.clone();
531        let (done, result) = mpsc::channel();
532        let timed = thread::spawn(move || {
533            reader.set_read_deadline(Some(deadline)).unwrap();
534            let mut buf = [99];
535            let error = reader.read(&mut buf).unwrap_err();
536            assert!(clock.now() >= deadline);
537            assert_eq!(buf, [99]);
538            done.send((reader, error.kind())).unwrap();
539        });
540        tester.wait_blocked(1);
541        tester.advance_to(deadline);
542        let (mut reader, kind) = result.recv().unwrap();
543        timed.join().unwrap();
544        assert_eq!(kind, io::ErrorKind::TimedOut);
545
546        // Clear expiry and wake a new indefinite read with input
547        reader.set_read_deadline(None).unwrap();
548        let (done, result) = mpsc::channel();
549        let reading = thread::spawn(move || {
550            let mut buf = [0];
551            reader.read_exact(&mut buf).unwrap();
552            done.send(buf).unwrap();
553        });
554        blocked(&pipe);
555        writer.write_all(b"x").unwrap();
556        assert_eq!(&result.recv().unwrap(), b"x");
557        reading.join().unwrap();
558    }
559
560    // write_all can accept a prefix before timing out on backpressure. A later
561    // write must follow that prefix, without an abandoned suffix appearing later.
562    #[test]
563    fn test_write_timeout_and_reuse() {
564        // Fill the pipe and park the remaining byte until its deadline
565        let mut tester = test_clock();
566        let clock = tester.clock();
567        let deadline = clock.now() + TIMEOUT;
568        let (host, ark) = duplex(3, &clock);
569        let (_host_read, mut writer, _host_close) = halves(host);
570        let (mut reader, _ark_write, _ark_close) = halves(ark);
571        let (done, result) = mpsc::channel();
572        let writing = thread::spawn(move || {
573            writer.set_write_deadline(deadline).unwrap();
574            let error = writer.write_all(b"abcd").unwrap_err();
575            assert!(clock.now() >= deadline);
576            done.send((writer, error.kind())).unwrap();
577        });
578        tester.wait_blocked(1);
579        tester.advance_to(deadline);
580        let (mut writer, kind) = result.recv().unwrap();
581        writing.join().unwrap();
582        assert_eq!(kind, io::ErrorKind::TimedOut);
583        let mut prefix = [0; 3];
584        reader.read_exact(&mut prefix).unwrap();
585        assert_eq!(&prefix, b"abc");
586
587        // Resume output after the accepted prefix with a fresh deadline
588        writer
589            .set_write_deadline(tester.clock().now() + Duration::from_secs(5))
590            .unwrap();
591        writer.write_all(b"ef").unwrap();
592        drop(writer);
593        let mut suffix = Vec::new();
594        reader.read_to_end(&mut suffix).unwrap();
595        assert_eq!(&suffix, b"ef");
596    }
597
598    // Draining a full queue wakes its writer and permits progress through multiple
599    // partial writes, while bounded reads reconstruct the original byte stream.
600    #[test]
601    fn test_backpressure_wakes_writer() {
602        // Fill a paused pipe and start a writer behind its queued bytes
603        let tester = test_clock();
604        let deadline = tester.clock().now() + Duration::from_secs(5);
605        let (host, ark) = duplex(3, &tester.clock());
606        let (_host_read, mut writer, _host_close) = halves(host);
607        let (mut reader, _ark_write, _ark_close) = halves(ark);
608        writer.write_all(b"abc").unwrap();
609        let pipe = writer.pipe.clone();
610        let (done, result) = mpsc::channel();
611        let writing = thread::spawn(move || {
612            writer.set_write_deadline(deadline).unwrap();
613            done.send(writer.write_all(b"defgh")).unwrap();
614        });
615        // Drain the pipe after the writer has parked
616        blocked(&pipe);
617        reader.set_read_deadline(Some(deadline)).unwrap();
618        let mut bytes = [0; 8];
619        reader.read_exact(&mut bytes).unwrap();
620        assert_eq!(&bytes, b"abcdefgh");
621        result.recv().unwrap().unwrap();
622        writing.join().unwrap();
623    }
624
625    // Explicit local shutdown and dropping the peer both release already blocked
626    // reads and writes, including operations that have no deadline at all.
627    #[test]
628    fn test_shutdown_wakes_both_directions() {
629        for local in [false, true] {
630            // Start a blocked read and a blocked write on the same clock
631            let tester = test_clock();
632            let (host, ark) = duplex(1, &tester.clock());
633            let (mut reader, mut writer, closer) = halves(host);
634            writer.write_all(b"a").unwrap();
635            let incoming = reader.pipe.clone();
636            let outgoing = writer.pipe.clone();
637            let (read_done, read_result) = mpsc::channel();
638            let reading = thread::spawn(move || {
639                read_done.send(reader.read(&mut [0])).unwrap();
640            });
641            let (write_done, write_result) = mpsc::channel();
642            let writing = thread::spawn(move || {
643                write_done.send(writer.write(b"b")).unwrap();
644            });
645            // Close only after both adapter calls have parked
646            blocked(&incoming);
647            blocked(&outgoing);
648            if local {
649                closer.close();
650            } else {
651                drop(ark);
652            }
653            assert_eq!(read_result.recv().unwrap().unwrap(), 0);
654            assert_eq!(
655                write_result.recv().unwrap().unwrap_err().kind(),
656                io::ErrorKind::BrokenPipe
657            );
658            reading.join().unwrap();
659            writing.join().unwrap();
660        }
661    }
662
663    // Closing discards local input but preserves accepted output for the peer to
664    // drain before EOF. Keeping the handles alive must not keep the connection open.
665    #[test]
666    fn test_shutdown_drains_output_and_discards_input() {
667        // Queue bytes in both directions on a paused clock
668        let tester = test_clock();
669        let clock = tester.clock();
670        let (host, ark) = duplex(3, &clock);
671        let (mut host_read, mut host_write, closer) = halves(host);
672        let (mut ark_read, mut ark_write, _ark_close) = halves(ark);
673        host_write.write_all(b"abc").unwrap();
674        ark_write.write_all(b"xy").unwrap();
675        host_read.set_read_deadline(Some(clock.now())).unwrap();
676        ark_read.set_read_deadline(Some(clock.now())).unwrap();
677
678        // Close twice and retain only the peer's accepted input
679        closer.close();
680        closer.close();
681        assert_eq!(host_read.read(&mut [0]).unwrap(), 0);
682        let mut bytes = Vec::new();
683        ark_read.read_to_end(&mut bytes).unwrap();
684        assert_eq!(&bytes, b"abc");
685        for writer in [&mut host_write, &mut ark_write] {
686            assert_eq!(
687                writer.write(b"z").unwrap_err().kind(),
688                io::ErrorKind::BrokenPipe
689            );
690            assert_eq!(
691                writer.flush().unwrap_err().kind(),
692                io::ErrorKind::BrokenPipe
693            );
694        }
695    }
696
697    // A peer that consumed every accepted byte before closing does not fail a
698    // later flush. Only output it closed without reading is reported as lost.
699    #[test]
700    fn test_flush_after_peer_drained_and_closed() {
701        // Drain the accepted output before closing the peer
702        let tester = test_clock();
703        let (host, ark) = duplex(4, &tester.clock());
704        let (_host_read, mut host_write) = host.into_halves();
705        let (mut ark_read, _ark_write) = ark.into_halves();
706        host_write.write_all(b"ab").unwrap();
707        let mut bytes = [0; 2];
708        ark_read.read_exact(&mut bytes).unwrap();
709        drop(ark_read);
710        // Allow a flush but reject any new output
711        host_write.flush().unwrap();
712        assert_eq!(
713            host_write.write(b"c").unwrap_err().kind(),
714            io::ErrorKind::BrokenPipe
715        );
716    }
717
718    // Splitting drops the stream's closer and output budget without closing
719    // its halves. Dropping one half still permits I/O in the other direction.
720    #[test]
721    fn test_into_halves_and_independent_drop() {
722        // Split both streams on a paused clock
723        let tester = test_clock();
724        let (host, ark) = duplex(4, &tester.clock());
725        let (mut host_read, mut host_write) = host.set_write_timeout(Duration::ZERO).into_halves();
726        let (mut ark_read, mut ark_write) = ark.into_halves();
727
728        // Drop one writer and let its peer drain to EOF
729        host_write.write_all(b"abc").unwrap();
730        drop(host_write);
731        let mut bytes = Vec::new();
732        ark_read.read_to_end(&mut bytes).unwrap();
733        assert_eq!(&bytes, b"abc");
734
735        // Keep the opposite direction usable until its own reader drops
736        ark_write.write_all(b"xy").unwrap();
737        let mut reply = [0; 2];
738        host_read.read_exact(&mut reply).unwrap();
739        assert_eq!(&reply, b"xy");
740        drop(host_read);
741        assert_eq!(
742            ark_write.write(b"z").unwrap_err().kind(),
743            io::ErrorKind::BrokenPipe
744        );
745    }
746
747    // Real protocol workers exchange a message larger than the pipe in both
748    // directions, then close while their transport readers are waiting for input.
749    #[test]
750    fn test_protocol_round_trip() {
751        use crate::protocol::{self, Message};
752        use crate::transport::mock::self_attestation;
753        use darkbio_crypto::xdsa;
754
755        // Connect protocol peers on a paused clock
756        let tester = test_clock();
757        let signer = xdsa::SecretKey::generate();
758        let identity = signer.public_key();
759        let attestation = self_attestation(&signer);
760        let (host, ark) = duplex(64 * 1024, &tester.clock());
761        let mut server = protocol::Server::new(ark, signer, attestation);
762        let (client, _) = protocol::connect(host, &identity).unwrap();
763        let mut session = server.accept().unwrap();
764        // Exchange a payload larger than the bounded transport pipe
765        let payload: Vec<u8> = (0..256 * 1024).map(|n| n as u8).collect();
766        let deadline = tester.clock().now() + Duration::from_secs(5);
767        let answer = client
768            .requester()
769            .request(payload.clone(), deadline)
770            .unwrap();
771        let (message, responder) = session.recv().unwrap();
772        assert_eq!(message, Message::Develop(payload.clone()));
773        let written = responder.reply(message, deadline).unwrap();
774        assert_eq!(answer.wait::<Vec<u8>>().unwrap(), payload);
775        written.wait().unwrap();
776        // Release both protocol owners after delivery
777        client.close();
778        server.close();
779    }
780}