Skip to main content

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 time::{OffsetDateTime, macros::time};
18use unicode_width::UnicodeWidthStr as _;
19
20/// Convert a string to a `PathBuf`
21///
22/// # Errors
23/// * This function never errors, but is wrapped to use with `map_or_else` and similar
24///
25#[allow(clippy::unnecessary_wraps)]
26pub fn to_path_buf(path: &String) -> Result<PathBuf> {
27    Ok(PathBuf::from(path))
28}
29
30/// The cutoff timestamp used for date-based cleanup: today's local midnight.
31///
32/// Records with a timestamp older than this are considered stale and eligible for
33/// deletion. Both the `bartos` `MariaDB` cleanup and the `bartoc` redb cleanup use this so
34/// they apply identical semantics.
35///
36/// # Errors
37/// * Returns an error if the local UTC offset cannot be determined.
38///
39pub fn midnight() -> Result<OffsetDateTime> {
40    let now = OffsetDateTime::now_local()?;
41    Ok(now.replace_time(time!(0:0:0)))
42}
43
44/// Send a timestamp ping
45#[must_use]
46pub fn send_ts_ping(origin: Instant) -> [u8; 12] {
47    let ts = Instant::now().duration_since(origin);
48    let (ts1, ts2) = (ts.as_secs(), ts.subsec_nanos());
49    let mut ts = [0; 12];
50    ts[0..8].copy_from_slice(&ts1.to_be_bytes());
51    ts[8..12].copy_from_slice(&ts2.to_be_bytes());
52    ts
53}
54
55/// Parse a received timestamp ping
56pub fn parse_ts_ping(bytes: &Bytes) -> Option<Duration> {
57    if bytes.len() == 12 {
58        let secs_bytes = <[u8; 8]>::try_from(&bytes[0..8]).unwrap_or([0; 8]);
59        let nanos_bytes = <[u8; 4]>::try_from(&bytes[8..12]).unwrap_or([0; 4]);
60        let secs = u64::from_be_bytes(secs_bytes);
61        let nanos = u32::from_be_bytes(nanos_bytes);
62        Some(Duration::new(secs, nanos))
63    } else {
64        None
65    }
66}
67
68#[allow(clippy::mut_mut)]
69pub(crate) fn until_err<T>(err: &mut &mut Result<()>, item: Result<T>) -> Option<T> {
70    match item {
71        Ok(item) => Some(item),
72        Err(e) => {
73            **err = Err(e);
74            None
75        }
76    }
77}
78
79/// Clean an output string by removing tabs, new lines, carriage returns, and ANSI escape codes.
80#[must_use]
81pub fn clean_output_string(data: &str) -> (String, usize) {
82    let data = data.replace('\t', "   ");
83    let data = data.replace('\n', " ");
84    let data = data.replace('\r', " ");
85    let final_data = String::from_utf8_lossy(&strip(data)).to_string();
86    let data_uw = final_data.width();
87    (final_data, data_uw)
88}
89
90#[cfg(test)]
91pub(crate) trait Mock {
92    fn mock() -> Self;
93}
94
95#[cfg(test)]
96pub(crate) mod test {
97    use std::time::Instant;
98
99    use bytes::Bytes;
100    use tracing::Level;
101    use tracing_subscriber_init::TracingConfig;
102    use unicode_width::UnicodeWidthStr as _;
103
104    use crate::TracingConfigExt;
105
106    use super::{clean_output_string, parse_ts_ping, send_ts_ping, to_path_buf};
107
108    pub(crate) struct TestConfig {
109        verbose: u8,
110        quiet: u8,
111        level: Level,
112        directives: Option<String>,
113    }
114
115    impl TestConfig {
116        pub(crate) fn with_directives() -> Self {
117            Self {
118                verbose: 3,
119                quiet: 0,
120                level: Level::INFO,
121                directives: Some("actix_web=error".to_string()),
122            }
123        }
124    }
125
126    impl Default for TestConfig {
127        fn default() -> Self {
128            Self {
129                verbose: 3,
130                quiet: 0,
131                level: Level::INFO,
132                directives: None,
133            }
134        }
135    }
136
137    impl TracingConfig for TestConfig {
138        fn quiet(&self) -> u8 {
139            self.quiet
140        }
141
142        fn verbose(&self) -> u8 {
143            self.verbose
144        }
145    }
146
147    impl TracingConfigExt for TestConfig {
148        fn level(&self) -> Level {
149            self.level
150        }
151
152        fn enable_stdout(&self) -> bool {
153            false
154        }
155
156        fn directives(&self) -> Option<&String> {
157            self.directives.as_ref()
158        }
159    }
160
161    #[test]
162    fn test_to_path_buf() {
163        let path_str = String::from("/some/test/path");
164        let path_buf = to_path_buf(&path_str).unwrap();
165        assert_eq!(path_buf.to_str().unwrap(), "/some/test/path");
166    }
167
168    #[test]
169    fn test_clean_output_string() {
170        let input = "Hello,\tWorld!\nThis is a test.\r\x1b[31mRed Text\x1b[0m";
171        let (cleaned, width) = clean_output_string(input);
172        assert_eq!(cleaned, "Hello,   World! This is a test. Red Text");
173        assert_eq!(width, cleaned.width()); // Ensure width matches cleaned string
174    }
175
176    #[test]
177    fn test_send_parse_ts_ping() {
178        let origin = Instant::now();
179        let ping = send_ts_ping(origin);
180        let bytes = Bytes::from(ping.to_vec());
181        let duration = parse_ts_ping(&bytes);
182        assert!(duration.is_some());
183    }
184
185    #[test]
186    fn test_parse_ts_ping_invalid() {
187        let bytes = Bytes::from(vec![0u8; 10]); // Invalid length
188        let duration = parse_ts_ping(&bytes);
189        assert!(duration.is_none());
190    }
191}