Skip to main content

imsg_obex/
client.rs

1use bytes::Bytes;
2use thiserror::Error;
3
4use crate::{
5    headers::Header,
6    packet::{OpCode, Packet, PacketError, PacketExtra},
7};
8
9const OBEX_VERSION: u8 = 0x10;
10const OBEX_FLAGS: u8 = 0x00;
11const OBEX_MAX_PACKET: u16 = 0xFFFF;
12
13// SETPATH 0x02: navigate to child, do not create
14const SETPATH_NAVIGATE: u8 = 0x02;
15// SETPATH 0x03: navigate to parent (backup bit set, no-create)
16const SETPATH_BACKUP: u8 = 0x03;
17
18/// OBEX client errors — connection state, packet codec, server rejection, and missing protocol headers.
19#[derive(Debug, Error)]
20pub enum ObexError {
21    /// No active connection; call `handle_connect_response` first.
22    #[error("not connected")]
23    NotConnected,
24    /// Packet codec failure during request encoding or response decode.
25    #[error("packet error: {0}")]
26    Packet(#[from] PacketError),
27    /// Server refused CONNECT; carries the response opcode byte.
28    #[error("connect rejected with opcode {0:#04x}")]
29    ConnectRejected(u8),
30    /// CONNECT response did not include a `ConnectionId` header; the session cannot be used.
31    #[error("connect response missing ConnectionId header")]
32    MissingConnectionId,
33    /// Message body exceeds the 4 GiB limit the OBEX `Length` header can express.
34    #[error("message body too large")]
35    BodyTooLarge,
36}
37
38enum State {
39    Disconnected,
40    Connected { conn_id: u32, max_packet: u16 },
41}
42
43/// Sans-IO OBEX client state machine. No I/O — callers handle transport.
44pub struct ObexClient {
45    state: State,
46}
47
48impl ObexClient {
49    /// Initial state: disconnected.
50    #[must_use]
51    pub const fn new() -> Self {
52        Self { state: State::Disconnected }
53    }
54
55    /// Targets the given 16-byte service UUID.
56    ///
57    /// # Errors
58    /// Returns `Packet` if encoding fails (packet too large — not possible in practice).
59    pub fn connect_request(target_uuid: &[u8; 16]) -> Result<Bytes, ObexError> {
60        Ok(Packet {
61            opcode: OpCode::Connect,
62            extra: PacketExtra::Connect {
63                version: OBEX_VERSION,
64                flags: OBEX_FLAGS,
65                max_packet: OBEX_MAX_PACKET,
66            },
67            headers: vec![Header::Target(Bytes::copy_from_slice(target_uuid))],
68        }
69        .encode()?)
70    }
71
72    /// Transitions to Connected on success; returns the assigned connection ID.
73    ///
74    /// # Errors
75    /// Returns `ConnectRejected` if the server returns a non-OK opcode, `MissingConnectionId`
76    /// if the response contains no `ConnectionId` header, or `Packet` on decode failure.
77    pub fn handle_connect_response(&mut self, data: &[u8]) -> Result<u32, ObexError> {
78        let packet = Packet::decode_connect_response(data)?;
79        if !packet.opcode.is_ok() {
80            return Err(ObexError::ConnectRejected(packet.opcode.to_byte()));
81        }
82        let conn_id = packet.header_connection_id().ok_or(ObexError::MissingConnectionId)?;
83        let max_packet = match &packet.extra {
84            PacketExtra::Connect { max_packet, .. } => *max_packet,
85            _ => OBEX_MAX_PACKET,
86        };
87        self.state = State::Connected { conn_id, max_packet };
88        Ok(conn_id)
89    }
90
91    /// SETPATH navigate-to-child by name.
92    ///
93    /// # Errors
94    /// Returns `NotConnected` if called before a successful CONNECT exchange.
95    pub fn setpath_request(&self, name: &str) -> Result<Bytes, ObexError> {
96        let conn_id = self.conn_id()?;
97        Ok(Packet {
98            opcode: OpCode::SetPath,
99            extra: PacketExtra::SetPath { flags: SETPATH_NAVIGATE, constants: 0x00 },
100            headers: vec![Header::ConnectionId(conn_id), Header::Name(name.to_owned())],
101        }
102        .encode()?)
103    }
104
105    /// SETPATH backup bit, empty Name. Use before navigating to a sibling. Does not validate current depth.
106    ///
107    /// # Errors
108    ///
109    /// Returns [`ObexError::NotConnected`] if called before a successful CONNECT exchange.
110    pub fn setpath_backup_request(&self) -> Result<Bytes, ObexError> {
111        let conn_id = self.conn_id()?;
112        Ok(Packet {
113            opcode: OpCode::SetPath,
114            extra: PacketExtra::SetPath { flags: SETPATH_BACKUP, constants: 0x00 },
115            headers: vec![Header::ConnectionId(conn_id), Header::Name(String::new())],
116        }
117        .encode()?)
118    }
119
120    /// GET FINAL with `Type`, optional `Name`, and optional `AppParams`.
121    ///
122    /// # Errors
123    /// Returns `NotConnected` if called before a successful CONNECT exchange.
124    pub fn get_request(
125        &self,
126        type_: &[u8],
127        name: Option<&str>,
128        app_params: Option<Bytes>,
129    ) -> Result<Bytes, ObexError> {
130        let conn_id = self.conn_id()?;
131        let mut headers =
132            vec![Header::ConnectionId(conn_id), Header::Type(Bytes::copy_from_slice(type_))];
133        if let Some(n) = name {
134            headers.push(Header::Name(n.to_owned()));
135        }
136        if let Some(params) = app_params {
137            headers.push(Header::AppParams(params));
138        }
139        Ok(Packet { opcode: OpCode::GetFinal, extra: PacketExtra::None, headers }.encode()?)
140    }
141
142    /// GET FINAL with only `ConnectionId` — continues a multi-packet exchange after `Continue`.
143    ///
144    /// # Errors
145    /// Returns `NotConnected` if called before a successful CONNECT exchange.
146    pub fn get_continue_request(&self) -> Result<Bytes, ObexError> {
147        let conn_id = self.conn_id()?;
148        Ok(Packet {
149            opcode: OpCode::GetFinal,
150            extra: PacketExtra::None,
151            headers: vec![Header::ConnectionId(conn_id)],
152        }
153        .encode()?)
154    }
155
156    /// Prepends `ConnectionId` and `Type` before `extra_headers`; opcode is always `PutFinal`.
157    ///
158    /// # Errors
159    ///
160    /// Returns `NotConnected` if called before a successful `CONNECT` exchange.
161    /// Returns `Packet` if encoding fails (not possible in practice for typical header counts).
162    pub fn put_final_request(
163        &self,
164        type_: &[u8],
165        extra_headers: Vec<Header>,
166    ) -> Result<Bytes, ObexError> {
167        let conn_id = self.conn_id()?;
168        let mut headers =
169            vec![Header::ConnectionId(conn_id), Header::Type(Bytes::copy_from_slice(type_))];
170        headers.extend(extra_headers);
171        Ok(Packet { opcode: OpCode::PutFinal, extra: PacketExtra::None, headers }.encode()?)
172    }
173
174    /// Sends `ConnectionId` in the DISCONNECT payload.
175    ///
176    /// # Errors
177    /// Returns `NotConnected` if called before a successful CONNECT exchange.
178    pub fn disconnect_request(&self) -> Result<Bytes, ObexError> {
179        let conn_id = self.conn_id()?;
180        Ok(Packet {
181            opcode: OpCode::Disconnect,
182            extra: PacketExtra::None,
183            headers: vec![Header::ConnectionId(conn_id)],
184        }
185        .encode()?)
186    }
187
188    /// Stateless decode — does not advance client state.
189    ///
190    /// # Errors
191    /// Returns `Packet` on any decode failure.
192    pub fn parse_response(data: &[u8]) -> Result<Packet, ObexError> {
193        Ok(Packet::decode(data)?)
194    }
195
196    /// Connection ID assigned by the remote in the CONNECT response.
197    ///
198    /// # Errors
199    ///
200    /// Returns [`ObexError::NotConnected`] before a successful CONNECT exchange.
201    pub const fn conn_id(&self) -> Result<u32, ObexError> {
202        match &self.state {
203            State::Connected { conn_id, .. } => Ok(*conn_id),
204            State::Disconnected => Err(ObexError::NotConnected),
205        }
206    }
207
208    /// True after a successful CONNECT exchange; false after disconnect or before first connect.
209    #[must_use]
210    pub const fn is_connected(&self) -> bool {
211        matches!(self.state, State::Connected { .. })
212    }
213
214    /// 0 if not connected; otherwise the server-negotiated max.
215    #[must_use]
216    pub const fn max_packet(&self) -> u16 {
217        match &self.state {
218            State::Connected { max_packet, .. } => *max_packet,
219            State::Disconnected => 0,
220        }
221    }
222}
223
224impl Default for ObexClient {
225    fn default() -> Self {
226        Self::new()
227    }
228}