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