Skip to main content

rlg/
utils.rs

1// utils.rs
2// Copyright © 2024-2026 RustLogs (RLG). All rights reserved.
3// SPDX-License-Identifier: Apache-2.0
4// SPDX-License-Identifier: MIT
5
6use crate::error::RlgResult;
7use dtt::datetime::DateTime;
8
9#[cfg(feature = "tokio")]
10use std::path::Path;
11
12#[cfg(feature = "tokio")]
13use tokio::fs::{self, File, OpenOptions};
14#[cfg(feature = "tokio")]
15use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
16
17/// Generates a timestamp string in ISO 8601 format.
18///
19/// # Returns
20///
21/// A `String` containing the current timestamp in ISO 8601 format.
22///
23/// # Examples
24///
25/// ```rust,no_run
26/// use rlg::utils::generate_timestamp;
27///
28/// let timestamp = generate_timestamp();
29/// println!("Current timestamp: {}", timestamp);
30/// ```
31#[must_use]
32pub fn generate_timestamp() -> String {
33    DateTime::new().to_string()
34}
35
36/// Sanitizes a string for use in log messages.
37///
38/// This function replaces newlines and control characters with spaces.
39///
40/// # Arguments
41///
42/// * `message` - A string slice that holds the message to be sanitized.
43///
44/// # Returns
45///
46/// A `String` with sanitized content.
47///
48/// # Examples
49///
50/// ```
51/// use rlg::utils::sanitize_log_message;
52///
53/// let message = "Hello\nWorld\r\u{0007}";
54/// let sanitized = sanitize_log_message(message);
55/// assert_eq!(sanitized, "Hello World  ");
56/// ```
57#[must_use]
58pub fn sanitize_log_message(message: &str) -> String {
59    message
60        .replace(['\n', '\r'], " ")
61        .replace(|c: char| c.is_control(), " ")
62}
63
64/// Checks if a file exists and is writable.
65///
66/// # Arguments
67///
68/// * `path` - A reference to a `Path` that holds the file path to check.
69///
70/// # Returns
71///
72/// A `RlgResult<bool>` which is `Ok(true)` if the file exists and is writable,
73/// `Ok(false)` otherwise, or an error if the operation fails.
74///
75/// # Errors
76///
77/// This function returns an error if the file metadata cannot be read.
78///
79/// # Examples
80///
81/// ```no_run
82/// use rlg::utils::is_file_writable;
83/// use std::path::Path;
84///
85/// #[tokio::main]
86/// async fn main() -> rlg::error::RlgResult<()> {
87///     let path = Path::new("example.log");
88///     let is_writable = is_file_writable(&path).await?;
89///     println!("Is file writable: {}", is_writable);
90///     Ok(())
91/// }
92/// ```
93#[cfg(feature = "tokio")]
94pub async fn is_file_writable(path: &Path) -> RlgResult<bool> {
95    if path.exists() {
96        let metadata = fs::metadata(path).await?;
97        Ok(metadata.is_file() && !metadata.permissions().readonly())
98    } else {
99        // If the file doesn't exist, check if we can create it
100        match File::create(path).await {
101            Ok(_) => {
102                fs::remove_file(path).await?;
103                Ok(true)
104            }
105            Err(_) => Ok(false),
106        }
107    }
108}
109
110/// Truncates the file at the given path to the specified size.
111///
112/// # Arguments
113///
114/// * `path` - A reference to a `Path` that holds the file path to truncate.
115/// * `size` - The size (in bytes) to truncate the file to.
116///
117/// # Returns
118///
119/// A `std::io::Result<()>` which is `Ok(())` if the operation succeeds,
120/// or an error if it fails.
121///
122/// # Errors
123///
124/// This function returns an error if the file cannot be opened, or if
125/// the seek or write operations fail.
126///
127/// # Examples
128///
129/// ```no_run
130/// use rlg::utils::truncate_file;
131/// use std::path::Path;
132///
133/// #[tokio::main]
134/// async fn main() -> std::io::Result<()> {
135///     let path = Path::new("example.log");
136///     truncate_file(&path, 1024).await?;
137///     println!("File truncated successfully");
138///     Ok(())
139/// }
140/// ```
141#[cfg(feature = "tokio")]
142pub async fn truncate_file(
143    path: &Path,
144    size: u64,
145) -> std::io::Result<()> {
146    let mut file = OpenOptions::new()
147        .read(true)
148        .write(true)
149        .create(true)
150        .truncate(false)
151        .open(path)
152        .await?;
153
154    let file_size = file.metadata().await?.len();
155
156    if size < file_size {
157        // Read the content
158        // SAFETY: Casting size to usize is safe here as we're truncating to a size that fits in memory for this operation.
159        #[allow(clippy::cast_possible_truncation)]
160        let mut content = vec![0; size as usize];
161        file.read_exact(&mut content).await?;
162
163        // Seek to the beginning of the file
164        file.seek(std::io::SeekFrom::Start(0)).await?;
165
166        // Write the truncated content
167        file.write_all(&content).await?;
168    }
169
170    // Set the file length
171    file.set_len(size).await?;
172
173    Ok(())
174}
175
176/// Formats a file size in a human-readable format.
177///
178/// # Arguments
179///
180/// * `size` - The file size in bytes.
181///
182/// # Returns
183///
184/// A `String` containing the formatted file size.
185///
186/// # Examples
187///
188/// ```
189/// use rlg::utils::format_file_size;
190///
191/// let size = 1_500_000;
192/// let formatted = format_file_size(size);
193/// assert_eq!(formatted, "1.43 MB");
194/// ```
195#[must_use]
196pub fn format_file_size(size: u64) -> String {
197    const UNITS: [&str; 6] = ["B", "KB", "MB", "GB", "TB", "PB"];
198    // SAFETY: Loss of precision is acceptable for human-readable file size formatting.
199    #[allow(clippy::cast_precision_loss)]
200    let mut size_f = size as f64;
201    let mut unit_index = 0;
202
203    while size_f >= 1024.0 && unit_index < UNITS.len() - 1 {
204        size_f /= 1024.0;
205        unit_index += 1;
206    }
207
208    format!("{size_f:.2} {unit}", unit = UNITS[unit_index])
209}
210
211/// Parses a datetime string in ISO 8601 format.
212///
213/// # Arguments
214///
215/// * `datetime_str` - A string slice containing the datetime in ISO 8601 format.
216///
217/// # Returns
218///
219/// A `RlgResult<DateTime>` which is `Ok(DateTime)` if parsing succeeds,
220/// or an error if parsing fails.
221///
222/// # Errors
223///
224/// This function returns an error if the datetime string cannot be parsed.
225///
226/// # Examples
227///
228/// ```rust,no_run
229/// use rlg::utils::parse_datetime;
230///
231/// let datetime_str = "2024-08-29T12:00:00Z";
232/// match parse_datetime(datetime_str) {
233///     Ok(dt) => println!("Parsed datetime: {}", dt),
234///     Err(e) => eprintln!("Failed to parse datetime: {}", e),
235/// }
236/// ```
237pub fn parse_datetime(datetime_str: &str) -> RlgResult<DateTime> {
238    DateTime::parse(datetime_str)
239        .map_err(|e| crate::error::RlgError::custom(e.to_string()))
240}
241
242/// Generates a highly unique, 16-character pseudo-random hex string suitable for OTLP span IDs.
243///
244/// # Returns
245/// A `String` containing the span ID.
246#[must_use]
247pub fn generate_span_id() -> String {
248    crate::commons::id::generate_random_hex()[..16].to_string()
249}
250
251/// Generates a highly unique, 32-character pseudo-random hex string suitable for OTLP trace IDs.
252///
253/// # Returns
254/// A `String` containing the trace ID.
255#[must_use]
256pub fn generate_trace_id() -> String {
257    crate::commons::id::generate_random_hex()
258}
259
260/// Checks if a directory is writable.
261///
262/// # Arguments
263///
264/// * `path` - A reference to a `Path` that holds the directory path to check.
265///
266/// # Returns
267///
268/// A `RlgResult<bool>` which is `Ok(true)` if the directory is writable,
269/// `Ok(false)` otherwise, or an error if the operation fails.
270///
271/// # Errors
272///
273/// This function returns an error if the temporary file used for testing writability cannot be removed.
274///
275/// # Examples
276///
277/// ```no_run
278/// use rlg::utils::is_directory_writable;
279/// use std::path::Path;
280///
281/// #[tokio::main]
282/// async fn main() -> rlg::error::RlgResult<()> {
283///     let path = Path::new(".");
284///     let is_writable = is_directory_writable(&path).await?;
285///     println!("Is directory writable: {}", is_writable);
286///     Ok(())
287/// }
288/// ```
289#[cfg(feature = "tokio")]
290pub async fn is_directory_writable(path: &Path) -> RlgResult<bool> {
291    if !path.is_dir() {
292        return Ok(false);
293    }
294
295    let test_file = path.join(".rlg_write_test");
296    match File::create(&test_file).await {
297        Ok(_) => {
298            fs::remove_file(&test_file).await?;
299            Ok(true)
300        }
301        Err(_) => Ok(false),
302    }
303}