darkbio-wire 0.17.1

Encrypted protocol between Ark and host
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
// wire-rs: encrypted protocol between Ark and host
// Copyright 2026 Dark Bio AG. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//! Waiting for request answers and reply write results.

use super::envelope::IncomingEnvelope;
use super::session::SessionInner;
use super::{Error, Message};
use std::fmt;
use std::marker::PhantomData;
use std::sync::{Arc, Mutex, Weak, mpsc};
use std::time::Instant;

/// Result of a queued request or reply.
///
/// Requests return `Promise<Message>`; their wait selects the expected response
/// type or takes the message directly. Replies return `Promise<()>`; their wait
/// observes local writing and flushing, not peer receipt or processing.
///
/// Dropping a promise leaves the request or reply running with its original
/// deadline. A timeout does not mean the peer stopped working.
/// Completed results remain available after the session closes.
///
/// A buffered response counts toward the session's inbound byte limit until
/// [`Promise::wait`] or drop. It stays encoded until waiting decodes it. Late
/// answers and answers with no matching request are discarded. Answers whose
/// promises were dropped are also discarded. Their payloads are never decoded.
///
/// Each promise returns its result once:
///
/// ```compile_fail,E0382
/// use darkbio_wire::protocol::schema::DeviceInfoResponse;
/// use darkbio_wire::protocol::{Message, Promise};
/// fn take_twice(promise: Promise<Message>) {
///     let _ = promise.wait::<DeviceInfoResponse>();
///     let _ = promise.wait::<DeviceInfoResponse>();
/// }
/// ```
pub struct Promise<T> {
    /// Channel receiving one result from the corresponding
    /// [`PendingOperation`](super::operation::PendingOperation).
    ///
    /// A buffered result remains available even after the session is dropped.
    result: mpsc::Receiver<Result<PromiseResult, Error>>,
    /// Completion and its optional notification, independent of session lifetime.
    notification: Arc<Mutex<NotificationState>>,
    /// Whether a callback was registered, since registration is single-use even
    /// after the notification has been sent.
    registered: bool,
    /// Marker for the public result type, since the channel carries responses
    /// still encoded.
    value: PhantomData<fn() -> T>,
    /// Session whose expired operations the waiter settles before receiving
    /// its result.
    session: Weak<SessionInner>,
    /// Deadline supplied with the request or reply, which waiting does not restart.
    deadline: Instant,
    /// One-shot notification just before entering the blocking receive.
    #[cfg(any(test, feature = "fuzz"))]
    wait_hook: Option<mpsc::Sender<()>>,
}

impl Promise<Message> {
    /// Blocks for completion, then decodes the response.
    ///
    /// The response must be accepted before the request's original deadline.
    /// Decoding is outside that deadline. An accepted response remains available
    /// after the deadline or closure.
    ///
    /// Taking the response removes its bytes from the inbound byte count before
    /// decoding it. A peer's error answer returns [`Error::Remote`]. Invalid
    /// protobuf returns [`Error::Malformed`] and closes its original session.
    ///
    /// Selects the expected response type at this call, either through inference
    /// or `wait::<Response>()`. Message extraction checks the content variant and
    /// returns [`Error::UnexpectedResponse`] on mismatch. The [`Message`] enum can
    /// also be taken directly for application pattern matching.
    pub fn wait<T>(self) -> Result<T, Error>
    where
        T: TryFrom<Message>,
        Error: From<T::Error>,
    {
        T::try_from(self.wait_result()?.response()?).map_err(Error::from)
    }

    /// Waits for the reader or the deadline worker to settle the promise,
    /// without servicing deadlines itself.
    #[cfg(any(test, feature = "fuzz"))]
    pub(super) fn wait_worker_result(self) -> Result<Message, Error> {
        self.worker_result()?.response()
    }
}

impl Promise<()> {
    /// Blocks until the reply is written and flushed locally, under its original
    /// deadline.
    ///
    /// The peer does not send another acknowledgment for this reply.
    pub fn wait(self) -> Result<(), Error> {
        self.wait_result()?.written()
    }

    /// Waits for the writer or the deadline worker to settle the promise,
    /// without servicing deadlines itself.
    #[cfg(any(test, feature = "fuzz"))]
    pub(super) fn wait_worker_result(self) -> Result<(), Error> {
        self.worker_result()?.written()
    }
}

impl<T> Promise<T> {
    /// Creates a promise and the sender that its
    /// [`PendingOperation`](super::operation::PendingOperation) will own.
    ///
    /// The channel holds one result without waiting for the caller to receive
    /// it. The `response` flag marks a request, which expects a peer answer
    /// rather than a local write result.
    pub(super) fn pair(
        session: Weak<SessionInner>,
        deadline: Instant,
        response: bool,
    ) -> (ResultSender, Self) {
        // Give completion one buffered result and a shared notification state
        let (sender, result) = mpsc::sync_channel(1);
        let notification = Arc::new(Mutex::new(NotificationState::default()));

        // Keep publication and observation independent of the session's lifetime
        (
            ResultSender {
                response,
                result: sender,
                notification: notification.clone(),
            },
            Self {
                result,
                notification,
                registered: false,
                value: PhantomData,
                session,
                deadline,
                #[cfg(any(test, feature = "fuzz"))]
                wait_hook: None,
            },
        )
    }

    /// Runs `callback` once the result is ready.
    ///
    /// Requests notify on a response or an error, replies on local write and
    /// flush completion or an error, so a notification implies neither success
    /// nor peer receipt. The result is published first, so [`Promise::wait`]
    /// then takes it without waiting for publication, though it still decodes
    /// a response.
    ///
    /// Registering or receiving a notification neither decodes the response nor
    /// releases its retained bytes. They remain charged until waiting or drop.
    /// Deadlines are unchanged, and registration does not service expiry.
    ///
    /// On a settled promise, the callback runs at once on the registering thread,
    /// even after its session is gone. Otherwise the thread settling the operation
    /// runs it, which can be a protocol worker. Either way it runs, and its captures
    /// drop, outside every wire lock, so it may call back into the session. It must
    /// return promptly and must not panic.
    ///
    /// Dropping the promise drops a callback still waiting for settlement, without
    /// canceling the operation. A callback already taken by settlement still runs.
    ///
    /// # Panics
    /// Panics if notification was already registered on this promise.
    pub fn notify(&mut self, callback: impl FnOnce() + Send + 'static) {
        // Check before locking so caller misuse cannot poison shared state and
        // cause another panic when the promise is dropped during unwinding
        assert!(!self.registered, "promise notification already registered");

        // Serialize registration with publication and promise drop
        self.registered = true;
        let mut notification = self.notification.lock().expect("notification not poisoned");
        if notification.done {
            drop(notification);
            callback();
        } else {
            notification.hook = Some(Box::new(callback));
        }
    }

    /// Expires pending operations, then waits for the session to publish its result.
    ///
    /// The session decides whether an answer or timeout came first.
    #[allow(unused_mut)] // only test and fuzz builds mutate self, taking the wait hook
    fn wait_result(mut self) -> Result<PromiseResult, Error> {
        // Settle any operations already expired before waiting
        if let Some(session) = self.session.upgrade() {
            session.expire();
        }

        // Let scenarios observe entry into the receive
        #[cfg(any(test, feature = "fuzz"))]
        if let Some(wait_hook) = self.wait_hook.take() {
            let _ = wait_hook.send(());
        }

        // Let the deadline worker settle timeouts while this receive stays untimed
        match self.result.recv() {
            Ok(result) => result,
            Err(mpsc::RecvError) => {
                unreachable!("registered operation settles before its sender is released");
            }
        }
    }

    /// Arms a one-shot notification sent just before [`Self::wait_result`]
    /// starts waiting on the result channel.
    ///
    /// A result sent before the wait stays buffered in that channel.
    #[cfg(any(test, feature = "fuzz"))]
    pub(super) fn watch_wait(&mut self) -> mpsc::Receiver<()> {
        let (sender, receiver) = mpsc::channel();
        self.wait_hook = Some(sender);
        receiver
    }

    /// Returns a probe reporting whether the notification lock is free, usable
    /// after the promise is gone.
    #[cfg(test)]
    pub(super) fn notification_unlocked(&self) -> impl Fn() -> bool + Send + 'static {
        let notification = self.notification.clone();
        move || notification.try_lock().is_ok()
    }

    /// Waits for a worker result without calling [`SessionInner::expire`], so
    /// tests can prove workers process deadlines without help from
    /// [`Promise::wait`].
    #[cfg(any(test, feature = "fuzz"))]
    fn worker_result(self) -> Result<PromiseResult, Error> {
        self.result
            .recv()
            .expect("protocol worker must settle the promise")
    }
}

impl<T> Drop for Promise<T> {
    /// Takes an unrun callback under the notification lock, then drops it outside.
    ///
    /// The protocol never drops a handed-out promise under its session lock.
    fn drop(&mut self) {
        // Clear registration before releasing the lock
        let hook = self
            .notification
            .lock()
            .expect("notification not poisoned")
            .hook
            .take();

        // Release application captures without the notification lock
        drop(hook);
    }
}

impl<T> fmt::Debug for Promise<T> {
    /// Shows the deadline, whether a notification is registered and whether
    /// the result has been published.
    ///
    /// A notification lock held elsewhere leaves the completion out.
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut promise = f.debug_struct("Promise");
        promise
            .field("deadline", &self.deadline)
            .field("registered", &self.registered);
        if let Ok(notification) = self.notification.try_lock() {
            promise.field("done", &notification.done);
        }
        promise.finish_non_exhaustive()
    }
}

/// Notification state shared by a promise and its sender, surviving the
/// session without retaining it.
#[derive(Default)]
struct NotificationState {
    /// Whether the result has been published, including when no hook was
    /// registered yet.
    done: bool,
    /// Application callback taken by settlement or promise drop.
    hook: Option<Box<dyn FnOnce() + Send>>,
}

/// Single-use result sender for a request or reply.
///
/// Its one-slot channel never needs to wait for the application to receive
/// the result.
pub(super) struct ResultSender {
    /// Whether the operation is a request expecting a peer answer, rather than
    /// a reply expecting local write completion.
    pub(super) response: bool,
    /// Channel carrying exactly one result before this sender is released.
    result: mpsc::SyncSender<Result<PromiseResult, Error>>,
    /// State serializing publication and notification with registration and
    /// promise drop.
    notification: Arc<Mutex<NotificationState>>,
}

impl ResultSender {
    /// Publishes the result and hands back its callback, to run once the caller
    /// holds no wire lock.
    ///
    /// Byte admission reads the delivery flag to learn whether the promise
    /// still exists.
    pub(super) fn send(self, result: Result<PromiseResult, Error>) -> Notification {
        // Lock before publishing, so a concurrent waiter cannot drop the promise
        // and clear its hook between receiving the result and our notification
        let mut notification = self.notification.lock().expect("notification not poisoned");
        let delivered = self.result.send(result).is_ok();
        if delivered {
            notification.done = true;
        }

        // Hand ownership to the caller before releasing the publication lock
        Notification {
            delivered,
            callback: notification.hook.take(),
        }
    }
}

/// Publication status and the callback to run once every wire lock is released.
#[must_use = "collect the notification and run it outside all wire locks"]
pub(super) struct Notification {
    /// Whether the promise still existed when its result was published.
    pub(super) delivered: bool,
    /// Callback removed atomically with result publication.
    callback: Option<Box<dyn FnOnce() + Send>>,
}

impl Notification {
    /// Runs the callback and drops its captures on the settling thread.
    pub(super) fn run(self) {
        if let Some(callback) = self.callback {
            callback();
        }
    }
}

/// Collector of callbacks that run on scope exit, including early returns.
///
/// Declare it before acquiring any wire locks, so their guards drop first.
#[derive(Default)]
pub(super) struct Notifications {
    /// Callbacks taken from promises settled in this scope.
    pending: Vec<Notification>,
}

impl Notifications {
    /// Retains callbacks until the caller has released its locks.
    pub(super) fn push(&mut self, notification: Notification) {
        if notification.callback.is_some() {
            self.pending.push(notification);
        }
    }

    /// Reports whether no callback is waiting to run.
    pub(super) fn is_empty(&self) -> bool {
        self.pending.is_empty()
    }
}

impl Drop for Notifications {
    /// Runs callbacks after guards declared later in the scope have released locks.
    fn drop(&mut self) {
        for notification in self.pending.drain(..) {
            notification.run();
        }
    }
}

/// An operation result before the application waits on its promise.
pub(super) enum PromiseResult {
    /// Original response bytes, decoded only when a request promise is observed.
    Response(IncomingEnvelope),
    /// Completion of the local reply write and flush, with no incoming message.
    Written,
}

impl PromiseResult {
    /// Decodes the response carried by a request operation's result channel.
    fn response(self) -> Result<Message, Error> {
        match self {
            Self::Response(message) => message.decode(),
            Self::Written => unreachable!("requests complete with responses"),
        }
    }

    /// Checks that a reply operation reported its local write completion.
    fn written(self) -> Result<(), Error> {
        match self {
            Self::Written => Ok(()),
            Self::Response(_) => unreachable!("replies complete with write results"),
        }
    }
}

/// Checks promise notifications and that promises move to application threads.
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
    use super::{Error, Message, NotificationState, Promise, PromiseResult};
    use darkbio_clock::TestClock;
    use std::fmt::Debug;
    use std::panic::{AssertUnwindSafe, catch_unwind};
    use std::sync::{Arc, Mutex, Weak, mpsc};
    use std::time::Duration;

    /// Checks that a duplicate registration panics and leaves the original
    /// callback and result usable.
    #[test]
    fn test_duplicate_notification() {
        for completed in [false, true] {
            // Register a callback on a pending promise
            let tester = TestClock::new();
            let (sender, mut promise) =
                Promise::<()>::pair(Weak::new(), tester.clock().now(), false);
            let (events, receiver) = mpsc::channel();
            promise.notify(move || {
                let _ = events.send(1);
            });

            // Reject another registration before or after settlement
            let sender = if completed {
                sender.send(Ok(PromiseResult::Written)).run();
                None
            } else {
                Some(sender)
            };
            assert!(
                catch_unwind(AssertUnwindSafe(
                    || promise.notify(|| panic!("duplicate callback ran"))
                ))
                .is_err()
            );

            // Keep the first callback and result intact without poisoning the lock
            if let Some(sender) = sender {
                sender.send(Ok(PromiseResult::Written)).run();
            }
            assert_eq!(receiver.try_recv(), Ok(1));
            assert!(receiver.try_recv().is_err());
            promise.wait().unwrap();
        }
    }

    /// Checks that a callback whose consumer is gone leaves success and failure
    /// results unchanged.
    #[test]
    fn test_disconnected_notification() {
        for completed in [false, true] {
            for success in [false, true] {
                // Disconnect the callback's consumer before registering it
                let tester = TestClock::new();
                let (sender, mut promise) =
                    Promise::<()>::pair(Weak::new(), tester.clock().now(), false);
                let (events, receiver) = mpsc::channel();
                drop(receiver);

                // Register on either side of publishing success or failure
                let result = if success {
                    Ok(PromiseResult::Written)
                } else {
                    Err(Error::Timeout)
                };
                if completed {
                    sender.send(result).run();
                    promise.notify(move || {
                        let _ = events.send(1);
                    });
                } else {
                    promise.notify(move || {
                        let _ = events.send(1);
                    });
                    sender.send(result).run();
                }

                // Observe the original result despite the lost notification consumer
                match promise.wait() {
                    Ok(()) => assert!(success),
                    Err(Error::Timeout) => assert!(!success),
                    result => panic!("unexpected result: {result:?}"),
                }
            }
        }
    }

    /// Checks that a returning waiter cannot clear a callback already taken by
    /// publication.
    #[test]
    fn test_notification_with_waiter() {
        for _ in 0..32 {
            // Register a callback and start a waiter before publishing
            let tester = TestClock::new();
            let (sender, mut promise) = Promise::<()>::pair(
                Weak::new(),
                tester.clock().now() + Duration::from_secs(5),
                false,
            );
            let (events, receiver) = mpsc::channel();
            promise.notify(move || {
                let _ = events.send(1);
            });
            let waiting = promise.watch_wait();
            let waiter = std::thread::spawn(move || promise.wait());
            waiting.recv().unwrap();

            // Let the waiter drop its promise before running the taken callback
            let notification = sender.send(Ok(PromiseResult::Written));
            waiter.join().unwrap().unwrap();
            notification.run();

            // Require exactly one notification after the promise is gone
            assert_eq!(receiver.try_recv(), Ok(1));
            assert!(receiver.try_recv().is_err());
        }
    }

    /// Checks that dropping a pending promise releases its callback captures
    /// outside the notification lock.
    #[test]
    fn test_dropped_promise_releases_callback_without_lock() {
        /// Capture reporting whether its destruction can acquire the
        /// notification lock.
        struct Capture {
            /// Shared registration state surviving the dropped promise.
            notification: Arc<Mutex<NotificationState>>,
            /// Channel reporting destruction to the test without blocking.
            dropped: mpsc::Sender<bool>,
        }

        impl Drop for Capture {
            /// Checks the lock from application destruction code.
            fn drop(&mut self) {
                let _ = self.dropped.send(self.notification.try_lock().is_ok());
            }
        }

        // Register a callback whose capture checks its destruction context
        let tester = TestClock::new();
        let (sender, mut promise) = Promise::<()>::pair(Weak::new(), tester.clock().now(), false);
        let (dropped, observed) = mpsc::channel();
        let capture = Capture {
            notification: promise.notification.clone(),
            dropped,
        };
        let (ran, running) = mpsc::channel();
        promise.notify(move || {
            let _ = ran.send(());
            drop(capture);
        });

        // Drop before settlement and require unlocked capture destruction
        drop(promise);
        assert_eq!(observed.try_recv(), Ok(true));
        assert!(running.try_recv().is_err());

        // Settle the abandoned operation without running its cleared callback
        let notification = sender.send(Ok(PromiseResult::Written));
        assert!(!notification.delivered);
        notification.run();
        assert!(running.try_recv().is_err());
        assert!(observed.try_recv().is_err());
    }

    /// Checks the bounds required to move a promise to an application thread
    /// and to print it.
    #[test]
    fn test_thread_capabilities() {
        /// Requires an owned value to be printable and transferable to a
        /// background thread.
        fn movable<T: Debug + Send + 'static>() {}
        movable::<Promise<Message>>();
        movable::<Promise<()>>();
    }
}