Skip to main content

cloudillo_types/
validation.rs

1// SPDX-FileCopyrightText: Szilárd Hajba
2// SPDX-License-Identifier: LGPL-3.0-or-later
3
4//! Shared low-level format validators.
5//!
6//! These live in `cloudillo-types` so that both the high-level Action DSL
7//! (`cloudillo-action`) and the low-level federation request client
8//! (`cloudillo-core`) can use the exact same definition without creating a
9//! dependency cycle between those crates.
10
11use regex::Regex;
12use std::sync::LazyLock;
13
14/// Regex for idTag format.
15///
16/// `^[a-z0-9-][a-z0-9.-]{3,60}[a-z0-9-]$` — lowercase letters, digits, `.` and
17/// `-`, between 5 and 62 characters, not starting/ending with `.`. This forbids
18/// `/`, `@`, `:`, whitespace and uppercase, which is what makes it safe to
19/// interpolate an idTag into a `https://cl-o.{id_tag}/...` request URL (no path,
20/// userinfo or port smuggling).
21static ID_TAG_RE: LazyLock<Regex> = LazyLock::new(|| {
22	Regex::new(r"^[a-z0-9-][a-z0-9.-]{3,60}[a-z0-9-]$")
23		.unwrap_or_else(|e| unreachable!("ID_TAG_RE regex compilation failed: {}", e))
24});
25
26/// Validate idTag format. Returns `true` if `id_tag` is a syntactically valid
27/// Cloudillo identity tag (see [`ID_TAG_RE`]).
28pub fn validate_id_tag(id_tag: &str) -> bool {
29	ID_TAG_RE.is_match(id_tag)
30}
31
32#[cfg(test)]
33mod tests {
34	use super::*;
35
36	#[test]
37	fn test_validate_id_tag() {
38		assert!(validate_id_tag("alice"));
39		assert!(validate_id_tag("bob-123"));
40		assert!(validate_id_tag("user-name-123"));
41		assert!(validate_id_tag("home.w9.hu"));
42
43		assert!(!validate_id_tag("Al")); // too short
44		assert!(!validate_id_tag("Alice")); // uppercase
45		assert!(!validate_id_tag("alice_123")); // underscore not allowed
46	}
47
48	#[test]
49	fn test_validate_id_tag_rejects_url_injection() {
50		// Path / authority injection attempts must all be rejected so that
51		// `https://cl-o.{id_tag}/api...` cannot be redirected or smuggled.
52		assert!(!validate_id_tag("alice/../../etc"));
53		assert!(!validate_id_tag("alice/admin"));
54		assert!(!validate_id_tag("alice@evil.com"));
55		assert!(!validate_id_tag("alice:8080"));
56		assert!(!validate_id_tag("alice evil"));
57		assert!(!validate_id_tag("alice?x=1"));
58		assert!(!validate_id_tag("alice#frag"));
59		assert!(!validate_id_tag("alice\\evil"));
60		assert!(!validate_id_tag("")); // empty
61	}
62}
63
64// vim: ts=4