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