1use super::attribute::{Attribute, Name, Value};
36use std::fmt::Debug;
37
38pub trait Specification: Debug + Clone + Copy {
40 fn validate_attribute(attribute: &Attribute<'_, Self>) -> Result<(), AttributeError>;
46}
47
48#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
50pub struct Raw;
51
52impl Specification for Raw {
53 fn validate_attribute(_attribute: &Attribute<'_, Self>) -> Result<(), AttributeError> {
54 Ok(())
55 }
56}
57
58#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
60pub struct Rfc2622;
61
62impl Rfc2622 {
63 fn validate_name<Spec: Specification>(name: &Name<Spec>) -> Result<(), InvalidNameError> {
64 if name.len() < 2 {
65 return Err(InvalidNameError::new(
66 name,
67 "must be at least two characters long",
68 ));
69 }
70
71 if !name.is_ascii() {
72 return Err(InvalidNameError::new(
73 name,
74 "must contain only ASCII characters",
75 ));
76 }
77
78 let mut chars = name.chars();
79 let first = chars.next().expect("must have a first character");
80 let last = name.chars().last().expect("must have a last character");
81 if !first.is_ascii_alphabetic() {
82 return Err(InvalidNameError::new(
83 name,
84 "must start with an ASCII alphabetic character",
85 ));
86 }
87 if !last.is_ascii_alphanumeric() {
88 return Err(InvalidNameError::new(
89 name,
90 "must end with an ASCII alphanumeric character",
91 ));
92 }
93
94 if !name
95 .chars()
96 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
97 {
98 return Err(InvalidNameError::new(
99 name,
100 "may only contain ASCII letters, digits, '-' or '_'",
101 ));
102 }
103
104 Ok(())
105 }
106
107 fn validate_value<Spec: Specification>(value: &Value<Spec>) -> Result<(), InvalidValueError> {
108 let validator = |v: &str| {
109 if !v.is_ascii() {
110 return Err(InvalidValueError::new(
111 value,
112 "must contain only ASCII characters",
113 ));
114 }
115
116 if v.chars().any(|c| c.is_ascii_control()) {
117 return Err(InvalidValueError::new(
118 value,
119 "must not contain ASCII control characters",
120 ));
121 }
122
123 if v.starts_with(|c: char| c.is_ascii_whitespace()) {
124 return Err(InvalidValueError::new(
125 value,
126 "must not start with whitespace",
127 ));
128 }
129
130 Ok(())
131 };
132 for v in value.with_content() {
133 validator(v)?;
134 }
135
136 Ok(())
137 }
138}
139
140impl Specification for Rfc2622 {
141 fn validate_attribute(attribute: &Attribute<'_, Self>) -> Result<(), AttributeError> {
142 Self::validate_name(&attribute.name)?;
143 Self::validate_value(&attribute.value)?;
144 Ok(())
145 }
146}
147
148#[derive(thiserror::Error, Debug, PartialEq)]
149pub enum AttributeError {
151 #[error("invalid attribute name {0}")]
153 InvalidName(#[from] InvalidNameError),
154 #[error("invalid attribute value {0}")]
156 InvalidValue(#[from] InvalidValueError),
157}
158
159#[derive(thiserror::Error, Debug, PartialEq)]
161#[error("`{name}`: {message}")]
162pub struct InvalidNameError {
163 pub name: Name<'static>,
165 pub message: String,
167}
168
169impl InvalidNameError {
170 pub(crate) fn new<Spec: Specification>(name: &Name<Spec>, message: impl Into<String>) -> Self {
171 Self {
172 name: name.clone().into_owned().into_raw(),
173 message: message.into(),
174 }
175 }
176}
177
178#[derive(thiserror::Error, Debug, PartialEq)]
180#[error("`{value:?}`: {message}")]
181pub struct InvalidValueError {
182 pub value: Value<'static>,
184 pub message: String,
186}
187
188impl InvalidValueError {
189 fn new<Spec: Specification>(value: &Value<Spec>, message: impl Into<String>) -> Self {
190 Self {
191 value: value.clone().into_owned().into_raw(),
192 message: message.into(),
193 }
194 }
195}
196
197#[cfg(test)]
198mod tests {
199
200 use proptest::prelude::*;
201 use rstest::*;
202
203 use crate::{spec::Rfc2622, Attribute, Name, Value};
204
205 #[rstest]
206 #[case("aut-num", "AS3257")]
207 #[case("ASNumber", "32934")]
208 #[case("phone", "+49 176 07071964")]
209 #[case("address", "* Equinix FR5, Kleyerstr, Frankfurt am Main")]
210 #[case("remarks", "Concerning abuse and spam ... mailto: abuse@asn.net")]
211 fn rfc2622_valid_attribute(#[case] name: &str, #[case] value: &str) {
212 let attribute = Attribute::new(name, value);
213 attribute.validate::<Rfc2622>().unwrap();
214 }
215
216 #[test]
217 fn rfc2622_attribute_name_single_letter_is_error() {
218 let n = Name::new("a");
219 assert!(Rfc2622::validate_name(&n).is_err());
220 }
221
222 proptest! {
223 #[test]
224 fn rfc2622_attribute_name_non_letter_first_char_is_error(char in r"[^A-Za-z]") {
225 let n = Name::new(format!("{char}remarks"));
226 assert!(Rfc2622::validate_name(&n).is_err());
227 }
228 }
229
230 proptest! {
231 #[test]
232 fn rfc2622_attribute_name_non_letter_or_digit_last_char_is_error(char in r"[^A-Za-z0-9]") {
233 let n = Name::new(format!("remarks{char}"));
234 assert!(Rfc2622::validate_name(&n).is_err());
235 }
236 }
237
238 proptest! {
239 #[test]
240 fn rfc2622_attribute_name_space_only_is_error(name in r"\s") {
241 let n = Name::new(name);
242 assert!(Rfc2622::validate_name(&n).is_err());
243 }
244 }
245
246 proptest! {
247 #[test]
248 fn rfc2622_attribute_name_non_ascii_is_err(name in r"[^[:ascii:]]") {
249 let n = Name::new(name);
250 assert!(Rfc2622::validate_name(&n).is_err());
251 }
252 }
253
254 proptest! {
255 #[test]
256 fn rfc2622_attribute_value_non_ascii_is_err(
257 value in r"[^[:ascii:]]"
258 .prop_filter("value should not be empty", |value| !value.trim().is_empty())
259 ) {
260 let v = Value::new_single(value);
261 assert!(Rfc2622::validate_value(&v).is_err());
262 }
263 }
264
265 proptest! {
266 #[test]
267 fn rfc2622_attribute_value_ascii_control_is_err(
268 value in r"[[:cntrl:]]"
269 .prop_filter("value should not be empty", |value| !value.trim().is_empty())
270 ) {
271 let v = Value::new_single(value);
272 assert!(Rfc2622::validate_value(&v).is_err());
273 }
274 }
275}