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 use only standard Rust synchronization, 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 std::collections::VecDeque;
14use std::io;
15use std::sync::{Arc, Condvar, Mutex, MutexGuard};
16use std::time::{Duration, Instant};
17
18/// One endpoint of an in-memory duplex connection, ready for a client or server.
19pub type Duplex = Stream<Reader, Writer>;
20
21impl Duplex {
22    /// Takes the reader and writer out of the stream without closing either half.
23    ///
24    /// Each half closes its own direction on drop. Dropping the writer lets the
25    /// peer drain accepted output before EOF; dropping the reader refuses further
26    /// peer writes. Obtain a [`crate::transport::Closer`] with [`Self::closer`] before splitting
27    /// if you need to shut down both halves from another thread.
28    ///
29    /// The stream's write timeout is discarded. Any deadlines already installed
30    /// on the halves are retained; new halves have no deadline until configured.
31    ///
32    /// ```
33    /// use darkbio_wire::memory;
34    /// use darkbio_wire::transport::Client;
35    ///
36    /// let (host, bus) = memory::duplex(64 * 1024);
37    /// let (reader, writer) = bus.into_halves();
38    /// let client = Client::new(host);
39    /// // Move `reader` and `writer` to the bus's input and output pumps.
40    /// ```
41    pub fn into_halves(self) -> (Reader, Writer) {
42        let (reader, writer, _, _) = self.into_parts();
43        (reader, writer)
44    }
45}
46
47/// Creates two connected streams with `capacity` bytes of buffering per direction.
48///
49/// Reads wait for data and writes wait for buffer space, bounded by the deadlines
50/// installed by Wire. Partial progress never refreshes a deadline. A timeout
51/// leaves the connection reusable and preserves any bytes already accepted.
52/// Flush checks its deadline but does not wait for the peer to consume output.
53/// Reads deliver available bytes, EOF and empty reads even after their deadline;
54/// the deadline only limits waiting for input. Writes and flushes refuse expired
55/// deadlines even if buffer space is available. Wire enforces its own deadlines
56/// before calling either half.
57///
58/// Closing or dropping an endpoint wakes blocked I/O on both sides. Its unread
59/// input is discarded; the peer can drain its accepted output before receiving
60/// EOF. Further nonempty writes fail with [`io::ErrorKind::BrokenPipe`]. A flush
61/// fails the same way only if the peer closed with accepted output still unread.
62/// Output the peer had consumed before closing flushes fine afterwards.
63///
64/// Both peers must run concurrently when exchanging data. Allow enough capacity
65/// for the handshake's initial output; `64 * 1024` is a useful starting point.
66/// Small buffers can cause handshake backpressure, just as a real stream can.
67///
68/// # Panics
69///
70/// Panics if `capacity` is zero.
71pub fn duplex(capacity: usize) -> (Duplex, Duplex) {
72    assert!(capacity > 0, "duplex capacity must be nonzero");
73    let incoming = Arc::new(Pipe::new(capacity));
74    let outgoing = Arc::new(Pipe::new(capacity));
75    (
76        endpoint(incoming.clone(), outgoing.clone()),
77        endpoint(outgoing, incoming),
78    )
79}
80
81/// Bundles independent I/O halves with shutdown that wakes both directions.
82fn endpoint(incoming: Arc<Pipe>, outgoing: Arc<Pipe>) -> Duplex {
83    Stream::new(
84        Reader {
85            pipe: incoming.clone(),
86            deadline: None,
87        },
88        Writer {
89            pipe: outgoing.clone(),
90            deadline: None,
91        },
92        move || {
93            incoming.close_reader();
94            outgoing.close_writer();
95        },
96    )
97}
98
99/// Receiving half of a [`Duplex`], with an independently configured read deadline.
100#[derive(Debug)]
101pub struct Reader {
102    pipe: Arc<Pipe>,
103    deadline: Option<Instant>,
104}
105
106impl io::Read for Reader {
107    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
108        let mut state = self.pipe.lock();
109        loop {
110            if buf.is_empty() || !state.reader_open {
111                return Ok(0);
112            }
113            if !state.bytes.is_empty() {
114                let count = buf.len().min(state.bytes.len());
115                for (out, byte) in buf.iter_mut().zip(state.bytes.drain(..count)) {
116                    *out = byte;
117                }
118                drop(state);
119                self.pipe.changed.notify_all();
120                return Ok(count);
121            }
122            if !state.writer_open {
123                return Ok(0);
124            }
125            state = self.pipe.wait(state, time_left(self.deadline)?);
126        }
127    }
128}
129
130impl Read for Reader {
131    fn set_read_deadline(&mut self, deadline: Option<Instant>) -> io::Result<()> {
132        self.deadline = deadline;
133        Ok(())
134    }
135}
136
137impl Drop for Reader {
138    fn drop(&mut self) {
139        self.pipe.close_reader();
140    }
141}
142
143/// Sending half of a [`Duplex`], sharing one deadline across writes and flushes.
144#[derive(Debug)]
145pub struct Writer {
146    pipe: Arc<Pipe>,
147    deadline: Option<Instant>,
148}
149
150impl io::Write for Writer {
151    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
152        let mut state = self.pipe.lock();
153        loop {
154            let timeout = time_left(self.deadline)?;
155            if buf.is_empty() {
156                return Ok(0);
157            }
158            if !state.reader_open || !state.writer_open {
159                return Err(io::ErrorKind::BrokenPipe.into());
160            }
161            let count = buf.len().min(self.pipe.capacity - state.bytes.len());
162            if count > 0 {
163                state.bytes.extend(&buf[..count]);
164                drop(state);
165                self.pipe.changed.notify_all();
166                return Ok(count);
167            }
168            state = self.pipe.wait(state, timeout);
169        }
170    }
171
172    fn flush(&mut self) -> io::Result<()> {
173        let state = self.pipe.lock();
174        time_left(self.deadline)?;
175        if !state.writer_open || state.lost {
176            return Err(io::ErrorKind::BrokenPipe.into());
177        }
178        Ok(())
179    }
180}
181
182impl Write for Writer {
183    fn set_write_deadline(&mut self, deadline: Instant) -> io::Result<()> {
184        self.deadline = Some(deadline);
185        Ok(())
186    }
187}
188
189impl Drop for Writer {
190    fn drop(&mut self) {
191        self.pipe.close_writer();
192    }
193}
194
195/// Shared bounded buffer for one direction. Waiting always releases the mutex.
196#[derive(Debug)]
197struct Pipe {
198    capacity: usize,
199    state: Mutex<State>,
200    changed: Condvar,
201}
202
203#[derive(Debug)]
204struct State {
205    bytes: VecDeque<u8>,
206    reader_open: bool,
207    writer_open: bool,
208    lost: bool, // Accepted output the reader closed without consuming
209    #[cfg(test)]
210    waiting: usize,
211}
212
213impl Pipe {
214    fn new(capacity: usize) -> Self {
215        Self {
216            capacity,
217            state: Mutex::new(State {
218                bytes: VecDeque::with_capacity(capacity),
219                reader_open: true,
220                writer_open: true,
221                lost: false,
222                #[cfg(test)]
223                waiting: 0,
224            }),
225            changed: Condvar::new(),
226        }
227    }
228
229    fn lock(&self) -> MutexGuard<'_, State> {
230        // Shutdown must remain usable even after a panic in an I/O operation.
231        self.state.lock().unwrap_or_else(|err| err.into_inner())
232    }
233
234    fn wait<'a>(
235        &self,
236        state: MutexGuard<'a, State>,
237        timeout: Option<Duration>,
238    ) -> MutexGuard<'a, State> {
239        #[cfg(test)]
240        let state = {
241            let mut state = state;
242            state.waiting += 1;
243            self.changed.notify_all();
244            state
245        };
246        let state = match timeout {
247            Some(timeout) => {
248                self.changed
249                    .wait_timeout(state, timeout)
250                    .unwrap_or_else(|err| err.into_inner())
251                    .0
252            }
253            None => self
254                .changed
255                .wait(state)
256                .unwrap_or_else(|err| err.into_inner()),
257        };
258        #[cfg(test)]
259        let state = {
260            let mut state = state;
261            state.waiting -= 1;
262            state
263        };
264        state
265    }
266
267    fn close_reader(&self) {
268        let mut state = self.lock();
269        state.reader_open = false;
270        state.lost |= !state.bytes.is_empty();
271        state.bytes.clear();
272        drop(state);
273        self.changed.notify_all();
274    }
275
276    fn close_writer(&self) {
277        self.lock().writer_open = false;
278        self.changed.notify_all();
279    }
280}
281
282/// Computes a fresh wait budget without extending the installed deadline.
283fn time_left(deadline: Option<Instant>) -> io::Result<Option<Duration>> {
284    deadline
285        .map(|deadline| {
286            deadline
287                .checked_duration_since(Instant::now())
288                .filter(|left| !left.is_zero())
289                .ok_or_else(|| io::ErrorKind::TimedOut.into())
290        })
291        .transpose()
292}
293
294#[cfg(test)]
295#[cfg_attr(coverage_nightly, coverage(off))]
296mod tests {
297    use super::*;
298    use crate::transport::Closer;
299    use std::io::{Read as _, Write as _};
300    use std::sync::mpsc;
301    use std::thread;
302
303    const PATIENCE: Duration = Duration::from_secs(5);
304    const TIMEOUT: Duration = Duration::from_millis(50);
305
306    /// Exposes standard I/O for adapter tests without closing the stream.
307    fn halves(stream: Duplex) -> (Reader, Writer, Closer) {
308        let closer = stream.closer();
309        let (reader, writer) = stream.into_halves();
310        (reader, writer, closer)
311    }
312
313    /// Waits until an operation has released its mutex in a condition-variable wait.
314    fn blocked(pipe: &Pipe) {
315        let deadline = Instant::now() + PATIENCE;
316        let mut state = pipe.lock();
317        while state.waiting == 0 {
318            let timeout = deadline.checked_duration_since(Instant::now()).unwrap();
319            state = pipe.changed.wait_timeout(state, timeout).unwrap().0;
320        }
321    }
322
323    #[test]
324    #[should_panic(expected = "duplex capacity must be nonzero")]
325    fn test_zero_capacity() {
326        duplex(0);
327    }
328
329    // Partial reads and writes preserve byte order across queue wraparound. Read
330    // and write deadlines remain independent, including empty I/O and flush.
331    #[test]
332    fn test_byte_stream_and_independent_deadlines() {
333        let (host, ark) = duplex(4);
334        let (mut host_read, mut host_write, _host_close) = halves(host);
335        let (mut ark_read, mut ark_write, _ark_close) = halves(ark);
336
337        assert_eq!(host_read.read(&mut []).unwrap(), 0);
338        assert_eq!(host_write.write(b"abcdef").unwrap(), 4);
339        assert_eq!(host_write.write(&[]).unwrap(), 0);
340        // Flush succeeds even while the queue is full.
341        host_write.flush().unwrap();
342        let mut first = [0; 2];
343        ark_read.read_exact(&mut first).unwrap();
344        assert_eq!(&first, b"ab");
345        host_write.write_all(b"ef").unwrap();
346        let mut rest = [0; 4];
347        ark_read.read_exact(&mut rest).unwrap();
348        assert_eq!(&rest, b"cdef");
349
350        ark_write.write_all(b"xy").unwrap();
351        host_read.set_read_deadline(Some(Instant::now())).unwrap();
352        host_read.read_exact(&mut first).unwrap();
353        assert_eq!(&first, b"xy");
354        assert_eq!(
355            host_read.read(&mut first).unwrap_err().kind(),
356            io::ErrorKind::TimedOut
357        );
358        assert_eq!(host_read.read(&mut []).unwrap(), 0);
359        host_write.write_all(b"q").unwrap();
360
361        host_write.set_write_deadline(Instant::now()).unwrap();
362        host_read.set_read_deadline(None).unwrap();
363        ark_write.write_all(b"uv").unwrap();
364        host_read.read_exact(&mut first).unwrap();
365        assert_eq!(&first, b"uv");
366        for bytes in [b"t".as_slice(), b""] {
367            assert_eq!(
368                host_write.write(bytes).unwrap_err().kind(),
369                io::ErrorKind::TimedOut
370            );
371        }
372        assert_eq!(
373            host_write.flush().unwrap_err().kind(),
374            io::ErrorKind::TimedOut
375        );
376
377        host_write
378            .set_write_deadline(Instant::now() + PATIENCE)
379            .unwrap();
380        host_write.write_all(b"rs").unwrap();
381        host_write.flush().unwrap();
382        let mut accepted = [0; 3];
383        ark_read.read_exact(&mut accepted).unwrap();
384        assert_eq!(&accepted, b"qrs");
385    }
386
387    // An idle timed read neither reports EOF nor changes the caller's buffer.
388    // Clearing that deadline restores an indefinite read, woken by fresh output.
389    #[test]
390    fn test_read_timeout_and_reuse() {
391        let (host, ark) = duplex(1);
392        let (mut reader, _host_write, _host_close) = halves(host);
393        let (_ark_read, mut writer, _ark_close) = halves(ark);
394        let pipe = reader.pipe.clone();
395        let (done, result) = mpsc::channel();
396        let timed = thread::spawn(move || {
397            let deadline = Instant::now() + TIMEOUT;
398            reader.set_read_deadline(Some(deadline)).unwrap();
399            let mut buf = [99];
400            let error = reader.read(&mut buf).unwrap_err();
401            assert!(Instant::now() >= deadline);
402            assert_eq!(buf, [99]);
403            done.send((reader, error.kind())).unwrap();
404        });
405        let (mut reader, kind) = result.recv_timeout(PATIENCE).unwrap();
406        timed.join().unwrap();
407        assert_eq!(kind, io::ErrorKind::TimedOut);
408
409        reader.set_read_deadline(None).unwrap();
410        let (done, result) = mpsc::channel();
411        let reading = thread::spawn(move || {
412            let mut buf = [0];
413            reader.read_exact(&mut buf).unwrap();
414            done.send(buf).unwrap();
415        });
416        blocked(&pipe);
417        writer.write_all(b"x").unwrap();
418        assert_eq!(&result.recv_timeout(PATIENCE).unwrap(), b"x");
419        reading.join().unwrap();
420    }
421
422    // write_all can accept a prefix before timing out on backpressure. A later
423    // write must follow that prefix, without an abandoned suffix appearing later.
424    #[test]
425    fn test_write_timeout_and_reuse() {
426        let (host, ark) = duplex(3);
427        let (_host_read, mut writer, _host_close) = halves(host);
428        let (mut reader, _ark_write, _ark_close) = halves(ark);
429        let (done, result) = mpsc::channel();
430        let writing = thread::spawn(move || {
431            let deadline = Instant::now() + TIMEOUT;
432            writer.set_write_deadline(deadline).unwrap();
433            let error = writer.write_all(b"abcd").unwrap_err();
434            assert!(Instant::now() >= deadline);
435            done.send((writer, error.kind())).unwrap();
436        });
437        let (mut writer, kind) = result.recv_timeout(PATIENCE).unwrap();
438        writing.join().unwrap();
439        assert_eq!(kind, io::ErrorKind::TimedOut);
440        let mut prefix = [0; 3];
441        reader.read_exact(&mut prefix).unwrap();
442        assert_eq!(&prefix, b"abc");
443
444        writer
445            .set_write_deadline(Instant::now() + PATIENCE)
446            .unwrap();
447        writer.write_all(b"ef").unwrap();
448        drop(writer);
449        let mut suffix = Vec::new();
450        reader.read_to_end(&mut suffix).unwrap();
451        assert_eq!(&suffix, b"ef");
452    }
453
454    // Draining a full queue wakes its writer and permits progress through multiple
455    // partial writes, while bounded reads reconstruct the original byte stream.
456    #[test]
457    fn test_backpressure_wakes_writer() {
458        let (host, ark) = duplex(3);
459        let (_host_read, mut writer, _host_close) = halves(host);
460        let (mut reader, _ark_write, _ark_close) = halves(ark);
461        writer.write_all(b"abc").unwrap();
462        let pipe = writer.pipe.clone();
463        let (done, result) = mpsc::channel();
464        let writing = thread::spawn(move || {
465            writer
466                .set_write_deadline(Instant::now() + PATIENCE)
467                .unwrap();
468            done.send(writer.write_all(b"defgh")).unwrap();
469        });
470        blocked(&pipe);
471        reader
472            .set_read_deadline(Some(Instant::now() + PATIENCE))
473            .unwrap();
474        let mut bytes = [0; 8];
475        reader.read_exact(&mut bytes).unwrap();
476        assert_eq!(&bytes, b"abcdefgh");
477        result.recv_timeout(PATIENCE).unwrap().unwrap();
478        writing.join().unwrap();
479    }
480
481    // Explicit local shutdown and dropping the peer both release already blocked
482    // reads and writes, including operations that have no deadline at all.
483    #[test]
484    fn test_shutdown_wakes_both_directions() {
485        for local in [false, true] {
486            let (host, ark) = duplex(1);
487            let (mut reader, mut writer, closer) = halves(host);
488            writer.write_all(b"a").unwrap();
489            let incoming = reader.pipe.clone();
490            let outgoing = writer.pipe.clone();
491            let (read_done, read_result) = mpsc::channel();
492            let reading = thread::spawn(move || {
493                read_done.send(reader.read(&mut [0])).unwrap();
494            });
495            let (write_done, write_result) = mpsc::channel();
496            let writing = thread::spawn(move || {
497                write_done.send(writer.write(b"b")).unwrap();
498            });
499            blocked(&incoming);
500            blocked(&outgoing);
501            if local {
502                closer.close();
503            } else {
504                drop(ark);
505            }
506            assert_eq!(read_result.recv_timeout(PATIENCE).unwrap().unwrap(), 0);
507            assert_eq!(
508                write_result
509                    .recv_timeout(PATIENCE)
510                    .unwrap()
511                    .unwrap_err()
512                    .kind(),
513                io::ErrorKind::BrokenPipe
514            );
515            reading.join().unwrap();
516            writing.join().unwrap();
517        }
518    }
519
520    // Closing discards local input but preserves accepted output for the peer to
521    // drain before EOF. Keeping the handles alive must not keep the connection open.
522    #[test]
523    fn test_shutdown_drains_output_and_discards_input() {
524        let (host, ark) = duplex(3);
525        let (mut host_read, mut host_write, closer) = halves(host);
526        let (mut ark_read, mut ark_write, _ark_close) = halves(ark);
527        host_write.write_all(b"abc").unwrap();
528        ark_write.write_all(b"xy").unwrap();
529        host_read.set_read_deadline(Some(Instant::now())).unwrap();
530        ark_read.set_read_deadline(Some(Instant::now())).unwrap();
531        closer.close();
532        closer.close();
533        assert_eq!(host_read.read(&mut [0]).unwrap(), 0);
534        let mut bytes = Vec::new();
535        ark_read.read_to_end(&mut bytes).unwrap();
536        assert_eq!(&bytes, b"abc");
537        for writer in [&mut host_write, &mut ark_write] {
538            assert_eq!(
539                writer.write(b"z").unwrap_err().kind(),
540                io::ErrorKind::BrokenPipe
541            );
542            assert_eq!(
543                writer.flush().unwrap_err().kind(),
544                io::ErrorKind::BrokenPipe
545            );
546        }
547    }
548
549    // A peer that consumed every accepted byte before closing does not fail a
550    // later flush. Only output it closed without reading is reported as lost.
551    #[test]
552    fn test_flush_after_peer_drained_and_closed() {
553        let (host, ark) = duplex(4);
554        let (_host_read, mut host_write) = host.into_halves();
555        let (mut ark_read, _ark_write) = ark.into_halves();
556        host_write.write_all(b"ab").unwrap();
557        let mut bytes = [0; 2];
558        ark_read.read_exact(&mut bytes).unwrap();
559        drop(ark_read);
560        host_write.flush().unwrap();
561        assert_eq!(
562            host_write.write(b"c").unwrap_err().kind(),
563            io::ErrorKind::BrokenPipe
564        );
565    }
566
567    // Splitting drops the stream's closer and output budget without closing
568    // its halves. Dropping one half still permits I/O in the other direction.
569    #[test]
570    fn test_into_halves_and_independent_drop() {
571        let (host, ark) = duplex(4);
572        let (mut host_read, mut host_write) = host.set_write_timeout(Duration::ZERO).into_halves();
573        let (mut ark_read, mut ark_write) = ark.into_halves();
574
575        host_write.write_all(b"abc").unwrap();
576        drop(host_write);
577        let mut bytes = Vec::new();
578        ark_read.read_to_end(&mut bytes).unwrap();
579        assert_eq!(&bytes, b"abc");
580
581        ark_write.write_all(b"xy").unwrap();
582        let mut reply = [0; 2];
583        host_read.read_exact(&mut reply).unwrap();
584        assert_eq!(&reply, b"xy");
585        drop(host_read);
586        assert_eq!(
587            ark_write.write(b"z").unwrap_err().kind(),
588            io::ErrorKind::BrokenPipe
589        );
590    }
591
592    // Real protocol workers exchange a message larger than the pipe in both
593    // directions, then close while their transport readers are waiting for input.
594    #[test]
595    fn test_protocol_round_trip() {
596        use crate::protocol::{self, Message};
597        use crate::transport::mock::self_attestation;
598        use darkbio_crypto::xdsa;
599
600        let signer = xdsa::SecretKey::generate();
601        let identity = signer.public_key();
602        let attestation = self_attestation(&signer);
603        let (host, ark) = duplex(64 * 1024);
604        let mut server = protocol::Server::new(ark, signer, attestation);
605        let (client, _) = protocol::connect(host, &identity).unwrap();
606        let mut session = server.accept().unwrap();
607        let payload: Vec<u8> = (0..256 * 1024).map(|n| n as u8).collect();
608        let deadline = Instant::now() + PATIENCE;
609        let answer = client
610            .requester()
611            .request(payload.clone(), deadline)
612            .unwrap();
613        let (message, responder) = session.recv().unwrap();
614        assert_eq!(message, Message::Develop(payload.clone()));
615        let written = responder.reply(message, deadline).unwrap();
616        assert_eq!(answer.wait::<Vec<u8>>().unwrap(), payload);
617        written.wait().unwrap();
618        client.close();
619        server.close();
620    }
621}