Skip to main content

darkbio_wire/protocol/
responder.rs

1// wire-rs: encrypted protocol between Ark and host
2// Copyright 2026 Dark Bio AG. All rights reserved.
3
4//! Sending one reply to a received request, or `UNANSWERED` when dropped.
5
6use super::session::SessionInner;
7use super::{Error, Message, Promise, RemoteError};
8use std::sync::Weak;
9use std::time::Instant;
10
11/// Handle for answering one incoming request through the session that received it.
12/// The handler selects the success content, without a static request/response map.
13/// This handle cannot keep its session open or address a replacement session.
14///
15/// Dropping an unanswered responder queues an `UNANSWERED` error without blocking
16/// on I/O, using the session's current abandonment timeout.
17/// Configure it with [`super::Session::set_abandonment_timeout`] or
18/// [`super::Server::set_abandonment_timeout`]. If the session has closed, no reply
19/// is queued.
20///
21/// A held responder counts toward the session's inbound request limit. Queuing a
22/// reply keeps that slot until the writer takes it or the reply is discarded.
23/// See [`super::Session::set_inbound_limits`].
24///
25/// Both [`Self::reply`] and [`Self::fail`] consume the responder, so it cannot be reused:
26///
27/// ```compile_fail,E0382
28/// use darkbio_wire::protocol::{DeviceInfoResponse, RemoteError, Responder};
29/// use std::time::Instant;
30///
31/// fn answer_twice(responder: Responder, deadline: Instant) {
32///     let _ = responder.reply(DeviceInfoResponse::default(), deadline);
33///     let _ = responder.fail(RemoteError::new(0x100, "refused"), deadline);
34/// }
35/// ```
36///
37/// Responders cannot be cloned either:
38///
39/// ```compile_fail,E0599
40/// use darkbio_wire::protocol::Responder;
41/// fn duplicate(responder: Responder) { let _ = responder.clone(); }
42/// ```
43pub struct Responder {
44    /// Session that received the request; holding a responder cannot keep it open.
45    session: Weak<SessionInner>,
46    /// Request ID to answer. Cleared after queueing a reply so `Drop` does nothing.
47    id: Option<u64>,
48}
49
50impl Responder {
51    /// Queues a successful response and consumes the responder. Returns a promise
52    /// for writing and flushing it. A closed session returns an error immediately.
53    /// The deadline includes time in the queue and I/O. Waiting on the promise
54    /// does not restart it.
55    ///
56    /// A message invalid for this session's direction fails the promise with
57    /// [`Error::WrongDirection`].
58    ///
59    /// A reply needs no further acknowledgment. Dropping its promise leaves it queued.
60    /// Accepts a protobuf response or [`Message`] directly. Use [`Self::fail`] to
61    /// return an error instead.
62    pub fn reply(
63        self,
64        response: impl Into<Message>,
65        deadline: Instant,
66    ) -> Result<Promise<()>, Error> {
67        self.enqueue(Ok(response.into()), deadline)
68    }
69
70    /// Queues an error response and consumes the responder. The deadline and write
71    /// promise work as in [`Self::reply`]. The error itself does not close the session.
72    /// Use [`RemoteError::reserved`] for a named protocol error or
73    /// [`RemoteError::new`] for a numeric error code.
74    pub fn fail(self, error: RemoteError, deadline: Instant) -> Result<Promise<()>, Error> {
75        self.enqueue(Err(error), deadline)
76    }
77
78    /// Queues either kind of response and marks this responder as answered.
79    fn enqueue(
80        mut self,
81        result: Result<Message, RemoteError>,
82        deadline: Instant,
83    ) -> Result<Promise<()>, Error> {
84        let promise = self.session.upgrade().ok_or(Error::Closed)?.reply(
85            self.id.expect("reply obligation present"),
86            result,
87            deadline,
88        )?;
89        self.id = None; // Prevent Drop from also queueing UNANSWERED.
90        Ok(promise)
91    }
92
93    /// Creates a responder for the request taken by `Session::recv()`.
94    pub(super) fn new(session: Weak<SessionInner>, id: u64) -> Self {
95        Self {
96            session,
97            id: Some(id),
98        }
99    }
100}
101
102impl Drop for Responder {
103    /// Queues `UNANSWERED` if this responder still has an ID and its session is
104    /// open. The writer sends the error later.
105    fn drop(&mut self) {
106        if let Some(id) = self.id.take()
107            && let Some(session) = self.session.upgrade()
108        {
109            session.reply_unanswered(id);
110        }
111    }
112}
113
114/// Checks responder ownership and compiles success, error and deferred reply paths.
115#[cfg(test)]
116#[cfg_attr(coverage_nightly, coverage(off))]
117mod tests {
118    use crate::protocol::{
119        DeviceInfoResponse, Error, Message, Promise, RemoteError, ReservedErrors, Responder,
120        Session,
121    };
122    use std::time::Instant;
123
124    /// Compiles receiving a host-side request and returning an application error.
125    #[allow(dead_code)]
126    fn receive_on_host(session: &mut Session, deadline: Instant) -> Result<(), Error> {
127        let (request, responder): (Message, Responder) = session.recv()?;
128        let _ = request;
129        responder
130            .fail(
131                RemoteError::reserved(ReservedErrors::Unspecified, "refused"),
132                deadline,
133            )?
134            .wait()
135    }
136
137    /// Compiles immediate replies, a background reverse request and responder abandonment.
138    #[allow(dead_code)]
139    fn receive_on_server(session: &mut Session, deadline: Instant) -> Result<(), Error> {
140        let (request, responder): (Message, Responder) = session.recv()?;
141        match request {
142            Message::DeviceInfoRequest(_) => {
143                let written: Promise<()> =
144                    responder.reply(DeviceInfoResponse::default(), deadline)?;
145                written.wait()?;
146            }
147            Message::Develop(bytes) => {
148                // Opaque development traffic is supported in both directions.
149                let requester = session.requester();
150                std::thread::spawn(move || -> Result<(), Error> {
151                    let answer: Vec<u8> = requester.request(bytes, deadline)?.wait()?;
152                    drop(responder.reply(answer, deadline)?);
153                    Ok(())
154                });
155            }
156            _ => drop(responder), // Schedules the standard unanswered error.
157        }
158        Ok(())
159    }
160
161    /// Checks the send bound required to transfer ownership to an application thread.
162    #[test]
163    fn test_thread_capabilities() {
164        /// Requires an owned value to be transferable to a background thread.
165        fn movable<T: Send + 'static>() {}
166        movable::<Responder>();
167    }
168}