better_config_core/utils/
misc.rs

1/// Common utilities for configuration handling
2use crate::error::Error;
3use std::path::Path;
4
5/// Validate and split a comma-separated list of file paths
6///
7/// # Arguments
8///
9/// * `paths_str` - A comma-separated list of file paths
10///
11/// # Returns
12///
13/// A vector of validated file paths
14///
15/// # Errors
16///
17/// Returns an error if the input is empty, contains invalid paths, or exceeds the maximum path length
18pub fn validate_and_split_paths(paths_str: &str) -> Result<Vec<String>, Error> {
19    if paths_str.trim().is_empty() {
20        return Err(Error::invalid_path(paths_str, "path cannot be empty"));
21    }
22
23    let file_paths: Vec<String> = paths_str
24        .split(',')
25        .map(|s| s.trim().to_string())
26        .filter(|s| !s.is_empty())
27        .collect();
28
29    if file_paths.is_empty() {
30        return Err(Error::invalid_path(paths_str, "no valid file paths found"));
31    }
32
33    // Basic path validation
34    for path in &file_paths {
35        if path.contains("..") {
36            return Err(Error::invalid_path(path, "path traversal not allowed"));
37        }
38        if path.len() > 260 {
39            return Err(Error::invalid_path(path, "path too long"));
40        }
41        // Check for invalid characters
42        if path
43            .chars()
44            .any(|c| matches!(c, '<' | '>' | ':' | '"' | '|' | '?' | '*'))
45        {
46            return Err(Error::invalid_path(path, "invalid characters in path"));
47        }
48    }
49
50    Ok(file_paths)
51}
52
53/// Check if a file exists and is readable
54///
55/// # Arguments
56///
57/// * `path` - The path to the file
58///
59/// # Returns
60///
61/// Ok(()) if the file exists and is readable, otherwise an error
62///
63/// # Errors
64///
65/// Returns an error if the file does not exist, is not a file, or cannot be read
66pub fn check_file_accessibility(path: &str) -> Result<(), Error> {
67    let path_obj = Path::new(path);
68    if !path_obj.exists() {
69        return Err(Error::file_not_found(path));
70    }
71    if !path_obj.is_file() {
72        return Err(Error::invalid_path(path, "path is not a file"));
73    }
74    Ok(())
75}
76
77/// Safely convert a string to a number without panicking
78///
79/// # Arguments
80///
81/// * `s` - The string to convert
82/// * `key` - The key associated with the value (for error messages)
83/// * `type_name` - The name of the type being converted to (for error messages)
84///
85/// # Returns
86///
87/// A result containing the parsed number or an error
88///
89/// # Errors
90///
91/// Returns an error if the string cannot be parsed as the specified number type
92pub fn safe_string_to_number<T: std::str::FromStr>(
93    s: &str,
94    key: &str,
95    type_name: &str,
96) -> Result<T, Error> {
97    s.parse()
98        .map_err(|_| Error::value_conversion_error(key, type_name, s))
99}
100
101#[cfg(test)]
102mod tests {
103    use crate::error::Error;
104    use crate::misc;
105
106    #[test]
107    fn test_validate_and_split_paths_valid() {
108        let result = misc::validate_and_split_paths("file1.json,file2.json");
109        assert!(result.is_ok());
110        assert_eq!(result.unwrap(), vec!["file1.json", "file2.json"]);
111    }
112
113    #[test]
114    fn test_validate_and_split_paths_empty() {
115        let result = misc::validate_and_split_paths("");
116        assert!(result.is_err());
117        match result.unwrap_err() {
118            Error::InvalidPathError { .. } => {}
119            _ => panic!("Expected InvalidPathError"),
120        }
121    }
122
123    #[test]
124    fn test_validate_and_split_paths_path_traversal() {
125        let result = misc::validate_and_split_paths("../config.json");
126        assert!(result.is_err());
127        match result.unwrap_err() {
128            Error::InvalidPathError { .. } => {}
129            _ => panic!("Expected InvalidPathError"),
130        }
131    }
132
133    #[test]
134    fn test_validate_and_split_paths_invalid_chars() {
135        let result = misc::validate_and_split_paths("config<test>.json");
136        assert!(result.is_err());
137        match result.unwrap_err() {
138            Error::InvalidPathError { .. } => {}
139            _ => panic!("Expected InvalidPathError"),
140        }
141    }
142
143    #[test]
144    fn test_check_file_accessibility_nonexistent() {
145        let result = misc::check_file_accessibility("nonexistent.json");
146        assert!(result.is_err());
147        match result.unwrap_err() {
148            Error::LoadFileError { .. } => {}
149            _ => panic!("Expected LoadFileError"),
150        }
151    }
152}