1use crate::{data::ObjectType, emit_error};
4use iri_string::{convert::MappedToUri, format::ToDedicatedString, types::IriStr};
5use std::{any::type_name, borrow::Cow};
6use thiserror::Error;
7use tracing::error;
8use url::Url;
9
10pub trait Validate: ToString {
16 fn validate(&self) -> Vec<ValidationError>;
19
20 fn is_valid(&self) -> bool {
26 let result = self.validate();
27 if result.is_empty() {
28 true
29 } else {
30 error!("[VALIDATION] {:?}", result);
31 false
32 }
33 }
34
35 fn check_validity(&self) -> Result<(), ValidationError> {
38 if self.is_valid() {
39 Ok(())
40 } else {
41 Err(ValidationError::ConstraintViolation(
42 format!("Instance of '{}' is invalid", type_name::<Self>()).into(),
43 ))
44 }
45 }
46}
47
48#[derive(Debug, Error)]
50pub enum ValidationError {
51 #[doc(hidden)]
52 #[error("Empty string: '{0}'")]
53 Empty(Cow<'static, str>),
54
55 #[doc(hidden)]
56 #[error("Invalid IRI: '{0}'")]
57 InvalidIRI(Cow<'static, str>),
58
59 #[doc(hidden)]
60 #[error("Invalid URI: '{0}'")]
61 InvalidURI(Cow<'static, str>),
62
63 #[doc(hidden)]
64 #[error("Invalid IRL: <{0}>")]
65 InvalidIRL(Cow<'static, str>),
66
67 #[doc(hidden)]
68 #[error("Invalid URL: <{0}>")]
69 InvalidURL(url::ParseError),
70
71 #[doc(hidden)]
72 #[error("Not a Normalized IRI: \"{0}\"")]
73 NotNormalizedIRI(Cow<'static, str>),
74
75 #[doc(hidden)]
76 #[error("Not UTC timezone: \"{0}\"")]
77 NotUTC(Cow<'static, str>),
78
79 #[doc(hidden)]
80 #[error("Wrong 'objectType'. Expected {expected} but found {found}")]
81 WrongObjectType {
82 expected: ObjectType,
83 found: Cow<'static, str>,
84 },
85
86 #[doc(hidden)]
87 #[error("SHA-1 sum string contains non hex characters or has wrong characters count")]
88 InvalidSha1String,
89
90 #[doc(hidden)]
91 #[error("SHA-2 hash string contains non hex characters or has wrong characters count")]
92 InvalidSha2String,
93
94 #[doc(hidden)]
95 #[error("Empty anonymous group")]
96 EmptyAnonymousGroup,
97
98 #[doc(hidden)]
99 #[error("Invalid timestamp: {0}")]
100 InvalidDateTime(
101 #[doc(hidden)]
102 #[from]
103 chrono::format::ParseError,
104 ),
105
106 #[doc(hidden)]
107 #[error("Invalid ISO-8601 duration: {0}")]
108 DurationParseError(speedate::ParseError),
109
110 #[doc(hidden)]
111 #[error("Invalid Language Tag: {0}")]
112 InvalidLanguageTag(Cow<'static, str>),
113
114 #[doc(hidden)]
115 #[error("{0} must have at least one IFI")]
116 MissingIFI(Cow<'static, str>),
117
118 #[doc(hidden)]
119 #[error("Missing '{0}'")]
120 MissingField(Cow<'static, str>),
121
122 #[doc(hidden)]
123 #[error("Invalid '{0}'")]
124 InvalidField(Cow<'static, str>),
125
126 #[doc(hidden)]
127 #[error("General constraint violation: {0}")]
128 ConstraintViolation(Cow<'static, str>),
129}
130
131pub(crate) fn validate_irl(val: &IriStr) -> Result<(), ValidationError> {
136 if val.is_empty() {
137 emit_error!(ValidationError::InvalidIRL(val.to_string().into()))
138 }
139
140 let uri = MappedToUri::from(val).to_dedicated_string();
141 let normalized_uri = uri.normalize().to_dedicated_string();
142 let s = normalized_uri.as_str();
143 match Url::parse(s) {
144 Ok(_) => Ok(()),
145 Err(x) => emit_error!(ValidationError::InvalidURL(x)),
146 }
147}
148
149pub(crate) fn validate_sha1sum(val: &str) -> Result<(), ValidationError> {
155 if val.chars().count() != 40 || !val.chars().all(|x| x.is_ascii_hexdigit()) {
156 emit_error!(ValidationError::InvalidSha1String)
157 } else {
158 Ok(())
159 }
160}
161
162pub(crate) fn validate_sha2(val: &str) -> Result<(), ValidationError> {
168 if !(32..65).contains(&val.chars().count()) || !val.chars().all(|x| x.is_ascii_hexdigit()) {
169 emit_error!(ValidationError::InvalidSha2String)
170 } else {
171 Ok(())
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use tracing::info;
179 use tracing_test::traced_test;
180 use url::Url;
181
182 #[test]
183 fn test_validate_irl() {
184 const PASS: &str = "http://résumé.example.org/foo/../";
185 let r1 = IriStr::new(PASS);
186 assert!(r1.is_ok());
187 assert!(validate_irl(r1.unwrap()).is_ok());
189
190 const FAIL: &str = "résumé/bar";
191 let r2 = IriStr::new(FAIL);
192 assert!(r2.is_err());
193 }
194
195 #[test]
196 fn test_validate_sha1sum() {
197 assert!(validate_sha1sum("ebd31e95054c018b10727ccffd2ef2ec3a016ee9").is_ok());
198
199 const H1: &str = "ebd31e95054c018b10727ccffd2ef2ec3a016ee9ab";
200 let r1 = validate_sha1sum(H1);
201 assert!(r1.is_err_and(|x| matches!(x, ValidationError::InvalidSha1String)));
202
203 const H2: &str = "ebd31x95054c018b10727ccffd2ef2ec3a016ee9";
204 let r2 = validate_sha1sum(H2);
205 assert!(r2.is_err_and(|x| matches!(x, ValidationError::InvalidSha1String)));
206 }
207
208 #[test]
209 fn test_validate_sha2() {
210 assert!(
211 validate_sha2("495395e777cd98da653df9615d09c0fd6bb2f8d4788394cd53c56a3bfdcd848a")
212 .is_ok()
213 );
214
215 const H1: &str = "1234567890123456789012345678901";
216 let r1 = validate_sha2(H1);
217 assert!(r1.is_err_and(|x| matches!(x, ValidationError::InvalidSha2String)));
218
219 const H2: &str = "x95395e777cd98da653df9615d09c0fd6bb2f8d4788394cd53c56a3bfdcd848a";
220 let r2 = validate_sha2(H2);
221 assert!(r2.is_err_and(|x| matches!(x, ValidationError::InvalidSha2String)));
222 }
223
224 #[traced_test]
225 #[test]
226 fn test_rfc3987_with_url_crate() {
227 const URIS: &[&str] = &[
230 "https://tools.ietf.org/html/rfc3987",
233 "https://datatracker.ietf.org/doc/html/rfc3987",
234 "http://xn--rsum-bpad.example.org",
236 "http://r%C3%A9sum%C3%A9.example.org",
237 "http://example.com/%F0%90%8C%80%F0%90%8C%81%F0%90%8C%82",
239 "http://www.example.org/r%C3%A9sum%C3%A9.html",
241 "http://www.example.org/r%E9sum%E9.html",
242 "http://www.example.org/D%C3%BCrst",
243 "http://www.example.org/D%FCrst",
244 "http://xn--99zt52a.example.org/%e2%80%ae",
245 "http://xn--99zt52a.example.org/%E2%80%AE",
246 "http://ab.CDEFGH.ij/kl/mn/op.html",
248 "http://ab.CDE.FGH/ij/kl/mn/op.html",
249 "http://AB.CD.ef/gh/IJ/KL.html",
250 "http://ab.cd.EF/GH/ij/kl.html",
251 "http://ab.CD.EF/GH/IJ/kl.html",
252 "http://ab.CDE123FGH.ij/kl/mn/op.html",
253 "http://ab.cd.ef/GH1/2IJ/KL.html",
254 "http://ab.cd.ef/GH%31/%32IJ/KL.html",
255 "http://ab.CDEFGH.123/kl/mn/op.html",
256 "eXAMPLE://a/./b/../b/%63/%7bfoo%7d/ros%C3%A9",
258 "HTTP://www.EXAMPLE.com/",
260 "http://www.example.com/",
261 "http://example.org/~user",
263 "http://example.org/%7euser",
264 "http://example.org/%7Euser",
265 "http://example.com",
267 "http://example.com/",
268 "http://example.com:/",
269 "http://example.com:80/",
270 "http://example.com/data",
272 "http://example.com/data/",
273 "http://www.example.org/red%09ros%C3%A9#red",
276 "http://AB.CD.EF/GH/IJ/KL?MN=OP;QR=ST#UV",
278 "http://r\u{E9}sum\u{E9}.example.org",
281 "http://example.com/\u{10300}\u{10301}\u{10302}",
283 "http://www.example.org/D\u{FC}rst",
284 "http://\u{7D0D}\u{8C46}.example.org/%E2%80%AE",
285 "http://example.org/ros\u{E9}",
287 "example://a/b/c/%7Bfoo%7D/ros\u{E9}",
289 "http://www.example.org/r\u{E9}sum\u{E9}.html",
291 "http://www.example.org/re\u{301}sume\u{301}.html",
292 "http://www.example.org/r%E9sum%E9.xml#r\u{E9}sum\u{E9}",
295 ];
296
297 for data in URIS {
298 let uri = Url::parse(data);
299 match uri {
300 Ok(_) => {}
301 Err(x) => {
302 error!("Failed <{}>: {}", data, x);
303 let iri = IriStr::new(data);
305 match iri {
306 Ok(_) => info!("...but passed iri_string!"),
307 Err(x) => error!("...and iri_string: {}", x),
308 }
309 }
310 }
311 }
312 }
313}