libbarto/
utils.rs

1// Copyright (c) 2025 barto developers
2//
3// Licensed under the Apache License, Version 2.0
4// <LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0> or the MIT
5// license <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6// option. All files in the project carrying such notice may not be copied,
7// modified, or distributed except according to those terms.
8
9use std::{
10    path::PathBuf,
11    time::{Duration, Instant},
12};
13
14use anyhow::Result;
15use bytes::Bytes;
16use strip_ansi_escapes::strip;
17use unicode_width::UnicodeWidthStr as _;
18
19/// Convert a string to a `PathBuf`
20///
21/// # Errors
22/// * This function never errors, but is wrapped to use with `map_or_else` and similar
23///
24#[allow(clippy::unnecessary_wraps)]
25pub fn to_path_buf(path: &String) -> Result<PathBuf> {
26    Ok(PathBuf::from(path))
27}
28
29/// Send a timestamp ping
30#[must_use]
31pub fn send_ts_ping(origin: Instant) -> [u8; 12] {
32    let ts = Instant::now().duration_since(origin);
33    let (ts1, ts2) = (ts.as_secs(), ts.subsec_nanos());
34    let mut ts = [0; 12];
35    ts[0..8].copy_from_slice(&ts1.to_be_bytes());
36    ts[8..12].copy_from_slice(&ts2.to_be_bytes());
37    ts
38}
39
40/// Parse a received timestamp ping
41pub fn parse_ts_ping(bytes: &Bytes) -> Option<Duration> {
42    if bytes.len() == 12 {
43        let secs_bytes = <[u8; 8]>::try_from(&bytes[0..8]).unwrap_or([0; 8]);
44        let nanos_bytes = <[u8; 4]>::try_from(&bytes[8..12]).unwrap_or([0; 4]);
45        let secs = u64::from_be_bytes(secs_bytes);
46        let nanos = u32::from_be_bytes(nanos_bytes);
47        Some(Duration::new(secs, nanos))
48    } else {
49        None
50    }
51}
52
53#[allow(clippy::mut_mut)]
54pub(crate) fn until_err<T>(err: &mut &mut Result<()>, item: Result<T>) -> Option<T> {
55    match item {
56        Ok(item) => Some(item),
57        Err(e) => {
58            **err = Err(e);
59            None
60        }
61    }
62}
63
64/// Clean an output string by removing tabs, new lines, carriage returns, and ANSI escape codes.
65#[must_use]
66pub fn clean_output_string(data: &str) -> (String, usize) {
67    let data = data.replace('\t', "   ");
68    let data = data.replace('\n', " ");
69    let data = data.replace('\r', " ");
70    let final_data = String::from_utf8_lossy(&strip(data)).to_string();
71    let data_uw = final_data.width();
72    (final_data, data_uw)
73}
74
75#[cfg(test)]
76pub(crate) trait Mock {
77    fn mock() -> Self;
78}
79
80#[cfg(test)]
81pub(crate) mod test {
82    use std::time::Instant;
83
84    use bytes::Bytes;
85    use tracing::Level;
86    use tracing_subscriber_init::TracingConfig;
87    use unicode_width::UnicodeWidthStr as _;
88
89    use crate::TracingConfigExt;
90
91    use super::{clean_output_string, parse_ts_ping, send_ts_ping, to_path_buf};
92
93    pub(crate) struct TestConfig {
94        verbose: u8,
95        quiet: u8,
96        level: Level,
97        directives: Option<String>,
98    }
99
100    impl TestConfig {
101        pub(crate) fn with_directives() -> Self {
102            Self {
103                verbose: 3,
104                quiet: 0,
105                level: Level::INFO,
106                directives: Some("actix_web=error".to_string()),
107            }
108        }
109    }
110
111    impl Default for TestConfig {
112        fn default() -> Self {
113            Self {
114                verbose: 3,
115                quiet: 0,
116                level: Level::INFO,
117                directives: None,
118            }
119        }
120    }
121
122    impl TracingConfig for TestConfig {
123        fn quiet(&self) -> u8 {
124            self.quiet
125        }
126
127        fn verbose(&self) -> u8 {
128            self.verbose
129        }
130    }
131
132    impl TracingConfigExt for TestConfig {
133        fn level(&self) -> Level {
134            self.level
135        }
136
137        fn enable_stdout(&self) -> bool {
138            false
139        }
140
141        fn directives(&self) -> Option<&String> {
142            self.directives.as_ref()
143        }
144    }
145
146    #[test]
147    fn test_to_path_buf() {
148        let path_str = String::from("/some/test/path");
149        let path_buf = to_path_buf(&path_str).unwrap();
150        assert_eq!(path_buf.to_str().unwrap(), "/some/test/path");
151    }
152
153    #[test]
154    fn test_clean_output_string() {
155        let input = "Hello,\tWorld!\nThis is a test.\r\x1b[31mRed Text\x1b[0m";
156        let (cleaned, width) = clean_output_string(input);
157        assert_eq!(cleaned, "Hello,   World! This is a test. Red Text");
158        assert_eq!(width, cleaned.width()); // Ensure width matches cleaned string
159    }
160
161    #[test]
162    fn test_send_parse_ts_ping() {
163        let origin = Instant::now();
164        let ping = send_ts_ping(origin);
165        let bytes = Bytes::from(ping.to_vec());
166        let duration = parse_ts_ping(&bytes);
167        assert!(duration.is_some());
168    }
169
170    #[test]
171    fn test_parse_ts_ping_invalid() {
172        let bytes = Bytes::from(vec![0u8; 10]); // Invalid length
173        let duration = parse_ts_ping(&bytes);
174        assert!(duration.is_none());
175    }
176}