Skip to main content

lightstreamer_rs/utils/
util.rs

1use std::sync::Arc;
2use tokio::sync::Notify;
3#[cfg(windows)]
4use tracing::error;
5use tracing::info;
6
7/// Clean the message from newlines and carriage returns.
8///
9/// Only the **message type prefix** (the portion before the first comma) is
10/// lowercased so that downstream `match` arms can use lowercase literals
11/// (`"u"`, `"conok"`, …). The rest of the message — including field values,
12/// session identifiers and protocol tokens — is preserved verbatim so that
13/// casing‑sensitive data (e.g. `DEAL`, `^P`, `^T`) is not corrupted.
14///
15/// Content inside curly braces `{}` is always preserved as‑is.
16pub fn clean_message(text: &str) -> String {
17    let chars_to_replace = ['\n', '\r'];
18    let stripped = text.replace(chars_to_replace, "");
19
20    // Find the first comma that is outside curly braces.
21    let mut in_brackets: usize = 0;
22    let mut first_comma: Option<usize> = None;
23    for (i, c) in stripped.char_indices() {
24        match c {
25            '{' => in_brackets += 1,
26            '}' => in_brackets = in_brackets.saturating_sub(1),
27            ',' if in_brackets == 0 => {
28                first_comma = Some(i);
29                break;
30            }
31            _ => {}
32        }
33    }
34
35    match first_comma {
36        Some(pos) => {
37            // Lowercase only the message‑type prefix.
38            let mut result = String::with_capacity(stripped.len());
39            result.push_str(&stripped[..pos].to_lowercase());
40            result.push_str(&stripped[pos..]);
41            result
42        }
43        None => {
44            // No comma found — the whole string is the prefix (e.g. "PROBE").
45            stripped.to_lowercase()
46        }
47    }
48}
49
50/// Parses a comma-separated string input into a vector of string slices (`Vec<&str>`).
51///
52/// This function supports skipping commas inside nested curly braces `{}`. It correctly handles
53/// nested structures, ensuring that commas within curly braces are not treated as delimiters.
54///
55/// # Parameters
56/// - `input`: A string slice (`&str`) containing comma-separated values, potentially with nested curly braces.
57///
58/// # Returns
59/// A `Vec<&str>` containing trimmed substrings split by commas outside of curly braces.
60///
61/// # Behavior
62/// - Commas outside of curly braces `{}` are treated as delimiters.
63/// - Commas inside curly braces are ignored for splitting purposes.
64/// - Leading and trailing whitespace around substrings are trimmed.
65/// - Empty substrings (those consisting solely of whitespace) are ignored.
66///
67/// # Caveats
68/// - The function requires matched curly braces `{}`. If the input contains unmatched curly braces,
69///   the function may produce unexpected results.
70///
71/// # Panics
72/// This function does not explicitly panic, but improper manipulation of indices or unmatched
73/// braces could lead to unintended behavior.
74///
75/// # Errors in Current Code:
76/// - There is a bug in the code where `arguments.push(*slice)` is used. The dereference operator
77///   (`*`) is invalid for string slices. It should be `arguments.push(slice)`.
78/// - Recommend fixing this bug by removing the dereference operator for proper functionality.
79pub fn parse_arguments(input: &str) -> Vec<&str> {
80    let mut arguments = Vec::new();
81    let mut start = 0;
82    let mut in_brackets = 0; // Tracks nesting level for curly braces
83
84    for (i, c) in input.char_indices() {
85        match c {
86            '{' => in_brackets += 1,
87            '}' => in_brackets -= 1,
88            ',' if in_brackets == 0 => {
89                // Outside of brackets, treat comma as a delimiter
90                let slice = &input[start..i].trim();
91                if !slice.is_empty() {
92                    arguments.push(*slice); // Dereference slice here
93                }
94                start = i + 1;
95            }
96            _ => {}
97        }
98    }
99
100    // Push the final argument if it's not empty
101    if start < input.len() {
102        let slice = &input[start..].trim();
103        if !slice.is_empty() {
104            arguments.push(*slice); // Dereference slice here
105        }
106    }
107
108    arguments
109}
110
111/// Sets up cross-platform signal handling for graceful shutdown.
112/// On Unix systems, handles SIGINT and SIGTERM signals.
113/// On Windows, handles Ctrl+C signal.
114///
115/// # Arguments
116///
117/// * `shutdown_signal` - `Arc<Notify>` to signal shutdown to other parts of the application.
118///
119/// # Panics
120///
121/// The function panics if it fails to create the signal handlers.
122///
123pub async fn setup_signal_hook(shutdown_signal: Arc<Notify>) {
124    #[cfg(unix)]
125    {
126        use tokio::signal::unix::{SignalKind, signal};
127        let mut sigint = signal(SignalKind::interrupt()).expect("Failed to create SIGINT handler");
128        let mut sigterm =
129            signal(SignalKind::terminate()).expect("Failed to create SIGTERM handler");
130
131        tokio::spawn(async move {
132            tokio::select! {
133                _ = sigint.recv() => {
134                    info!("Received SIGINT signal");
135                    shutdown_signal.notify_one();
136                }
137                _ = sigterm.recv() => {
138                    info!("Received SIGTERM signal");
139                    shutdown_signal.notify_one();
140                }
141            }
142        });
143    }
144
145    #[cfg(windows)]
146    {
147        use tokio::signal;
148        tokio::spawn(async move {
149            match signal::ctrl_c().await {
150                Ok(()) => {
151                    info!("Received Ctrl+C signal");
152                    shutdown_signal.notify_one();
153                }
154                Err(err) => {
155                    error!("Failed to listen for Ctrl+C: {}", err);
156                }
157            }
158        });
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    mod clean_message_tests {
167        use super::*;
168
169        #[test]
170        fn test_clean_message_basic() {
171            // No comma → entire string is the prefix → lowercased
172            let text = "Hello\nWorld";
173            let result = clean_message(text);
174            assert_eq!(result, "helloworld");
175        }
176
177        #[test]
178        fn test_clean_message_with_partial_braces() {
179            // No comma → entire string is the prefix → lowercased
180            let text = "{partial brace content} followed by text";
181            let result = clean_message(text);
182            assert_eq!(result, "{partial brace content} followed by text");
183        }
184
185        #[test]
186        fn test_clean_message_with_ending_brace() {
187            // No comma → entire string is the prefix → lowercased
188            let text = "text followed by {partial brace content}";
189            let result = clean_message(text);
190            assert_eq!(result, "text followed by {partial brace content}");
191        }
192
193        #[test]
194        fn test_clean_message_with_carriage_return() {
195            // No comma → entire string is the prefix → lowercased
196            let text = "Hello\r\nWorld";
197            let result = clean_message(text);
198            assert_eq!(result, "helloworld");
199        }
200
201        #[test]
202        fn test_clean_message_lowercase_conversion() {
203            // No comma → entire string is the prefix → lowercased
204            let text = "Hello WORLD";
205            let result = clean_message(text);
206            assert_eq!(result, "hello world");
207        }
208
209        #[test]
210        fn test_clean_message_empty_string() {
211            let text = "";
212            let result = clean_message(text);
213            assert_eq!(result, "");
214        }
215
216        #[test]
217        fn test_clean_message_preserve_braces_content() {
218            // No comma → entire string is the prefix → lowercased (braces don't matter
219            // when the whole message is a single token).
220            let text = "Message with {Preserved\nContent} and not preserved\nContent";
221            let result = clean_message(text);
222            assert_eq!(
223                result,
224                "message with {preservedcontent} and not preservedcontent"
225            );
226        }
227
228        #[test]
229        fn test_clean_message_nested_braces() {
230            // No comma → entire string is the prefix → lowercased
231            let text = "Message with {Outer{Inner\nContent}Outer} and regular\nContent";
232            let result = clean_message(text);
233            assert_eq!(
234                result,
235                "message with {outer{innercontent}outer} and regularcontent"
236            );
237        }
238
239        #[test]
240        fn test_clean_message_unbalanced_braces() {
241            // No comma → entire string is the prefix → lowercased
242            let text = "Message with {Unbalanced and regular\nContent";
243            let result = clean_message(text);
244            assert_eq!(result, "message with {unbalanced and regularcontent");
245        }
246
247        #[test]
248        fn test_clean_message_protocol_example() {
249            // Only the prefix is lowercased; the session ID preserves casing.
250            let text = "CONOK,S8f4aec42c3c14ad0,50000,5000,*\r\n";
251            let result = clean_message(text);
252            assert_eq!(result, "conok,S8f4aec42c3c14ad0,50000,5000,*");
253
254            let text = "PROBE\r\n";
255            let result = clean_message(text);
256            assert_eq!(result, "probe");
257        }
258
259        #[test]
260        fn test_clean_message_update_preserves_field_values() {
261            // Update messages: only "U" → "u", field values are preserved.
262            let text = "U,1,1,DEAL|1.32|#|^P\r\n";
263            let result = clean_message(text);
264            assert_eq!(result, "u,1,1,DEAL|1.32|#|^P");
265        }
266
267        #[test]
268        fn test_clean_message_update_preserves_caret_commands() {
269            let text = "U,1,1,^5|CLOSED|-0.5\r\n";
270            let result = clean_message(text);
271            assert_eq!(result, "u,1,1,^5|CLOSED|-0.5");
272        }
273
274        #[test]
275        fn test_clean_message_reqerr_preserves_details() {
276            let text = "REQERR,21,InvalidField\r\n";
277            let result = clean_message(text);
278            assert_eq!(result, "reqerr,21,InvalidField");
279        }
280    }
281
282    mod parse_arguments_tests {
283        use super::*;
284
285        #[test]
286        fn test_parse_arguments_basic() {
287            let input = "arg1,arg2,arg3";
288            let result = parse_arguments(input);
289            assert_eq!(result, vec!["arg1", "arg2", "arg3"]);
290        }
291
292        #[test]
293        fn test_parse_arguments_empty_string() {
294            let input = "";
295            let result = parse_arguments(input);
296            assert_eq!(result, Vec::<&str>::new());
297        }
298
299        #[test]
300        fn test_parse_arguments_single_argument() {
301            let input = "arg1";
302            let result = parse_arguments(input);
303            assert_eq!(result, vec!["arg1"]);
304        }
305
306        #[test]
307        fn test_parse_arguments_with_whitespace() {
308            let input = " arg1 , arg2 , arg3 ";
309            let result = parse_arguments(input);
310            assert_eq!(result, vec!["arg1", "arg2", "arg3"]);
311        }
312
313        #[test]
314        fn test_parse_arguments_empty_arguments() {
315            let input = "arg1,,arg3";
316            let result = parse_arguments(input);
317            assert_eq!(result, vec!["arg1", "arg3"]);
318        }
319
320        #[test]
321        fn test_parse_arguments_with_braces() {
322            let input = "arg1,{inner1,inner2},arg3";
323            let result = parse_arguments(input);
324            assert_eq!(result, vec!["arg1", "{inner1,inner2}", "arg3"]);
325        }
326
327        #[test]
328        fn test_parse_arguments_nested_braces() {
329            let input = "arg1,{outer{inner1,inner2}outer},arg3";
330            let result = parse_arguments(input);
331            assert_eq!(result, vec!["arg1", "{outer{inner1,inner2}outer}", "arg3"]);
332        }
333
334        #[test]
335        fn test_parse_arguments_unbalanced_braces() {
336            let input = "arg1,{unbalanced,arg3";
337            let result = parse_arguments(input);
338            // Even with unbalanced braces, we expect it to treat everything inside as one argument
339            assert_eq!(result, vec!["arg1", "{unbalanced,arg3"]);
340        }
341
342        #[test]
343        fn test_parse_arguments_protocol_examples() {
344            // TLCP protocol message example arguments
345            let input = "CONOK,S8f4aec42c3c14ad0,50000,5000,*";
346            let result = parse_arguments(input);
347            assert_eq!(
348                result,
349                vec!["CONOK", "S8f4aec42c3c14ad0", "50000", "5000", "*"]
350            );
351
352            let input = "u,1,1,a|b|c";
353            let result = parse_arguments(input);
354            assert_eq!(result, vec!["u", "1", "1", "a|b|c"]);
355        }
356    }
357}