1use regex::Regex;
2use std::collections::BTreeMap;
3use std::fmt::Write;
4use std::sync::LazyLock;
5use subtle::ConstantTimeEq;
6
7static SOCKET_ID_PATTERN: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\d+\.\d+$").unwrap());
9
10static USER_ID_PATTERN: LazyLock<Regex> =
11 LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9_\-=@,.;]+$").unwrap());
12
13pub fn to_ordered_array(map: &BTreeMap<String, String>) -> Vec<String> {
15 map.iter()
16 .map(|(key, value)| {
17 let mut result = String::with_capacity(key.len() + value.len() + 1);
18 write!(&mut result, "{}={}", key, value).unwrap();
19 result
20 })
21 .collect()
22}
23
24pub fn get_md5(body: &str) -> String {
27 let digest = md5::compute(body.as_bytes());
28 hex::encode(digest.as_ref())
29}
30
31pub fn secure_compare(a: &str, b: &str) -> bool {
33 if a.len() != b.len() {
34 return false;
35 }
36
37 let a_bytes = a.as_bytes();
38 let b_bytes = b.as_bytes();
39
40 a_bytes.ct_eq(b_bytes).into()
42}
43
44pub fn is_encrypted_channel(channel: &str) -> bool {
46 channel.starts_with("private-encrypted-")
47}
48
49pub fn validate_channel(channel: &str) -> crate::Result<()> {
51 use crate::channel::Channel;
52 Channel::from_string(channel)?;
53 Ok(())
54}
55
56pub fn validate_presence_channel(channel: &str) -> crate::Result<()> {
58 validate_channel(channel)?;
59 if !channel.starts_with("presence-") {
60 return Err(crate::SockudoError::Validation {
61 message: format!(
62 "Presence history is only available for presence channels: '{}'",
63 channel
64 ),
65 });
66 }
67 Ok(())
68}
69
70pub fn validate_socket_id(socket_id: &str) -> crate::Result<()> {
72 if !SOCKET_ID_PATTERN.is_match(socket_id) {
73 return Err(crate::SockudoError::Validation {
74 message: format!(
75 "Invalid socket id: '{}'. Must be in format: \\d+.\\d+",
76 socket_id
77 ),
78 });
79 }
80 Ok(())
81}
82
83pub fn validate_user_id(user_id: &str) -> crate::Result<()> {
85 if user_id.is_empty() {
86 return Err(crate::SockudoError::Validation {
87 message: "User ID cannot be empty".to_string(),
88 });
89 }
90
91 if user_id.len() > 200 {
92 return Err(crate::SockudoError::Validation {
93 message: format!("User ID too long: '{}' (max 200 characters)", user_id),
94 });
95 }
96
97 if !USER_ID_PATTERN.is_match(user_id) {
98 return Err(crate::SockudoError::Validation {
99 message: format!(
100 "Invalid user ID: '{}'. Must match pattern: [a-zA-Z0-9_\\-=@,.;]+",
101 user_id
102 ),
103 });
104 }
105
106 Ok(())
107}
108
109pub fn join_strings<'a, I>(items: I, separator: &str) -> String
111where
112 I: IntoIterator<Item = &'a str>,
113 I::IntoIter: ExactSizeIterator,
114{
115 let iter = items.into_iter();
116 let (lower_bound, _) = iter.size_hint();
117
118 let mut result = String::with_capacity(lower_bound * 20); for (i, item) in iter.enumerate() {
122 if i > 0 {
123 result.push_str(separator);
124 }
125 result.push_str(item);
126 }
127
128 result
129}
130
131pub fn current_timestamp() -> String {
133 std::time::SystemTime::now()
134 .duration_since(std::time::UNIX_EPOCH)
135 .unwrap()
136 .as_secs()
137 .to_string()
138}
139
140pub fn format_duration(duration: std::time::Duration) -> String {
142 let secs = duration.as_secs();
143 if secs < 60 {
144 format!("{}s", secs)
145 } else if secs < 3600 {
146 format!("{}m {}s", secs / 60, secs % 60)
147 } else {
148 format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn test_secure_compare() {
158 assert!(secure_compare("hello", "hello"));
159 assert!(!secure_compare("hello", "world"));
160 assert!(!secure_compare("hello", "hello!"));
161 assert!(!secure_compare("hello", "hell"));
162 }
163
164 #[test]
165 fn test_is_encrypted_channel() {
166 assert!(is_encrypted_channel("private-encrypted-test"));
167 assert!(!is_encrypted_channel("private-test"));
168 assert!(!is_encrypted_channel("public-test"));
169 }
170
171 #[test]
172 fn test_validate_socket_id() {
173 assert!(validate_socket_id("123.456").is_ok());
174 assert!(validate_socket_id("0.0").is_ok());
175 assert!(validate_socket_id("123").is_err());
176 assert!(validate_socket_id("123.456.789").is_err());
177 assert!(validate_socket_id("abc.def").is_err());
178 }
179
180 #[test]
181 fn test_validate_user_id() {
182 assert!(validate_user_id("user123").is_ok());
183 assert!(validate_user_id("user-123_test@example.com").is_ok());
184 assert!(validate_user_id("").is_err());
185 assert!(validate_user_id(&"a".repeat(201)).is_err());
186 assert!(validate_user_id("user with spaces").is_err());
187 }
188
189 #[test]
190 fn test_join_strings() {
191 let items = ["a", "b", "c"];
192 assert_eq!(join_strings(items.iter().copied(), ","), "a,b,c");
193 assert_eq!(join_strings(["single"].iter().copied(), ","), "single");
194 assert_eq!(join_strings(Vec::<&str>::new().iter().copied(), ","), "");
195 }
196
197 #[test]
198 fn test_format_duration() {
199 use std::time::Duration;
200
201 assert_eq!(format_duration(Duration::from_secs(30)), "30s");
202 assert_eq!(format_duration(Duration::from_secs(90)), "1m 30s");
203 assert_eq!(format_duration(Duration::from_secs(3661)), "1h 1m");
204 }
205
206 #[test]
207 fn test_to_ordered_array() {
208 let mut map = BTreeMap::new();
209 map.insert("key1".to_string(), "value1".to_string());
210 map.insert("key2".to_string(), "value2".to_string());
211
212 let result = to_ordered_array(&map);
213 assert_eq!(result, vec!["key1=value1", "key2=value2"]);
214 }
215
216 #[test]
217 fn test_get_md5() {
218 let hash = get_md5("hello");
219 assert_eq!(hash, "5d41402abc4b2a76b9719d911017c592");
220 }
221}