Skip to main content

truefix_twsapi_client/
comm.rs

1use crate::constants::{INFINITY_STR, UNSET_DOUBLE, UNSET_INTEGER};
2use crate::error::{TwsApiError, TwsApiResult};
3
4/// A parsed length-prefixed frame and the remaining bytes after it.
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct Frame<'a> {
7    /// Payload length from the 4-byte big-endian prefix.
8    pub size: usize,
9    /// Complete frame payload.
10    pub payload: &'a [u8],
11    /// Bytes following the complete frame.
12    pub rest: &'a [u8],
13}
14
15/// Builds the enhanced-handshake message body, including the 4-byte length prefix.
16pub fn make_initial_msg(text: &str) -> Vec<u8> {
17    let bytes = text.as_bytes();
18    let mut out = Vec::with_capacity(4 + bytes.len());
19    out.extend_from_slice(&(bytes.len() as u32).to_be_bytes());
20    out.extend_from_slice(bytes);
21    out
22}
23
24/// Builds the full `API\0` enhanced handshake.
25pub fn make_client_handshake(
26    min_client_ver: i32,
27    max_client_ver: i32,
28    options: Option<&str>,
29) -> Vec<u8> {
30    let mut version = format!("v{min_client_ver}..{max_client_ver}");
31    if let Some(options) = options.filter(|value| !value.is_empty()) {
32        version.push(' ');
33        version.push_str(options);
34    }
35
36    let mut out = b"API\0".to_vec();
37    out.extend_from_slice(&make_initial_msg(&version));
38    out
39}
40
41/// Builds a length-prefixed protobuf payload with a raw 4-byte message id.
42pub fn make_msg_proto(msg_id: i32, protobuf_data: &[u8]) -> Vec<u8> {
43    let mut payload = Vec::with_capacity(4 + protobuf_data.len());
44    payload.extend_from_slice(&msg_id.to_be_bytes());
45    payload.extend_from_slice(protobuf_data);
46
47    let mut out = Vec::with_capacity(4 + payload.len());
48    out.extend_from_slice(&(payload.len() as u32).to_be_bytes());
49    out.extend_from_slice(&payload);
50    out
51}
52
53/// Builds a length-prefixed field-based payload.
54pub fn make_msg(msg_id: i32, use_raw_int_msg_id: bool, text: &str) -> TwsApiResult<Vec<u8>> {
55    let mut payload = Vec::new();
56    if use_raw_int_msg_id {
57        payload.extend_from_slice(&msg_id.to_be_bytes());
58        payload.extend_from_slice(text.as_bytes());
59    } else {
60        payload.extend_from_slice(make_field(msg_id)?.as_bytes());
61        payload.extend_from_slice(text.as_bytes());
62    }
63
64    let mut out = Vec::with_capacity(4 + payload.len());
65    out.extend_from_slice(&(payload.len() as u32).to_be_bytes());
66    out.extend_from_slice(&payload);
67    Ok(out)
68}
69
70/// Encodes a NUL-terminated field.
71pub fn make_field<T>(value: T) -> TwsApiResult<String>
72where
73    T: TwsField,
74{
75    let text = value.to_tws_field()?;
76    ensure_ascii_printable(&text)?;
77    Ok(format!("{text}\0"))
78}
79
80/// Encodes a NUL-terminated field, mapping unset integer/double sentinels to an empty value.
81pub fn make_field_handle_empty<T>(value: T) -> TwsApiResult<String>
82where
83    T: TwsNullableField,
84{
85    let text = value.to_tws_nullable_field()?;
86    ensure_ascii_printable(&text)?;
87    Ok(format!("{text}\0"))
88}
89
90/// Reads one complete frame from `buf`.
91pub fn read_msg(buf: &[u8]) -> TwsApiResult<Option<Frame<'_>>> {
92    if buf.len() < 4 {
93        return Ok(None);
94    }
95
96    let (prefix, data) = buf.split_at(4);
97    let prefix: [u8; 4] = prefix
98        .try_into()
99        .map_err(|_| TwsApiError::IncompleteFrame {
100            needed: 4,
101            available: buf.len(),
102        })?;
103    let size = u32::from_be_bytes(prefix);
104    let size = usize::try_from(size).map_err(|_| TwsApiError::FrameTooLarge(size))?;
105    let needed = 4 + size;
106    if buf.len() < needed {
107        return Ok(None);
108    }
109
110    Ok(Some(Frame {
111        size,
112        payload: data.get(..size).ok_or(TwsApiError::IncompleteFrame {
113            needed,
114            available: buf.len(),
115        })?,
116        rest: data.get(size..).ok_or(TwsApiError::IncompleteFrame {
117            needed,
118            available: buf.len(),
119        })?,
120    }))
121}
122
123/// Splits a payload into NUL-terminated fields, dropping the final empty segment like Python.
124pub fn read_fields(buf: &[u8]) -> Vec<&[u8]> {
125    let mut fields = buf.split(|byte| *byte == 0).collect::<Vec<_>>();
126    if fields.last().is_some_and(|field| field.is_empty()) {
127        fields.pop();
128    }
129    fields
130}
131
132/// TWS field serialization used by [`make_field`].
133pub trait TwsField {
134    /// Converts a value into its TWS field string.
135    fn to_tws_field(self) -> TwsApiResult<String>;
136}
137
138/// TWS field serialization used by [`make_field_handle_empty`].
139pub trait TwsNullableField {
140    /// Converts a value into its TWS field string, with unset sentinels mapped to empty.
141    fn to_tws_nullable_field(self) -> TwsApiResult<String>;
142}
143
144impl TwsField for &str {
145    fn to_tws_field(self) -> TwsApiResult<String> {
146        Ok(self.to_owned())
147    }
148}
149
150impl TwsField for &String {
151    fn to_tws_field(self) -> TwsApiResult<String> {
152        Ok(self.clone())
153    }
154}
155
156impl TwsField for String {
157    fn to_tws_field(self) -> TwsApiResult<String> {
158        Ok(self)
159    }
160}
161
162impl TwsField for bool {
163    fn to_tws_field(self) -> TwsApiResult<String> {
164        Ok(i32::from(self).to_string())
165    }
166}
167
168impl TwsField for i32 {
169    fn to_tws_field(self) -> TwsApiResult<String> {
170        Ok(self.to_string())
171    }
172}
173
174impl TwsField for i64 {
175    fn to_tws_field(self) -> TwsApiResult<String> {
176        Ok(self.to_string())
177    }
178}
179
180impl TwsField for usize {
181    fn to_tws_field(self) -> TwsApiResult<String> {
182        Ok(self.to_string())
183    }
184}
185
186impl TwsField for f64 {
187    fn to_tws_field(self) -> TwsApiResult<String> {
188        let mut value = self.to_string();
189        if self.is_finite() && !value.contains('.') && !value.contains('e') && !value.contains('E')
190        {
191            value.push_str(".0");
192        }
193        Ok(value)
194    }
195}
196
197impl TwsNullableField for i32 {
198    fn to_tws_nullable_field(self) -> TwsApiResult<String> {
199        if self == UNSET_INTEGER {
200            return Ok(String::new());
201        }
202        self.to_tws_field()
203    }
204}
205
206impl TwsNullableField for f64 {
207    fn to_tws_nullable_field(self) -> TwsApiResult<String> {
208        if self == UNSET_DOUBLE {
209            return Ok(String::new());
210        }
211        if self.is_infinite() && self.is_sign_positive() {
212            return Ok(INFINITY_STR.to_owned());
213        }
214        self.to_tws_field()
215    }
216}
217
218impl<T> TwsNullableField for Option<T>
219where
220    T: TwsField,
221{
222    fn to_tws_nullable_field(self) -> TwsApiResult<String> {
223        match self {
224            Some(value) => value.to_tws_field(),
225            None => Err(TwsApiError::MissingField),
226        }
227    }
228}
229
230fn ensure_ascii_printable(value: &str) -> TwsApiResult<()> {
231    if value
232        .bytes()
233        .all(|byte| byte == b'\t' || (0x20..=0x7e).contains(&byte))
234    {
235        return Ok(());
236    }
237    Err(TwsApiError::NonPrintableAscii(value.to_owned()))
238}