rskit_validation/
input.rs1use rskit_errors::{AppError, AppResult};
4
5pub fn validate_required_trimmed(field: &str, value: &str) -> AppResult<()> {
7 reject_unicode_controls(field, value)?;
8 if value.trim().is_empty() {
9 return Err(AppError::invalid_input(field, "is required"));
10 }
11 if value != value.trim() {
12 return Err(AppError::invalid_input(
13 field,
14 "cannot contain leading or trailing whitespace",
15 ));
16 }
17 Ok(())
18}
19
20pub fn validate_path_safe_identifier(field: &str, value: &str) -> AppResult<()> {
22 validate_required_trimmed(field, value)?;
23 if value.contains(['/', '\\', ':']) || value == "." || value == ".." {
24 return Err(AppError::invalid_input(
25 field,
26 "cannot contain path separators or traversal markers",
27 ));
28 }
29 Ok(())
30}
31
32pub fn validate_optional_trimmed(field: &str, value: Option<String>) -> AppResult<Option<String>> {
34 value
35 .map(|value| {
36 validate_required_trimmed(field, &value)?;
37 Ok(value)
38 })
39 .transpose()
40}
41
42pub fn validate_safe_path(path: &str) -> AppResult<()> {
44 reject_unicode_controls("path", path)?;
45 if path.is_empty()
46 || path.starts_with('/')
47 || path.starts_with('\\')
48 || path.split(['/', '\\']).any(|segment| {
49 segment.is_empty() || segment == "." || segment == ".." || segment.contains(':')
50 })
51 {
52 return Err(AppError::invalid_input(
53 "path",
54 "path must be relative and must not contain traversal segments",
55 ));
56 }
57 Ok(())
58}
59
60pub fn reject_unicode_controls(field: &str, value: &str) -> AppResult<()> {
62 for ch in value.chars() {
63 if ch.is_control()
64 || matches!(
65 ch,
66 '\u{202A}'..='\u{202E}' | '\u{2066}'..='\u{2069}' | '\u{200E}' | '\u{200F}'
67 )
68 {
69 return Err(AppError::invalid_input(
70 field,
71 "input contains forbidden Unicode control characters",
72 ));
73 }
74 }
75 Ok(())
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 #[test]
83 fn path_validation_rejects_traversal_and_mixed_separators() {
84 assert!(validate_safe_path("tenant/report.json").is_ok());
85 assert!(validate_safe_path("../secret").is_err());
86 assert!(validate_safe_path("tenant\\..\\secret").is_err());
87 assert!(validate_safe_path("tenant/..\\secret").is_err());
88 }
89
90 #[test]
91 fn unicode_validation_rejects_control_characters() {
92 assert!(reject_unicode_controls("identifier", "safe-id").is_ok());
93 assert!(reject_unicode_controls("identifier", "safe\u{202e}txt").is_err());
94 assert!(reject_unicode_controls("identifier", "павел").is_ok());
95 }
96
97 #[test]
98 fn required_trimmed_rejects_whitespace_only_and_untrimmed_values() {
99 assert!(validate_required_trimmed("name", "value").is_ok());
100 assert!(validate_required_trimmed("name", " ").is_err());
101 assert!(validate_required_trimmed("name", "\t").is_err());
102 assert!(validate_required_trimmed("name", " value").is_err());
103 assert!(validate_required_trimmed("name", "value ").is_err());
104 }
105
106 #[test]
107 fn optional_trimmed_accepts_absent_and_rejects_invalid_present_values() {
108 assert_eq!(validate_optional_trimmed("name", None).unwrap(), None);
109 assert_eq!(
110 validate_optional_trimmed("name", Some("value".to_string())).unwrap(),
111 Some("value".to_string())
112 );
113 assert!(validate_optional_trimmed("name", Some(" value".to_string())).is_err());
114 assert!(validate_optional_trimmed("name", Some("\n".to_string())).is_err());
115 }
116
117 #[test]
118 fn path_safe_identifier_rejects_controls_traversal_and_separators() {
119 assert!(validate_path_safe_identifier("id", "tenant_01").is_ok());
120 assert!(validate_path_safe_identifier("id", ".").is_err());
121 assert!(validate_path_safe_identifier("id", "..").is_err());
122 assert!(validate_path_safe_identifier("id", "tenant/name").is_err());
123 assert!(validate_path_safe_identifier("id", "tenant\\name").is_err());
124 assert!(validate_path_safe_identifier("id", "tenant:name").is_err());
125 assert!(validate_path_safe_identifier("id", "tenant\u{202e}name").is_err());
126 }
127
128 #[test]
129 fn safe_path_rejects_empty_absolute_controls_traversal_and_separators() {
130 assert!(validate_safe_path("tenant/report.json").is_ok());
131 assert!(validate_safe_path("").is_err());
132 assert!(validate_safe_path("/tenant/report.json").is_err());
133 assert!(validate_safe_path("\\tenant\\report.json").is_err());
134 assert!(validate_safe_path("./tenant").is_err());
135 assert!(validate_safe_path("tenant//report.json").is_err());
136 assert!(validate_safe_path("tenant/report:name").is_err());
137 assert!(validate_safe_path("tenant/re\u{202e}port").is_err());
138 }
139}