Skip to main content

track_core/
path_component.rs

1use std::path::{Component, Path};
2
3use crate::errors::{ErrorCode, TrackError};
4
5// =============================================================================
6// Storage Path Component Validation
7// =============================================================================
8//
9// Several domain identifiers eventually become directories or filenames under
10// the track data root. Validating them in one shared helper keeps every caller
11// on the same safety contract instead of relying on each HTTP or CLI entrypoint
12// to remember its own path traversal checks.
13pub fn validate_single_normal_path_component(
14    value: &str,
15    field_name: &str,
16    error_code: ErrorCode,
17) -> Result<String, TrackError> {
18    let trimmed = value.trim();
19
20    if trimmed.is_empty() || trimmed.contains('/') || trimmed.contains('\\') {
21        return Err(invalid_path_component(field_name, error_code));
22    }
23
24    let mut components = Path::new(trimmed).components();
25    match (components.next(), components.next()) {
26        (Some(Component::Normal(_)), None) => Ok(trimmed.to_owned()),
27        _ => Err(invalid_path_component(field_name, error_code)),
28    }
29}
30
31fn invalid_path_component(field_name: &str, error_code: ErrorCode) -> TrackError {
32    TrackError::new(
33        error_code,
34        format!(
35            "{field_name} must be one non-empty path component without separators or `.` / `..`."
36        ),
37    )
38}
39
40#[cfg(test)]
41mod tests {
42    use crate::errors::ErrorCode;
43
44    use super::validate_single_normal_path_component;
45
46    #[test]
47    fn accepts_a_single_normal_component() {
48        let validated = validate_single_normal_path_component(
49            " project-x ",
50            "Task project",
51            ErrorCode::InvalidPathComponent,
52        )
53        .expect("single normal components should validate");
54
55        assert_eq!(validated, "project-x");
56    }
57
58    #[test]
59    fn rejects_values_that_are_not_single_normal_components() {
60        for invalid in ["", ".", "..", "project/x", "project\\x", "project-x/"] {
61            let error = validate_single_normal_path_component(
62                invalid,
63                "Task project",
64                ErrorCode::InvalidPathComponent,
65            )
66            .expect_err("invalid path components should be rejected");
67
68            assert_eq!(error.code, ErrorCode::InvalidPathComponent);
69        }
70    }
71}