Skip to main content

app_json_settings/core/
validation.rs

1use crate::core::error::{ConfigError, Result};
2
3/// Windows reserved device names. Windows treats these as device names in
4/// any directory, at any case, and for the stem of any file name -- so
5/// `NUL.txt` refers to the null device just as `NUL` does.
6const RESERVED_DEVICE_NAMES: [&str; 22] = [
7    "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8",
8    "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
9];
10
11/// Returns `true` when `value`'s stem (the portion before the first `.`)
12/// case-insensitively matches a Windows reserved device name.
13///
14/// Checked on every platform, not just Windows, so that the same value is
15/// either accepted or rejected regardless of host OS -- see
16/// [`is_safe_path_component`]'s documentation for why that consistency
17/// matters here.
18fn is_reserved_device_name(value: &str) -> bool {
19    let stem = value.split('.').next().unwrap_or(value);
20    RESERVED_DEVICE_NAMES
21        .iter()
22        .any(|reserved| stem.eq_ignore_ascii_case(reserved))
23}
24
25/// Returns `true` when `value` is safe to use as a single file name.
26///
27/// This intentionally rejects path separators and drive separators regardless
28/// of the host OS so tests behave consistently on Windows, macOS, and Unix.
29pub fn is_plain_file_name(value: &str) -> bool {
30    is_safe_path_component(value)
31}
32
33/// Returns `true` when `value` is safe to append as one path component.
34///
35/// This also rejects Windows reserved device names (`CON`, `PRN`, `AUX`,
36/// `NUL`, `COM1`-`COM9`, `LPT1`-`LPT9`, case-insensitively, including with
37/// an extension such as `NUL.txt`) on every platform, not just Windows --
38/// consistent with this function's existing OS-independent behavior. On
39/// Windows these names refer to devices rather than files in any directory,
40/// so a value that passes here but is later used as a file name would fail,
41/// or silently target a device, only there.
42pub fn is_safe_path_component(value: &str) -> bool {
43    !value.is_empty()
44        && value != "."
45        && value != ".."
46        && !value.contains('/')
47        && !value.contains('\\')
48        && !value.contains(':')
49        && !value.chars().any(char::is_control)
50        && !is_reserved_device_name(value)
51}
52
53pub fn validate_plain_file_name(value: &str) -> Result<&str> {
54    if is_plain_file_name(value) {
55        Ok(value)
56    } else {
57        Err(ConfigError::InvalidPathComponent(value.to_string()))
58    }
59}
60
61pub fn validate_path_component(value: &str) -> Result<&str> {
62    if is_safe_path_component(value) {
63        Ok(value)
64    } else {
65        Err(ConfigError::InvalidPathComponent(value.to_string()))
66    }
67}
68
69#[cfg(test)]
70mod tests;