Skip to main content

wasm_dbms_api/dbms/validate/
web.rs

1use crate::prelude::{DbmsError, Validate, Value};
2
3/// A validator that checks if a string is a valid MIME type.
4///
5/// The value must have the form `type/subtype`, where both the type and the subtype
6/// follow the RFC 6838 section 4.2 `restricted-name` grammar. Parameters such as
7/// `; charset=utf-8` are not accepted.
8///
9/// # Example
10///
11/// ```rust
12/// use wasm_dbms_api::prelude::{MimeTypeValidator, Validate, Value};
13///
14/// let validator = MimeTypeValidator;
15/// let valid_mime = Value::Text(wasm_dbms_api::prelude::Text("text/plain".into()));
16/// assert!(validator.validate(&valid_mime).is_ok());
17/// let invalid_mime = Value::Text(wasm_dbms_api::prelude::Text("invalid-mime".into()));
18/// assert!(validator.validate(&invalid_mime).is_err());
19/// ```
20pub struct MimeTypeValidator;
21
22impl Validate for MimeTypeValidator {
23    fn validate(&self, value: &crate::prelude::Value) -> crate::prelude::DbmsResult<()> {
24        let Value::Text(text) = value else {
25            return Err(DbmsError::Validation("Value is not a Text".to_string()));
26        };
27
28        let s = &text.0;
29
30        // must have exactly '/' character
31        let parts: Vec<&str> = s.split('/').collect();
32        if parts.len() != 2 {
33            return Err(DbmsError::Validation(format!(
34                "MIME type '{s}' must contain exactly one '/'"
35            )));
36        }
37
38        if !is_restricted_name(parts[0]) {
39            return Err(DbmsError::Validation(format!(
40                "MIME type '{s}' has invalid type part"
41            )));
42        }
43        if !is_restricted_name(parts[1]) {
44            return Err(DbmsError::Validation(format!(
45                "MIME type '{s}' has invalid subtype part"
46            )));
47        }
48
49        Ok(())
50    }
51}
52
53/// Maximum length of a MIME type or subtype name, per RFC 6838 section 4.2.
54const MIME_RESTRICTED_NAME_MAX_LEN: usize = 127;
55
56/// Returns whether `name` matches the RFC 6838 section 4.2 `restricted-name` grammar.
57///
58/// The first character must be an ASCII letter or digit; the following ones may also be
59/// any of `!#$&-^_.+`. The whole name must be between 1 and 127 characters long.
60fn is_restricted_name(name: &str) -> bool {
61    let mut chars = name.chars();
62    name.len() <= MIME_RESTRICTED_NAME_MAX_LEN
63        && chars.next().is_some_and(|c| c.is_ascii_alphanumeric())
64        && chars.all(|c| c.is_ascii_alphanumeric() || "!#$&-^_.+".contains(c))
65}
66
67/// A validator that checks if a string is a valid URL.
68///
69/// # Example
70///
71/// ```rust
72/// use wasm_dbms_api::prelude::{UrlValidator, Validate, Value};
73/// let validator = UrlValidator;
74/// let valid_url = Value::Text(wasm_dbms_api::prelude::Text("http://example.com".into()));
75/// assert!(validator.validate(&valid_url).is_ok());
76/// ```
77pub struct UrlValidator;
78
79impl Validate for UrlValidator {
80    fn validate(&self, value: &crate::prelude::Value) -> crate::prelude::DbmsResult<()> {
81        let Value::Text(text) = value else {
82            return Err(DbmsError::Validation("Value is not a Text".to_string()));
83        };
84
85        let s = &text.0;
86
87        if url::Url::parse(s).is_err() {
88            return Err(DbmsError::Validation(format!(
89                "Value '{s}' is not a valid URL"
90            )));
91        }
92
93        Ok(())
94    }
95}
96
97#[cfg(test)]
98mod tests {
99
100    use super::*;
101    use crate::prelude::Text;
102
103    #[test]
104    fn test_should_not_validate_mime_if_not_text() {
105        let value = Value::Uint32(crate::prelude::Uint32(42));
106        let result = MimeTypeValidator.validate(&value);
107        assert!(result.is_err());
108    }
109
110    #[test]
111    fn test_mime_type_validator() {
112        let valid_mime_types = vec![
113            "text/plain",
114            "image/jpeg",
115            "application/json",
116            "application/vnd.api+json",
117            "audio/mpeg",
118        ];
119        for mime in valid_mime_types {
120            let value = Value::Text(Text(mime.to_string()));
121            assert!(
122                MimeTypeValidator.validate(&value).is_ok(),
123                "MIME type '{mime}' should be valid"
124            );
125        }
126    }
127
128    #[test]
129    fn test_invalid_mime_type_validator() {
130        let invalid_mime_types = vec![
131            "textplain",
132            "image//jpeg",
133            "/json",
134            "application/vnd.api+json/extra",
135            "audio/mpeg/",
136            "audio/mpe g",
137        ];
138        for mime in invalid_mime_types {
139            let value = Value::Text(Text(mime.to_string()));
140            assert!(
141                MimeTypeValidator.validate(&value).is_err(),
142                "MIME type '{mime}' should be invalid"
143            );
144        }
145    }
146
147    #[test]
148    fn test_mime_type_validator_accepts_rfc6838_restricted_name_chars() {
149        let max_name = "a".repeat(127);
150        let max_len_mime = format!("{max_name}/{max_name}");
151        let valid_mime_types = [
152            "application/vnd.example_test",
153            "application/e!example",
154            "application/x#y$z&w^v",
155            "1type/2sub",
156            "application/vnd.example+json",
157            "Text/Plain",
158            max_len_mime.as_str(),
159        ];
160        for mime in valid_mime_types {
161            let value = Value::Text(Text(mime.to_string()));
162            assert!(
163                MimeTypeValidator.validate(&value).is_ok(),
164                "MIME type '{mime}' should be valid"
165            );
166        }
167    }
168
169    #[test]
170    fn test_mime_type_validator_rejects_invalid_restricted_names() {
171        let long_name = "a".repeat(128);
172        let long_type = format!("{long_name}/plain");
173        let long_subtype = format!("text/{long_name}");
174        let invalid_mime_types = [
175            "-/plain",
176            "+/plain",
177            "./plain",
178            "_/plain",
179            "text/-plain",
180            "text/.plain",
181            "text/+json",
182            "text/!plain",
183            "text/",
184            "text /plain",
185            "text/pl ain",
186            "text/plain; charset=utf-8",
187            "text/pl=ain",
188            "text/pl*ain",
189            "text/plĂ in",
190            long_type.as_str(),
191            long_subtype.as_str(),
192        ];
193        for mime in invalid_mime_types {
194            let value = Value::Text(Text(mime.to_string()));
195            assert!(
196                MimeTypeValidator.validate(&value).is_err(),
197                "MIME type '{mime}' should be invalid"
198            );
199        }
200    }
201
202    #[test]
203    fn test_url_validator() {
204        let valid_urls = vec![
205            "http://example.com",
206            "https://example.com/path?query=param#fragment",
207            "ftp://ftp.example.com/resource",
208            "mailto:christian@example.com",
209        ];
210        for url in valid_urls {
211            let value = Value::Text(Text(url.to_string()));
212            assert!(
213                UrlValidator.validate(&value).is_ok(),
214                "URL '{url}' should be valid"
215            );
216        }
217    }
218
219    #[test]
220    fn test_invalid_url_validator() {
221        let invalid_urls = vec![
222            //"htp:/example.com",
223            "://missing.scheme.com",
224            "http//missing.colon.com",
225            "justastring",
226            "http://in valid.com",
227        ];
228        for url in invalid_urls {
229            let value = Value::Text(Text(url.to_string()));
230            assert!(
231                UrlValidator.validate(&value).is_err(),
232                "URL '{url}' should be invalid"
233            );
234        }
235    }
236
237    #[test]
238    fn test_should_not_validate_url_if_not_text() {
239        let value = Value::Uint32(crate::prelude::Uint32(42));
240        let result = UrlValidator.validate(&value);
241        assert!(result.is_err());
242    }
243}