Skip to main content

reinhardt_forms/fields/
regex_field.rs

1use crate::field::{FieldError, FieldResult, FormField, Widget};
2use regex::Regex;
3use std::net::Ipv6Addr;
4use std::sync::OnceLock;
5
6/// RegexField for pattern-based validation
7///
8/// Compiled regex is cached using `OnceLock` to avoid repeated
9/// compilation which could lead to ReDoS via allocation overhead.
10pub struct RegexField {
11	/// The field name used as the form data key.
12	pub name: String,
13	/// Optional human-readable label for display.
14	pub label: Option<String>,
15	/// Whether this field must be filled in.
16	pub required: bool,
17	/// Optional help text displayed alongside the field.
18	pub help_text: Option<String>,
19	/// The widget type used for rendering this field.
20	pub widget: Widget,
21	/// Optional initial (default) value for the field.
22	pub initial: Option<serde_json::Value>,
23	/// Cached compiled regex to prevent repeated compilation (ReDoS mitigation)
24	regex_cache: OnceLock<Regex>,
25	/// Raw pattern string stored for lazy compilation
26	pattern: String,
27	/// The error message shown when the regex does not match.
28	pub error_message: String,
29	/// Maximum allowed character count.
30	pub max_length: Option<usize>,
31	/// Minimum required character count.
32	pub min_length: Option<usize>,
33}
34
35impl RegexField {
36	/// Create a new RegexField
37	///
38	/// The regex is compiled lazily on first use and cached for subsequent calls.
39	///
40	/// # Examples
41	///
42	/// ```
43	/// use reinhardt_forms::fields::RegexField;
44	///
45	/// let field = RegexField::new("pattern".to_string(), r"^\d+$").unwrap();
46	/// assert_eq!(field.name, "pattern");
47	/// ```
48	pub fn new(name: String, pattern: &str) -> Result<Self, regex::Error> {
49		// Validate the pattern eagerly so callers get errors at construction time
50		let compiled = Regex::new(pattern)?;
51		let cache = OnceLock::new();
52		let _ = cache.set(compiled);
53		Ok(Self {
54			name,
55			label: None,
56			required: true,
57			help_text: None,
58			widget: Widget::TextInput,
59			initial: None,
60			regex_cache: cache,
61			pattern: pattern.to_string(),
62			error_message: "Enter a valid value".to_string(),
63			max_length: None,
64			min_length: None,
65		})
66	}
67
68	/// Get the cached compiled regex
69	fn regex(&self) -> &Regex {
70		self.regex_cache.get_or_init(|| {
71			Regex::new(&self.pattern).expect("Pattern was validated at construction")
72		})
73	}
74	/// Overrides the default error message for validation failures.
75	pub fn with_error_message(mut self, message: String) -> Self {
76		self.error_message = message;
77		self
78	}
79}
80
81impl std::fmt::Debug for RegexField {
82	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83		f.debug_struct("RegexField")
84			.field("name", &self.name)
85			.field("label", &self.label)
86			.field("required", &self.required)
87			.field("help_text", &self.help_text)
88			.field("widget", &self.widget)
89			.field("initial", &self.initial)
90			.field("pattern", &self.pattern)
91			.field("error_message", &self.error_message)
92			.field("max_length", &self.max_length)
93			.field("min_length", &self.min_length)
94			.finish()
95	}
96}
97
98impl Clone for RegexField {
99	fn clone(&self) -> Self {
100		let cache = OnceLock::new();
101		if let Some(regex) = self.regex_cache.get() {
102			let _ = cache.set(regex.clone());
103		}
104		Self {
105			name: self.name.clone(),
106			label: self.label.clone(),
107			required: self.required,
108			help_text: self.help_text.clone(),
109			widget: self.widget.clone(),
110			initial: self.initial.clone(),
111			regex_cache: cache,
112			pattern: self.pattern.clone(),
113			error_message: self.error_message.clone(),
114			max_length: self.max_length,
115			min_length: self.min_length,
116		}
117	}
118}
119
120impl FormField for RegexField {
121	fn name(&self) -> &str {
122		&self.name
123	}
124
125	fn label(&self) -> Option<&str> {
126		self.label.as_deref()
127	}
128
129	fn required(&self) -> bool {
130		self.required
131	}
132
133	fn help_text(&self) -> Option<&str> {
134		self.help_text.as_deref()
135	}
136
137	fn widget(&self) -> &Widget {
138		&self.widget
139	}
140
141	fn initial(&self) -> Option<&serde_json::Value> {
142		self.initial.as_ref()
143	}
144
145	fn clean(&self, value: Option<&serde_json::Value>) -> FieldResult<serde_json::Value> {
146		match value {
147			None if self.required => Err(FieldError::required(None)),
148			None => Ok(serde_json::Value::Null),
149			Some(v) => {
150				let s = v
151					.as_str()
152					.ok_or_else(|| FieldError::invalid(None, "Expected string"))?;
153
154				if s.is_empty() {
155					if self.required {
156						return Err(FieldError::required(None));
157					}
158					return Ok(serde_json::Value::Null);
159				}
160
161				// Length validation using character count (not byte count)
162				// for correct multi-byte character handling
163				let char_count = s.chars().count();
164				if let Some(max) = self.max_length
165					&& char_count > max
166				{
167					return Err(FieldError::validation(
168						None,
169						&format!("Ensure this value has at most {} characters", max),
170					));
171				}
172
173				if let Some(min) = self.min_length
174					&& char_count < min
175				{
176					return Err(FieldError::validation(
177						None,
178						&format!("Ensure this value has at least {} characters", min),
179					));
180				}
181
182				// Regex validation (uses cached compiled regex)
183				if !self.regex().is_match(s) {
184					return Err(FieldError::validation(None, &self.error_message));
185				}
186
187				Ok(serde_json::Value::String(s.to_string()))
188			}
189		}
190	}
191}
192
193/// SlugField for URL-safe slugs
194#[derive(Debug, Clone)]
195pub struct SlugField {
196	/// The field name used as the form data key.
197	pub name: String,
198	/// Optional human-readable label for display.
199	pub label: Option<String>,
200	/// Whether this field must be filled in.
201	pub required: bool,
202	/// Optional help text displayed alongside the field.
203	pub help_text: Option<String>,
204	/// The widget type used for rendering this field.
205	pub widget: Widget,
206	/// Optional initial (default) value for the field.
207	pub initial: Option<serde_json::Value>,
208	/// Maximum allowed character count (defaults to 50).
209	pub max_length: Option<usize>,
210	/// Whether to allow Unicode characters in the slug.
211	pub allow_unicode: bool,
212}
213
214impl SlugField {
215	/// Creates a new `SlugField` with a default max length of 50.
216	pub fn new(name: String) -> Self {
217		Self {
218			name,
219			label: None,
220			required: true,
221			help_text: None,
222			widget: Widget::TextInput,
223			initial: None,
224			max_length: Some(50),
225			allow_unicode: false,
226		}
227	}
228
229	fn is_valid_slug(&self, s: &str) -> bool {
230		if self.allow_unicode {
231			s.chars().all(|c| {
232				c.is_alphanumeric() || c == '-' || c == '_' || (!c.is_ascii() && c.is_alphabetic())
233			})
234		} else {
235			s.chars()
236				.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
237		}
238	}
239}
240
241impl FormField for SlugField {
242	fn name(&self) -> &str {
243		&self.name
244	}
245
246	fn label(&self) -> Option<&str> {
247		self.label.as_deref()
248	}
249
250	fn required(&self) -> bool {
251		self.required
252	}
253
254	fn help_text(&self) -> Option<&str> {
255		self.help_text.as_deref()
256	}
257
258	fn widget(&self) -> &Widget {
259		&self.widget
260	}
261
262	fn initial(&self) -> Option<&serde_json::Value> {
263		self.initial.as_ref()
264	}
265
266	fn clean(&self, value: Option<&serde_json::Value>) -> FieldResult<serde_json::Value> {
267		match value {
268			None if self.required => Err(FieldError::required(None)),
269			None => Ok(serde_json::Value::Null),
270			Some(v) => {
271				let s = v
272					.as_str()
273					.ok_or_else(|| FieldError::invalid(None, "Expected string"))?;
274
275				if s.is_empty() {
276					if self.required {
277						return Err(FieldError::required(None));
278					}
279					return Ok(serde_json::Value::Null);
280				}
281
282				// Use character count for correct multi-byte handling
283				if let Some(max) = self.max_length
284					&& s.chars().count() > max
285				{
286					return Err(FieldError::validation(
287						None,
288						&format!("Ensure this value has at most {} characters", max),
289					));
290				}
291
292				if !self.is_valid_slug(s) {
293					let msg = if self.allow_unicode {
294						"Enter a valid slug consisting of Unicode letters, numbers, underscores, or hyphens"
295					} else {
296						"Enter a valid slug consisting of letters, numbers, underscores or hyphens"
297					};
298					return Err(FieldError::validation(None, msg));
299				}
300
301				Ok(serde_json::Value::String(s.to_string()))
302			}
303		}
304	}
305}
306
307/// GenericIPAddressField for IPv4 and IPv6 addresses
308#[derive(Debug, Clone)]
309pub struct GenericIPAddressField {
310	/// The field name used as the form data key.
311	pub name: String,
312	/// Optional human-readable label for display.
313	pub label: Option<String>,
314	/// Whether this field must be filled in.
315	pub required: bool,
316	/// Optional help text displayed alongside the field.
317	pub help_text: Option<String>,
318	/// The widget type used for rendering this field.
319	pub widget: Widget,
320	/// Optional initial (default) value for the field.
321	pub initial: Option<serde_json::Value>,
322	/// Which IP protocol versions to accept.
323	pub protocol: IPProtocol,
324}
325
326/// Specifies which IP address protocol versions are accepted.
327#[derive(Debug, Clone, Copy)]
328pub enum IPProtocol {
329	/// Accept both IPv4 and IPv6 addresses.
330	Both,
331	/// Accept only IPv4 addresses.
332	IPv4,
333	/// Accept only IPv6 addresses.
334	IPv6,
335}
336
337impl GenericIPAddressField {
338	/// Creates a new `GenericIPAddressField` that accepts both IPv4 and IPv6.
339	pub fn new(name: String) -> Self {
340		Self {
341			name,
342			label: None,
343			required: true,
344			help_text: None,
345			widget: Widget::TextInput,
346			initial: None,
347			protocol: IPProtocol::Both,
348		}
349	}
350
351	fn is_valid_ipv4(&self, s: &str) -> bool {
352		let parts: Vec<&str> = s.split('.').collect();
353		if parts.len() != 4 {
354			return false;
355		}
356
357		parts.iter().all(|part| {
358			part.parse::<u8>()
359				.map(|_| !part.starts_with('0') || part.len() == 1)
360				.unwrap_or(false)
361		})
362	}
363
364	fn is_valid_ipv6(&self, s: &str) -> bool {
365		// Use std::net::Ipv6Addr for comprehensive IPv6 validation,
366		// covering compressed (::1), IPv4-mapped (::ffff:192.0.2.1),
367		// and all other valid IPv6 address formats.
368		s.parse::<Ipv6Addr>().is_ok()
369	}
370}
371
372impl FormField for GenericIPAddressField {
373	fn name(&self) -> &str {
374		&self.name
375	}
376
377	fn label(&self) -> Option<&str> {
378		self.label.as_deref()
379	}
380
381	fn required(&self) -> bool {
382		self.required
383	}
384
385	fn help_text(&self) -> Option<&str> {
386		self.help_text.as_deref()
387	}
388
389	fn widget(&self) -> &Widget {
390		&self.widget
391	}
392
393	fn initial(&self) -> Option<&serde_json::Value> {
394		self.initial.as_ref()
395	}
396
397	fn clean(&self, value: Option<&serde_json::Value>) -> FieldResult<serde_json::Value> {
398		match value {
399			None if self.required => Err(FieldError::required(None)),
400			None => Ok(serde_json::Value::Null),
401			Some(v) => {
402				let s = v
403					.as_str()
404					.ok_or_else(|| FieldError::invalid(None, "Expected string"))?;
405
406				if s.is_empty() {
407					if self.required {
408						return Err(FieldError::required(None));
409					}
410					return Ok(serde_json::Value::Null);
411				}
412
413				let is_valid = match self.protocol {
414					IPProtocol::IPv4 => self.is_valid_ipv4(s),
415					IPProtocol::IPv6 => self.is_valid_ipv6(s),
416					IPProtocol::Both => self.is_valid_ipv4(s) || self.is_valid_ipv6(s),
417				};
418
419				if !is_valid {
420					return Err(FieldError::validation(None, "Enter a valid IP address"));
421				}
422
423				Ok(serde_json::Value::String(s.to_string()))
424			}
425		}
426	}
427}
428
429#[cfg(test)]
430mod tests {
431	use super::*;
432	use rstest::rstest;
433
434	#[test]
435	fn test_regex_field() {
436		let field = RegexField::new("code".to_string(), r"^[A-Z]{3}\d{3}$").unwrap();
437
438		assert!(field.clean(Some(&serde_json::json!("ABC123"))).is_ok());
439		assert!(matches!(
440			field.clean(Some(&serde_json::json!("abc123"))),
441			Err(FieldError::Validation(_))
442		));
443	}
444
445	#[test]
446	fn test_forms_regex_field_slug() {
447		let field = SlugField::new("slug".to_string());
448
449		assert!(field.clean(Some(&serde_json::json!("my-slug"))).is_ok());
450		assert!(field.clean(Some(&serde_json::json!("my_slug"))).is_ok());
451		assert!(matches!(
452			field.clean(Some(&serde_json::json!("my slug"))),
453			Err(FieldError::Validation(_))
454		));
455	}
456
457	#[test]
458	fn test_ip_field_ipv4() {
459		let mut field = GenericIPAddressField::new("ip".to_string());
460		field.protocol = IPProtocol::IPv4;
461
462		assert!(field.clean(Some(&serde_json::json!("192.168.1.1"))).is_ok());
463		assert!(matches!(
464			field.clean(Some(&serde_json::json!("999.999.999.999"))),
465			Err(FieldError::Validation(_))
466		));
467	}
468
469	#[test]
470	fn test_ip_field_ipv6() {
471		let mut field = GenericIPAddressField::new("ip".to_string());
472		field.protocol = IPProtocol::IPv6;
473
474		assert!(
475			field
476				.clean(Some(&serde_json::json!(
477					"2001:0db8:85a3:0000:0000:8a2e:0370:7334"
478				)))
479				.is_ok()
480		);
481		assert!(field.clean(Some(&serde_json::json!("::1"))).is_ok());
482	}
483
484	#[rstest]
485	#[case("::1", true)]
486	#[case("::", true)]
487	#[case("::ffff:192.0.2.1", true)]
488	#[case("2001:db8::1", true)]
489	#[case("fe80::1%eth0", false)]
490	#[case("2001:db8:85a3::8a2e:370:7334", true)]
491	#[case("::ffff:10.0.0.1", true)]
492	#[case("2001:db8::", true)]
493	#[case("::192.168.1.1", true)]
494	#[case("not-an-ip", false)]
495	#[case("2001:db8::g1", false)]
496	#[case("12345::1", false)]
497	fn test_ipv6_comprehensive_validation(#[case] input: &str, #[case] should_accept: bool) {
498		// Arrange
499		let mut field = GenericIPAddressField::new("ip".to_string());
500		field.protocol = IPProtocol::IPv6;
501
502		// Act
503		let result = field.clean(Some(&serde_json::json!(input)));
504
505		// Assert
506		if should_accept {
507			assert!(
508				result.is_ok(),
509				"Expected valid IPv6 '{}' to be accepted, got: {:?}",
510				input,
511				result,
512			);
513		} else {
514			assert!(
515				result.is_err(),
516				"Expected invalid IPv6 '{}' to be rejected, got: {:?}",
517				input,
518				result,
519			);
520		}
521	}
522}