aster_forge_validation/
display.rs1use crate::{Result, ValidationError};
11
12pub fn normalize_bounded_display_text(
18 field_name: &str,
19 value: &str,
20 max_len: usize,
21) -> Result<String> {
22 let normalized = value.trim();
23 if normalized.len() > max_len {
24 return Err(ValidationError::new(format!(
25 "{field_name} exceeds {max_len} characters"
26 )));
27 }
28 if strip_control_chars(normalized) != normalized {
29 return Err(ValidationError::new(format!(
30 "{field_name} cannot contain control characters"
31 )));
32 }
33 Ok(normalized.to_string())
34}
35
36pub fn strip_control_chars(value: &str) -> String {
38 value.chars().filter(|ch| !ch.is_control()).collect()
39}
40
41pub fn display_text_or_default(
47 value: Option<String>,
48 default: &str,
49 field_name: &str,
50 max_len: usize,
51) -> String {
52 value
53 .map(|value| strip_control_chars(&value))
54 .and_then(|value| normalize_bounded_display_text(field_name, &value, max_len).ok())
55 .filter(|value| !value.is_empty())
56 .unwrap_or_else(|| default.to_string())
57}
58
59pub fn normalize_public_asset_url(field_name: &str, value: &str, max_len: usize) -> Result<String> {
67 let normalized = value.trim();
68 if normalized.is_empty() {
69 return Ok(String::new());
70 }
71 if normalized.len() > max_len {
72 return Err(ValidationError::new(format!(
73 "{field_name} exceeds {max_len} characters"
74 )));
75 }
76 if normalized.chars().any(char::is_whitespace) {
77 return Err(ValidationError::new(format!(
78 "{field_name} cannot contain whitespace"
79 )));
80 }
81 if !is_public_asset_url(normalized) {
82 return Err(ValidationError::new(format!(
83 "{field_name} must be an absolute http(s) URL or a root-relative path"
84 )));
85 }
86 Ok(normalized.to_string())
87}
88
89pub fn is_public_asset_url(value: &str) -> bool {
91 value.starts_with('/') || value.starts_with("https://") || value.starts_with("http://")
92}
93
94pub fn public_asset_url_or_default(value: Option<String>, default: &str) -> String {
96 value
97 .map(|value| value.trim().to_string())
98 .filter(|value| !value.is_empty())
99 .filter(|value| is_public_asset_url(value))
100 .unwrap_or_else(|| default.to_string())
101}
102
103#[cfg(test)]
104mod tests {
105 use super::{
106 display_text_or_default, is_public_asset_url, normalize_bounded_display_text,
107 normalize_public_asset_url, public_asset_url_or_default, strip_control_chars,
108 };
109
110 #[test]
111 fn display_text_trims_allows_empty_and_rejects_control_characters() {
112 assert_eq!(
113 normalize_bounded_display_text("title", " Aster ", 20).unwrap(),
114 "Aster"
115 );
116 assert_eq!(
117 normalize_bounded_display_text("title", " ", 20).unwrap(),
118 ""
119 );
120 assert!(normalize_bounded_display_text("title", "abc", 2).is_err());
121 assert!(normalize_bounded_display_text("title", "hello\nworld", 20).is_err());
122 }
123
124 #[test]
125 fn display_text_default_reader_strips_control_characters_before_fallback() {
126 assert_eq!(strip_control_chars("A\u{0000}ster"), "Aster");
127 assert_eq!(
128 display_text_or_default(
129 Some(" Site\u{0000} Name ".to_string()),
130 "Default",
131 "title",
132 20
133 ),
134 "Site Name"
135 );
136 assert_eq!(
137 display_text_or_default(Some(" ".to_string()), "Default", "title", 20),
138 "Default"
139 );
140 }
141
142 #[test]
143 fn public_asset_url_trims_allows_empty_and_rejects_invalid_values() {
144 assert_eq!(
145 normalize_public_asset_url("favicon", " /assets/icon.svg?v=1 ", 2048).unwrap(),
146 "/assets/icon.svg?v=1"
147 );
148 assert_eq!(
149 normalize_public_asset_url("favicon", " ", 2048).unwrap(),
150 ""
151 );
152 assert!(
153 normalize_public_asset_url("favicon", "https://cdn.example.com/icon 1.svg", 2048)
154 .is_err()
155 );
156 assert!(normalize_public_asset_url("favicon", "javascript:alert(1)", 2048).is_err());
157 assert!(normalize_public_asset_url("favicon", "icons/favicon.svg", 2048).is_err());
158 }
159
160 #[test]
161 fn public_asset_default_reader_accepts_same_url_predicate() {
162 assert!(is_public_asset_url("/favicon.svg"));
163 assert!(is_public_asset_url("https://cdn.example.com/favicon.svg"));
164 assert!(is_public_asset_url("http://cdn.example.com/favicon.svg"));
165 assert!(!is_public_asset_url("favicon.svg"));
166 assert_eq!(
167 public_asset_url_or_default(Some("/custom.svg".to_string()), "/favicon.svg"),
168 "/custom.svg"
169 );
170 assert_eq!(
171 public_asset_url_or_default(Some("bad url".to_string()), "/favicon.svg"),
172 "/favicon.svg"
173 );
174 }
175}