Skip to main content

rpsl/
spec.rs

1//! Specifications and validation rules for RPSL objects.
2//!
3//! Parsing is intentionally lenient and produces objects typed with [`Raw`], which performs no
4//! validation. This module lets callers opt into stricter checks after parsing by validating
5//! objects against a [`Specification`].
6//!
7//! ## Using a specification
8//!
9//! You can validate after parsing or convert an object into a typed specification:
10//!
11//! ```rust
12//! # use rpsl::{parse_object, Object, spec::Rfc2622};
13//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
14//! let obj = parse_object("role: ACME Company\nsource: RIPE\n\n")?;
15//! let typed: Object<Rfc2622> = obj.into_spec()?;
16//! # Ok(())
17//! # }
18//! ```
19//!
20//! ## Validation vs conversion
21//!
22//! - Use [`Object::validate`](crate::Object::validate) to keep an object as-is while checking it
23//!   against a specification. This collects all attribute failures so you can report everything
24//!   that is invalid at once.
25//! - Use [`Object::into_spec`](crate::Object::into_spec) to convert into a typed object. This
26//!   validates as it converts and returns the first [`AttributeError`] encountered.
27//!
28//! ## Errors
29//!
30//! The primary error type is [`AttributeError`], which wraps [`InvalidNameError`] and
31//! [`InvalidValueError`]. These errors explain why a single attribute failed validation.
32//!
33//! Implement [`Specification`] to define custom rules by validating each attribute.
34
35use super::attribute::{Attribute, Name, Value};
36use std::fmt::Debug;
37
38/// Defines how parsed attributes should be validated for a given specification.
39pub trait Specification: Debug + Clone + Copy {
40    /// Validate a single attribute according to the specification.
41    ///
42    /// # Errors
43    /// Returns an [`AttributeError`] when the attribute name or value does not satisfy
44    /// the specification's rules.
45    fn validate_attribute(attribute: &Attribute<'_, Self>) -> Result<(), AttributeError>;
46}
47
48/// Default specification after parsing, does not perform any validation.
49#[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/// Validation rules matching RFC 2622.
59#[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)]
149/// An invalid attribute was encountered during validation.
150pub enum AttributeError {
151    /// The name of the attribute is invalid.
152    #[error("invalid attribute name {0}")]
153    InvalidName(#[from] InvalidNameError),
154    /// The value of the attribute is invalid.
155    #[error("invalid attribute value {0}")]
156    InvalidValue(#[from] InvalidValueError),
157}
158
159/// The attribute has an invalid name.
160#[derive(thiserror::Error, Debug, PartialEq)]
161#[error("`{name}`: {message}")]
162pub struct InvalidNameError {
163    /// The invalid attribute name.
164    pub name: Name<'static>,
165    /// Context about why the name is invalid.
166    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/// The attribute has an invalid value.
179#[derive(thiserror::Error, Debug, PartialEq)]
180#[error("`{value:?}`: {message}")]
181pub struct InvalidValueError {
182    /// The invalid attribute value.
183    pub value: Value<'static>,
184    /// Context about why the value is invalid.
185    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}