rustsmb/io/context.rs
1//! The [`IoContext`] typestate and the [`Command`] handler trait.
2//!
3//! `IoContext<S, O>` is the server's per-request work item, generic over its
4//! lifecycle state `S` and origin `O`. Transitions consume `self` by value, so
5//! the previous state becomes unusable — the borrow checker enforces the
6//! request lifecycle. Everything here is static dispatch (see the plan's §12).
7
8use smb_server_proto::types::Status;
9
10use super::origin::{Bare, Origin, Solicited, Unsolicited};
11use super::state::{Accepted, Pending};
12use super::wire::{ReplyHeader, SmbResponse};
13use super::SmbRequest;
14
15/// Per-request access to the mutable connection and shared server state a
16/// handler needs. Borrowed for the duration of one dispatch on the connection's
17/// single thread, so no locking is required for the per-connection half.
18pub struct Resources<'r> {
19 /// Per-connection state (handles, session, credits, durable table…).
20 pub conn: &'r mut crate::smb2::Smb2Conn,
21 /// Server-wide shared state (shares, lock/oplock/lease tables, sessions…).
22 pub server: &'r std::sync::Arc<crate::state::ServerShared>,
23 /// The complete single-command frame, for handlers that read the raw body
24 /// (e.g. WRITE payload) zero-copy.
25 pub frame: &'r [u8],
26}
27
28/// The server's work item for one request (or one server event). Owns the reply
29/// header and — while `Solicited` — the parsed request; parameterized by the
30/// lifecycle state `S` and origin `O`.
31#[derive(Debug)]
32pub struct IoContext<S: super::IoState, O: Origin = Solicited> {
33 /// Header the eventual reply is framed with.
34 pub reply: ReplyHeader,
35 /// Owned request payload for this origin (`SmbRequest` for `Solicited`,
36 /// `()` otherwise). Accessed only through the state-appropriate methods.
37 request: O::Request,
38 /// Lifecycle state value (a ZST for data-less states; carries data for
39 /// `Pending`).
40 state: S,
41}
42
43impl IoContext<Accepted, Solicited> {
44 /// Accept a parsed request for processing; the [`SmbRequest`] is moved in
45 /// and owned by the context from here on.
46 pub fn accept(reply: ReplyHeader, request: SmbRequest) -> Self {
47 IoContext { reply, request, state: Accepted }
48 }
49
50 /// Split the request out to hand it to its concrete [`Command`] handler; the
51 /// context keeps the reply header and becomes request-neutral (`Bare`).
52 pub(crate) fn split(self) -> (IoContext<Accepted, Bare>, SmbRequest) {
53 let ctx = IoContext { reply: self.reply, request: (), state: Accepted };
54 (ctx, self.request)
55 }
56}
57
58impl IoContext<Accepted, Unsolicited> {
59 /// Build a request-less context for a server-initiated response.
60 pub fn from_event(reply: ReplyHeader) -> Self {
61 IoContext { reply, request: (), state: Accepted }
62 }
63}
64
65impl<O: Origin> IoContext<Accepted, O> {
66 /// Produce the terminal reply, consuming the context (the request payload is
67 /// dropped). Reachable in the `Accepted` state for any origin.
68 pub fn respond(self, status: Status, body: Vec<u8>) -> SmbResponse {
69 SmbResponse { status, reply: self.reply, body }
70 }
71}
72
73impl IoContext<Accepted, Bare> {
74 /// Defer this request: park it as [`Pending`] so an async worker owns it and
75 /// completes it later; the reader loop is never blocked.
76 pub fn defer(self, async_id: u64) -> IoContext<Pending, Bare> {
77 IoContext { reply: self.reply, request: (), state: Pending { async_id } }
78 }
79}
80
81impl IoContext<Pending, Bare> {
82 /// The async correlation id this deferred op was parked under.
83 pub fn async_id(&self) -> u64 {
84 self.state.async_id
85 }
86
87 /// Build the immediate interim reply for this deferred op without consuming
88 /// the parked context ([MS-SMB2] §3.3.4.2). The worker still owns `self` and
89 /// completes it later.
90 pub fn interim(&self, status: Status, body: Vec<u8>) -> SmbResponse {
91 SmbResponse { status, reply: self.reply.clone(), body }
92 }
93
94 /// Complete a deferred op with its final reply, consuming the parked context.
95 /// Only callable in the `Pending` state, so a request cannot be completed
96 /// unless it was actually deferred.
97 pub fn complete(self, status: Status, body: Vec<u8>) -> SmbResponse {
98 SmbResponse { status, reply: self.reply, body }
99 }
100}
101
102/// The result of serving one accepted request. Encodes the final-vs-interim
103/// distinction in the type system.
104#[derive(Debug)]
105pub enum Outcome {
106 /// Terminal reply; the context was consumed.
107 Final(SmbResponse),
108 /// Interim STATUS_PENDING reply now; the final reply is owed later and the
109 /// work is parked in the returned [`Pending`] context ([MS-SMB2] §3.3.4.2).
110 Interim {
111 /// The parked context the async worker will `complete`.
112 parked: IoContext<Pending, Bare>,
113 /// The interim reply to send immediately.
114 interim: SmbResponse,
115 },
116 /// No reply at all (e.g. CANCEL of an unknown id, or a terminating check).
117 Silent,
118}
119
120/// One command's server-side behaviour: map its owned request to an [`Outcome`],
121/// using the connection/server [`Resources`]. Dispatched by a concrete `match`
122/// (never as a trait object), so it uses native `async fn` and needs no boxed
123/// future.
124#[allow(async_fn_in_trait)]
125pub trait Command {
126 /// The request type this command decodes and consumes.
127 type Request: super::Decode;
128 /// Serve the request. The context is request-neutral (`Bare`) and owns
129 /// everything except the request and the shared resources, passed alongside.
130 async fn serve(
131 ctx: IoContext<Accepted, Bare>,
132 req: Self::Request,
133 res: &mut Resources<'_>,
134 ) -> Outcome;
135}