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