track_core/
path_component.rs1use std::path::{Component, Path};
2
3use crate::errors::{ErrorCode, TrackError};
4
5pub 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}